[CONSEJO — Doc 10] Contrarian: PayMind procesa el dato más personal que existe en el contexto laboral: el salario de cada empleado. En muchas culturas LATAM, el salario es un dato más privado que el número de cuenta bancaria. Si un empleado descubre que su colega ganó más por el mismo trabajo, el daño relacional puede ser irreversible. El sistema debe garantizar que ningún empleado puede ver el salario de otro empleado, incluso si tienen el mismo nivel de acceso. Auditor Seg.: El escenario de ataque más realista para PayMind no es un hacker externo — es un empleado interno con acceso al sistema que descarga todos los registros de salarios. El RBAC debe limitar estrictamente qué roles pueden ver salarios, y el acceso debe quedar en el audit log con cada consulta. Executor: Compliance de protección de datos para PayMind es más complejo que para los otros módulos porque los "datos personales" son datos de empleados — personas con derechos laborales específicos adicionales a los derechos ARCO generales. En CR, el empleado tiene derecho a solicitar copia de su expediente a la empresa (Art. 69 Código de Trabajo).
1. Modelo de amenazas PayMind
| Amenaza | Vector | Control |
|---|---|---|
| Spoofing | Gerente RRHH accede como otro empleado | MFA + JWT con claim de rol verificado en cada request |
| Tampering | Modificar el salario de un empleado antes de confirmar la planilla | Audit log de cada cambio + aprobación dual para cambios de salario >20% |
| Repudiation | Empleado dice "no aprobé mis vacaciones" | Firma digital del empleado en solicitud self-service |
| Information Disclosure | Empleado ve salarios de colegas | Column-level encryption + RBAC restrictivo + audit log de lectura |
| Elevation of Privilege | Asistente de RRHH accede a función de confirmar planilla | RBAC estricto: solo RRHH Manager y Owner pueden confirmar planillas |
| DoS | Reintento masivo de cálculos de planilla | Rate limit por tenant en endpoint /calcular-borrador |
2. Column-level encryption para datos sensibles
Tres campos requieren cifrado a nivel de columna (no solo disco):
# Datos sensibles cifrados con pgcrypto (key en Doppler, no en código)
CAMPOS_SENSIBLES = {
'pm_empleados': ['cedula_enc', 'salario_mensual_enc', 'iban_enc'],
}
# Acceso al salario — SOLO desde PayrollService, con log de auditoría
class PayrollService:
async def get_salario_empleado(self, empleado_id: UUID, tenant_id: UUID) -> Decimal:
row = await self.db.execute(
"SELECT pgp_sym_decrypt(salario_mensual_enc, :key)::numeric FROM pm_empleados WHERE id = :id AND tenant_id = :tid",
{"key": APP_ENCRYPTION_KEY, "id": empleado_id, "tid": tenant_id}
)
# Log obligatorio
await self.audit_log.log(
accion='pm_salario_accedido',
registro_id=empleado_id,
tenant_id=tenant_id
)
return Decimal(row.scalar())
3. RBAC PayMind
| Rol | Puede ver salarios | Puede editar empleados | Puede confirmar planilla | Puede aprobar vacaciones |
|---|---|---|---|---|
| Owner | ✅ Todos | ✅ | ✅ | ✅ |
| RRHH Manager | ✅ Todos (con audit log) | ✅ | ✅ | ✅ |
| Gerente Financiero | ✅ Solo totales de planilla | ❌ | ❌ | ❌ |
| Jefe de Departamento | ❌ | ❌ | ❌ | ✅ Solo su equipo |
| Empleado self-service | ✅ Solo el propio | ✅ Solo datos propios | ❌ | ✅ Solo propias solicitudes |
| Contador externo / CPA | ✅ Solo totales planilla | ❌ | ❌ | ❌ |
| Cheryx Admin | ❌ (nunca datos de empleados) | ❌ | ❌ | ❌ |
3.1 Enforcement en FastAPI
async def verificar_acceso_salario(
user: CurrentUser,
empleado_id: UUID,
db: AsyncSession
):
if user.rol not in ['owner', 'rrhh_manager']:
if user.rol == 'empleado' and user.empleado_id != empleado_id:
raise PermissionError("No puede acceder al salario de otro empleado")
elif user.rol != 'empleado':
raise PermissionError("Rol no autorizado para ver salarios individuales")
# Si tiene acceso, registrar en audit log antes de retornar
await audit_log.log_salary_access(user.id, empleado_id, user.tenant_id)
4. Inmutabilidad de planillas confirmadas
Una vez que el RRHH confirma una planilla (y se genera el archivo banco), es inmutable:
REVOKE UPDATE ON pm_planillas FROM app_role;
REVOKE DELETE ON pm_planillas FROM app_role;
REVOKE UPDATE ON pm_planilla_detalle FROM app_role;
REVOKE DELETE ON pm_planilla_detalle FROM app_role;
Si hay un error en una planilla ya confirmada (ej: el salario de un empleado estaba mal): 1. No se modifica la planilla original — es inmutable 2. Se crea una planilla de corrección en el siguiente período que incluye el ajuste 3. El audit log registra la corrección con referencia a la planilla original
5. Compliance de protección de datos — datos laborales
5.1 Ley 8968 CR — datos de empleados
Los datos de los empleados son datos personales de los propios empleados (no del tenant). El tenant es el empleador y tiene obligación legal de protegerlos. BookMind (como sistema del empleador) es Encargado de datos del empleador, pero en el caso de PayMind, la situación es más compleja:
| Dato | Titular del dato | Responsable | Encargado |
|---|---|---|---|
| Datos del empleado (nombre, cédula, salario) | El empleado | El empleador (tenant) | PayMind/Cheryx |
| Datos de planilla (cálculos, deducciones) | El empleado | El empleador (tenant) | PayMind/Cheryx |
El empleado tiene derecho a: - Acceder a sus propios datos (portal self-service) - Solicitar copia de su expediente (exportación PDF) - Solicitar corrección de datos incorrectos
PayMind NO puede borrar datos laborales a solicitud del empleado mientras exista relación laboral (y hasta 4 años después por prescripción de acciones laborales CR).
5.2 Artículo 69 Código de Trabajo CR
El empleado tiene derecho a solicitar copia de su expediente al empleador. PayMind implementa esto como feature de exportación en el portal self-service: el empleado puede descargar su expediente completo (contratos, planillas, incapacidades, evaluaciones) en PDF.
6. Tablas regulatorias — control de integridad
Las tablas regulatorias son el dato más crítico del motor de planilla:
class TablaRegulatoriasService:
async def publicar_tabla(self, tabla: NuevaTablaRegulatoriaRequest, aprobador: UUID):
# Paso 1: Validar la tabla contra fixtures de referencia
resultados = []
for fixture in cargar_fixtures_validacion(tabla.pais):
motor = get_motor(tabla.pais)
resultado = motor.calcular_con_tabla(fixture.input, tabla)
resultados.append((fixture, resultado))
if abs(resultado.salario_neto - fixture.esperado_neto) > 1:
raise ValueError(f"Tabla falla fixture {fixture.id}: esperado {fixture.esperado_neto}, obtenido {resultado.salario_neto}")
# Paso 2: Requiere aprobación de dos personas (dual control)
await self.pending_approvals.crear(tabla, aprobador, requiere_segundo_aprobador=True)
# Paso 3: Solo después de aprobación dual → publicar
# La tabla es inmutable desde el momento de publicación
7. Checklist de seguridad pre-lanzamiento PayMind
- [ ] Column-level encryption en
cedula_enc,salario_mensual_enc,iban_enc— verificar que campos no aparecen en logs - [ ] RBAC verificado: empleado no puede acceder al salario de otro empleado
- [ ] Planillas inmutables: REVOKE UPDATE/DELETE verificado con test de DB
- [ ] Tablas regulatorias inmutables: REVOKE UPDATE/DELETE verificado
- [ ] Proceso dual-control para publicación de nuevas tablas regulatorias
- [ ] Audit log de cada acceso a salario individual (detectar patrones de acceso masivo)
- [ ] Portal self-service: empleado solo ve sus propios datos (test cross-empleado)
- [ ] Exportación de expediente de empleado (derecho Art. 69 Código Trabajo CR)
- [ ] Rate limit en endpoint de cálculo de planilla (max 5 cálculos/min por tenant)
- [ ] DPA actualizado con datos de empleados (titular = empleado, responsable = empleador)
8. Log de auditoría PayMind
Eventos específicos que deben quedar en sm_audit_log:
| Evento | Cuándo | Datos auditados |
|---|---|---|
pm_salario_accedido |
Cada vez que se lee un salario individual | user_id, empleado_id, timestamp, IP |
pm_salario_modificado |
Cambio de salario de cualquier empleado | user_id, empleado_id, salario_anterior_hash, motivo |
pm_planilla_confirmada |
Confirmación de planilla | user_id, planilla_id, total_neto, n_empleados |
pm_tabla_regulatoria_publicada |
Nueva tabla de tasas publicada | user_id, pais, version, aprobadores |
pm_empleado_liquidado |
Liquidación de empleado | user_id, empleado_id (solo hash), motivo |
pm_expediente_exportado |
Empleado exporta su expediente | empleado_id, timestamp |
Ver también: Doc 08 (Modelo de Datos — column-level encryption en pm_empleados) · Doc 09 (Motor Core — inmutabilidad del cálculo) · Doc 14 (Marco Legal — DPA y derechos del empleado)