Writing

Authorization in Practice: Who Can Do What to Which Resource?

A practical application-security guide to RBAC, ABAC, ReBAC, enforcement points, Java authorization checks, multi-tenant boundaries, testing, and failure behavior.

  • security
  • authorization
  • access-control
  • application-security
  • Java

Alice is a manager at Acme. She sends this request:

POST /api/expenses/4182/approve
Cookie: session=<opaque-value>

Her session proves which principal made the request. It does not prove that expense 4182 belongs to Acme, that Alice manages its owner, that the expense is awaiting approval, or that Alice is allowed to approve her own claim.

Authorization reduces those questions to one decision:

allow(principal, action, resource, context)?

For this request: May Alice perform expense:approve on expense 4182, under the current tenant, relationships, object state, and policy?

Authentication supplies a principal, not permission

  • Authentication establishes who or what is making the request.
  • Authorization decides whether that principal may perform an action on a resource.
  • Access control is the mechanism that evaluates and enforces that decision.
  • Policy is the rule being evaluated.
  • Entitlement is an assigned capability such as a role, group membership, or permission.

A valid session, JSON Web Token (JWT), API key, or client certificate is an input to authorization—not a blanket allow. OWASP recommends least privilege, deny by default, and permission validation on every request because an attacker needs only one unprotected path.

Four models explain most application policies

Model Simple mental model Useful for Common trap
Access Control List (ACL) The resource lists who may do what A document shared with named people Permissions become difficult to review across many resources
Role-Based Access Control (RBAC) Principals receive roles; roles contain permissions Stable job functions such as employee, manager, and billing admin Creating a new role for every exception or resource scope
Attribute-Based Access Control (ABAC) Evaluate attributes of the principal, resource, action, and environment Tenant, region, data classification, time, device, or object-state rules Using stale or untrusted attributes and producing policies nobody can explain
Relationship-Based Access Control (ReBAC) Permission follows a relationship such as owner, member, parent, or manager Collaboration, projects, folders, organizations, and sharing Treating the relationship graph’s consistency and authorization latency as someone else’s problem

NIST defines RBAC around roles and their permitted actions. Its ABAC guidance describes evaluating subject, object, operation, and sometimes environment attributes. Google’s Zanzibar paper is the best-known production example of a relationship-based system used for products such as Drive, Calendar, and Photos.

Real applications rarely choose exactly one. Acme’s rule might combine all three:

allow when
  principal.tenantId == expense.tenantId
  and principal has role MANAGER
  and principal manages expense.owner
  and expense.status == PENDING
  and principal.id != expense.ownerId

The role grants the general function. Tenant and status are attributes. “Manages” is a relationship. The useful design question is not “Which acronym wins?” but “Which trusted facts are necessary for this decision?”

Put enforcement before the side effect

A Policy Enforcement Point (PEP) intercepts the operation, asks for a decision, and enforces the result. A Policy Decision Point (PDP) evaluates the policy. They may be two modules in one process or separate services; the trust boundary matters more than the deployment vocabulary.

Enforcement asks; policy decides

The endpoint stops the request at a policy enforcement point. The decision combines the principal, action, resource, relationships, and current context before business code runs.

Enforcement asks; policy decidesThe endpoint stops the request at a policy enforcement point. The decision combines the principal, action, resource, relationships, and current context before business code runs. Select a component to inspect its trust decision.Request pathPolicy inputsprincipal + inputprincipal · action · resource · contextattributesallow / deny + reasonallow onlyAPIrequestapprove expense 4182Enforcementpointintercept + enforceBusinessoperationruns only after allowPolicydecisionallow or denyTrustedpolicy datatenant · roles · relations

Authorization request path

Authentication supplies a principal, not a blanket allow. The application gathers trusted resource and relationship data, obtains an allow or deny decision, enforces it, and records the result.

An application may keep simple policy in code or use a policy engine such as Open Policy Agent, which separates decision-making from enforcement. A central service can improve consistency and audit, but it also adds latency, availability, cache-freshness, and rollout concerns. Do not build a network dependency merely to replace a clear local check.

For a sensitive operation, default to deny when required policy data or the decision service is unavailable. If a low-risk read path deliberately uses cached decisions, document the accepted staleness and never silently apply that exception to writes or administration.

A role check is usually not enough

This Spring controller looks protected:

@PostMapping("/api/expenses/{expenseId}/approve")
@PreAuthorize("hasRole('MANAGER')")
Expense approve(@PathVariable long expenseId) {
    return expenses.approve(expenseId);
}

It proves only that Alice has a manager role. If expenses.approve loads a global ID, Alice may approve another tenant’s expense or an employee she does not manage.

A safer service loads the trusted resource and evaluates the complete decision before changing it:

Expense approve(AppPrincipal principal, long expenseId) {
    Expense expense = expenses.findById(expenseId)
            .orElseThrow(notFound());

    AuthorizationRequest request = new AuthorizationRequest(
            principal,
            "expense:approve",
            expense,
            RequestContext.current()
    );

    authorization.requireAllowed(request);
    return expenses.approve(expense);
}

The policy receives tenant and user identity from the validated principal and authoritative application data—not a caller-supplied tenantId, ownerId, or role. The business write runs only after an allow decision.

For simple ownership, make the database query part of the boundary:

return expenses.findByIdAndTenantIdAndOwnerId(
        expenseId,
        principal.tenantId(),
        principal.userId()
).orElseThrow(notFound());

That pattern prevents a global row from entering the request path. More complicated sharing, delegation, or administrative rules belong in a consistent policy layer. A dedicated Broken Object Level Authorization (BOLA), also known as Insecure Direct Object Reference (IDOR), review should test this object-level failure in depth.

Enforce at every path that can reach the resource

Authorization is usually defense in depth, not one annotation:

  • Gateway or router: coarse function, audience, or scope checks. It usually cannot decide whether Alice owns expense 4182.
  • Application service: business action, tenant, object state, ownership, and relationship checks.
  • Repository or database: tenant-scoped queries, row-level security, and constraints that limit mistakes above it.
  • Response construction: field-level authorization for salaries, secrets, private comments, or regulated data.
  • Asynchronous paths: background jobs and queue consumers must preserve the initiating principal or use an explicitly authorized workload identity.

Apply the same reasoning to list, search, count, export, batch, GraphQL, attachment, restore, and administrative endpoints. Protecting GET /expenses/{id} while /expenses/export ignores tenant scope is still broken access control.

Effective access is the final result, not one policy file

An application may receive permissions from a role, resource ACL, parent organization, delegated relationship, and organization-wide guardrail. Define how they combine.

A defensible default is:

  1. No matching allow means implicit deny.
  2. Guardrails and explicit denies can restrict otherwise granted access.
  3. The narrowest tenant and resource boundary still applies.
  4. Every decision records which policy version and trusted facts produced it.

AWS IAM is a useful concrete example: requests are implicitly denied, an applicable allow is required, and an explicit deny overrides an allow. Your product does not need to copy AWS policy syntax, but it should have equally explainable conflict rules.

Test a matrix, not only the happy path

Create known principals and resources, then test positive and negative combinations:

Principal Resource Action Expected result
Employee Alice Alice’s draft expense Edit Allow
Employee Alice Bob’s expense Edit Deny
Manager Maria Direct report’s pending expense Approve Allow
Manager Maria Her own expense Approve Deny
Acme administrator Other tenant’s expense Read Deny
Support engineer Acme expense without active elevation Read Deny

Repeat the matrix for alternate API versions, bulk operations, exports, jobs, and cached responses. Then change a role or relationship and test how quickly access disappears. A strong authorization test suite verifies revocation and denial, not only successful access.

Log the principal, tenant, action, resource type and identifier, allow or deny result, reason, and policy version. Do not log entire tokens or sensitive resource contents. Repeated cross-tenant denials are useful detection signals, but monitoring is not a substitute for enforcement.

The AppSec review in six questions

  1. Where does the server prove that this principal may perform this action on this resource?
  2. Which decision inputs come from trusted sources, and how fresh are they?
  3. Can another route, batch request, background job, cache, or export bypass the check?
  4. How do tenant boundaries and object relationships constrain a broad role?
  5. What happens when policy data or the decision mechanism is unavailable?
  6. How quickly do role, relationship, account, and emergency revocations take effect?

If the team can answer those questions with code, tests, and observable behavior, it has an authorization design. If the answer is only “the user already logged in,” it has an authentication system and an access-control vulnerability waiting to happen.

References and further reading

Back to writing