KAHIBARO
Discord Login Register

14.3. Permission-Based Access Control

Understanding Permission-Based Access Control

Role based access control (RBAC) gives users roles like admin or user. Permission based access control (PBAC) goes one level deeper and works with fine grained actions such as read:users or delete:orders. In backend development, you often combine both ideas, but here we focus on the permission side itself.

This chapter assumes you already understand users and permissions in general, and basic role based access control. We will focus on what is specific to permission based access control and how to implement it in a backend API.


What Is Permission-Based Access Control?

In permission based access control, the central concept is the permission, not the role.

A permission describes a specific allowed action on a specific type of resource, for example:

You then assign permissions to users, roles, or both. When the user calls your API, your backend checks whether the user has the required permission for that operation.

Key idea: In PBAC, access decisions are based on individual permissions, not only on high level roles.

Typical features of PBAC:

PBAC is often implemented on top of RBAC:

You can also assign permissions directly to users for exceptions.


Permission Models and Naming

Designing Permissions

You need a consistent way to name permissions. A simple and popular pattern is:

$resource:$action

Examples:

ResourceActionPermission string
usersreadusers:read
usersupdateusers:update
orderscreateorders:create
orderscancelorders:cancel
productsdeleteproducts:delete

For more complex systems, you might add a third piece:

$resource:$action:$scope

Examples:

ResourceActionScopePermission
ordersreadownorders:read:own
ordersreadallorders:read:all
usersreadprofileusers:read:profile

Rule: Choose a simple, consistent naming scheme for permissions and use it everywhere in your code and database.

CRUD-based Permissions

For APIs built around standard CRUD operations, you often define permissions like:

For example:

You can also define special actions that are not CRUD:

Static vs Dynamic Permissions

There are two common approaches:

  1. Static permission list in code
    • You define a central list like:
python
     PERMISSIONS = [
         "users:read",
         "users:update",
         "orders:read",
         "orders:create",
         "orders:cancel",
     ]
  1. Dynamic permissions in database
    • Permissions are rows in a permissions table
    • Admins can create new permissions from an admin UI
    • More flexible, but also more complex

For beginners, keeping a static list in code and linking it to database rows is often a good compromise.


Storing Permissions in the Database

Although exact schema design belongs in database chapters, it is useful to see how a typical PBAC database model looks at a high level.

Common Schema

A common schema for permissions is:

TablePurpose
usersApplication users
rolesNamed roles (admin, editor, viewer)
permissionsPermission definitions (orders:read)
user_rolesMany to many between users and roles
role_permissionsMany to many between roles and permissions
user_permissionsOptional, direct permissions per user

You do not need all of these from day one, but the pattern is useful.

A simplified version:

text
users
  id
  email
  ...
roles
  id
  name              -- e.g. "admin"
permissions
  id
  name              -- e.g. "orders:read"
user_roles
  user_id
  role_id
role_permissions
  role_id
  permission_id
user_permissions
  user_id
  permission_id

With this, you can answer:

Important: Store permissions normalized, do not keep a comma separated string like "orders:read,orders:cancel" in a single column. Use join tables instead.


Checking Permissions in Your Backend

The heart of PBAC is the permission check that happens inside your backend application.

Basic Permission Check Flow

When handling a request, the backend usually:

  1. Authenticates the user (for example via a session or JWT)
  2. Loads all permissions of the user (from DB or from a token)
  3. Checks if the user has the required permission
  4. If not, returns 403 Forbidden

In plain pseudocode:

python
def has_permission(user, permission: str) -> bool:
    return permission in user.permissions

Then in your API endpoint:

python
def cancel_order(order_id, user):
    if not has_permission(user, "orders:cancel"):
        raise ForbiddenError("Missing permission: orders:cancel")
    # continue with cancel logic

Combining RBAC and PBAC

If you have roles, and permissions attached to roles, you often do something like:

  1. Load user roles
  2. Load permissions for all these roles
  3. Add direct user permissions if you support them
  4. Combine into a set

Example flow:

python
def load_user_permissions(user_id: int) -> set[str]:
    role_permissions = query_role_permissions_for_user(user_id)
    direct_permissions = query_direct_permissions_for_user(user_id)
    return set(role_permissions) | set(direct_permissions)

Then:

python
def has_permission(user_id: int, permission: str) -> bool:
    permissions = load_user_permissions(user_id)
    return permission in permissions

You usually add caching to avoid hitting the database for every request, but the concept is the same.

Permission Check Location

You have several places where you can enforce permissions:

LocationExampleProsCons
In route / handlerCheck in each endpointVery explicitRepeated code
As decorator@require_permission("x:y")Reusable, cleanNeeds some framework knowledge
As middlewareCheck certain pathsCentral place, less duplicationHarder to handle very specific conditions
In service layerInside business logic functionsKeeps HTTP layer cleanMust not forget to call it everywhere

Most backends use decorators or framework specific mechanisms to keep APIs clean.


Permissions vs Roles in Practice

Permission based access control is often introduced because RBAC alone becomes too coarse.

Where RBAC Alone Fails

Imagine you only have these roles:

Some real world needs:

If you try to model this with only roles, you might end up creating too many roles:

This becomes unmanageable.

With PBAC, you do:

Then give each user exactly the set of permissions they need, directly or through roles.

Combining RBAC and PBAC

A typical pattern is:

For example:

Users get one or more roles, and possibly extra direct permissions such as reports:export.


Designing Permission Boundaries

Permission based access control is only as good as the way you define boundaries around actions.

Coarse vs Fine Grained Permissions

If you define:

Then every user with this permission can do everything with orders, which is not very safe.

If you define:

You have more flexibility, but also more entities to manage.

A good practice is to:

For example:

Rule: Split permissions when actions have different risk levels or different business owners.

Ownership vs Global Permissions

PBAC focuses on what the user can do, but often you also need to know to which resources they can do it.

For example, an user might be allowed to read only their own orders, not all orders. You can model this in two ways:

  1. Permissions encode scope:
    • orders:read:own
    • orders:read:all

The code then checks both permission and ownership.

  1. Permissions encode only the action, and separate logic enforces ownership.
    • Permission: orders:read
    • Business rule: if the user is not an admin, only show orders where order.user_id == current_user.id

The second method is more common, but for very complex systems the first method can be helpful.


Implementing Permission Checks in a Web API

Here we focus on how permission based access looks from an API and backend perspective.

Example Endpoint Requirements

Imagine a Task Management API with permissions:

Possible policy:

EndpointRequired permission
POST /taskstasks:create
GET /taskstasks:read
GET /tasks/{task_id}tasks:read
PUT /tasks/{task_id}tasks:update
DELETE /tasks/{task_id}tasks:delete

At the code level, you might define a simple decorator:

python
def require_permission(permission: str):
    def decorator(handler):
        def wrapper(request, *args, **kwargs):
            user = request.user
            if permission not in user.permissions:
                raise ForbiddenError(f"Missing permission: {permission}")
            return handler(request, *args, **kwargs)
        return wrapper
    return decorator

Then use it:

python
@require_permission("tasks:create")
def create_task(request):
    ...
@require_permission("tasks:delete")
def delete_task(request, task_id):
    ...

Permissions in JWT Tokens

In token based authentication, you can include permissions in the token payload so you do not need a database query on every request.

Example JWT payload:

json
{
  "sub": "123",
  "email": "alice@example.com",
  "permissions": [
    "tasks:create",
    "tasks:read",
    "tasks:update"
  ],
  "exp": 1735690000
}

At request time:

  1. Verify token
  2. Extract permissions claim
  3. Check if required permission is in this list

Tradeoff:

Often you combine:

Common Patterns and Examples

Permission Hierarchies

Sometimes you want one permission to imply others. For example:

You can implement permission hierarchies in code:

python
PERMISSION_TREE = {
    "orders:admin": [
        "orders:read",
        "orders:create",
        "orders:update",
        "orders:delete",
    ]
}

Then when you load user permissions, expand them:

python
def expand_permissions(perms: set[str]) -> set[str]:
    expanded = set(perms)
    for perm in list(perms):
        children = PERMISSION_TREE.get(perm, [])
        expanded.update(children)
    return expanded

So if a user has orders:admin, checks for orders:read or orders:update will pass.

Feature Flags vs Permissions

Sometimes you see feature flags used in similar ways to permissions. Key differences:

AspectPermissionsFeature Flags
TargetSecurity, access controlGradual rollout, experiments
Who controlsSecurity / product ownersProduct managers, developers
LifetimeLong term, part of policyOften temporary

You might have both:

The permission is checked on the backend. The feature flag might be used in frontend to show or hide new UI elements.

Example: Combining User Attributes and Permissions

PBAC does not replace business rules. For example, a business rule could be:

Your code could look like:

python
def refund_order(order_id: int, user):
    if "orders:refund" not in user.permissions:
        raise ForbiddenError("Missing permission orders:refund")
    order = get_order(order_id)
    if order.age_in_days > 30:
        raise BusinessRuleError("Order too old to refund")
    # process refund

Permissions are about who can try a certain action. Business rules are about when the action is allowed.


Managing Permissions Over Time

In real systems, permissions evolve. You need ways to maintain and migrate them.

Changing or Removing Permissions

If you rename or remove permissions, you must:

  1. Update references in code
  2. Migrate database records
  3. Regenerate or expire tokens if they include old permission names

Example migration:

Migration path:

  1. Add new permissions to permissions table
  2. For each user that had orders:refund, decide which of the new ones they need
  3. Update role and user permission mappings
  4. Remove old permission from code and DB

Auditing and Logging

Permission systems are often used in sensitive contexts. You should log:

Example log entries:

These logs can be crucial for debugging and for security investigations.


Summary

Permission based access control focuses on specific, named permissions like orders:read or tasks:delete. It gives you fine grained control over what users can do and is often built on top of roles that group those permissions.

Key points to remember:

With these patterns, you can design backends that are both flexible and secure, and that adapt as your application's requirements grow more complex.

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!