Access control
The exd authorization model: principals, resources, permission vocabulary, role expansion, service-token expansion, public-evaluation gating, error semantics, and audit requirements.
Token format, lifecycle, and the management API live in tokens.
Authorization model
Every request is evaluated as:
- Authenticate the principal. The bearer credential identifies a human session or service token. Missing, malformed, expired, or revoked credentials fail with
401 Unauthorized. - Resolve the target resource. The server resolves tenant, flag namespace, manifest version, token record, or admin membership from the path, query string, or request body.
- Compute effective permissions. Human permissions come from current tenant / flag-namespace role membership. Service-token permissions come from token type, binding, and optional scopes.
- Authorize the action. The request is allowed only if the principal has the required permission on the resolved resource and any additional condition is satisfied.
- Record an audit event for authorization-sensitive actions.
Access is deny-by-default. Permissions are additive; there are no explicit deny rules in v1.
Principals
| Principal | Source | Permission source |
|---|---|---|
anonymous | No valid bearer credential | No API permissions. |
human_session | Tenant login flow | Current tenant membership, tenant-admin grants, namespace-admin grants, and email-domain tenant rules. |
namespace-read token | exd_read_... | Token type plus bound flag namespace. |
namespace-write token | exd_write_... | Token type, bound flag namespace, and optional scopes. |
namespace-client token | exd_client_... | Token type, bound flag namespace, bound environment, and Origin allowlist. Public-by-design; see public evaluation. |
tenant-admin token | exd_tenant_... | Token type, bound tenant, and optional scopes. |
superadmin token | exd_admin_... | Installation-level token type and optional scopes. |
Service tokens are NOT users. They never inherit human roles and are never added to admin membership lists.
Resources
| Resource | Identifier | Parent |
|---|---|---|
| Installation | singleton | none |
| Tenant | tenant_slug | installation |
| Flag namespace | (tenant_slug, namespace_slug) | tenant |
| Manifest version | (tenant_slug, namespace_slug, version) | flag namespace |
| Namespace admin membership | (tenant_slug, namespace_slug, user_id) | flag namespace |
| Tenant admin membership | (tenant_slug, user_id) | tenant |
| Service-token record | token_id | tenant, flag namespace, or installation depending on token binding |
| Snapshot | optional tenant filter | tenant or installation |
Namespace slugs are unique per-tenant, not globally — two tenants may both own a payments flag namespace. Every namespace identifier therefore carries the owning tenant; the URL shape /api/v1/tenants/{tenant}/namespaces/{ns}/... makes the binding explicit. Access checks resolve (tenant, namespace) jointly and verify the principal has access to that pair.
Permission vocabulary
| Permission | Resource | Meaning |
|---|---|---|
tenant.create | installation | Create a tenant. |
tenant.read | tenant | Read tenant metadata. |
tenant.admin.manage | tenant | Grant or revoke tenant-admin membership. |
namespace.create | tenant | Create a flag namespace in the tenant. |
namespace.read | namespace | Read flag-namespace metadata and discover the namespace. |
namespace.delete | namespace | Delete the flag namespace. |
namespace.admin.read | namespace | List explicit namespace admins. |
namespace.admin.manage | namespace | Grant or revoke namespace-admin membership. |
manifest.read | namespace | Download current or historical manifest data. |
manifest.write | namespace | Upload, lint, or rollback manifests. |
evaluate | namespace | Call server-side evaluation endpoints in any environment of the flag namespace. |
evaluate.public | namespace / environment | Call evaluation endpoints under a namespace-client token. Granted only to namespace-client tokens, scoped to the bound (namespace, environment), and gated by public_evaluate = true on the bound environment. |
snapshot.read.tenant | tenant | Download a snapshot limited to one tenant. |
snapshot.read.global | installation | Download a snapshot across all tenants. |
token.read | token record | Read service-token metadata. |
token.create.namespace | namespace | Create namespace-bound service tokens. |
token.create.tenant | tenant | Create tenant-admin service tokens. |
token.create.superadmin | installation | Create superadmin service tokens. |
token.rotate | token record | Create a replacement for an existing service token. |
token.revoke | token record | Revoke a service token. |
Per-endpoint pages use Auth required summaries, but implementation authorizes against this permission vocabulary.
Human role permissions
Tenant member
A human tenant member without an admin role can:
| Permission | Scope |
|---|---|
tenant.read | Tenants where the user is admitted. |
Tenant members do not automatically see all flag namespaces. They see only flag namespaces where they are namespace admins, unless they are tenant admins.
Namespace admin
A namespace_admin receives these permissions for that flag namespace:
| Permission |
|---|
namespace.read |
namespace.admin.read |
namespace.admin.manage |
manifest.read |
manifest.write |
evaluate |
token.read for namespace-bound tokens in that namespace |
token.create.namespace |
token.rotate for namespace-bound tokens in that namespace |
token.revoke for namespace-bound tokens in that namespace |
Namespace admins cannot create or delete flag namespaces. They cannot create tenant-admin or superadmin tokens.
Tenant admin
A tenant_admin receives these permissions for the tenant and every flag namespace in it:
| Permission |
|---|
tenant.read |
tenant.admin.manage |
namespace.create |
namespace.read |
namespace.delete |
namespace.admin.read |
namespace.admin.manage |
manifest.read |
manifest.write |
evaluate |
snapshot.read.tenant |
token.read for tenant-bound and namespace-bound tokens in the tenant |
token.create.namespace |
token.create.tenant |
token.rotate for tenant-bound and namespace-bound tokens in the tenant |
token.revoke for tenant-bound and namespace-bound tokens in the tenant |
For email_domain tenants, every admitted verified user is a tenant admin. For sso tenants, tenant-admin rights come from explicit grants or trusted SSO claims. Effective human authorization is computed on each request, so changes to admin membership or SSO claims take effect without waiting for a session token to expire.
Superadmin human
Superadmin human access is installation configuration, not tenant membership. A superadmin human receives all permissions, including tenant.create, snapshot.read.global, and token.create.superadmin.
Service-token permissions
| Token type | Permissions |
|---|---|
namespace-read | namespace.read, manifest.read, evaluate for the bound flag namespace. |
namespace-write | namespace.read, manifest.read, manifest.write, evaluate for the bound flag namespace. |
namespace-client | evaluate.public for the bound (namespace, environment). No other permissions. Cannot read manifests, list versions, lint, or call any admin endpoint. |
tenant-admin | tenant.read, namespace.create, namespace.read, namespace.delete, namespace.admin.read, namespace.admin.manage, manifest.read, manifest.write, evaluate, snapshot.read.tenant, token.read, token.create.namespace, token.rotate, token.revoke for namespace-bound tokens in the bound tenant. |
superadmin | All permissions for all tenants and flag namespaces. |
The tenant-admin service token intentionally cannot create, rotate, or revoke tenant-admin tokens. Tenant-admin service-token lifecycle is controlled by human tenant admins or superadmins.
namespace-client tokens are categorically excluded from evaluate: a request specifying a flag in an environment other than the token's bound environment is rejected with 403 Forbidden even if the bound environment has public_evaluate = true. This rules out a class of mistakes where a token issued for a production-public mirror could be used to probe staging or development.
Schema 1.0 defines no optional service-token scopes. The scopes field on token records is reserved for future use and MUST be the empty array.
Public evaluation
Public evaluation is the third deployment mode for evaluation traffic alongside server-side (authenticated) evaluation and SDK-side (manifest-distribution) evaluation. It serves untrusted clients — typically browsers — under tokens that are assumed to be public.
A namespace-client token authenticates if and only if all of the following hold:
- The token is
active(not expired, not revoked). - The bound
(tenant_slug, namespace_slug)matches the request path. - The bound
environment_slugmatches theenvironmentfield of the evaluation request body. If the body omitsenvironment, the bound environment is used and no check is needed; if the body specifies a different environment, the request is rejected with403 Forbidden. - The current manifest declares
[namespace.environments.<env>].public_evaluate = truefor the bound environment. - The requested operation is
evaluate.public(i.e.,POST /api/v1/tenants/{tenant}/namespaces/{namespace}/evaluateor.../evaluate/all). Any other endpoint returns403 Forbidden. - If the request carries an
Originheader (i.e., it is a browser fetch), theOriginvalue matches an entry in the token'sallowed_originslist. Native callers that do not sendOriginare not subject to this check; the Origin allowlist is a CORS guard, not an authentication guard.
Failures at steps 1–2 return 401 Unauthorized. Failures at steps 3–6 return 403 Forbidden.
public_evaluate = true is a per-environment manifest declaration; flipping it to false in a subsequent manifest version is sufficient to immediately reject every existing namespace-client token bound to that environment, without requiring per-token revocation. This is the recommended kill switch for incident response.
Event stream
The GET /api/v1/events endpoint streams manifest version changes to subscribed clients (see server-api/events). Authorization is identical to the manifest-download path: the principal must hold manifest.read on every flag namespace listed in the ?ns= subscription. The endpoint is single-tenant per connection; subscription slugs are interpreted relative to the principal's bound tenant (superadmin tokens, which span the installation, must include the tenant inline as ?ns=<tenant>/<slug>:<flag-list>).
Concretely, for each (tenant, slug) resolved from the subscription the server runs the same authorization rules as GET /api/v1/tenants/{tenant}/namespaces/{slug}/manifest and rejects the entire connection if any namespace fails. The check is performed before the stream begins, so a partial-permission caller never sees any events leak through.
namespace-client tokens are categorically excluded — they do not hold manifest.read for any flag namespace. They obtain fresh values via repeated POST /evaluate{,/all} calls instead of subscribing to the stream.
The endpoint does NOT introduce a new permission. manifest.read grants the right to refresh the manifest by any transport — polling GET /manifest with If-None-Match, git fetch, or subscribing to the event stream.
Closure endpoint
The GET /api/v1/tenants/{tenant}/namespaces/{slug}/closure endpoint serves the tar.gz archive that backs delivery: snapshot and event: resync recovery. Two authentication paths are supported; a request must satisfy at least one:
- Bearer auth — same
manifest.readpermission used forGET /api/v1/tenants/{tenant}/namespaces/{slug}/manifest. The bearer principal's tenant binding must match{tenant}. Operators and ad-hoc tooling use this path. - Signed
tokenquery parameter — issued by the SSE handler at the moment adelivery: snapshotorevent: resyncpayload is constructed. The token is an HMAC over(principal_id, tenant, namespace, version, subscription, expires_at)with a 60-second TTL, verified in constant time. The signed-token path lets the SDK reuse a one-shot URL embedded in the SSE payload without round-tripping the bearer credential a second time.
A signed token is always tied to the principal and the (tenant, namespace) pair that originally subscribed; it cannot be replayed by a different token holder, redirected to a different tenant, or widened to a different subscription. If both auth paths are presented and either one fails, the request is rejected — there is no fallback from a malformed signature to bearer auth (this prevents a downgrade attack against the signing key).
namespace-client tokens are categorically excluded from this endpoint, mirroring the event-stream rule.
Untrusted attributes
namespace-client callers control the context.attributes map. The server treats these attributes as untrusted: anyone holding the embedded token can construct a request claiming any attribute value. Manifest authors using public_evaluate = true MUST NOT branch on attributes whose value confers entitlement (e.g., user.is_admin, user.entitlement_tier) unless they accept that any consumer of the public client can observe the variant returned for that attribute. For sensitive flags, route through a server-side namespace-read token instead.
CORS
For requests carrying an Origin header that matches the token's allowed_origins, the server includes:
Access-Control-Allow-Origin: <Origin>(echoed exactly; never*)Access-Control-Allow-Credentials: falseAccess-Control-Allow-Methods: POST, OPTIONSAccess-Control-Allow-Headers: Authorization, Content-Type, X-Exd-Manifest-VersionAccess-Control-Max-Age: 600Vary: Origin
OPTIONS preflight requests are answered without invoking authentication when the path is one of the public-evaluation endpoints; the preflight response is identical regardless of whether a token is present, so a misconfigured browser does not leak whether a token is valid before the actual request.
Token management authorization
Token management is governed by the same permission vocabulary:
| Operation | Required permission |
|---|---|
Create namespace-read or namespace-write token | token.create.namespace on the target flag namespace. |
Create tenant-admin token | token.create.tenant on the target tenant. |
Create superadmin token | token.create.superadmin on the installation. |
| List token metadata | token.read on each returned token. |
| Read one token record | token.read on that token. |
| Rotate a token | token.rotate on that token AND permission to create the replacement token type. |
| Revoke a token | token.revoke on that token. A token may also revoke itself. |
Namespace-bound service tokens cannot create, rotate, list, or revoke tokens other than self-revocation.
Resource visibility and errors
Authentication and authorization errors are distinct:
| Condition | Response |
|---|---|
| Missing, malformed, expired, or revoked credential | 401 Unauthorized (unauthorized) |
| Valid credential, visible resource, missing permission | 403 Forbidden (forbidden) |
| Valid credential, resource does not exist | 404 Not Found with the resource-specific code |
| Valid credential, resource exists but is outside the principal's tenant / flag-namespace visibility | 404 Not Found for normal resource reads; 403 Forbidden is allowed for mutation attempts where the request already proves knowledge of the resource. |
List endpoints filter out resources the principal cannot see. They do not include denied entries with per-item errors.
Endpoint permission summary
| Endpoint group | Required permission |
|---|---|
POST /api/v1/tenants | tenant.create |
GET /api/v1/tenants | tenant.read for returned tenants |
GET /api/v1/tenants/{tenant} | tenant.read |
PUT/DELETE /api/v1/tenants/{tenant}/admins/{user_id} | tenant.admin.manage |
POST /api/v1/tokens | Token management permissions above |
GET /api/v1/tokens | token.read for returned tokens |
GET /api/v1/tokens/{token_id} | token.read |
POST /api/v1/tokens/{token_id}/rotate | token.rotate plus permission to create the replacement type |
DELETE /api/v1/tokens/{token_id} | token.revoke or self-revocation |
POST /api/v1/tenants/{tenant}/namespaces | namespace.create on {tenant} |
GET /api/v1/namespaces (cross-tenant list) | namespace.read for returned flag namespaces. Implicitly filtered to the principal's visible scope; superadmin sees all; optional ?tenant= further narrows. |
GET /api/v1/tenants/{tenant}/namespaces/{namespace} | namespace.read on (tenant, namespace) |
DELETE /api/v1/tenants/{tenant}/namespaces/{namespace} | namespace.delete on (tenant, namespace) |
GET /api/v1/tenants/{tenant}/namespaces/{namespace}/admins | namespace.admin.read on (tenant, namespace) |
PUT/DELETE /api/v1/tenants/{tenant}/namespaces/{namespace}/admins/{user_id} | namespace.admin.manage on (tenant, namespace) |
Manifest upload, lint, rollback (.../manifest...) | manifest.write on (tenant, namespace) |
Manifest download and version history (.../manifest...) | manifest.read on (tenant, namespace) |
Evaluation endpoints, authenticated (POST .../evaluate{,/all}) | evaluate on (tenant, namespace) |
Evaluation endpoints, namespace-client callers | evaluate.public on the bound (tenant, namespace, environment); see public evaluation |
GET /api/v1/manifest/snapshot?tenant=<tenant> | snapshot.read.tenant |
GET /api/v1/manifest/snapshot without tenant filter | snapshot.read.global |
GET /api/v1/events | manifest.read on every (tenant, slug) resolved from the ?ns= subscription; see event stream |
GET /api/v1/tenants/{tenant}/namespaces/{ns}/closure | manifest.read on (tenant, namespace) or a valid signed-token query parameter; see closure endpoint |
Audit requirements
The server records audit events for successful and denied authorization-sensitive operations:
| Category | Events |
|---|---|
| Tenant administration | Tenant create, tenant-admin grant, tenant-admin revoke. |
| Namespace administration | Namespace create / delete, namespace-admin grant / revoke. |
| Manifest changes | Upload, rollback. |
| Token lifecycle | Create, rotate, revoke, expire, successful authentication (see tokens § Audit events). |
| Snapshots | Tenant snapshot and global snapshot downloads. |
Audit entries include actor principal type, actor user id or token id, target resource, permission checked, decision, request id, timestamp, and remote address hash when available. Audit entries must never include service-token secrets or full evaluation-context attributes.
See also
- tokens — credential format, lifecycle, management API.
- server-api — the HTTP surface this authorization model gates.
- core concepts § User and admin roles — definitional overview.