ABAC & Cedar Policies

On top of role-based access control, Tetrapus runs an attribute-based check on every authenticated request using Cedar. The ABAC layer exposes an Authorizer trait and a CedarAuthorizer implementation backed by a bounded LRU decision cache. Cedar runs after RBAC; if either layer denies, the request is denied.

Decision pipeline

graph LR REQ["WS / REST request"] --> RBAC["Permission check<br/>(ws_authz::check)"] RBAC -->|deny| DENY["403 + audit"] RBAC -->|allow| ABAC["Cedar check<br/>(ws_authz::check_abac)"] ABAC -->|cache hit| FAST["Decision<br/>(no eval)"] ABAC -->|cache miss| EVAL["CedarAuthorizer.is_authorized"] EVAL --> CACHE["DecisionCache<br/>(LRU + TTL)"] CACHE --> FAST FAST -->|Allow| OK["dispatch"] FAST -->|Deny| DENY EVAL -.->|engine error| FAILCLOSED["fail-closed<br/>+ warn!"] FAILCLOSED --> DENY

Sample policy

From policies/base.cedar:

Text
// 1. System-wide same-Org scoping — every action must be in the same Org.
forbid (
    principal,
    action,
    resource
) when {
    principal.org != resource.org
};

// 2. Org admins pass every action inside their Org.
permit (
    principal is User,
    action,
    resource
) when {
    principal.org == resource.org &&
    (principal.role == "org_owner" || principal.role == "org_admin")
};

// 3. Stream-read requires every required marking on the stream.
permit (
    principal,
    action == Action::"stream_read",
    resource is Stream
) when {
    principal.org == resource.org &&
    principal.markings.containsAll(resource.required_markings)
};

Schema

Text
entity Org;

entity User in Org = {
    "org": Org,
    "role": String,
    "markings": Set<String>
};

entity Stream in Org = {
    "org": Org,
    "required_markings": Set<String>
};

entity AuditLog in Org = { "org": Org };
entity Workspace in Org = { "org": Org };
entity Device in Org    = { "org": Org };
entity Service in Org   = { "org": Org };

action stream_read appliesTo { principal: [User, Device, Service], resource: Stream };
action command_issue appliesTo { principal: [User], resource: Command };
action audit_read   appliesTo { principal: [User], resource: AuditLog };

Decision cache

  • Capacity — 4096 entries (bounded LRU, eviction on insert).
  • TTL — 30 seconds. Expired entries are recomputed lazily on next lookup.
  • Key(principal_id, action, resource, markings_fingerprint). Markings are hashed in declaration order for stability.
  • Hit-rate — ~98% on realistic stream-read traffic; per-message authz cost drops to a single hashmap lookup.
  • Invalidation — principal-scoped flush on permission grants, marking changes, or role updates.

Status: the trait + Passthrough authorizer ship today; CedarAuthorizer with the bundled base policies is wired into the WS hot-path. Per-Org policy bundles (uploadable Cedar files) are on the roadmap.

Related

  • Permissions — the RBAC layer Cedar runs after.
  • Markings — the labels Cedar consumes for stream-read gating.
  • Audit Trail — every Cedar deny is recorded.

Questions?

Reach out for help with integration, deployment, or custom domain codecs.