Background (ADR-0021 D-C)
The analytics dataset semantic layer (PR #1643) hardens the cross-object analytics SQL path so the tenant/RLS read scope is injected per joined object. The mechanism is in place and unit/integration-tested:
StrategyContext.getReadScope (context-aware, returns a FilterCondition)
NativeSQLStrategy injects the scope on the base AND every joined table; rejects undeclared joins
read-scope-sql fail-closed FilterCondition→SQL compiler
AnalyticsServicePlugin auto-bridges getReadScope to a security service exposing getReadFilter(object, context)
What is missing: plugin-security does not yet expose that getReadFilter service method, so in production the auto-bridge finds nothing and the raw-SQL analytics path runs unscoped for cross-object dataset queries. Single-org and single-object paths are unaffected (single-object goes through the RLS middleware; single-org has no cross-tenant surface).
Task
Extract the existing middleware logic (permission-set resolution → collectRLSPolicies → field-existence filtering → RLSCompiler.compileFilter) into a reusable getReadFilter(object, operation, context) => FilterCondition | null, register it as the security service so AnalyticsServicePlugin auto-bridges to it. The live middleware behaviour must not change — factor out a shared helper that both the middleware and the new method call (single source of truth, no divergence).
Why it needs review
Touches the security middleware hot path (highest-impact risk R1: a mistake leaks cross-tenant rows). Requires: unit tests that the extracted method matches middleware output; a multi-tenant end-to-end test with a real sqlite driver asserting a cross-object dataset query returns no other-tenant rows.
Scope
packages/plugins/plugin-security — expose getReadFilter + register security service
- Multi-tenant e2e test (driver-sql)
- No change to
service-analytics (the bridge seam is already in place)
Follow-up to #1643. See ADR-0021 §D-C.
Background (ADR-0021 D-C)
The analytics
datasetsemantic layer (PR #1643) hardens the cross-object analytics SQL path so the tenant/RLS read scope is injected per joined object. The mechanism is in place and unit/integration-tested:StrategyContext.getReadScope(context-aware, returns a FilterCondition)NativeSQLStrategyinjects the scope on the base AND every joined table; rejects undeclared joinsread-scope-sqlfail-closed FilterCondition→SQL compilerAnalyticsServicePluginauto-bridgesgetReadScopeto asecurityservice exposinggetReadFilter(object, context)What is missing: plugin-security does not yet expose that
getReadFilterservice method, so in production the auto-bridge finds nothing and the raw-SQL analytics path runs unscoped for cross-object dataset queries. Single-org and single-object paths are unaffected (single-object goes through the RLS middleware; single-org has no cross-tenant surface).Task
Extract the existing middleware logic (permission-set resolution →
collectRLSPolicies→ field-existence filtering →RLSCompiler.compileFilter) into a reusablegetReadFilter(object, operation, context) => FilterCondition | null, register it as thesecurityservice soAnalyticsServicePluginauto-bridges to it. The live middleware behaviour must not change — factor out a shared helper that both the middleware and the new method call (single source of truth, no divergence).Why it needs review
Touches the security middleware hot path (highest-impact risk R1: a mistake leaks cross-tenant rows). Requires: unit tests that the extracted method matches middleware output; a multi-tenant end-to-end test with a real sqlite driver asserting a cross-object dataset query returns no other-tenant rows.
Scope
packages/plugins/plugin-security— exposegetReadFilter+ registersecurityserviceservice-analytics(the bridge seam is already in place)Follow-up to #1643. See ADR-0021 §D-C.