Authorisation and Role-Based Access Control
Authorisation answers: what is this authenticated user allowed to do? A blog platform has visitors (can read), authors (can write), editors (can approve), and admins (can delete). Each role has different capabilities.
A common mistake: checking if a user is authenticated but not checking if they own the resource. "User is logged in, so they can edit this post." But what if the post belongs to someone else? Authorisation is about resource-level access control.
Role-Based Access Control (RBAC)
Users are assigned roles (admin, editor, viewer). Roles have permissions (can create post, can approve post, can view analytics). When a user requests an action, check if their role has that permission.
Database schema:
- Users table: id, email, password hash
- Roles table: id, name (admin, editor, viewer)
- User-Role relationship: user_id, role_id
- Permissions table: id, name (create_post, edit_post, delete_post)
- Role-Permission relationship: role_id, permission_id
In code:
- Load user and their roles
- Check if any role has the required permission
- If yes, allow; if no, return 403 Forbidden
Permission-Based Systems
An alternative: skip roles, assign permissions directly to users. User 123 can create_post, edit_post. User 456 can only view_post.
This is more flexible but less maintainable. With 100 users and 50 permissions, managing individual permissions becomes tedious. Roles are a layer of abstraction that makes this manageable.
Most systems use roles as the primary mechanism, with option for permission overrides when needed.
The Least-Privilege Principle
Grant only the permissions necessary for the task. Don't make everyone an admin by default. Don't give users more permissions than they need.
This limits damage if an account is compromised. If a user account is hacked and the attacker is only an editor, they can edit posts but not delete them or access user data.
Row-Level Security
Role-based permissions control coarse-grained access (can you edit posts?). Row-level security controls fine-grained access (can you edit this specific post?).
Example: an author can edit their own posts but not others' posts. An admin can edit any post. This requires checking: does the post belong to this user?
Implementation: when a user requests to edit a post, check if they own it (or are an admin):
post = Post.find(id)
if post.user_id != current_user.id and !current_user.admin?
return 403 Forbidden
update post
Row-level security is critical for multi-user systems. Without it, one user can access another user's data.
Multi-Tenancy Authorization
In a multi-tenant system, each customer's data must be completely isolated. A user from company A should never see company B's data, regardless of their role.
Implementation: every query filters by tenant_id. A user's request includes their tenant_id (from their account). All queries add a where clause: tenant_id = user.tenant_id.
This is so critical it should be enforced at the database level when possible. A bug here is a security disaster.
Where Authorization Happens
Authorization must be checked on the server. Front-end visibility controls are UX, not security. A developer can bypass your front-end restrictions by crafting API requests.
Never trust client-side authorization. Always verify on the server before performing sensitive actions.
Common Authorization Mistakes
- Only checking if user is authenticated: A user is logged in, so they can do anything. You must also check if they have permission.
- Not checking resource ownership: User can edit posts, so they can edit any post. You must verify they own the post.
- Client-side only: Trusting the front-end to enforce permissions. An attacker bypasses the front-end.
- Missing tenant isolation: Multi-tenant systems without proper isolation are a data-leak ticking bomb.
- Overly permissive defaults: Assuming users are admins unless proven otherwise, instead of assuming they're viewers unless granted permission.
Authorization Decision Framework
When implementing a feature, ask:
- Is the user authenticated? (Do we know who they are?)
- Is the user authorized? (Do they have a role/permission for this action?)
- Do they own the resource? (Is this their data?)
- Are we in the right tenant? (Is this data in their organization?)
If any of these fails, return 403 Forbidden. If all pass, proceed.
Authorization Libraries
For simple role checks, built-in functionality is sufficient. For complex authorization, libraries help:
- Pundit (Ruby): Policy objects for authorization
- django-guardian (Python): Object-level permissions
- Casbin (multi-language): Policy engine for complex authorization rules
For most applications, a simple role-based check is sufficient. Only use a library if you have genuinely complex authorization needs.
The Principle
Authorization is about trust boundaries. Trust the user to be who they say (authentication). Don't trust them to be allowed to do what they're asking (authorization). Check every time.
Good authorization prevents data leaks, fraud, and system abuse. It's essential, often overlooked, and incredibly important.