diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index ab131e9bacec3..7348d54411761 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -648,6 +648,12 @@ "recipes/configuration/custom-data-model-per-tenant" ] }, + { + "group": "Access Control", + "pages": [ + "recipes/access-control/conditional-row-level-access" + ] + }, { "group": "Core Data API", "pages": [ diff --git a/docs-mintlify/docs/data-modeling/data-access-policies.mdx b/docs-mintlify/docs/data-modeling/data-access-policies.mdx index cf65f076bfa3d..9cd3816bc63f1 100644 --- a/docs-mintlify/docs/data-modeling/data-access-policies.mdx +++ b/docs-mintlify/docs/data-modeling/data-access-policies.mdx @@ -492,6 +492,14 @@ view(`orders_view`, { +### Conditional row-level filters + +You can switch which `row_level` filter applies based on the +[security context][ref-sec-ctx] — for example, to scope a group to its region +or to let admins bypass row filters with `row_level.allow_all: true`. See the +[conditional row-level access recipe][ref-recipe-conditional-row-level] for +worked examples in YAML and JavaScript. + ### Mandatory filters You can apply mandatory row-level filters to specific groups to ensure they only see data matching certain criteria: @@ -621,4 +629,5 @@ cube(`orders`, { [ref-ref-dap-role]: /reference/data-modeling/data-access-policies#role [ref-ref-dap-masking]: /reference/data-modeling/data-access-policies#member-masking [ref-ref-mask-dim]: /reference/data-modeling/dimensions#mask -[ref-core-data-apis]: /reference/core-data-apis \ No newline at end of file +[ref-core-data-apis]: /reference/core-data-apis +[ref-recipe-conditional-row-level]: /recipes/access-control/conditional-row-level-access \ No newline at end of file diff --git a/docs-mintlify/recipes/access-control/conditional-row-level-access.mdx b/docs-mintlify/recipes/access-control/conditional-row-level-access.mdx new file mode 100644 index 0000000000000..9c51d741bcf70 --- /dev/null +++ b/docs-mintlify/recipes/access-control/conditional-row-level-access.mdx @@ -0,0 +1,280 @@ +--- +title: Conditional row-level access +description: Switch row-level filters based on the security context, and let an admin policy grant unrestricted row access on top of more restrictive policies. +--- + +## Use case + +You want different row-level filters to apply to the same group depending on +who the user is. For example: + +- Users in different regional groups should see only rows for their region. +- Admins should bypass row filters entirely, even when other policies restrict +the same role. + +You can express both patterns with [`access_policy`][ref-ref-dap] by combining +[`conditions`][ref-ref-dap-conditions], [`row_level.allow_all`][ref-ref-dap-row-level], +and the [_OR_ semantics across policies][ref-dap-rls] that match the same group. + +## Data modeling + +### Region-based switching + +Define one policy per region, and gate each policy with a `conditions` entry +that checks the user's [security context][ref-sec-ctx]. Only the policy whose +condition evaluates to `true` contributes its `row_level` filter to the query — +the others are skipped. + +In the following example, users in the `analyst` group see rows for their +region: members of the `emea` group are restricted to EMEA orders, and members +of the `amer` group are restricted to AMER orders. + + + +```yaml title="YAML" +cubes: + - name: orders + # ... + + access_policy: + - group: analyst + conditions: + - if: "{ security_context.groups and security_context.groups.includes('emea') }" + row_level: + filters: + - member: region + operator: equals + values: ["EMEA"] + + - group: analyst + conditions: + - if: "{ security_context.groups and security_context.groups.includes('amer') }" + row_level: + filters: + - member: region + operator: equals + values: ["AMER"] +``` + +```javascript title="JavaScript" +cube(`orders`, { + // ... + + access_policy: [ + { + group: `analyst`, + conditions: [ + { if: securityContext.groups && securityContext.groups.includes(`emea`) } + ], + row_level: { + filters: [ + { + member: `region`, + operator: `equals`, + values: [`EMEA`] + } + ] + } + }, + { + group: `analyst`, + conditions: [ + { if: securityContext.groups && securityContext.groups.includes(`amer`) } + ], + row_level: { + filters: [ + { + member: `region`, + operator: `equals`, + values: [`AMER`] + } + ] + } + } + ] +}) +``` + + + +In JavaScript, you can collapse the two policies into one by computing the +filter `values` from `securityContext` at evaluation time — the `values` array +is evaluated per request, so a ternary can pick the region for the caller: + +```javascript title="JavaScript" +cube(`orders`, { + // ... + + access_policy: [ + { + group: `analyst`, + row_level: { + filters: [ + { + member: `region`, + operator: `equals`, + values: [ + securityContext.groups && securityContext.groups.includes(`emea`) + ? `EMEA` + : `AMER` + ] + } + ] + } + } + ] +}) +``` + + +This single-policy form is JavaScript-only. YAML `{ ... }` expressions don't +support inline conditionals (Python `if`/`else`) or comparison operators, so in +YAML use one policy per region as shown above. Note that `row_level` itself is +always a static object — only the `member` and `values` inside a filter are +evaluated against `securityContext`; you can't replace `row_level` with a +function. + + +### Admin override with `allow_all` + +To let admins bypass row-level filters that apply to a role, add a second +policy for the same group that grants [`row_level.allow_all`][ref-ref-dap-row-level] +when `securityContext.is_admin` is true. Because policies that match the same +group are combined with _OR_ semantics, the admin policy unlocks every row +regardless of the more restrictive analyst policy: + + + +```yaml title="YAML" +cubes: + - name: orders + # ... + + access_policy: + # Region-restricted access for regular analysts + - group: analyst + row_level: + filters: + - member: region + operator: equals + values: ["{ security_context.region }"] + + # Admin override: full row access when the user is an admin + - group: analyst + conditions: + - if: "{ security_context.is_admin }" + row_level: + allow_all: true +``` + +```javascript title="JavaScript" +cube(`orders`, { + // ... + + access_policy: [ + { + // Region-restricted access for regular analysts + group: `analyst`, + row_level: { + filters: [ + { + member: `region`, + operator: `equals`, + values: [securityContext.region] + } + ] + } + }, + { + // Admin override: full row access when the user is an admin + group: `analyst`, + conditions: [ + { if: securityContext.is_admin } + ], + row_level: { + allow_all: true + } + } + ] +}) +``` + + + +### Composing boolean logic with `conditions` + +`conditions` accept boolean logic, so you can switch which `row_level` +applies based on combined checks against the security context and user +attributes. In YAML, use `and`, `or`, `not`, and parentheses inside +`{ ... }`. In JavaScript, use `&&`, `||`, and `!`. Multiple `conditions` +entries on a single policy are combined with _AND_ semantics; multiple +matching policies are combined with _OR_ semantics. + +In the following example, full-time senior analysts who are _either_ admins +_or_ owners and are _not_ contractors get unrestricted row access; everyone +else in the `analyst` group falls back to the region-restricted policy above. + + + +```yaml title="YAML" +cubes: + - name: orders + # ... + + access_policy: + - group: analyst + conditions: + - if: "{ user_attributes.is_full_time_employee and user_attributes.is_senior }" + - if: "{ user_attributes.is_admin or user_attributes.is_owner }" + - if: "{ not (security_context.groups and security_context.groups.includes('contractors')) }" + row_level: + allow_all: true +``` + +```javascript title="JavaScript" +cube(`orders`, { + // ... + + access_policy: [ + { + group: `analyst`, + conditions: [ + { if: userAttributes.is_full_time_employee && userAttributes.is_senior }, + { if: userAttributes.is_admin || userAttributes.is_owner }, + { if: !(securityContext.groups && securityContext.groups.includes(`contractors`)) } + ], + row_level: { + allow_all: true + } + } + ] +}) +``` + + + + +YAML `{ ... }` expressions are parsed as Python and support `and`, `or`, `not`, +parentheses, member access, and method calls like `.includes(...)`. They do +**not** support comparison operators (`==`, `>=`, `in`) or inline `if`/`else` +conditionals — use boolean [user attributes][ref-sec-ctx] or method calls +instead. The JavaScript form has no such restriction. + + +## Result + +With these policies in place: + +- Regional analysts see only rows for the region attached to their security +context, because only the policy whose `conditions` match contributes its +`row_level` filter. +- Admins see all rows, because the admin policy's `row_level.allow_all: true` +combines with the regional policy via _OR_ semantics. +- Users without any matching policy are denied access by default. + + +[ref-ref-dap]: /reference/data-modeling/data-access-policies +[ref-ref-dap-conditions]: /reference/data-modeling/data-access-policies#conditions +[ref-ref-dap-row-level]: /reference/data-modeling/data-access-policies#row-level +[ref-dap-rls]: /docs/data-modeling/data-access-policies#row-level-access +[ref-sec-ctx]: /docs/data-modeling/access-control/context diff --git a/docs-mintlify/reference/data-modeling/data-access-policies.mdx b/docs-mintlify/reference/data-modeling/data-access-policies.mdx index 5698a33e556a5..bc4b9130dc421 100644 --- a/docs-mintlify/reference/data-modeling/data-access-policies.mdx +++ b/docs-mintlify/reference/data-modeling/data-access-policies.mdx @@ -208,6 +208,65 @@ cube(`orders`, { +#### Boolean logic in `if` expressions + +`if` expressions support boolean logic so you can compose checks against +the [security context][ref-sec-ctx] and user attributes: + +- In YAML, expressions inside `{ ... }` are parsed as Python and support the +`and`, `or`, and `not` operators, parentheses for grouping, member access, and +method calls such as `.includes(...)`. Comparison operators (`==`, `>=`, `in`) +and inline `if`/`else` conditionals are **not** supported — use boolean user +attributes or method calls instead. +- In JavaScript, expressions use native operators: `&&`, `||`, `!`, and any +valid JavaScript expression. +- Multiple `conditions` entries on a single policy are combined with _AND_ +semantics — every entry must evaluate to `true` for the policy to take effect. +- Multiple policies that match the same group are combined with _OR_ semantics — +if any matching policy grants access, the user gets access. + +In the following example, a single policy combines three `conditions` entries +that each use a different boolean operator: + + + +```yaml title="YAML" +cubes: + - name: orders + # ... + + access_policy: + - group: manager + conditions: + - if: "{ user_attributes.is_full_time_employee and user_attributes.is_senior }" + - if: "{ user_attributes.is_admin or user_attributes.is_owner }" + - if: "{ not (security_context.groups and security_context.groups.includes('contractors')) }" + member_level: + includes: "*" +``` + +```javascript title="JavaScript" +cube(`orders`, { + // ... + + access_policy: [ + { + group: `manager`, + conditions: [ + { if: userAttributes.is_full_time_employee && userAttributes.is_senior }, + { if: userAttributes.is_admin || userAttributes.is_owner }, + { if: !(securityContext.groups && securityContext.groups.includes(`contractors`)) } + ], + member_level: { + includes: `*` + } + } + ] +}) +``` + + + ### `member_level` The optional `member_level` parameter, when present, configures [member-level @@ -452,6 +511,63 @@ REST (JSON) API][ref-rest-query-filters] queries, allowing to use the same set o You can also use `and` and `or` parameters to combine multiple filters into [boolean logical operators][ref-rest-boolean-ops]. +#### `allow_all` + +Use `allow_all: true` to make the row-level intent of a policy explicit without +listing any filters. It grants the policy access to all rows and is equivalent +to omitting `row_level` from the policy — useful when you want the policy to +clearly state that no row-level restriction applies, or as an admin override +that combines with more restrictive policies via [_OR_ semantics][ref-dap-rls]. + +A `row_level` block must contain either `filters` or `allow_all` — the two are +mutually exclusive. `allow_all` only accepts `true`; `allow_all: false` is not a +valid value. To deny a group all rows, don't grant it a matching policy: access +policies are deny-by-default, so any member not granted by a matching policy is +inaccessible. + + + +```yaml title="YAML" +cubes: + - name: orders + # ... + + access_policy: + - group: admin + row_level: + allow_all: true +``` + +```javascript title="JavaScript" +cube(`orders`, { + // ... + + access_policy: [ + { + group: `admin`, + row_level: { + allow_all: true + } + } + ] +}) +``` + + + +#### Conditional `row_level` + +You can make `row_level` conditional so that different filters apply depending +on the [security context][ref-sec-ctx] or user attributes. Attach +[`conditions`](#conditions) to a policy so its `row_level` is skipped when the +conditions don't match, and define separate policies for the different filters +you want to apply. In JavaScript, you can additionally compute a filter's +`member` or `values` from `securityContext` at evaluation time (for example, +with a ternary), which lets a single policy vary its filter per request. When a +policy's `row_level` is skipped, other matching policies still apply; if no +policy matches, access is denied by default. See the [conditional row-level +access recipe][ref-recipe-conditional-row-level] for worked examples. + Note that access policies also respect [row-level security][ref-rls] restrictions configured via the `query_rewrite` configuration option. See [row-level access][ref-dap-rls] to learn more about policy evaluation. @@ -513,4 +629,5 @@ cube(`orders`, { [ref-mask-dim]: /reference/data-modeling/dimensions#mask [ref-rest-query-filters]: /reference/rest-api/query-format#filters-format [ref-rest-query-ops]: /reference/rest-api/query-format#filters-operators -[ref-rest-boolean-ops]: /reference/rest-api/query-format#boolean-logical-operators \ No newline at end of file +[ref-rest-boolean-ops]: /reference/rest-api/query-format#boolean-logical-operators +[ref-recipe-conditional-row-level]: /recipes/access-control/conditional-row-level-access \ No newline at end of file