fix(core,crm): habilitar RBAC por carril sin romper el entorno dev
- Crea el módulo faltante permissions/seed_v2.py (register_core_permissions), que sync_permissions y bootstrap_super_admin importan; sin él ambos fallaban con ImportError y el auto-bootstrap dev quedaba inoperante. - seed: ensure_company (a76.company id=1, requerida por la FK de company_roles) y seed_carril_roles (Ventas, Operaciones, Facturación, Consulta) con sus permisos; pobla el PermissionRegistry importando los permisos de cada dominio. - Verificado end-to-end: con enforcement activo el usuario dev auto-bootstrapea a super_admin y crm/ops/fin responden 200 (no 403). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
27
backend/api/v1/modules/core/permissions/seed_v2.py
Normal file
27
backend/api/v1/modules/core/permissions/seed_v2.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Registro de permisos base del núcleo (core).
|
||||
|
||||
``PermissionService.sync_permissions`` y ``bootstrap_super_admin`` importan este
|
||||
módulo por su efecto secundario: dar de alta en el ``PermissionRegistry`` los
|
||||
permisos transversales del sistema antes de sincronizarlos a la base de datos.
|
||||
Los permisos de cada dominio (crm, ops, fin) se registran en el
|
||||
``register_permissions()`` de su propio módulo al importar sus routers.
|
||||
"""
|
||||
|
||||
from .registry import registry
|
||||
|
||||
MODULE = "core"
|
||||
|
||||
|
||||
def register_core_permissions() -> None:
|
||||
"""Da de alta los permisos base del sistema (idempotente)."""
|
||||
registry.register(code="core.access", description="Acceso al sistema", module=MODULE, action="access")
|
||||
registry.register(code="core.admin", description="Administración del sistema", module=MODULE, action="admin")
|
||||
registry.register(
|
||||
code="core.permissions.manage",
|
||||
description="Gestionar roles y permisos",
|
||||
module=MODULE,
|
||||
action="manage",
|
||||
)
|
||||
|
||||
|
||||
register_core_permissions()
|
||||
@@ -33,6 +33,10 @@ from api.v1.modules.fin.invoices import service as invoices_service
|
||||
from api.v1.modules.fin.invoices.dto import PaymentCreate
|
||||
from api.v1.modules.core.permissions.models import CompanyRole, Permission, RolePermission
|
||||
from api.v1.modules.core.permissions.service import PermissionService
|
||||
# Efecto secundario: poblar el PermissionRegistry con los permisos de cada dominio
|
||||
import api.v1.modules.crm.permissions # noqa: F401
|
||||
import api.v1.modules.ops.permissions # noqa: F401
|
||||
import api.v1.modules.fin.permissions # noqa: F401
|
||||
from core.database import CoreSessionLocal
|
||||
|
||||
# Deben coincidir con DEV_LOCAL_AUTH_TENANT_ID / DEV_LOCAL_AUTH_COMPANY_ID
|
||||
@@ -383,16 +387,35 @@ def seed_invoicing_and_events(db) -> None:
|
||||
# Carriles del proceso (R-T-07): a qué módulos/acciones puede acceder cada rol.
|
||||
# clave = (code, nombre); valor = función que decide si un permiso pertenece al rol.
|
||||
def _carril_roles() -> dict:
|
||||
# cada función recibe el CÓDIGO del permiso (str) y decide si pertenece al carril
|
||||
return {
|
||||
("ventas", "Ventas"): lambda p: p.module == "crm" or p in {"ops.access", "ops.shipment.view"},
|
||||
("operaciones", "Operaciones"): lambda p: p.module == "ops"
|
||||
or p in {"crm.access", "crm.account.view", "crm.quote.view", "crm.service_request.view", "fin.access", "fin.invoice.view"},
|
||||
("facturacion", "Facturación"): lambda p: p.module == "fin"
|
||||
or p in {"ops.access", "ops.shipment.view", "crm.access", "crm.account.view"},
|
||||
("consulta", "Consulta"): lambda p: p.endswith(".access") or p.endswith(".view"),
|
||||
("ventas", "Ventas"): lambda c: c.startswith("crm.") or c in {"ops.access", "ops.shipment.view"},
|
||||
("operaciones", "Operaciones"): lambda c: c.startswith("ops.")
|
||||
or c in {"crm.access", "crm.account.view", "crm.quote.view", "crm.service_request.view", "fin.access", "fin.invoice.view"},
|
||||
("facturacion", "Facturación"): lambda c: c.startswith("fin.")
|
||||
or c in {"ops.access", "ops.shipment.view", "crm.access", "crm.account.view"},
|
||||
("consulta", "Consulta"): lambda c: c.endswith(".access") or c.endswith(".view"),
|
||||
}
|
||||
|
||||
|
||||
def ensure_company(db) -> None:
|
||||
"""Garantiza la empresa dev (a76.company id=1), requerida por la FK company_id de
|
||||
los roles/permisos (core.company_roles → a76.company). La plantilla no la crea."""
|
||||
from sqlalchemy import text
|
||||
|
||||
exists = db.execute(text("SELECT 1 FROM a76.company WHERE id = :id"), {"id": COMPANY_ID}).first()
|
||||
if exists:
|
||||
print(f"• Empresa id={COMPANY_ID} ya existe (a76.company)")
|
||||
return
|
||||
db.execute(
|
||||
text("INSERT INTO a76.company (id, tenant_id) VALUES (:id, :tid)"),
|
||||
{"id": COMPANY_ID, "tid": TENANT_ID},
|
||||
)
|
||||
db.execute(text("SELECT setval('a76.company_id_seq', (SELECT MAX(id) FROM a76.company))"))
|
||||
db.commit()
|
||||
print(f"✓ Empresa demo id={COMPANY_ID} creada (a76.company)")
|
||||
|
||||
|
||||
def seed_carril_roles(db) -> None:
|
||||
"""Crea los roles por carril del proceso con sus permisos (R-T-07). Idempotente."""
|
||||
# Poblar el catálogo de permisos desde el registry de código
|
||||
@@ -436,6 +459,7 @@ def main() -> None:
|
||||
seed_suppliers_and_related(db)
|
||||
seed_commercial_and_ops(db)
|
||||
seed_invoicing_and_events(db)
|
||||
ensure_company(db)
|
||||
seed_carril_roles(db)
|
||||
print("\nSeed CRM completado.")
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user