fix: handle credential decryption errors gracefully (#740)

- Credential.get_all() now uses per-row error handling instead of failing on first bad row
- Broken credentials include decryption_error field with descriptive message
- DELETE endpoint falls back to direct DB delete when credential can't be decrypted
- Frontend shows amber warning alert for broken credentials with disabled test/edit/discover
- Added i18n translation keys for decryption error warning in all 9 locales
This commit is contained in:
Luis Novo 2026-04-12 21:22:37 -03:00
parent 4ae459ca5e
commit ba01f7df4e
15 changed files with 120 additions and 18 deletions

View file

@ -223,6 +223,7 @@ def credential_to_response(cred: Credential, model_count: int = 0) -> Credential
created=str(cred.created) if cred.created else "",
updated=str(cred.updated) if cred.updated else "",
model_count=model_count,
decryption_error=cred.decryption_error,
)

View file

@ -617,6 +617,7 @@ class CredentialResponse(BaseModel):
created: str
updated: str
model_count: int = 0
decryption_error: Optional[str] = None
class CredentialDeleteResponse(BaseModel):

View file

@ -27,16 +27,22 @@ from pydantic import SecretStr
from api.credentials_service import (
credential_to_response,
discover_with_config,
migrate_from_env as svc_migrate_from_env,
migrate_from_provider_config as svc_migrate_from_provider_config,
get_provider_status,
register_models,
require_encryption_key,
test_credential as svc_test_credential,
validate_url,
)
from api.credentials_service import (
get_env_status as svc_get_env_status,
get_provider_status,
)
from api.credentials_service import (
migrate_from_env as svc_migrate_from_env,
)
from api.credentials_service import (
migrate_from_provider_config as svc_migrate_from_provider_config,
)
from api.credentials_service import (
test_credential as svc_test_credential,
)
from api.models import (
CreateCredentialRequest,
@ -48,6 +54,7 @@ from api.models import (
RegisterModelsResponse,
UpdateCredentialRequest,
)
from open_notebook.database.repository import ensure_record_id, repo_delete, repo_query
from open_notebook.domain.credential import Credential
router = APIRouter(prefix="/credentials", tags=["credentials"])
@ -260,7 +267,36 @@ async def delete_credential(
- Otherwise, linked models are cascade-deleted automatically
"""
try:
cred = await Credential.get(credential_id)
try:
cred = await Credential.get(credential_id)
except Exception as decrypt_err:
# Credential exists but can't be decrypted (wrong encryption key).
# Fall back to direct DB operations for deletion.
logger.warning(
f"Cannot decrypt credential {credential_id}, "
f"falling back to direct delete: {decrypt_err}"
)
# Delete linked models directly
linked = await repo_query(
"SELECT * FROM model WHERE credential = $cred_id",
{"cred_id": ensure_record_id(credential_id)},
)
deleted_models = 0
for model_row in linked:
model_id = str(model_row.get("id", ""))
if model_id:
await repo_delete(model_id)
deleted_models += 1
# Delete the credential itself
await repo_delete(credential_id)
return CredentialDeleteResponse(
message="Credential deleted successfully",
deleted_models=deleted_models,
)
linked_models = await cred.get_linked_models()
deleted_models = 0