Security
Permissions & RBAC
Authentication proves who a user is. Authorization (permissions) determines what they can do. This guide covers how to implement fine-grained access control so users access only what they should.
Role-Based Access Control (RBAC)
RBAC is the simplest and most common authorization model. You define roles (admin, editor, viewer) and assign each user a role. The app checks the user's role and grants or denies access.
Example: An admin can edit articles, publish them, and delete them. An editor can create and edit articles but can't publish or delete. A viewer can only read published articles. The role is the unit of authorization—all admins can do the same things.
Defining Roles
Start by thinking about user types. Who are the different groups of people using your app? What can each group do? Document these roles explicitly. Common roles include: Admin (full access), Editor (create/edit content), Reviewer (review/approve), User (personal actions), Guest (read-only). Don't create too many roles—each adds complexity. If you have more than 5-10 roles, you might need a more sophisticated system.
Assigning Roles to Users
Store each user's role in your database. A user has one or more roles (some systems allow multiple). When the user logs in, include their role in the session or token. When checking access, look up the role and allow/deny the operation.
Permission Checks in Code
Implement permission checks at the API level (backend), never relying on frontend checks alone. A frontend might hide a button, but the API should enforce authorization. Check permissions before executing any operation:
if (user.role !== 'admin') { throw UnauthorizedError(); }
For complex logic, use middleware or decorators. Express middleware can check the user's role before the route handler runs. Decorators (@RequireRole("admin")) in Python/Java/TypeScript make permission requirements explicit.
Attribute-Based Access Control (ABAC)
RBAC works well for simple cases, but real-world requirements are often complex. A user might be able to edit articles they wrote but not articles written by others. An admin might be able to edit articles created after a certain date. These requirements don't fit neatly into roles.
Attribute-Based Access Control evaluates attributes of the user, resource, and action. A policy says: "A user can edit an article if they are the author or they are an admin." This is more flexible than RBAC but also more complex to implement and reason about.
Can user edit article? user_id = article.author_id OR user.role = 'admin'
Many apps use hybrid approaches: RBAC for coarse-grained access (can you access the admin panel?) and ABAC for fine-grained access (can you edit this specific document?).
Row-Level Security (RLS)
Row-level security ensures users can only see and modify their own data. This is critical in multi-tenant applications where each customer's data must be isolated.
Example: A user logs in to a CRM. The app queries orders. Without RLS, the user might see all orders from all customers. With RLS, the query is implicitly filtered to show only orders from their customer account.
Implement RLS by always filtering queries by the current user's ID. If you have a multi-tenant SaaS, always filter by tenant_id. Never show data from other tenants, even if the user somehow constructs a request with another tenant's ID.
Permission Matrices
Document who can do what in a permission matrix. Rows are roles, columns are actions. Cells indicate whether that role can perform that action.
| Action | Guest | Editor | Admin |
|---|---|---|---|
| View Articles | ✓ | ✓ | ✓ |
| Create Article | ✗ | ✓ | ✓ |
| Edit Own Article | ✗ | ✓ | ✓ |
| Edit Any Article | ✗ | ✗ | ✓ |
| Delete Article | ✗ | ✗ | ✓ |
| Manage Users | ✗ | ✗ | ✓ |
| View Analytics | ✗ | ✗ | ✓ |
Create this matrix early in development. It becomes your specification for what to implement. As requirements change, update the matrix. Having it documented prevents misunderstandings and makes it easier to audit permissions.
Frontend vs Backend Enforcement
The frontend should check permissions to improve user experience—hide buttons the user can't click, disable forms they can't submit. This makes the app feel responsive and prevents wasted requests.
However, the frontend must never be the only place permissions are checked. An attacker can modify frontend code. Always enforce permissions in the backend. The frontend is a courtesy to honest users; the backend is your security boundary.
Pattern: Frontend checks to decide what UI to show. Backend checks before executing any operation. If the frontend incorrectly hides an action but the backend allows it, that's bad UX but not a security problem. If the backend doesn't check and the frontend hides an action, that's a security vulnerability.
Multi-Tenant Permission Scoping
In SaaS applications, each customer (tenant) is separate. A user in one tenant should never access data from another tenant, even if they have the same role. Scoping ensures this.
Always include the tenant ID in permission checks. A user's role is relative to their tenant. They might be an admin in Tenant A but a viewer in Tenant B. When they log in, include their tenant ID in the session. When checking permissions, verify both the user's role and that they belong to the requested tenant.
Can user access document? user.tenant_id = document.tenant_id AND user.role allows reading
API-Level Authorization
For APIs, check permissions early. Middleware can verify the user's token, extract their role, and make it available to route handlers. Handlers then check if the role allows the operation.
Pattern: Request includes token → Middleware extracts user from token → Middleware checks role is appropriate for the action → Route handler executes with confidence the user is authorized.
For more complex cases, use policy-based authorization libraries (Casbin, Open Policy Agent). Define policies in a declarative language, and the library evaluates them. This makes complex authorization logic testable and auditable.
RBAC vs ABAC: A Comparison
| Aspect | RBAC | ABAC |
|---|---|---|
| Simplicity | Simple, easy to understand | Complex, requires careful design |
| Scalability | Works for small to medium | Works at any scale |
| Flexibility | Rigid—rules tied to roles | Flexible—rules tied to attributes |
| Auditability | Easy to audit (check role) | Harder (multiple conditions) |
| Example | User is admin, can delete | User created article, can edit |
| Maintenance | Add/remove roles | Update attribute policies |
| Best for | Apps with distinct user types | Apps with complex, data-driven rules |
Audit Logging
Log permission-sensitive actions. When a user deletes data, changes permissions, or accesses sensitive information, log it. Include the user, action, resource, timestamp, and whether it was allowed or denied.
Audit logs help with compliance (HIPAA, GDPR require records of who accessed what), incident response (determine what an attacker accessed), and debugging (figure out how a user got unauthorized access). Keep audit logs separate from application logs and ensure they can't be modified by the app.
Libraries and Frameworks
Don't build authorization from scratch. Use libraries designed for it:
Casbin: A popular RBAC/ABAC library supporting multiple languages. Define policies in a simple configuration format, and Casbin evaluates them.
Open Policy Agent (OPA): A general-purpose policy engine. Define policies in Rego (a policy language), and OPA evaluates them. More powerful but steeper learning curve.
Pundit (Ruby): Rails authorization gem. Define policy classes that specify who can do what. Simple and Rails-native.
Permissions are foundational to security. Start with a clear permission matrix, implement RBAC for simplicity, and graduate to ABAC if complexity demands it. Enforce permissions in the backend, audit sensitive actions, and use a tested library rather than rolling your own. Your users will thank you for protecting their data.