"""Catálogo mestre de funções Ligbox — padrão Odoo res.groups aplicado à plataforma DevOps. Uma função Desk (`desk_role`) é a **fonte de verdade**; cada serviço/VM recebe um mapeamento explícito (grupo nativo, módulo, permissão API). Spec 027. Analogia Odoo: res.users.role → desk_users.role (VM122) res.groups → PLATFORM_ROLE_CATALOG[*].bindings[*] ir.model.access → permissions.py helpers + route guards record rules → should_mask_sensitive, ticket assignee, … """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any from app.modules.registry import ROLE_MODULE_DEFAULTS, role_module_defaults @dataclass(frozen=True) class ServiceBinding: """Como uma função se materializa num serviço concreto.""" service: str # desk | vm112 | vm123_foss | vm123_odoo | vm123_openpanel | infra binding_type: str # group | module | permission | deep_link | ssh value: str access: str = "full" # full | read | link | api | none @dataclass(frozen=True) class PlatformRole: id: str label: str category: str # ops | commercial | business | platform | system description: str bindings: tuple[ServiceBinding, ...] = () desk_modules: tuple[str, ...] | None = None # None = legacy ops (global toggles) def _desk_perms(*perms: str) -> tuple[ServiceBinding, ...]: return tuple(ServiceBinding("desk", "permission", p) for p in perms) # ── Catálogo mestre (fonte única para docs, provisionamento e introspecção) ── PLATFORM_ROLE_CATALOG: dict[str, PlatformRole] = { "super_admin": PlatformRole( id="super_admin", label="Super Admin", category="ops", description="Dono — users, tenants, purge, config global", desk_modules=None, bindings=( ServiceBinding("vm123_odoo", "group", "base.group_system"), ServiceBinding("vm123_foss", "group", "admin"), ServiceBinding("vm123_openpanel", "role", "Super Admin"), ServiceBinding("vm112", "permission", "assist.takeover"), ServiceBinding("vm112", "permission", "purge.domain"), ServiceBinding("infra", "access", "ssh", "full"), *_desk_perms("manage_users", "manage_billing", "run_audit", "manage_vm112_domains"), ), ), "ops_lead": PlatformRole( id="ops_lead", label="Chefe Ops", category="ops", description="Gestão operacional, audit, tickets, domínios VM112", desk_modules=None, bindings=( ServiceBinding("vm112", "permission", "assist.takeover"), ServiceBinding("vm112", "permission", "purge.domain"), ServiceBinding("infra", "access", "ssh", "link"), *_desk_perms("run_audit", "manage_vm112_domains", "manage_billing"), ), ), "technician": PlatformRole( id="technician", label="Suporte", category="ops", description="Tickets atribuídos, assist wizard, migração", desk_modules=None, bindings=( ServiceBinding("vm112", "permission", "assist.takeover"), ServiceBinding("vm123_openpanel", "permission", "autologin"), *_desk_perms("patch_assigned_tickets", "read_migration"), ), ), "noc": PlatformRole( id="noc", label="NOC", category="ops", description="Monitorização read-only — dados sensíveis mascarados", desk_modules=None, bindings=( ServiceBinding("desk", "permission", "mask_sensitive"), ServiceBinding("vm104", "permission", "wazuh.read"), *_desk_perms("read_tickets", "read_billing"), ), ), "sales_admin": PlatformRole( id="sales_admin", label="Sales Admin", category="commercial", description="Gerente comercial — pipeline, billing validation, FOSS+Odoo manager", desk_modules=tuple(ROLE_MODULE_DEFAULTS["sales_admin"]), bindings=( ServiceBinding("vm123_foss", "group", "ligbox-sales-admin"), ServiceBinding("vm123_odoo", "group", "sales_team.group_sale_manager"), ServiceBinding("vm123_openpanel", "role", "Admin"), *_desk_perms("validate_billing", "create_foss_order", "read_crm_leads"), ), ), "sales_support": PlatformRole( id="sales_support", label="Sales Support", category="commercial", description="Analista comercial — pedidos e CRM, sem validar billing", desk_modules=tuple(ROLE_MODULE_DEFAULTS["sales_support"]), bindings=( ServiceBinding("vm123_foss", "group", "ligbox-sales-support"), ServiceBinding("vm123_odoo", "group", "sales_team.group_sale_salesman"), ServiceBinding("vm123_openpanel", "permission", "autologin"), *_desk_perms("create_foss_order", "read_crm_leads"), ), ), "finance": PlatformRole( id="finance", label="Financeiro", category="business", description="FOSSBilling, Odoo fiscal, validação billing", desk_modules=tuple(ROLE_MODULE_DEFAULTS["finance"]), bindings=( ServiceBinding("vm123_foss", "group", "ligbox-finance-admin"), ServiceBinding("vm123_odoo", "group", "account.group_account_manager"), ServiceBinding("vm123_odoo", "group", "account.group_account_invoice"), *_desk_perms("validate_billing", "create_foss_order"), ), ), "marketing": PlatformRole( id="marketing", label="Marketing", category="business", description="Campanhas, leads, produtos FOSS", desk_modules=tuple(ROLE_MODULE_DEFAULTS["marketing"]), bindings=( ServiceBinding("vm123_foss", "group", "ligbox-marketing"), ServiceBinding("vm123_openpanel", "permission", "autologin"), *_desk_perms("read_crm_leads", "read_funnel"), ), ), "seo": PlatformRole( id="seo", label="SEO", category="business", description="DNS, Search Console, sites OpenPanel", desk_modules=tuple(ROLE_MODULE_DEFAULTS["seo"]), bindings=( ServiceBinding("vm123_openpanel", "permission", "autologin"), ServiceBinding("infra", "permission", "cloudflare_dns.read"), *_desk_perms("read_funnel", "read_crm_leads"), ), ), "developer": PlatformRole( id="developer", label="Developer", category="platform", description="Código wizard/Desk, GitHub, APIs", desk_modules=tuple(ROLE_MODULE_DEFAULTS["developer"]), bindings=( ServiceBinding("vm123_foss", "group", "ligbox-dev-api"), ServiceBinding("vm112", "permission", "api.dev_key"), ServiceBinding("infra", "access", "github", "full"), *_desk_perms("read_events"), ), ), "devops": PlatformRole( id="devops", label="DevOps", category="platform", description="Proxmox, Traefik, pfSense, OpenPanel admin", desk_modules=tuple(ROLE_MODULE_DEFAULTS["devops"]), bindings=( ServiceBinding("vm123_openpanel", "role", "Super Admin"), ServiceBinding("infra", "access", "ssh", "full"), ServiceBinding("infra", "permission", "proxmox", "full"), *_desk_perms("manage_vm112_domains"), ), ), "security_analyst": PlatformRole( id="security_analyst", label="Segurança / SOC", category="platform", description="Wazuh, incidentes, resposta", desk_modules=tuple(ROLE_MODULE_DEFAULTS["security_analyst"]), bindings=( ServiceBinding("vm104", "permission", "wazuh.manage"), *_desk_perms("read_audit_overview"), ), ), "content_editor": PlatformRole( id="content_editor", label="Conteúdo / CMS", category="platform", description="Sites clientes OpenPanel", desk_modules=tuple(ROLE_MODULE_DEFAULTS["content_editor"]), bindings=( ServiceBinding("vm123_openpanel", "permission", "autologin"), ), ), "agentic_operator": PlatformRole( id="agentic_operator", label="Operador Agentes IA", category="platform", description="Aprova runbooks A7 e acções agentes", desk_modules=tuple(ROLE_MODULE_DEFAULTS["agentic_operator"]), bindings=( ServiceBinding("desk", "permission", "approve_agent_remediation"), ), ), "api_service": PlatformRole( id="api_service", label="API Service", category="system", description="M2M webhooks e workers", desk_modules=("core",), bindings=( ServiceBinding("vm123_foss", "group", "ligbox-dev-api"), ServiceBinding("vm123_odoo", "group", "base.group_system"), ), ), "agent_system": PlatformRole( id="agent_system", label="Agent System", category="system", description="Conta dos agentes A0–A7", desk_modules=("core", "events"), bindings=(), ), } def catalog_for_role(role_id: str) -> PlatformRole | None: return PLATFORM_ROLE_CATALOG.get(role_id) def bindings_for_service(role_id: str, service: str) -> list[ServiceBinding]: role = catalog_for_role(role_id) if not role: return [] return [b for b in role.bindings if b.service == service] def catalog_export() -> dict[str, Any]: """JSON para API / docs — visão unificada estilo Odoo groups.""" out: dict[str, Any] = {"roles": {}, "services": ["desk", "vm112", "vm123_foss", "vm123_odoo", "vm123_openpanel", "infra", "vm104"]} for rid, role in PLATFORM_ROLE_CATALOG.items(): mods = role.desk_modules if mods is None: mods_list = list(role_module_defaults(rid) or []) # type: ignore[arg-type] legacy = True else: mods_list = list(mods) legacy = rid in ("super_admin", "ops_lead", "technician", "noc") out["roles"][rid] = { "id": rid, "label": role.label, "category": role.category, "description": role.description, "desk_modules": mods_list, "desk_modules_legacy_global": legacy, "bindings": [ {"service": b.service, "type": b.binding_type, "value": b.value, "access": b.access} for b in role.bindings ], } return out