feat(security): expose getReadFilter; analytics enforces real RLS (ADR-0021 D-C) [security review]#1657
Merged
Conversation
… RLS (ADR-0021 D-C, Task #9) Closes the last P0 gap: the analytics raw-SQL path (which bypasses the ObjectQL engine middleware) can now enforce the SAME tenant/RLS row scoping the engine applies on `find`, in production — not just when a test wires a provider. ## Security side (plugin-security) - Extract the middleware's per-request RLS resolution into two shared private methods used by BOTH the engine middleware and the new public method, so the find-path and the analytics path are provably in lock-step: - resolvePermissionSetsForContext() — roles + explicit + implicit/post-resolution baseline fallback. - computeRlsFilter() — collect applicable policies → field-existence safety (wildcard org policy on a column the object lacks → deny sentinel; tenancy opt-out → skip) → compile (+ deny sentinel). The middleware now calls these; behavior is byte-identical (59 existing tests green). - New `async getReadFilter(object, context)` composing the two. Returns: undefined (system / anonymous → no scope), a FilterCondition (AND into the object's scan), or the RLS_DENY_FILTER sentinel (policies applied but none compiled, OR resolution threw) — FAIL-CLOSED: a degraded permission subsystem denies (zero rows), never leaks cross-tenant rows. - Register the `security` service exposing getReadFilter (once metadata/ql/dbLoader handles are wired in start()), which the analytics plugin auto-bridges to. ## Analytics side (service-analytics) - The provider may now be async (getReadFilter can hit the DB). Since the strategy builds SQL synchronously, AnalyticsService pre-resolves the read scope for every object the query scans — base (`cube.sql`) + every `cube.joins[*].name`, a superset of what the strategy scopes — into a Map BEFORE the SQL builder runs; the strategy reads each object's filter synchronously from that map. The NativeSQLStrategy is untouched (StrategyContext.getReadScope stays sync → no spec contract change). - Fail-closed: if the provider throws for any object, the whole query is rejected (no unscoped SQL is ever emitted/executed). ## Tests - plugin-security: +8 getReadFilter tests (parity with find-path, fail-closed deny on missing column + on resolution throw, tenancy opt-out, system/anon bypass, service registration). 86 green. - service-analytics: +2 (async provider scopes base+joined by tenant; async reject → fail-closed, zero SQL emitted). 87 green. Existing sync-provider + deny-sentinel integration gates still pass unchanged. - Smoke: kernel boots clean with both plugins (Security + AnalyticsServicePlugin). Resolves the SecurityPlugin half of Task #9; the analytics auto-bridge seam was already in place from the P0 work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
findmiddleware (R1 hot path) and exposes it to the analytics raw-SQL path. Not auto-merged.Closes the last P0 gap of ADR-0021 D-C: the analytics raw-SQL path (which bypasses the ObjectQL engine middleware) can now enforce the same tenant/RLS row scoping the engine applies on
find, in production — not just when a test wires a provider. The analytics auto-bridge seam (getService('security').getReadFilter) was already in place from the earlier P0 work; this supplies the security side + the async plumbing.Security side (
plugin-security)resolvePermissionSetsForContext()— roles + explicit perms + implicit/post-resolution baseline fallback.computeRlsFilter()— collect applicable policies → field-existence safety (wildcardorganization_idpolicy on a column the object lacks → deny sentinel; explicit tenancy opt-out → skip) → compile (+ deny sentinel).The middleware now calls these; behavior is byte-identical (59 existing tests green).
async getReadFilter(object, context). Returnsundefined(system / anonymous → no scope), aFilterCondition(AND into the object's scan), or theRLS_DENY_FILTERsentinel (policies applied but none compiled, or resolution threw) — fail-closed: a degraded permission subsystem denies (zero rows), never leaks cross-tenant data.securityservice exposinggetReadFilter(only once the metadata/ql/dbLoader handles are wired instart()).Analytics side (
service-analytics)AnalyticsServicepre-resolves the scope for every object the query scans — base (cube.sql) + everycube.joins[*].name, a superset of what the strategy scopes — into aMapBEFORE the SQL builder runs; the strategy reads each object's filter synchronously from that map.NativeSQLStrategyis untouched (StrategyContext.getReadScopestays sync → no spec contract change).Review focus
computeRlsFilter/resolvePermissionSetsForContextare exact extractions of the middleware logic — confirm no behavioral drift (the suite asserts find-path outputs unchanged).cube.sql+ allcube.joins[*].name) ⊇ the objectsapplyReadScopescans.getReadFilterreturningundefinedfor single-tenant (org-scoping off) — intended (tenant_isolation is stripped withoutplugin-org-scoping).Tests
plugin-security: +8 (parity with find-path, fail-closed deny on missing column + resolution throw, tenancy opt-out, system/anon bypass, service registration) — 86 green.service-analytics: +2 (async provider scopes base+joined by tenant; async reject → fail-closed, zero SQL emitted) — 87 green. Existing sync-provider + deny-sentinel gates unchanged.🤖 Generated with Claude Code