KAHIBARO
Discord Login Register

14.2. Role-Based Access Control

Why Role-Based Access Control Matters

When your application has more than one type of user, you must decide who can do what. For example, an admin can delete any post, but a regular user can only delete their own posts. Role-Based Access Control, usually called RBAC, is a simple and very common way to solve this.

In RBAC you do not assign permissions directly to users. Instead, you create roles, such as admin or editor, and assign permissions to those roles. Then you assign roles to users. This keeps your authorization rules easier to understand and easier to change.

RBAC is especially useful in backend systems because:

Key idea: In RBAC, users get permissions through their roles, not directly.

Core RBAC Concepts

Users, Roles, and Permissions

RBAC is built from three main concepts.

ConceptDescriptionExample values
UserSomeone or something that uses your systemuser_id = 42, email = "alice@example.com"
RoleA named collection of permissions"admin", "editor", "customer"
PermissionA specific action on a specific resource"post:read", "post:create", "user:delete"

Common relationships:

So you have two important many-to-many relationships:

You almost never want Users ↔ Permissions directly, because that becomes hard to manage.

Example: Blog Application

Imagine a blog backend. You might define:

Connect roles to permissions:

RolePermissions
adminpost:* (all post permissions), user:manage
authorpost:create, post:edit:own, post:delete:own, post:read:any
readerpost:read:any

Then assign roles to users:

UserRoles
Alice (id 1)admin
Bob (id 2)author
Carol (id 3)reader
Dave (id 4)author, reader

Now you can decide if Bob can delete a post by checking: Does Bob have a role that includes the permission post:delete:own or post:delete:any?

Roles vs Permissions

Why Not Just Use Roles Everywhere?

Roles are simple to think about, but too many roles can create problems.

Imagine an e-commerce system. You could try to encode everything into roles:

Soon you might need combinations like:

This quickly grows and is hard to maintain.

A better approach:

Then attach many permissions to one role.

Rule: Use few, stable roles and many, detailed permissions.
Do not create a new role for every tiny difference in access.

Mapping Roles to Permissions in Code

In a simple Python backend you might keep a dictionary:

python
ROLE_PERMISSIONS = {
    "admin": {
        "user:read",
        "user:write",
        "post:read",
        "post:create",
        "post:edit:any",
        "post:delete:any",
    },
    "author": {
        "post:read",
        "post:create",
        "post:edit:own",
        "post:delete:own",
    },
    "reader": {
        "post:read",
    },
}

Then the effective permissions for a user with multiple roles is the union of all their role permissions.

python
def get_user_permissions(user_roles: list[str]) -> set[str]:
    permissions: set[str] = set()
    for role in user_roles:
        permissions |= ROLE_PERMISSIONS.get(role, set())
    return permissions

If the user has roles ["author", "reader"], they get all permissions from both roles.

Simple RBAC Data Models

In a real backend you usually store roles and permissions in a database. Here is a common relational model.

RBAC Tables

You can represent RBAC with these tables:

TablePurpose
usersStores user accounts
rolesStores available roles
permissionsStores available permissions
user_rolesMany-to-many link: which roles a user has
role_permissionsMany-to-many link: which perms a role has

Example schemas in SQL-like form:

sql
CREATE TABLE users (
    id          SERIAL PRIMARY KEY,
    email       TEXT UNIQUE NOT NULL,
    password    TEXT NOT NULL  -- hashed
);
CREATE TABLE roles (
    id          SERIAL PRIMARY KEY,
    name        TEXT UNIQUE NOT NULL
);
CREATE TABLE permissions (
    id          SERIAL PRIMARY KEY,
    code        TEXT UNIQUE NOT NULL  -- e.g. 'post:create'
);
CREATE TABLE user_roles (
    user_id     INT REFERENCES users(id),
    role_id     INT REFERENCES roles(id),
    PRIMARY KEY (user_id, role_id)
);
CREATE TABLE role_permissions (
    role_id        INT REFERENCES roles(id),
    permission_id  INT REFERENCES permissions(id),
    PRIMARY KEY (role_id, permission_id)
);

In a beginner backend project you might skip the permissions table and only have:

and hard-code what each role can do in the application code. This is still RBAC, only simpler.

Example Data

Insert some roles:

sql
INSERT INTO roles (name) VALUES
  ('admin'),
  ('author'),
  ('reader');

Insert some permissions:

sql
INSERT INTO permissions (code) VALUES
  ('post:read'),
  ('post:create'),
  ('post:edit:any'),
  ('post:edit:own'),
  ('post:delete:any'),
  ('post:delete:own'),
  ('user:manage');

Connect admin to several permissions:

sql
-- Get role id and permission ids (usually done by code)
-- Example link statements:
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r, permissions p
WHERE r.name = 'admin'
  AND p.code IN (
    'post:read',
    'post:create',
    'post:edit:any',
    'post:delete:any',
    'user:manage'
  );

Assign the author role to user id 2:

sql
INSERT INTO user_roles (user_id, role_id)
SELECT 2, r.id FROM roles r WHERE r.name = 'author';

Your backend logic can now query which permissions a user has, based on their roles.

Enforcing RBAC in API Endpoints

Most of the time you will enforce roles at the API layer. The general pattern is:

  1. Authenticate the request and get the user identity.
  2. Load user roles from the database or from the token.
  3. Decide if the user is allowed to perform this action.
  4. If not allowed, return 403 Forbidden.

Endpoint-Level Role Checks

A simple example in a Python style pseudocode:

python
from fastapi import Depends, HTTPException, status
def require_roles(*required_roles: str):
    def wrapper(user = Depends(get_current_user)):
        user_roles = set(user.roles)  # e.g. ["author", "reader"]
        if not user_roles.intersection(required_roles):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Insufficient role"
            )
        return user
    return wrapper

Use it in an endpoint:

python
@app.get("/admin/dashboard")
def read_admin_dashboard(user = Depends(require_roles("admin"))):
    return {"message": "Hello admin"}

Only users with the admin role can access /admin/dashboard.

Permission-Level Checks

Sometimes roles are too broad. You might want to check for a specific permission, like post:delete:any.

You can create a similar helper:

python
def require_permission(permission: str):
    def wrapper(user = Depends(get_current_user)):
        if permission not in user.permissions:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Missing permission"
            )
        return user
    return wrapper

Then:

python
@app.delete("/posts/{post_id}")
def delete_post(post_id: int, user = Depends(require_permission("post:delete:any"))):
    # only allowed if user has that permission
    ...

You can also combine role and permission checks, for example allow admin or anyone with post:delete:any.

Rule: Authorization checks belong in every endpoint that protects sensitive data or actions.
Do not rely only on frontend checks such as hiding buttons.

Ownership and RBAC

RBAC alone does not solve resource ownership. For example, an author should be able to edit their own posts, but not posts from other authors. You must combine RBAC with ownership checks.

A common pattern:

  1. Check that the user has a role or permission like post:edit:own.
  2. Load the resource from the database.
  3. Check that resource.owner_id == user.id.
  4. If not, return 403 Forbidden.

Example:

python
@app.put("/posts/{post_id}")
def update_post(
    post_id: int,
    data: PostUpdate,
    user = Depends(get_current_user),
):
    post = get_post_by_id(post_id)
    if post is None:
        raise HTTPException(status_code=404, detail="Post not found")
    # Admins can edit any post
    if "admin" in user.roles:
        return update_post_in_db(post, data)
    # Authors can only edit their own posts
    if "author" in user.roles and post.author_id == user.id:
        return update_post_in_db(post, data)
    raise HTTPException(status_code=403, detail="Not allowed to edit this post")

This mixes:

You can refactor these checks into helper functions to keep code clean.

Designing Roles for Real Applications

Start from Real-World Responsibilities

When designing roles, think about how people actually work with your system.

For example, in an online store:

From this you can design roles:

Then define permissions and link them.

Keep Roles Stable, Keep Permissions Flexible

Changing roles later is painful because they are used in:

Permissions are easier to add and change. For example:

You usually do not need a new role for every change, just adjust role-to-permission mappings.

Example Role Set for a Typical SaaS Application

RoleTypical Permissions
adminmanage users, all data, all settings
organization_ownermanage org, billing, invite members, all org data
organization_adminmanage org members and settings, not billing
memberstandard app features
readonlyread-only access to data

Then you define many permissions like:

and assign them to the roles above.

Practical Examples of RBAC Checks

Below are some typical patterns that appear in a backend with RBAC.

Protecting an Admin-Only Endpoint

python
@app.post("/admin/users/{user_id}/ban")
def ban_user(user_id: int, user = Depends(get_current_user)):
    if "admin" not in user.roles:
        raise HTTPException(status_code=403, detail="Admin only")
    # perform ban
    ...

Allowing Multiple Roles

You might have actions that both admin and moderator can do.

python
def has_any_role(user, roles: list[str]) -> bool:
    return bool(set(user.roles).intersection(roles))
@app.delete("/comments/{comment_id}")
def delete_comment(comment_id: int, user = Depends(get_current_user)):
    if not has_any_role(user, ["admin", "moderator"]):
        raise HTTPException(status_code=403, detail="Moderator or admin required")
    ...

Combination of Own vs Any

You can allow:

python
@app.delete("/comments/{comment_id}")
def delete_comment(comment_id: int, user = Depends(get_current_user)):
    comment = get_comment_by_id(comment_id)
    if comment is None:
        raise HTTPException(status_code=404, detail="Comment not found")
    if "admin" in user.roles:
        return delete_comment_in_db(comment_id)
    if comment.author_id == user.id:
        return delete_comment_in_db(comment_id)
    raise HTTPException(status_code=403, detail="Not allowed to delete this comment")

Here admin is handled as a special powerful role, and others are restricted by ownership.

Simple Strategies for Beginners

If you are building your first backend, you can start with a very simple RBAC approach.

Strategy 1: Single Role Field on User

Add a role column in the users table:

sql
ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user';

Allowed values might be:

In code, you check this field directly:

python
def require_admin(user = Depends(get_current_user)):
    if user.role != "admin":
        raise HTTPException(status_code=403, detail="Admin only")
    return user

This is enough for many small projects.

Strategy 2: User ↔ Role Many-to-Many

If you need multiple roles per user, create roles and user_roles tables as shown earlier. Your user object then has a roles: list[str] field.

You can still keep actual permissions hard-coded in dictionaries or code, and only store roles in the database.

Strategy 3: Full RBAC with Permissions

When your application grows, you can add:

Then load the complete permission set into memory or compute it when a user logs in, and include those permissions in the authentication token or in the session.

Common Pitfalls and How to Avoid Them

Pitfall 1: Putting Business Logic in the Frontend Only

If you rely on the frontend to hide or show buttons based on roles, but do not perform checks in the backend, a user can still send forbidden requests using tools like Postman.

Fix: Always enforce RBAC in the backend, in controllers or endpoint functions.

Pitfall 2: Hard-Coding Too Much in Endpoints

If every endpoint has its own custom role checks, your code will become messy.

Fix: Use reusable helpers, decorators, or dependency functions like require_roles or require_permission.

Pitfall 3: Overusing the Admin Role

It is easy to grant admin to testers or developers for convenience, then forget to remove it. If an admin account is compromised, the damage is very large.

Fix:

Pitfall 4: Not Thinking About Ownership

Just checking the role is often not enough. For example, an author should not edit posts of another author.

Fix: Always ask, "Is this action global or resource-specific?" If resource-specific, add ownership checks.

Pitfall 5: Inconsistent Role Names

Using unclear names like power_user, basic, or standard can confuse everyone.

Fix: Use role names that clearly describe responsibilities, for example admin, moderator, support_agent, customer.

Summary

Role-Based Access Control is a core concept in backend development. It helps you answer the question "Can this user do this action?" in a structured way.

Remember:

With a good RBAC design, your backend becomes safer, easier to maintain, and easier to extend when new user types and features are added.

Views: 4

Comments

Please login to add a comment.

Don't have an account? Register now!