Skip to content

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.

sluice apikey hash --pepper subjects-demo-pepper --id sl_demo_report --material supersecret
620a77cf17a34dd71c24dff75930f8b7333b80894faf65670e52d07bb63a57b1

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>"
}
sluice budget show research-agent --state-dir data/state
subject research-agent: cpu_seconds=0 rows=3

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.subjectId or issuer on the binding. With issuer, each subject has its own limit and counter.
  • Without rateLimit, limits.defaultSubjectRps and limits.defaultSubjectBurst apply.
  • A reload resets all rate limits.
  • Sluice counts usage after a query. One large query can pass the budget.
  • budget.db in budget.stateDir holds one row per UTC day, subject id and issuer. At start, Sluice deletes rows older than budget.retentionDays (default 35).

See also