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¶
- An error in one member fails the request with
ERR_POLICY_UNAVAILABLE. An OpenFGA server that does not answer within the policy'sbackend.timeoutgivesERR_TIMEOUT. - A deny from any member wins.
- Then a reject from any member wins.
- 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.
- 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¶
rebacworks only as a member ofcomposite.opaneedspolicies.opa.moduleDir. Without it the server does not start.- The rewrite cache (
cache.rewrite.enabled) works only withyaml. sluice policy testandsluice policy explainuse the YAML engine only.GET /admin/subjects/explainand the MCP toolexplain_accessuse the configured engine.