Summary
aws s3 ls s3://{account}/ returns only public products, even when the caller owns the account. Unlisted and restricted products are invisible to their own owner.
Cause
SourceCoopRegistry::list_products passes None as the subject, so the product-list call is the only Source API lookup that is not subject-scoped — it goes out with no Authorization header:
https://github.com/source-cooperative/data.source.coop/blob/main/src/source_api/registry.rs#L29-L38
pub async fn list_products(&self, account: &str) -> Result<Vec<String>, ProxyError> {
let product_list = super::cache::get_or_fetch_product_list(
&self.api_base_url,
account,
&self.api_auth,
&self.request_id,
None, // <-- subject
)
With subject: None, cached_fetch skips the Authorization header and cache_key_with_subject returns the bare URL, so all callers share one cache entry.
Downstream, GET /api/v1/products/{account_id} filters on Actions.ListRepository, which returns true for an anonymous caller only when visibility === "public". Hence the owner sees the anonymous view of their own account.
Structural obstacle
AccountListHandler is a RouteHandler, and route handlers run before identity resolution in the multistore pipeline — so there is no resolved subject available at the point list_products is called. Fixing this likely means either resolving identity earlier for this route, or moving account listing off the route-handler path.
Impact
- Fails safe — over-restrictive, no data leak.
- But listing is a primary discovery path, and an owner cannot see their own non-public products through the S3 API.
- The shared, unsubjected cache entry is also inconsistent with the subject-scoped keying used by every other lookup.
Notes
Found while auditing the ADRs in #115 against the implementation. ADR-005 and ADR-007 currently claim all lookups are subject-scoped; that text is being corrected separately.
Summary
aws s3 ls s3://{account}/returns only public products, even when the caller owns the account. Unlisted and restricted products are invisible to their own owner.Cause
SourceCoopRegistry::list_productspassesNoneas the subject, so the product-list call is the only Source API lookup that is not subject-scoped — it goes out with noAuthorizationheader:https://github.com/source-cooperative/data.source.coop/blob/main/src/source_api/registry.rs#L29-L38
With
subject: None,cached_fetchskips theAuthorizationheader andcache_key_with_subjectreturns the bare URL, so all callers share one cache entry.Downstream,
GET /api/v1/products/{account_id}filters onActions.ListRepository, which returnstruefor an anonymous caller only whenvisibility === "public". Hence the owner sees the anonymous view of their own account.Structural obstacle
AccountListHandleris aRouteHandler, and route handlers run before identity resolution in the multistore pipeline — so there is no resolved subject available at the pointlist_productsis called. Fixing this likely means either resolving identity earlier for this route, or moving account listing off the route-handler path.Impact
Notes
Found while auditing the ADRs in #115 against the implementation. ADR-005 and ADR-007 currently claim all lookups are subject-scoped; that text is being corrected separately.