[RBAC-001] feat: Complete RBAC middleware with permission caching
- Add permission-cache.service.ts for Redis-based permission caching (5min TTL) - Complete requirePermission middleware with cache check and DB fallback - Add rbac.middleware.ts with requirePerm, requireAnyPerm, requireAllPerms, requireAccess - Export new service from shared/services/index.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
01c5d98c54
commit
a9abe0876f
@ -3,6 +3,12 @@ import jwt from 'jsonwebtoken';
|
|||||||
import { config } from '../../config/index.js';
|
import { config } from '../../config/index.js';
|
||||||
import { AuthenticatedRequest, JwtPayload, UnauthorizedError, ForbiddenError } from '../types/index.js';
|
import { AuthenticatedRequest, JwtPayload, UnauthorizedError, ForbiddenError } from '../types/index.js';
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
|
import { permissionsService } from '../../modules/roles/permissions.service.js';
|
||||||
|
import {
|
||||||
|
checkCachedPermission,
|
||||||
|
getCachedPermissions,
|
||||||
|
setCachedPermissions,
|
||||||
|
} from '../services/permission-cache.service.js';
|
||||||
|
|
||||||
// Re-export AuthenticatedRequest for convenience
|
// Re-export AuthenticatedRequest for convenience
|
||||||
export { AuthenticatedRequest } from '../types/index.js';
|
export { AuthenticatedRequest } from '../types/index.js';
|
||||||
@ -73,26 +79,78 @@ export function requirePermission(resource: string, action: string) {
|
|||||||
throw new UnauthorizedError('Usuario no autenticado');
|
throw new UnauthorizedError('Usuario no autenticado');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { userId, tenantId, roles } = req.user;
|
||||||
|
|
||||||
// Superusers bypass permission checks
|
// Superusers bypass permission checks
|
||||||
if (req.user.roles.includes('super_admin')) {
|
if (roles.includes('super_admin')) {
|
||||||
|
logger.debug('Permission bypassed (super_admin)', { userId, resource, action });
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Check permission in database
|
// Check cache first
|
||||||
// For now, we'll implement this when we have the permission checking service
|
const cachedResult = await checkCachedPermission(tenantId, userId, resource, action);
|
||||||
logger.debug('Permission check', {
|
|
||||||
userId: req.user.userId,
|
if (cachedResult !== null) {
|
||||||
|
if (cachedResult) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
logger.warn('Access denied (cached)', {
|
||||||
|
userId,
|
||||||
|
tenantId,
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
roles,
|
||||||
|
});
|
||||||
|
throw new ForbiddenError(`No tiene permiso para ${action} en ${resource}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss - check database
|
||||||
|
const hasPermission = await permissionsService.hasPermission(tenantId, userId, resource, action);
|
||||||
|
|
||||||
|
if (hasPermission) {
|
||||||
|
// Warm up cache with all user permissions for future requests
|
||||||
|
const effectivePermissions = await permissionsService.getEffectivePermissions(tenantId, userId);
|
||||||
|
await setCachedPermissions(tenantId, userId, effectivePermissions);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission denied - log and throw
|
||||||
|
logger.warn('Access denied', {
|
||||||
|
userId,
|
||||||
|
tenantId,
|
||||||
resource,
|
resource,
|
||||||
action,
|
action,
|
||||||
|
roles,
|
||||||
});
|
});
|
||||||
|
throw new ForbiddenError(`No tiene permiso para ${action} en ${resource}`);
|
||||||
next();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preload user permissions into cache (call after login)
|
||||||
|
*/
|
||||||
|
export async function preloadUserPermissions(tenantId: string, userId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const cached = await getCachedPermissions(tenantId, userId);
|
||||||
|
if (cached) {
|
||||||
|
return; // Already cached
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = await permissionsService.getEffectivePermissions(tenantId, userId);
|
||||||
|
await setCachedPermissions(tenantId, userId, permissions);
|
||||||
|
logger.debug('User permissions preloaded', { tenantId, userId, count: permissions.length });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error preloading user permissions', {
|
||||||
|
error: (error as Error).message,
|
||||||
|
tenantId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function optionalAuth(
|
export function optionalAuth(
|
||||||
req: AuthenticatedRequest,
|
req: AuthenticatedRequest,
|
||||||
_res: Response,
|
_res: Response,
|
||||||
|
|||||||
294
src/shared/middleware/rbac.middleware.ts
Normal file
294
src/shared/middleware/rbac.middleware.ts
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
import { Response, NextFunction } from 'express';
|
||||||
|
import { AuthenticatedRequest, UnauthorizedError, ForbiddenError } from '../types/index.js';
|
||||||
|
import { logger } from '../utils/logger.js';
|
||||||
|
import { permissionsService } from '../../modules/roles/permissions.service.js';
|
||||||
|
import {
|
||||||
|
checkCachedPermission,
|
||||||
|
getCachedPermissions,
|
||||||
|
setCachedPermissions,
|
||||||
|
} from '../services/permission-cache.service.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse permission code format "resource:action" or "resource.action"
|
||||||
|
*/
|
||||||
|
function parsePermissionCode(code: string): { resource: string; action: string } {
|
||||||
|
const separator = code.includes(':') ? ':' : '.';
|
||||||
|
const [resource, action] = code.split(separator);
|
||||||
|
if (!resource || !action) {
|
||||||
|
throw new Error(`Invalid permission code format: ${code}. Expected "resource:action" or "resource.action"`);
|
||||||
|
}
|
||||||
|
return { resource, action };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has permission (helper function)
|
||||||
|
*/
|
||||||
|
async function userHasPermission(
|
||||||
|
tenantId: string,
|
||||||
|
userId: string,
|
||||||
|
roles: string[],
|
||||||
|
resource: string,
|
||||||
|
action: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
// Super admin bypass
|
||||||
|
if (roles.includes('super_admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
const cachedResult = await checkCachedPermission(tenantId, userId, resource, action);
|
||||||
|
if (cachedResult !== null) {
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check database
|
||||||
|
const hasPermission = await permissionsService.hasPermission(tenantId, userId, resource, action);
|
||||||
|
|
||||||
|
// Warm up cache
|
||||||
|
if (!hasPermission) {
|
||||||
|
const permissions = await permissionsService.getEffectivePermissions(tenantId, userId);
|
||||||
|
await setCachedPermissions(tenantId, userId, permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand middleware for permission check using code format
|
||||||
|
* @param code Permission in format "resource:action" (e.g., "invoices:create")
|
||||||
|
*/
|
||||||
|
export function requirePerm(code: string) {
|
||||||
|
const { resource, action } = parsePermissionCode(code);
|
||||||
|
|
||||||
|
return async (req: AuthenticatedRequest, _res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
throw new UnauthorizedError('Usuario no autenticado');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId, tenantId, roles } = req.user;
|
||||||
|
|
||||||
|
const hasPermission = await userHasPermission(tenantId, userId, roles, resource, action);
|
||||||
|
|
||||||
|
if (hasPermission) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn('Access denied (requirePerm)', {
|
||||||
|
userId,
|
||||||
|
tenantId,
|
||||||
|
permission: code,
|
||||||
|
roles,
|
||||||
|
});
|
||||||
|
throw new ForbiddenError(`No tiene permiso: ${code}`);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require ANY of the specified permissions (OR logic)
|
||||||
|
* @param codes Permission codes (e.g., "invoices:read", "invoices:list")
|
||||||
|
*/
|
||||||
|
export function requireAnyPerm(...codes: string[]) {
|
||||||
|
const permissions = codes.map(parsePermissionCode);
|
||||||
|
|
||||||
|
return async (req: AuthenticatedRequest, _res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
throw new UnauthorizedError('Usuario no autenticado');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId, tenantId, roles } = req.user;
|
||||||
|
|
||||||
|
// Super admin bypass
|
||||||
|
if (roles.includes('super_admin')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each permission
|
||||||
|
for (const { resource, action } of permissions) {
|
||||||
|
const hasPermission = await userHasPermission(tenantId, userId, roles, resource, action);
|
||||||
|
if (hasPermission) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn('Access denied (requireAnyPerm)', {
|
||||||
|
userId,
|
||||||
|
tenantId,
|
||||||
|
permissions: codes,
|
||||||
|
roles,
|
||||||
|
});
|
||||||
|
throw new ForbiddenError(`No tiene ninguno de los permisos requeridos: ${codes.join(', ')}`);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require ALL of the specified permissions (AND logic)
|
||||||
|
* @param codes Permission codes (e.g., "invoices:read", "payments:create")
|
||||||
|
*/
|
||||||
|
export function requireAllPerms(...codes: string[]) {
|
||||||
|
const permissions = codes.map(parsePermissionCode);
|
||||||
|
|
||||||
|
return async (req: AuthenticatedRequest, _res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
throw new UnauthorizedError('Usuario no autenticado');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId, tenantId, roles } = req.user;
|
||||||
|
|
||||||
|
// Super admin bypass
|
||||||
|
if (roles.includes('super_admin')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all permissions
|
||||||
|
const missingPermissions: string[] = [];
|
||||||
|
for (let i = 0; i < permissions.length; i++) {
|
||||||
|
const { resource, action } = permissions[i];
|
||||||
|
const hasPermission = await userHasPermission(tenantId, userId, roles, resource, action);
|
||||||
|
if (!hasPermission) {
|
||||||
|
missingPermissions.push(codes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingPermissions.length === 0) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn('Access denied (requireAllPerms)', {
|
||||||
|
userId,
|
||||||
|
tenantId,
|
||||||
|
required: codes,
|
||||||
|
missing: missingPermissions,
|
||||||
|
roles,
|
||||||
|
});
|
||||||
|
throw new ForbiddenError(`Faltan permisos: ${missingPermissions.join(', ')}`);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: AuthenticatedRequest, _res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
throw new UnauthorizedError('Usuario no autenticado');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId, tenantId, roles } = req.user;
|
||||||
|
|
||||||
|
// Super admin bypass
|
||||||
|
if (roles.includes('super_admin')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission first (if specified)
|
||||||
|
if (parsedPermission) {
|
||||||
|
const hasPermission = await userHasPermission(
|
||||||
|
tenantId,
|
||||||
|
userId,
|
||||||
|
roles,
|
||||||
|
parsedPermission.resource,
|
||||||
|
parsedPermission.action
|
||||||
|
);
|
||||||
|
if (hasPermission) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to role check (if specified)
|
||||||
|
if (requiredRoles && requiredRoles.length > 0) {
|
||||||
|
const hasRole = requiredRoles.some(role => roles.includes(role));
|
||||||
|
if (hasRole) {
|
||||||
|
logger.debug('Access granted via role (hybrid mode)', {
|
||||||
|
userId,
|
||||||
|
tenantId,
|
||||||
|
permission,
|
||||||
|
matchedRoles: requiredRoles.filter(r => roles.includes(r)),
|
||||||
|
});
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn('Access denied (requireAccess)', {
|
||||||
|
userId,
|
||||||
|
tenantId,
|
||||||
|
permission,
|
||||||
|
requiredRoles,
|
||||||
|
userRoles: roles,
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorDetails = [];
|
||||||
|
if (permission) errorDetails.push(`permiso: ${permission}`);
|
||||||
|
if (requiredRoles) errorDetails.push(`roles: ${requiredRoles.join(', ')}`);
|
||||||
|
throw new ForbiddenError(`Acceso denegado. Requiere ${errorDetails.join(' o ')}`);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: AuthenticatedRequest, _res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
throw new UnauthorizedError('Usuario no autenticado');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId, tenantId, roles } = req.user;
|
||||||
|
const resourceOwnerId = req.params[ownerField] || req.body?.[ownerField];
|
||||||
|
|
||||||
|
// Super admin bypass
|
||||||
|
if (roles.includes('super_admin')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is the owner
|
||||||
|
if (resourceOwnerId === userId) {
|
||||||
|
logger.debug('Access granted (owner)', { userId, resource, action });
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
const hasPermission = await userHasPermission(tenantId, userId, roles, resource, action);
|
||||||
|
if (hasPermission) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn('Access denied (requirePermOrOwner)', {
|
||||||
|
userId,
|
||||||
|
tenantId,
|
||||||
|
permission: code,
|
||||||
|
resourceOwnerId,
|
||||||
|
roles,
|
||||||
|
});
|
||||||
|
throw new ForbiddenError(`No tiene permiso: ${code}`);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -4,3 +4,12 @@ export {
|
|||||||
FeatureFlagContext,
|
FeatureFlagContext,
|
||||||
featureFlagService,
|
featureFlagService,
|
||||||
} from './feature-flags.service';
|
} from './feature-flags.service';
|
||||||
|
|
||||||
|
export {
|
||||||
|
getCachedPermissions,
|
||||||
|
setCachedPermissions,
|
||||||
|
invalidateUserPermissions,
|
||||||
|
invalidateTenantPermissions,
|
||||||
|
checkCachedPermission,
|
||||||
|
permissionCacheService,
|
||||||
|
} from './permission-cache.service';
|
||||||
|
|||||||
173
src/shared/services/permission-cache.service.ts
Normal file
173
src/shared/services/permission-cache.service.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import { redisClient, isRedisConnected } from '../../config/redis.js';
|
||||||
|
import { logger } from '../utils/logger.js';
|
||||||
|
import { EffectivePermission } from '../../modules/roles/permissions.service.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for caching user permissions in Redis
|
||||||
|
* Key format: permissions:{tenantId}:{userId}
|
||||||
|
* TTL: 5 minutes (300 seconds)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CACHE_TTL = 300; // 5 minutes
|
||||||
|
const CACHE_PREFIX = 'permissions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the cache key for user permissions
|
||||||
|
*/
|
||||||
|
function buildKey(tenantId: string, userId: string): string {
|
||||||
|
return `${CACHE_PREFIX}:${tenantId}:${userId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached permissions for a user
|
||||||
|
* @returns Array of effective permissions or null if not cached
|
||||||
|
*/
|
||||||
|
export async function getCachedPermissions(
|
||||||
|
tenantId: string,
|
||||||
|
userId: string
|
||||||
|
): Promise<EffectivePermission[] | null> {
|
||||||
|
if (!isRedisConnected()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = buildKey(tenantId, userId);
|
||||||
|
const cached = await redisClient.get(key);
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
logger.debug('Permission cache hit', { tenantId, userId });
|
||||||
|
return JSON.parse(cached) as EffectivePermission[];
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Permission cache miss', { tenantId, userId });
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error reading permission cache', {
|
||||||
|
error: (error as Error).message,
|
||||||
|
tenantId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache permissions for a user
|
||||||
|
*/
|
||||||
|
export async function setCachedPermissions(
|
||||||
|
tenantId: string,
|
||||||
|
userId: string,
|
||||||
|
permissions: EffectivePermission[]
|
||||||
|
): Promise<void> {
|
||||||
|
if (!isRedisConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = buildKey(tenantId, userId);
|
||||||
|
await redisClient.setex(key, CACHE_TTL, JSON.stringify(permissions));
|
||||||
|
logger.debug('Permissions cached', {
|
||||||
|
tenantId,
|
||||||
|
userId,
|
||||||
|
count: permissions.length,
|
||||||
|
ttl: CACHE_TTL,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error caching permissions', {
|
||||||
|
error: (error as Error).message,
|
||||||
|
tenantId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate cached permissions for a specific user
|
||||||
|
* Call this when user roles or permissions change
|
||||||
|
*/
|
||||||
|
export async function invalidateUserPermissions(
|
||||||
|
tenantId: string,
|
||||||
|
userId: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (!isRedisConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = buildKey(tenantId, userId);
|
||||||
|
await redisClient.del(key);
|
||||||
|
logger.info('User permission cache invalidated', { tenantId, userId });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error invalidating user permission cache', {
|
||||||
|
error: (error as Error).message,
|
||||||
|
tenantId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate cached permissions for all users in a tenant
|
||||||
|
* Call this when role permissions are modified at tenant level
|
||||||
|
*/
|
||||||
|
export async function invalidateTenantPermissions(tenantId: string): Promise<void> {
|
||||||
|
if (!isRedisConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pattern = `${CACHE_PREFIX}:${tenantId}:*`;
|
||||||
|
let cursor = '0';
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const [nextCursor, keys] = await redisClient.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
|
||||||
|
cursor = nextCursor;
|
||||||
|
|
||||||
|
if (keys.length > 0) {
|
||||||
|
await redisClient.del(...keys);
|
||||||
|
deletedCount += keys.length;
|
||||||
|
}
|
||||||
|
} while (cursor !== '0');
|
||||||
|
|
||||||
|
logger.info('Tenant permission cache invalidated', {
|
||||||
|
tenantId,
|
||||||
|
keysDeleted: deletedCount,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error invalidating tenant permission cache', {
|
||||||
|
error: (error as Error).message,
|
||||||
|
tenantId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific permission exists in cached permissions
|
||||||
|
* Returns null if not in cache (need to check DB)
|
||||||
|
*/
|
||||||
|
export async function checkCachedPermission(
|
||||||
|
tenantId: string,
|
||||||
|
userId: string,
|
||||||
|
resource: string,
|
||||||
|
action: string
|
||||||
|
): Promise<boolean | null> {
|
||||||
|
const permissions = await getCachedPermissions(tenantId, userId);
|
||||||
|
|
||||||
|
if (permissions === null) {
|
||||||
|
return null; // Not cached, need DB check
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions.some(p => p.resource === resource && p.action === action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export service object for convenience
|
||||||
|
*/
|
||||||
|
export const permissionCacheService = {
|
||||||
|
get: getCachedPermissions,
|
||||||
|
set: setCachedPermissions,
|
||||||
|
invalidateUser: invalidateUserPermissions,
|
||||||
|
invalidateTenant: invalidateTenantPermissions,
|
||||||
|
checkPermission: checkCachedPermission,
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user