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:
| Role | Scope |
|---|---|
platform-operator | Cross-tenant (Amphoze). Superset of tenant-admin for every tenant — with one exception, below. |
tenant-admin | The 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.