Skip to content

docs: draft access policies v2 design spec#10804

Open
keydunov wants to merge 2 commits into
masterfrom
docs/access-policies-v2-design
Open

docs: draft access policies v2 design spec#10804
keydunov wants to merge 2 commits into
masterfrom
docs/access-policies-v2-design

Conversation

@keydunov
Copy link
Copy Markdown
Member

@keydunov keydunov commented May 3, 2026

Working doc capturing the proposed access policies primitives (access_policies registry, required_access_policies on cubes/views and members, mask_unless_access_policies + mask, access_filters with apply_if_access_policies). Includes worked examples on a deals dataset and a "Potential gaps" section flagging open design questions.

Lives at the root of docs-mintlify/ as .md (not .mdx) so it stays out of the published Mintlify nav.

Made-with: Cursor

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

Working doc capturing the proposed access policies primitives
(access_policies registry, required_access_policies on cubes/views and
members, mask_unless_access_policies + mask, access_filters with
apply_if_access_policies). Includes worked examples on a deals dataset
and a "Potential gaps" section flagging open design questions.

Lives at the root of docs-mintlify/ as .md (not .mdx) so it stays out
of the published Mintlify nav.

Made-with: Cursor
@mintlify
Copy link
Copy Markdown
Contributor

mintlify Bot commented May 3, 2026

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

Project Status Preview Updated (UTC)
cubed3 🟢 Ready View Preview May 3, 2026, 11:50 PM

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

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 3, 2026

Claude finished @keydunov's task in 3m 6s —— View job


Review: Access Policies v2 Design Spec

  • Read the design spec document
  • Analyze design completeness and consistency
  • Review worked examples for correctness
  • Check for security considerations
  • Post review feedback

Overall this is a well-structured design doc. The three-layer model (project registry → cube/view gating → member-level access) is clean, the policy expression algebra (all_of/any_of/none_of) is intuitive, and the OR-combination semantics for access filters (row-level grants, not row-level restrictions) is a good design choice. The worked examples are correct — I verified all three against the evaluation rules and the per-deal visibility tables check out.

That said, I found one significant design inconsistency and several smaller issues. I've left 8 inline comments on specific lines; here's a summary of the most important findings:

Critical: View-cube member composition contradicts the ecommerce example

The spec states (lines 375–378): "Member access on a view is independent of the underlying cubes… the underlying cube member's parameters are not combined."

But the ecommerce example (lines 599–685) includes cost from the products cube through a view without redeclaring mask_unless_access_policies, yet the summary table claims cost is still masked for non-finance users. Under the stated composition rule, the cube's masking should be dropped and cost would be unmasked for everyone — a PII leak by design.

This needs to be resolved before the spec is shared. Either the composition rule should say cube-level member policies are inherited unless overridden, or the example needs to redeclare the masking on the view. See inline comment for details.

Other notable issues

Issue Location Severity
values: ["*"] wildcard syntax used but never defined Line 419 Medium
Reference section says mask applies to "denied members" but hard-deny hides the member entirely Line 697 Low
All Reference section links and [filter operators](#) are broken (point to #) Lines 171, 579, 689–699 Low
and: filter syntax used once but grammar not formally defined (nesting rules, or: support?) Lines 662–669 Medium
Default mask behavior for measures is underspecified Lines 308–310 Low
CUBEJS_ACCESS_POLICY_MASK_* env vars mentioned without any documentation of accepted values Lines 334–335 Low

Strengths

  • The "Potential gaps" section is excellent — it's honest, thorough, and covers the right concerns (default-open for new fields, no audit trail, cache key explosion, AI agent identity). This is the kind of section that saves weeks of back-and-forth later.
  • The naming nits section is also valuable. I agree that apply_if_access_policies and mask_unless_access_policies are verbose. The shorter forms (apply_if, mask_unless) read naturally in context since the value is always a policy expression.
  • The OR-combination of access filters is well-explained with the right mental model (row-level grants, not SQL WHERE narrowing). The warning about this being unfamiliar to SQL users (line 793) is good to have.

Suggestions for a future revision

  1. Resolve the view-cube composition semantics — this is the one blocker. Pick inherit-unless-overridden or view-is-authoritative, and make the examples consistent.
  2. Add a "Filter syntax" subsection documenting the full grammar of an access filter entry (member/operator/values vs and:/or: wrappers, where apply_if_access_policies sits).
  3. Fill in or mark placeholder links — the broken # anchors in the Reference section look like the doc is further along than it is.
  4. Document the values: ["*"] wildcard and the CUBEJS_ACCESS_POLICY_MASK_* env vars, even briefly.

Comment on lines +599 to +618
# views/order_revenue.yml
views:
- name: order_revenue
cubes:
- join_path: order_items
includes: [status, created_at, total_sale_price, count]
- join_path: order_items.products
includes: [brand, category, cost]
- join_path: order_items.users
includes: [country, traffic_source]

# No required_access_policies → open to everyone. cost is
# soft-masked for non-finance users (see products cube below).
access_filters:
# Region filter applies to everyone except org admins
- member: country
operator: equals
values: ["{ userAttributes.region }"]
apply_if_access_policies:
none_of: [org_admin]
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.

Design inconsistency: View-cube member composition contradicts this example.

Lines 375–378 state:

Member access on a view is independent of the underlying cubes. A member exposed by a view uses the view's own required_access_policies / mask_unless_access_policies / mask; the underlying cube member's parameters are not combined.

But this view includes cost from products cube without redeclaring mask_unless_access_policies on it. According to the composition rule, the cube's mask_unless_access_policies: [finance] (line 632) should not apply through the view — meaning cost would be unmasked for everyone.

Yet the summary table (line 681–685) says cost is masked for non-finance users through this view.

Either:

  1. The composition rule needs to say cube-level member policies are inherited by views unless explicitly overridden, or
  2. This view definition needs to redeclare mask_unless_access_policies: [finance] and mask: -1 on the cost include, or
  3. The summary table is wrong and cost is actually unmasked through this view.

This is a critical ambiguity — getting it wrong in either direction leads to PII leaks or surprising access denials. I'd recommend resolving this before the spec is shared broadly.

Comment on lines +375 to +381
A member exposed by a view uses the view's own `required_access_policies` /
`mask_unless_access_policies` / `mask`; the underlying cube member's parameters are
not combined. This mirrors SQL column visibility — once a column is
exposed by a view, the view's grants are authoritative.
- **Access filters** on a view compose with the underlying cubes.
Filters from both layers apply.

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.

This rule has significant security implications that the spec acknowledges in the "Potential gaps" section (line 759–763) but doesn't resolve. Consider adding a concrete recommendation here, e.g.:

Recommended: When a view includes a cube member that has required_access_policies or mask_unless_access_policies, the view author should explicitly redeclare the access policy on the included member. A future lint rule will warn when cube-level policies are silently dropped.

Without this, a developer adding a new view that includes a PII-protected cube member can unknowingly bypass the cube's masking — and the "view is authoritative" rule means this is by design, not a bug.

access_policies:
region_user:
user_attribute: securityContext.region
values: ["*"]
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.

The values: ["*"] wildcard syntax here is not explained anywhere in the spec. What does it mean — "any non-null value"? "Any value at all including null"? "Attribute must exist"?

This needs a definition, or it will be a source of confusion. Consider adding a section on special values/wildcards in the policy expression reference.

- [`apply_if_access_policies`](#) — scope when an access filter applies; omit to apply to everyone
- [`required_access_policies`](#) — strict member-access requirement
- [`mask_unless_access_policies`](#) — soft mask: members stay queryable but return `mask` unless the user satisfies a listed policy
- [`mask`](#) — value returned in place of real data for masked or denied members
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.

The reference section says mask is the "value returned in place of real data for masked or denied members." But for denied members (hard deny via required_access_policies), the member is hidden from metadata and queries that reference it fail (line 240). No mask value is ever returned in the hard-deny path.

This should read just "for masked members" — the "or denied" part is misleading.

Suggested change
- [`mask`](#) — value returned in place of real data for masked or denied members
- [`mask`](#) — value returned in place of real data for masked members

`access_filters` is a list of row-level grants applied **after** the
user has passed `required_access_policies`.

Filters use the same format as REST (JSON) API query filters,
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.

All [filter operators](#), [Ecommerce demo](#), and the Reference section links point to # (self-link). These should either be filled in with real anchors or marked as TODO so they're not mistaken for working links during review.

Comment on lines +308 to +310
Default mask: `MD5(<value>)` for strings (deterministic — joins and
grouping still work), `NULL` for numbers/booleans/times. Override
per-member:
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.

Nit: The default mask for measures is underspecified. MD5(<value>) for strings and NULL otherwise makes sense for dimensions, but what does MD5(<value>) mean for a sum or count measure? The aggregated result is a number, so it would fall to NULL — but a count_distinct of a string dimension exposed as a measure could be surprising.

Worth adding a sentence clarifying that the default mask is determined by the member's type, not the underlying SQL type, to avoid ambiguity.

Comment on lines +662 to +669
- and:
- member: user_id
operator: equals
values: ["{ userAttributes.userId }"]
- member: country
operator: equals
values: ["{ userAttributes.region }"]
apply_if_access_policies: [sales]
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.

The and: wrapper introduces a structurally different filter format (array of conditions under and:) compared to single-member filters. This is the only example in the doc using this syntax, and the grammar isn't formally defined.

A few questions for clarity:

  • Can or: also be used here for explicit OR within a single filter entry?
  • Can and: / or: be nested (e.g., and: [or: [...], ...])?
  • Where does apply_if_access_policies sit relative to and: — is it always a sibling at the top level of the filter entry?

Consider adding a brief "Filter syntax" subsection that shows the full grammar of a filter entry.

Comment on lines +334 to +335
Globally override defaults via `CUBEJS_ACCESS_POLICY_MASK_STRING`,
`_NUMBER`, `_BOOLEAN`, `_TIME`.
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.

Minor: CUBEJS_ACCESS_POLICY_MASK_STRING, _NUMBER, _BOOLEAN, _TIME are mentioned but not documented — what values do they accept? For _STRING, is it a literal string, a SQL expression, or a template? For _NUMBER, is 0 vs -1 vs NULL the expected range?

Even a one-line example per env var would help.

Extends the "Sales deals with regional Closed Won access" worked
example with a subsection showing member-level required_access_policies
on the region dimension. Translates the old-spec per-group
member_level.includes pattern into the new declarative per-member
form and adds a per-user field-visibility table.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant