Policies
You can define policies that target specific groups and contain member-level and (or) row-level security rules:Policy evaluation
When processing a request, Cube will evaluate the access policies and combine them with relevant custom security rules, e.g.,public parameters for member-level security
and query_rewrite filters for row-level security.
The permission space
It helps to think of access control as a two-dimensional permission space — a grid of members (the columns a user may query: dimensions and measures) and rows (the records a user may see):- one axis is members — what a user can look at;
- the other axis is rows — which records they can look at.
member_level chooses the members (the horizontal extent) and its
row_level chooses the rows (the vertical extent). Defaults widen the region —
a policy with no row_level (or with row_level: { allow_all: true }) spans
every row, and a policy with no member_level spans every member.
member_masking marks part of a region as visible but masked rather than
fully readable.
A user usually matches more than one policy (for example, through multiple
groups), so their effective access is the combination of
every region granted by every matching policy:
- Members are unioned. A member is accessible if any matching policy grants it. A user who matches several policies sees every member those policies expose, even when no single policy exposes all of them.
- Rows are intersected across the queried members. For each queried member, the visible rows are the union of the row filters of the policies that grant that member (a policy with no row filter adds no restriction). A row is returned only when it is visible for every queried member.
- A member is masked when no granting policy gives it unconditional full
access through
member_level, but some matching policy lists it undermember_masking. - Access is denied (an empty result) only when a queried member is granted by no matching policy at all.
Diagram and behavior
Consider anorders_view matched by two of a user’s groups:
- the
supportgroup —member_level: [status, count],row_levelrestricted toregion = 'US'; - the
financegroup —member_level: [count, revenue],row_levelrestricted toregion = 'EU'.
count sits
in the overlap (both policies grant it); status and revenue are each granted
by only one policy:
US rows | EU rows | |
|---|---|---|
status | ✓ (support) | — |
count | ✓ (support) | ✓ (finance) |
revenue | — | ✓ (finance) |
| Queried members | How rows resolve | Visible rows |
|---|---|---|
status, count | US ∩ (US ∪ EU) | US rows |
count, revenue | (US ∪ EU) ∩ EU | EU rows |
count | US ∪ EU | all rows |
status, count, revenue | US ∩ EU | none (empty result) |
- Querying
statusandcountreturns onlyUSrows:statusis granted only by thesupportpolicy, so records outside the US can never satisfy the query. - Querying
countalone returns all rows: both policies grantcount, so its visible rows are the union of the two regions. - Querying
status,count, andrevenuereturns nothing:statusis visible only onUSrows andrevenueonly onEUrows, and no record is in both. The result is empty rather than leaking US-only members onto EU rows.
A policy without a
row_level filter defaults to all rows (allow-all). So
when every policy that grants the queried members is filter-less, there is no
row restriction at all — the members are simply unioned and all rows are
returned. Row filters only narrow the result when a granting policy defines them.Member-level access
Member-level security rules in access policies are combined together withpublic parameters of cube and view members using the AND semantics.
Both will apply to the request.
When querying a view, member-level security rules defined in the view are not combined together
with member-level security rules defined in relevant cubes.
Only the ones from the view will apply to the request.
This is consistent with how column-level security works in SQL databases. If you have
a view that exposes a subset of columns from a table, it doesnt matter if the
columns in the table are public or not, the view will expose them anyway.
Row-level access
Row-level filters in access policies are combined together with filters defined using thequery_rewrite configuration option.
Both will apply to the request.
When querying a view, row-level filters defined in the view are combined together
with row-level filters defined in relevant cubes. Both will apply to the request.
This is consistent with how row-level security works in SQL databases. If you have
a view that exposes a subset of rows from another view, the result set will be
filtered by the row-level security rules of both views.
Data masking
With data masking, you can return masked values for restricted members instead of denying access entirely. Users who don’t have full access to a member will see a transformed value (e.g.,***, -1, NULL) rather than receiving an error.
To use data masking, define a mask parameter on dimensions
or measures, and add member_masking to your access policy alongside member_level.
Members in member_level get real values; members not in member_level but in
member_masking get masked values; members in neither are denied.
manager group will see:
| Member | Value |
|---|---|
status | Real value (full access via member_level) |
count | Real value (full access via member_level) |
secret_code | Masked via SQL: ***xyz |
revenue | Masked: -1 |
mask is defined on a member, the default mask value is NULL. You can
customize defaults with the CUBEJS_ACCESS_POLICY_MASK_STRING,
CUBEJS_ACCESS_POLICY_MASK_NUMBER, CUBEJS_ACCESS_POLICY_MASK_BOOLEAN, and
CUBEJS_ACCESS_POLICY_MASK_TIME environment variables.
Masking across multiple policies
Because member access is unioned, full access wins over masking. If any matching policy grants a member unconditional full access throughmember_level (with no row_level filter), the user sees the real
value — even if another matching policy lists that member under member_masking.
Masking only takes effect when no matching policy grants unconditional full
access. There are two sub-cases:
- Masked only. The member is exposed solely through
member_masking(or any full-access policy is itself row-restricted). The member is masked for all rows. - Conditionally unmasked. Another policy grants full access and defines a
row_levelfilter — full access is conditional on that filter. Masking then becomes conditional on the row filter: rows matching the filter show the real value, while the rest show the masked value. The generated SQL is roughlyCASE WHEN {rowFilter} THEN {value} ELSE {mask} END.
This lets you combine a broad masking policy (e.g. the
* group sees masked
values) with a narrower policy that reveals real values only for the rows a
group is entitled to (its row_level range).country = 'US' and the mask filter is
country = 'US') — then every returned row would show the real value anyway.
In that case the CASE WHEN is unnecessary and the member is unmasked. This
also lets a conditionally-masked aggregate measure render its real value
instead of being masked, even without grouping by the filter’s member.
Conditional masking on measures
Conditional masking is evaluated per row, which works naturally for dimensions (theCASE WHEN expression is part of the GROUP BY). For an
aggregate measure (e.g. sum, count), that per-row expression can only be
applied when the members referenced by the row filter are part of the query’s
GROUP BY.
When a query selects a conditionally-masked measure but does not group by the
members referenced in the row filter, Cube cannot decide the condition per
aggregated group. Rather than emit invalid SQL (where the filter column is
neither grouped nor aggregated — which fails on strict engines like BigQuery),
it renders the mask value for the entire measure (NULL by default) instead
of the conditional expression.
| Query groups by the row filter’s members? | Result for the measure |
|---|---|
| Yes | Conditional: real value for matching rows, masked otherwise |
| No | Fully masked (the mask value, e.g. NULL) |
member_masking reference.
Common patterns
Restrict access to specific groups
To restrict access to a view to only specific groups, define access policies for those groups. Access is automatically denied to all other groups:groups parameter (plural) to apply the same policy to multiple groups at once:
Filter by user attribute
You can filter data based on user attributes to ensure users only see data they’re authorized to access. For example, sales people can see only their own deals, while sales managers can see all deals:Filter by multiple user attributes
You can pass multiple values in thevalues array to match a dimension against
more than one user attribute. This is useful when users may have access based on
multiple properties, such as a country and a custom country property:
Mask sensitive members
You can mask sensitive members for most users while granting full access to privileged groups:Mandatory filters
You can apply mandatory row-level filters to specific groups to ensure they only see data matching certain criteria:Custom mapping
Cube cloud platform automatically maps authenticated users to groups for access policies. If you are using Cube Core or authenticating against Core Data APIs directly, you might need to map the security context to groups manually.Using securityContext
TheuserAttributes object is only available in Cube Cloud platform. If you are using Cube Core or authenticating against Core Data APIs directly, you won’t have access to userAttributes. Instead, you need to use securityContext directly when referencing user attributes in access policies (e.g., in row_level filters or conditions). For example, use securityContext.userId instead of userAttributes.userId.