Approval workflow¶
An ApprovalPolicy holds a query on sensitive columns until a person accepts it.
Example¶
Start the server with examples/approval-workflow/serve.sh. It also starts webhook.py, which stands in for the approver's inbox.
The policy holds a query that names ssn or a salary* column, or that filters on country = 'de'.
apiVersion: sluice.bino.bi/v1alpha1
kind: ApprovalPolicy
metadata: { name: approve-pii-reads, priority: 100 }
spec:
match:
any:
- resources: { tables: [people] }
when:
columnsAccessed: ["ssn", "salary*"]
predicates:
- { column: "country", op: "=", value: "de" }
reason: "PII columns on people need the approval of a data steward"
The server config needs the approval block. Sluice does not start without publicBaseUrl.
approval:
# The accept and reject links in the webhook start with this URL.
publicBaseUrl: "http://localhost:8080"
# A held query waits this long for a decision. Then it answers ERR_APPROVAL_PENDING.
syncWait: 1s
# An approval without a decision expires after this time.
requestTtl: 15m
# After the accept, the query must run within this time.
grantTtl: 5m
webhooks:
- url: "http://localhost:8090/"
Alice asks for ssn. Sluice holds the query.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_alice.world" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT name, ssn FROM shop.main.people ORDER BY id"}'
{
"code": "ERR_APPROVAL_PENDING",
"message": "human approval required; a decision has been requested. Poll or await the approval, then re-run the IDENTICAL query (execute_sql or sample_table call) within the grant window to execute.",
"query_id": "<query-id>",
"details": {
"approval_id": "<approval-id>",
"expires_at": "<time>"
}
}
webhook.py stored the webhook that Sluice sent.
{
"approval_id": "<approval-id>",
"subject": {
"id": "alice",
"issuer": "analysts",
"groups": ["analysts"]
},
"sql": "SELECT name, ssn FROM shop.main.people ORDER BY id",
"reasons": ["policy approve-pii-reads: PII columns on people need the approval of a data steward"],
"policies": ["approve-pii-reads"],
"accept_url": "<accept-url>",
"reject_url": "<reject-url>",
"requested_at": "<time>",
"expires_at": "<time>"
}
The approver opens the accept link and confirms on the page. The step sends the POST of that page.
Alice sends the same query again. Now it runs.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_alice.world" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT name, ssn FROM shop.main.people ORDER BY id"}'
{
"query_id": "<query-id>",
"columns": ["name", "ssn"],
"rows": [
["Anna", "111-22-3333"],
["Bernd", "222-33-4444"],
["Chloe", "333-44-5555"]
],
"row_count": 3,
"truncated": false
}
She sends it a third time. Sluice holds it again.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_alice.world" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT name, ssn FROM shop.main.people ORDER BY id"}'
{
"code": "ERR_APPROVAL_PENDING",
"message": "human approval required; a decision has been requested. Poll or await the approval, then re-run the IDENTICAL query (execute_sql or sample_table call) within the grant window to execute.",
"query_id": "<query-id>",
"details": {
"approval_id": "<approval-id>",
"expires_at": "<time>"
}
}
What happened: The accept created a grant for one run of the identical query. The second run used it up.
More examples¶
The approver opens the reject link of the new approval and confirms.
Alice reads the state of her approval with her own key.
curl -s "http://localhost:8080/v1/approvals/$(cat data/approval_id)" \
-H "X-Api-Key: sl_demo_alice.world"
Limits¶
SELECT *on the table needs approval too. Sluice cannot tell which columns a star reads.- The token in the link is the only check. Anyone who has the link can accept.
See also¶
- Approvals
- MCP tools:
check_approvalandawait_approvalfor agents - Break-glass access