Build a Blog API with Authentication โ€ข Lesson 4

Authorization Levels

Implement role-based access control (RBAC) to manage user permissions. Create admin, author, and reader roles with different access levels.

๐ŸŽฏ What You'll Learn

  • โ€ขImplement role-based access control (RBAC)
  • โ€ขCreate permission decorators and middleware
  • โ€ขBuild admin, author, and reader permission systems
  • โ€ขSecure endpoints with role requirements

Authorization Levels

Welcome to Role-Based Access Control (RBAC)! In this lesson, you'll implement a sophisticated permission system that controls what users can do based on their roles.

๐ŸŽฏ What We're Building

Your authorization system will provide:

  • ๐Ÿ‘‘ Admin users with full system access
  • โœ๏ธ Author users who can create and manage content
  • ๐Ÿ‘€ Reader users with read-only access
  • ๐Ÿ”’ Permission decorators to protect endpoints
  • ๐Ÿ›ก๏ธ Resource ownership validation

๐Ÿ—๏ธ RBAC Fundamentals

What is Role-Based Access Control?

RBAC is a security model that restricts access based on user roles:

  • Roles: Job functions (Admin, Author, Reader)
  • Permissions: Specific actions (create_post, manage_users)
  • Users: Individuals assigned to roles
  • Resources: Things being protected (posts, user profiles)

Role Hierarchy

Admin (Full Access)
โ”œโ”€โ”€ Can manage users
โ”œโ”€โ”€ Can access admin panel
โ”œโ”€โ”€ Can edit/delete any content
โ””โ”€โ”€ Inherits all Author + Reader permissions

Author (Content Creator)
โ”œโ”€โ”€ Can create posts
โ”œโ”€โ”€ Can edit own posts
โ”œโ”€โ”€ Can publish posts
โ””โ”€โ”€ Inherits all Reader permissions

Reader (Basic Access)
โ”œโ”€โ”€ Can read posts
โ”œโ”€โ”€ Can update own profile
โ””โ”€โ”€ Basic access only

๐ŸŽจ Permission Architecture

1. Permission Manager

class PermissionManager:
    def __init__(self):
        # Define role hierarchy
        self.role_hierarchy = {
            UserRole.ADMIN: [UserRole.AUTHOR, UserRole.READER],
            UserRole.AUTHOR: [UserRole.READER],
            UserRole.READER: []
        }
        
        # Define role-specific permissions
        self.permissions = {
            UserRole.READER: ['read_posts', 'update_own_profile'],
            UserRole.AUTHOR: ['create_post', 'update_own_post'],
            UserRole.ADMIN: ['manage_users', 'access_admin_panel']
        }

2. Authorization Decorators

Protect functions with role/permission requirements:

@require_role("admin")
async def admin_function():
    return "Admin only content"

@require_permission("create_post")
async def create_post():
    return "Can create posts"

@require_ownership_or_admin()
async def edit_post(post_id: int, current_user: dict):
    # User can edit their own posts, admin can edit any
    return f"Editing post {post_id}"

3. FastAPI Integration

from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer

security = HTTPBearer()

async def get_current_user(token: str = Depends(security)):
    user = await auth_service.get_current_user(token.credentials)
    if not user:
        raise HTTPException(401, "Invalid token")
    return user

async def require_admin(current_user: dict = Depends(get_current_user)):
    if current_user['role'] != 'admin':
        raise HTTPException(403, "Admin access required")
    return current_user

@app.get("/admin/users")
async def get_users(admin_user: dict = Depends(require_admin)):
    return await get_all_users()

๐Ÿ”’ Security Patterns

1. Principle of Least Privilege

Give users only the minimum access needed:

# โœ… Good: Specific permissions
@require_permission("read_own_posts")
async def get_my_posts():
    pass

# โŒ Bad: Overly broad access
@require_role("admin")  # Too much for reading own posts
async def get_my_posts():
    pass

2. Defense in Depth

Multiple layers of security:

@app.put("/posts/{post_id}")
@require_permission("update_post")  # Layer 1: General permission
async def update_post(
    post_id: int,
    post_data: PostUpdate,
    current_user: dict = Depends(get_current_user)  # Layer 2: Authentication
):
    # Layer 3: Ownership check
    post = await get_post(post_id)
    if post.author_id != current_user['id'] and current_user['role'] != 'admin':
        raise HTTPException(403, "Can only edit your own posts")
    
    return await update_post_in_db(post_id, post_data)

3. Fail-Safe Defaults

Default to denial when unsure:

def can_access_resource(self, user_role: str, permission: str) -> bool:
    # Default to False if anything is unclear
    if not user_role or not permission:
        return False
    
    if user_role not in self.valid_roles:
        return False
        
    return permission in self.get_user_permissions(user_role)

๐Ÿ› ๏ธ Implementation Patterns

1. Decorator Factory Pattern

Create reusable decorators:

def require_role(required_role: str):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            current_user = get_current_user()
            if not has_role_or_higher(current_user['role'], required_role):
                raise Exception(f"Role '{required_role}' required")
            return await func(*args, **kwargs)
        return wrapper
    return decorator

# Usage
@require_role("author")
async def create_post():
    pass

2. Context-Aware Authorization

Check permissions based on context:

def require_ownership_or_admin(resource_field: str = 'author_id'):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            current_user = get_current_user()
            
            # Admin bypass
            if current_user['role'] == 'admin':
                return await func(*args, **kwargs)
            
            # Check ownership
            resource_owner_id = kwargs.get(resource_field)
            if resource_owner_id != current_user['id']:
                raise Exception("Access denied")
                
            return await func(*args, **kwargs)
        return wrapper
    return decorator

3. Permission Inheritance

Higher roles inherit lower permissions:

def get_user_permissions(self, user_role: str) -> List[str]:
    permissions = set()
    
    # Add direct permissions
    permissions.update(self.permissions.get(user_role, []))
    
    # Add inherited permissions
    for inherited_role in self.role_hierarchy.get(user_role, []):
        permissions.update(self.permissions.get(inherited_role, []))
    
    return list(permissions)

๐Ÿงช Testing Authorization

1. Unit Tests for Permissions

def test_permission_hierarchy():
    pm = PermissionManager()
    
    # Admin should have all permissions
    admin_perms = pm.get_user_permissions('admin')
    author_perms = pm.get_user_permissions('author')
    
    assert len(admin_perms) > len(author_perms)
    assert 'create_post' in admin_perms  # Inherited from author
    assert 'read_posts' in admin_perms   # Inherited from reader

def test_decorator_access():
    @require_role('admin')
    async def admin_func():
        return 'success'
    
    # Test with admin user
    set_current_user({'role': 'admin'})
    result = await admin_func()
    assert result == 'success'
    
    # Test with regular user
    set_current_user({'role': 'reader'})
    with pytest.raises(Exception):
        await admin_func()

2. Integration Tests

async def test_endpoint_security():
    # Test admin endpoint
    response = await client.get('/admin/users', 
                              headers={'Authorization': 'Bearer admin_token'})
    assert response.status_code == 200
    
    # Test with insufficient permissions
    response = await client.get('/admin/users',
                              headers={'Authorization': 'Bearer reader_token'})
    assert response.status_code == 403

๐Ÿ’ก Best Practices

1. Clear Permission Names

# โœ… Clear and specific
'create_post', 'update_own_post', 'delete_any_post'

# โŒ Vague or confusing  
'post_access', 'manage_content', 'admin_stuff'

2. Audit Trail

Log authorization decisions:

async def check_permission(user_id: int, permission: str) -> bool:
    result = has_permission(user_id, permission)
    
    # Log the authorization check
    logger.info(f"User {user_id} permission check: {permission} = {result}")
    
    return result

3. Graceful Degradation

Provide meaningful error messages:

@require_role("author")
async def create_post():
    pass

# Instead of generic "Access Denied"
# Provide: "This action requires Author role or higher. You have Reader role."

๐Ÿšจ Common Pitfalls

1. Role Confusion

# โŒ Don't check roles directly everywhere
if user.role == 'admin':
    allow_access()

# โœ… Check permissions instead
if has_permission(user, 'manage_users'):
    allow_access()

2. Missing Edge Cases

# โŒ Assumes user exists
def check_permission(user, permission):
    return permission in user.permissions  # Crashes if user is None

# โœ… Handle edge cases
def check_permission(user, permission):
    if not user or not permission:
        return False
    return permission in user.get('permissions', [])

3. Inconsistent Enforcement

# โŒ Some endpoints protected, others not
@app.get("/posts")  # No protection
async def get_posts(): pass

@app.post("/posts")
@require_permission("create_post")
async def create_post(): pass

# โœ… Consistent protection strategy
@app.get("/posts")
@require_permission("read_posts")
async def get_posts(): pass

๐ŸŽฏ Your Implementation Tasks

  1. Create PermissionManager with role hierarchy
  2. Build authorization decorators for functions
  3. Implement AuthorizationService with token integration
  4. Test all permission combinations thoroughly
  5. Handle edge cases and error scenarios

๐Ÿš€ Real-World Applications

After mastering RBAC, you can apply it to:

  • Multi-tenant applications (organization-based roles)
  • Microservices (service-to-service permissions)
  • API gateways (centralized authorization)
  • Content management (editor/reviewer workflows)

Ready to secure your blog API with bulletproof authorization? Let's build it! ๐Ÿ”’

๐Ÿ’ก Hint

Start by defining the role hierarchy - admins inherit author permissions, authors inherit reader permissions. Use decorators to protect functions and check user roles/permissions before allowing access.

Ready to Practice?

Now that you understand the theory, let's put it into practice with hands-on coding!

Start Interactive Lesson