Catalyst / admin/Synapse-Cortex 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Cortex

public

Self Hosted ITSM Tool with RBAC/Tenanting and MFA

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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)