Policies
The Policy system in Balda.js provides a flexible, type-safe way to implement authorization and access control. Policies are organized by scope and allow you to define reusable authorization handlers that can be used throughout your application.
Overview
The Policy system uses a PolicyManager class that manages multiple policy providers organized by scope. Each scope contains multiple handlers that determine whether a user or entity has access to perform specific actions.
Basic Concepts
Policy Provider
A policy provider is an object that contains multiple authorization handlers. Each handler is a function that takes any arguments and returns a boolean or Promise<boolean>.
type PolicyProvider = {
[K: string]: (...args: any[]) => Promise<boolean> | boolean;
};
Policy Manager
The PolicyManager class manages multiple policy providers, each organized under a scope name. It provides a type-safe canAccess method to check permissions.
Creating a Policy Manager
You can define multiple scopes, each with their own handlers:
export const policyManager = new PolicyManager({
users: {
adminRoute: async (user: { id: string; role: string }) => {
return user.role === 'admin';
},
canView: async (user: { id: string; role: string }, targetUserId: string) => {
return user.id === targetUserId || user.role === 'admin';
},
canEdit: async (user: { id: string; role: string }, targetUserId: string) => {
return user.id === targetUserId || user.role === 'admin';
},
canDelete: async (user: { id: string; role: string }) => {
return user.role === 'admin';
},
},
documents: {
canView: async (user: { id: string; role: string }, document: { id: string; ownerId: string; isPublic: boolean }) => {
if (document.isPublic) {
return true;
}
return document.ownerId === user.id || user.role === 'admin';
},
canEdit: async (user: { id: string; role: string }, document: { id: string; ownerId: string }) => {
return document.ownerId === user.id || user.role === 'admin';
},
canDelete: async (user: { id: string; role: string }, document: { id: string; ownerId: string }) => {
return document.ownerId === user.id || user.role === 'admin';
},
canShare: async (user: { id: string; role: string }, document: { id: string; ownerId: string }) => {
return document.ownerId === user.id || user.role === 'admin';
},
},
});
Using the Policy Manager
Checking Access
Use the canAccess method to check if a user has permission to perform an action:
const user = { id: '1', name: 'John', role: 'admin' };
// Check if user can access admin route
const hasAccess = await policyManager.canAccess('test', 'adminRoute', user); // type safe
if (hasAccess) {
// User has access
}
Policy Decorator
The policy decorator provides a declarative way to attach policy metadata to controllers and route handlers. This allows you to define authorization requirements directly on your classes and methods.
Creating a Decorator
Create a type-safe decorator from your policy manager using the createDecorator method:
export const policy = policyManager.createDecorator();
Class-Level Policies
Apply policies to an entire controller class:
@policy('users', 'adminRoute')
class AdminController {
// All routes in this controller require admin access
}
Method-Level Policies
You can also apply policies to individual methods:
class UserController {
@policy('users', 'canView')
async getUser(req: Request, res: Response) {
// Only users with 'canView' permission can access this route
}
@policy('users', 'canEdit')
async updateUser(req: Request, res: Response) {
// Only users with 'canEdit' permission can access this route
}
@policy('users', 'canDelete')
async deleteUser(req: Request, res: Response) {
// Only users with 'canDelete' permission can access this route
}
}
Combining Class and Method Policies
Policies can be stacked - class-level policies apply to all methods, and method-level policies add additional checks:
@policy('users', 'adminRoute')
class AdminController {
@policy('documents', 'canEdit')
async editDocument(req: Request, res: Response) {
// Requires both 'users.adminRoute' AND 'documents.canEdit' policies
}
}
Controller Example
export const adminRoute = async (req: Request, res: Response) => {
const user = req.user;
const hasAccess = await policyManager.canAccess('users', 'adminRoute', user);
if (!hasAccess) {
return res.forbidden({ message: 'You do not have access to this route' });
}
return res.ok({ message: 'You have access to this route' });
};