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.fin.invoices.dto import PaymentCreate
|
||||||
from api.v1.modules.core.permissions.models import CompanyRole, Permission, RolePermission
|
from api.v1.modules.core.permissions.models import CompanyRole, Permission, RolePermission
|
||||||
from api.v1.modules.core.permissions.service import PermissionService
|
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
|
from core.database import CoreSessionLocal
|
||||||
|
|
||||||
# Deben coincidir con DEV_LOCAL_AUTH_TENANT_ID / DEV_LOCAL_AUTH_COMPANY_ID
|
# 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.
|
# 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.
|
# clave = (code, nombre); valor = función que decide si un permiso pertenece al rol.
|
||||||
def _carril_roles() -> dict:
|
def _carril_roles() -> dict:
|
||||||
|
# cada función recibe el CÓDIGO del permiso (str) y decide si pertenece al carril
|
||||||
return {
|
return {
|
||||||
("ventas", "Ventas"): lambda p: p.module == "crm" or p in {"ops.access", "ops.shipment.view"},
|
("ventas", "Ventas"): lambda c: c.startswith("crm.") or c in {"ops.access", "ops.shipment.view"},
|
||||||
("operaciones", "Operaciones"): lambda p: p.module == "ops"
|
("operaciones", "Operaciones"): lambda c: c.startswith("ops.")
|
||||||
or p in {"crm.access", "crm.account.view", "crm.quote.view", "crm.service_request.view", "fin.access", "fin.invoice.view"},
|
or c 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"
|
("facturacion", "Facturación"): lambda c: c.startswith("fin.")
|
||||||
or p in {"ops.access", "ops.shipment.view", "crm.access", "crm.account.view"},
|
or c in {"ops.access", "ops.shipment.view", "crm.access", "crm.account.view"},
|
||||||
("consulta", "Consulta"): lambda p: p.endswith(".access") or p.endswith(".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:
|
def seed_carril_roles(db) -> None:
|
||||||
"""Crea los roles por carril del proceso con sus permisos (R-T-07). Idempotente."""
|
"""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
|
# 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_suppliers_and_related(db)
|
||||||
seed_commercial_and_ops(db)
|
seed_commercial_and_ops(db)
|
||||||
seed_invoicing_and_events(db)
|
seed_invoicing_and_events(db)
|
||||||
|
ensure_company(db)
|
||||||
seed_carril_roles(db)
|
seed_carril_roles(db)
|
||||||
print("\nSeed CRM completado.")
|
print("\nSeed CRM completado.")
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
Reference in New Issue
Block a user