mirror of
https://github.com/lfnovo/open-notebook.git
synced 2026-04-30 12:30:01 +00:00
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:
parent
4ae459ca5e
commit
ba01f7df4e
15 changed files with 120 additions and 18 deletions
|
|
@ -53,6 +53,7 @@ class Credential(ObjectModel):
|
|||
provider: str
|
||||
modalities: List[str] = []
|
||||
api_key: Optional[SecretStr] = None
|
||||
decryption_error: Optional[str] = None
|
||||
base_url: Optional[str] = None
|
||||
endpoint: Optional[str] = None
|
||||
api_version: Optional[str] = None
|
||||
|
|
@ -130,18 +131,47 @@ class Credential(ObjectModel):
|
|||
|
||||
@classmethod
|
||||
async def get_all(cls, order_by=None) -> List["Credential"]:
|
||||
"""Override get_all() to handle api_key decryption."""
|
||||
instances = await super().get_all(order_by=order_by)
|
||||
for instance in instances:
|
||||
if instance.api_key:
|
||||
raw = (
|
||||
instance.api_key.get_secret_value()
|
||||
if isinstance(instance.api_key, SecretStr)
|
||||
else instance.api_key
|
||||
"""Override get_all() to handle api_key decryption with per-row error handling."""
|
||||
order_clause = f" ORDER BY {order_by}" if order_by else ""
|
||||
results = await repo_query(
|
||||
f"SELECT * FROM {cls.table_name}{order_clause}",
|
||||
{},
|
||||
)
|
||||
credentials = []
|
||||
for row in results:
|
||||
try:
|
||||
cred = cls._from_db_row(row)
|
||||
credentials.append(cred)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to decrypt credential {row.get('id', 'unknown')}: {e}"
|
||||
)
|
||||
decrypted = decrypt_value(raw)
|
||||
object.__setattr__(instance, "api_key", SecretStr(decrypted))
|
||||
return instances
|
||||
# Create a minimal credential with error info from raw DB fields
|
||||
try:
|
||||
error_cred = cls(
|
||||
name=row.get("name", "Unknown"),
|
||||
provider=row.get("provider", "unknown"),
|
||||
modalities=row.get("modalities", []),
|
||||
decryption_error=f"Failed to decrypt API key. The encryption key may have changed. Error: {e}",
|
||||
)
|
||||
# Preserve the DB id, created, updated from the raw row
|
||||
if row.get("id"):
|
||||
object.__setattr__(error_cred, "id", str(row["id"]))
|
||||
if row.get("created"):
|
||||
object.__setattr__(error_cred, "created", row["created"])
|
||||
if row.get("updated"):
|
||||
object.__setattr__(error_cred, "updated", row["updated"])
|
||||
# Mark that it had an api_key (even though we can't decrypt it)
|
||||
if row.get("api_key"):
|
||||
object.__setattr__(
|
||||
error_cred, "api_key", SecretStr("UNDECRYPTABLE")
|
||||
)
|
||||
credentials.append(error_cred)
|
||||
except Exception as inner_e:
|
||||
logger.error(
|
||||
f"Failed to create error credential for {row.get('id', 'unknown')}: {inner_e}"
|
||||
)
|
||||
return credentials
|
||||
|
||||
async def get_linked_models(self) -> list:
|
||||
"""Get all models linked to this credential."""
|
||||
|
|
@ -159,6 +189,8 @@ class Credential(ObjectModel):
|
|||
"""Override to encrypt api_key before storage."""
|
||||
data = {}
|
||||
for key, value in self.model_dump().items():
|
||||
if key == "decryption_error":
|
||||
continue
|
||||
if key == "api_key":
|
||||
# Handle SecretStr: extract, encrypt, store
|
||||
if self.api_key:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue