Authorization: RBAC, ABAC & Best Practices
Role-Based Access Control (RBAC)
RBAC assigns permissions to roles, then roles to users. Simple to manage for most applications.
// Roles and permissions
const ROLES = {
admin: ['read', 'write', 'delete', 'manage_users'],
editor: ['read', 'write'],
viewer: ['read'],
moderator: ['read', 'write', 'delete_comments'],
} as const
// Simple RBAC check
function can(user: User, action: string): boolean {
const permissions = ROLES[user.role] ?? []
return permissions.includes(action)
}
// Hierarchical RBAC — roles inherit from parent roles
const ROLE_HIERARCHY = {
admin: ['editor', 'viewer'],
editor: ['viewer'],
viewer: [],
}
// Resource-level RBAC — can user X do action Y on resource Z?
async function authorize(user: User, action: string, resource: Resource): Promise<boolean> {
if (user.role === 'admin') return true
if (action === 'read' && resource.isPublic) return true
if (action === 'write' && resource.ownerId === user.id) return true
return false
}
// Middleware example (Express)
function requireRole(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!roles.includes(req.user.role))
return res.status(403).json({ error: 'Forbidden' })
next()
}
}
app.delete('/api/users/:id', requireRole('admin'), deleteUser)Attribute-Based Access Control (ABAC)
ABAC evaluates policies based on attributes of the user, resource, and environment. More flexible than RBAC for complex rules.
// Policy-based authorization
interface AuthContext {
user: { id: string; role: string; department: string; clearanceLevel: number }
resource: { ownerId: string; classification: string; department: string }
environment: { time: Date; ipAddress: string }
}
// Policies are pure functions — easy to test
const policies = {
'document:read': (ctx: AuthContext) =>
ctx.resource.classification === 'public' ||
ctx.user.department === ctx.resource.department ||
ctx.user.clearanceLevel >= 3,
'document:delete': (ctx: AuthContext) =>
ctx.user.role === 'admin' ||
(ctx.user.id === ctx.resource.ownerId && ctx.user.department === ctx.resource.department),
'system:access': (ctx: AuthContext) =>
ctx.environment.time.getHours() >= 9 &&
ctx.environment.time.getHours() < 18, // business hours only
}
function authorize(action: string, ctx: AuthContext): boolean {
return policies[action]?.(ctx) ?? false
}
// Open Policy Agent (OPA) — external policy engine
// Rego policy language; decouples policy from application codeRow-Level Security
// Always filter data by ownership — never return all rows to non-admins
async function getPosts(userId: string, isAdmin: boolean) {
if (isAdmin) return db.posts.findMany() // admin sees all
return db.posts.findMany({ where: { authorId: userId }}) // user sees own
}
// PostgreSQL Row Level Security (RLS) — enforce at DB level
// CREATE POLICY user_isolation ON posts
// USING (author_id = current_user_id());
// ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
// Supabase uses RLS extensively
// CREATE POLICY "Users can view own posts" ON posts
// FOR SELECT USING (auth.uid() = user_id);Best Practices
Principle of least privilege: grant minimum permissions needed. Default deny — explicitly grant access.
Check authorization on every request: don't rely on UI hiding. Always enforce on the server.
BOLA / IDOR: verify object ownership on every resource endpoint. GET /orders/123 must verify order 123 belongs to the requesting user.
Centralize authorization logic: don't scatter if (user.role === "admin") checks — use a policy/guard layer.
Audit log: log all authorization failures and sensitive actions (who, what, when, from where).
Separate AuthN and AuthZ: authentication verifies identity; authorization grants permissions. Keep the code separate.
Re-verify on sensitive operations: even with a valid session, require re-authentication for password change, payment, account deletion.
Test authorization: explicitly test that non-owners, non-admins, and unauthenticated users are denied. Happy path tests alone are not enough.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free