Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 64

RBAC and ABAC: Designing Access Control Systems

Recommended resources

Hello! Welcome to your daily lesson on advanced architectural patterns.

In our last lesson, we secured our system's entry points by implementing stateless authentication with JWTs. We can now confidently answer the question, "Who is this user?" Today, we address the critical next question: "What is this user allowed to do?"

Your goal for this lesson is to learn how to design an authorization system using Role-Based (RBAC) and Attribute-Based (ABAC) Access Control. Moving beyond simple authentication, this is a core competency for designing secure, scalable, and maintainable systems. Mastering the trade-offs between these models is a hallmark of an elite engineer.

We will tackle this through five interconnected concepts, progressing from foundational ideas to industry-leading designs:

  1. The Authorization Problem: The Peril of Hardcoded Rules
  2. A Structured Solution: Role-Based Access Control (RBAC)
  3. The Limits of RBAC: Role Explosion and the Need for Context
  4. The Power of Context: Implementing Attribute-Based Access Control (ABAC)
  5. Elite Deep Dive: Relationship-Based Access Control (ReBAC) and Google's Zanzibar

1. The Authorization Problem: The Peril of Hardcoded Rules (Easy)

  • Technical Explanation:
    Authorization is the process of verifying that an authenticated user has the necessary permissions to perform a specific action on a resource. A naive approach is to scatter if/else statements throughout your codebase, checking the user's role directly.

    For example: if user.role == 'admin' or user.role == 'moderator': delete_comment().

    This becomes an unmaintainable nightmare. What happens when you add a "super_moderator" role? You must now find and update every single one of these checks. If you decide moderators can no longer delete comments, you face the same codebase-wide search-and-replace mission. This tightly couples your business logic to your authorization logic, violating the Single Responsibility Principle and making your system brittle.

How To Handle Permissions Like A Senior Dev

To see just how quickly this naive approach breaks down, watch this short segment from the 'How To Handle Permissions Like A Senior Dev' video by Web Dev Simplified. It perfectly illustrates the problem we're about to solve.

Watch from 00:45 to 02:52. The presenter clearly demonstrates how a simple role-based check spirals out of control as new requirements are added, scattering authorization logic throughout the code.

  • Metaphor for a 5-Year-Old:
    Imagine you have a treehouse with different rooms. Authentication was showing your secret handshake to get into the treehouse. Now for authorization, you start by writing rules on the wall next to each door: "Only Timmy (admin) can enter the snack room." When Sarah (moderator) joins the club and also needs snacks, you have to find that wall and add her name. When you build a new "games room", you have to write new rules on its door. Your walls get messy and it's easy to make mistakes.

  • Python Code Snippet (The "Bad" Way):
    This simple Flask app shows how authorization logic gets mixed with business logic.

    from flask import Flask, jsonify, request
    
    app = Flask(__name__)
    
    # In a real app, this user object would come from a decoded JWT.
    class MockUser:
        def __init__(self, user_id, role):
            self.id = user_id
            self.role = role
    
    COMMENTS = {
        1: {'text': 'This is the first comment.', 'author_id': 10},
        2: {'text': 'This is the second comment.', 'author_id': 20}
    }
    
    @app.route('/comments/<int:comment_id>', methods=['DELETE'])
    def delete_comment(comment_id):
        # Assume we get the user from the request context (e.g., from a JWT).
        user = MockUser(user_id=1, role=request.headers.get("X-User-Role", "guest"))
    
        comment = COMMENTS.get(comment_id)
        if not comment:
            return jsonify({'message': 'Comment not found'}), 404
        
        # --- HARDCODED AUTHORIZATION LOGIC ---
        if user.role == 'admin' or (user.role == 'moderator' and user.id != comment['author_id']):
            # Complex, hard-to-read, and brittle logic scattered in the endpoint.
            del COMMENTS[comment_id]
            return jsonify({'message': f'Comment {comment_id} deleted.'}), 200
        else:
            return jsonify({'message': 'You are not authorized to perform this action.'}), 403
    
    # To test:
    # curl -X DELETE -H "X-User-Role: admin" http://127.0.0.1:5000/comments/1
    # curl -X DELETE -H "X-User-Role: moderator" http://127.0.0.1:5000/comments/2
    # curl -X DELETE -H "X-User-Role: member" http://127.0.0.1:5000/comments/1 -> Fails (403)
    


2. A Structured Solution: Role-Based Access Control (RBAC) (Medium)

  • Technical Explanation:
    RBAC decouples users from permissions by introducing an intermediate layer: Roles. The logic is: a User is assigned one or more Roles, and each Role is granted a set of Permissions.

    • User: The authenticated subject (e.g., user_id: 123).
    • Role: A grouping of permissions that represents a job function (e.g., admin, editor, viewer).
    • Permission: A specific action on a specific resource (e.g., delete:comment, edit:post).

    This centralizes permission management. To change what moderators can do, you modify the moderator role's permission set in one place. When a new user joins, you simply assign them a role. The application code no longer checks the role; it checks for a permission.

    This data model from the Design Gurus article shows the core entities in a multi-tenant authorization system. For RBAC, focus on the relationships between User, Role, and Permission.

How do you design RBAC vs ABAC vs ReBAC for multi‑tenant ...

The Design Gurus article 'How do you design RBAC vs ABAC vs ReBAC' provides an excellent step-by-step guide for structuring authorization systems. We'll start with their explanation of RBAC as the backbone.

Read 'Step 1. Define your core entities' and 'Step 2. Start with RBAC as the backbone'. This will give you the foundational data model and the core logic for a robust RBAC system.

  • Metaphor for a 5-Year-Old:
    Instead of writing names on the treehouse walls, you create a set of keycards: a "Manager" keycard and a "Member" keycard. You have a central rule board that says:

    • Manager Keycard: Unlocks Snack Room, Game Room.
    • Member Keycard: Unlocks Game Room.
      When Timmy joins, you give him a Manager keycard. When Jane joins, she gets a Member keycard. To change the rules, you just update the central rule board. The doors don't need to know who Timmy or Jane are; they just need to know if the keycard is valid for that door.
  • Python Code Snippet (The "Good" RBAC Way):
    We refactor the app to use a centralized RBAC policy.

    from functools import wraps
    
    # --- CENTRALIZED RBAC POLICY ---
    # In a real app, this would live in a database.
    RBAC_POLICY = {
        'admin': {'permissions': ['delete:comment', 'edit:post']},
        'moderator': {'permissions': ['delete:comment']},
        'member': {'permissions': ['edit:post']}
    }
    
    def has_permission(user_role, required_permission):
        """Checks if a user's role has the required permission."""
        role_permissions = RBAC_POLICY.get(user_role, {}).get('permissions', [])
        return required_permission in role_permissions
    
    # --- Authorization Decorator ---
    def permission_required(permission):
        def decorator(f):
            @wraps(f)
            def decorated_function(*args, **kwargs):
                user_role = request.headers.get("X-User-Role", "guest")
                if not has_permission(user_role, permission):
                    return jsonify({'message': 'Permission denied.'}), 403
                return f(*args, **kwargs)
            return decorated_function
        return decorator
    
    @app.route('/comments/<int:comment_id>', methods=['DELETE'])
    @permission_required('delete:comment') # <-- Clean, declarative authorization
    def delete_comment_rbac(comment_id):
        # Business logic is now clean.
        # del COMMENTS[comment_id]
        return jsonify({'message': f'Comment {comment_id} deleted.'}), 200
    
    # Now, if we want to remove delete permission from moderators,
    # we just change RBAC_POLICY in one place!
    # To test:
    # curl -X DELETE -H "X-User-Role: admin" http://127.0.0.1:5000/comments/1 -> OK
    # curl -X DELETE -H "X-User-Role: moderator" http://127.0.0.1:5000/comments/1 -> OK
    # curl -X DELETE -H "X-User-Role: member" http://127.0.0.1:5000/comments/1 -> Fails (403)
    


3. The Limits of RBAC: Role Explosion and the Need for Context (Hard)

  • Technical Explanation:
    RBAC is great for static permissions, but it breaks down when authorization depends on the properties (attributes) of the user or the resource. Consider these rules:

    • "A user can edit their own comment."
    • "A manager can only approve expense reports up to $500."
    • "Content can only be viewed by users in the same country."

    To handle the first rule in RBAC, you might create a permission like edit:own_comment. But the application logic still needs to check if user.id == comment.author_id. This leaks authorization logic back into the endpoint. To handle the second rule, you could create roles like manager_under_500, manager_under_1000, etc. This leads to role explosion, where you have hundreds of finely-grained roles that are impossible to manage.

    This limitation reveals the need for a more dynamic model: Attribute-Based Access Control (ABAC).

*This table visually summarizes the trade-offs. RBAC is simple to set up but struggles with granularity and dynamic rules. ABAC is more complex initially but excels where RBAC fails.*
  • Metaphor for a 5-Year-Old:
    Your keycard system is good, but what about the rule "you can only eat snacks that you brought"? Your "Manager" keycard can't know which snacks are yours. Or "you can only enter the water-slide room if you are wearing a swimsuit." The keycard doesn't know what you're wearing. We need a smart guard who can look at your keycard, then look at the snack's label, and then look at what you're wearing, before making a decision.

  • Python Code Snippet (Stretching RBAC to its Limit):
    Here's how you might try to solve the "edit own comment" problem with RBAC, showing the awkwardness.

    # We add a new permission to our policy
    RBAC_POLICY['member']['permissions'].append('edit:own_comment')
    
    @app.route('/comments/<int:comment_id>', methods=['PUT'])
    def edit_comment_stretched_rbac(comment_id):
        user = MockUser(user_id=int(request.headers.get("X-User-ID")), role=request.headers.get("X-User-Role"))
        comment = COMMENTS.get(comment_id)
    
        can_edit_any = has_permission(user.role, 'edit:comment')
        can_edit_own = has_permission(user.role, 'edit:own_comment')
        is_author = user.id == comment['author_id']
    
        # --- AWKWARD LOGIC ---
        # The authorization logic is back in the endpoint, defeating the purpose of the decorator.
        if can_edit_any or (can_edit_own and is_author):
            return jsonify({'message': f'Comment {comment_id} updated.'}), 200
        else:
            return jsonify({'message': 'Permission denied.'}), 403
    
    # To test:
    # curl -X PUT -H "X-User-Role: member" -H "X-User-ID: 10" http://127.0.0.1:5000/comments/1 -> OK
    # curl -X PUT -H "X-User-Role: member" -H "X-User-ID: 99" http://127.0.0.1:5000/comments/1 -> Fails
    


4. The Power of Context: Implementing Attribute-Based Access Control (ABAC) (Harder)

  • Technical Explanation:
    ABAC makes authorization decisions by evaluating policies against the attributes of four components:

    1. Subject: The user trying to perform the action (e.g., user.role, user.department, user.clearance_level).
    2. Resource: The object the action is being performed on (e.g., document.sensitivity, expense_report.amount).
    3. Action: The action being attempted (e.g., read, write, approve).
    4. Environment: The context of the request (e.g., time_of_day, ip_address, device_type).

    Instead of a static list of permissions, an ABAC policy engine evaluates rules like:
    ALLOW action='edit' ON resource.type='comment' IF subject.id == resource.author_id

    This is incredibly powerful. The policies are now expressive functions that can encode complex business rules. In code, this means our policy definitions move from being simple lists of strings to functions that return True or False.

How To Handle Permissions Like A Senior Dev

The Web Dev Simplified video provides one of the best practical ABAC implementations I've seen. We will model our code on this approach, where policies are defined as boolean values or functions.

First, watch from 19:01 to 20:54 to understand the four core components of ABAC. Then, study the implementation from 24:17 to 28:36 very carefully. This section is key. Notice how he defines policies for different resources and how the permissions for the 'user' role are functions that accept both the user and the resource to make a decision.

  • Metaphor for a 5-Year-Old:
    We fire the guy who just checks keycards and hire a very smart robot guard (our ABAC engine). We give the robot a dynamic rulebook. For the snack room, the rule says: "To enter, check the person's keycard. If it's a 'Manager' keycard, let them in. If it's a 'Member' keycard, ask them which snack they want to eat. Look at the label on the snack. If the person's name matches the name on the snack label, let them in. Otherwise, say no." The robot can check anything and follow complex rules.

  • Python Code Snippet (The "Elite" ABAC Way):
    This implementation is inspired by the video and shows the true power of ABAC.

    # --- CENTRALIZED ABAC POLICY ---
    # Permissions are now functions that receive the user and resource!
    def is_author(user, resource):
        return user.id == resource.get('author_id')
    
    ABAC_POLICY = {
        'admin': {
            'delete:comment': lambda user, resource: True # Admins can do anything
        },
        'moderator': {
            'delete:comment': lambda user, resource: True
        },
        'member': {
            # A member can only delete a comment if they are the author.
            'delete:comment': is_author
        }
    }
    
    def check_abac_permission(user, permission, resource):
        """Evaluates the ABAC policy for the user and resource."""
        policy_for_role = ABAC_POLICY.get(user.role, {})
        permission_rule = policy_for_role.get(permission)
        
        if not permission_rule:
            return False
        
        # The rule is a function, so we execute it with the context.
        return permission_rule(user, resource)
    
    def abac_permission_required(permission):
        def decorator(f):
            @wraps(f)
            def decorated_function(*args, **kwargs):
                user_role = request.headers.get("X-User-Role")
                user_id = int(request.headers.get("X-User-ID"))
                user = MockUser(user_id=user_id, role=user_role)
                
                comment_id = kwargs['comment_id']
                resource = COMMENTS.get(comment_id)
                
                if not check_abac_permission(user, permission, resource):
                    return jsonify({'message': 'Permission denied by ABAC policy.'}), 403
                
                return f(*args, **kwargs)
            return decorated_function
        return decorator
    
    @app.route('/abac/comments/<int:comment_id>', methods=['DELETE'])
    @abac_permission_required('delete:comment')
    def delete_comment_abac(comment_id):
        # The business logic is once again clean and separated.
        # All complex rules are in the central policy.
        return jsonify({'message': f'Comment {comment_id} deleted.'}), 200
    
    # To test:
    # curl -X DELETE -H "X-User-Role: member" -H "X-User-ID: 20" http://127.0.0.1:5000/abac/comments/2 -> OK
    # curl -X DELETE -H "X-User-Role: member" -H "X-User-ID: 99" http://127.0.0.1:5000/abac/comments/2 -> Fails
    


5. Elite Deep Dive: Relationship-Based Access Control (ReBAC) and Google's Zanzibar (Elite)

  • Technical Explanation:
    While ABAC is powerful, some authorization questions are about connections between entities. This is where Relationship-Based Access Control (ReBAC) shines. ReBAC defines permissions based on a graph of relationships.

    The canonical example is Google's Zanzibar, the authorization system behind Google Docs, YouTube, and Cloud. A user can access a document if:

    • The user is the owner of the document.
    • The user is a member of a group, and that group is a viewer of the document.
    • The user is an editor of the folder that contains the document.

    ReBAC answers the question "Can User U access Resource R?" by traversing a graph of stored relationships, called tuples. A tuple is a simple fact, like (user:ann, relation:editor, object:doc:roadmap). The authorization check becomes a graph traversal problem to find if a path exists between the user and the resource.

Google Zanzibar Authorization System: Deep Dive and Implementation Tips

For a deep dive into the system that defines modern authorization at scale, this talk on Google's Zanzibar is essential. It explains the concepts of tuples, relations, and namespaces that are at the heart of ReBAC.

Watch from 05:35 to 11:21. This segment introduces ReBAC as the model behind Zanzibar and then breaks down the core concepts: how authorization is reframed as a relationship check, what tuples are, and how they combine with an authorization model (namespace) to grant access.

  • Metaphor for a 5-Year-Old:
    Think of Facebook. You can see a photo because:

    1. You are friends with the person who posted it. (Direct relationship)
    2. You are tagged in the photo. (Direct relationship)
    3. You are a member of a group where the photo was shared. (Indirect relationship)
      ReBAC is like a detective who traces all these connections on a giant map to see if you are connected to the photo in any allowed way.
  • Python Code Snippet (Conceptual ReBAC Check):
    Implementing a full Zanzibar is a massive undertaking. This conceptual code demonstrates the core logic of checking a set of relationship tuples.

    # A database of relationship tuples: (subject, relation, object)
    RELATIONSHIP_TUPLES = {
        ('user:ann', 'owner', 'doc:plan'),
        ('user:bob', 'member', 'group:devs'),
        ('group:devs#member', 'viewer', 'doc:plan'), # '#member' means 'members of this group'
        ('user:carl', 'editor', 'folder:projects'),
        ('doc:plan', 'parent', 'folder:projects'),
    }
    
    # The authorization model defines how relations imply other relations
    AUTH_MODEL = {
        'doc': {
            'viewer': ['owner', 'editor'], # An owner or editor is also a viewer
            'parent': None
        },
        'folder': {
            'editor': ['owner']
        }
    }
    
    def check_rebac(subject, relation, obj):
        """Conceptual check to see if a relationship exists, traversing the graph."""
        # Direct check
        if (subject, relation, obj) in RELATIONSHIP_TUPLES:
            return True
    
        # Check for implied relations (e.g., editor is also a viewer)
        implied_by = AUTH_MODEL.get(obj.split(':')[0], {}).get(relation, [])
        for implied_relation in implied_by:
            if check_rebac(subject, implied_relation, obj):
                return True
    
        # Check for group membership
        for s, r, o in RELATIONSHIP_TUPLES:
            if o == obj and r == relation and '#member' in s:
                group = s.split('#')[0]
                if check_rebac(subject, 'member', group):
                    return True
        
        # Check for parent inheritance (very simplified)
        for s, r, o in RELATIONSHIP_TUPLES:
            if s == obj and r == 'parent':
                parent_obj = o
                if check_rebac(subject, relation, parent_obj):
                    return True
    
        return False
    
    # --- Test Cases ---
    print(f"Can Ann view the plan? {check_rebac('user:ann', 'viewer', 'doc:plan')}") # True (owner)
    print(f"Can Bob view the plan? {check_rebac('user:bob', 'viewer', 'doc:plan')}") # True (via group)
    print(f"Can Carl view the plan? {check_rebac('user:carl', 'viewer', 'doc:plan')}") # False (editor of folder, but we didn't implement that inheritance fully)
    
Test your understanding! (Interview-Style Question)

You are designing the authorization system for a multi-tenant SaaS application like Slack or Notion. Tenants (companies) need to manage their own users and permissions. How would you combine RBAC, ABAC, and ReBAC to create a comprehensive and flexible authorization model?

Show answer

This is a classic senior system design question. The best answer involves layering the models, using each for what it does best. The Design Gurus article (LINK) provides a perfect framework for this answer.

  1. RBAC as the Foundation for Tenant Administration:

    • Purpose: To manage coarse-grained permissions within a tenant. This is intuitive for customers.
    • Implementation: Each tenant defines its own set of roles (e.g., Admin, Member, Billing Manager). A User is assigned a Role within a specific Tenant.
    • Core Check: User U has Role R in Tenant T. The Role R grants a set of permissions like invite:user, create:channel, manage:billing. This provides the essential tenant isolation and basic administrative structure.
  2. ABAC for Fine-Grained, Contextual Policies:

    • Purpose: To enforce rules that RBAC can't handle without role explosion.
    • Implementation: Layer ABAC policies on top of RBAC permissions.
    • Examples:
      • Resource Attribute: A Member can archive:channel, but only if channel.is_active == True.
      • User Attribute: A Member with user.department == 'Legal' can view documents tagged confidentiality:legal.
      • Environment Attribute: Access to audit logs might be denied if the request.ip_address is outside the company's VPN range.
    • Core Check: Access is granted if (RBAC check passes) AND (all applicable ABAC policies evaluate to True).
  3. ReBAC for Collaboration and Sharing:

    • Purpose: To manage permissions based on dynamic relationships, which is the essence of a collaborative tool.
    • Implementation: Model sharing and team structures as a graph.
    • Examples:
      • A user can view a private channel because they are a member of that channel.
      • A user can edit a document because it was shared_with them directly with editor rights.
      • A user can see a post because they are a member of @project-team, and the post was mentioned in @project-team.
    • Core Check: The final authorization decision is (RBAC pass OR ReBAC pass) AND (ABAC policies pass). This means a user has access either because their role grants it broadly, OR because of a specific relationship, as long as no overriding ABAC rule denies it.

This layered approach provides a powerful, scalable, and maintainable authorization system suitable for a complex multi-tenant application.



Conclusion

Today, you've journeyed from simple, brittle if statements to the sophisticated graph-based models used by Google. You now have a framework for analyzing and designing authorization systems, a critical skill for building secure, large-scale software.

Key Takeaways:

  • RBAC is a great starting point for managing permissions through roles, but it's inflexible for context-dependent rules.
  • ABAC solves RBAC's limitations by evaluating policies based on attributes of the subject, resource, action, and environment, enabling highly dynamic and fine-grained control.
  • ReBAC excels at modeling complex collaboration and sharing scenarios by treating permissions as a graph of relationships between entities.
  • Production-grade systems often layer these models, using RBAC for administration, ReBAC for collaboration, and ABAC for contextual security policies.

Next Lesson Preview

We've secured our system with authentication and authorization. However, as systems scale, complex queries—especially those involving authorization checks like "list all documents this user can see"—can put immense strain on our primary database. This creates a performance bottleneck.

In our next lesson, we will explore a powerful architectural pattern to solve this: Command Query Responsibility Segregation (CQRS). You'll learn how to separate your read and write workloads to build highly responsive and scalable applications.