Subjects, keys and budgets¶
A SubjectBinding maps an API key or a JWT to a subject. It can also set a rate limit and a daily budget. A request without a valid credential gets 401 ERR_UNAUTHORIZED.
Example¶
Goal: a reporting job logs in with an API key.
apiVersion: sluice.bino.bi/v1alpha1
kind: SubjectBinding
metadata:
name: reporting
spec:
claims:
subjectId: "reporting-bot"
apiKeys:
- id: "sl_demo_report"
hashRef: "secret://env/SLUICE_APIKEY_REPORT_HASH"
groups: ["analysts"]
A key is <id>.<secret>. Sluice stores only its HMAC-SHA256 hash, keyed with the pepper in identity.apiKeyPepper. Without the pepper, Sluice accepts no API key.
Store this value where hashRef points. The client sends the key.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_report.supersecret" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT id, country, amount FROM shop.main.orders ORDER BY id"}'
{
"query_id": "<query-id>",
"columns": ["id", "country", "amount"],
"rows": [
[1, "de", 120],
[2, "fr", 80],
[3, "de", 45]
],
"row_count": 3,
"truncated": false
}
What happened: The key became the subject reporting-bot in group analysts. A policy allows that group. Authorization: ApiKey <id>.<secret> works too.
Fields¶
| Field | Type | For a JWT | For an API key |
|---|---|---|---|
issuer | string | Required iss. One binding per issuer. | Issuer of the subject. Default: binding name. |
audience | string | Required aud entry. Empty: no check. | |
hmacSecretRef | string | Secret for HS256, HS384. | |
jwksUrl | string | Keys for RS256, RS384, ES256, ES384. Needs issuer and audience. | |
jwksCacheTtl | duration | Cache time of these keys. Default 10m. | |
clockSkew | duration | Tolerance for exp, nbf and iat. Default 60s. | |
claims.subjectId | string | Path to the id. Default sub. | The id. Default: key id. |
claims.email | string | Path to the email. Default email. | The email. |
claims.groups | string | Path to a list or comma-separated string. Without it, the subject has no groups. | One group, for keys without groups. |
claims.tenantId, claims.allowedRegions | string | Paths to the values for {{ subject.tenantId }}, {{ subject.allowedRegions }}. | The values themselves. |
apiKeys[] | list | id, hashRef, groups per key. | |
rateLimit | object | rps: requests per second. burst: requests at once, default 1. | Same. |
budget | object | rowsPerDay, cpuSecondsPerDay: rows and wall-clock seconds per UTC day. 0: no limit. | Same. |
A path looks like $.realm.roles or tenants[0].
More examples¶
JWT¶
Goal: clients log in with a token from your identity provider.
apiVersion: sluice.bino.bi/v1alpha1
kind: SubjectBinding
metadata:
name: corp-idp
spec:
issuer: "https://idp.example.com"
audience: "sluice"
hmacSecretRef: "secret://env/SLUICE_JWT_SECRET"
claims:
groups: "$.realm.roles"
mint-jwt.py stands in for the identity provider. It signs these claims and adds exp.
{
"iss": "https://idp.example.com",
"aud": "sluice",
"sub": "alice",
"email": "alice@example.com",
"realm": {"roles": ["analysts"]}
}
curl -s http://localhost:8080/v1/query \
-H "Authorization: Bearer $(python3 mint-jwt.py token-claims.json)" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT id, country, amount FROM shop.main.orders ORDER BY id"}'
{
"query_id": "<query-id>",
"columns": ["id", "country", "amount"],
"rows": [
[1, "de", 120],
[2, "fr", 80],
[3, "de", 45]
],
"row_count": 3,
"truncated": false
}
What happened: Sluice checked the signature, iss, aud, exp, nbf and iat. The list at realm.roles became the groups.
A token needs exp. Sluice accepts only the algorithms from the table, never none. A binding maps the subject id, the email, the groups, tenantId and allowedRegions. Templates and CEL read the other claims by their token names. A token with an iat in the future is refused.
Rate limit and budget¶
Goal: after an agent read 3 rows on one day, Sluice refuses its next query.
apiVersion: sluice.bino.bi/v1alpha1
kind: SubjectBinding
metadata:
name: research-agent
spec:
claims:
subjectId: "research-agent"
apiKeys:
- id: "sl_demo_agent"
hashRef: "secret://env/SLUICE_APIKEY_AGENT_HASH"
groups: ["analysts"]
# 2 queries at once, then 1 query every 10 seconds.
rateLimit:
rps: 0.1
burst: 2
budget:
rowsPerDay: 3
Budgets are off until the server config turns them on.
budget:
enabled: true
stateDir: "./data/state"
# The demo writes the counters to disk often. The default is 5s.
flushInterval: 100ms
The agent sends this query three times. The first run returns 3 rows.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_agent.supersecret" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT id, country, amount FROM shop.main.orders ORDER BY id"}'
The second run:
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_agent.supersecret" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT id, country, amount FROM shop.main.orders ORDER BY id"}'
{
"code": "ERR_BUDGET_EXCEEDED",
"message": "daily row budget exhausted",
"query_id": "<query-id>"
}
The third run:
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_agent.supersecret" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT id, country, amount FROM shop.main.orders ORDER BY id"}'
{
"code": "ERR_RATE_LIMITED",
"message": "per-subject rate limit exceeded",
"query_id": "<query-id>"
}
What happened: Both refusals have status 429. Sluice checks the rate limit first, so a query over budget counts for it too.
Limits¶
- A rate limit or budget needs
claims.subjectIdorissueron the binding. Withissuer, each subject has its own limit and counter. - Without
rateLimit,limits.defaultSubjectRpsandlimits.defaultSubjectBurstapply. - A reload resets all rate limits.
- Sluice counts usage after a query. One large query can pass the budget.
budget.dbinbudget.stateDirholds one row per UTC day, subject id and issuer. At start, Sluice deletes rows older thanbudget.retentionDays(default 35).