Authorization = Policies, not decorators sprinkled across the code
Authorization lives in testable policy functions — not scattered `@RequireRole` decorators on controllers.
nestjs-auth-policies-not-decorators
Why it matters
| Stake | If ignored |
|---|---|
| Agent gets lost |
|
| Data leak |
|
| Hidden coupling |
|
How to fix
Not @RequireRole('admin'), not @CanAccess('sites:write') on the controller. A permission is a question about a user and a resource, and the answer lives in a policy function you can test and reuse.
Examples
ts
@Controller('sites')
export class SitesController {
@Patch(':id')
@RequireRole('admin') // ← admin of what?
async update(@Param('id') id: string) { /* ... */ }
@Delete(':id')
@RequirePermission('sites:delete') // ← but the owner can also delete
async delete(@Param('id') id: string) { /* ... */ }
}ts
// libs/sites-management/backend-feature/src/lib/sites.policy.ts
@Injectable()
export class SitesPolicy {
canRead(user: User, site: Site): boolean {
return site.isPublic || site.ownerId === user.id || user.role === 'admin';
}
canUpdate(user: User, site: Site): boolean {
return site.ownerId === user.id || user.role === 'admin';
}
canDelete(user: User, site: Site): boolean {
if (site.protected) return user.role === 'admin';
return site.ownerId === user.id;
}
}
// in the service
async update(siteId: string, user: User, dto: UpdateSiteDto) {
const site = await this.repo.findById(siteId);
if (!this.policy.canUpdate(user, site)) throw new ForbiddenException();
return this.repo.update(siteId, dto);
}