Version: 1.0
Fecha: 25 de Marzo, 2026
Contexto: Fase 4 del plan de migracion backend — traduccion de roles del JWT Simappe a roles internos de DataVault, manteniendo el sistema de permisos granulares por modulo
Arquitecto: Carlos Alberto Torres Camargo
Clasificacion: Interno — Arquitectura
Documentar el mapeo entre los roles que viajan en el JWT de SimappeOAuth2 y los roles internos de DataVault, incluyendo la estrategia para mantener los permisos granulares por modulo que DataVault maneja internamente.
Los roles se almacenan en la tabla user_tenants.role (por relacion usuario-tenant):
| Rol DataVault | Descripcion | Alcance |
|---|---|---|
superadmin |
Administrador global del sistema | Cross-tenant. Acceso total |
admin_empresa |
Administrador de una empresa/tenant | Single-tenant. Gestion de usuarios, modulos, configuracion |
rh |
Recursos Humanos | Single-tenant. Gestion HR (employees, contracts, etc.) |
archivista |
Gestor documental | Single-tenant. Ingesta, repositorio, preservacion |
auditor |
Auditor | Single-tenant. Solo lectura + logs de auditoria |
consulta |
Consulta basica | Single-tenant. Solo lectura limitada |
Los roles viajan como lista en el claim roles del JWT:
{
"roles": [
{"id": 1, "name": "SUPER_ADMIN"},
{"id": 2, "name": "ADMIN"},
{"id": 3, "name": "USER"},
{"id": 4, "name": "AUDITOR"}
]
}
| Rol Simappe | Descripcion |
|---|---|
SUPER_ADMIN |
Administrador global de la plataforma |
ADMIN |
Administrador de compania |
USER |
Usuario estandar |
AUDITOR |
Auditor |
VIEWER |
Solo lectura |
| Rol JWT Simappe | Rol DataVault equivalente | Notas |
|---|---|---|
SUPER_ADMIN |
superadmin |
Acceso total, cross-tenant |
ADMIN |
admin_empresa |
Administra tenant especifico |
USER |
archivista (default) |
Rol por defecto. Se refina con permisos granulares |
AUDITOR |
auditor |
Solo lectura + auditoria |
VIEWER |
consulta |
Solo lectura limitada |
Nota sobre
rh: El rolrhde DataVault no tiene equivalente directo en Simappe porque los modulos HR se excluyen del alcance de Nebula Vault. Si fuera necesario, un usuario con rolUSER+ permiso granular de modulohractuaria comorh.
Archivo: backend/app/utils/security.py
Agregar despues de la definicion de SimappeUserSession:
# ============================================================
# Mapeo de roles Simappe → DataVault
# ============================================================
# Mapeo de roles JWT Simappe a roles internos DataVault
ROLE_MAPPING: dict[str, str] = {
"SUPER_ADMIN": "superadmin",
"ADMIN": "admin_empresa",
"USER": "archivista",
"AUDITOR": "auditor",
"VIEWER": "consulta",
}
# Jerarquia de roles (mayor numero = mayor privilegio)
ROLE_HIERARCHY: dict[str, int] = {
"consulta": 10,
"auditor": 20,
"archivista": 30,
"rh": 35,
"admin_empresa": 40,
"superadmin": 50,
}
def get_datavault_role(user_session: SimappeUserSession) -> str:
"""
Determina el rol DataVault del usuario a partir de sus roles JWT.
Regla: si el usuario tiene multiples roles, se toma el de mayor jerarquia.
Args:
user_session: Sesion del usuario con roles del JWT
Returns:
Nombre del rol DataVault (string)
"""
if not user_session.roles:
return "consulta" # Default: minimo privilegio
best_role = "consulta"
best_priority = 0
for jwt_role in user_session.roles:
dv_role = ROLE_MAPPING.get(jwt_role, "consulta")
priority = ROLE_HIERARCHY.get(dv_role, 0)
if priority > best_priority:
best_priority = priority
best_role = dv_role
return best_role
def is_superadmin(user_session: SimappeUserSession) -> bool:
"""Verifica si el usuario es superadmin."""
return "SUPER_ADMIN" in user_session.roles
def has_minimum_role(user_session: SimappeUserSession, required_role: str) -> bool:
"""
Verifica si el usuario tiene al menos el rol requerido.
Ejemplo:
has_minimum_role(session, "archivista") → True si es archivista, admin o superadmin
"""
user_role = get_datavault_role(user_session)
user_priority = ROLE_HIERARCHY.get(user_role, 0)
required_priority = ROLE_HIERARCHY.get(required_role, 0)
return user_priority >= required_priority
Archivo: backend/app/utils/security.py
Agregar como dependencias FastAPI:
# ============================================================
# Dependencias de autorizacion para routers
# ============================================================
async def require_superadmin(
user_session: SimappeUserSession = Depends(get_current_user_session),
) -> SimappeUserSession:
"""Requiere rol SUPER_ADMIN."""
if not is_superadmin(user_session):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Se requiere rol de superadministrador",
)
return user_session
async def require_admin(
user_session: SimappeUserSession = Depends(require_company_selected),
) -> SimappeUserSession:
"""Requiere rol ADMIN o superior."""
if not has_minimum_role(user_session, "admin_empresa"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Se requiere rol de administrador",
)
return user_session
async def require_archivista(
user_session: SimappeUserSession = Depends(require_company_selected),
) -> SimappeUserSession:
"""Requiere rol archivista o superior."""
if not has_minimum_role(user_session, "archivista"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Se requiere rol de archivista o superior",
)
return user_session
async def require_auditor(
user_session: SimappeUserSession = Depends(require_company_selected),
) -> SimappeUserSession:
"""Requiere rol auditor o superior."""
if not has_minimum_role(user_session, "auditor"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Se requiere rol de auditor o superior",
)
return user_session
# ============================================================
# Ejemplo: api/admin_tenants.py — Solo superadmin
# ============================================================
from app.utils.security import require_superadmin, SimappeUserSession
from app.database import get_tenant_db
@router.get("/admin/dashboard")
async def admin_dashboard(
user_session: SimappeUserSession = Depends(require_superadmin),
):
"""Solo accesible por superadmin."""
...
# ============================================================
# Ejemplo: api/ingest.py — Solo archivista o superior
# ============================================================
from app.utils.security import require_archivista, SimappeUserSession
@router.post("/projects")
async def create_project(
project_data: ProjectCreate,
user_session: SimappeUserSession = Depends(require_archivista),
db: AsyncSession = Depends(get_tenant_db),
):
"""Crear proyecto de ingesta — requiere archivista."""
...
# ============================================================
# Ejemplo: api/audit.py — Solo auditor o superior
# ============================================================
from app.utils.security import require_auditor, SimappeUserSession
@router.get("/logs")
async def list_audit_logs(
user_session: SimappeUserSession = Depends(require_auditor),
db: AsyncSession = Depends(get_tenant_db),
):
"""Listar logs de auditoria — requiere auditor."""
...
DataVault tiene un sistema de permisos granulares via tabla module_permissions:
# Estado actual de ModulePermission
class ModulePermission(Base):
__tablename__ = "module_permissions"
id = Column(UUID, primary_key=True)
tenant_id = Column(UUID, ForeignKey("tenants.id")) # ← Eliminar
user_id = Column(UUID, ForeignKey("users.id"))
module_name = Column(String(100)) # ej: "ingesta", "preservacion", "legal_hold"
can_view = Column(Boolean, default=False)
can_create = Column(Boolean, default=False)
can_edit = Column(Boolean, default=False)
can_delete = Column(Boolean, default=False)
Estrategia: Mantener module_permissions como capa adicional sobre los roles JWT.
# ============================================================
# Verificacion de permiso granular
# ============================================================
async def check_module_permission(
user_session: SimappeUserSession,
db: AsyncSession,
module_name: str,
action: str = "view", # view, create, edit, delete
) -> bool:
"""
Verifica si el usuario tiene permiso granular en un modulo especifico.
Logica:
1. Superadmin → siempre True
2. Admin → siempre True para su tenant
3. Otros roles → verificar en tabla module_permissions
"""
role = get_datavault_role(user_session)
# Superadmin y admin tienen acceso total
if role in ("superadmin", "admin_empresa"):
return True
# Buscar permiso granular
result = await db.execute(
select(ModulePermission)
.filter(
ModulePermission.user_id == user_session.id,
ModulePermission.module_name == module_name,
)
)
permission = result.scalar_one_or_none()
if not permission:
return False
action_map = {
"view": permission.can_view,
"create": permission.can_create,
"edit": permission.can_edit,
"delete": permission.can_delete,
}
return action_map.get(action, False)
Nota: La tabla
module_permissionspierdetenant_idpero mantieneuser_id. Eluser_idahora referencia al ID del usuario en Simappe (campoiddel JWT). Se necesita validar que eluser_idenmodule_permissionssea compatible con el tipoLongde Simappe vsUUIDactual de DataVault.
| Sistema | Tipo de user_id |
Formato |
|---|---|---|
| DataVault actual | UUID |
550e8400-e29b-41d4-a716-446655440000 |
| Simappe | Long |
42 |
Decision: Agregar campo simappe_user_id (Long) a la tabla module_permissions para referenciar al usuario de Simappe, o migrar user_id a Long.
# Opcion A: Campo adicional (recomendado para compatibilidad gradual)
class ModulePermission(Base):
__tablename__ = "module_permissions"
id = Column(UUID, primary_key=True)
simappe_user_id = Column(BigInteger, nullable=False, index=True) # ← NUEVO
module_name = Column(String(100))
can_view = Column(Boolean, default=False)
can_create = Column(Boolean, default=False)
can_edit = Column(Boolean, default=False)
can_delete = Column(Boolean, default=False)
| Grupo de Endpoints | Rol Minimo | Dependencia FastAPI |
|---|---|---|
GET /api/vault/dashboard/* |
consulta |
require_company_selected |
GET /api/vault/repository/* |
consulta |
require_company_selected |
POST /api/vault/ingest/* |
archivista |
require_archivista |
GET /api/vault/preservation/* |
auditor |
require_auditor |
POST /api/vault/preservation/repair |
archivista |
require_archivista |
GET /api/vault/retention/* |
auditor |
require_auditor |
POST /api/vault/retention/* |
admin_empresa |
require_admin |
GET /api/vault/legal-hold/* |
auditor |
require_auditor |
POST /api/vault/legal-hold/* |
admin_empresa |
require_admin |
GET /api/vault/audit/* |
auditor |
require_auditor |
GET /api/vault/cloud/* |
archivista |
require_archivista |
POST /api/vault/cloud/daemon/* |
admin_empresa |
require_admin |
GET /api/vault/admin/* |
superadmin |
require_superadmin |
POST /api/vault/admin/* |
superadmin |
require_superadmin |
GET /api/vault/notifications/* |
consulta |
require_company_selected |
| # | Riesgo | Control |
|---|---|---|
| R1 | Rol USER de Simappe es generico — no distingue archivista de auditor |
Permisos granulares via module_permissions refinan el acceso |
| R2 | UUID → Long migration rompe FK existentes | Agregar campo simappe_user_id sin eliminar user_id UUID |
| R3 | Endpoint sin guard de autorizacion | Busqueda: grep -r "Depends(require_company_selected)" api/ — todos los endpoints deben tener guard |
| R4 | Role escalation: usuario cambia roles en JWT | JWT es firmado por Simappe — no manipulable. La validacion de firma previene esto |
get_datavault_role() en utils/security.pyhas_minimum_role() y is_superadmin()require_superadmin, require_admin, require_archivista, require_auditormodule_permissions con simappe_user_idcheck_module_permission()USER no puede acceder a endpoint ADMINSUPER_ADMIN puede acceder a todo| Version | Fecha | Autor | Descripcion |
|---|---|---|---|
| 1.0.0 | 2026-03-25 | Carlos Torres | Creacion del documento de mapeo de roles y permisos |