API keys
cdk_ keys, admin-only management routes, scope validation, and runtime authentication.
Intended audience: Stakeholders, Business analysts, Solution architects, Developers, Testers
Learning outcomes by role
Stakeholders
- Explain API keys as long-lived automation credentials with scope risk.
Business analysts
- Write stories for issuance, rotation, and revocation of cdk_ keys.
Solution architects
- Plan storage, hashing, and encryption for keys at rest and in transit.
Developers
- Send X-API-KEY and interpret ApiKeyTokenSession behavior.
Testers
- Validate scope enforcement, org binding, and invalid key paths.
API keys are long-lived secrets for machine-to-machine automation — scripts, CI pipelines, and service integrations that cannot go through the browser OAuth flow. The raw key value is shown exactly once at creation; after that only a hash is stored. At runtime, callers send the key in a single header and the platform resolves it to an ApiKeyTokenSession with the same permission model as interactive users.
Summary for stakeholders
Section titled “Summary for stakeholders”- Risk profile — A leaked key provides persistent access until explicitly revoked. Keys bypass interactive MFA; treat them like passwords, scope them to the minimum permissions needed, and rotate on any suspicion of exposure.
- Lifecycle — Only platform sys-admins create keys. Revocation is immediate once the key row is deleted.
Business analysis
Section titled “Business analysis”- Actors — Platform admins (or delegated automation accounts) create keys; integrations authenticate without an interactive user session.
- Acceptance — Each key carries a
scopeslist ofcadence:*permission strings. Requesting a scope the creating user does not hold is rejected at creation.
Architecture and integration
Section titled “Architecture and integration”
See Security and access for JWT versus API key paths
and cadence.core.middleware_setup registration order in cadence.main.
Prerequisites
Section titled “Prerequisites”sys_adminflag on the caller’s session (enforced byrequire_platform_sys_admin)cadence:system:api_keys:writepermission for create and revokecadence:system:api_keys:readpermission for list
API key fields
Section titled “API key fields”| Field | Present at | Notes |
|---|---|---|
id | Always | UUID — use for revoke |
name | Always | Human-readable label set at creation |
key_prefix | Always | First few chars of the key for identification without exposing the secret |
raw_key | Creation response only | Full cdk_ secret — store immediately, never retrievable again |
scopes | Always | List of cadence:* permission strings the key grants |
expires_at | Always | Optional expiry; null means no expiry |
last_used_at | Always | Best-effort timestamp of last authenticated request |
created_at | Always | Creation timestamp |
is_active | Always | false means the key is soft-revoked and will return 401 |
Managing keys
Section titled “Managing keys”All routes live under POST /api/admin/users/api-keys. They require both sys_admin session flag and the matching system permission — there is no self-service /api/me/api-keys surface.
| Method | Path | Permission | Action |
|---|---|---|---|
POST | /api/admin/users/api-keys | cadence:system:api_keys:write | Create a key; returns raw_key once |
GET | /api/admin/users/api-keys | cadence:system:api_keys:read | List active keys for the caller’s user |
DELETE | /api/admin/users/api-keys/{key_id} | cadence:system:api_keys:write | Revoke a key immediately |
Scope validation
Section titled “Scope validation”When creating a key, each requested scope must satisfy two conditions. First, the scope must appear in the global PERMISSIONS set — requesting an unknown permission returns 422. Second, unless the creating user is sys_admin, the user must already hold that permission either globally or in at least one org — you cannot grant a scope you do not yourself possess.
def validate_scopes(self, scopes: List[str], session) -> None: """Ensure each scope is known and the caller may grant it.""" for s in scopes: if s not in PERMISSIONS: raise ValidationError(f"Unknown scope/permission: {s}") if session.is_sys_admin: continue if session.has_permission(s, None): continue org_ok = any( session.has_permission(s, oid) for oid in session.membership_org_ids ) if not org_ok: raise AuthorizationError(f"Cannot grant scope you do not hold: {s}")At runtime, TenantContextMiddleware further strips any dangerous permissions from the session via sanitize_api_key_flat_scopes — so even if a scope was granted at creation, certain elevated permissions are removed from the effective session.
Using a key at runtime
Section titled “Using a key at runtime”- Send the raw
cdk_secret in theX-API-KEYheader on every request. Do not useAuthorization: Bearer— that header is for JWTs only (see Security and access). AuthenticationMiddlewarehashes the incoming value and looks up the matching row. A missing or revoked row returns401.TenantContextMiddlewarebuilds anApiKeyTokenSessioncarrying the user id, org memberships, and the sanitized scope list. Route-levelroles_allowedchecks run against this session exactly as they would for a JWT session.