admin / Synapse-Cortex
publicSelf Hosted ITSM Tool with RBAC/Tenanting and MFA
Synapse-Cortex / Synapse-Cortexv2 / app / routers / ai_settings.py
5193 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | from datetime import datetime, timezone from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from .. import models, schemas from ..ai.investigator import test_api_key from ..ai.vault import VaultError, decrypt_secret, encrypt_secret from ..audit import log_audit from ..database import get_db from ..deps import get_session_user router = APIRouter(prefix="/api/v1/admin/ai", tags=["ai-settings"]) def _require_global_admin(user: Optional[models.User]) -> None: if not user: raise HTTPException(status.HTTP_401_UNAUTHORIZED) if user.role != models.UserRole.GLOBAL_ADMIN: raise HTTPException(status.HTTP_403_FORBIDDEN, detail="Only a global admin can change the AI module setting") def _settings_out(tenant: models.Tenant) -> schemas.AiSettingsOut: return schemas.AiSettingsOut( ai_enabled=tenant.ai_enabled, anthropic_key_configured=bool(tenant.anthropic_api_key_encrypted), anthropic_key_last_test_ok=tenant.anthropic_key_last_test_ok, anthropic_key_last_tested_at=tenant.anthropic_key_last_tested_at, ) @router.get("", response_model=schemas.AiSettingsOut) def get_ai_settings(db: Session = Depends(get_db), user: Optional[models.User] = Depends(get_session_user)): _require_global_admin(user) tenant = db.get(models.Tenant, user.tenant_id) return _settings_out(tenant) @router.patch("", response_model=schemas.AiSettingsOut) def update_ai_settings( payload: schemas.AiSettingsUpdate, db: Session = Depends(get_db), user: Optional[models.User] = Depends(get_session_user), ): _require_global_admin(user) tenant = db.get(models.Tenant, user.tenant_id) if tenant.ai_enabled != payload.ai_enabled: tenant.ai_enabled = payload.ai_enabled log_audit( db, tenant_id=user.tenant_id, user_id=user.id, action="ai_toggle", detail={"ai_enabled": payload.ai_enabled} ) db.commit() return _settings_out(tenant) @router.patch("/anthropic-key", response_model=schemas.AiSettingsOut) def save_anthropic_key( payload: schemas.AnthropicKeyUpdate, db: Session = Depends(get_db), user: Optional[models.User] = Depends(get_session_user), ): """Encrypts and stores the tenant's Claude API key using the vault key - a vault must already exist (Admin -> Vault) before a key can be saved. Clears any previous test result, since a newly-saved key hasn't been tested yet.""" _require_global_admin(user) tenant = db.get(models.Tenant, user.tenant_id) try: tenant.anthropic_api_key_encrypted = encrypt_secret(tenant, payload.api_key) except VaultError as exc: raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(exc)) tenant.anthropic_key_last_test_ok = None tenant.anthropic_key_last_tested_at = None # Never log the key itself. log_audit(db, tenant_id=user.tenant_id, user_id=user.id, action="ai_key_save", detail={}) db.commit() db.refresh(tenant) return _settings_out(tenant) @router.delete("/anthropic-key", response_model=schemas.AiSettingsOut) def remove_anthropic_key(db: Session = Depends(get_db), user: Optional[models.User] = Depends(get_session_user)): _require_global_admin(user) tenant = db.get(models.Tenant, user.tenant_id) tenant.anthropic_api_key_encrypted = None tenant.anthropic_key_last_test_ok = None tenant.anthropic_key_last_tested_at = None log_audit(db, tenant_id=user.tenant_id, user_id=user.id, action="ai_key_remove", detail={}) db.commit() db.refresh(tenant) return _settings_out(tenant) @router.post("/anthropic-key/test", response_model=schemas.AnthropicKeyTestResult) def test_anthropic_key( payload: schemas.AnthropicKeyTestRequest, db: Session = Depends(get_db), user: Optional[models.User] = Depends(get_session_user), ): """Tests payload.api_key if provided (an unsaved candidate straight from the input field), otherwise decrypts and tests the currently saved key. Only the latter updates the tenant's last-tested status, since testing a candidate that was never saved shouldn't overwrite the status shown for the key actually in use.""" _require_global_admin(user) tenant = db.get(models.Tenant, user.tenant_id) testing_saved_key = payload.api_key is None if payload.api_key is not None: api_key = payload.api_key else: if not tenant.anthropic_api_key_encrypted: raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="No Claude API key is saved yet.") try: api_key = decrypt_secret(tenant, tenant.anthropic_api_key_encrypted) except VaultError as exc: raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(exc)) ok, message = test_api_key(api_key) if testing_saved_key: tenant.anthropic_key_last_test_ok = ok tenant.anthropic_key_last_tested_at = datetime.now(timezone.utc) log_audit(db, tenant_id=user.tenant_id, user_id=user.id, action="ai_key_test", detail={"ok": ok}) db.commit() return schemas.AnthropicKeyTestResult(ok=ok, message=message) |