admin / Synapse-Cortex
publicSelf Hosted ITSM Tool with RBAC/Tenanting and MFA
Synapse-Cortex / Synapse-Cortexv2 / app / routers / credentials.py
6258 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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | import uuid from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from .. import models, schemas from ..ai.vault import VaultError, encrypt_secret from ..audit import log_audit from ..database import get_db from ..deps import get_session_user router = APIRouter(prefix="/api/v1/credentials", tags=["credentials"]) # Any authenticated tenant user can vault/link a credential - agents need # this to link a host credential to a ticket, it isn't admin-only. @router.get("", response_model=list[schemas.CredentialOut]) def list_credentials(db: Session = Depends(get_db), user: Optional[models.User] = Depends(get_session_user)): if not user: raise HTTPException(status.HTTP_401_UNAUTHORIZED) return ( db.query(models.CredentialVaultEntry) .filter(models.CredentialVaultEntry.tenant_id == user.tenant_id) .order_by(models.CredentialVaultEntry.label) .all() ) @router.post("", response_model=schemas.CredentialOut, status_code=status.HTTP_201_CREATED) def create_credential( payload: schemas.CredentialCreate, db: Session = Depends(get_db), user: Optional[models.User] = Depends(get_session_user), ): if not user: raise HTTPException(status.HTTP_401_UNAUTHORIZED) existing = ( db.query(models.CredentialVaultEntry) .filter(models.CredentialVaultEntry.tenant_id == user.tenant_id, models.CredentialVaultEntry.label == payload.label) .first() ) if existing: raise HTTPException(status.HTTP_409_CONFLICT, detail="A credential with this label already exists") host = (payload.host or "").strip() asset: Optional[models.Asset] = None if payload.asset_id: asset = ( db.query(models.Asset) .filter(models.Asset.id == payload.asset_id, models.Asset.tenant_id == user.tenant_id) .first() ) if not asset: raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Asset not found in this tenant") if not asset.ip_address: raise HTTPException( status.HTTP_400_BAD_REQUEST, detail="This asset has no recorded IP address yet - it can't be used as a credential's host.", ) # Always derive host from the asset's own recorded IP rather than # trusting a client-supplied value, so the two can never drift. host = asset.ip_address already = ( db.query(models.CredentialVaultEntry) .filter( models.CredentialVaultEntry.tenant_id == user.tenant_id, models.CredentialVaultEntry.asset_id == payload.asset_id, ) .first() ) if already: raise HTTPException( status.HTTP_409_CONFLICT, detail=( f"A credential is already vaulted for this asset ({already.label}). " "Update it instead of creating another one." ), ) elif not host: raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail="host is required when asset_id is not set") tenant = db.get(models.Tenant, user.tenant_id) try: secret_encrypted = encrypt_secret(tenant, payload.secret) except VaultError as exc: raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(exc)) credential = models.CredentialVaultEntry( tenant_id=user.tenant_id, label=payload.label, username=payload.username, host=host, port=payload.port, secret_encrypted=secret_encrypted, asset_id=payload.asset_id, created_by_id=user.id, ) db.add(credential) db.flush() # Never log the secret - label/host/username only. log_audit( db, tenant_id=user.tenant_id, user_id=user.id, action="credential_create", detail={"label": credential.label, "host": credential.host, "asset_id": str(payload.asset_id) if payload.asset_id else None}, ) db.commit() db.refresh(credential) return credential @router.patch("/{credential_id}", response_model=schemas.CredentialOut) def update_credential( credential_id: uuid.UUID, payload: schemas.CredentialUpdate, db: Session = Depends(get_db), user: Optional[models.User] = Depends(get_session_user), ): if not user: raise HTTPException(status.HTTP_401_UNAUTHORIZED) credential = ( db.query(models.CredentialVaultEntry) .filter(models.CredentialVaultEntry.id == credential_id, models.CredentialVaultEntry.tenant_id == user.tenant_id) .first() ) if not credential: raise HTTPException(status.HTTP_404_NOT_FOUND) fields = payload.model_dump(exclude_unset=True, exclude={"secret"}) for field, value in fields.items(): setattr(credential, field, value) if payload.secret is not None: tenant = db.get(models.Tenant, user.tenant_id) try: credential.secret_encrypted = encrypt_secret(tenant, payload.secret) except VaultError as exc: raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(exc)) log_audit( db, tenant_id=user.tenant_id, user_id=user.id, action="credential_update", detail={"label": credential.label}, ) db.commit() db.refresh(credential) return credential @router.delete("/{credential_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_credential( credential_id: uuid.UUID, db: Session = Depends(get_db), user: Optional[models.User] = Depends(get_session_user), ): if not user: raise HTTPException(status.HTTP_401_UNAUTHORIZED) credential = ( db.query(models.CredentialVaultEntry) .filter(models.CredentialVaultEntry.id == credential_id, models.CredentialVaultEntry.tenant_id == user.tenant_id) .first() ) if not credential: raise HTTPException(status.HTTP_404_NOT_FOUND) log_audit(db, tenant_id=user.tenant_id, user_id=user.id, action="credential_delete", detail={"label": credential.label}) db.delete(credential) db.commit() return None |