Skip to main content

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:

  1. Authenticate the principal. The bearer credential identifies a human session or service token. Missing, malformed, expired, or revoked credentials fail with 401 Unauthorized.
  2. 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.
  3. Compute effective permissions. Human permissions come from current tenant / flag-namespace role membership. Service-token permissions come from token type, binding, and optional scopes.
  4. 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.
  5. 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

PrincipalSourcePermission source
anonymousNo valid bearer credentialNo API permissions.
human_sessionTenant login flowCurrent tenant membership, tenant-admin grants, namespace-admin grants, and email-domain tenant rules.
namespace-read tokenexd_read_...Token type plus bound flag namespace.
namespace-write tokenexd_write_...Token type, bound flag namespace, and optional scopes.
namespace-client tokenexd_client_...Token type, bound flag namespace, bound environment, and Origin allowlist. Public-by-design; see public evaluation.
tenant-admin tokenexd_tenant_...Token type, bound tenant, and optional scopes.
superadmin tokenexd_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

ResourceIdentifierParent
Installationsingletonnone
Tenanttenant_sluginstallation
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 recordtoken_idtenant, flag namespace, or installation depending on token binding
Snapshotoptional tenant filtertenant 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

PermissionResourceMeaning
tenant.createinstallationCreate a tenant.
tenant.readtenantRead tenant metadata.
tenant.admin.managetenantGrant or revoke tenant-admin membership.
namespace.createtenantCreate a flag namespace in the tenant.
namespace.readnamespaceRead flag-namespace metadata and discover the namespace.
namespace.deletenamespaceDelete the flag namespace.
namespace.admin.readnamespaceList explicit namespace admins.
namespace.admin.managenamespaceGrant or revoke namespace-admin membership.
manifest.readnamespaceDownload current or historical manifest data.
manifest.writenamespaceUpload, lint, or rollback manifests.
evaluatenamespaceCall server-side evaluation endpoints in any environment of the flag namespace.
evaluate.publicnamespace / environmentCall 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.tenanttenantDownload a snapshot limited to one tenant.
snapshot.read.globalinstallationDownload a snapshot across all tenants.
token.readtoken recordRead service-token metadata.
token.create.namespacenamespaceCreate namespace-bound service tokens.
token.create.tenanttenantCreate tenant-admin service tokens.
token.create.superadmininstallationCreate superadmin service tokens.
token.rotatetoken recordCreate a replacement for an existing service token.
token.revoketoken recordRevoke 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:

PermissionScope
tenant.readTenants 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 typePermissions
namespace-readnamespace.read, manifest.read, evaluate for the bound flag namespace.
namespace-writenamespace.read, manifest.read, manifest.write, evaluate for the bound flag namespace.
namespace-clientevaluate.public for the bound (namespace, environment). No other permissions. Cannot read manifests, list versions, lint, or call any admin endpoint.
tenant-admintenant.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.
superadminAll 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:

  1. The token is active (not expired, not revoked).
  2. The bound (tenant_slug, namespace_slug) matches the request path.
  3. The bound environment_slug matches the environment field of the evaluation request body. If the body omits environment, the bound environment is used and no check is needed; if the body specifies a different environment, the request is rejected with 403 Forbidden.
  4. The current manifest declares [namespace.environments.<env>].public_evaluate = true for the bound environment.
  5. The requested operation is evaluate.public (i.e., POST /api/v1/tenants/{tenant}/namespaces/{namespace}/evaluate or .../evaluate/all). Any other endpoint returns 403 Forbidden.
  6. If the request carries an Origin header (i.e., it is a browser fetch), the Origin value matches an entry in the token's allowed_origins list. Native callers that do not send Origin are 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:

  1. Bearer auth — same manifest.read permission used for GET /api/v1/tenants/{tenant}/namespaces/{slug}/manifest. The bearer principal's tenant binding must match {tenant}. Operators and ad-hoc tooling use this path.
  2. Signed token query parameter — issued by the SSE handler at the moment a delivery: snapshot or event: resync payload 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: false
  • Access-Control-Allow-Methods: POST, OPTIONS
  • Access-Control-Allow-Headers: Authorization, Content-Type, X-Exd-Manifest-Version
  • Access-Control-Max-Age: 600
  • Vary: 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:

OperationRequired permission
Create namespace-read or namespace-write tokentoken.create.namespace on the target flag namespace.
Create tenant-admin tokentoken.create.tenant on the target tenant.
Create superadmin tokentoken.create.superadmin on the installation.
List token metadatatoken.read on each returned token.
Read one token recordtoken.read on that token.
Rotate a tokentoken.rotate on that token AND permission to create the replacement token type.
Revoke a tokentoken.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:

ConditionResponse
Missing, malformed, expired, or revoked credential401 Unauthorized (unauthorized)
Valid credential, visible resource, missing permission403 Forbidden (forbidden)
Valid credential, resource does not exist404 Not Found with the resource-specific code
Valid credential, resource exists but is outside the principal's tenant / flag-namespace visibility404 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 groupRequired permission
POST /api/v1/tenantstenant.create
GET /api/v1/tenantstenant.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/tokensToken management permissions above
GET /api/v1/tokenstoken.read for returned tokens
GET /api/v1/tokens/{token_id}token.read
POST /api/v1/tokens/{token_id}/rotatetoken.rotate plus permission to create the replacement type
DELETE /api/v1/tokens/{token_id}token.revoke or self-revocation
POST /api/v1/tenants/{tenant}/namespacesnamespace.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}/adminsnamespace.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 callersevaluate.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 filtersnapshot.read.global
GET /api/v1/eventsmanifest.read on every (tenant, slug) resolved from the ?ns= subscription; see event stream
GET /api/v1/tenants/{tenant}/namespaces/{ns}/closuremanifest.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:

CategoryEvents
Tenant administrationTenant create, tenant-admin grant, tenant-admin revoke.
Namespace administrationNamespace create / delete, namespace-admin grant / revoke.
Manifest changesUpload, rollback.
Token lifecycleCreate, rotate, revoke, expire, successful authentication (see tokens § Audit events).
SnapshotsTenant 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