mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-25 15:17:26 +08:00
Merge branch 'release-1.10.0' into chore/deployments-tech-debt-review
This commit is contained in:
@ -1701,16 +1701,6 @@
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"src/backend/base/langflow/api/utils/core.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "src/backend/base/langflow/api/utils/core.py",
|
||||
"hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4",
|
||||
"is_verified": false,
|
||||
"line_number": 413,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json": [
|
||||
{
|
||||
"type": "Hex High Entropy String",
|
||||
@ -8373,5 +8363,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2026-04-15T15:25:36Z"
|
||||
"generated_at": "2026-04-21T23:47:54Z"
|
||||
}
|
||||
|
||||
@ -398,19 +398,44 @@ def custom_params(
|
||||
return Params(page=page or MIN_PAGE_SIZE, size=size or MAX_PAGE_SIZE)
|
||||
|
||||
|
||||
def extract_global_variables_from_headers(headers) -> dict[str, str]:
|
||||
"""Extract global variables from HTTP headers with prefix X-LANGFLOW-GLOBAL-VAR-*.
|
||||
# Well-known authentication headers that can be propagated to nested MCP calls
|
||||
# when ``include_auth_headers=True`` is passed. These are stored under their
|
||||
# lowercase header names so that nested server configs can reference them
|
||||
# directly, e.g. ``{"x-api-key": "x-api-key"}`` in the MCP server headers config.
|
||||
_AUTH_HEADERS_TO_PROPAGATE = frozenset({"x-api-key", "authorization"})
|
||||
|
||||
|
||||
def extract_global_variables_from_headers(headers, *, include_auth_headers: bool = False) -> dict[str, str]:
|
||||
"""Extract global variables from HTTP headers.
|
||||
|
||||
By default, only headers with the ``X-LANGFLOW-GLOBAL-VAR-*`` prefix are
|
||||
extracted. When ``include_auth_headers=True``, the well-known authentication
|
||||
headers ``x-api-key`` and ``authorization`` are additionally captured under
|
||||
their lowercase names so that nested MCP server configs can reference them
|
||||
directly (e.g. ``{"x-api-key": "x-api-key"}``).
|
||||
|
||||
SECURITY NOTE: Only pass ``include_auth_headers=True`` from MCP call sites
|
||||
(see ``api/v1/mcp_projects.py``). On non-MCP routes such as ``/run`` and
|
||||
``/workflow``, ``x-api-key`` is Langflow's own authentication key — exposing
|
||||
it in ``request_variables`` would make it readable by any component that
|
||||
reads the graph context.
|
||||
|
||||
Args:
|
||||
headers: HTTP headers object (e.g., from FastAPI Request.headers)
|
||||
headers: HTTP headers object (e.g., from FastAPI Request.headers).
|
||||
include_auth_headers: When True, also extract well-known authentication
|
||||
headers (``x-api-key``, ``authorization``) under their lowercase
|
||||
names. Should only be set by MCP request handlers that need to
|
||||
propagate these values to nested MCP calls.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping variable names (uppercase) to their values
|
||||
Dictionary mapping variable names to their values.
|
||||
|
||||
Example:
|
||||
headers = {"X-LANGFLOW-GLOBAL-VAR-API-KEY": "secret", "Content-Type": "application/json"}
|
||||
result = extract_global_variables_from_headers(headers)
|
||||
# Returns: {"API_KEY": "secret"}
|
||||
headers = {"X-LANGFLOW-GLOBAL-VAR-API-KEY": "secret", "x-api-key": "mykey"}
|
||||
extract_global_variables_from_headers(headers)
|
||||
# Returns: {"API-KEY": "secret"}
|
||||
extract_global_variables_from_headers(headers, include_auth_headers=True)
|
||||
# Returns: {"API-KEY": "secret", "x-api-key": "mykey"}
|
||||
"""
|
||||
variables: dict[str, str] = {}
|
||||
|
||||
@ -420,6 +445,8 @@ def extract_global_variables_from_headers(headers) -> dict[str, str]:
|
||||
if header_lower.startswith(LANGFLOW_GLOBAL_VAR_HEADER_PREFIX):
|
||||
var_name = header_lower[len(LANGFLOW_GLOBAL_VAR_HEADER_PREFIX) :].upper()
|
||||
variables[var_name] = header_value
|
||||
elif include_auth_headers and header_lower in _AUTH_HEADERS_TO_PROPAGATE:
|
||||
variables[header_lower] = header_value
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# Log the error but don't raise - we want to continue execution
|
||||
logger.exception("Failed to extract global variables from headers: %s", exc)
|
||||
|
||||
@ -345,7 +345,7 @@ async def handle_project_sse(
|
||||
|
||||
user_token = current_user_ctx.set(current_user)
|
||||
project_token = current_project_ctx.set(project_id)
|
||||
variables = extract_global_variables_from_headers(request.headers)
|
||||
variables = extract_global_variables_from_headers(request.headers, include_auth_headers=True)
|
||||
req_vars_token = current_request_variables_ctx.set(variables or None)
|
||||
|
||||
try:
|
||||
@ -386,7 +386,7 @@ async def _handle_project_sse_messages(
|
||||
"""Handle POST messages for a project-specific MCP server using SSE transport."""
|
||||
user_token = current_user_ctx.set(current_user)
|
||||
project_token = current_project_ctx.set(project_id)
|
||||
variables = extract_global_variables_from_headers(request.headers)
|
||||
variables = extract_global_variables_from_headers(request.headers, include_auth_headers=True)
|
||||
req_vars_token = current_request_variables_ctx.set(variables or None)
|
||||
|
||||
try:
|
||||
@ -443,7 +443,7 @@ async def _dispatch_project_streamable_http(
|
||||
|
||||
user_token = current_user_ctx.set(current_user)
|
||||
project_token = current_project_ctx.set(project_id)
|
||||
variables = extract_global_variables_from_headers(request.headers)
|
||||
variables = extract_global_variables_from_headers(request.headers, include_auth_headers=True)
|
||||
request_vars_token = current_request_variables_ctx.set(variables or None)
|
||||
|
||||
try:
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from langflow.api.utils.core import extract_global_variables_from_headers
|
||||
from langflow.api.v1 import mcp_utils
|
||||
from lfx.interface.components import component_cache
|
||||
|
||||
@ -122,3 +123,80 @@ async def test_handle_list_tools_skips_blocked_custom_flows(monkeypatch):
|
||||
tools = await mcp_utils.handle_list_tools()
|
||||
|
||||
assert tools == []
|
||||
|
||||
|
||||
class TestExtractGlobalVariablesFromHeaders:
|
||||
"""Unit tests for ``extract_global_variables_from_headers``.
|
||||
|
||||
Covers the MCP auth-header propagation fix (issue #12529): ``x-api-key``
|
||||
and ``authorization`` should be captured under their lowercase names when
|
||||
(and only when) ``include_auth_headers=True`` is passed. The default
|
||||
behavior must remain backwards-compatible for non-MCP routes, where
|
||||
``x-api-key`` is Langflow's own auth key and must not leak into the graph
|
||||
context.
|
||||
"""
|
||||
|
||||
def test_langflow_global_var_prefix_still_extracted(self):
|
||||
"""Regression guard: ``X-LANGFLOW-GLOBAL-VAR-*`` extraction is preserved."""
|
||||
headers = {
|
||||
"X-LANGFLOW-GLOBAL-VAR-API-KEY": "secret-value",
|
||||
"X-LANGFLOW-GLOBAL-VAR-DB-URL": "postgres://host/db",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
result = extract_global_variables_from_headers(headers)
|
||||
|
||||
assert result == {"API-KEY": "secret-value", "DB-URL": "postgres://host/db"}
|
||||
|
||||
def test_auth_headers_not_extracted_by_default(self):
|
||||
"""Non-MCP call sites: ``x-api-key`` / ``authorization`` must not leak through."""
|
||||
headers = {
|
||||
"x-api-key": "langflow-auth-key",
|
||||
"authorization": "Bearer token",
|
||||
"X-LANGFLOW-GLOBAL-VAR-MY-VAR": "value",
|
||||
}
|
||||
|
||||
result = extract_global_variables_from_headers(headers)
|
||||
|
||||
assert "x-api-key" not in result
|
||||
assert "authorization" not in result
|
||||
assert result == {"MY-VAR": "value"}
|
||||
|
||||
def test_auth_headers_extracted_under_lowercase_when_opted_in(self):
|
||||
"""MCP call sites: lowercase auth headers are captured when opted in."""
|
||||
headers = {
|
||||
"x-api-key": "api-key-value",
|
||||
"authorization": "Bearer jwt-token",
|
||||
}
|
||||
|
||||
result = extract_global_variables_from_headers(headers, include_auth_headers=True)
|
||||
|
||||
assert result == {"x-api-key": "api-key-value", "authorization": "Bearer jwt-token"}
|
||||
|
||||
def test_auth_header_matching_is_case_insensitive(self):
|
||||
"""Headers with mixed or uppercase casing still match (e.g. ``X-Api-Key``, ``AUTHORIZATION``)."""
|
||||
headers = {
|
||||
"X-Api-Key": "mixed-case-value",
|
||||
"AUTHORIZATION": "Bearer UPPER",
|
||||
}
|
||||
|
||||
result = extract_global_variables_from_headers(headers, include_auth_headers=True)
|
||||
|
||||
assert result == {"x-api-key": "mixed-case-value", "authorization": "Bearer UPPER"}
|
||||
|
||||
def test_both_categories_extracted_together(self):
|
||||
"""``X-LANGFLOW-GLOBAL-VAR-*`` and auth headers coexist when opted in."""
|
||||
headers = {
|
||||
"X-LANGFLOW-GLOBAL-VAR-API-KEY": "global-secret",
|
||||
"x-api-key": "incoming-mcp-key",
|
||||
"Authorization": "Bearer mcp-token",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
result = extract_global_variables_from_headers(headers, include_auth_headers=True)
|
||||
|
||||
assert result == {
|
||||
"API-KEY": "global-secret",
|
||||
"x-api-key": "incoming-mcp-key",
|
||||
"authorization": "Bearer mcp-token",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user