Skip to content

feat(schema-compiler): require includes or excludes in accessPolicy memberLevel - #11934

Open
paveltiunov wants to merge 6 commits into
masterfrom
pavel-claude/nifty-archimedes-hbxga4
Open

paveltiunov wants to merge 6 commits into
masterfrom
pavel-claude/nifty-archimedes-hbxga4

Conversation

@paveltiunov

@paveltiunov paveltiunov commented Sep 18, 2026

Copy link
Copy Markdown
Member

Check List

  • Tests have been run in packages where changes have been made if available
  • Linter has been run for changed code
  • Tests for the changes have been added if not covered yet
  • Docs have been added / updated if required

Description of Changes Made

memberLevel.includes defaults to '*' (CubeEvaluator.prepareAccessPolicy, policy.memberLevel.includes || '*'), so memberLevel: {} silently expands to every measure, dimension and segment of the cube — the opposite of how an empty block reads.

It is also a silent no-op for memberMasking in the same policy when that policy has no restrictive rowLevel. applyRowLevelSecurity masks a member only when no granting policy gives it unconditional full access, and an empty memberLevel with no rowLevel gives exactly that. So this compiles clean today and returns the raw value for the member it was meant to mask:

accessPolicy: [{
  group: 'analyst',
  memberLevel: {},                               // reads as "nothing configured"
  memberMasking: { includes: ['ssn'] },          // never fires
}]

This PR requires memberLevel to spell out at least one of includes or excludes when it is present. The intent above is expressible as memberLevel: { excludes: ['ssn'] }, which masks correctly.

Scope of the rule — deliberately a presence rule, not a resolved-grant rule

It rejects a memberLevel block that configures nothing. It does not reject every spelling that resolves to a full grant: memberLevel: { includes: '*' } stays valid, because that is an explicit statement of intent rather than an accident. Pairing includes: '*' with memberMasking still yields the no-op, which the error message now warns about rather than recommending.

The adjacent shapes were checked and need no rule of their own: { excludes: [] } and { includes: [], excludes: [] } were already rejected before this PR (excludes carries .required() on its array item, so an empty array fails, unlike includes), and { excludes: '*' } grants nothing, so masking applies normally.

Behaviour

Policy Before After
memberLevel: {} + memberMasking: { includes: ['ssn'] } compiles, ssn returned unmasked rejected at compile time
memberLevel: { excludes: ['ssn'] } + same masking ssn masked unchanged
memberLevel: { includes: [...] | '*' }, excludes, or both valid unchanged
memberLevel: { includes: '*' } + rowLevel.filters + masking conditionally masked (CASE WHEN … ELSE mask) unchanged
no memberLevel key at all grants all members unchanged
YAML member_level: with an empty body (parses as null) already rejected (must be of type object) unchanged

The error message keeps Joi's {{#label}}, so a cube with several policies names the offending one. Without it, formatErrorMessage's dedupe (keyed on message text) collapsed two broken policies into a single line naming neither.

Breaking change

A data model with an empty member_level block in an access_policy no longer compiles. Use includes: '*' to keep granting all members, or excludes to grant all but some. Nothing in this repository relies on it — every member_level in fixtures, tests, birdbox models and docs already sets includes or excludes.

Open question for the reviewer: whether this warrants an errorReporter.warning release before the hard error. The policy's runtime behaviour is unchanged by this PR, so a warning round would cost nothing in the interim. Note it is not a severity flip in place — CubeValidator funnels every Joi result through a single errorReporter.error (CubeValidator.ts:1440), so a warning means moving the check into CubeEvaluator.prepareAccessPolicy and dropping the .or().

Testing

  • 10 cases in cube-validator.test.ts: the two rejections (bare {}, and {} paired with memberMasking), the five shapes that must stay valid, the omitted-memberLevel case, one pinning the error message's guidance so a reword can't reintroduce an inaccurate claim, and one pinning that several empty policies are each named rather than deduped away.
  • Full cubejs-schema-compiler unit suite green on the default (Tesseract) planner: 49/49 suites, 975/975 tests, 110/110 snapshots.
  • transpiledFieldsPatterns re-run, per the note in CubeValidator.ts about schema-shape changes.
  • oxlint clean on the changed files.
  • Verified end to end through a real compiler.compile() and through applyRowLevelSecurity + getSql, not only at the Joi layer — including the conditional-masking shape (includes: '*' + rowLevel.filters) to confirm the error message's wording holds there.

CI

The red driver checks (mssql, snowflake-export-bucket-azure{,-prefix}, bigquery-export-bucket-gcs) are not this PR's — see the CI notes in the comments. The pre-aggregation failures reproduce with an invariant count across four commits whose only deltas were docs prose and an error string, and point at 42bfe75 (#11629), which rewrote PreAggregationLoader.ts / PreAggregations.ts shortly before this branch. This PR touches nothing in packages/cubejs-query-orchestrator/.

Follow-ups, deliberately not in this PR

  • memberMasking has the identical includes || '*' default, so memberMasking: {} silently means "mask every member".
  • A dead-mask check for a member that is explicitly named in memberMasking.includes while also granted unconditionally by memberLevel. It has to be restricted to explicitly-named members: a blanket overlap rule would reject the documented member_level: { includes: [status, count] } + member_masking: { includes: "*" } pattern, where the wildcard necessarily overlaps the granted members and masking works correctly today.
  • There is no unit coverage of applyRowLevelSecurity's masking branch anywhere (CompilerApi.test.ts covers only dispose()), which is why this no-op could exist unnoticed. That belongs in cubejs-server-core/test/unit/.

🤖 Generated with Claude Code

https://claude.ai/code/session_017GxjcRUJBFdm1JBch2Fcr1

…emberLevel

`memberLevel.includes` defaults to '*', so `memberLevel: {}` silently grants
access to every member of the cube -- the opposite of how an empty block reads.

It is also a silent no-op for `memberMasking` in the same policy: members
granted in full by `memberLevel` are never masked, so a policy pairing an empty
`memberLevel` with `memberMasking` compiles without complaint and returns the
raw value for the member it was meant to mask.

Require `memberLevel` to spell out at least one of `includes` or `excludes`
when it is present. Policies that omit `memberLevel` entirely are unaffected
and still grant all members.

BREAKING CHANGE: a data model with an empty `member_level` block in an
`access_policy` no longer compiles. Use `includes: '*'` to keep granting all
members, or `excludes` to grant all but some.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017GxjcRUJBFdm1JBch2Fcr1
@paveltiunov
paveltiunov requested review from a team and keydunov as code owners September 18, 2026 21:28
@mintlify

mintlify Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
cubed3 🟢 Ready View Preview Sep 18, 2026, 9:47 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@github-actions github-actions Bot added docs Issues that require a documentation improvement javascript Pull requests that update Javascript code labels Sep 18, 2026
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @paveltiunov's task in 2m 53s —— View job


Sound change, incomplete against the bug it targets — 1 medium, 1 low (comment length), 1 top-level note. 2 inline comments posted.

Full review
  • Read changed files and surrounding code
  • Verify claimed behaviour (CubeEvaluator.prepareAccessPolicy, CompilerApi.applyRowLevelSecurity)
  • Check breaking-change surface (fixtures, YAML, docs)
  • Tests + lint
  • Review threads pass
  • Post review

Findings

Medium — the rule blocks one spelling of the footgun, and the error message recommends another (CubeValidator.ts:1124, posted inline)

memberLevel: { includes: '*' } + memberMasking: { includes: ['ssn'] } returns ssn unmasked, exactly as memberLevel: {} does. CompilerApi.ts:703-708 computes hasUnconditionalFullAccess from includesMembers + absence of rowLevel, so both spellings skip maskedMembersSet.add at 718. The new message — "use includes: '*' if that is intended" — steers a user straight into the equivalent silent no-op. The condition that actually characterises a dead mask is overlap between memberLevel grants and memberMasking members, resolvable in CubeEvaluator.ts:410-424 where both member sets already exist.

The shape rule still has independent value (memberLevel: {} reading as "nothing configured" while granting everything), so this is an argument for adding the overlap check, not for dropping this one.

Low — comment length (CubeValidator.ts:1118-1121, posted inline). 4 lines against the repo's 3-line limit, and the masking sentence overstates the rule.

Top-level note — breaking change, no deprecation window. A model with an empty member_level stops compiling on upgrade with a hard error. Nothing in the repo relies on it (checked: all member_level uses in birdbox-fixtures/rbac/, schema-compiler fixtures and both docs pages set includes or excludes), so the blast radius is user models only. Worth deciding deliberately whether this warrants errorReporter.warning for a release before the error — the policy still behaves as it did before, so a warning loses nothing in the meantime.

Verified, no issue

  • Ordering is safe: cubeValidator runs in preTranspileCubeCompilers (stage 1, DataSchemaCompiler.ts:514) and cubeEvaluator in stage 3, so the internal includesMembers/excludesMembers fields are never present when .or() is evaluated, and the policy objects are not re-validated after prepareAccessPolicy mutates them.
  • extends inheritance (CubeSymbols.ts:497-509) clones the parent's raw policies, so an inherited memberLevel is validated in the same pre-evaluation shape.
  • includes: [] stays valid and still means "no members" ([] || '*' keeps [] — an empty array is truthy).
  • Docs change is accurate and lands in the right section of the reference page; the sibling guide page has no empty-member_level example to correct.
  • Test coverage matches the change — both rejection paths plus the four valid shapes and the omitted-memberLevel case.

Not verified

Could not run the test suite — yarn jest in packages/cubejs-schema-compiler was denied by the sandbox in this environment. The PR's own run is reported green (49/49 suites, 973/973 tests).

Review threads

The unresolved-thread listing returned 0 of my own threads (no prior threads); 0 resolved, 0 re-affirmed.

· branch [`pavel-claude/nifty-archimedes-hbxga4`](https://github.com/cube-js/cube/tree/pavel-claude/nifty-archimedes-hbxga4)

Comment thread packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts Outdated
Comment thread packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts Outdated
`includes: '*'` grants every member exactly as an empty `memberLevel` does, so
recommending it as the blanket fix steers a policy that also defines
`memberMasking` straight into the same silent no-op: the member is granted
unconditionally, so the mask never fires.

Point at `excludes` for members the policy intends to mask, and say why. Same
caveat added to the reference docs. Tighten the schema comment, which
attributed the dead mask to emptiness when any unconditional grant causes it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017GxjcRUJBFdm1JBch2Fcr1
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @paveltiunov's task in 4m 20s —— View job


Review: request changes — 1 medium (inaccurate "never masked" claim in the error message and docs), 2 low

Full review
  • Read PR diff and the changed files in full
  • Traced memberLevel through CubeEvaluator.prepareAccessPolicy and CompilerApi.applyRowLevelSecurity
  • Checked birdbox RBAC fixtures for patterns the new rule/message would mis-advise
  • Handled prior review threads
  • Posted findings inline

Findings

1. "A member granted by memberLevel is never masked" is false for policies with row filters — medium

packages/cubejs-server-core/src/core/CompilerApi.ts:703-707 gates the no-mask path on unconditional full access: inFullAccess && (!policy.rowLevel || policy.rowLevel.allowAll). A member granted by memberLevel in a policy that carries rowLevel.filters still reaches the masking branch and renders CASE WHEN {rowFilter} THEN {value} ELSE {mask} END.

That is the shipped conditional-masking feature, exercised by packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/conditional_masking_test.yaml (member_level: {includes: "*"} + row_level.filters + a masking policy). The new Joi message at packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts:1123 and the new <Warning> at docs-mintlify/reference/data-modeling/data-access-policies.mdx:288-293 both state the blanket form, which tells users to move members out of member_level and off that pattern. Needs narrowing to "in a policy without row-level filters".

Raised as a reply on the existing open thread for the message itself; posted inline on the docs <Warning>.

2. New test pins the inaccurate wording — low

packages/cubejs-schema-compiler/test/unit/cube-validator.test.ts:1866-1868 asserts 'never masked' and 'belong in excludes'. Correcting the message breaks these two assertions, and the cheapest fix for whoever hits it is to restore the wrong wording. Inline.

3. .or('includes', 'excludes') is presence-only — low

memberLevel: { excludes: [] } passes and resolves, via CubeEvaluator.ts:399-407, to exactly the includes: '*', excludes: [] expansion that memberLevel: {} gets — so the paired memberMasking is still a silent no-op. The PR closes the literal {} spelling, not the class of empty grants. Inline; fine if the narrower scope is deliberate, but then the message and docs should say so.

Notes (no inline comment)

  • Breaking change. Models with an empty member_level block stop compiling on upgrade — a hard deploy failure, not a degraded query. Nothing in this repo relies on it (confirmed across fixtures, birdbox models, docs), but an external model would break. Worth confirming the release channel and whether a deprecation warning round is wanted before a hard error.
  • memberMasking: {} has the identical includes || '*' default (CubeEvaluator.ts:418), meaning "mask every member". The PR body already calls this out as deliberately out of scope — agreed on scope, worth a follow-up issue so it doesn't get lost.
  • The .or() constraint sits on MemberLevelPolicySchema, which also accepts the internal includesMembers/excludesMembers keys. Validation runs before prepareAccessPolicy populates them, so no re-validation hazard today — just noting it as a coupling if the validate/prepare order ever changes.
  • The two-line comment at CubeValidator.ts:1117-1118 reads well and earns its place: without it a later edit would plausibly delete the .or() as redundant.

Verification

  • Tests were not runyarn jest in packages/cubejs-schema-compiler needs approval in this environment, so the PR's claimed 973/973 green run is unverified here. Findings above are from reading the code paths, not from a failing run.
  • Prior review threads: the listing returned 2 of my own threads. 1 resolved (PRRT_kwDOCOH19M6j5oPW, the explanatory-comment wording — fix is in at the current site). 1 re-affirmed and left open (PRRT_kwDOCOH19M6j5oMj, the Joi message) — the human disputed my earlier proposed remedy with a valid counter-example and deferred the narrowed version to the author, so it carries a live item; finding 1 went in as a reply there rather than a duplicate thread.
• [branch `pavel-claude/nifty-archimedes-hbxga4`](https://github.com/cube-js/cube/tree/pavel-claude/nifty-archimedes-hbxga4)

Comment thread docs-mintlify/reference/data-modeling/data-access-policies.mdx Outdated
Comment thread packages/cubejs-schema-compiler/test/unit/cube-validator.test.ts Outdated
Comment thread packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts
…sked

"A member granted by memberLevel is never masked" is false when the granting
policy also carries rowLevel filters: `hasUnconditionalFullAccess` requires
`inFullAccess && (!rowLevel || rowLevel.allowAll)`, so a policy granting all
members with row filters still falls through to the masking branch and renders
`CASE WHEN {rowFilter} THEN {value} ELSE {mask} END` -- the conditional_masking
fixture shape.

As written the message told those users to move the member into `excludes`,
which revokes access instead of masking it. Say what is actually true: a
granted member is unmasked on every row the policy grants, so only a member
that must always be masked belongs in `excludes`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017GxjcRUJBFdm1JBch2Fcr1
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @paveltiunov's task in 4m 12s —— View job


Review complete — the rule is sound; 0 high, 0 medium, 2 low (deprecation path, concepts-page doc gap)

Full review
  • Gather context (diff, changed files, prior threads)
  • Review validator change
  • Review tests
  • Review docs
  • Resolve stale threads / dedupe findings
  • Post review

Verdict

The rule itself is correct and the semantics claim in the new message and docs checks out against the runtime. hasUnconditionalFullAccess in packages/cubejs-server-core/src/core/CompilerApi.ts:703-708 requires inFullAccess && (!policy.rowLevel || policy.rowLevel.allowAll), and the conditional branch at :719-731 builds memberMaskFiltersMap for granting policies that do have row filters — so "a member granted by memberLevel is unmasked on every row the policy grants" and the row_level parenthetical are both accurate, not approximations.

Checked and clean:

  • includes: [] still validates (.or tests key presence, not truthiness) and CubeEvaluator.ts:401 keeps [] because an empty array is truthy — the "grant nothing, mask via memberMasking" shape is unaffected. Test covers it.
  • .messages({'object.missing': ...}) is scoped to this object and this one key; MemberLevelPolicySchema has no other presence rule, so no other diagnostic is overwritten.
  • Both cubes and views go through the rule (accessPolicy lives in baseSchema, CubeValidator.ts:1209, inherited by cubeSchema and viewSchema) — no view-specific gap.
  • No Joi.func() field added, so transpiledFieldsPatterns genuinely doesn't move.
  • Every member_level in the repo (birdbox fixtures, schema-compiler fixtures, both docs pages) already sets includes or excludes — nothing in-tree breaks. The includes: [] example at docs-mintlify/docs/data-modeling/data-access-policies.mdx:45 still validates.

Findings

# Severity Where Concern
1 low packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts:1120 Previously-compiling models break at upgrade with no warning release; ErrorReporter.warning exists but CubeValidator only emits errors
2 low docs-mintlify/docs/data-modeling/data-access-policies.mdx:96 Concepts page enumerates the widening defaults without the new constraint — completeness gap, the sentence itself stays true

Both posted inline.

Not run

yarn jest and node are outside this run's allowed tools (This command requires approval), so the suite result is the PR body's claim, not something I reproduced. The review above is static plus cross-reading of the runtime in CompilerApi.ts.

Review threads

4 of my own unresolved threads returned; 3 resolved (2 fixed by be69d1d/f44a167 — the error-message wording and the reference-docs "never masked" clause; 1 withdrawn, my excludes: [] counter-example was wrong since excludes items carry .required(), replied on the thread before resolving). 1 re-affirmed and still open: the test-file comment length plus the verbatim-prose assertions at test/unit/cube-validator.test.ts — I hit the same finding this round and folded it into the existing thread rather than opening a second.

· branch [`pavel-claude/nifty-archimedes-hbxga4`](https://github.com/cube-js/cube/tree/pavel-claude/nifty-archimedes-hbxga4)

})
// `includes` defaults to '*' in CubeEvaluator.prepareAccessPolicy, so an empty
// memberLevel grants every member — the opposite of how it reads.
.or('includes', 'excludes')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A model shape that compiles on the current release stops compiling on upgrade, with no release where the user is told before it breaks. memberLevel: {} is not a syntax error the user can be assumed to have never written — it is the shape that reads as "nothing configured", which is exactly why it is worth rejecting, and equally why some models in the field will have it.

ErrorReporter already supports the softer landing (ErrorReporter.ts:63, public warning(...)); CubeValidator only ever calls errorReporter.error(formatErrorMessage(result.error)) at CubeValidator.ts:1440, so nothing here can degrade to a warning without special-casing this one Joi rule out of the schema result — e.g. checking the empty memberLevel in prepareAccessPolicy (where includes || '*' lives and where the memberMasking-without-memberLevel diagnostic already sits, CubeEvaluator.ts:411) and reporting it as a warning for one minor, error after.

Your call as maintainer — a hard error in a minor is defensible for a rule this narrow, and the PR body does call the break out. Flagging it because the cost lands at upgrade time on a model that was silently wrong rather than at authoring time, and the same message reads fine as a warning.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed this is the author's call, and it's with them — I've raised it twice and it's stated as an open question in the PR body. Not landing either way unilaterally.

Confirmed your mechanics, which are the useful part here, because they make the choice more expensive than it looks:

  • ErrorReporter.warning exists (ErrorReporter.ts:63).
  • CubeValidator never calls it — .error at 1440, 1461, 1531 only, and 1440 is the single funnel for every Joi result. There's no per-rule severity, so the .or() can't degrade to a warning in place.
  • CubeEvaluator.ts:412 is the memberMasking -without- memberLevel diagnostic inside prepareAccessPolicy, which is the natural home for a warning-based version — detection would be !memberLevel.includes && !memberLevel.excludes, checked before includes || '*' resolves it.

So the warning route isn't a severity flip on this rule, it's moving the check out of the schema into prepareAccessPolicy and dropping the .or() — a different change, with the rule no longer expressed where the rest of the policy shape is. That's the real trade-off against a hard error in a minor, and worth stating plainly rather than leaving it to read as a one-liner.

One correction to your framing: the cost lands at upgrade time, but not on a model that is silently wrong in the sense of broken — a model with memberLevel: {} behaves today exactly as includes: '*'. It's wrong only where it also carries memberMasking, which is the case this PR exists for. A model using memberLevel: {} as a plain "grant everything" is working as intended and would break on upgrade purely for spelling. That arguably strengthens the deprecation argument rather than weakening it.


Generated by Claude Code

Comment thread docs-mintlify/reference/data-modeling/data-access-policies.mdx
… page

The concepts page enumerates the widening defaults ("a policy with no
member_level spans every member") without the new constraint. The sentence
stays true for an omitted member_level, but a reader can take it as licence to
write an empty block, which now fails to compile with the rule documented only
on the reference page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017GxjcRUJBFdm1JBch2Fcr1
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @paveltiunov's task in 5m 16s —— View job


Review complete — the rule is correct and well-targeted. 1 medium (new), 2 prior threads re-affirmed, 0 blockers.

Full review
  • Read the diff and changed files in full
  • Checked the rule against CubeEvaluator.prepareAccessPolicy and CompilerApi.applyRowLevelSecurity
  • Reviewed tests and both docs pages
  • Resolved stale review threads / deduped findings
  • Posted findings

What holds up

The core claim checks out at the source. CubeEvaluator.ts:402-410 resolves policy.memberLevel.includes || '*', so memberLevel: {} is exactly { includes: '*' }, and CompilerApi.ts:703-708 masks a member only when no granting policy has inFullAccess && (!rowLevel || rowLevel.allowAll) — an empty memberLevel with no rowLevel satisfies that, so a same-policy memberMasking is a genuine no-op. The rule closes it.

The corrected message wording — "unmasked on every row the policy grants" — is accurate for both shapes: with no rowLevel the policy grants every row (degenerates to "never masked"), and with filters it describes the CASE WHEN … ELSE mask path at CompilerApi.ts:717-724. The docs <Warning> matches, and it does not contradict the canonical member_masking example at docs-mintlify/reference/data-modeling/data-access-policies.mdx:399-406 (member_level: { includes: [status, count] } + member_masking: { includes: "*" }): status/count are granted and unmasked, every other member is masked, which is what the surrounding prose at line 384 already says.

Scope check on the breaking change: every member_level in the repo sets includes or excludespolicy_overlap_test.yaml, customers.yaml, masking_test.yaml, orders_incorrect_acl.yml, orders_nonexist_acl.yml. includes: [] (used as "grant nothing" in masking_test.yaml:48) stays valid, correctly. viewSchema inherits baseSchema, so the rule reaches view access policies too.

Finding posted

CubeValidator.ts:1122 — the custom object.missing message drops Joi's {{#label}}. The error never names which policy failed (accessPolicy[1].memberLevel), and because formatErrorMessageFromDetails keys its dedupe map on the message string (CubeValidator.ts:1354-1356), two empty memberLevel blocks in one cube collapse into a single line. errorReporter.inContext supplies the cube name but not the policy index. Posted inline with a {{#label}} suggestion; note that the two toContain('memberLevel must define either includes or excludes') assertions need updating alongside it, since line 1355 rewrites the quoted label to (label = value).

Review threads

The listing returned 2 threads first-authored by claude, single page. 0 resolved, 2 re-affirmed:

  • PRRT_kwDOCOH19M6j5vlo (test file) — the stale-assertion half is genuinely fixed (cube-validator.test.ts:1864-1865 pin the corrected wording), but the comment at cube-validator.test.ts:1857-1861 grew to 5 lines and its first three sentences narrate the message's design history rather than the invariant; only the last sentence carries the rowLevel reason the assertion exists. Trim to that.
  • PRRT_kwDOCOH19M6j51fJ — hard error vs a deprecation round. Still the maintainer's call, still unlanded; the PR body states it as an open question.

One further concern deliberately not posted: memberMasking carries the identical includes || '*' default, so memberMasking: {} silently means "mask every member". The author lists it as an explicit follow-up, so it stays out of this PR.

Not verified

Tests were not run — node_modules is absent in this checkout and dependency installation is not available in the sandbox. The findings above are from reading CubeValidator.ts, CubeEvaluator.ts, CubeSymbols.ts, CompilerApi.ts and the fixtures, not from an executed suite. The author reports 49/49 suites and 974/974 tests green.

· branch [`pavel-claude/nifty-archimedes-hbxga4`](https://github.com/cube-js/cube/tree/pavel-claude/nifty-archimedes-hbxga4)

Copy link
Copy Markdown
Member Author

CI note — tests (mssql, true) failed on 633895e, not this PR's failure.

Every one of the 97 failures is the same error, before any test body runs:

ConnectionError: Login failed for user 'sa'.
  at connectListener (node_modules/mssql/lib/tedious/connection-pool.js:85:17)
  at Connection.onConnect (node_modules/tedious/src/connection.ts:1784:9)

97 failed / 50 skipped / 147 total — the SQL Server container rejected authentication, so the suite never reached a query.

Why it isn't this PR's:

  • The diff is a Joi presence rule on MemberLevelPolicySchema plus docs. Nothing in it can affect SQL Server container credentials.
  • packages/cubejs-testing-drivers/ contains no member_level or accessPolicy at all, so the new rule cannot fire anywhere in that suite — grepped the whole package.
  • The failure mode is uniform and pre-test (connection refused on login), not an assertion or a compile error.

Not re-running it manually: the failure is on 633895e, and the Drivers tests workflow is already re-running from scratch on the current head c1ffc02 (run 35398487544). That run is the re-run. If mssql comes back red there with the same login error, it's an infrastructure issue in the runner's SQL Server container rather than anything in this branch, and I'll say so here rather than papering over it. If it comes back red for any other reason, that's mine to root-cause.

No fix ported because there is nothing to port — this isn't a code failure with a known fix elsewhere.


Generated by Claude Code

Comment thread packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts Outdated

Copy link
Copy Markdown
Member Author

CI note 2 — tests (snowflake-export-bucket-azure, true) and tests (snowflake-export-bucket-azure-prefix, true) failed on 633895e. Different cause from the mssql one above, so recording it separately.

19 failed / 24 skipped / 102 passed / 145 total. Every failure is the same error:

Error: No pre-aggregation partitions were built yet for the pre-aggregation serving
this query and this API instance wasn't set up to build pre-aggregations.
Expected table name patterns: drivers_tests_preaggs.big_e_commerce__category_flat_external_*_thjuegpt_*

102 tests passed, so this is not a connectivity or credentials problem — the suite ran, and only the pre-aggregation-dependent tests failed.

Why it isn't this PR's:

  • This PR changes four files: CubeValidator.ts (one Joi presence rule), its test, and two docs pages. Nothing in packages/cubejs-query-orchestrator/, which is where pre-aggregation building lives.
  • The new rule cannot fire in this suite at all — packages/cubejs-testing-drivers/ contains no member_level or accessPolicy.
  • The base commit of this branch, 42bfe75 ("fix(query-orchestrator): don't report failed pre-agg build jobs as done", fix(query-orchestrator): don't report failed pre-agg build jobs as done #11629, merged ~4h before this branch), modifies PreAggregationLoader.ts (+75/−2) and PreAggregations.ts (+83/−11) — precisely the build path failing here. Its own description notes it changed whether a build job runs on an instance that only serves queries: "an instance configured to never build pre-aggregations still ran them in-process. Such an instance now refuses the job", and "the driver test suite drives every one of its builds through the jobs API against an instance that serves queries only." The failure text — "this API instance wasn't set up to build pre-aggregations" — is that guard.

What I verified vs. inferred: verified that this PR's diff contains no query-orchestrator changes, that the driver fixtures carry no access policies, and that 42bfe75 rewrites the failing path. I could not retrieve a Drivers tests run on master to confirm the check is red on the base branch directly — the workflow-run filters available to me returned unfiltered results. So the base-commit attribution is a strong inference from the diff and the error text, not a green-to-red comparison I ran.

No fix ported because none exists to port — there's no revert or follow-up PR for 42bfe75 that I'm aware of, and inventing an orchestrator change here would widen this PR well past a validator rule.

Not re-running manually: Drivers tests is already re-running on the current head c1ffc02 (run 35398487544). If these two come back red there the same way, it wants someone with context on #11629; if they come back red differently, that's mine.

cc @waralexrom as the author of #11629, in case this is already known.


Generated by Claude Code

The custom `object.missing` message replaced Joi's default wholesale, dropping
`{{#label}}` with it, so the error never said which policy was wrong. Worse,
`formatErrorMessageFromDetails` keys its dedupe map on the message text, so two
policies with an empty `memberLevel` in the same cube produced byte-identical
messages and collapsed into a single line naming neither:

  before: 1 error,  names accessPolicy[0]? no,  accessPolicy[1]? no
  after:  2 errors, names accessPolicy[0]? yes, accessPolicy[1]? yes

A cube with several policies now points at the one to edit. The assertions move
off the `memberLevel must ...` prefix, since the rendered label is quoted ahead
of it and `formatErrorMessage` rewrites it to `(label = value)` in the reasons
list, and a new case pins both labels and the un-deduped count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017GxjcRUJBFdm1JBch2Fcr1
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @paveltiunov's task in 5m 25s —— View job


Approve — 0 high, 0 medium, 1 low. The rule, the message and both docs pages check out; the one note is test hygiene.

Full review
  • Read the diff and all four changed files in full
  • Verify the Joi rule (placement, label, dedup, memberLevel-adjacent shapes)
  • Check the error message against the actual masking code path
  • Check both docs pages, and sweep the repo for models the rule would break
  • Attempt the unit suite (blocked — see below)
  • Handle prior review threads
  • Post findings

What I verified

The rule is in the right place and cannot misfire. .or('includes', 'excludes') at packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts:1120 sits on MemberLevelPolicySchema, which is reached only through GroupPolicySchema.memberLevelbaseSchema.accessPolicy (CubeValidator.ts:1146, 1209). Validation runs once, before evaluation — CubeValidator.compile (1417) over cubeSymbols.cubeList, and CubeEvaluator.prepareAccessPolicy (CubeEvaluator.ts:362) only runs on cubes that already passed isCubeValid. So the rule never sees the includesMembers/excludesMembers that prepareAccessPolicy writes back at CubeEvaluator.ts:400-407, and there's no second validation pass where a prepared policy could trip it.

Cube extends clones parent policies (CubeSymbols.ts:497-509) before validation, so an inherited empty member_level is rejected in the child too — consistent, and the right behaviour.

The error message's advice is correct, including the case it previously got wrong. Traced memberLevel: { excludes: ['ssn'] } + memberMasking: { includes: ['ssn'] } through CompilerApi.ts: policyGrantsMember (640-657) still grants ssn via the masking branch even though memberLevel excludes it, hasUnconditionalFullAccess (703-708) is false, so ssn lands in maskedMembersSet (718) — masked, not hidden. patchVisibilityByAccessPolicy (1014-1019) keeps it visible in meta for the same reason. The "use excludes for it" guidance produces a masked member, not a revoked one.

The "unmasked on every row the policy grants" phrasing also survives the rowLevel case: conditionalFullAccessPolicies (720-732) records the filter so the member renders CASE WHEN … ELSE mask END rather than being unmasked outright.

Nothing in the repo breaks. Swept every member_level / memberLevel occurrence across birdbox fixtures, schema-compiler fixtures, links.test.ts, yaml-schema.test.ts and all three docs pages — every one spells out includes or excludes. The "grant nothing" spelling in the docs (docs-mintlify/docs/data-modeling/data-access-policies.mdx:46, includes: []) stays valid, since includes has no .required() on its array item.

Docs. The concepts-page parenthetical sits inside the "no member_level" clause, which is the distinction that now matters — omitting the key and writing an empty block diverge. docs-mintlify/docs/data-modeling/access-control/member-level-security.mdx needs no change: it documents includes/excludes as the two options and shows no empty block.

Comment lengths. Both source and test comments are 2-3 lines; none exceeds the limit.

Finding

Severity Location Issue
Low packages/cubejs-schema-compiler/test/unit/cube-validator.test.ts:1846 The memberMasking-paired rejection test drives the same Joi path as the bare-{} test at :1840 and asserts the same substring; its comment describes an applyRowLevelSecurity behaviour the test never reaches. Posted inline.

Not run

yarn jest / npx jest / node -e in packages/cubejs-schema-compiler were all denied by the sandbox in this job, so I did not execute the new tests or reproduce the Joi output myself — the review above is static plus the code paths read end to end. The PR reports 49/49 suites and 974/974 tests green; that number is the author's, not mine.

One consequence I could therefore only infer, not confirm: formatErrorMessage (CubeValidator.ts:1350-1358) rewrites "label" into (label = value), so the new message will surface as (accessPolicy[0].memberLevel = [object Object]) must define either…. That is pre-existing formatting behaviour — yaml-schema.test.ts:770 already asserts the same [object Object] shape for another rule — and keeping {{#label}} is still required for the dedup the :1865 test pins, so it is an observation, not a request to change anything here.

Review threads

The listing returned 3 threads authored by me. Resolved 2 — PRRT_kwDOCOH19M6j5vlo (test pinned the pre-correction wording; now pins unmasked on every row the policy grants, addressed by be69d1d/0116692) and PRRT_kwDOCOH19M6j58w4 (missing {{#label}}; restored at CubeValidator.ts:1122, with the two-policy label test at :1868). Left 1 open: PRRT_kwDOCOH19M6j51fJ, hard error vs. deprecation warning — still a live maintainer decision, and the .or() is still a hard error at head. Re-affirmed 0 as duplicates; the one new finding matches no existing thread.

On that open thread, for the record: I agree with the PR body that this is the author's call. The breaking surface is narrow — every member_level in this repo already complies, and the only models that break are ones whose behaviour is unchanged by the fix (memberLevel: {} used as plain "grant everything"). A warning round would cost a move of the check out of the schema and into prepareAccessPolicy, since CubeValidator has no per-rule severity.
· branch pavel-claude/nifty-archimedes-hbxga4

Comment thread packages/cubejs-schema-compiler/test/unit/cube-validator.test.ts Outdated
…t pins

The comment claimed `ssn` is granted in full and the mask never fires, which is
applyRowLevelSecurity behaviour the test never reaches -- it stops at
`result.error`. The rule reads only memberLevel, so the fixture's memberMasking
is inert to it.

Say what the case actually pins: that the motivating combination stays
rejected. The masking no-op itself belongs to applyRowLevelSecurity in
server-core, which schema-compiler cannot reach -- the dependency runs the
other way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017GxjcRUJBFdm1JBch2Fcr1
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @paveltiunov's task in 4m 14s —— View job


Review round on aac0147 — no new findings; 1 prior thread left open for the author (the hard-error-vs-deprecation question)

Full review
  • Read the diff and the changed files in full
  • Verify the Joi rule's runtime behaviour statically (Joi installed deps unavailable — see caveat)
  • Review the tests
  • Review the docs changes
  • Resolve stale threads / dedupe findings
  • Post findings (none to post inline)

What I checked

The rule itselfpackages/cubejs-schema-compiler/src/compiler/CubeValidator.ts:1117-1123. .or('includes', 'excludes') on MemberLevelPolicySchema is the right level: it sits next to the sibling shape rules (.xor('filters','allowAll') at 1139, .with('memberMasking','memberLevel') at 1155), and memberLevel is only ever validated pre-prepareAccessPolicyCubeValidator.compile runs over cubeSymbols.cubeList, and the includesMembers/excludesMembers keys are written later at CubeEvaluator.ts:400-406. So there is no second validation pass over a prepared policy where {} would have already been widened into { includesMembers: [...] } and slipped through. Nothing in packages/ constructs a memberLevel programmatically either — the only non-test references are CompilerApi.ts, CubeEvaluator.ts, CubeSymbols.ts, CubeValidator.ts.

The label fix in 0116692 is load-bearing, and the test comment at cube-validator.test.ts:1866-1868 states the mechanism correctly. formatErrorMessage builds a Map keyed on d.context.message || d.message (CubeValidator.ts:1354-1356), so two identical messages collapse to one entry. object.missing's Joi context carries peers/peersWithLabels/label/value but no message, so the key is the rendered text and {{#label}} is the only thing making the two policies distinct. The (label = value) rewrite at 1355 matches on "label" with quotes, which {{#label}} produces — so both the headline and the "Possible reasons" list carry the index.

abortEarly: false at CubeValidator.ts:1425 is what makes the multi-policy test's toHaveLength(2) meaningful — worth noting the assertion depends on that option, not just on the label.

Tests. The nine cases cover the shapes that matter, including includes: [], which is the "grant nothing" idiom real fixtures use (packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/conditional_masking_test.yaml:31) — breaking that would have been the expensive mistake here. The breaking-change claim holds: grepping every member_level/memberLevel under packages/ turns up no empty block outside this PR's own test.

Docs. The <Warning> at docs-mintlify/reference/data-modeling/data-access-policies.mdx:286-296 is accurate against CompilerApi.ts:703-732 — "unmasked on every row the policy grants" is true both when hasUnconditionalFullAccess short-circuits masking at 710 and when the conditional branch at 720-732 records the filter for the CASE WHEN … ELSE mask render. The concepts-page parenthetical (docs/data-modeling/data-access-policies.mdx:96-98) correctly keeps omitting the key and writing an empty block as distinct cases.

Comment lengths — the four comments in the diff are 2, 3, 2 and 3 lines, each stating a non-obvious invariant (the includes || '*' default, the rowLevel wording constraint, the message-dedup mechanism). None flagged.

Caveat on verification

I could not execute the test suite: this checkout has no node_modules at the root or in packages/cubejs-schema-compiler, and the dependency install was not permitted in this run (npm install denied). The Joi semantics above are read off the schema and formatErrorMessage, not observed — the author's own reproduction in the threads is the empirical evidence, and it matches what the code says. If you want this run to execute the suite, the install command needs allowlisting.

Not raised inline

  • MemberMaskingPolicySchema (CubeValidator.ts:1124-1135) has the identical includes || '*' default with no .or(), so memberMasking: {} means "mask everything". The PR body already scopes this out as a follow-up; recording it rather than re-arguing it.

Review threads

The listing returned 2 of my own threads, both unresolved.

  • Resolved 1PRRT_kwDOCOH15M6j6Gwr (the cube-validator.test.ts comment overstating what the memberLevel: {} + memberMasking case pins). Fixed in aac0147; the comment at cube-validator.test.ts:1846-1848 now says the rule reads only memberLevel and that the masking no-op itself lives in applyRowLevelSecurity.
  • Re-affirmed 1PRRT_kwDOCOH15M6j51fJ on CubeValidator.ts:1120, the hard-error-in-a-minor vs. errorReporter.warning-first question. The schema still hard-errors and the PR body still carries it as an open question for the author, so the concern is live. Left open deliberately, not overlooked.
· branch [`pavel-claude/nifty-archimedes-hbxga4`](https://github.com/cube-js/cube/tree/pavel-claude/nifty-archimedes-hbxga4)

Copy link
Copy Markdown
Member Author

CI note 3 — tests (bigquery-export-bucket-gcs, false) failed on 633895e. Posting a third note only because this is a second driver family and it sharpens the picture rather than repeating it.

Unlike the others, exactly 1 test failed (34 skipped, 115 passed, 150 total) — and it's the one every pre-aggregated test depends on:

● Queries with the @cubejs-backend/bigquery-driver › must built pre-aggregations

  thrown: "Cube pre-aggregations build failed: failure: Not found: Table
  cube-open-source:dev_pre_aggregations.ec__t_a_external20201201_pjg3baai_kz5c2cui_1lard3b"

Two things worth drawing out:

1. Same root cause as the snowflake failures, different surface. Snowflake's 19 failures were all "No pre-aggregation partitions were built yet" — the downstream symptom of builds not landing. BigQuery catches it at the build step itself. Both are pre-aggregation builds failing; only the point of detection differs.

2. The error text is 42bfe75's own new reporting path. That commit is titled "don't report failed pre-agg build jobs as done" — its entire purpose was to make a failed build report failure instead of done. "Cube pre-aggregations build failed: failure: ..." is that new path firing. So either it is correctly surfacing a build problem that was previously silent, or the build tracking it introduced is reporting a healthy build as failed. Its own commit message describes exactly this hazard: "Reporting the build as failed for a partition that is complete and already serving queries is worse than the missing failure this tracking was added for."

Either reading points at 42bfe75, not at this PR. This branch changes one Joi rule and two docs pages, and touches nothing in packages/cubejs-query-orchestrator/.

Running total of driver checks red, across 633895e, f44a167 and be69d1d — three commits whose only deltas were docs text and an error string:

check failures error
mssql 97/147 Login failed for user 'sa' (container auth)
snowflake-export-bucket-azure 19 no pre-agg partitions built
snowflake-…-azure-prefix 19 no pre-agg partitions built
bigquery-export-bucket-gcs 1 pre-agg build failed: table not found

The pre-agg failure count is invariant at 19 across every commit — it does not track anything in this branch.

@waralexrom — flagging again given the second driver family; this looks like it wants eyes on #11629 independent of this PR, since anything branching off current master will hit it.


Generated by Claude Code

@codecov

codecov Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 61.23%. Comparing base (42bfe75) to head (aac0147).

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #11934   +/-   ##
=======================================
  Coverage   61.23%   61.23%           
=======================================
  Files         247      247           
  Lines       19824    19824           
  Branches     4043     4043           
=======================================
  Hits        12140    12140           
  Misses       7112     7112           
  Partials      572      572           
Flag Coverage Δ
cube-backend 61.23% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Member Author

CI update on aac0147 — narrowing the earlier notes, two of which have aged out.

Everything this PR could plausibly affect is green on the current head:

check result
unit (24.x, 3.13), unit (26.x, 3.13) ✅ success — the 10 new validator cases ran here
unit-core, lint, build, build-cubestore ✅ success
Build native linux, CodeQL, Debian image build ✅ success
~30 integration (…) jobs, integration-smoke ×2, integration-cubestore ✅ success
codecov/patch, codecov/project ✅ success — all modified lines covered, project coverage unchanged at 61.23%

Correction to CI note 1: tests (mssql, true) and tests (mssql, false) both pass on aac0147. The Login failed for user 'sa' failures were transient container auth, and they have cleared on their own. I said that failure would want someone else's attention if it persisted — it didn't persist, so disregard that part.

Correction to CI note 3: tests (bigquery-export-bucket-gcs, false) is currently re-running rather than red; the earlier must built pre-aggregations failure has not recurred on this head so far. I'll leave the analysis standing but it may turn out to have been transient too.

What actually remains red: tests (snowflake-export-bucket-azure, true) and tests (snowflake-export-bucket-azure-prefix, true) — still the pre-aggregation build failure described in CI note 2, still with an invariant 19 failures, now across six commits. Everything else in that note holds: this PR touches nothing in packages/cubejs-query-orchestrator/, and these two are the only checks standing between the branch and green.

So the blast radius is narrower than my earlier notes implied — two snowflake-azure checks, not a broad driver-suite outage. Apologies for the noise on the ones that turned out to be transient; I'd rather correct them than leave three notes overstating the situation.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

Retracting half of my last comment. I said bigquery-export-bucket-gcs might have been transient. It wasn't — it has now completed on aac0147 and failed, with one more failure than before:

● must built pre-aggregations
    Cube pre-aggregations build failed: failure: Not found: Table
    cube-open-source:dev_pre_aggregations.big_e_commerce__t_a_external20200101_bw10jfnq_…

● querying BigECommerce: partitioned pre-agg with multi time dimension
    Error: Not found: Table cube-open-source:dev_pre_aggregations.big_e_commerce__multi_time_dim_for_count_external20200501_…

2 failed / 34 skipped / 114 passed (previously 1 failed / 115 passed). CI note 3's analysis stands as originally written; the retraction I offered for it does not.

Corrected standing on aac0147:

✅ genuinely cleared tests (mssql, true) and (mssql, false) — the container auth failures were transient and now pass
❌ persistent snowflake-export-bucket-azure, snowflake-export-bucket-azure-prefix, bigquery-export-bucket-gcs — all Not found: Table / no-partitions-built in the pre-aggregation path

So: one of the two problems was transient, the other is real and spans two driver families on the current head. That is the shape I described in CI notes 2 and 3, and only the mssql part of CI note 1 has aged out.

Everything this PR can affect remains green — unit on both Node versions, lint, build, all ~30 integration jobs, both smoke suites, codecov. This branch changes one Joi rule and two docs pages and touches nothing in packages/cubejs-query-orchestrator/.

Sorry for the churn across these notes — I over-corrected before the bigquery job had finished, which I should have waited for rather than reporting an in-progress check as evidence of recovery.


Generated by Claude Code

This branch was successfully deployed

1 active (outdated) deployment
staging - docs-mintlify c1ffc02a Deployed Sep 18, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Issues that require a documentation improvement javascript Pull requests that update Javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants