Multi-tenant isolation¶
Two tenants share one database. One row filter limits each API key to the rows of its tenant.
Example¶
Start the server with examples/multi-tenant/serve.sh. Each tenant has a subject binding. For API keys, the claims of a binding are fixed values.
# One binding per tenant. The tenantId claim feeds the row filter.
apiVersion: sluice.bino.bi/v1alpha1
kind: SubjectBinding
metadata:
name: tenant-acme
spec:
claims:
subjectId: "acme-analyst"
tenantId: "acme"
apiKeys:
- id: "sl_demo_acme"
hashRef: "secret://env/SLUICE_APIKEY_ACME_HASH"
groups: ["analytics"]
---
apiVersion: sluice.bino.bi/v1alpha1
kind: SubjectBinding
metadata:
name: tenant-beta
spec:
claims:
subjectId: "beta-analyst"
tenantId: "beta"
apiKeys:
- id: "sl_demo_beta"
hashRef: "secret://env/SLUICE_APIKEY_BETA_HASH"
groups: ["analytics"]
The row filter compares tenant_id with the tenantId claim.
apiVersion: sluice.bino.bi/v1alpha1
kind: RowFilterPolicy
metadata:
name: filter-tenant
priority: 80
spec:
match:
any:
- resources:
catalogs: ["shop"]
schemas: ["main"]
tables: ["customers", "orders"]
combine: restrictive
filter:
predicate:
column: tenant_id
op: Equals
value: "{{ subject.tenantId }}"
The acme key reads the customers.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_acme.supersecret" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT id, tenant_id, email FROM shop.main.customers ORDER BY id"}'
{
"query_id": "<query-id>",
"columns": ["id", "tenant_id", "email"],
"rows": [
[1, "acme", "a1@acme.example"],
[2, "acme", "a2@acme.example"]
],
"row_count": 2,
"truncated": false
}
The beta key sends the same SQL.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_beta.supersecret" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT id, tenant_id, email FROM shop.main.customers ORDER BY id"}'
{
"query_id": "<query-id>",
"columns": ["id", "tenant_id", "email"],
"rows": [
[3, "beta", "b1@beta.example"],
[4, "beta", "b2@beta.example"]
],
"row_count": 2,
"truncated": false
}
What happened: Sluice put the filter around the table and bound the claim value as parameter $1. The data also holds a tenant gamma. No binding has that claim, so no client reads those rows.
This suite case shows the rewritten SQL. sluice policy test policies.d --tests tests runs the suite without a server.
- name: acme reads only acme customers
identity: { subject: acme-analyst, groups: [analytics], claims: { tenantId: acme } }
sql: "SELECT id, email FROM shop.main.customers"
expect:
outcome: allow
filters: ["shop.main.customers"]
rewrittenSql: >-
SELECT id, email
FROM (SELECT * FROM shop.main.customers sluice_src WHERE sluice_src.tenant_id = $1) customers
More examples¶
A join filters both tables.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_acme.supersecret" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT c.email, o.id, o.amount_cents FROM shop.main.customers c JOIN shop.main.orders o ON o.customer_id = c.id ORDER BY o.id"}'
{
"query_id": "<query-id>",
"columns": ["email", "id", "amount_cents"],
"rows": [
["a1@acme.example", 10, 1999],
["a2@acme.example", 11, 4599]
],
"row_count": 2,
"truncated": false
}
A subquery is filtered too. The table holds five orders. The acme key counts two.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_acme.supersecret" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT (SELECT count(*) FROM shop.main.orders) AS orders_visible"}'
{
"query_id": "<query-id>",
"columns": ["orders_visible"],
"rows": [
[2]
],
"row_count": 1,
"truncated": false
}
A WHERE clause cannot widen the filter. The acme key asks for beta rows and gets none.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_acme.supersecret" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{"sql": "SELECT id, tenant_id FROM shop.main.orders WHERE tenant_id = 'beta'"}
EOF
{
"query_id": "<query-id>",
"columns": ["id", "tenant_id"],
"rows": [],
"row_count": 0,
"truncated": false
}
Limits¶
- A subject without the
tenantIdclaim getsACL_DENIEDand no rows.
See also¶
- Row filters
- Subjects: API keys and claims
- Testing policies