Commita governance, user-wizard, operational-feed e catálogo RBAC; adiciona deploy-desk-full.sh, smoke-desk.sh e regra anti-deploy parcial; documenta credencial VM112 @betinplace. Co-authored-by: Cursor <cursoragent@cursor.com>
263 lines
7.9 KiB
Python
263 lines
7.9 KiB
Python
"""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 {}
|