"""Desk governance — audit log, user meta, module access. Spec 040 · DS-API-001.""" from __future__ import annotations import json import secrets import sqlite3 from datetime import datetime, timezone from typing import Any GOVERNANCE_MODULES = ( "desk", "openpanel", "billing", "api", "security", "ai_agents", ) ACCESS_LEVELS = ("none", "read", "partial", "full") ROLE_GROUPS: dict[str, str] = { "super_admin": "Ops", "ops_lead": "Ops", "technician": "Ops", "noc": "Ops", "sales_admin": "Comercial", "sales_support": "Comercial", "finance": "Negócio", "marketing": "Negócio", "seo": "Negócio", "developer": "Plataforma", "devops": "Plataforma", "security_analyst": "Plataforma", "content_editor": "Plataforma", "agentic_operator": "Plataforma", "partner": "Externo", } def _now() -> str: return datetime.now(timezone.utc).isoformat() def init_governance_schema(conn: sqlite3.Connection) -> None: conn.executescript( """ CREATE TABLE IF NOT EXISTS desk_governance_audit ( id INTEGER PRIMARY KEY AUTOINCREMENT, actor_username TEXT NOT NULL, actor_role TEXT, action TEXT NOT NULL, target_type TEXT NOT NULL, target_id TEXT NOT NULL, summary TEXT NOT NULL, payload_json TEXT, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_desk_gov_audit_target ON desk_governance_audit(target_type, target_id); CREATE INDEX IF NOT EXISTS idx_desk_gov_audit_created ON desk_governance_audit(created_at DESC); CREATE TABLE IF NOT EXISTS desk_user_meta ( username TEXT PRIMARY KEY, internal_id TEXT NOT NULL UNIQUE, main_group TEXT, secondary_groups_json TEXT NOT NULL DEFAULT '[]', account_status TEXT NOT NULL DEFAULT 'active', force_password_change INTEGER NOT NULL DEFAULT 0, api_access INTEGER NOT NULL DEFAULT 0, notifications_enabled INTEGER NOT NULL DEFAULT 1, module_permissions_json TEXT NOT NULL DEFAULT '{}', invite_token TEXT, notes TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); """ ) def _gen_internal_id() -> str: return f"LB-{secrets.token_hex(4).upper()}" def _gen_invite_token() -> str: return secrets.token_urlsafe(24) def group_for_role(role: str) -> str: return ROLE_GROUPS.get(role, "—") def default_module_permissions(role: str) -> dict[str, str]: base = {m: "none" for m in GOVERNANCE_MODULES} if role == "super_admin": return {m: "full" for m in GOVERNANCE_MODULES} if role in ("ops_lead", "devops"): base.update({"desk": "full", "openpanel": "partial", "security": "partial", "api": "read"}) elif role == "technician": base.update({"desk": "partial", "openpanel": "read"}) elif role == "finance": base.update({"billing": "full", "desk": "read"}) elif role == "agentic_operator": base.update({"ai_agents": "full", "desk": "partial"}) return base def log_audit( conn: sqlite3.Connection, *, actor_username: str, actor_role: str | None, action: str, target_type: str, target_id: str, summary: str, payload: dict | None = None, ) -> dict: now = _now() conn.execute( """ INSERT INTO desk_governance_audit (actor_username, actor_role, action, target_type, target_id, summary, payload_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( actor_username, actor_role, action, target_type, target_id, summary, json.dumps(payload or {}, ensure_ascii=False), now, ), ) return { "actor_username": actor_username, "actor_role": actor_role, "action": action, "target_type": target_type, "target_id": target_id, "summary": summary, "created_at": now, } def list_audit( conn: sqlite3.Connection, *, target_type: str | None = None, target_id: str | None = None, limit: int = 50, ) -> list[dict]: q = "SELECT * FROM desk_governance_audit WHERE 1=1" params: list[Any] = [] if target_type: q += " AND target_type = ?" params.append(target_type) if target_id: q += " AND target_id = ?" params.append(target_id) q += " ORDER BY id DESC LIMIT ?" params.append(limit) rows = conn.execute(q, params).fetchall() out = [] for row in rows: item = dict(row) try: item["payload"] = json.loads(item.pop("payload_json") or "{}") except json.JSONDecodeError: item["payload"] = {} out.append(item) return out def get_user_meta(conn: sqlite3.Connection, username: str) -> dict | None: row = conn.execute( "SELECT * FROM desk_user_meta WHERE username = ?", (username,), ).fetchone() if not row: return None item = dict(row) try: item["secondary_groups"] = json.loads(item.pop("secondary_groups_json") or "[]") except json.JSONDecodeError: item["secondary_groups"] = [] try: item["module_permissions"] = json.loads(item.pop("module_permissions_json") or "{}") except json.JSONDecodeError: item["module_permissions"] = {} item["force_password_change"] = bool(item.get("force_password_change")) item["api_access"] = bool(item.get("api_access")) item["notifications_enabled"] = bool(item.get("notifications_enabled")) return item def ensure_user_meta(conn: sqlite3.Connection, username: str, role: str) -> dict: existing = get_user_meta(conn, username) if existing: return existing now = _now() perms = default_module_permissions(role) conn.execute( """ INSERT INTO desk_user_meta (username, internal_id, main_group, secondary_groups_json, account_status, force_password_change, api_access, notifications_enabled, module_permissions_json, invite_token, notes, created_at, updated_at) VALUES (?, ?, ?, '[]', 'active', 0, 0, 1, ?, NULL, NULL, ?, ?) """, ( username, _gen_internal_id(), group_for_role(role), json.dumps(perms), now, now, ), ) return get_user_meta(conn, username) or {} def upsert_user_meta(conn: sqlite3.Connection, username: str, **fields: Any) -> dict: row = get_user_meta(conn, username) now = _now() if not row: raise ValueError("user meta missing") secondary = fields.get("secondary_groups", row.get("secondary_groups", [])) perms = fields.get("module_permissions", row.get("module_permissions", {})) conn.execute( """ UPDATE desk_user_meta SET main_group = ?, secondary_groups_json = ?, account_status = ?, force_password_change = ?, api_access = ?, notifications_enabled = ?, module_permissions_json = ?, invite_token = ?, notes = ?, updated_at = ? WHERE username = ? """, ( fields.get("main_group", row.get("main_group")), json.dumps(secondary), fields.get("account_status", row.get("account_status", "active")), 1 if fields.get("force_password_change", row.get("force_password_change")) else 0, 1 if fields.get("api_access", row.get("api_access")) else 0, 1 if fields.get("notifications_enabled", row.get("notifications_enabled", True)) else 0, json.dumps(perms), fields.get("invite_token", row.get("invite_token")), fields.get("notes", row.get("notes")), now, username, ), ) return get_user_meta(conn, username) or {}