Authorization Model

DotID’s Authorization Service implements a resource-based access-control system inspired by AWS IAM. It acts as a Policy Decision Point (PDP): callers describe who wants to do what on which resource, and the service returns ALLOW or DENY.

Core Concepts

Accounts

An Account is the top-level organizational container (tenant). Every resource FRN includes an account-id segment so that policies can be scoped per tenant.

Groups

A Group collects one or more principals (users or service-account clients). Groups are the “who” axis of the authorization model.

Each member is stored as a GroupMember with:

  • principalId – the Keycloak subject identifier (sub claim).

  • principalTypeuser or client.

A principal may belong to multiple groups.

Policy Sets and Policies

A PolicySet is a named container that groups related policies.

A Policy holds the actual authorization rules in a JSON document stored as JSON. See Policy Document Format for the full schema.

Permissions (the Binding)

A Permission is the three-way binding that answers:

“Which policies apply when members of Group G access resources in Account A?”

The answer is the policies inside Policy Set S.

Principal  -->  GroupMember  -->  Group
                                   |
                               Permission
                                   |
                          Account (resource scope)
                                   |
                              PolicySet
                                   |
                              Policy (1..N)
                                   |
                            Statement[]

The tuple (group_id, account_id, policy_set_id) is unique – duplicate bindings are rejected with HTTP 409.

Evaluation Algorithm

The policy engine implements a classic explicit-deny-wins evaluation model with support for IAM policies, SCPs, resource policies, permission boundaries, and delegated administrator grants. A single authorization check proceeds as follows:

  1. Parse the resource FRN – validates the FRN string; rejects malformed identifiers immediately.

  2. Resource-based policy – if a resource policy exists for the target FRN and explicitly allows/denies the action, return early.

  3. Root user bypass – root users have implicit full access to their own account (like AWS root), no policy attachment needed. If the principal is a root user and the target resource belongs to the same account, return ALLOW with reason ROOT_USER_BYPASS.

  4. Resolve identity-based policies – resolves group memberships via account assignments, IAM managed/inline policies, and permission set policies for the principal.

  5. Explicit Deny (identity) – iterate every statement with "Effect": "Deny". If any statement matches, return DENY immediately (deny always wins).

  6. SCP evaluation – for principals in an organization, evaluate Service Control Policies. If the action is not allowed by any SCP, return DENY.

  7. Explicit Allow (identity) – iterate every statement with "Effect": "Allow". If a statement matches, proceed to boundary check.

  8. Delegated Admin Allow – if no identity policy granted access, check whether the principal qualifies as a delegated administrator:

    • The principal must be a root user (userType == "root").

    • The principal’s account must have a delegation for the action’s service namespace (the prefix before the first : in the action string, e.g., "audit" from "audit:Event:Read").

    • The namespace must be eligible for delegation (see below).

    • The target resource must belong to an account within the same organization as the delegation.

    • Requests targeting the principal’s own account are skipped (root users already have full access on their own account).

    • Wildcard FRN targets (account segment *) are allowed.

    If all conditions pass, the check returns matchedStatement: "DelegatedAdminAllow" and proceeds to the permission boundary check.

    Namespace delegation eligibility

    Only operational and observability namespaces may be delegated. Governance and security-critical namespaces are permanently excluded to prevent privilege escalation:

    • iamnot delegable. A delegate could modify or remove other users’ permissions, including the root user’s own policies.

    • orgnot delegable. Organization structure is a management-account-only concern.

    • scpnot delegable. Delegating SCP management would allow weakening security guardrails across the organization.

    • stsnot delegable. Cross-account role assumption is identity infrastructure, not an operational service.

    • auditdelegable. Read-only audit log access for org-wide compliance visibility.

    • quotadelegable. View and manage service quotas across the organization.

    Future namespaces follow the same rule: if delegating the namespace could allow the delegate to alter, escalate, or remove permissions for other principals, it must not be delegable.

  9. Permission boundary – if a permission boundary is set for the user, verify the action is within the boundary. If not, return DENY.

  10. Return ALLOW – if a matching allow statement (or delegation grant) survived all checks, return ALLOW.

  11. Default DENY – if no statement matched, return DENY.

  12. Audit logging – every decision (including default denies) is persisted to the audit log.

Statement Matching

A statement matches only when all three sub-checks pass:

Action matching

  • Exact match: "devices:Read" matches only "devices:Read".

  • Global wildcard: "*" matches any action.

  • Service prefix wildcard: "devices:*" matches any action starting with "devices:".

Resource matching

  • Literal "*" matches any resource.

  • Otherwise the pattern is parsed as an FRN and matched segment-by-segment using FrnMatcher (see FRN Specification).

Condition matching

Conditions apply additional constraints based on the request context map. All operators in a statement must pass (AND logic).

Supported Condition Operators

Operator

Semantics

StringEquals

actual must be in the expected value list (exact match).

StringNotEquals

actual must not be in the expected value list.

StringLike

actual must match at least one glob pattern (* = any chars).

Bool

actual.toString() must be in the expected value list (e.g., "true").

Unknown operators evaluate to false and log a warning.

Condition keys may carry a dotid: prefix (stripped before lookup). Context keys are tried in both their original form and a camelCase to snake_case converted form.

Batch Evaluation

The batch endpoint loads group memberships and policies once for a principal, then evaluates each check against the pre-loaded data. This is significantly more efficient than issuing individual requests.

Capabilities

Capabilities are account-level business qualifications stored on the Account entity in the DotID authorization service. They are distinct from — and evaluated after — the SCP/IAM policy evaluation described above.

The evaluation order is:

  1. Roles, SCPs, and IAM policies are evaluated (steps 1–11 above).

  2. If the result is ALLOW, the target service checks whether the account holds the required capability for the requested operation.

  3. If the capability is absent, the service returns 403 Forbidden ("account not qualified contact platform support").

Capabilities are not part of the policy document format and are not evaluated by the Policy Decision Point. They are checked independently by each service after authorization passes.

Known capabilities:

  • enroll_things — register new devices in ThingHub

  • approve_licenses — approve and issue licenses in TrustMint

  • publish_marketplace — publish products to the Bazaar marketplace

  • manage_ota_rollouts — create and manage OTA rollouts in OTAForge

Capabilities are granted and revoked by platform admins in SuperCrew. Accounts start with no capabilities. See Access Control Layers for the full three-layer model (Role → SCP → Capability) and how these layers interact.

Cache Invalidation

Every mutation (create, update, delete) on any authorization entity increments a global policy version counter and broadcasts a policy.changed event over Server-Sent Events (SSE).

Downstream caches should either:

  • Subscribe to GET /api/v1/events/stream and invalidate on any received event, or

  • Poll GET /api/v1/policy-version periodically.