Skip to main content
Commerce Security logo, "All 12 PCI DSS Requirements in Plain English," "Get it now for free," "Complete Survival Guide" and a button toclick to get it
Build an Authorization System That Actually Makes SenseAccess Control Models
7 min readFor IAM Architects

Build an Authorization System That Actually Makes Sense

You're about to design a new authorization layer. Your team mentions "RBAC or ABAC?" and the conversation stalls because nobody's sure what either term really covers anymore. Does RBAC mean you can't use attributes? Does ABAC mean you need a policy engine? The labels compete instead of clarifying.

Here's the underlying problem: authorization terminology conflates six different design decisions into a handful of overloaded acronyms. RBAC and ABAC describe what data a decision is based on. Policy-Based Access Control describes how that decision gets made. Mandatory Access Control and Discretionary Access Control describe who sets the rules. They answer different questions, but we treat them like competing options.

This guide walks you through building an authorization system by separating those questions. You'll make six independent choices, validate each one, and end up with a system you can actually explain to your security team.

What You Need Before Starting

Context about your access patterns:

  • Who needs to grant access? Central security team, resource owners, or both?
  • What drives most decisions? User identity, role membership, attributes like department or clearance level, or relationships between users and resources?
  • How often do rules change? Daily tweaks or quarterly policy reviews?

Technical inventory:

  • Your identity provider and what claims it can assert (roles, groups, custom attributes)
  • Your application's request context (headers, session data, database connections)
  • Available policy evaluation tools (built-in conditionals, Open Policy Agent, AWS Cedar, commercial authorization services)

A single representative use case. Pick one authorization decision your system makes frequently. Example: "Can this user view this document?" You'll implement that case end-to-end before generalizing.

Step-by-Step Implementation

1. Decide Who Administers the Rules

This is your authorization administration model. Three options:

Centralized (Mandatory Access Control): Security or platform team sets all rules. Users can't override.

Decentralized (Discretionary Access Control): Resource owners decide. Think file sharing permissions.

Hybrid: Central team sets baseline rules; owners can grant additional access but can't remove baseline restrictions.

For your representative use case, answer: who should be allowed to write the rule that governs this decision?

If it's centralized, you need admin tooling that only your security team can reach. If it's decentralized, you need UI for resource owners to manage their own permissions. If it's hybrid, you need both, plus clear precedence rules.

Implementation checkpoint: Create a test rule using your chosen administration path. If you picked centralized, have a security team member define it through your admin interface. If decentralized, have a resource owner set it through the owner UI. Verify the rule persists where you expect it.

2. Choose Your Authorization Model

This is what data type drives the decision. Four common models:

Identity-based (ACL): Check if the specific user ID appears on a list attached to the resource. Simple, doesn't scale well beyond small teams.

Role-based (RBAC): Check if the user holds a role that's been granted the permission. Scales better, but role explosion becomes a problem in large orgs.

Attribute-based (ABAC): Check user attributes (department, clearance level), resource attributes (classification, owner), environmental attributes (time of day, IP range). Flexible but complex to audit.

Relationship-based (ReBAC): Check the relationship between user and resource by traversing a graph. Example: "Can this user edit this document? Yes, if they're a member of the team that owns it."

For your use case, which model fits? If you're protecting documents in a multi-tenant SaaS app where teams own resources and membership changes frequently, ReBAC makes sense. If you're enforcing regulatory controls that depend on user clearance level and data classification, that's ABAC.

Implementation: Define the data shape your decision needs. For RBAC, that's role assignments. For ABAC, it's a set of attributes you'll need to fetch or receive. For ReBAC, it's the relationship graph.

Example (RBAC): User "[email protected]" has role "editor". Resource "document-123" grants "edit" permission to role "editor".

Example (ABAC): User has attribute "department=finance". Resource has attribute "sensitivity=internal". Rule: allow if user.department == resource.owner_department OR resource.sensitivity == "public".

3. Pick Your Policy Format

How will you express the rule?

Hardcoded conditionals: Fast, version-controlled, requires deployment to change.

if user.role == "admin" or (user.role == "editor" and resource.owner == user.id):
    return ALLOW

Structured document (JSON/YAML): Easier to change without redeployment, harder to validate.

Declarative policy language (Rego, Cedar, XACML): Purpose-built for authorization, steeper learning curve, better auditability.

For your first implementation, hardcoded conditionals are fine if rules change infrequently and you already have CI/CD. If non-engineers need to adjust policies or you need runtime updates, reach for a structured format or policy engine.

Implementation: Write your representative rule in your chosen format. If you picked hardcoded, add the conditional to your application's authorization middleware. If you picked a policy language, write a policy file and configure your evaluation engine to load it.

4. Wire Up Your Information Sources

Your rule depends on data. Where does it come from?

Token-based: Data arrives as claims in a JWT from your identity provider. Fast, but limited to what the IdP can assert at login time.

Looked up: Application queries a database or directory at decision time. Flexible, adds latency.

Wired in: Data is already in your request context (user session, HTTP headers). Zero extra lookup cost.

For your use case, map each piece of data your rule needs to a source. User roles often come from tokens. Resource ownership usually requires a lookup. Environmental context like IP address is wired in.

Implementation: Add the data fetch logic. If you're pulling roles from a JWT, decode and verify the token. If you're looking up resource metadata, write the query. If you're checking request time, pull it from your server's clock.

Test this independently: given a known user and resource, verify you can retrieve all the data your rule needs before you try to evaluate the rule itself.

5. Implement the Decision Point

This is your Policy Decision Point: the code or service that evaluates the rule against the data and returns allow or deny.

If you hardcoded conditionals, your PDP is just the function containing those conditionals. If you're using a policy engine, your PDP is the engine itself.

Implementation: Write a function that takes subject, action, object, and context as inputs and returns a boolean or an explicit ALLOW/DENY.

def authorize(subject, action, resource, context):
    # Fetch data (step 4)
    user_roles = get_roles_from_token(context.token)
    resource_owner = db.get_resource_owner(resource.id)
    
    # Evaluate policy (step 3)
    if "admin" in user_roles:
        return ALLOW
    if action == "read" and resource.visibility == "public":
        return ALLOW
    if action == "edit" and subject.id == resource_owner:
        return ALLOW
    
    return DENY

6. Enforce the Decision

Your Policy Enforcement Point is wherever you actually block or allow the request based on the decision. This is usually middleware in your application or an API gateway.

Implementation: Wrap your protected endpoints with enforcement logic that calls your PDP and acts on the result.

@app.route('/documents/<doc_id>')
def get_document(doc_id):
    decision = authorize(
        subject=current_user,
        action="read",
        resource=Document(doc_id),
        context=request_context
    )
    if decision == DENY:
        abort(403)
    return render_document(doc_id)

Validation: How to Verify It Works

Test the decision point in isolation. Write unit tests that feed known inputs and assert expected outputs:

  • User with admin role requests any action: expect ALLOW
  • User with no roles requests edit on someone else's resource: expect DENY
  • User requests read on public resource: expect ALLOW regardless of role

Test the enforcement point. Make actual HTTP requests (or equivalent) as different users and verify:

  • Allowed requests return 200 and the expected resource
  • Denied requests return 403 with no resource leakage in the error message

Audit the administration path. If you built a UI for resource owners, have a non-admin user try to set permissions on their own resource and verify the rule takes effect. Have them try to set permissions on someone else's resource and verify it's blocked.

Load test the information sources. If you're doing database lookups at decision time, simulate realistic request volume and measure latency. If you're seeing P95 latency over 100ms, consider caching or moving data into tokens.

Maintenance and Ongoing Tasks

Policy versioning: Treat policy changes like code changes. Use version control, require review, tag releases. If you're using a policy engine, version the policy files alongside your application code.

Audit logging: Log every authorization decision with enough context to reconstruct it later: timestamp, subject, action, object, decision, and the data values that fed the decision. You'll need this for compliance and incident response.

Periodic access reviews: Even if your rules are correct, the underlying data (role assignments, group memberships) drifts. Schedule quarterly reviews where resource owners or security teams verify who still needs access.

Monitor decision latency: Track P50, P95, and P99 latency for your PDP. If it climbs, you're either doing too many lookups or your policy evaluation is too complex. Cache aggressively or simplify the policy.

Test policy changes in staging: Never deploy a policy change directly to production. Run the new policy in shadow mode first: evaluate both old and new policies, log where they disagree, and verify the new behavior is what you intended before enforcing it.

The six-axis model isn't just cleaner terminology. It's a forcing function that makes you answer the right questions in the right order, so you end up with an authorization system your team can actually reason about.

Promotional banner highlighting failures found in PCI audits and how to spot the gaps

You Might Also Like