Files
A-Team-Security-Infra-Agent…/backend/routes/settings.py
nogueiraguh 1135e9d6a9 refactor: decompose monolith — app.py 10,600→143 lines, 30+ modules
Fase 0 complete: extract all business logic, auth, database, and 176
endpoints from monolithic app.py into dedicated modules.

Structure:
- app.py: FastAPI hub (CORS, routers, startup/shutdown)
- models.py: 13 Pydantic request models
- utils.py: shared utilities (embed status, upload validation, process registries)
- config.py: constants, env vars, model catalogs
- auth/: crypto, jwt_auth, oidc, rate_limit
- database/: db(), init_db(), schema DDL
- services/: genai, compliance, chat, terraform, embeddings, cis_reports, mcp
- routes/: 13 APIRouter modules (auth, users, oci_config, oci_explorer,
  genai, mcp, adb, embeddings, reports, chat, terraform, settings, cis_engine)

Also: README updated with OCIR auth instructions for manual docker run.

86 tests passing.
2026-04-06 15:20:10 -03:00

301 lines
17 KiB
Python

"""Settings routes — audit log, config logs, chat logs, app settings, system prompts, config export/import."""
import uuid
import json
import base64
from datetime import datetime
from pathlib import Path
from fastapi import APIRouter, HTTPException, Depends, Query, UploadFile, File
from fastapi.responses import StreamingResponse
from config import VERSION, OCI_DIR, log
from database import db
from auth.jwt_auth import current_user, require, _audit
from auth.crypto import _enc, _dec, _mask, _safe_dec
router = APIRouter()
# ── Audit Log ────────────────────────────────────────────────────────────────
@router.get("/api/audit-log")
async def audit_log(skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=500), u=Depends(current_user)):
with db() as c:
if u["role"] == "admin":
rows = c.execute("SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ? OFFSET ?", (limit, skip)).fetchall()
else:
rows = c.execute("SELECT * FROM audit_log WHERE user_id=? ORDER BY created_at DESC LIMIT ? OFFSET ?", (u["id"], limit, skip)).fetchall()
return [dict(r) for r in rows]
# ── Config Logs ──────────────────────────────────────────────────────────────
@router.get("/api/config-logs")
async def get_config_logs(
config_type: str = Query(None), config_id: str = Query(None),
severity: str = Query(None), limit: int = Query(50, le=200),
u=Depends(current_user)
):
query = "SELECT * FROM config_logs WHERE 1=1"
params = []
if config_type: query += " AND config_type=?"; params.append(config_type)
if config_id: query += " AND config_id=?"; params.append(config_id)
if severity: query += " AND severity=?"; params.append(severity)
if u["role"] != "admin": query += " AND user_id=?"; params.append(u["id"])
query += " ORDER BY created_at DESC LIMIT ?"
params.append(limit)
with db() as c: rows = c.execute(query, params).fetchall()
return [dict(r) for r in rows]
# ── Chat Logs ────────────────────────────────────────────────────────────────
@router.get("/api/chat-logs")
async def get_chat_logs(
severity: str = Query(None), session_id: str = Query(None),
limit: int = Query(50, le=200), u=Depends(current_user)
):
query = "SELECT * FROM chat_logs WHERE 1=1"
params = []
if severity: query += " AND severity=?"; params.append(severity)
if session_id: query += " AND session_id=?"; params.append(session_id)
if u["role"] != "admin": query += " AND user_id=?"; params.append(u["id"])
query += " ORDER BY created_at DESC LIMIT ?"
params.append(limit)
with db() as c: rows = c.execute(query, params).fetchall()
return [dict(r) for r in rows]
# ── App Settings ─────────────────────────────────────────────────────────────
_SENSITIVE_SETTING_PREFIXES = ("oidc_", "secret_", "app_secret", "encryption_")
@router.get("/api/settings/{key}")
async def get_setting(key: str, u=Depends(current_user)):
if any(key.startswith(p) or key == p for p in _SENSITIVE_SETTING_PREFIXES) and u["role"] != "admin":
raise HTTPException(403, "Acesso negado a configuração sensível")
with db() as c:
row = c.execute("SELECT value FROM app_settings WHERE key=?", (key,)).fetchone()
val = row["value"] if row else ""
if key == "oidc_client_secret" and val:
val = _mask(_safe_dec(val))
return {"key": key, "value": val}
@router.put("/api/settings/{key}")
async def put_setting(key: str, body: dict, u=Depends(require("admin"))):
value = body.get("value", "")
if key == "oidc_client_secret" and value and "\u2022" in value:
return {"key": key, "value": value}
if key == "oidc_client_secret" and value:
value = _enc(value)
with db() as c:
c.execute("INSERT INTO app_settings (key,value,updated_at) VALUES (?,?,datetime('now')) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at", (key, value))
return {"key": key, "value": value}
@router.get("/api/settings/timezone/options")
async def timezone_options(u=Depends(current_user)):
"""List common timezone options."""
return {"timezones": [
"America/Sao_Paulo", "America/New_York", "America/Chicago", "America/Denver",
"America/Los_Angeles", "America/Toronto", "America/Mexico_City", "America/Bogota",
"America/Argentina/Buenos_Aires", "America/Santiago",
"Europe/London", "Europe/Paris", "Europe/Berlin", "Europe/Madrid", "Europe/Lisbon",
"Asia/Tokyo", "Asia/Shanghai", "Asia/Singapore", "Asia/Kolkata", "Asia/Dubai",
"Australia/Sydney", "Pacific/Auckland", "UTC",
]}
@router.put("/api/users/me/timezone")
async def set_user_timezone(body: dict, u=Depends(current_user)):
"""Set the current user's timezone."""
tz = body.get("timezone", "").strip()
if not tz:
raise HTTPException(400, "timezone is required")
try:
import zoneinfo
zoneinfo.ZoneInfo(tz)
except Exception:
raise HTTPException(400, f"Invalid timezone: {tz}")
with db() as c:
c.execute("UPDATE users SET timezone=? WHERE id=?", (tz, u["id"]))
_audit(u["id"], u["username"], "set_timezone", tz)
return {"ok": True, "timezone": tz}
@router.get("/api/users/me/timezone")
async def get_user_timezone(u=Depends(current_user)):
"""Get the current user's timezone."""
return {"timezone": u.get("timezone", "America/Sao_Paulo")}
# ── System Prompts ───────────────────────────────────────────────────────────
@router.get("/api/prompts/{agent}")
async def list_prompts(agent: str, u=Depends(current_user)):
with db() as c:
rows = c.execute("SELECT * FROM system_prompts WHERE agent=? ORDER BY is_active DESC, created_at DESC", (agent,)).fetchall()
return [dict(r) for r in rows]
@router.post("/api/prompts")
async def save_prompt(body: dict, u=Depends(require("admin"))):
agent = body.get("agent", "chat")
with db() as c:
count = c.execute("SELECT COUNT(*) FROM system_prompts WHERE agent=?", (agent,)).fetchone()[0]
if count >= 10:
raise HTTPException(400, "Limite de 10 prompts atingido. Exclua um antes de criar outro.")
pid = str(uuid.uuid4())
name = body.get("name", "Sem nome")
content = body.get("content", "")
is_active = body.get("is_active", False)
with db() as c:
if is_active:
c.execute("UPDATE system_prompts SET is_active=0 WHERE agent=?", (agent,))
c.execute("INSERT INTO system_prompts (id,name,agent,content,is_active) VALUES (?,?,?,?,?)",
(pid, name, agent, content, int(is_active)))
return {"id": pid}
@router.put("/api/prompts/{pid}")
async def update_prompt(pid: str, body: dict, u=Depends(require("admin"))):
with db() as c:
existing = c.execute("SELECT * FROM system_prompts WHERE id=?", (pid,)).fetchone()
if not existing:
raise HTTPException(404)
name = body.get("name", existing["name"])
content = body.get("content", existing["content"])
is_active = body.get("is_active", existing["is_active"])
with db() as c:
if is_active:
c.execute("UPDATE system_prompts SET is_active=0 WHERE agent=?", (existing["agent"],))
c.execute("UPDATE system_prompts SET name=?,content=?,is_active=? WHERE id=?",
(name, content, int(is_active), pid))
return {"id": pid}
@router.delete("/api/prompts/{pid}")
async def delete_prompt(pid: str, u=Depends(require("admin"))):
with db() as c:
c.execute("DELETE FROM system_prompts WHERE id=?", (pid,))
return {"ok": True}
# ── Export / Import Configuration ────────────────────────────────────────────
@router.get("/api/config/export")
async def export_config(u=Depends(require("admin"))):
"""Export configurations as JSON. Excludes sensitive credentials (private keys, passwords, secrets)."""
_OCI_SAFE_COLS = "id,user_id,tenancy_name,region,compartment_id,auth_type,ssh_public_key,created_at"
_ADB_SAFE_COLS = "id,user_id,config_name,dsn,username,table_name,use_mtls,is_active,is_global,genai_config_id,embedding_model_id,created_at"
data = {"version": VERSION, "exported_at": datetime.now().isoformat()}
with db() as c:
# OCI configs (no private keys, no encrypted OCIDs, no passphrases)
data["oci_configs"] = [dict(r) for r in c.execute(f"SELECT {_OCI_SAFE_COLS} FROM oci_configs").fetchall()]
# GenAI configs (no secrets — model info only)
data["genai_configs"] = [dict(r) for r in c.execute("SELECT * FROM genai_configs").fetchall()]
# MCP servers
data["mcp_servers"] = [dict(r) for r in c.execute("SELECT * FROM mcp_servers").fetchall()]
# System prompts
data["system_prompts"] = [dict(r) for r in c.execute("SELECT * FROM system_prompts").fetchall()]
# App settings (exclude sensitive keys)
data["app_settings"] = [dict(r) for r in c.execute("SELECT * FROM app_settings").fetchall()
if not any(r["key"].startswith(p) for p in _SENSITIVE_SETTING_PREFIXES)]
# ADB vector configs (no passwords, no wallet passwords)
data["adb_vector_configs"] = [dict(r) for r in c.execute(f"SELECT {_ADB_SAFE_COLS} FROM adb_vector_configs").fetchall()]
# ADB vector tables
data["adb_vector_tables"] = [dict(r) for r in c.execute("SELECT * FROM adb_vector_tables").fetchall()]
return StreamingResponse(
iter([json.dumps(data, indent=2, ensure_ascii=False)]),
media_type="application/json",
headers={"Content-Disposition": f'attachment; filename="oci-cis-agent-config-{datetime.now().strftime("%Y%m%d-%H%M%S")}.json"'}
)
@router.post("/api/config/import")
async def import_config(file: UploadFile = File(...), u=Depends(require("admin"))):
"""Import configurations from exported JSON. Merges by ID (skip existing)."""
from utils import validate_upload
await validate_upload(file, {".json"})
file.file.seek(0)
content = await file.read()
try:
data = json.loads(content)
except json.JSONDecodeError:
raise HTTPException(400, "Arquivo JSON inválido")
counts = {"oci_configs": 0, "genai_configs": 0, "mcp_servers": 0, "system_prompts": 0, "app_settings": 0, "adb_vector_configs": 0, "adb_vector_tables": 0, "skipped": 0}
with db() as c:
# OCI configs
for cfg in data.get("oci_configs", []):
if c.execute("SELECT 1 FROM oci_configs WHERE id=?", (cfg["id"],)).fetchone():
counts["skipped"] += 1; continue
# Import metadata only — credentials must be reconfigured manually
cdir = OCI_DIR / cfg["id"]; cdir.mkdir(parents=True, exist_ok=True)
kp = cdir / "oci_api_key.pem"
# Restore private key only if present in legacy export format
if cfg.get("_private_key_b64"):
kp.write_bytes(base64.b64decode(cfg["_private_key_b64"])); kp.chmod(0o600)
cfg_file = cdir / "config"
cfg_content = (f"[DEFAULT]\nuser={_dec(cfg['user_ocid']) if cfg.get('user_ocid') else ''}\n"
f"fingerprint={_dec(cfg['fingerprint']) if cfg.get('fingerprint') else ''}\n"
f"tenancy={_dec(cfg['tenancy_ocid']) if cfg.get('tenancy_ocid') else ''}\n"
f"region={cfg.get('region','')}\nkey_file={kp}\n")
if cfg.get("_key_passphrase"):
cfg_content += f"pass_phrase={cfg['_key_passphrase']}\n"
cfg_file.write_text(cfg_content); cfg_file.chmod(0o600)
c.execute("INSERT INTO oci_configs (id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,key_file_path,compartment_id,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
(cfg["id"], cfg.get("user_id", u["id"]), cfg.get("tenancy_name",""),
_enc(cfg["tenancy_ocid"]) if cfg.get("tenancy_ocid") else "",
_enc(cfg["user_ocid"]) if cfg.get("user_ocid") else "",
_enc(cfg["fingerprint"]) if cfg.get("fingerprint") else "",
cfg.get("region",""), str(kp),
_enc(cfg["compartment_id"]) if cfg.get("compartment_id") else None,
cfg.get("created_at")))
counts["oci_configs"] += 1
# GenAI configs
for cfg in data.get("genai_configs", []):
if c.execute("SELECT 1 FROM genai_configs WHERE id=?", (cfg["id"],)).fetchone():
counts["skipped"] += 1; continue
c.execute("INSERT INTO genai_configs (id,user_id,name,oci_config_id,model_id,model_ocid,compartment_id,genai_region,endpoint,serving_type,dedicated_endpoint_id,temperature,max_tokens,top_p,top_k,frequency_penalty,presence_penalty,reasoning_effort,is_default,system_prompt,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(cfg["id"], cfg.get("user_id", u["id"]), cfg.get("name",""), cfg.get("oci_config_id",""), cfg.get("model_id",""),
cfg.get("model_ocid"), cfg.get("compartment_id",""), cfg.get("genai_region",""), cfg.get("endpoint",""),
cfg.get("serving_type","ON_DEMAND"), cfg.get("dedicated_endpoint_id"), cfg.get("temperature",1),
cfg.get("max_tokens",6000), cfg.get("top_p",0.95), cfg.get("top_k",1), cfg.get("frequency_penalty",0),
cfg.get("presence_penalty",0), cfg.get("reasoning_effort"), cfg.get("is_default",0), cfg.get("system_prompt",""), cfg.get("created_at")))
counts["genai_configs"] += 1
# MCP servers
for srv in data.get("mcp_servers", []):
if c.execute("SELECT 1 FROM mcp_servers WHERE id=?", (srv["id"],)).fetchone():
counts["skipped"] += 1; continue
c.execute("INSERT INTO mcp_servers (id,user_id,name,description,server_type,command,args,env_vars,url,module_path,tools,linked_adb_id,is_active,is_global,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(srv["id"], srv.get("user_id", u["id"]), srv.get("name",""), srv.get("description"), srv.get("server_type","stdio"),
srv.get("command"), srv.get("args"), srv.get("env_vars"), srv.get("url"), srv.get("module_path"),
srv.get("tools"), srv.get("linked_adb_id"), srv.get("is_active",1), srv.get("is_global",0), srv.get("created_at")))
counts["mcp_servers"] += 1
# System prompts
for p in data.get("system_prompts", []):
if c.execute("SELECT 1 FROM system_prompts WHERE id=?", (p["id"],)).fetchone():
counts["skipped"] += 1; continue
c.execute("INSERT INTO system_prompts (id,name,agent,content,is_active,created_at) VALUES (?,?,?,?,?,?)",
(p["id"], p.get("name",""), p.get("agent","chat"), p.get("content",""), p.get("is_active",0), p.get("created_at")))
counts["system_prompts"] += 1
# App settings
for s in data.get("app_settings", []):
if any(s["key"].startswith(p) for p in _SENSITIVE_SETTING_PREFIXES):
counts["skipped"] += 1; continue
c.execute("INSERT INTO app_settings (key,value,updated_at) VALUES (?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at",
(s["key"], s.get("value",""), s.get("updated_at")))
counts["app_settings"] += 1
# ADB vector configs
for cfg in data.get("adb_vector_configs", []):
if c.execute("SELECT 1 FROM adb_vector_configs WHERE id=?", (cfg["id"],)).fetchone():
counts["skipped"] += 1; continue
c.execute("INSERT INTO adb_vector_configs (id,user_id,config_name,dsn,username,password_enc,wallet_dir,wallet_password_enc,table_name,use_mtls,is_active,is_global,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
(cfg["id"], cfg.get("user_id", u["id"]), cfg.get("config_name",""), cfg.get("dsn",""), cfg.get("username",""),
cfg.get("password_enc",""), cfg.get("wallet_dir"), cfg.get("wallet_password_enc"), cfg.get("table_name","CIS_EMBEDDINGS"),
cfg.get("use_mtls",1), cfg.get("is_active",1), cfg.get("is_global",0), cfg.get("created_at")))
counts["adb_vector_configs"] += 1
# ADB vector tables
for t in data.get("adb_vector_tables", []):
if c.execute("SELECT 1 FROM adb_vector_tables WHERE id=?", (t["id"],)).fetchone():
counts["skipped"] += 1; continue
c.execute("INSERT INTO adb_vector_tables (id,adb_config_id,table_name,description,is_active,created_at) VALUES (?,?,?,?,?,?)",
(t["id"], t.get("adb_config_id",""), t.get("table_name",""), t.get("description",""), t.get("is_active",1), t.get("created_at")))
counts["adb_vector_tables"] += 1
_audit(u["id"], u["username"], "import_config", "bulk", json.dumps(counts))
return counts