Skip to content

Policy engines

The engine decides on each query: allow, deny or reject, and which row filters and column masks apply. You select it with policies.engine in the server config.

policies.engine Decides with
yaml (default) The YAML policies in the policy directory
opa Rego modules, run by an embedded Open Policy Agent. See OPA
composite The engines in policies.composite.members, in order

A member of composite is yaml, opa or rebac. The member rebac checks RelationshipPolicy objects against OpenFGA. See ReBAC.

Sluice checks these names at start, and so does sluice config validate. The opa engine and an opa member need policies.opa.moduleDir.

Example

Goal: a YAML policy allows two tables. A Rego module denies a query that reads both.

policies:
  directory: "./policies.d"
  engine: composite
  composite:
    members: ["yaml", "opa"]
  opa:
    moduleDir: "./rego"
apiVersion: sluice.bino.bi/v1alpha1
kind: SqlAccessPolicy
metadata:
  name: allow-analytics
spec:
  effect: allow
  match:
    any:
      - subjects:
          groups: ["analytics"]
        resources:
          catalogs: ["shop"]
          schemas: ["main"]
          tables: ["customers", "payments"]
package sluice

import rego.v1

# No opinion by default. The YAML policies decide.
default main := {"abstain": true}

main := {
    "allow": false,
    "deny_reason": {"message": "customers and payments must not be read together"},
} if {
    reads("shop.main.customers")
    reads("shop.main.payments")
}

reads(key) if {
    some t in input.tables
    t.key == key
}

Sluice reads the rule main of the package sluice. Change this with policies.opa.query.

A query on one table runs:

curl -s http://localhost:8080/v1/query \
  -H "X-Api-Key: sl_demo_ana.supersecret" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT id, name FROM shop.main.customers ORDER BY id"}'
{
  "query_id": "<query-id>",
  "columns": ["id", "name"],
  "rows": [
    [1, "Alice"],
    [2, "Bob"]
  ],
  "row_count": 2,
  "truncated": false
}

A join of both tables is denied:

curl -s http://localhost:8080/v1/query \
  -H "X-Api-Key: sl_demo_ana.supersecret" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT c.name, p.amount_cents FROM shop.main.customers c JOIN shop.main.payments p ON p.customer_id = c.id"}'
{
  "code": "ACL_DENIED",
  "message": "customers and payments must not be read together",
  "query_id": "<query-id>"
}

What happened: for one table the module abstains (gives no opinion), so the YAML policy decides alone. For the join the module denies, and a deny from any member wins.

What each engine can return

Result yaml opa rebac
Allow yes yes no
Deny yes yes yes
Reject yes yes no
Row filters, column masks yes yes no
Row limit, sample, timeout yes no no
Approval yes no no
Abstain (no opinion) no allow matches abstain: true no check fails

A Rego module that returns no result denies the query. An unknown field in its result fails the request with ERR_POLICY_UNAVAILABLE.

How composite merges

  1. An error in one member fails the request with ERR_POLICY_UNAVAILABLE. An OpenFGA server that does not answer within the policy's backend.timeout gives ERR_TIMEOUT.
  2. A deny from any member wins.
  3. Then a reject from any member wins.
  4. One member must allow. An allow from Rego is enough, also for a table that no YAML policy allows. If all members abstain, Sluice denies the query.
  5. On allow, the restrictions of all members apply. Row filters join with AND. For one column, the mask of the first member wins. The lowest row limit and timeout win, and the first sample. Approvals add up.

Limits

  • rebac works only as a member of composite.
  • opa needs policies.opa.moduleDir. Without it the server does not start.
  • The rewrite cache (cache.rewrite.enabled) works only with yaml.
  • sluice policy test and sluice policy explain use the YAML engine only. GET /admin/subjects/explain and the MCP tool explain_access use the configured engine.

See also