fix: Raise on an undecryptable secret at the variable-resolution (#13681)

fix: Raise on an undecryptable secret at the variable-resolution boundary

Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local>
This commit is contained in:
Janardan Singh Kavia
2026-06-18 13:22:50 -04:00
committed by GitHub
parent b033bfa22b
commit 2b8f7f90a0
3 changed files with 1935 additions and 844 deletions

File diff suppressed because it is too large Load Diff

View File

@ -219,7 +219,15 @@ class DatabaseVariableService(VariableService, Service):
if variable.type == CREDENTIAL_TYPE:
from pydantic import SecretStr
return SecretStr(auth_utils.decrypt_api_key(variable.value))
decrypted = auth_utils.decrypt_api_key(variable.value)
if not decrypted:
msg = (
f"Could not decrypt credential variable '{name}'. The stored value cannot be "
"decrypted with the current LANGFLOW_SECRET_KEY — it may have been encrypted "
"with a different key."
)
raise ValueError(msg)
return SecretStr(decrypted)
# GENERIC type - return as-is
return variable.value

View File

@ -1,8 +1,11 @@
import secrets
from datetime import datetime
from unittest.mock import patch
from uuid import uuid4
import pytest
from cryptography.fernet import Fernet
from langflow.services.auth.utils import ensure_fernet_key
from langflow.services.database.models.variable.model import VariableUpdate
from langflow.services.deps import get_settings_service
from langflow.services.variable.constants import CREDENTIAL_TYPE, GENERIC_TYPE
@ -103,6 +106,42 @@ async def test_get_variable__typeerror(service, session: AsyncSession):
assert "purpose is to prevent the exposure of value" in str(exc.value)
async def test_get_variable__credential_decrypt_failure(service, session: AsyncSession):
"""Store credential under SECRET_KEY=A, resolve with SECRET_KEY=B → raises naming the variable.
Uses ensure_fernet_key (the real SECRET_KEY→Fernet derivation path) so the test exercises
the exact mismatch scenario described in the ticket: a key rotation or missing persisted key.
The test auth service (lfx stub) is a passthrough, so we patch at the auth_utils boundary.
"""
secret_key_a = secrets.token_urlsafe(32)
secret_key_b = secrets.token_urlsafe(32)
fernet_a = Fernet(ensure_fernet_key(secret_key_a))
fernet_b = Fernet(ensure_fernet_key(secret_key_b))
user_id = uuid4()
name = "MY_CRED"
# Phase 1: store credential encrypted under SECRET_KEY = secret_key_a.
with patch(
"langflow.services.variable.service.auth_utils.encrypt_api_key",
side_effect=lambda v: fernet_a.encrypt(v.encode()).decode(),
):
await service.create_variable(user_id, name, "secret123", type_=CREDENTIAL_TYPE, session=session)
# Phase 2: resolve with SECRET_KEY = secret_key_b (mismatched) — Fernet raises InvalidToken → "".
def decrypt_with_wrong_key(ciphertext: str) -> str:
try:
return fernet_b.decrypt(ciphertext.encode()).decode()
except Exception:
return ""
with (
patch("langflow.services.variable.service.auth_utils.decrypt_api_key", side_effect=decrypt_with_wrong_key),
pytest.raises(ValueError, match=r"MY_CRED.*SECRET_KEY"),
):
await service.get_variable(user_id, name, "", session=session)
async def test_list_variables(service, session: AsyncSession):
user_id = uuid4()
names = ["name1", "name2", "name3"]