Skip to content

Cybersecurity Engineering Handbook / Chapter 12

Authorization and Access-Control Architecture

Design authorization so every object, tenant, administrative action, export, and background job has an explicit enforcement point.

The support operator from the previous chapter has signed in, completed step-up authentication, and asked to refund an order. The console knows the operator’s identity and the strength of the recent proof. Neither fact answers the question that can move money: may this operator refund this order, for this tenant, for this amount, under this case, now?

A role check such as operator.isSupport cannot carry that decision. It says nothing about the order’s tenant, the operator’s assigned region, the refund limit, the approval state, or whether the request arrived through the ordinary console or a bulk tool. It may still return true after the operator changes teams. A background worker may never execute the check at all.

Authorization architecture makes the same decision unavoidable wherever data or power can move. It gives the decision complete facts, denies when required facts are missing or untrusted, and leaves enough evidence to reconstruct what happened.

Authorization request path flows from caller through gateway, service, policy enforcement point, and policy decision point. The decision point consults attributes, relationships, roles, tenant, and owner data before allowing a data or API action or denying and writing an audit event.
Authorization is a decision system, not a UI condition. The enforcement point must ask a policy decision with enough context to allow, deny, and record the result.

Write the decision before choosing a model

Start with one consequential request, expressed as five facts:

  • Actor: the support operator, plus the console workload acting for that operator.
  • Action: refund_payment, not the vague permission manage_orders.
  • Object: order ord_8041 and its payment, not merely an identifier supplied by the caller.
  • Scope: tenant meridian, the operator’s support region, and case case_417.
  • Context: refund amount, order state, recent authentication assurance, approval state, environment, and time.

The resulting rule can be read without knowing a policy language:

Allow a named support operator to refund an order only when the operator is assigned to the order’s tenant and region, the case is open and linked to that order, recent authentication meets the required assurance, and the amount is within the operator’s limit. Require an independent approver above that limit. Deny otherwise.

This is the beginning of an authorization model worksheet. Add the owner of the rule, each source of decision facts, the enforcement locations, the behavior when a dependency is stale or unavailable, and the evidence emitted for allow and deny. The worksheet is useful because it exposes omissions before notation makes the policy look finished.

Choose access-control models after the rule is visible. Role-based access control is a good coarse filter for stable job functions: a support operator may enter the refund workflow while a customer may not. Attributes express conditions such as tenant, region, amount, environment, assurance, and time. Relationships express ownership, membership, hierarchy, sharing, delegation, and the link between a support case and an order. A capability can grant possession-based authority to one resource and action, provided it is narrowly scoped, short-lived, and protected against disclosure and replay.

These models are complementary. The refund rule may begin with a role, narrow through attributes, and depend on relationships. Policy-as-code is not another source of authority; it is a way to represent, test, version, and distribute the resulting rule. Centralizing syntax does not repair bad facts or a bypassable enforcement point.

Follow one decision through the system

The refund request enters through a route handler, but the route is only one possible policy enforcement point. An enforcement point is the code that must obtain a decision before the protected action proceeds. It may live in gateway middleware, a service method, a database access layer, a queue consumer, or a tool broker. Place it close enough to the action that another caller cannot bypass it, while keeping the policy decision reusable across every legitimate path.

The enforcement point sends a structured request to a policy decision point. The decision point evaluates the applicable policy and returns allow or deny, normally with a reason code and sometimes with obligations: mask specified fields, require another approval, cap the amount, or emit a particular audit event. The caller must not treat an error, timeout, malformed answer, or unknown action as permission.

The decision point needs trustworthy facts. Policy information points supply the operator’s active assignment, the order’s tenant and owner, the case relationship, the authentication assurance, and the approval state. Each fact has an authority and a useful lifetime. A tenant identifier copied from the request body is not an authoritative object attribute. A role claim minted before a transfer may be too stale for a privileged action. Cache design therefore belongs to authorization design: record what may be cached, for how long, how revocation propagates, and which decisions must fail closed when fresh facts are unavailable.

The policy administration point is the controlled path by which people or automation change roles, relationships, rules, and exceptions. A safe runtime decision can be undone by an unsafe administrative write. Treat permission grants, tenant membership changes, refund-limit changes, relationship edits, and emergency exceptions as protected actions of their own. Give them owners, validation, review proportionate to impact, version history, and revocation.

For the refund, a useful decision record contains a request identifier, actor and delegated workload, action, object and tenant, material context, allow or deny, policy version, reason code, obligations, and the identifiers of supporting case and approval records. Do not log credentials or unnecessarily duplicate sensitive object data. The record should let an investigator distinguish “the policy allowed this request” from “the payment provider eventually completed it.” Authorization and business outcome are related evidence, not the same event.

Bind policy to the object the system found

Object-level authorization fails when the system authenticates the caller, accepts an object identifier, and never asks whether the caller may act on the object it resolves. The secure sequence is short:

  1. Derive tenant and caller context from trusted identity and routing state, not from a convenient client field.
  2. Resolve the object within that boundary, or retrieve enough authoritative metadata to make the decision.
  3. Authorize the action on the resolved object and any sensitive properties involved.
  4. Perform the action without reopening a gap in which the relevant ownership, relationship, or workflow state can change unnoticed.
  5. Record the decision and outcome with a shared correlation identifier.

Opaque identifiers reduce casual guessing but do not grant permission. A valid order identifier from another tenant must still be denied. So must an order in the same tenant when the operator lacks the required relationship, action, approval, or field access.

Collection paths make the rule harder. A list query must constrain the result set to objects the actor may discover; fetching everything and filtering afterward can leak counts, timing, cache entries, or data through another consumer. Search, reports, and exports need both per-object or query-equivalent authorization and a decision about the aggregate action. Permission to view one order does not imply permission to export ten thousand orders. Field-level policy still applies when a permitted object contains payment, identity, security, or audit fields the actor may not read or change.

Bulk mutations must authorize every target and the operation as a whole. Define whether one denial rejects the entire batch or produces explicit per-item results. Silent partial success is dangerous when money or access changes. Pagination, filters, alternate identifiers, nested resources, and parent-child relationships all need tests because each creates another way to name the same protected object.

Preserve authority across asynchronous work

The console authenticates as a workload when it calls the decision service, while the human operator remains the accountable actor. The downstream service needs both facts: who is calling now, and on whose authority the requested refund is being attempted. Passing a broad browser token through every service confuses those roles and enlarges the damage from token theft.

When a downstream service needs a token, exchange the incoming proof for one restricted to the destination, purpose, audience, and useful lifetime. Express whether the service is impersonating the subject or acting as a delegate; do not let a generic sub claim erase the service that actually made the call. The receiving service authenticates the workload and makes its own authorization decision from the bounded delegation and required business context.

A queue does not suspend policy. Before publishing a refund command, authorize the producer to request that action and bind the message to the approved object, amount, tenant, policy decision, and expiry. The consumer authenticates the producer or trusted messaging path, verifies message integrity and freshness, and authorizes its own mutation. A reconciliation job that retries the refund later must prove it is executing the same approved intent, not infer authority from possession of an old message.

Events that announce a completed fact differ from commands that ask another component to act. Consumers may trust an authorized producer to attest that a refund was approved, but each consumer still controls the effects it can cause in its own boundary. Replay, delayed delivery, policy changes, and revoked delegation need explicit behavior. For dangerous actions, a stale command should not become valid merely because the worker was offline when authority expired.

Internal services are therefore not universal deputies. Give each workload the actions and resource classes it needs. Where local enforcement and a shared decision service divide the work, define policy version behavior during rollout and outage. A cached last-known decision may be acceptable for a low-impact read; it is a poor fallback for a new privileged refund. Deny-by-default must be an engineered failure mode with observable reason codes, not an accidental outage that operators learn to bypass.

Give privileged paths less ambient power

Least privilege is a continuing subtraction process. Begin with narrow actions and scopes, then add only the authority a real workflow needs. “Support admin” is rarely a useful final role: viewing a masked customer record, issuing a small refund, changing an account owner, exporting tenant data, and editing another user’s permissions have different consequences and should not travel as one bundle.

For the refund workflow, ordinary support access may permit reading a masked order and drafting a refund. Just-enough elevation can permit the refund action for a bounded tenant, amount, and duration. Larger or unusual refunds can require a second person who did not initiate the request. Just-in-time access reduces standing privilege only if expiry is enforced and the underlying group, token, session, and cached decision all lose the grant on time.

Administrative routes, support consoles, database tools, feature controls, policy editors, and impersonation features need the same explicit actor-action-object rule as customer APIs. Impersonation should preserve the administrator’s identity, show the mode visibly, bind access to a case or approved purpose, limit fields and actions, and record entry, use, and exit. It must not turn an administrator into an indistinguishable customer session.

Emergency access may bypass a failed normal dependency, but it must not bypass attribution. Bound its reach and duration, alert on every attempt, protect approval or dual-control evidence where the risk requires it, and review every use. An emergency grant that quietly becomes permanent is simply an overbroad role with a dramatic name.

Make the permission record reviewable

A permission matrix earns its place during design even when it is stored as data rather than printed as a large table. Give every row one actor class or relationship, one action, one resource class, and an explicit scope. Add conditions, required approval, decision owner, enforcement points, evidence, and exception expiry. Include denies for dangerous near-neighbors, not only allows.

For example, the refund record should distinguish these permissions:

  • a support operator may read masked order fields for an assigned tenant and open case;
  • the same operator may draft a refund without moving money;
  • an elevated operator may execute a refund within a stated amount and recent-authentication window;
  • an independent approver may approve above that amount but may not approve a request they initiated;
  • the payment worker may execute only the approved refund object named in an unexpired command;
  • the reconciliation worker may inspect and reconcile that execution but may not create a new refund intent;
  • no role receives tenant-wide export merely because it can read one order.

This record is also the basis for access review. Review actual effective permissions and relationships, not role names alone. For each human or workload, show the grant source, resource and tenant scope, privileged conditions, last use where reliable, owner, justification, expiry, conflicting duties, and the reviewer’s keep, narrow, or remove decision. Give remediation an owner and deadline. Include inherited group membership, temporary grants, service accounts, capabilities, policy exceptions, and dormant relationships; otherwise the review describes the directory rather than the authority the system will honor.

Test the paths that almost look allowed

An authorization test suite should make one valid case pass, then change one security-relevant fact at a time. For the refund action, keep the operator authenticated and the order identifier valid while testing:

  • wrong tenant, region, owner, case, or relationship;
  • expired elevation, stale assurance, amount above limit, or missing independent approval;
  • another object’s identifier in a path, body, nested field, filter, cursor, or relationship update;
  • a mixed-tenant bulk request and a page whose later results cross the permitted boundary;
  • search, export, support, administrative, and background-job paths to the same data;
  • a valid human subject paired with the wrong calling workload or token audience;
  • replayed, expired, altered, duplicated, or out-of-order commands;
  • missing attributes, unknown actions, policy timeouts, stale caches, and incompatible policy versions;
  • sensitive fields that may be read but not changed, or changed but not returned.

Generate matrix tests from the permission record so every allow has credible deny neighbors. Fuzz identifiers and relationship edges, but do not rely on random strings alone: the revealing test is often a real object that belongs to somebody else. Run tenant-isolation tests against read, write, list, search, export, batch, job, cache, and administrative paths. Verify not only the response but also the absence of mutation, disclosure, downstream messages, and misleading success audit events.

Policy tests establish that a rule produces the intended answer. Enforcement tests establish that every path asks. You need both. A perfect decision function cannot protect a forgotten export handler, and an everywhere-present check cannot rescue a policy that grants manage_all to convenience roles.

Before release, trace one consequential action from entry to effect. Name the actor, action, object, scope, and context; identify every enforcement point and fact authority; inspect the policy and permission records; force deny, dependency failure, and revocation; then follow the audit correlation through synchronous and asynchronous work. Repeat the trace for direct object access, collections, bulk work, administration, exports, and background jobs.

The refund is ready for API design only when changing the route, caller, identifier, tenant, delivery mechanism, or execution time cannot quietly change the policy question. Chapter 13 can then turn that stable decision into endpoint and service gates without inventing authority inside each handler.