Careeroza
RBAC with a Roles Guard
Authentication vs Authorization
Authentication answers "who are you?" (401 if unknown). Authorization answers "what may you do?" (403 if not allowed). RBAC (role-based access control) gives each user roles, and each route lists the roles it needs.
| Route | customer | seller | admin |
|---|---|---|---|
GET /products | Yes | Yes | Yes |
POST /products | No | Yes (own store) | Yes |
GET /orders/:id | Own orders | Orders for own products | All |
PATCH /orders/:id/status | No | Yes (ship) | Yes |
GET /admin/users | No | No | Yes |
The @Roles() decorator and RolesGuard were built in the medium level (Guards module). With the global JwtAuthGuard from the Authentication module running first, request.user.roles is always available to the roles guard.
Role Hierarchy
Example
Often an admin should pass every seller check. A small hierarchy keeps @Roles() lists short:
type Role = 'customer' | 'seller' | 'admin';
const inherits: Record<Role, Role[]> = { customer: [], seller: ['customer'], admin: ['seller', 'customer'] };
function expand(roles: Role[]): Set<Role> {
const out = new Set<Role>();
const visit = (r: Role) => { if (!out.has(r)) { out.add(r); inherits[r].forEach(visit); } };
roles.forEach(visit);
return out;
}
function canActivate(userRoles: Role[], required: Role[] | undefined): boolean {
if (!required?.length) return true;
const effective = expand(userRoles);
return required.some((r) => effective.has(r));
}
const routes: [string, Role[] | undefined][] = [
['GET /products', undefined],
['POST /products', ['seller']],
['GET /admin/users', ['admin']],
];
for (const user of [['customer'], ['seller'], ['admin']] as Role[][]) {
console.log(user[0].padEnd(8), routes.map(([r, req]) => r + '=' + (canActivate(user, req) ? 'allow' : '403')).join(' '));
}Output
customer GET /products=allow POST /products=403 GET /admin/users=403 seller GET /products=allow POST /products=allow GET /admin/users=403 admin GET /products=allow POST /products=allow GET /admin/users=allow