Introduction
"Admins can view everything, but regular users only their own data" — implementing permissions ad hoc leads to gaps. Use RBAC to systematically manage permissions. Generate designs with Claude Code.
CLAUDE.md RBAC Rules
## RBAC Design Rules
### Role Design
- Principle of least privilege: grant only minimum necessary permissions
- Role hierarchy: admin > manager > member > guest
- Prohibit cross-tenant roles (manage roles per tenant)
### Permission Checks
- Always check roles in controller layer (not Service layer)
- Implement resource owner check separately (distinct from roles)
- Insufficient permissions return 403 (distinguish from auth error 401)
### Auditing
- Always record audit log for permission changes
- Log failed access attempts
- In production, require 2 approvals for permission changes
Generated RBAC Implementation
// src/authorization/abilities.ts
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
export function defineAbilitiesFor(user: AuthUser): AppAbility {
const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
switch (user.role) {
case 'admin':
can('manage', 'all');
break;
case 'manager':
can('manage', 'Order');
can('read', 'User');
can('manage', 'Product');
can('read', 'Report');
cannot('delete', 'User'); // Managers cannot delete users
break;
case 'member':
can('read', 'Order', { userId: user.id }); // Own orders only
can('create', 'Order');
can('update', 'Order', { userId: user.id, status: 'pending' });
can('read', 'Product');
break;
case 'guest':
can('read', 'Product');
break;
}
return build({ detectSubjectType: (s) => s.__typename ?? s.constructor.name });
}
// src/authorization/middleware.ts
export function authorize(action: Action, subjectType: Subject) {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.user) return res.status(401).json({ error: 'Authentication required' });
const ability = defineAbilitiesFor(req.user);
const resourceId = req.params.id;
if (resourceId) {
const resource = await loadResource(subjectType, resourceId);
if (!resource) return res.status(404).json({ error: 'Resource not found' });
if (ability.cannot(action, subject(subjectType, resource))) {
logger.warn({ userId: req.user.id, action, subject: subjectType, resourceId }, 'Access denied');
return res.status(403).json({ error: 'Forbidden' });
}
req.resource = resource;
} else {
if (ability.cannot(action, subjectType)) return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
Apply to Routes
// All orders: admin + manager only
router.get('/orders', authenticate, requireRole('admin', 'manager'), async (req, res) => {
const orders = await prisma.order.findMany();
res.json(orders);
});
// Order detail: resource-based permission (owner or admin can view)
router.get('/orders/:id', authenticate, authorize('read', 'Order'), async (req, res) => {
res.json(req.resource);
});
// User delete: admin only
router.delete('/users/:id', authenticate, authorize('delete', 'User'), async (req, res) => {
await prisma.user.delete({ where: { id: req.params.id } });
res.status(204).send();
});
Summary
Design RBAC with Claude Code:
- CLAUDE.md — least privilege, no cross-tenant roles, audit permission changes
- Casl library — declarative role definitions (managed as code)
- subject() — pass resource instance for owner checks
- Log access failures as structured logs (use as audit trail)
Review RBAC designs with **Security Pack (¥1,480)* using /security-check at prompt-works.jp*
myouga (@myougatheaxo) — Axolotl VTuber.
Top comments (0)