[RBAC-005] feat: Propagate RBAC middleware from erp-core
- Add rbac.middleware.ts with requirePerm, requireAnyPerm, requireAllPerms, requireAccess, requirePermOrOwner - Add shared middleware index exports - Configure glass-specific roles (manager, production, sales, admin, super_admin) Propagated from erp-core TASK-2026-01-30-RBAC Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bbbc562128
commit
6a005a6225
6
src/shared/middleware/index.ts
Normal file
6
src/shared/middleware/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Shared Middleware - Exports
|
||||
* ERP Vidrio Templado
|
||||
*/
|
||||
|
||||
export * from './rbac.middleware';
|
||||
304
src/shared/middleware/rbac.middleware.ts
Normal file
304
src/shared/middleware/rbac.middleware.ts
Normal file
@ -0,0 +1,304 @@
|
||||
/**
|
||||
* RBAC Middleware - Role-Based Access Control
|
||||
* Propagated from erp-core (TASK-2026-01-30-RBAC)
|
||||
*
|
||||
* Provides permission-based authorization with hybrid role fallback
|
||||
* for gradual migration from role-based to permission-based access control.
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
/**
|
||||
* Parse permission code format "resource:action" or "resource.action"
|
||||
*/
|
||||
function parsePermissionCode(code: string): { resource: string; action: string } {
|
||||
const separator = code.includes(':') ? ':' : '.';
|
||||
const parts = code.split(separator);
|
||||
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
||||
throw new Error(`Invalid permission code format: ${code}. Expected "resource:action" or "resource.action"`);
|
||||
}
|
||||
return { resource: parts[0], action: parts[1] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has permission (role-based fallback for verticals without full RBAC)
|
||||
*/
|
||||
async function userHasPermission(
|
||||
roles: string[],
|
||||
resource: string,
|
||||
action: string
|
||||
): Promise<boolean> {
|
||||
// Super admin bypass
|
||||
if (roles.includes('super_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Admin has all permissions except system:manage
|
||||
if (roles.includes('admin') && !(resource === 'system' && action === 'manage')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Manager has CRUD on most resources
|
||||
if (roles.includes('manager')) {
|
||||
const managerResources = [
|
||||
'orders', 'quotes', 'products', 'inventory', 'customers',
|
||||
'production', 'cutting', 'tempering', 'reports', 'employees'
|
||||
];
|
||||
if (managerResources.includes(resource)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Production worker has limited access
|
||||
if (roles.includes('production')) {
|
||||
const productionResources = ['orders', 'production', 'cutting', 'tempering'];
|
||||
const allowedActions = ['read', 'list', 'update'];
|
||||
if (productionResources.includes(resource) && allowedActions.includes(action)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Sales has customer and quote access
|
||||
if (roles.includes('sales')) {
|
||||
const salesResources = ['customers', 'quotes', 'orders', 'products'];
|
||||
if (salesResources.includes(resource)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand middleware for permission check using code format
|
||||
* @param code Permission in format "resource:action" (e.g., "orders:create")
|
||||
*/
|
||||
export function requirePerm(code: string) {
|
||||
const { resource, action } = parsePermissionCode(code);
|
||||
|
||||
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
const user = (req as any).user;
|
||||
|
||||
if (!user) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: { code: 'NOT_AUTHENTICATED', message: 'Authentication required' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const roles = user.roles || [];
|
||||
const hasPermission = await userHasPermission(roles, resource, action);
|
||||
|
||||
if (hasPermission) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: { code: 'FORBIDDEN', message: `No tiene permiso: ${code}` },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Require ANY of the specified permissions (OR logic)
|
||||
* @param codes Permission codes (e.g., "orders:read", "orders:list")
|
||||
*/
|
||||
export function requireAnyPerm(...codes: string[]) {
|
||||
const permissions = codes.map(parsePermissionCode);
|
||||
|
||||
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
const user = (req as any).user;
|
||||
|
||||
if (!user) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: { code: 'NOT_AUTHENTICATED', message: 'Authentication required' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const roles = user.roles || [];
|
||||
|
||||
// Super admin bypass
|
||||
if (roles.includes('super_admin')) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check each permission
|
||||
for (const { resource, action } of permissions) {
|
||||
const hasPermission = await userHasPermission(roles, resource, action);
|
||||
if (hasPermission) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: { code: 'FORBIDDEN', message: `No tiene ninguno de los permisos: ${codes.join(', ')}` },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Require ALL of the specified permissions (AND logic)
|
||||
* @param codes Permission codes (e.g., "orders:read", "production:update")
|
||||
*/
|
||||
export function requireAllPerms(...codes: string[]) {
|
||||
const permissions = codes.map(parsePermissionCode);
|
||||
|
||||
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
const user = (req as any).user;
|
||||
|
||||
if (!user) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: { code: 'NOT_AUTHENTICATED', message: 'Authentication required' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const roles = user.roles || [];
|
||||
|
||||
// Super admin bypass
|
||||
if (roles.includes('super_admin')) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check all permissions
|
||||
const missingPermissions: string[] = [];
|
||||
for (let i = 0; i < permissions.length; i++) {
|
||||
const { resource, action } = permissions[i];
|
||||
const hasPermission = await userHasPermission(roles, resource, action);
|
||||
if (!hasPermission) {
|
||||
missingPermissions.push(codes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingPermissions.length === 0) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: { code: 'FORBIDDEN', message: `Faltan permisos: ${missingPermissions.join(', ')}` },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hybrid access control for gradual migration from role-based to permission-based
|
||||
* Allows access if user has ANY of the specified roles OR has the specified permission
|
||||
* @param options.roles Legacy roles to check (fallback)
|
||||
* @param options.permission Permission code to check (preferred)
|
||||
*/
|
||||
export function requireAccess(options: { roles?: string[]; permission?: string }) {
|
||||
const { roles: requiredRoles, permission } = options;
|
||||
const parsedPermission = permission ? parsePermissionCode(permission) : null;
|
||||
|
||||
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
const user = (req as any).user;
|
||||
|
||||
if (!user) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: { code: 'NOT_AUTHENTICATED', message: 'Authentication required' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const roles = user.roles || [];
|
||||
|
||||
// Super admin bypass
|
||||
if (roles.includes('super_admin')) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check permission first (if specified)
|
||||
if (parsedPermission) {
|
||||
const hasPermission = await userHasPermission(
|
||||
roles,
|
||||
parsedPermission.resource,
|
||||
parsedPermission.action
|
||||
);
|
||||
if (hasPermission) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to role check (if specified)
|
||||
if (requiredRoles && requiredRoles.length > 0) {
|
||||
const hasRole = requiredRoles.some(role => roles.includes(role));
|
||||
if (hasRole) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const errorDetails = [];
|
||||
if (permission) errorDetails.push(`permiso: ${permission}`);
|
||||
if (requiredRoles) errorDetails.push(`roles: ${requiredRoles.join(', ')}`);
|
||||
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: { code: 'FORBIDDEN', message: `Acceso denegado. Requiere ${errorDetails.join(' o ')}` },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check permission for resource owner (user can access their own resources)
|
||||
* @param code Permission code for non-owners
|
||||
* @param ownerField Field name in request params that contains owner ID (default: 'userId')
|
||||
*/
|
||||
export function requirePermOrOwner(code: string, ownerField: string = 'userId') {
|
||||
const { resource, action } = parsePermissionCode(code);
|
||||
|
||||
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
const user = (req as any).user;
|
||||
|
||||
if (!user) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: { code: 'NOT_AUTHENTICATED', message: 'Authentication required' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = user.sub || user.userId;
|
||||
const roles = user.roles || [];
|
||||
const resourceOwnerId = req.params[ownerField] || req.body?.[ownerField];
|
||||
|
||||
// Super admin bypass
|
||||
if (roles.includes('super_admin')) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is the owner
|
||||
if (resourceOwnerId === userId) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check permission
|
||||
const hasPermission = await userHasPermission(roles, resource, action);
|
||||
if (hasPermission) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: { code: 'FORBIDDEN', message: `No tiene permiso: ${code}` },
|
||||
});
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user