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..9bf3352bd9a31 --- /dev/null +++ b/docs-mintlify/recipes/access-control/conditional-row-level-access.mdx @@ -0,0 +1,284 @@ +--- +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 'emea' in security_context.groups }" + row_level: + filters: + - member: region + operator: equals + values: ["EMEA"] + + - group: analyst + conditions: + - if: "{ security_context.groups and 'amer' in security_context.groups }" + 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 also express the same pattern by making `row_level` +itself a function of `securityContext` and returning different filters +depending on the caller: + + + +```yaml title="YAML" +cubes: + - name: orders + # ... + + access_policy: + - group: analyst + conditions: + - if: "{ security_context.groups and ('emea' in security_context.groups or 'amer' in security_context.groups) }" + row_level: + filters: + - member: region + operator: equals + values: + - "{ 'EMEA' if 'emea' in security_context.groups else 'AMER' }" +``` + +```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` + ] + } + ] + } + } + ] +}) +``` + + + +### 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 full 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 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.tenure_years >= 2 }" + - if: "{ user_attributes.is_admin or user_attributes.is_owner }" + - if: "{ not (security_context.groups and 'contractors' in security_context.groups) }" + row_level: + allow_all: true +``` + +```javascript title="JavaScript" +cube(`orders`, { + // ... + + access_policy: [ + { + group: `analyst`, + conditions: [ + { if: userAttributes.is_full_time_employee && userAttributes.tenure_years >= 2 }, + { if: userAttributes.is_admin || userAttributes.is_owner }, + { if: !(securityContext.groups && securityContext.groups.includes(`contractors`)) } + ], + row_level: { + allow_all: true + } + } + ] +}) +``` + + + +## 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/access-control/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..5ab02668a91e1 100644 --- a/docs-mintlify/reference/data-modeling/data-access-policies.mdx +++ b/docs-mintlify/reference/data-modeling/data-access-policies.mdx @@ -208,6 +208,61 @@ cube(`orders`, { +#### Boolean logic in `if` expressions + +`if` expressions support full boolean logic so you can compose checks against +the [security context][ref-sec-ctx] and user attributes: + +- In YAML, expressions inside `{ ... }` use Jinja-style operators: `and`, `or`, +`not`, and parentheses for grouping. +- In JavaScript, expressions use native operators: `&&`, `||`, and `!`. +- 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.tenure_years >= 2 }" + - if: "{ user_attributes.is_admin or user_attributes.is_owner }" + - if: "{ not (security_context.groups and 'contractors' in security_context.groups) }" + member_level: + includes: "*" +``` + +```javascript title="JavaScript" +cube(`orders`, { + // ... + + access_policy: [ + { + group: `manager`, + conditions: [ + { if: userAttributes.is_full_time_employee && userAttributes.tenure_years >= 2 }, + { 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 +507,68 @@ 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 the `allow_all` parameter to make the row-level intent of a policy explicit +without listing any filters: + +- `allow_all: true` grants the policy access to all rows. It is equivalent to +omitting `row_level` (or omitting `filters`) and is useful when you want the +policy to clearly state that no row-level restriction applies. +- `allow_all: false` denies the policy access to all rows. Other matching +policies (if any) still apply on top — see [row-level access][ref-dap-rls]. + + + +```yaml title="YAML" +cubes: + - name: orders + # ... + + access_policy: + - group: admin + row_level: + allow_all: true + + - group: guest + row_level: + allow_all: false +``` + +```javascript title="JavaScript" +cube(`orders`, { + // ... + + access_policy: [ + { + group: `admin`, + row_level: { + allow_all: true + } + }, + { + group: `guest`, + row_level: { + allow_all: false + } + } + ] +}) +``` + + + +#### 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. In YAML, attach +[`conditions`](#conditions) to a policy so its `row_level` is skipped when the +conditions don't match. In JavaScript, you can additionally make `row_level` a +function of `securityContext` and return different filters at evaluation time. +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 +630,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