fix: validate api key in global settings upon migration (#11280)

* validate api key upon migration

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* resolve conflicts

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* handle validation in models and variables

* update starter projects

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* fix mypy errors

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* fix test

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* validate upon initialization

* [autofix.ci] apply automated fixes

* fix enabled providers test

* fix tests in enabled_providers

* [autofix.ci] apply automated fixes

* fix ruff errors

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Himavarsha
2026-01-23 09:37:28 -05:00
committed by GitHub
parent a1ddb06301
commit ce42e3198b
8 changed files with 324 additions and 41 deletions

View File

@ -197,7 +197,11 @@ async def get_enabled_providers(
current_user: CurrentActiveUser,
providers: Annotated[list[str] | None, Query()] = None,
):
"""Get enabled providers for the current user."""
"""Get enabled providers for the current user.
Only providers with valid API keys are marked as enabled. This prevents
providers from appearing enabled when they have invalid credentials.
"""
variable_service = get_variable_service()
try:
if not isinstance(variable_service, DatabaseVariableService):
@ -205,14 +209,14 @@ async def get_enabled_providers(
status_code=500,
detail="Variable service is not an instance of DatabaseVariableService",
)
# Get all credential variables for the user
# Get all variables to check which credential variables exist
all_variables = await variable_service.get_all(user_id=current_user.id, session=session)
# Get all credential variable names (regardless of default_fields)
# This includes both env variables and explicitly created model provider credentials
credential_names = {var.name for var in all_variables if var.type == CREDENTIAL_TYPE}
credential_variable_names = {var.name for var in all_variables if var.type == CREDENTIAL_TYPE}
if not credential_names:
if not credential_variable_names:
return {
"enabled_providers": [],
"provider_status": {},
@ -221,14 +225,39 @@ async def get_enabled_providers(
# Get the provider-variable mapping
provider_variable_map = get_model_provider_variable_mapping()
enabled_providers = []
provider_status = {}
# Build credential_variables dict with objects that have encrypted values
# VariableRead sets value=None for CREDENTIAL_TYPE (via validator), but _validate_and_get_enabled_providers
# needs the encrypted value to decrypt and validate. So we create simple objects with the encrypted value.
credential_variables = {}
for provider, var_name in provider_variable_map.items():
is_enabled = var_name in credential_names
provider_status[provider] = is_enabled
if is_enabled:
enabled_providers.append(provider)
for var_name in credential_variable_names:
if var_name and var_name in provider_variable_map.values():
try:
# Get the raw Variable object to access the encrypted value
variable_obj = await variable_service.get_variable_object(
user_id=current_user.id, name=var_name, session=session
)
if variable_obj and variable_obj.value:
# Create a simple object with the encrypted value
# _validate_and_get_enabled_providers only needs .value attribute
class VarWithValue:
def __init__(self, value):
self.value = value
credential_variables[var_name] = VarWithValue(variable_obj.value)
except (ValueError, Exception) as e: # noqa: BLE001
# Variable not found or error accessing it - skip
logger.debug("Skipping variable %s due to error: %s", var_name, e)
continue
# Use shared helper to validate and get enabled providers
from lfx.base.models.unified_models import _validate_and_get_enabled_providers
enabled_providers_set = _validate_and_get_enabled_providers(credential_variables, provider_variable_map)
enabled_providers = list(enabled_providers_set)
# Build provider_status dict for all providers
provider_status = {provider: provider in enabled_providers_set for provider in provider_variable_map}
result = {
"enabled_providers": enabled_providers,

View File

@ -14,6 +14,7 @@ from langflow.api.v1.models import (
get_model_names_for_provider,
get_provider_from_variable_name,
)
from langflow.services.auth import utils as auth_utils
from langflow.services.database.models.variable.model import VariableCreate, VariableRead, VariableUpdate
from langflow.services.deps import get_variable_service
from langflow.services.variable.constants import CREDENTIAL_TYPE, GENERIC_TYPE
@ -148,19 +149,141 @@ async def read_variables(
session: DbSession,
current_user: CurrentActiveUser,
):
"""Read all variables."""
"""Read all variables.
Model provider credentials are validated when reading from the database.
If a provider key is invalid, its default_fields are cleared to prevent
the provider from appearing enabled.
Each variable in the response includes:
- is_valid: bool | None - True if valid, False if invalid, None if not a provider credential
- validation_error: str | None - Error message if validation failed
Returns a list of variables with validation status for model provider credentials.
"""
variable_service = get_variable_service()
if not isinstance(variable_service, DatabaseVariableService):
msg = "Variable service is not an instance of DatabaseVariableService"
raise TypeError(msg)
try:
all_variables = await variable_service.get_all(user_id=current_user.id, session=session)
# Filter out internal variables (those starting and ending with __)
return [
filtered_variables = [
var for var in all_variables if not (var.name and var.name.startswith("__") and var.name.endswith("__"))
]
# Validate model provider credentials and clear default_fields if invalid
# Build dict of credential variables for validation
credential_variables = {var.name: var for var in filtered_variables if var.type == CREDENTIAL_TYPE}
provider_variable_map = get_model_provider_variable_mapping()
# Create reverse mapping: variable_name -> provider
var_to_provider = {var_name: provider for provider, var_name in provider_variable_map.items()}
# Validate each provider credential once and capture both enabled status and error messages
validation_results: dict[
str, tuple[bool, str | None, list[str] | None]
] = {} # var_name -> (is_valid, error, default_fields)
for var_name in provider_variable_map.values():
if var_name in credential_variables:
is_valid = False
error_message = None
variable_obj = None
try:
# Get the raw Variable object to access the encrypted value
variable_obj = await variable_service.get_variable_object(
user_id=current_user.id, name=var_name, session=session
)
if variable_obj and variable_obj.value:
# Decrypt the API key value
from langflow.services.deps import get_settings_service
settings_service = get_settings_service()
decrypted_value = auth_utils.decrypt_api_key(
variable_obj.value, settings_service=settings_service
)
if decrypted_value and decrypted_value.strip():
# Validate the key (this will raise ValueError if invalid)
await asyncio.to_thread(validate_model_provider_key, var_name, decrypted_value)
# Validation passed
is_valid = True
error_message = None
else:
error_message = "API key is empty"
else:
error_message = "Variable value is empty"
except ValueError as e:
# Validation failed - get the error message
error_message = str(e)
except Exception as e: # noqa: BLE001
error_message = f"Validation error: {e!s}"
# Update default_fields based on validation result
updated_default_fields = None
if variable_obj and variable_obj.id:
try:
if is_valid:
# Key is valid - ensure default_fields are set (important for migration)
provider_name = var_to_provider.get(var_name)
expected_default_fields = [provider_name, "api_key"] if provider_name else []
if variable_obj.default_fields != expected_default_fields:
await variable_service.update_variable_fields(
user_id=current_user.id,
variable_id=variable_obj.id,
variable=VariableUpdate(
id=variable_obj.id,
default_fields=expected_default_fields,
),
session=session,
)
updated_default_fields = expected_default_fields
else:
# Key is invalid - clear default_fields
if variable_obj.default_fields:
await variable_service.update_variable_fields(
user_id=current_user.id,
variable_id=variable_obj.id,
variable=VariableUpdate(
id=variable_obj.id,
default_fields=[],
),
session=session,
)
updated_default_fields = []
except Exception: # noqa: BLE001
# Log but don't fail if we can't update
# Use current default_fields if update failed
updated_default_fields = variable_obj.default_fields if variable_obj else None
validation_results[var_name] = (is_valid, error_message, updated_default_fields)
# Set validation status on each variable and update default_fields in response
for var in filtered_variables:
if var.name and var.name in model_provider_variable_mapping.values() and var.type == CREDENTIAL_TYPE:
result = validation_results.get(var.name)
if result:
is_valid, error_message, updated_default_fields = result
var.is_valid = is_valid
var.validation_error = error_message
# Update default_fields in response to reflect what we set in database
# This is important for migration - valid keys will have default_fields set
if updated_default_fields is not None:
var.default_fields = updated_default_fields
else:
# Variable not found in validation results
var.is_valid = False
var.validation_error = "Variable not found"
else:
# Not a model provider credential, validation fields remain None
var.is_valid = None
var.validation_error = None
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
else:
return filtered_variables
@router.patch("/{variable_id}", response_model=VariableRead, status_code=200)

View File

@ -55,6 +55,12 @@ class VariableRead(SQLModel):
type: str | None = Field(None, description="Type of the variable")
value: str | None = Field(None, description="Encrypted value of the variable")
default_fields: list[str] | None = Field(None, description="Default fields for the variable")
validation_error: str | None = Field(
None, description="Validation error message if this is a model provider credential with an invalid key"
)
is_valid: bool | None = Field(
None, description="Whether this model provider credential has a valid key (None if not a provider credential)"
)
@field_validator("value")
@classmethod

View File

@ -68,7 +68,22 @@ class DatabaseVariableService(VariableService, Service):
try:
if is_api_key_variable:
provider_name = var_to_provider[var_name]
default_fields = [provider_name, "api_key"]
# Validate the API key before setting default_fields
# This prevents invalid keys from enabling providers during migration
try:
from lfx.base.models.unified_models import validate_model_provider_key
validate_model_provider_key(var_name, value)
# Only set default_fields if validation passes
default_fields = [provider_name, "api_key"]
await logger.adebug(f"Validated {var_name} - provider will be enabled")
except (ValueError, Exception) as validation_error: # noqa: BLE001
# Validation failed - don't set default_fields
# This prevents the provider from appearing as "Enabled"
default_fields = []
await logger.adebug(
f"Skipping default_fields for {var_name} - validation failed: {validation_error!s}"
)
existing = (await session.exec(query)).first()
except Exception as e: # noqa: BLE001
await logger.aexception(f"Error querying {var_name} variable: {e!s}")

View File

@ -98,7 +98,10 @@ async def test_enabled_providers_after_credential_creation(client: AsyncClient,
assert create_response.status_code == status.HTTP_201_CREATED
# Check status after credential creation
after_response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
# Mock validation for enabled_providers endpoint as well
with mock.patch("lfx.base.models.unified_models.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
after_response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
after_result = after_response.json()
assert after_response.status_code == status.HTTP_200_OK
@ -137,8 +140,10 @@ async def test_enabled_providers_multiple_credentials(
await client.post("api/v1/variables/", json=anthropic_var, headers=logged_in_headers)
await client.post("api/v1/variables/", json=google_var, headers=logged_in_headers)
# Check enabled providers
response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
# Check enabled providers - mock validation for enabled_providers endpoint
with mock.patch("lfx.base.models.unified_models.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@ -170,8 +175,10 @@ async def test_enabled_providers_after_credential_deletion(client: AsyncClient,
created_credential = create_response.json()
credential_id = created_credential["id"]
# Verify enabled
enabled_response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
# Verify enabled - mock validation for enabled_providers endpoint as well
with mock.patch("lfx.base.models.unified_models.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
enabled_response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
enabled_result = enabled_response.json()
assert "OpenAI" in enabled_result["enabled_providers"]
assert enabled_result["provider_status"]["OpenAI"] is True
@ -213,10 +220,12 @@ async def test_enabled_providers_filter_by_specific_providers(
await client.post("api/v1/variables/", json=openai_var, headers=logged_in_headers)
await client.post("api/v1/variables/", json=anthropic_var, headers=logged_in_headers)
# Request specific providers (only providers that are in the mapping)
response = await client.get(
"api/v1/models/enabled_providers?providers=OpenAI&providers=Anthropic", headers=logged_in_headers
)
# Request specific providers (only providers that are in the mapping) - mock validation
with mock.patch("lfx.base.models.unified_models.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
response = await client.get(
"api/v1/models/enabled_providers?providers=OpenAI&providers=Anthropic", headers=logged_in_headers
)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@ -333,13 +342,16 @@ async def test_enabled_providers_reflects_models_endpoint(client: AsyncClient, o
mock_validate.return_value = None
await client.post("api/v1/variables/", json=variable_payload, headers=logged_in_headers)
# Get enabled providers
enabled_response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
enabled_result = enabled_response.json()
# Get enabled providers and models - mock validation in unified_models so providers are marked enabled
with mock.patch("lfx.base.models.unified_models.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
# Get models (which should include provider information)
models_response = await client.get("api/v1/models", headers=logged_in_headers)
models_result = models_response.json()
enabled_response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
enabled_result = enabled_response.json()
# Get models (which should include provider information)
models_response = await client.get("api/v1/models", headers=logged_in_headers)
models_result = models_response.json()
assert models_response.status_code == status.HTTP_200_OK

View File

@ -4,11 +4,12 @@ import type {
SelectionChangedEvent,
ValueFormatterParams,
} from "ag-grid-community";
import { useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import Dropdown from "@/components/core/dropdownComponent";
import GlobalVariableModal from "@/components/core/GlobalVariableModal/GlobalVariableModal";
import TableComponent from "@/components/core/parameterRenderComponent/components/tableComponent";
import { PROVIDER_VARIABLE_MAPPING } from "@/constants/providerConstants";
import {
useDeleteGlobalVariables,
useGetGlobalVariables,
@ -88,6 +89,59 @@ export default function GlobalVariablesPage() {
const { data: globalVariables } = useGetGlobalVariables();
const { mutate: mutateDeleteGlobalVariable } = useDeleteGlobalVariables();
// Get list of provider variable names to identify provider credentials
const providerVariableNames = useMemo(
() => new Set(Object.values(PROVIDER_VARIABLE_MAPPING)),
[],
);
// Filter out invalid provider credentials
const validGlobalVariables = useMemo(() => {
if (!globalVariables) return [];
return globalVariables.filter((variable) => {
// Check if this is a provider credential variable
const isProviderCredential =
variable.type === "Credential" &&
providerVariableNames.has(variable.name);
if (isProviderCredential) {
// If validation failed (is_valid === false), filter it out
if (variable.is_valid === false) {
return false; // Filter out invalid provider credentials
}
}
return true; // Keep all other variables and valid provider credentials
});
}, [globalVariables, providerVariableNames]);
// Show validation errors for invalid provider credentials (only once when detected)
useEffect(() => {
if (!globalVariables) return;
const invalidProviderVars = globalVariables.filter(
(variable) =>
variable.type === "Credential" &&
providerVariableNames.has(variable.name) &&
variable.is_valid === false,
);
if (invalidProviderVars.length > 0) {
const errorMessages = invalidProviderVars.map(
(variable) =>
`${variable.name}: ${variable.validation_error || "Invalid API key"}`,
);
setErrorData({
title: "Invalid Provider Credentials Detected",
list: [
`${invalidProviderVars.length} provider credential(s) with invalid keys have been hidden from the list.`,
...errorMessages,
],
});
}
}, [globalVariables, providerVariableNames, setErrorData]);
async function removeVariables() {
selectedRows.map(async (row) => {
const id = globalVariables?.find((variable) => variable.name === row)?.id;
@ -150,7 +204,7 @@ export default function GlobalVariablesPage() {
suppressRowClickSelection={true}
pagination={true}
columnDefs={colDefs}
rowData={globalVariables ?? []}
rowData={validGlobalVariables ?? []}
onDelete={removeVariables}
/>
{initialData.current && (

View File

@ -7,4 +7,6 @@ export type GlobalVariable = {
name: string;
value?: string;
category?: string;
is_valid?: boolean | null;
validation_error?: string | null;
};

View File

@ -275,6 +275,46 @@ def get_api_key_for_provider(user_id: UUID | str | None, provider: str, api_key:
return run_until_complete(_get_variable())
def _validate_and_get_enabled_providers(
credential_variables: dict[str, Any],
provider_variable_map: dict[str, str],
) -> set[str]:
"""Validate API keys and return set of enabled providers.
This helper function validates API keys for credential variables and returns
only providers with valid keys. Used by get_enabled_providers and model options functions.
Args:
credential_variables: Dictionary mapping variable names to VariableRead objects
provider_variable_map: Dictionary mapping provider names to variable names
Returns:
Set of provider names that have valid API keys
"""
from langflow.services.auth import utils as auth_utils
from langflow.services.deps import get_settings_service
settings_service = get_settings_service()
enabled = set()
for provider, var_name in provider_variable_map.items():
if var_name in credential_variables:
# Validate the API key before marking as enabled
credential_var = credential_variables[var_name]
try:
# Decrypt the API key value
api_key = auth_utils.decrypt_api_key(credential_var.value, settings_service=settings_service)
# Validate the key (this will raise ValueError if invalid)
if api_key and api_key.strip():
validate_model_provider_key(var_name, api_key)
enabled.add(provider)
except (ValueError, Exception) as e: # noqa: BLE001
# Key validation failed or decryption failed - don't enable provider
logger.debug("Provider %s validation failed for variable %s: %s", provider, var_name, e)
return enabled
def validate_model_provider_key(variable_name: str, api_key: str) -> None:
"""Validate a model provider API key by making a minimal test call.
@ -425,7 +465,7 @@ def get_language_model_options(
# If we can't get model status, continue without filtering
pass
# Get enabled providers (those with credentials configured)
# Get enabled providers (those with credentials configured and validated)
enabled_providers = set()
if user_id:
try:
@ -435,6 +475,7 @@ def get_language_model_options(
variable_service = get_variable_service()
if variable_service is None:
return set()
from langflow.services.variable.constants import CREDENTIAL_TYPE
from langflow.services.variable.service import DatabaseVariableService
@ -444,11 +485,11 @@ def get_language_model_options(
user_id=UUID(user_id) if isinstance(user_id, str) else user_id,
session=session,
)
credential_names = {var.name for var in all_vars if var.type == CREDENTIAL_TYPE}
credential_vars = {var.name: var for var in all_vars if var.type == CREDENTIAL_TYPE}
provider_variable_map = get_model_provider_variable_mapping()
return {
provider for provider, var_name in provider_variable_map.items() if var_name in credential_names
}
# Use shared helper to validate and get enabled providers
return _validate_and_get_enabled_providers(credential_vars, provider_variable_map)
enabled_providers = run_until_complete(_get_enabled_providers())
except Exception: # noqa: BLE001, S110
@ -611,7 +652,7 @@ def get_embedding_model_options(user_id: UUID | str | None = None) -> list[dict[
# If we can't get model status, continue without filtering
pass
# Get enabled providers (those with credentials configured)
# Get enabled providers (those with credentials configured and validated)
enabled_providers = set()
if user_id:
try:
@ -621,6 +662,7 @@ def get_embedding_model_options(user_id: UUID | str | None = None) -> list[dict[
variable_service = get_variable_service()
if variable_service is None:
return set()
from langflow.services.variable.constants import CREDENTIAL_TYPE
from langflow.services.variable.service import DatabaseVariableService
@ -630,11 +672,11 @@ def get_embedding_model_options(user_id: UUID | str | None = None) -> list[dict[
user_id=UUID(user_id) if isinstance(user_id, str) else user_id,
session=session,
)
credential_names = {var.name for var in all_vars if var.type == CREDENTIAL_TYPE}
credential_vars = {var.name: var for var in all_vars if var.type == CREDENTIAL_TYPE}
provider_variable_map = get_model_provider_variable_mapping()
return {
provider for provider, var_name in provider_variable_map.items() if var_name in credential_names
}
# Use shared helper to validate and get enabled providers
return _validate_and_get_enabled_providers(credential_vars, provider_variable_map)
enabled_providers = run_until_complete(_get_enabled_providers())
except Exception: # noqa: BLE001, S110