@hemia/iam
v0.0.2
Published
Gestión de identidad y acceso para sistemas Hemia
Readme
@hemia/iam
Un paquete robusto, flexible y escalable de autorización IAM (Identity and Access Management) basado en RBAC (Role-Based Access Control) y ABAC (Attribute-Based Access Control) inspirado en XACML. Ideal para aplicaciones Node.js y TypeScript que requieren un control granular sobre los accesos.
Características Principales
- ✨ RBAC: Manejo claro de permisos basado en roles.
- ⚖️ ABAC: Políticas dinámicas que permiten evaluaciones detalladas y granulares basadas en atributos.
- 🚀 Integración sencilla con Express y Node.js.
- ⚙️ Fácilmente extensible y personalizable.
Instalación
npm install @hemia/iamUso básico
Definición de roles y permisos
import { Role, Permission } from '@hemia/iam';
const roles: Role[] = [
{
name: 'admin',
permissions: [
{ type: '*', action: '*' },
],
},
{
name: 'user',
permissions: [
{ type: 'user-profile', action: 'view' },
{ type: 'user-profile', action: 'edit' },
],
},
];Definición de políticas ABAC
import { IAMService, Policy } from '@hemia/iam';
const policies: Policy[] = [
{
name: 'OwnProfilePolicy',
conditions: (subject, resource, action) =>
resource.type === 'user-profile' && resource.id === subject.id && ['view', 'edit', 'delete'].includes(action),
},
];
const iamService = new IAMService(policies);Configuración del Contexto IAM
import { IAMContext, IAMService } from '@hemia/iam';
import { JwtManager } from '@hemia/jwt-manager';
import { CacheService } from '@hemia/cache-manager';
// Configurar servicios
const jwtService = new JwtManager();
const cacheService = new CacheService();
const iamService = new IAMService(policies);
// Crear contexto IAM
const iamContext = new IAMContext(
iamService,
jwtService,
cacheService,
'your-secret-key',
{ prefixKey: 'user:', fieldKey: 'userId' }
);Middleware en Express
Ejemplo básico con validación de token y permisos
import express from 'express';
import { IAMMiddleware } from '@hemia/iam';
import { TokenType } from '@hemia/iam';
const router = express.Router();
// Ruta protegida que requiere autenticación y autorización
router.get('/profile',
IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'view',
resource: { type: 'user-profile' },
system: 'user-management'
}),
(req, res) => {
res.json({
message: 'Perfil del usuario obtenido exitosamente',
user: req.user
});
}
);Ejemplo con múltiples tipos de token
// Acepta tanto tokens de credenciales como tokens de sesión
router.post('/admin/users',
IAMMiddleware(iamContext, [TokenType.CREDENTIALS, TokenType.TOKEN], {
action: 'create',
resource: { type: 'user' },
system: 'admin-panel'
}),
(req, res) => {
// Crear nuevo usuario
res.json({ message: 'Usuario creado exitosamente' });
}
);Ejemplo con recurso dinámico basado en parámetros
// El ID del recurso se obtiene de los parámetros de la URL
router.put('/users/:id',
IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'edit',
resource: {
type: 'user',
getId: (req) => req.params.id
},
system: 'user-management'
}),
(req, res) => {
const userId = req.params.id;
// Actualizar usuario
res.json({ message: `Usuario ${userId} actualizado exitosamente` });
}
);Ejemplo con sujeto personalizado
// Configurar sujeto personalizado para validaciones específicas
router.delete('/projects/:projectId/members/:memberId',
IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'remove',
resource: {
type: 'project-member',
getId: (req) => req.params.memberId
},
subject: {
get: (req) => req.user,
getId: (req) => req.user?.id
},
system: 'project-management'
}),
(req, res) => {
// Remover miembro del proyecto
res.json({ message: 'Miembro removido del proyecto' });
}
);Ejemplo con autenticación opcional
// Permite acceso sin token pero con funcionalidad limitada
router.get('/public/posts',
IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'view',
resource: { type: 'post' },
system: 'blog',
auth: {
allowNoToken: true,
skipAuthorization: false
}
}),
(req, res) => {
if (req.user) {
// Usuario autenticado - mostrar posts personalizados
res.json({ posts: getPersonalizedPosts(req.user) });
} else {
// Usuario anónimo - mostrar posts públicos
res.json({ posts: getPublicPosts() });
}
}
);Ejemplo saltando autorización (solo autenticación)
// Solo valida el token pero no verifica permisos
router.get('/dashboard',
IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'view',
resource: { type: 'dashboard' },
system: 'main-app',
auth: {
skipAuthorization: true
}
}),
(req, res) => {
// Cualquier usuario autenticado puede acceder
res.json({
message: 'Dashboard cargado',
user: req.user
});
}
);Ejemplo con router de API completo
// Router completo para gestión de usuarios
const userRouter = express.Router();
// Listar usuarios (solo administradores)
userRouter.get('/',
IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'list',
resource: { type: 'user' },
system: 'admin-panel'
}),
getUsersController
);
// Obtener usuario específico
userRouter.get('/:id',
IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'view',
resource: {
type: 'user',
getId: (req) => req.params.id
},
system: 'user-management'
}),
getUserController
);
// Crear nuevo usuario
userRouter.post('/',
IAMMiddleware(iamContext, [TokenType.CREDENTIALS], {
action: 'create',
resource: { type: 'user' },
system: 'admin-panel'
}),
createUserController
);
// Actualizar usuario
userRouter.put('/:id',
IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'edit',
resource: {
type: 'user',
getId: (req) => req.params.id
},
system: 'user-management'
}),
updateUserController
);
// Eliminar usuario
userRouter.delete('/:id',
IAMMiddleware(iamContext, [TokenType.CREDENTIALS], {
action: 'delete',
resource: {
type: 'user',
getId: (req) => req.params.id
},
system: 'admin-panel'
}),
deleteUserController
);
// Montar el router
app.use('/api/users', userRouter);Ejemplo con manejo de errores personalizado
// Middleware con manejo de errores específico
router.get('/sensitive-data',
IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'access',
resource: { type: 'sensitive-data' },
system: 'security'
}),
(req, res) => {
try {
const data = getSensitiveData(req.user.id);
res.json({ data });
} catch (error) {
res.status(500).json({
message: 'Error al obtener datos sensibles',
error: error.message
});
}
}
);
// Middleware de manejo de errores global
app.use((err, req, res, next) => {
if (err.code === 'FORBIDDEN') {
return res.status(403).json({
message: 'Acceso denegado',
required: err.required,
system: err.system
});
}
if (err.status === 401) {
return res.status(401).json({
message: 'Token de autorización requerido o inválido'
});
}
next(err);
});Estructura del Modelo
Subject (Usuario autenticado)
export interface Subject {
id: string;
roles: Role[];
attributes?: Record<string, any>;
}Resource (Recurso protegido)
export interface Resource {
type: string;
id?: string;
attributes?: Record<string, any>;
}Casos de Uso Avanzados
Políticas ABAC Complejas
const advancedPolicies: Policy[] = [
{
name: 'OwnerOrAdminPolicy',
conditions: (subject, resource, action) => {
// El propietario puede hacer cualquier cosa con sus recursos
if (resource.id === subject.id) return true;
// Los administradores pueden hacer cualquier cosa
return subject.roles.some(role => role.name === 'admin');
}
},
{
name: 'BusinessHoursPolicy',
conditions: (subject, resource, action) => {
const now = new Date();
const hour = now.getHours();
// Operaciones sensibles solo en horario laboral (9-17)
if (['delete', 'admin'].includes(action)) {
return hour >= 9 && hour <= 17;
}
return true;
}
},
{
name: 'DepartmentAccessPolicy',
conditions: (subject, resource, action) => {
// Acceso basado en departamento
if (resource.attributes?.department) {
return subject.attributes?.department === resource.attributes.department;
}
return true;
}
}
];Configuración de Múltiples Sistemas
// Diferentes configuraciones para diferentes módulos
const createSystemMiddleware = (system: string) => {
return (action: string, resourceType: string, options = {}) => {
return IAMMiddleware(iamContext, [TokenType.TOKEN], {
action,
resource: { type: resourceType },
system,
...options
});
};
};
// Middleware específico por sistema
const adminMiddleware = createSystemMiddleware('admin-panel');
const userMiddleware = createSystemMiddleware('user-management');
const reportMiddleware = createSystemMiddleware('reporting');
// Uso en routers
router.get('/admin/stats', adminMiddleware('view', 'statistics'), getStatsController);
router.get('/users/profile', userMiddleware('view', 'user-profile'), getProfileController);
router.get('/reports/sales', reportMiddleware('generate', 'sales-report'), getSalesReportController);Middleware Condicional
// Aplicar diferentes niveles de seguridad según el entorno
const conditionalSecurity = (req: Request, res: Response, next: NextFunction) => {
const isProduction = process.env.NODE_ENV === 'production';
const isHighSecurity = req.path.includes('/admin') || req.path.includes('/financial');
if (isProduction && isHighSecurity) {
// En producción, rutas de alta seguridad requieren credenciales
return IAMMiddleware(iamContext, [TokenType.CREDENTIALS], {
action: 'access',
resource: { type: 'high-security' },
system: 'security'
})(req, res, next);
} else {
// Otras rutas solo requieren token de sesión
return IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'access',
resource: { type: 'standard' },
system: 'general'
})(req, res, next);
}
};
router.use('/admin/*', conditionalSecurity);
router.use('/financial/*', conditionalSecurity);Mejores Prácticas
1. Organización de Middlewares
// middlewares/auth.ts
export const authMiddlewares = {
// Autenticación básica
basic: IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'access',
resource: { type: 'basic' },
system: 'general',
auth: { skipAuthorization: true }
}),
// Solo administradores
adminOnly: IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'admin',
resource: { type: 'admin-resource' },
system: 'admin-panel'
}),
// Propietario del recurso
ownerOnly: (resourceType: string) => IAMMiddleware(iamContext, [TokenType.TOKEN], {
action: 'owner-access',
resource: {
type: resourceType,
getId: (req) => req.params.id
},
system: 'ownership'
})
};2. Manejo de Errores Centralizados
// middlewares/errorHandler.ts
export const iamErrorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
// Log de errores de seguridad
if (err.status === 403 || err.code === 'FORBIDDEN') {
console.warn(`🚫 Access denied: ${req.method} ${req.path}`, {
user: req.user?.id,
required: err.required,
system: err.system,
ip: req.ip
});
}
// Respuestas consistentes
if (err.status === 401) {
return res.status(401).json({
code: 'UNAUTHORIZED',
message: 'Authentication required'
});
}
if (err.status === 403 || err.code === 'FORBIDDEN') {
return res.status(403).json({
code: 'FORBIDDEN',
message: 'Insufficient permissions',
required: err.required
});
}
next(err);
};3. Testing del Middleware
// tests/middleware.test.ts
import request from 'supertest';
import { app } from '../app';
describe('IAM Middleware', () => {
test('should deny access without token', async () => {
const response = await request(app)
.get('/api/users')
.expect(401);
expect(response.body.code).toBe('UNAUTHORIZED');
});
test('should allow access with valid token and permissions', async () => {
const token = generateTestToken({ roles: ['admin'] });
const response = await request(app)
.get('/api/users')
.set('Authorization', `Bearer ${token}`)
.expect(200);
});
test('should deny access with insufficient permissions', async () => {
const token = generateTestToken({ roles: ['user'] });
const response = await request(app)
.delete('/api/users/123')
.set('Authorization', `Bearer ${token}`)
.expect(403);
expect(response.body.code).toBe('FORBIDDEN');
});
});Ejemplo de verificación de autorización
const subject: Subject = {
id: 'user123',
roles: [roles[1]],
attributes: { department: 'marketing' },
};
const resource: Resource = { type: 'user-profile', id: 'user123' };
const allowed = await iamService.can(subject, resource, 'view');
console.log(allowed ? 'Acceso concedido' : 'Acceso denegado');Licencia
MIT — © Hemia Technologies
