Skip to content

feat(api): enumerate authorized exports via loopback collection GET - #443

Closed
seonghobae wants to merge 1 commit into
feat/export-retrieval-get-gap-003afrom
feat/export-collection-get-gap-003a
Closed

feat(api): enumerate authorized exports via loopback collection GET#443
seonghobae wants to merge 1 commit into
feat/export-retrieval-get-gap-003afrom
feat/export-collection-get-gap-003a

Conversation

@seonghobae

@seonghobae seonghobae commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Folded into #444

Closed as superseded_by_fold, not discarded. #444's head contains this PR as its direct ancestor and has been retargeted to this PR's former base, so the export collection GET implementation/tests and this review history remain intact while queue WIP is reduced.

Canonical landing vehicle: #444 (feat(api): consolidate export collection GET and CLI).

Do not reopen unless the folded head demonstrably loses unique behavior or evidence.

GAP-003A unique slice stacked on export retrieval GET: loopback
GET /v1/exports lists metric-free purpose-bound identities on
AnalysisRunLiveService / tepp-loopback. LineageWeave refused.
NaruonLiveService stays POST-only. ADR 0075.
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: eff71cc6-3919-422b-84ae-d0ae5f9bbc66

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 7 potential issues.

Devin Review

Comment on lines +138 to +140
let end = (start + limit).min(items.len());
let next_cursor = (end < items.len()).then(|| items[end - 1].export_id.clone());
(items[start..end].to_vec(), next_cursor)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Invalid page limits panic callers

Calling page_export_collection_items with zero and remaining items indexes below zero. Large limits can also overflow, crashing direct library consumers.

Prompt for agents
Make page_export_collection_items safe for every public input. The exported helper currently accepts an arbitrary usize, although only the HTTP parser enforces 1..=64. Zero can underflow end - 1 and large values can overflow start + limit. Either return Result and validate the public limit or implement checked/saturating bounds while defining zero-limit cursor behavior. Add direct helper tests for zero, usize::MAX, empty input, and cursors at or beyond the end.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +45 to +62
pub fn new(items: Vec<ExportRetrieval>, next_cursor: Option<String>) -> Result<Self, ApiError> {
if items.len() > EXPORT_COLLECTION_MAX_LIMIT {
return Err(ApiError::LimitExceeded);
}
if let Some(cursor) = next_cursor.as_deref() {
require_nonempty(cursor)?;
if cursor.len() > EXPORT_COLLECTION_CURSOR_MAX_LEN {
return Err(ApiError::LimitExceeded);
}
if cursor.contains('/') || cursor.contains('\0') {
return Err(ApiError::InvalidWirePayload);
}
}
let collection = Self { items, next_cursor };
let payload = to_json(&collection)?;
require_byte_limit(&payload, DEFAULT_ANALYSIS_RUN_BYTE_LIMIT)?;
refuse_metrics_on_export_retrieval_payload(&payload)?;
Ok(collection)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Invalid receipts enter valid collections

ExportCollection::new accepts malformed public receipt values without item validation. Callers can publish denied decisions, empty identities, or unsupported versions as valid pages.

Prompt for agents
Validate every ExportRetrieval in ExportCollection::new and to_json before accepting or serializing the page. ExportRetrieval currently keeps validate private, so expose an appropriate crate-level validation method or reconstruct each item through its validated API. Also provide a validated collection deserialization entry point if collections are consumed from JSON. Test malformed decision codes, contract versions, empty fields, unknown purposes, and oversized identities.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +379 to +381
let (page, next_cursor) = page_export_collection_items(items, cursor.as_deref(), limit);
let collection = ExportCollection::new(page, next_cursor)?;
Ok(json_response(200, "OK", collection.to_json()?))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Valid maximum pages return errors

With 64 valid receipts containing heavily escaped identifiers, page_export_collection_items builds a response beyond 64 KiB. The accepted maximum limit then returns 413.

Prompt for agents
Build collection pages against both the requested item count and DEFAULT_ANALYSIS_RUN_BYTE_LIMIT. If the requested rows exceed the serialized response bound, return the largest nonempty prefix that fits and set next_cursor to its last export_id. Ensure a single valid receipt always fits, and add an endpoint test with 64 receipts whose artifact IDs and idempotency keys require maximal JSON escaping.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +369 to +373
let limit =
parse_export_collection_page_limit(headers.get("tepp-page-limit").map(String::as_str))?;
let cursor = parse_export_collection_page_cursor(
headers.get("tepp-page-cursor").map(String::as_str),
)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Unknown headers bypass collection contract

list_exports ignores arbitrary and misspelled pagination headers. ADR 0075 requires unknown collection keys to fail closed, so these requests need rejection.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +131 to +137
items.sort_by(|left, right| left.export_id.cmp(&right.export_id));
let start = cursor.map_or(0, |cursor| {
items
.iter()
.position(|item| item.export_id.as_str() > cursor)
.unwrap_or(items.len())
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Hash order does not affect pagination

page_export_collection_items sorts every receipt by its canonical export identity before applying the cursor. Unordered map iteration therefore cannot reorder pages.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +374 to +379
let items = self
.authorized_exports
.values()
.map(|stored| stored.retrieval.clone())
.collect();
let (page, next_cursor) = page_export_collection_items(items, cursor.as_deref(), limit);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Current service avoids page races

list_exports clones one current view while the single-request service holds exclusive access. Export insertion cannot interleave during page construction.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +362 to +380
let consumer = require_headers(headers, self.bound_addr, false)?;
if consumer != NARUON_CONSUMER_CODE {
return Err(ApiError::InvalidWirePayload);
}
if headers.contains_key("idempotency-key") {
return Err(ApiError::InvalidWirePayload);
}
let limit =
parse_export_collection_page_limit(headers.get("tepp-page-limit").map(String::as_str))?;
let cursor = parse_export_collection_page_cursor(
headers.get("tepp-page-cursor").map(String::as_str),
)?;
let items = self
.authorized_exports
.values()
.map(|stored| stored.retrieval.clone())
.collect();
let (page, next_cursor) = page_export_collection_items(items, cursor.as_deref(), limit);
let collection = ExportCollection::new(page, next_cursor)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟥 Export capabilities exposed without authorization

Any local process claiming naruon can enumerate all export capabilities. This bypasses per-export capability possession and exposes every artifact receipt across tenants.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@seonghobae seonghobae closed this Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant