Skip to content

Conditions

spec.conditions holds CEL expressions. A policy applies only when its selector matches and every condition is true.

Example

Only subjects with the claim tier: gold read reports.

apiVersion: sluice.bino.bi/v1alpha1
kind: SqlAccessPolicy
metadata:
  name: allow-reports-gold
  priority: 100
spec:
  effect: allow
  match:
    any:
      - resources:
          catalogs: ["shop"]
          schemas: ["main"]
          tables: ["reports"]
  conditions:
    # The "in" test guards the lookup: a missing claim gives false, not an error.
    - name: gold-tier
      expression: "'tier' in subject.claims && subject.claims.tier == 'gold'"

The suite:

cases:
  - name: the gold tier reads reports
    identity: { subject: gina, claims: { tier: gold } }
    sql: "SELECT id FROM shop.main.reports"
    expect:
      outcome: allow

  - name: another tier is denied
    identity: { subject: sam, claims: { tier: silver } }
    sql: "SELECT id FROM shop.main.reports"
    expect:
      outcome: deny

  - name: a subject without the claim is denied, with no error
    identity: { subject: nora }
    sql: "SELECT id FROM shop.main.reports"
    expect:
      outcome: deny
sluice policy test policies.d --tests tests/claim.yaml
PASS  the gold tier reads reports
PASS  another tier is denied
PASS  a subject without the claim is denied, with no error

3 passed, 0 failed, 3 total

What happened: only the first subject passes the condition. No policy allows the other two subjects, so Sluice denies them.

Fields

Field Type Default Meaning
conditions[].name string none Shown in error messages.
conditions[].expression string required CEL expression that returns a boolean.

Sluice checks every expression at load. A value alone, such as query.has_cte, is a load error. Write query.has_cte == true.

Variables

Variable Type Value
subject.id, subject.email, subject.issuer string Id, email and issuer of the subject.
subject.groups list Groups of the subject.
subject.claims map Claims of the subject.
subject.auth_method string jwt or api_key.
request.remote_ip, request.user_agent string IP address and user agent of the client. Empty over stdio.
request.headers map Request headers, names in lower case: request.headers["x-region"]. Credential headers are left out.
query.tables list Tables as catalog.schema.table, lower case. A missing part stays empty: ..orders.
query.catalogs list Catalogs that the query reads.
query.joins int Count of JOIN keywords and of commas between FROM items.
query.has_limit, query.limit boolean, int The query has a LIMIT, and its value, or 0 when that is not a plain number, such as 500 + 1 or '500'.
query.has_where, query.where_columns boolean, list The query has a WHERE, and its columns.
query.is_aggregate, query.group_by_columns boolean, list The query aggregates, and its GROUP BY columns.
query.has_select_star, query.has_order_by, query.has_union boolean The query has *, ORDER BY, or a UNION, INTERSECT or EXCEPT.
query.has_cte, query.is_recursive_cte boolean The query has WITH or WITH RECURSIVE.

The rules of a QueryRejectPolicy use the same variables. That page says more about the query values.

More examples

Check the query

This allow needs a LIMIT and a query.limit of at most 100.

apiVersion: sluice.bino.bi/v1alpha1
kind: SqlAccessPolicy
metadata:
  name: allow-orders-small
  priority: 100
spec:
  effect: allow
  match:
    any:
      - subjects:
          groups: ["interns"]
        resources:
          catalogs: ["shop"]
          schemas: ["main"]
          tables: ["orders"]
  conditions:
    - name: small-result
      expression: "query.has_limit == true && query.limit <= 100"
- name: a query with a small limit is allowed
  identity: { subject: ines, groups: [interns] }
  sql: "SELECT id FROM shop.main.orders LIMIT 50"
  expect:
    outcome: allow

- name: without a limit the policy does not apply
  identity: { subject: ines, groups: [interns] }
  sql: "SELECT id FROM shop.main.orders"
  expect:
    outcome: deny

- name: a limit that is not a constant counts as 0
  identity: { subject: ines, groups: [interns] }
  sql: "SELECT id FROM shop.main.orders LIMIT 500 + 1"
  expect:
    outcome: allow

- name: a limit inside one SELECT of a union does not count
  identity: { subject: ines, groups: [interns] }
  sql: "(SELECT id FROM shop.main.orders LIMIT 50) UNION ALL SELECT id FROM shop.main.orders"
  expect:
    outcome: deny

A LIMIT that is not a plain number still passes, so this is not a row limit. For that, use a QueryRewritePolicy.

A condition that fails

A lookup of a missing claim is an error, not false. Sluice then denies the whole query with ACL_DENIED, also when another policy allows the table.

apiVersion: sluice.bino.bi/v1alpha1
kind: ColumnMaskPolicy
metadata:
  name: mask-card-number
  priority: 50
spec:
  match:
    any:
      - resources:
          catalogs: ["shop"]
          schemas: ["main"]
          tables: ["payments"]
          columns: ["card_number"]
  conditions:
    # No guard: this lookup fails for a subject without the claim.
    - name: no-high-clearance
      expression: "subject.claims.clearance != 'high'"
  mask:
    type: "null"
- name: with the claim the condition decides
  identity: { subject: carl, groups: [analysts], claims: { clearance: low } }
  sql: "SELECT id, card_number FROM shop.main.payments"
  expect:
    outcome: allow
    masks: ["shop.main.payments.card_number=null"]

- name: without the claim the condition fails and the query is denied
  identity: { subject: bob, groups: [analysts] }
  sql: "SELECT id, card_number FROM shop.main.payments"
  expect:
    outcome: deny
    errorCode: ACL_DENIED

sluice policy explain shows the reason:

sluice policy explain --policies-dir policies.d \
  --user bob --groups analysts \
  --sql "SELECT id, card_number FROM shop.main.payments"
subject : bob
resource: shop.main.payments
decision: deny
Kind             Name  Priority  Effect
----             ----  --------  ------
SqlAccessPolicy        -         rejected: policy condition evaluation failed: condition "no-high-clearance": no such key: clearance

Limits

  • No variable holds the current time.
  • Most query.* values do not read subqueries. See Rejects and rewrites.
  • The client sets its headers and its user agent. Trust them only when a proxy that you control sets them.
  • Only queries carry request.* values. The MCP tools that list or describe tables, explain_access and the admin API see empty values. So a condition on request.* can hide a table from the lists that a query may read.
  • A row filter has its own CEL form. See Row filters.

See also