Skip to main content

Authorization planes

Authentication says who you are (Authentication); these three separate decisions say what you may do. They're kept as distinct interfaces on purpose, so one plane's policy can never accidentally leak into another's.

1. Publish plane (S2S)

"Is this app credential allowed to emit for this tenant?"

AuthorizePublish(ctx, PublishRequest{TenantID, AppID, EventType, TargetSubjectIDs}) (bool, error)

Enforced only on POST /v1/events, and only when the caller authenticated as an app/publisher (a service-JWT) — never for an end-user caller. Checked against the app's registered allowed_event_types (see Apps & credentials); denial is 403.

2. Subscribe/read plane (end user)

"Is this subject allowed this channel/scope?" Server-enforced, always — a client can never widen its own read access by editing a query parameter. A requested scope is only allowed if it resolves to the caller's own subject_id, or a group the caller has been explicitly granted.

AuthorizeSubscribe(ctx, SubscribeRequest{TenantID, SubjectID, Scope}) (bool, error)

Used on GET /v1/feed and GET /v1/stream. There are, deliberately, two call sites shaped like this one:

  • core/authz.SubscribeAuthorizer — CNS's own HTTP-API-level check.
  • adapters/realtime.Realtime.AuthorizeSubscribe — the realtime provider's own subscribe callback (e.g. Centrifugo's webhook).

Both must agree — cmd/cns-server wires the identical authorizer instance into both call sites specifically so they can't drift apart. See Realtime & streaming for why GET /v1/stream runs both checks and what that means for a noop realtime configuration.

3. Admin plane

A third plane, entirely separate from the two above — registering apps, editing policy, managing RBAC, and reading tenant-wide data all go through it. Two roles, Phase 1 keeps them intentionally thin:

RoleScope
platform-operatorCross-tenant (Amphoze). Superset of tenant-admin for every tenant — with one exception, below.
tenant-adminThe caller's own tenant only.

The PII gate — the one place the superset rule does not apply

Three routes expose per-subject employee data — the activity timeline, the audit log, and the per-subject feed inspector. These require an explicit tenant-admin grant; holding platform-operator alone is not sufficient and gets 403. The rationale is explicit in the design: operating the platform is not the same permission as reading a specific tenant's employee PII. A platform operator still retains non-PII aggregate analytics.

Bootstrapping

The Admin API's own RBAC routes are role-gated — so nothing could grant the very first role through the API itself. CNS_ADMIN_BOOTSTRAP_SUBJECT grants platform-operator to one subject at server boot, specifically to break that deadlock. There's no other out-of-band bootstrap path yet (an operator CLI or seeded migration would replace this) — flagged as a Phase-1 limitation.

Granting platform-operator requires already holding it

Granting or revoking the global platform-operator role additionally requires the calling subject to already hold it — checked inside the handler, not just the route's minimum role — so a tenant-admin can reach the RBAC routes (to manage its own tenant's tenant-admin/group grants) without being able to mint a platform operator.

Full route-by-route detail: API Reference → Admin API.