Repository navigation
[Security Gap] Push analytics tenant isolation into repository queries #326
Description
Buyer/security outcome
Ensure tenant analytics never materializes another tenant's conversion jobs into the application process merely to filter them afterward. The storage boundary should enforce the tenant predicate before returning job objects.
Fresh protected-main evidence — 2026-08-10
Protected main is 83ec6f7fe2b04bdcd28bf98ec350e41e55730a18.
AnalyticsController.kpiSnapshot(...) authenticates analytics:read, but then executes:
KpiSnapshotResponse.from(repository.findAll().stream()
.filter(job -> job.belongsToTenant(tenantContext.tenantId()))
.toList());The current ConversionJobRepository exposes only global findAll() for list reads. This produces the correct response for the in-memory implementation, but a durable multi-tenant adapter would have to materialize cross-tenant job objects before the controller filters them. That unnecessarily widens the confidentiality boundary, increases tenant-size-dependent work, and contradicts the storage-level tenant-scoping direction already established for administrative lifecycle operations.
Active PR #268 introduces a fail-closed findAllByTenantId(String tenantId) repository contract for its administrative/lifecycle work, but its current AnalyticsController remains byte-for-byte unchanged and still calls findAll() plus an application-side filter. Therefore this issue is a successor acceptance gap, not a duplicate implementation of #268.
Required contract
After #270 integrates and #268 is reconciled onto the stable protected baseline:
AnalyticsController.kpiSnapshotmust call the tenant-scoped repository query directly;- no analytics endpoint may call global
findAll()and then filter tenant ownership in application code; - durable repository adapters must apply tenant predicates at the storage query boundary before returning
ConversionJobobjects; - the default scoped repository method remains fail-closed rather than falling back to a global lookup;
- the in-memory standalone adapter implements the same scoped semantic contract;
- cross-tenant analytics requests remain response-isolated and must not alter another tenant's KPI snapshot/audit state;
- tenant identifiers remain server-authoritative from verified claims, never query/body parameters.
Test-first acceptance
- Reconcile this work only after fix(security): require signed tenant claims on admin endpoints #268's scoped repository API is on the intended baseline; do not duplicate that interface on a competing branch now.
- Add a RED controller/repository interaction test proving the analytics path currently invokes global
findAll()rather than the scoped query. - Change only the analytics read path and any directly required in-memory adapter/test contract.
- Add a hostile repository seam whose global
findAll()fails the test if analytics touches it whilefindAllByTenantId()returns tenant-owned jobs. - Verify two tenants with overlapping status mixes receive only their own KPI counts and snapshot evidence.
- Preserve exact owned production coverage/Javadocs and rerun exact-head CI/security/SAST/fuzz/review gates.
Security basis
OWASP Foundation. (2023). API1:2023 Broken Object Level Authorization. OWASP API Security Top 10. https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/
OWASP recommends checking authorization on every record-access operation. Clearfolio's stronger repository contract should make the tenant predicate part of the data-access operation rather than relying on post-fetch filtering.
Dependency / non-duplication
- fix(security): harden audit pseudonymization and refresh Netty evidence #270 remains the protected security/privacy parent.
- fix(security): require signed tenant claims on admin endpoints #268 owns the scoped repository API and administrative/deletion lifecycle implementation.
- This issue owns analytics adoption of that scoped repository contract after fix(security): require signed tenant claims on admin endpoints #268 is reconciled.
- Do not modify fix(security): require signed tenant claims on admin endpoints #268 while its parent is still unintegrated merely to add this successor concern; preserve one implementation owner per lane.
Current implementation evidence — 2026-08-11
Protected main still does not ship storage-level tenant query isolation, so this issue remains open. The historical baseline/dependency narrative above is preserved as origin evidence; current live refs supersede it for execution decisions.
- PR fix(security): push analytics tenant isolation into repository query #342 is the current
ACTIVE_PRlist-query reconciliation on protected main. It introduces a fail-closedfindAllByTenantId(String tenantId)contract, implements the process-local scoped query, and changes analytics to consume that repository boundary instead of globalfindAll()plus application filtering. - Draft PR fix(security): fail closed on tenant-scoped job ID lookup #361 is stacked directly on fix(security): push analytics tenant isolation into repository query #342 for the remaining identifier-query boundary. It makes the default
findByTenantAndId(...)fail closed instead of invoking globalfindById(...), and gives the in-memory adapter a direct tenant-scoped lookup path. - fix(security): fail closed on tenant-scoped job ID lookup #361 was developed test-first: the focused test-only state proved both global-ID fallbacks were still exercised; the current Draft head has exact-head CI and fuzz GREEN, including zero missed owned production lines/branches and the repository Javadoc/test gates within CI.
- These PRs are
ACTIVE_PR/Draft evidence only, not shipped truth. fix(security): push analytics tenant isolation into repository query #342 must satisfy live governance and integrate first; fix(security): fail closed on tenant-scoped job ID lookup #361 must then be reconciled against the new protected base and regenerate base-sensitive evidence before it can leave Draft. - Durable database/storage adapters still need to apply both tenant predicates at their actual storage query boundary. This issue therefore remains the acceptance owner for storage-level tenant isolation until protected-main integration and durable-adapter evidence exist.
Live GitHub refs, checks, formal reviews, and protected-main identity remain authoritative; this issue text is traceability, not merge evidence.
Activity