Authorization engine
Strataplane 1.0 embeds Cedar in the server for permission decisions. Its public authorization model is structured role bindings, ownership and selectors. Application-owned Cedar policies evaluate that model; Postgres holds its authoritative inputs.
The permissions per API group live in architecture.md §7, the secret delivery rule in §11, and approval requirements in spec-format.md §6. This document specifies the engine integration and its verification.
1. Scope
Cedar runs in process, with no authorization service or runtime network dependency (Rust API, authorization semantics). It takes the permission decision; domain logic computes its inputs:
| Concern | Permission decision | Domain computation |
|---|---|---|
| Management access | Actor may perform an action within a binding's scope | Identity, membership, ownership, selector matching |
| Token restrictions | The granting binding's role is allowed by this credential | Token authentication, expiry and revocation |
| Secret delivery | Agent may receive this secret under the delivery rule | Referenced versions, grant match, request certificate verification |
| Reviews | Human may approve this requirement, subject to author separation | Required teams, digest binding, counting valid approvals |
| Change submission and merge | Actor may change every affected resource | Candidate tree, validation, impact and concurrency checks |
| Rollouts and pins | Actor may perform the requested operation on its targets | Cohorts, pins, guardrails and state transitions |
Cedar returns a permission decision. Approval requirements, rendering and rollout state remain typed domain logic. Database tenant isolation remains independent of the engine.
2. Integration boundary
sp-authz owns the Cedar schema, policy templates, action definitions,
binding projection and decision wrapper. It accepts typed facts and returns
Allow, Deny or EvaluationError. It performs no database or network I/O,
and no other code calls Cedar's authorizer, so the deny-on-error rule
cannot be bypassed.
The domain layer constructs compound checks; sp-store loads their facts
and executes authorized query plans within the caller's transaction.
authenticated request + tenant
-> transaction: load actor, credential restrictions, bindings and targets
-> typed authorization plan
-> entity projection + Cedar -> permission decision
-> parameterized SQL filter -> authorized lists and aggregates
-> domain validation and transition
-> write + audit + commit
The internal entry points are:
| Entry point | Contract |
|---|---|
authorize(actor, action, resource, facts) | One decision with internal policy/binding identifiers |
authorize_all(actor, action, resources, facts) | Every resource must be allowed; errors reject the whole operation |
authorized_filter(actor, action, resource_kind, facts) | Typed query predicate for list/count/group operations |
These are domain entry points used by handlers, workers and agent modules. HTTP middleware alone cannot authorize a change set or rollout because its affected resources are computed after routing.
3. Entities, actions and scoped bindings
3.1 Entity projection
| Entity | Source and use |
|---|---|
User | User plus active tenant membership; parents are the tenant's current teams |
ServiceAccount | User of kind service with its tenant membership; binding subject directly, never through a team |
Team | Tenant team, identified by immutable id |
Agent | Authenticated agent instance; separate from human/service management principals |
Tenant | Resolved tenant identity |
Scope | Virtual entity per role binding, identified by the binding id |
| Domain resource | Host, agent-management resource, spec object, secret, review requirement, chat or other protected object |
| Platform principal/resource | Separate types and policy set for platform operations |
Tenant entity identifiers include tenant id and immutable object id. User projection identifies the tenant membership, while audit attribution retains the underlying user id. Names, hostnames and team slugs are display or lookup values, not permission identities. The assistant uses its user's principal.
The loader supplies the complete entity slice required by the selected application policies, including parent groups and scopes. Schema validation does not replace checks that those referenced entities exist and belong to the resolved tenant. Only the server constructs entities and request context.
3.2 Preserve the role-scope association
Each role_binding produces a template-linked permit with:
- principal slot: its user, team or service account;
- resource slot: the binding's virtual
Scopeentity; - action set: the operations explicitly granted by its role;
- condition: that exact role is allowed by the request credential.
A resource is a member of the binding's scope precisely when the structured scope matches it. Scope memberships are computed for the request, not stored as a host-by-binding table. The binding id is retained as the linked policy id for diagnostics.
Each role has one template, generated from the action catalog: its action set is every action the role grants, and its condition names the role. The operator template for host actions:
permit (
principal in ?principal,
action in [Action::"host.read", Action::"host.operate"],
resource in ?resource
)
when { context.allowedRoles.contains("operator") };
?principal and ?resource are the two supported Cedar template slots;
linking supplies entity identities, not interpolated policy text. See
policy templates.
An operator binding for team A and a viewer binding for team B remain two independent grants. Neither roles nor scopes are flattened into independent sets. Sessions allow all role names through the credential restriction; tokens use their allowlist, or all names when the allowlist is absent. An empty allowlist grants nothing. Membership and bindings still determine which roles the actor actually holds.
3.3 Scope domains and selectors
Tenant scopes cover resources in the resolved tenant. Owner scopes use the resource's authoritative owner: host ownership for agents, spec ownership for spec objects and journey evidence, and the required team for a review requirement. Selector scopes apply to hosts and host-backed agent resources. They do not confer edit rights on spec files. Compound operations check files and impacted hosts separately.
Other resources use their domain access rule from architecture §7, such as
the owner of a chat. Unsupported role/scope/resource combinations confer no
permission. Tenant administration actions require tenant scope. Tenant-wide
data (tenant.read, and rollout.write, the right to start and act on
rollouts and pins at all) is in every scope, so a binding of a role that
grants it confers it whatever its scope; the targets of a rollout or pin are
still authorized host by host.
The existing structured selector AST remains the authoritative language.
Its in-memory evaluator produces scope membership; its SQL compiler produces
the corresponding predicate. Both use the same missing-label, numeric and
glob semantics (spec-format.md §2), and
match host labels only. Binding selectors may not reference owner; an
owner scope expresses team ownership by team id.
The SQL compiler guards numeric casts with CASE, because Postgres does
not keep the order of AND; it compiles globs to anchored regular
expressions without newline-sensitive flags and compares character ranges
by code point (COLLATE "C"); matchLabels compiles to containment, so
it can use the GIN index on host.labels.
Selectors are not translated to Cedar string expressions. In particular,
Cedar's like wildcard is *, whereas Strataplane selectors also support
? and character classes. See
Cedar operators.
4. Policy package and evaluation
One application-owned action catalog defines domain action ids, principal
and resource types, granting roles, applicable scope domains and supported
context conditions. It generates the Cedar action schema, role templates
and the metadata used by authorized query plans. Endpoint coverage checks
require every protected operation to name its action or compound check.
Before the handler runs, one middleware refuses a tenant request whose
actor has no binding granting the operation's action anywhere (for a
compound check: tenant.read), the plan of §5 being empty; the handler
then decides on the resources. A filtered list stays a list of nothing.
Role permissions follow architecture §7; roles have no implicit ordering.
The policy package also contains mandatory restrictions for tenant consistency, credential restrictions and operation-specific requirements. Tenant and platform packages are separate: platform administration does not make a user a tenant administrator. Agent-delivery actions are distinct from management actions, so management roles cannot grant secret delivery.
The package is embedded and versioned with the server. CI and startup run strict schema validation; an invalid package prevents readiness. Requests, context and entities are schema checked before authorization. A failure to load required facts rejects the request.
Cedar normally ignores an individual policy that errors. The wrapper
returns EvaluationError if any evaluated policy reports an error, even if
the Cedar decision is Allow. Only an error-free Allow authorizes an
operation. See Cedar evaluation semantics.
Policy selection links only the bindings whose subject includes the actor, and the projection computes scope membership only for those bindings. Cedar evaluates every linked policy for each decision, so fleet-wide checks need this selection. Mandatory restrictions for the action are always included. The entity loader and policy selection are tested together: omitting a restriction must never be an optimization.
5. Lists, pagination and fleet-wide checks
The structured authorization plan is shared by Cedar projection and SQL filtering. The SQL path compiles the supported application rule model; it does not attempt to translate arbitrary Cedar programs. Changes to a role template or restriction require a corresponding plan representation and equivalence tests.
For an inventory query, the plan restricts to the tenant and combines the scopes of bindings whose subjects include the actor, whose role grants the action and whose role survives the token allowlist. Owner and selector predicates retain their binding association. An empty union is false.
Filtering happens before sorting, pagination, counts and grouping. Cursor requests recompute permissions from current facts; a cursor grants no access. The filter and returned rows use one database snapshot. Bounded result pages are also checked through Cedar before serialization. A disagreement is an evaluation error for the entire response, not a reason to silently remove a row from the page. Aggregate equivalence is covered by database tests because aggregate responses do not enumerate their source rows.
Changes and rollouts require authorization of the entire affected set; for a rollout action that is every agent the rollout can reach, assigned or in a later stage. Checks run in bounded batches through Cedar or, for impact sets beyond a few thousand agents, an equivalent SQL anti-join for unauthorized targets, under a consistent snapshot. They never check only the first page or sample. Authorization is applied to the actual candidate, including its before and after ownership/targeting where those change, so a mutation cannot grant itself permission. The domain layer retains responsibility for revalidating the candidate at its state transition.
The implementation uses stable Cedar APIs. Production list filtering does not depend on the experimental partial/batched evaluation features listed in the Rust API.
6. Workflows, secret delivery and freshness
Review requirements are computed by the approval workflow. Each required
team is an independently authorized review target; any requested
acting_teams must be eligible. Author separation is an explicit
restriction when four eyes applies. Service accounts are ineligible for
human approval actions. A permission decision does not establish that a
review's digest is current or that all required teams have approved; the
workflow checks both.
For secret delivery, the agent module supplies the authenticated instance, the eligible secret/version references, grant match, current quarantine/class status and assurance of this request. Cedar evaluates the delivery rule from architecture §11. Neither stored assurance nor a client's reported labels can substitute for verified request facts. Secret values are never policy inputs, and the delivery decision is taken before decryption and audited with the delivery.
The grant and minAssurance come from the Secret at the latest revision
(architecture.md §11).
Facts used for a decision come from one transaction snapshot. Mutations authorize and write in the same transaction, with their domain locks or optimistic version checks. Concurrent requests follow database transaction ordering; a completed revocation is reflected in newly started authorization transactions. Authorization decisions and mutable entity memberships are not cached across requests in 1.0. Immutable parsed policies may be cached. Token lookup caches do not replace checks of current credential validity.
Long-running operations retain the originating actor and credential restriction, then recheck current access before a privileged transition. Workers never substitute a tenant-wide administrator for that actor. Long-polls and SSE refresh authorization before sending protected data; they do not retain an opening request's permission indefinitely. Tenant lifecycle rules remain those in schema.md.
7. Storage and API contract
Bindings, memberships, owners, labels and credential restrictions remain the authoritative rows in schema.md. Linked Cedar policies and scope entities are deterministic projections. This integration adds no policy store, authorization graph table or durable scope-membership table.
The management API continues to expose structured bindings and existing
domain operations. It carries no client-supplied Cedar entities or trusted
authorization context. Error behavior is specified in
openapi.yaml; detailed policy and binding identifiers remain
in internal diagnostics. Mutating audit records carry the policy package
version and the determining binding ids
(audit_event), without recording secret values
or the full entity slice.
User and service-account principals are distinct in Cedar. Both are
app_user rows, so authorship and
attribution reference either, and the API names them as ActorRef.
8. Verification
CI runs these correctness cases:
- Operator on team A plus viewer on team B cannot operate on B; direct service-account and team grants retain the same scope behavior.
- Missing role, empty token allowlist and a narrowed token deny the corresponding action, including when another role permits related actions.
- Cross-tenant resource, group, scope and token inputs cannot grant access; platform-admin status alone grants no tenant permissions.
- Membership removal, binding removal, token revocation, ownership changes and label changes take effect on the next authorization snapshot.
- Four-eyes self-approval and ineligible
acting_teamsfail; changed digests and insufficient approvals fail in the workflow even with permission. - Low request assurance, absent artifact reference, failed grant, quarantine and class mode each prevent secret delivery as required by its contract.
- Missing entity data, invalid context and an error in a restriction never allow the operation; diagnostics contain no secret values.
- Property tests compare SQL and in-memory selectors for all operators, missing keys, glob escaping and numeric edge cases.
- Database tests compare SQL-visible sets, counts and groups with exhaustive Cedar decisions, including overlapping bindings, pagination and changing permissions. An unauthorized target beyond the first page rejects a compound write.
- New protected endpoints and policy package changes fail CI when their action mapping or SQL equivalence coverage is missing.
Load tests use the fleet sizes in operations.md §8, with overlapping owner/selector grants and large impacted sets, alongside the agent polling and heartbeat load. No operation loads the whole fleet to authorize a bounded inventory page.