npm package: @hemia/iam
Description: Gestión de identidad y acceso para sistemas Hemia
Author: Hemia Technologies
Latest version: 0.0.2

# @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

```bash
npm install @hemia/iam
```

## Uso básico

### Definición de roles y permisos

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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)

```typescript
// 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

```typescript
// 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

```typescript
// 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)

```typescript
export interface Subject {
  id: string;
  roles: Role[];
  attributes?: Record<string, any>;
}
```

### Resource (Recurso protegido)

```typescript
export interface Resource {
  type: string;
  id?: string;
  attributes?: Record<string, any>;
}
```

## Casos de Uso Avanzados

### Políticas ABAC Complejas

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
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