fix: solución de bloqueos y estandarización de permisos

This commit is contained in:
2026-04-27 10:47:30 -05:00
parent fa3cbb4d0a
commit 5c2a84e95d
312 changed files with 13889 additions and 7911 deletions

View File

@@ -18,6 +18,10 @@ from .dependencies import (
get_current_user_permissions,
)
from .routes import router
from .seed_v2 import register_core_permissions
# Registrar permisos al cargar el módulo
register_core_permissions()
__all__ = [
# Models

View File

@@ -0,0 +1,65 @@
"""
Script CLI para LIMPIEZA TOTAL del sistema de permisos.
Borra todos los roles, asignaciones y el catálogo de permisos.
Úselo con precaución.
Uso:
docker exec -it <container> python3 -m api.v1.modules.core.permissions.cleanup_cli
"""
import sys
import os
import logging
from sqlalchemy import text
# Configurar logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
# Asegurar que el backend esté en el path
sys.path.append(os.path.abspath("."))
sys.path.append(os.path.abspath("backend"))
from core.database import CoreSessionLocal
def run_cleanup():
"""Ejecuta el borrado de tablas en orden de dependencias."""
logger.warning("INICIANDO LIMPIEZA TOTAL DE PERMISOS Y ROLES...")
db = CoreSessionLocal()
try:
# 1. Borrar asignaciones directas de permisos a usuarios
logger.info("Borrando asignaciones directas de usuario...")
db.execute(text("DELETE FROM core.user_company_permissions"))
# 2. Borrar relación entre roles y permisos
logger.info("Borrando mapeo de roles y permisos...")
db.execute(text("DELETE FROM core.role_permissions"))
# 3. Borrar asignación de roles a usuarios
logger.info("Borrando asignación de roles a usuarios...")
db.execute(text("DELETE FROM core.user_company_roles"))
# 4. Borrar los roles mismos
logger.info("Borrando el catálogo de roles...")
db.execute(text("DELETE FROM core.company_roles"))
# 5. Borrar el catálogo base de permisos
logger.info("Borrando el catálogo base de permisos...")
db.execute(text("DELETE FROM core.permissions"))
db.commit()
logger.info("=" * 40)
logger.info("LIMPIEZA COMPLETADA CON ÉXITO")
logger.info("El sistema de permisos está ahora en blanco.")
logger.info("=" * 40)
except Exception as e:
db.rollback()
logger.error(f"Error crítico durante la limpieza: {e}")
sys.exit(1)
finally:
db.close()
if __name__ == "__main__":
run_cleanup()

View File

@@ -0,0 +1,101 @@
"""
Registro centralizado para la modulación de permisos.
Permite que cada módulo registre sus propios permisos de forma dinámica.
"""
import logging
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
@dataclass
class PermissionDefinition:
"""Representa la definición de un permiso en un módulo."""
code: str
description: Optional[str] = None
module: Optional[str] = None
action: Optional[str] = None
is_active: bool = True
def __post_init__(self):
"""Lógica de autocompletado para evitar redundancia."""
# Si el código tiene el formato "modulo.sub.accion" o "modulo.accion"
parts = self.code.split(".")
# Extraer módulo si no se especificó
if not self.module and len(parts) > 1:
self.module = parts[0]
elif not self.module:
self.module = "system" # Default fallback
# Extraer acción si no se especificó (es la última parte del código)
if not self.action and len(parts) > 1:
self.action = parts[-1]
elif not self.action:
self.action = "view" # Default fallback
class PermissionRegistry:
"""
Registro Singleton para permisos de la aplicación.
Cada módulo de la API debe importar este registro y dar de alta sus permisos.
"""
_instance = None
_permissions: Dict[str, PermissionDefinition] = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super(PermissionRegistry, cls).__new__(cls)
cls._permissions = {}
return cls._instance
@classmethod
def register(cls,
code: str,
description: Optional[str] = None,
module: Optional[str] = None,
action: Optional[str] = None) -> None:
"""
Registra un nuevo permiso en el sistema.
"""
if code in cls._permissions:
logger.debug(f"Permiso {code} ya está registrado, actualizando metadatos.")
cls._permissions[code] = PermissionDefinition(
code=code,
description=description,
module=module,
action=action
)
@classmethod
def register_many(cls, permissions_list: List[tuple]) -> None:
"""
Registra múltiples permisos desde una lista de tuplas.
Útil para migrar seeds estáticos.
"""
for item in permissions_list:
if len(item) == 2: # (code, description)
cls.register(code=item[0], description=item[1])
elif len(item) >= 3: # (code, description, module, ...)
cls.register(
code=item[0],
description=item[1],
module=item[2],
action=item[3] if len(item) > 3 else None
)
@classmethod
def get_all(cls) -> List[PermissionDefinition]:
"""Retorna todos los permisos registrados."""
return list(cls._permissions.values())
@classmethod
def get_by_module(cls, module_name: str) -> List[PermissionDefinition]:
"""Retorna los permisos de un módulo específico."""
return [p for p in cls._permissions.values() if p.module == module_name]
# Instancia global para facilitar el acceso
registry = PermissionRegistry()

View File

@@ -55,17 +55,45 @@ async def get_my_permissions(
permission_service: PermissionService = Depends(get_permission_service),
):
"""
Obtiene los permisos y roles del usuario actual en el company actual.
No requiere permisos especiales ya que es información propia.
Obtiene los permisos y roles del usuario actual en la compañía actual.
Si la compañía no tiene roles definidos, realiza un bootstrap automático.
También realiza bootstrap si es un usuario con rol 'admin' en Keycloak pero sin roles locales.
"""
# 1. Validar acceso básico a la compañía
# Nota: pass None en required_permissions permite el paso al bootstrap
tenant_id = validate_access_to_resource(db, company_id, current_user)
user_id = current_user.get("sub") or current_user.get("id")
# Obtener permisos
# 2. Determinar si es un admin de Keycloak para forzar bootstrap si es necesario
realm_roles = current_user.get("realm_access", {}).get("roles", [])
client_roles = []
for client in current_user.get("resource_access", {}).values():
client_roles.extend(client.get("roles", []))
is_keycloak_admin = "admin" in realm_roles or "admin" in client_roles
# 3. Bootstrap: si la compañía no tiene roles, o si el usuario es admin, o si estamos en desarrollo y el usuario no tiene roles
from .models import CompanyRole, UserCompanyRole
from core.config import settings
has_roles = db.query(CompanyRole).filter(CompanyRole.company_id == company_id).first() is not None
user_has_roles = db.query(UserCompanyRole).filter(
UserCompanyRole.user_id == user_id,
UserCompanyRole.company_id == company_id
).first() is not None
auto_bootstrap = (not has_roles) or \
(is_keycloak_admin and not user_has_roles) or \
(settings.ENVIRONMENT == "development" and not user_has_roles)
if auto_bootstrap:
# Nota: bootstrap_super_admin ya hace commit e intenta no duplicar si el rol ya existe
permission_service.bootstrap_super_admin(user_id, company_id)
# 4. Obtener permisos finales
permissions = permission_service.get_user_permissions(user_id, company_id)
# Obtener roles
# 5. Obtener roles
roles = permission_service.get_user_roles(user_id, company_id)
role_names = [role.name for role in roles]
@@ -81,9 +109,9 @@ async def get_my_permissions(
async def list_available_permissions(
db: Session = Depends(get_core_db),
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(100, ge=1, le=500, description="Tamaño de página"),
page_size: int = Query(100, ge=1, le=1000, description="Tamaño de página"),
module: Optional[str] = Query(None, description="Filtrar por módulo"),
_: None = Depends(RequirePermission("permissions.view")),
_: None = Depends(RequirePermission("roles.view")),
):
"""
Lista todos los permisos disponibles en el sistema.
@@ -108,13 +136,13 @@ async def list_company_roles(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(100, ge=1, le=500, description="Tamaño de página"),
page_size: int = Query(100, ge=1, le=1000, description="Tamaño de página"),
):
"""
Lista todos los roles del companye actual.
TODO: Agregar verificación de permisos
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
query = db.query(CompanyRole).filter(
CompanyRole.company_id == company_id
@@ -139,7 +167,7 @@ async def get_user_permissions(
Obtiene los permisos y roles de un usuario específico.
Requiere permiso: user.view
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
permissions = permission_service.get_user_permissions(user_id, company_id)
roles = permission_service.get_user_roles(user_id, company_id)
@@ -162,7 +190,7 @@ async def get_user_permissions(
async def list_permissions(
db: Session = Depends(get_core_db),
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(100, ge=1, le=500, description="Tamaño de página"),
page_size: int = Query(100, ge=1, le=1000, description="Tamaño de página"),
module: Optional[str] = Query(None, description="Filtrar por módulo"),
action: Optional[str] = Query(None, description="Filtrar por acción"),
search: Optional[str] = Query(None, description="Buscar por código o descripción"),
@@ -241,7 +269,7 @@ async def list_user_roles(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(100, ge=1, le=500, description="Tamaño de página"),
page_size: int = Query(100, ge=1, le=1000, description="Tamaño de página"),
):
"""
Lista todas las asignaciones de roles a usuarios en la compañía.
@@ -249,7 +277,7 @@ async def list_user_roles(
from .models import UserCompanyRole
from sqlalchemy.orm import joinedload
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
query = db.query(UserCompanyRole).options(
joinedload(UserCompanyRole.company_role)
@@ -286,7 +314,7 @@ async def assign_user_role(
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
assigner_id = current_user.get("sub") or current_user.get("id")
# Verificar que el rol existe
@@ -342,7 +370,7 @@ async def remove_user_role(
"""
from .models import UserCompanyRole
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
user_role = db.query(UserCompanyRole).filter(
UserCompanyRole.id == user_role_id,
@@ -380,6 +408,26 @@ async def get_permission(
return permission
@router.post("/sync", response_model=Dict[str, Any])
async def sync_permissions_endpoint(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
permission_service: PermissionService = Depends(get_permission_service),
_: None = Depends(RequirePermission("roles.edit")),
):
"""
Sincroniza dinámicamente los permisos registrados en los módulos con la base de datos.
Requiere permiso: permissions.edit
"""
result = permission_service.sync_permissions()
return {
"success": True,
"message": "Permissions synchronized successfully",
"data": result
}
from .schemas import CreatePermissionRequest
@@ -488,7 +536,7 @@ async def create_role(
Crea un nuevo rol personalizado para el companye.
TODO: Agregar verificación de permisos
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.create"])
# Verificar que el código no esté en uso
existing = (
@@ -540,7 +588,7 @@ async def update_role(
Actualiza un rol existente.
TODO: Agregar verificación de permisos
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.edit"])
role = (
db.query(CompanyRole)
@@ -580,7 +628,7 @@ async def delete_role(
"""
from .models import RolePermission, UserCompanyRole
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.delete"])
role = (
db.query(CompanyRole)
@@ -629,7 +677,7 @@ async def get_role_permissions(
"""
from .models import RolePermission
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
# Verificar que el rol existe y pertenece al company
role = (
@@ -687,7 +735,7 @@ async def assign_permission_to_role(
"""
from .models import RolePermission
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
# Verificar que el rol existe
role = (
@@ -753,7 +801,7 @@ async def assign_multiple_permissions_to_role(
"""
from .models import RolePermission
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
# Verificar que el rol existe
role = (
@@ -822,7 +870,7 @@ async def remove_permission_from_role(
"""
from .models import RolePermission
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
# Buscar la asignación
role_permission = (
@@ -865,7 +913,7 @@ async def assign_role(
Asigna un rol a un usuario en el companye actual.
TODO: Agregar verificación de permisos
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
assigner_id = current_user.get("sub") or current_user.get("id")
@@ -902,7 +950,7 @@ async def grant_permission(
Concede un permiso directo a un usuario en el companye actual.
Requiere permiso: permissions.grant
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
assigner_id = current_user.get("sub") or current_user.get("id")
@@ -1035,7 +1083,7 @@ async def get_user_individual_permissions(
from .models import UserCompanyPermission
from sqlalchemy.orm import joinedload
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
user_permissions = (
db.query(UserCompanyPermission)
@@ -1072,7 +1120,7 @@ async def get_user_effective_permissions(
from .models import UserCompanyPermission, UserCompanyRole, RolePermission
from sqlalchemy.orm import joinedload
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
# 1. Obtener permisos de roles
role_permissions_query = (
@@ -1153,7 +1201,7 @@ async def assign_user_permission(
"""
from .models import UserCompanyPermission
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
# Verificar que el permiso existe
permission = db.query(Permission).filter(Permission.id == request.permission_id).first()
@@ -1216,7 +1264,7 @@ async def remove_user_permission(
"""
from .models import UserCompanyPermission
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
user_permission = (
db.query(UserCompanyPermission)

View File

@@ -17,47 +17,58 @@ seed_invoices = [
("invoice.imp.cm.delete", "Eliminar facturas", "invoice", "delete"),
("invoice.imp.cm.process", "Procesar facturas", "invoice", "process"),
("invoice.imp.cr.view", "Ver facturas temporales de importación", "invoice", "view"),
("invoice.imp.cr.create", "Crear facturas temporales de importación", "invoice", "create"),
("invoice.imp.cr.view", "Ver facturas de importación con corrección", "invoice", "view"),
("invoice.imp.cr.create", "Crear facturas de importación con corrección", "invoice", "create"),
("invoice.imp.cr.edit", "Editar facturas", "invoice", "edit"),
("invoice.imp.cr.delete", "Eliminar facturas", "invoice", "delete"),
("invoice.imp.cr.process", "Procesar facturas", "invoice", "process"),
("invoice.exp.view", "Ver facturas temporales de importación", "invoice", "view"),
("invoice.exp.create", "Crear facturas temporales de importación", "invoice", "create"),
("invoice.exp.edit", "Editar facturas", "invoice", "edit"),
("invoice.exp.delete", "Eliminar facturas", "invoice", "delete"),
("invoice.exp.process", "Procesar facturas", "invoice", "process"),
("invoice.exp.rep.view", "Ver facturas temporales de importación", "invoice", "view"),
("invoice.exp.rep.create", "Crear facturas temporales de importación", "invoice", "create"),
("invoice.exp.rep.edit", "Editar facturas", "invoice", "edit"),
("invoice.exp.rep.delete", "Eliminar facturas", "invoice", "delete"),
("invoice.exp.rep.process", "Procesar facturas", "invoice", "process"),
("invoice.imp.rep.view", "Ver facturas de importación reparación (Activo Fijo)", "invoice", "view"),
("invoice.imp.rep.create", "Crear facturas de importación reparación (Activo Fijo)", "invoice", "create"),
("invoice.imp.rep.edit", "Editar facturas de importación reparación", "invoice", "edit"),
("invoice.imp.rep.delete", "Eliminar facturas de importación reparación", "invoice", "delete"),
("invoice.imp.rep.process", "Procesar facturas de importación reparación", "invoice", "process"),
("invoice.exp.view", "Ver facturas de exportación", "invoice", "view"),
("invoice.exp.create", "Crear facturas de exportación", "invoice", "create"),
("invoice.exp.edit", "Editar facturas de exportación", "invoice", "edit"),
("invoice.exp.delete", "Eliminar facturas de exportación", "invoice", "delete"),
("invoice.exp.process", "Procesar facturas de exportación", "invoice", "process"),
("invoice.exp.rep.view", "Ver facturas de exportación reparación", "invoice", "view"),
("invoice.exp.rep.create", "Crear facturas de exportación reparación", "invoice", "create"),
("invoice.exp.rep.edit", "Editar facturas de exportación reparación", "invoice", "edit"),
("invoice.exp.rep.delete", "Eliminar facturas de exportación reparación", "invoice", "delete"),
("invoice.exp.rep.process", "Procesar facturas de exportación reparación", "invoice", "process"),
("invoice.exp.donac.view", "Ver facturas de exportación donación", "invoice", "view"),
("invoice.exp.donac.create", "Crear facturas de exportación donación", "invoice", "create"),
("invoice.exp.donac.edit", "Editar facturas de exportación donación", "invoice", "edit"),
("invoice.exp.donac.delete", "Eliminar facturas de exportación donación", "invoice", "delete"),
("invoice.exp.donac.process", "Procesar facturas de exportación donación", "invoice", "process"),
]
seed_user = [
("user.view", "Ver facturas temporales de importación", "invoice", "view"),
("user.create", "Crear facturas temporales de importación", "invoice", "create"),
("user.edit", "Editar facturas", "invoice", "edit"),
("user.delete", "Eliminar facturas", "invoice", "delete"),
("user.process", "Procesar facturas", "invoice", "process"),
("user.view", "Ver Usuarios", "invoice", "view"),
("user.create", "Crear Usuarios", "invoice", "create"),
("user.edit", "Editar Usuarios", "invoice", "edit"),
("user.delete", "Eliminar Usuarios", "invoice", "delete"),
("user.process", "Procesar Usuarios", "invoice", "process"),
]
seed_report = [
("report.view", "Ver facturas temporales de importación", "invoice", "view"),
("report.create", "Crear facturas temporales de importación", "invoice", "create"),
("report.edit", "Editar facturas", "invoice", "edit"),
("report.delete", "Eliminar facturas", "invoice", "delete"),
("report.process", "Procesar facturas", "invoice", "process"),
("report.view", "Ver Reportes", "invoice", "view"),
("report.create", "Crear Reportes", "invoice", "create"),
("report.edit", "Editar Reportes", "invoice", "edit"),
("report.delete", "Eliminar Reportes", "invoice", "delete"),
("report.process", "Procesar Reportes", "invoice", "process"),
]
seed_roles = [
("roles.view", "Ver facturas temporales de importación", "invoice", "view"),
("roles.create", "Crear facturas temporales de importación", "invoice", "create"),
("roles.edit", "Editar facturas", "invoice", "edit"),
("roles.delete", "Eliminar facturas", "invoice", "delete"),
("roles.process", "Procesar facturas", "invoice", "process"),
("roles.view", "Ver Roles", "invoice", "view"),
("roles.create", "Crear Roles", "invoice", "create"),
("roles.edit", "Editar Roles", "invoice", "edit"),
("roles.delete", "Eliminar Roles", "invoice", "delete"),
("roles.process", "Procesar Roles", "invoice", "process"),
]

View File

@@ -0,0 +1,232 @@
"""
Semilla V2 para el sistema de permisos.
Corrige las descripciones erróneas y organiza los permisos por módulos reales.
Utiliza el PermissionRegistry para la modulación por API.
"""
from .registry import registry
from .seed import seed_invoices, seed_user, seed_report, seed_roles
# ============================================================================
# HELPER: LIMPIEZA DE MÓDULOS DESDE SEED.PY
# ============================================================================
def clean_seed_permissions(perms):
"""
Toma los permisos de seed.py (que vienen todos como module='invoice')
y los re-asigna al módulo correcto según su prefijo para la UI.
"""
cleaned = []
for code, desc, _, action in perms:
module = "invoice_imp" # Default
if code.startswith("user."): module = "users"
elif code.startswith("report."): module = "reports"
elif code.startswith("roles."): module = "roles"
elif code.startswith("invoice.exp"): module = "invoice_exp"
cleaned.append((code, desc, module, action))
return cleaned
# ============================================================================
# PERMISOS DE USUARIOS (CORREGIDOS)
# ============================================================================
# ============================================================================
# AUDITORÍA Y BITÁCORA
# ============================================================================
permissions_audit = [
("audit_logs.view", "Ver bitácora de movimientos", "audit_logs", "view"),
]
# ============================================================================
# DATOS DE REFERENCIA (Catálogos Fijos - Solo Ver)
# ============================================================================
permissions_reference = [
("ref_pedimento_regimens.view", "Ver Regímenes de Pedimento", "reference_data", "view"),
("ref_containers.view", "Ver Contenedores", "reference_data", "view"),
("ref_countries.view", "Ver Países", "reference_data", "view"),
("ref_currency_types.view", "Ver Tipos de Moneda", "reference_data", "view"),
("ref_customs_sections.view", "Ver Secciones Aduaneras", "reference_data", "view"),
("ref_customs_warehouses.view", "Ver Recintos Fiscalizados", "reference_data", "view"),
("ref_incoterms.view", "Ver Incoterms", "reference_data", "view"),
("ref_invoice_types.view", "Ver Tipos de Factura", "reference_data", "view"),
("ref_material_types.view", "Ver Tipos de Material", "reference_data", "view"),
("ref_payment_methods.view", "Ver Formas de Pago", "reference_data", "view"),
("ref_pedimento_codes.view", "Ver Claves de Pedimento", "reference_data", "view"),
("ref_sectors.view", "Ver Sectores", "reference_data", "view"),
("ref_states.view", "Ver Estados", "reference_data", "view"),
("ref_transport_modes.view", "Ver Modos de Transporte", "reference_data", "view"),
("ref_transport_types.view", "Ver Tipos de Transporte", "reference_data", "view"),
("ref_valuation_methods.view", "Ver Métodos de Valoración", "reference_data", "view"),
]
# ============================================================================
# CATÁLOGOS GENERALES (CRUD)
# ============================================================================
def gen_crud(resource, desc, module):
return [
(f"{resource}.view", f"Ver {desc}", module, "view"),
(f"{resource}.create", f"Crear {desc}", module, "create"),
(f"{resource}.edit", f"Editar {desc}", module, "edit"),
(f"{resource}.delete", f"Eliminar {desc}", module, "delete"),
]
permissions_general = (
gen_crud("cat_company", "Información de la Empresa", "general_catalogs") +
gen_crud("cat_packages", "Bultos", "general_catalogs") +
gen_crud("cat_concepts", "Conceptos", "general_catalogs") +
gen_crud("cat_broker_concepts", "Conceptos de Agente Aduanal", "general_catalogs") +
gen_crud("cat_classification", "Clasificación de Conceptos", "general_catalogs") +
gen_crud("cat_identifiers", "Identificadores", "general_catalogs") +
gen_crud("cat_incoterms", "Incoterms (Catálogo)", "general_catalogs") +
gen_crud("cat_inpc", "INPC", "general_catalogs") +
gen_crud("cat_legends", "Leyendas Fijas", "general_catalogs") +
gen_crud("cat_seals", "Sellos", "general_catalogs") +
gen_crud("cat_valuation", "Métodos de Valoración", "general_catalogs") +
gen_crud("cat_countries", "Países", "general_catalogs") +
gen_crud("cat_ports", "Puertos", "general_catalogs") +
gen_crud("cat_um_general", "UdM Generales", "general_catalogs") +
gen_crud("cat_um_customs", "UdM Aduana MEX", "general_catalogs") +
gen_crud("cat_um_american", "UdM Aduana AME", "general_catalogs") +
gen_crud("cat_um_ace", "UdM ACE", "general_catalogs") +
gen_crud("cat_um_oma", "UdM OMA", "general_catalogs") +
gen_crud("cat_unit_conversions", "Conversiones", "general_catalogs") +
gen_crud("cat_equivalencies", "Equivalencias", "general_catalogs") +
gen_crud("cat_exchange_rates", "Tipos de Cambio", "general_catalogs") +
gen_crud("cat_currency", "Tipos de Moneda", "general_catalogs") +
gen_crud("cat_multi_currency_types", "Tipos de Moneda (Múltiples)", "general_catalogs") +
gen_crud("cat_inv_types", "Tipos de Factura", "general_catalogs") +
gen_crud("cat_signatures", "Firmas Electrónicas", "general_catalogs") +
gen_crud("cat_errors", "Catálogo de Errores", "general_catalogs") +
gen_crud("cat_doda", "DODA", "general_catalogs") +
gen_crud("cat_prevalidators", "Prevalidadores", "general_catalogs") +
gen_crud("cat_notices", "Avisos Electrónicos", "general_catalogs") +
gen_crud("cat_crossing", "Avisos de Cruce", "general_catalogs") +
gen_crud("cat_warehouses", "Recintos", "general_catalogs") +
gen_crud("cat_sectors", "Sectores", "general_catalogs")
)
# ============================================================================
# FRACCIONES
# ============================================================================
permissions_fractions = [
# Solo ver para SITAR
("frac_sitar.view", "Ver Fracciones SITAR", "fractions", "view"),
("frac_sitar_7.view", "Ver Fracciones SITAR (Séptima Enmienda)", "fractions", "view"),
("frac_sitar_us.view", "Ver Fracciones SITAR (USA)", "fractions", "view"),
] + gen_crud("frac_american", "Fracciones Americanas", "fractions") + \
gen_crud("frac_canadian", "Fracciones Canadienses", "fractions") + \
gen_crud("frac_historical", "Fracciones Históricas", "fractions") + \
[("frac_sectors.view", "Ver Sectores", "fractions", "view")]
# ============================================================================
# TRANSPORTES (CRUD)
# ============================================================================
permissions_transports = (
gen_crud("transporters", "Transportistas", "transports") +
gen_crud("drivers", "Conductores", "transports") +
gen_crud("trailers", "Trailers", "transports") +
gen_crud("vehicles", "Vehículos", "transports")
)
# ============================================================================
# MERCANCÍAS (CRUD)
# ============================================================================
permissions_goods = (
gen_crud("goods_classes", "Clases de Activo Fijo", "goods") +
gen_crud("goods_parts", "Partes / Productos", "goods") +
gen_crud("goods_fda", "Códigos FDA", "goods")
)
# ============================================================================
# PEDIMENTOS
# ============================================================================
permissions_pedimentos = (
gen_crud("pedimentos_mgmt", "Gestión de Pedimentos", "pedimentos") +
[
("pedimentos_codes.view", "Ver Claves de Pedimento", "pedimentos", "view"),
("pedimentos_regimes.view", "Ver Regímenes Aduanales", "pedimentos", "view"),
("pedimentos_payments.view", "Ver Formas de Pago", "pedimentos", "view"),
("pedimentos_sections.view", "Ver Secciones Aduaneras", "pedimentos", "view"),
("pedimentos_anexo22.view", "Ver Anexo 22 / Apéndice 31", "pedimentos", "view"),
]
)
# ============================================================================
# EXPORTACIÓN
# ============================================================================
permissions_export = (
[("export_catalog.view", "Ver Catálogo Base Exportación", "export", "view")] +
[("export_repair.view", "Ver Reparaciones", "export", "view")] +
gen_crud("export_manifest", "Manifiestos de Exportación", "export") +
[("export_proforma.view", "Ver Proformas", "export", "view")] +
[("export_reports.view", "Ver Reportes Oficiales Exportación", "export", "view")] +
[("export_used.view", "Ver Materiales Utilizados", "export", "view")] +
[("export_destruction.view", "Ver Destrucciones", "export", "view")] +
[("export_special.view", "Ver Procesos Especiales", "export", "view")]
)
# ============================================================================
# FACTURACIÓN, USUARIOS, REPORTES Y ROLES (DESDE SEED.PY)
# ============================================================================
# Estas secciones se nutren directamente de seed.py
permissions_invoice_imp_seed = clean_seed_permissions(seed_invoices)
permissions_users_seed = clean_seed_permissions(seed_user)
permissions_reports_seed = clean_seed_permissions(seed_report)
permissions_roles_seed = clean_seed_permissions(seed_roles)
# ============================================================================
# CLIENTES Y PROVEEDORES (CRUD)
# ============================================================================
permissions_clients = gen_crud("partners_mgmt", "Clientes y Proveedores", "partners")
# ============================================================================
# AGENTES ADUANALES (CRUD)
# ============================================================================
permissions_brokers = gen_crud("customs_brokers", "Agentes Aduanales", "brokers")
# ======================================# USUARIOS DEL SISTEMA (YA NO SE USA AQUÍ, VIENE DE SEED.PY)
# ============================================================================
# permissions_users = ...
# ============================================================================
# CONFIGURACIÓN GENERAL Y AYUDA
# ============================================================================
permissions_settings = [
("settings_general.view", "Ver Configuración General", "settings", "view"),
("settings_general.edit", "Editar Configuración General", "settings", "edit"),
]
permissions_help = gen_crud("help_center", "Centro de Ayuda y Documentos", "help")
# ============================================================================
# CSV UPLOAD MASIVO
# ============================================================================
permissions_csv = [
("csv_upload.process", "Procesar Cargas Masivas CSV", "csv_upload", "process"),
]
def register_core_permissions():
"""Registra los permisos granulados de la aplicación según Sidebar."""
registry.register_many(permissions_audit)
registry.register_many(permissions_reference)
registry.register_many(permissions_general)
registry.register_many(permissions_fractions)
registry.register_many(permissions_transports)
registry.register_many(permissions_goods)
registry.register_many(permissions_pedimentos)
# Registro de permisos desde seed.py (limpios)
registry.register_many(permissions_invoice_imp_seed)
registry.register_many(permissions_users_seed)
registry.register_many(permissions_reports_seed)
registry.register_many(permissions_roles_seed)
registry.register_many(permissions_export)
registry.register_many(permissions_clients)
registry.register_many(permissions_brokers)
registry.register_many(permissions_settings)
registry.register_many(permissions_help)
registry.register_many(permissions_csv)
# Ejecutar registro al importar este módulo
register_core_permissions()

View File

@@ -296,3 +296,153 @@ class PermissionService:
self.db.refresh(user_permission)
return user_permission
def sync_permissions(self) -> dict:
"""
Sincroniza los permisos registrados en el registry con la base de datos.
Inserta nuevos permisos y actualiza los existentes.
"""
from .registry import registry
# IMPORTANTE: Importar seed_v2 para que se ejecute register_core_permissions()
from . import seed_v2
registered_permissions = registry.get_all()
synced_count = 0
updated_count = 0
for p_def in registered_permissions:
# Buscar permiso existente
db_permission = self.db.query(Permission).filter(Permission.code == p_def.code).first()
if db_permission:
# Actualizar si hay cambios
changed = False
if db_permission.description != p_def.description:
db_permission.description = p_def.description
changed = True
if db_permission.module != p_def.module:
db_permission.module = p_def.module
changed = True
if db_permission.action != p_def.action:
db_permission.action = p_def.action
changed = True
if db_permission.is_active != p_def.is_active:
db_permission.is_active = p_def.is_active
changed = True
if changed:
updated_count += 1
else:
# Crear nuevo
new_permission = Permission(
code=p_def.code,
description=p_def.description,
module=p_def.module,
action=p_def.action,
is_active=p_def.is_active
)
self.db.add(new_permission)
synced_count += 1
self.db.commit()
return {
"synced": synced_count,
"updated": updated_count,
"total_registered": len(registered_permissions)
}
def bootstrap_super_admin(self, user_id: str, company_id: int) -> bool:
"""
Crea un rol de Super Administrador con todos los permisos y se lo asigna al usuario.
Diseñado para el primer inicio de una compañía o para asegurar acceso a administradores.
"""
from .models import CompanyRole, RolePermission, UserCompanyRole, Permission
import logging
logger = logging.getLogger(__name__)
try:
# 0. Sincronizar permisos por si la tabla esta vacía
# Esto puebla la tabla 'permissions' desde el registry de código
logger.info("Bootstrap: Sincronizando catálogo de permisos desde el registry...")
from . import seed_v2
sync_res = self.sync_permissions()
logger.info(f"Bootstrap: Sincronización completa. {sync_res.get('synced', 0)} nuevos, {sync_res.get('total_registered', 0)} totales.")
# 1. Obtener el tenant_id
tenant_id = 1
try:
from api.v1.modules.a76.general_catalogs.company.models import Company
company = self.db.query(Company).filter(Company.id == company_id).first()
if company:
tenant_id = company.tenant_id
except:
pass
# 2. Buscar si ya existe el rol "super_admin"
admin_role = self.db.query(CompanyRole).filter(
CompanyRole.company_id == company_id,
CompanyRole.code == "super_admin"
).first()
if not admin_role:
logger.info(f"Bootstrap: Creando Super Administrador para usuario {user_id} (Company: {company_id})")
# Crear el rol Super Administrador si no existe
admin_role = CompanyRole(
company_id=company_id,
tenant_id=tenant_id,
name="Super Administrador",
code="super_admin",
description="Rol con acceso total al sistema (generado automáticamente)",
is_active=True
)
self.db.add(admin_role)
self.db.flush()
# 4. Asignar TODOS los permisos activos al rol
all_perms = self.db.query(Permission).filter(Permission.is_active == True).all()
if not all_perms:
logger.warning("Bootstrap: ¡ALERTA! No se encontraron permisos en la DB ni tras la sincronización.")
for perm in all_perms:
role_perm = RolePermission(
company_role_id=admin_role.id,
permission_id=perm.id,
tenant_id=tenant_id,
company_id=company_id
)
self.db.add(role_perm)
else:
logger.info(f"Bootstrap: El rol super_admin ya existe para la compañía {company_id}.")
# 5. Asegurar que el usuario tenga el rol asignado
user_has_role = self.db.query(UserCompanyRole).filter(
UserCompanyRole.user_id == user_id,
UserCompanyRole.company_id == company_id,
UserCompanyRole.company_role_id == admin_role.id
).first()
if not user_has_role:
logger.info(f"Bootstrap: Asignando rol Super Administrador al usuario {user_id}")
user_role = UserCompanyRole(
user_id=user_id,
company_id=company_id,
tenant_id=tenant_id,
company_role_id=admin_role.id,
is_active=True,
assigned_by="SYSTEM_BOOTSTRAP"
)
self.db.add(user_role)
self.db.commit()
return True
else:
logger.info(f"Bootstrap: El usuario {user_id} ya tiene el rol asignado.")
self.db.commit()
return False
except Exception as e:
self.db.rollback()
logger.error(f"Bootstrap: ERROR CRÍTICO - {str(e)}")
import traceback
logger.error(traceback.format_exc())
return False

View File

@@ -0,0 +1,50 @@
"""
Script CLI para sincronizar los permisos de la aplicación.
Útil para el bootstrap inicial cuando el endpoint /sync aún no es accesible
o cuando se desea forzar una actualización desde la consola/Docker.
Uso:
docker exec -it <container> python3 -m api.v1.modules.core.permissions.sync_cli
"""
import sys
import os
import logging
# Configurar logging básico para ver resultados en consola
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
# Asegurar que el backend esté en el path
sys.path.append(os.path.abspath("."))
sys.path.append(os.path.abspath("backend"))
from core.database import CoreSessionLocal
from api.v1.modules.core.permissions.service import PermissionService
def run_sync():
"""Ejecuta la lógica de sincronización modular."""
logger.info("Iniciando Sincronización Modular de Permisos...")
db = CoreSessionLocal()
try:
service = PermissionService(db)
result = service.sync_permissions()
logger.info("-" * 40)
logger.info(f"Sincronización Exitosa!")
logger.info(f" - Nuevos insertados: {result['synced']}")
logger.info(f" - Existentes actualizados: {result['updated']}")
logger.info(f" - Total en Registry: {result['total_registered']}")
logger.info("-" * 40)
except Exception as e:
logger.error(f"Error crítico durante la sincronización: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
db.close()
if __name__ == "__main__":
run_sync()