Role-Based Access Control¶
TDB Enterprise enforces a three-tier role system on every DB-managed API key. Roles are checked on every request; a denied request returns HTTP 403 with a clear message.
Roles¶
| Role | Can query | Can register/delete sources | Can manage API keys | Can verify audit log |
|---|---|---|---|---|
read |
✅ | ❌ | ❌ | ❌ |
readwrite |
✅ | ✅ | ❌ | ❌ |
admin |
✅ | ✅ | ✅ | ✅ |
Role sources:
| Key type | Role |
|---|---|
Static env keys (TDB_API_KEYS) |
Always admin. With tenants on (from 0.33.0), the operator: manages tenants and reads none |
| JWT tokens | role claim in the JWT payload (default: read from 0.11.0; admin before) |
| DB-managed keys | role column in the registry database (a key created without a role is read from 0.11.0; admin before) |
Endpoint access matrix¶
| Endpoint | Minimum role |
|---|---|
GET / |
None (public — version banner) |
GET /health |
None (public) |
GET /metrics |
None (public — restrict via network policy in production) |
GET /v1/version |
any valid key |
GET /v1/sources |
read |
GET /v1/sources/{id} |
read |
GET /v1/sources/{id}/schema |
read |
GET /v1/sources/{ref}/annotations |
read |
POST /v1/query |
read |
POST /v1/query/stream |
read |
POST /v1/mcp |
read |
GET /v1/views |
read |
GET /v1/views/{name} |
read |
POST /v1/views/{name}/run |
read |
GET /v1/approvals, GET /v1/approvals/{id} |
any valid key or SSO token that names someone (Mid-Market, from 0.32.0): shows only requests you made or may decide |
POST /v1/approvals/{id}/approve, /deny |
one of the source's approvers in the policy file, never the requester; not a role |
POST /v1/approvals/{id}/run |
the requester only |
POST /v1/sources |
readwrite |
DELETE /v1/sources/{id} |
readwrite |
PUT /v1/sources/{ref}/annotations |
readwrite |
DELETE /v1/sources/{ref}/annotations |
readwrite |
POST /v1/auth/keys |
admin |
GET /v1/auth/keys |
admin |
DELETE /v1/auth/keys/{id} |
admin |
POST /v1/auth/keys/{id}/rotate |
admin |
PUT /v1/auth/keys/{id}/rate-limit |
admin |
PUT /v1/auth/keys/{id}/role |
admin |
PUT /v1/auth/keys/{id}/tools |
admin |
PUT /v1/auth/keys/{id}/lean-mode |
admin |
PUT /v1/auth/keys/{id}/attributes |
admin |
GET /v1/policies |
admin |
GET /v1/sources/{ref}/policy-suggestions |
admin |
GET /v1/audit/verify |
admin |
GET /v1/audit/history |
admin |
POST /v1/audit/rotate |
admin |
POST /v1/audit/export |
admin |
GET /v1/audit/export/status |
admin |
Grouped by minimum role rather than by path, since the question this table answers is "what can a key with role X do".
Column and row policies narrow this further
A role says which endpoints a key may call; a policy (Mid-Market,
from 0.22.0) says which columns and rows its queries see. A non-admin key that a
policy restricts also cannot register or delete sources (403,
policy_registry_change): a copy of a source under a new name would carry no rule.
Rate limiting is checked before the role
A key that is over its rate limit gets 429 on any endpoint, including one its role would not have allowed anyway. A 429 therefore does not mean the role was sufficient.
Creating keys with a specific role¶
# Read-only key for a dashboard or BI tool
curl -X POST http://localhost:8000/v1/auth/keys \
-H "Authorization: Bearer <ADMIN_KEY>" \
-H "Content-Type: application/json" \
-d '{"name": "dashboard", "role": "read"}'
# Readwrite key for a pipeline that registers sources
curl -X POST http://localhost:8000/v1/auth/keys \
-H "Authorization: Bearer <ADMIN_KEY>" \
-H "Content-Type: application/json" \
-d '{"name": "etl-pipeline", "role": "readwrite"}'
Changing a key's role¶
Role changes take effect immediately.
curl -X PUT http://localhost:8000/v1/auth/keys/<KEY_ID>/role \
-H "Authorization: Bearer <ADMIN_KEY>" \
-H "Content-Type: application/json" \
-d '{"role": "readwrite"}'
Valid values: "read", "readwrite", "admin". Invalid values return HTTP 422.
Restricting tool access¶
Beyond the role system, you can restrict which MCP tools a key can call. This is useful when you want a key that can query data but cannot introspect schema, or cannot run aggregate queries — or a key for an agent that may only run the views you defined.
# Allow only query_source and schema_source
curl -X PUT http://localhost:8000/v1/auth/keys/<KEY_ID>/tools \
-H "Authorization: Bearer <ADMIN_KEY>" \
-H "Content-Type: application/json" \
-d '{"tools": ["query_source", "schema_source"]}'
# Remove restriction (allow all tools)
curl -X PUT http://localhost:8000/v1/auth/keys/<KEY_ID>/tools \
-H "Authorization: Bearer <ADMIN_KEY>" \
-H "Content-Type: application/json" \
-d '{"tools": null}'
Available MCP tools: query_source, schema_source, preview_source, filter_source, aggregate_source, list_views, run_view, and from 0.32.0 run_approved. A scope that names any query tool may also use run_approved, which runs only what a person approved for that key; you need not list it.
The scope also governs the REST routes that do the same thing, from enterprise
0.12.0. Earlier releases checked it only on MCP, so a key scoped to
["run_view"] could still send any SQL to POST /v1/query.
| REST route | Requires tool |
|---|---|
POST /v1/query, POST /v1/query/stream (from 0.31.0) |
query_source |
GET /v1/sources/{ref}/schema |
schema_source |
GET /v1/views, GET /v1/views/{name} |
list_views |
POST /v1/views/{name}/run |
run_view |
POST /v1/approvals/{id}/run (from 0.32.0) |
run_approved, or any query tool |
A REST request outside the scope returns 403 and is audited as
tool_not_permitted_<tool>. Routes with no MCP equivalent (source and key
management, audit) are governed by the role alone.
Note
Tool-level restrictions only apply to DB-managed keys. Static env keys and JWT tokens are never tool-restricted.
Error responses¶
| Scenario | HTTP status | Detail |
|---|---|---|
| No credentials | 401 | Invalid or missing credentials |
| Invalid token | 401 | Invalid or expired token |
| Valid key, insufficient role | 403 | Insufficient privileges. Required: 'readwrite', have: 'read'. |
| Valid key, tool not allowed (MCP) | 200 (tool-error) | Tool 'schema_source' is not permitted for this API key. |
| Valid key, tool not allowed (REST, from 0.12.0) | 403 | This API key's allowed_tools does not include 'query_source', which this endpoint requires. |
MCP tool-access denials return HTTP 200 with isError: true in the JSON-RPC result, not a protocol-level error, so MCP clients handle them as a failed tool call rather than a connection failure.
Recommended setup for production¶
# 1. Bootstrap key — used only to create other keys, then locked away
TDB_API_KEYS=your-strong-bootstrap-key
# 2. Create a read-only key for each integration
curl -X POST .../v1/auth/keys -d '{"name":"grafana","role":"read","expires_in_days":365}'
curl -X POST .../v1/auth/keys -d '{"name":"claude-desktop","role":"read"}'
# 3. Create a readwrite key for the team that registers sources
curl -X POST .../v1/auth/keys -d '{"name":"data-platform-team","role":"readwrite"}'
# 4. Restrict the Claude Desktop key to safe MCP tools only
curl -X PUT .../v1/auth/keys/<claude_key_id>/tools \
-d '{"tools":["query_source","schema_source","preview_source"]}'