langflow icon indicating copy to clipboard operation
langflow copied to clipboard

feat: sso with keycloak as well as serveral bug fixes

Open patrykattc opened this issue 1 year ago • 40 comments

This pull request introduces several significant enhancements that improve the Langflow platform's security, monitoring, and deployment capabilities. The changes span multiple areas of the codebase, from authentication to Docker configurations and monitoring.

Key Improvements

1. SSO Integration with Keycloak

  • Added comprehensive Keycloak SSO integration for secure authentication
  • Created detailed SSO_INTEGRATION.md documentation explaining architecture, configuration and testing
  • Implemented token validation, role mapping, and seamless authentication flow
  • Added frontend support for handling OAuth2/OpenID Connect flow
  • Enhanced session management with proper logout and page refresh handling

2. Prometheus Monitoring Integration

  • Implemented a flexible metrics collection system using OpenTelemetry and Prometheus
  • Added support for two metrics deployment modes: inline and separate server
  • Created metrics for application performance and usage statistics
  • Added gauges for FastAPI and Langflow versions
  • Comprehensive PROMETHEUS.md documentation with configuration options and best practices
  • Docker Compose integration with Prometheus and Grafana services for visualization

3. Docker and Deployment Improvements

  • Enhanced docker/basic-test.sh with proper monitoring functionality, reliable cleanup, and better user interaction
  • Created .dockerignore file for the frontend directory to optimize Docker build context
  • Improved Docker documentation with detailed READMEs for both frontend and backend configurations
  • Created comprehensive Makefile documentation explaining build targets and workflows

4. Logging and Debugging Enhancements

  • Improved logging configuration with better formatting and organization
  • Enhanced error handling and reporting in critical code paths
  • Added more context to log messages for easier debugging

5. Bug Fixes

  • Various bug fixes

Technical Details

  • Authentication: Implemented Keycloak integration following OAuth 2.0 and OpenID Connect standards
  • Monitoring: Used OpenTelemetry for metrics collection and Prometheus for storage/visualization
  • Docker: Improved multi-stage builds, layer caching, and startup scripts
  • Documentation: Added comprehensive documentation for key features and integrations

Testing

  • All new features are covered by unit and integration tests
  • Manual testing performed for SSO integration with Keycloak
  • Docker builds and deployments tested in multiple environments
  • Metrics collection verified with Prometheus queries

Documentation

  • SSO_INTEGRATION.md - Complete guide to Keycloak integration
  • PROMETHEUS.md - Detailed documentation of Prometheus monitoring
  • MAKEFILE.md - Comprehensive explanation of Makefile targets
  • docker/README.md - Backend Docker configuration documentation
  • docker/frontend/README.md - Frontend Docker configuration guide

These improvements enhance Langflow's security posture, operational visibility, and deployment flexibility, making the platform more robust for production use cases.

patrykattc avatar Mar 29 '25 14:03 patrykattc

Hi! I'm autofix logoautofix.ci, a bot that automatically fixes trivial issues such as code formatting in pull requests.

I would like to apply some automated changes to this pull request, but it looks like I don't have the necessary permissions to do so. To get this pull request into a mergeable state, please do one of the following two things:

  1. Allow edits by maintainers for your pull request, and then re-trigger CI (for example by pushing a new commit).
  2. Manually fix the issues identified for your pull request (see the GitHub Actions output for details on what I would like to change).

autofix-ci[bot] avatar Mar 29 '25 14:03 autofix-ci[bot]

This looks so good @patrykattc

A PR of this size might take a long time (and quite a few people) to review. Do you think we could have a few different PRs or should we work on this one?

ogabrielluiz avatar Mar 31 '25 14:03 ogabrielluiz

Sorry for late response, ice storm, no power for few days. If you don't mind, this PR is just for the SSO, and it made sense in my head to push as is, as a whole. We have more business features to push and we thought we can have one PR for the SSO and multiple ones for the more functional requirements. So if possible, let's review this one. I'm always here to participate in the PR process, unless there is no power, haha.

patrykattc avatar Mar 31 '25 22:03 patrykattc

Hey @patrykattc

You might need to change this PR's permissions so we can apply changes to it.

I have a git patch with all the lint issues fixed:

diff --git a/src/backend/base/langflow/api/v1/login.py b/src/backend/base/langflow/api/v1/login.py
index 436a12f1a9..9b6b2b928f 100644
--- a/src/backend/base/langflow/api/v1/login.py
+++ b/src/backend/base/langflow/api/v1/login.py
@@ -232,7 +232,7 @@ async def logout_from_keycloak(request: Request, keycloak_service: KeycloakServi
         try:
             await keycloak_service.logout(refresh_token)
             logger.info("Successfully logged out from Keycloak")
-        except Exception as e:
+        except Exception as e:  # noqa: BLE001
             logger.error(f"Failed to log out from Keycloak: {e!s}")
 
 
diff --git a/src/backend/base/langflow/components/composio/composio_api.py b/src/backend/base/langflow/components/composio/composio_api.py
index 4222199926..de807f2272 100644
--- a/src/backend/base/langflow/components/composio/composio_api.py
+++ b/src/backend/base/langflow/components/composio/composio_api.py
@@ -10,6 +10,9 @@ from composio import Action, App
 from composio_langchain import ComposioToolSet
 from langchain_core.tools import Tool
 
+# Third-party imports
+from loguru import logger
+
 # Local imports
 from langflow.base.langchain_utilities.model import LCToolComponent
 from langflow.inputs import (
@@ -294,10 +297,10 @@ class ComposioAPIComponent(LCToolComponent):
             lockfile_path = tmp_dir / ".composio.lock"
 
             # Debug information
-            logger.info(f"Using tmp_dir: {tmp_dir}")
-            logger.info(f"Lockfile path: {lockfile_path}")
-            logger.info(f"Directory exists: {tmp_dir.exists()}")
-            logger.info(f"Directory is writable: {os.access(tmp_dir, os.W_OK)}")
+            logger.info("Using tmp_dir: %s", tmp_dir)
+            logger.info("Lockfile path: %s", lockfile_path)
+            logger.info("Directory exists: %s", tmp_dir.exists())
+            logger.info("Directory is writable: %s", os.access(tmp_dir, os.W_OK))
 
             # Pass the lockfile explicitly
             return ComposioToolSet(api_key=self.api_key, entity_id=self.entity_id, lockfile=lockfile_path)
diff --git a/src/backend/base/langflow/logging/logger.py b/src/backend/base/langflow/logging/logger.py
index 4b046142e5..1acd1166cf 100644
--- a/src/backend/base/langflow/logging/logger.py
+++ b/src/backend/base/langflow/logging/logger.py
@@ -9,7 +9,6 @@ from threading import Lock, Semaphore
 from typing import TypedDict
 
 import orjson
-from langflow.settings import DEV
 from loguru import _defaults, logger
 from loguru._error_interceptor import ErrorInterceptor
 from loguru._file_sink import FileSink
@@ -18,6 +17,8 @@ from platformdirs import user_cache_dir
 from rich.logging import RichHandler
 from typing_extensions import NotRequired, override
 
+from langflow.settings import DEV
+
 VALID_LOG_LEVELS = ["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
 # Human-readable
 DEFAULT_LOG_FORMAT = (
@@ -302,7 +303,7 @@ def configure_container_logging(log_level: str, log_format: str | None, env_mode
         )
 
 
-def configure_standard_logging(log_level: str, log_file: Path | None, log_format: str | None, async_file: bool) -> None:
+def configure_standard_logging(log_level: str, log_file: Path | None, log_format: str | None, async_file: bool) -> None:  # noqa: FBT001
     """Configure standard logging for non-container environments."""
     if os.getenv("LANGFLOW_LOG_FORMAT") and log_format is None:
         log_format = os.getenv("LANGFLOW_LOG_FORMAT")
diff --git a/src/backend/base/langflow/services/auth/constants.py b/src/backend/base/langflow/services/auth/constants.py
index 668cce30d1..c3028331a7 100644
--- a/src/backend/base/langflow/services/auth/constants.py
+++ b/src/backend/base/langflow/services/auth/constants.py
@@ -5,7 +5,7 @@ such as cookie names and default expiration times.
 """
 
 # Cookie Names
-COOKIE_REFRESH_TOKEN = "refresh_token_lf"
-COOKIE_ACCESS_TOKEN = "access_token_lf"
+COOKIE_REFRESH_TOKEN = "refresh_token_lf"  # noqa: S105
+COOKIE_ACCESS_TOKEN = "access_token_lf"  # noqa: S105
 COOKIE_API_KEY = "apikey_tkn_lflw"
-COOKIE_KEYCLOAK_REFRESH_TOKEN = "keycloak_refresh_token"
+COOKIE_KEYCLOAK_REFRESH_TOKEN = "keycloak_refresh_token"  # noqa: S105
diff --git a/src/backend/base/langflow/services/database/models/user/crud.py b/src/backend/base/langflow/services/database/models/user/crud.py
index 4861b06c01..3a71b19dd3 100644
--- a/src/backend/base/langflow/services/database/models/user/crud.py
+++ b/src/backend/base/langflow/services/database/models/user/crud.py
@@ -11,7 +11,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
 from langflow.services.database.models.user.model import User, UserUpdate
 
 
-async def get_user_by_username(db: AsyncSession, username: str, include_deleted: bool = False) -> User | None:
+async def get_user_by_username(db: AsyncSession, username: str, include_deleted: bool = False) -> User | None:  # noqa: FBT001, FBT002
     """Get a user by username.
 
     Args:
@@ -25,11 +25,11 @@ async def get_user_by_username(db: AsyncSession, username: str, include_deleted:
     if include_deleted:
         stmt = select(User).where(User.username == username)
     else:
-        stmt = select(User).where(User.username == username, User.is_deleted == False)
+        stmt = select(User).where(User.username == username, not User.is_deleted)
     return (await db.exec(stmt)).first()
 
 
-async def get_user_by_id(db: AsyncSession, user_id: UUID, include_deleted: bool = False) -> User | None:
+async def get_user_by_id(db: AsyncSession, user_id: UUID, include_deleted: bool = False) -> User | None:  # noqa: FBT001, FBT002
     """Get a user by ID.
 
     Args:
@@ -46,7 +46,7 @@ async def get_user_by_id(db: AsyncSession, user_id: UUID, include_deleted: bool
     if include_deleted:
         stmt = select(User).where(User.id == user_id)
     else:
-        stmt = select(User).where(User.id == user_id, User.is_deleted == False)
+        stmt = select(User).where(User.id == user_id, not User.is_deleted)
     return (await db.exec(stmt)).first()
 
 
diff --git a/src/backend/base/langflow/services/keycloak/service.py b/src/backend/base/langflow/services/keycloak/service.py
index 95f1787bc2..d00e3cb2f3 100644
--- a/src/backend/base/langflow/services/keycloak/service.py
+++ b/src/backend/base/langflow/services/keycloak/service.py
@@ -80,7 +80,7 @@ class KeycloakService(Service):
             logger.warning(client_id_error_msg)
             raise ValueError(client_id_error_msg)
 
-        client_secret_error_msg = "Keycloak client secret is not configured"
+        client_secret_error_msg = "Keycloak client secret is not configured"  # noqa: S105
         if not hasattr(auth_settings, "KEYCLOAK_CLIENT_SECRET") or not auth_settings.KEYCLOAK_CLIENT_SECRET:
             logger.warning(client_secret_error_msg)
             raise ValueError(client_secret_error_msg)
diff --git a/src/backend/base/langflow/services/keycloak/utils.py b/src/backend/base/langflow/services/keycloak/utils.py
index 368d608859..50b2861df0 100644
--- a/src/backend/base/langflow/services/keycloak/utils.py
+++ b/src/backend/base/langflow/services/keycloak/utils.py
@@ -192,7 +192,10 @@ async def get_or_create_user(db: AsyncSession, decoded_token: dict, keycloak_ser
 
 
 async def create_keycloak_user(
-    db: AsyncSession, username: str, email: str | None = None, is_admin: bool = False
+    db: AsyncSession,
+    username: str,
+    email: str | None = None,
+    is_admin: bool = False,  # noqa: FBT001, FBT002
 ) -> User:
     """Create a new user in the database from Keycloak information.
 
diff --git a/src/backend/base/langflow/services/telemetry/service.py b/src/backend/base/langflow/services/telemetry/service.py
index 57c13e596c..6fc373ce13 100644
--- a/src/backend/base/langflow/services/telemetry/service.py
+++ b/src/backend/base/langflow/services/telemetry/service.py
@@ -153,7 +153,7 @@ class TelemetryService(Service):
             self._start_time = datetime.now(timezone.utc)
             self.worker_task = asyncio.create_task(self.telemetry_worker())
             self.log_package_version_task = asyncio.create_task(self.log_package_version())
-        except Exception as e:
+        except Exception as e:  # noqa: BLE001
             logger.exception(f"Error starting TelemetryService telemetry: {e}")
             self.running = False  # Reset running state on failure
 
@@ -203,7 +203,7 @@ class TelemetryService(Service):
             logger.warning("Metrics server flag was set but thread is dead. Proceeding to restart it.")
 
         port = self.settings_service.settings.prometheus_port
-        host = os.getenv("LANGFLOW_METRICS_HOST", "0.0.0.0")
+        host = os.getenv("LANGFLOW_METRICS_HOST", "0.0.0.0")  # noqa: S104
         log_level = os.getenv("LANGFLOW_METRICS_LOG_LEVEL", "warning")
 
         metrics_app = make_asgi_app()
@@ -220,7 +220,7 @@ class TelemetryService(Service):
                     access_log=False,
                     timeout_keep_alive=5,
                 )
-            except Exception as e:
+            except Exception as e:  # noqa: BLE001
                 logger.exception(f"Failed to start Prometheus metrics server on {host}:{port}: {e}")
 
         self._metrics_thread = threading.Thread(
diff --git a/src/backend/tests/unit/services/keycloak/test_service.py b/src/backend/tests/unit/services/keycloak/test_service.py
index 374711f727..d10fc5c991 100644
--- a/src/backend/tests/unit/services/keycloak/test_service.py
+++ b/src/backend/tests/unit/services/keycloak/test_service.py
@@ -8,7 +8,7 @@ from types import SimpleNamespace
 from unittest.mock import Mock
 
 import pytest
-from jose import jwt
+from jose import JWTError
 from keycloak import KeycloakOpenID
 from langflow.services.keycloak.service import KeycloakService
 from pytest_mock import MockerFixture
@@ -481,10 +481,10 @@ async def test_decode_invalid_token(fixture_keycloak_service: KeycloakService, m
     fixture_keycloak_service._keycloak_openid = mocker.Mock()
 
     # Configure jwt.decode to raise a JWTError
-    mocker.patch("jose.jwt.decode", side_effect=jwt.JWTError("Invalid token"))
+    mocker.patch("jose.jwt.decode", side_effect=JWTError("Invalid token"))
 
     # Decoding an invalid token should raise an exception
-    with pytest.raises(Exception):
+    with pytest.raises(JWTError):
         await fixture_keycloak_service.decode_token("invalid_token")
 
 
diff --git a/src/backend/tests/unit/services/keycloak/test_utils.py b/src/backend/tests/unit/services/keycloak/test_utils.py
index 731266c787..c1d1b9af1c 100644
--- a/src/backend/tests/unit/services/keycloak/test_utils.py
+++ b/src/backend/tests/unit/services/keycloak/test_utils.py
@@ -419,7 +419,7 @@ async def test_get_or_create_user_new_user(
         id=2,
         username="new_user",
         email="[email protected]",
-        password="hashed_password",
+        password="hashed_password",  # noqa: S106
         is_superuser=True,
         is_active=True,
         is_keycloak_user=True,
@@ -438,7 +438,7 @@ async def test_get_or_create_user_new_user(
         )
 
         # Verify new user was created
-        mock_create_user.assert_called_once_with(fixture_mock_db_session, "new_user", "[email protected]", True)
+        mock_create_user.assert_called_once_with(fixture_mock_db_session, "new_user", "[email protected]", True)  # noqa: FBT003
 
         # Verify result
         assert result == new_user
@@ -485,7 +485,7 @@ async def test_get_or_create_user_with_generated_username(
         id=3,
         username="test.user",
         email=None,
-        password="hashed_password",
+        password="hashed_password",  # noqa: S106
         is_superuser=False,
         is_active=True,
         is_keycloak_user=True,
@@ -526,7 +526,7 @@ async def test_get_or_create_user_with_random_username(
         id=4,
         username="user_12345678",  # This would be random in reality
         email=None,
-        password="hashed_password",
+        password="hashed_password",  # noqa: S106
         is_superuser=False,
         is_active=True,
         is_keycloak_user=True,
@@ -568,7 +568,7 @@ async def test_create_keycloak_user(fixture_mock_db_session: AsyncMock):
         id=5,
         username="keycloak_user",
         email="[email protected]",
-        password="hashed_password",
+        password="hashed_password",  # noqa: S106
         is_superuser=True,
         is_active=True,
         is_keycloak_user=True,
@@ -626,7 +626,7 @@ async def test_create_new_user(fixture_mock_db_session: AsyncMock):
     user = User(
         username="new_db_user",
         email="[email protected]",
-        password="hashed_password",
+        password="hashed_password",  # noqa: S106
         is_superuser=False,
         is_active=True,
         is_keycloak_user=True,
diff --git a/src/backend/tests/unit/test_logger.py b/src/backend/tests/unit/test_logger.py
index a2407e1b34..5f2970b3de 100644
--- a/src/backend/tests/unit/test_logger.py
+++ b/src/backend/tests/unit/test_logger.py
@@ -161,7 +161,7 @@ def test_configure_disables_logger(monkeypatch: pytest.MonkeyPatch) -> None:
     """Test that calling configure with disable=True disables the logger."""
     disable_called = False
 
-    def fake_disable(name: str) -> None:
+    def fake_disable(name: str) -> None:  # noqa: ARG001
         nonlocal disable_called
         disable_called = True
 
@@ -174,7 +174,7 @@ def test_configure_container_logging(monkeypatch: pytest.MonkeyPatch) -> None:
     """Test that container logging configuration sets up logger.add correctly when LANGFLOW_LOG_FORMAT is not set."""
     add_calls: list[dict[str, Any]] = []
 
-    def fake_add(sink: object, **kwargs: Any) -> None:
+    def fake_add(sink: object, **kwargs: Any) -> None:  # noqa: ARG001
         add_calls.append(kwargs)
 
     monkeypatch.setattr(loguru_logger, "add", fake_add)
@@ -196,13 +196,13 @@ def test_configure_standard_logging(monkeypatch: pytest.MonkeyPatch) -> None:
     monkeypatch.setattr(loguru_logger, "configure", fake_configure)
     add_calls: list[dict[str, Any]] = []
 
-    def fake_add(sink: object, **kwargs: Any) -> int:
+    def fake_add(sink: object, **kwargs: Any) -> int:  # noqa: ARG001
         add_calls.append(kwargs)
         return 1
 
     monkeypatch.setattr(loguru_logger, "add", fake_add)
     monkeypatch.delenv("LANGFLOW_LOG_FORMAT", raising=False)
-    temp_log_file: Path = Path("/tmp/test_langflow.log")
+    temp_log_file: Path = Path("/tmp/test_langflow.log")  # noqa: S108
     configure_standard_logging("INFO", temp_log_file, None, async_file=False)
     assert "handlers" in captured_config
     assert len(add_calls) >= 1
@@ -215,13 +215,13 @@ def test_configure_calls_standard(monkeypatch: pytest.MonkeyPatch) -> None:
     add_calls: list[dict[str, Any]] = []
     config_calls: list[Any] = []
     monkeypatch.setattr(loguru_logger, "remove", lambda: None)
-    monkeypatch.setattr(loguru_logger, "add", lambda *args, **kwargs: add_calls.append(kwargs))
+    monkeypatch.setattr(loguru_logger, "add", lambda *args, **kwargs: add_calls.append(kwargs))  # noqa: ARG005
     monkeypatch.setattr(loguru_logger, "configure", lambda handlers: config_calls.append(handlers))
     with (
         patch("langflow.logging.logger.setup_uvicorn_logger", lambda: None),
         patch("langflow.logging.logger.setup_gunicorn_logger", lambda: None),
     ):
-        temp_log_file: Path = Path("/tmp/test_configure.log")
+        temp_log_file: Path = Path("/tmp/test_configure.log")  # noqa: S108
         configure(
             log_level="WARNING",
             log_file=temp_log_file,
@@ -238,7 +238,7 @@ def test_configure_with_env_log_level(monkeypatch: pytest.MonkeyPatch) -> None:
     """Test that configure uses the LANGFLOW_LOG_LEVEL environment variable when log_level is not provided."""
     monkeypatch.setenv("LANGFLOW_LOG_LEVEL", "INFO")
     add_calls: list[dict[str, Any]] = []
-    monkeypatch.setattr(loguru_logger, "add", lambda *args, **kwargs: add_calls.append(kwargs))
+    monkeypatch.setattr(loguru_logger, "add", lambda *args, **kwargs: add_calls.append(kwargs))  # noqa: ARG005
     configure(disable=False)
     levels = [call.get("level", "") for call in add_calls if "level" in call]
     assert any(level == "INFO" for level in levels)
@@ -248,8 +248,8 @@ def test_configure_standard_logging_invalid_format(monkeypatch: pytest.MonkeyPat
     """Test that configure_standard_logging falls back to DEFAULT_LOG_FORMAT if an invalid log format is provided."""
     monkeypatch.delenv("LANGFLOW_LOG_FORMAT", raising=False)
     add_calls: list[dict[str, Any]] = []
-    monkeypatch.setattr(loguru_logger, "add", lambda *args, **kwargs: add_calls.append(kwargs))
-    temp_log_file: Path = Path("/tmp/test_invalid_format.log")
+    monkeypatch.setattr(loguru_logger, "add", lambda *args, **kwargs: add_calls.append(kwargs))  # noqa: ARG005
+    temp_log_file: Path = Path("/tmp/test_invalid_format.log")  # noqa: S108
     configure_standard_logging("INFO", temp_log_file, "invalid_format", async_file=False)
     for call in add_calls:
         assert call.get("format") == DEFAULT_LOG_FORMAT
@@ -277,7 +277,7 @@ def test_is_valid_log_format_invalid() -> None:
 @pytest.mark.asyncio
 async def test_async_file_sink_write() -> None:
     """Test the asynchronous writing of log messages using AsyncFileSink."""
-    temp_log_file: Path = Path("/tmp/test_async_sink.log")
+    temp_log_file: Path = Path("/tmp/test_async_sink.log")  # noqa: S108
     sink: AsyncFileSink = AsyncFileSink(temp_log_file)
     test_message: str = "Test async log message"
     await sink.write_async(test_message)
@@ -285,7 +285,7 @@ async def test_async_file_sink_write() -> None:
     assert True
 
 
-def test_intercept_handler_emit(capfd: pytest.LogCaptureFixture) -> None:
+def test_intercept_handler_emit(capfd: pytest.LogCaptureFixture) -> None:  # noqa: ARG001
     """Test that the InterceptHandler correctly processes and logs a record."""
     handler: InterceptHandler = InterceptHandler()
     record: logging.LogRecord = logging.LogRecord(
@@ -299,7 +299,7 @@ def test_intercept_handler_emit(capfd: pytest.LogCaptureFixture) -> None:
     )
     try:
         handler.emit(record)
-    except Exception as exc:
+    except Exception as exc:  # noqa: BLE001
         pytest.fail(f"InterceptHandler.emit raised an exception: {exc}")
     assert True
 
@@ -310,7 +310,7 @@ def test_intercept_handler_emit(capfd: pytest.LogCaptureFixture) -> None:
 
 
 @pytest.mark.parametrize(
-    "message,expected",
+    "message,expected",  # noqa: PT006
     [
         ("GET /health", False),
         ("POST /health_check", False),
@@ -328,7 +328,7 @@ def test_log_filter_excludes_known_paths(message, expected):
 def test_serialize_log_valid() -> None:
     """Test that serialize_log returns a valid JSON string representation of a given log record."""
     # Create a mock Loguru-style record that matches your function's expectations
-    mock_time = datetime.datetime.now()
+    mock_time = datetime.datetime.now(tz=datetime.timezone.utc)
 
     # Use SimpleNamespace objects for nested attributes that need to be accessed with dot notation
     level_obj = SimpleNamespace(name="DEBUG")
@@ -369,7 +369,7 @@ def test_serialize_log_valid() -> None:
 
 def create_mock_record(message="Test message", level="INFO", exception=None) -> dict:
     """Helper function to create a mock Loguru record."""
-    mock_time = datetime.datetime.now()
+    mock_time = datetime.datetime.now(tz=datetime.timezone.utc)
     level_obj = SimpleNamespace(name=level)
     process_obj = SimpleNamespace(id=1234)
     thread_obj = SimpleNamespace(id=5678, name="test_thread")
@@ -391,7 +391,7 @@ def test_serialize_log_basic():
     """Test basic serialization with minimal fields."""
     mock_record = create_mock_record()
 
-    with patch("langflow.logging.logger.DEV", False):
+    with patch("langflow.logging.logger.DEV", value=False):
         result = serialize_log(mock_record)
 
     parsed = json.loads(result)
@@ -405,13 +405,14 @@ def test_serialize_log_basic():
 def test_serialize_log_with_exception_dev_mode():
     """Test serialization with exception info in development mode."""
     try:
-        raise ValueError("Test exception")
+        msg = "Test exception"
+        raise ValueError(msg)
     except ValueError as e:
         exception_info = SimpleNamespace(type=type(e), value=e)
 
     mock_record = create_mock_record(exception=exception_info)
 
-    with patch("langflow.logging.logger.DEV", True):
+    with patch("langflow.logging.logger.DEV", value=True):
         result = serialize_log(mock_record)
 
     parsed = json.loads(result)
@@ -422,13 +423,14 @@ def test_serialize_log_with_exception_dev_mode():
 def test_serialize_log_with_exception_prod_mode():
     """Test that exceptions are null in production mode even when present."""
     try:
-        raise ValueError("Test exception")
+        msg = "Test exception"
+        raise ValueError(msg)
     except ValueError as e:
         exception_info = SimpleNamespace(type=type(e), value=e)
 
     mock_record = create_mock_record(exception=exception_info)
 
-    with patch("langflow.logging.logger.DEV", False):
+    with patch("langflow.logging.logger.DEV", value=False):
         result = serialize_log(mock_record)
 
     parsed = json.loads(result)
@@ -440,7 +442,7 @@ def test_serialize_log_different_levels():
     for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
         mock_record = create_mock_record(level=level)
 
-        with patch("langflow.logging.logger.DEV", False):
+        with patch("langflow.logging.logger.DEV", value=False):
             result = serialize_log(mock_record)
 
         parsed = json.loads(result)
@@ -449,11 +451,11 @@ def test_serialize_log_different_levels():
 
 def test_serialize_log_timestamp_format():
     """Test that timestamp is correctly formatted."""
-    fixed_time = datetime.datetime(2023, 1, 15, 12, 30, 45, 123456)
+    fixed_time = datetime.datetime(2023, 1, 15, 12, 30, 45, 123456, tzinfo=datetime.timezone.utc)
     mock_record = create_mock_record()
     mock_record["time"] = fixed_time
 
-    with patch("langflow.logging.logger.DEV", False):
+    with patch("langflow.logging.logger.DEV", value=False):
         result = serialize_log(mock_record)
 
     parsed = json.loads(result)
@@ -472,7 +474,7 @@ def test_serialize_log_with_non_standard_exception():
     exception_info = CustomException()
     mock_record = create_mock_record(exception=exception_info)
 
-    with patch("langflow.logging.logger.DEV", True):
+    with patch("langflow.logging.logger.DEV", value=True):
         result = serialize_log(mock_record)
 
     parsed = json.loads(result)
@@ -486,7 +488,7 @@ def test_serialize_log_with_unicode_message():
     unicode_message = "测试消息 - こんにちは - مرحبا"
     mock_record = create_mock_record(message=unicode_message)
 
-    with patch("langflow.logging.logger.DEV", False):
+    with patch("langflow.logging.logger.DEV", value=False):
         result = serialize_log(mock_record)
 
     parsed = json.loads(result)
@@ -498,7 +500,7 @@ def test_serialize_log_with_very_long_message():
     long_message = "A" * 10000
     mock_record = create_mock_record(message=long_message)
 
-    with patch("langflow.logging.logger.DEV", False):
+    with patch("langflow.logging.logger.DEV", value=False):
         result = serialize_log(mock_record)
 
     parsed = json.loads(result)
diff --git a/src/backend/tests/unit/test_telemetry.py b/src/backend/tests/unit/test_telemetry.py
index f12ee5ac2e..70999a7042 100644
--- a/src/backend/tests/unit/test_telemetry.py
+++ b/src/backend/tests/unit/test_telemetry.py
@@ -214,7 +214,7 @@ def test_langflow_version_gauge_with_up_down_counter_method(fixture_opentelemetr
 class MockSettings:
     """Mock settings used to simulate Langflow's Settings object for telemetry testing."""
 
-    def __init__(self, prometheus_port: int = 9001, prometheus_enabled: bool = True):
+    def __init__(self, prometheus_port: int = 9001, *, prometheus_enabled: bool = True):
         self.prometheus_port = prometheus_port
         self.prometheus_enabled = prometheus_enabled
         self.telemetry_base_url = "http://localhost"
@@ -295,7 +295,7 @@ def test_metrics_server_respects_env(monkeypatch):
 
     captured_args = {}
 
-    def mock_run(app, **kwargs):
+    def mock_run(app, **kwargs):  # noqa: ARG001
         captured_args.update(kwargs)
 
     monkeypatch.setattr("uvicorn.run", mock_run)

ogabrielluiz avatar Apr 02 '25 11:04 ogabrielluiz

Hi, @ogabrielluiz. I'm back at work. We finally got power back here in my part of Canada, so the drama is over. I don't see the "Allow edits from maintainers" checkbox. I suspect that is because the fork was in our TC Org GitHub and the Org policies there. I've added you to the forked repo as a member. I think that should work. If you happen to know what Org policy to change on our site, I'll quickly update it.

patrykattc avatar Apr 02 '25 21:04 patrykattc

Allowing edits from orgs repos seems to be an unresolved issue: https://github.com/orgs/community/discussions/5634 How do you guys manage this, or has this never come up since usually forks come personal accounts?

patrykattc avatar Apr 03 '25 10:04 patrykattc

I've transferred the repo form out of Org to my personal accounts. Now the "Allow edits and access to secrets by maintainers" checkbox is there, and it was checked. I've told our guys to use personal accounts for the next PRs.

patrykattc avatar Apr 03 '25 10:04 patrykattc

Hey guys, we made the linter gods happy, but I think we might need approvals to move this along and address the other failing checks, @ogabrielluiz @mneedham

patrykattc avatar Apr 04 '25 19:04 patrykattc

Hey @patrykattc

I setup the CI to run so you can check what's broken more easily.

Again, if you can open the PR for maintainers we can help with most of it.

ogabrielluiz avatar Apr 08 '25 12:04 ogabrielluiz

Hi @ogabrielluiz, Thanks for your help.

The check box for "Allow edits and access to secrets by maintainers" has been checked, I'm assuming it is opened for maintainers.

We are also available to push for any required fixes. We are already running this code in production and have a lot more code to push. The first one is always the most difficult.

patrykattc avatar Apr 08 '25 12:04 patrykattc

Starting my look at this today

jordanrfrazier avatar Apr 09 '25 20:04 jordanrfrazier

Seems to be great! Waiting for its integration.

flefevre avatar Apr 16 '25 12:04 flefevre

CodSpeed Performance Report

Merging #7346 will degrade performances by 36.45%

Comparing patrykattc:feature-sso (922bbd8) with main (cf16595)

Summary

⚡ 15 improvements
❌ 1 regressions
✅ 3 untouched benchmarks

:warning: Please fix the performance issues or acknowledge them on CodSpeed.

Benchmarks breakdown

Benchmark BASE HEAD Change
test_build_flow 247.9 ms 123.5 ms ×2
test_build_flow_from_request_data 257.4 ms 139.1 ms +85.07%
test_build_flow_invalid_job_id 9.5 ms 8.2 ms +16.7%
test_build_flow_polling 257.5 ms 136.7 ms +88.3%
test_build_flow_start_only 244.1 ms 126.7 ms +92.68%
test_build_flow_start_with_inputs 236.6 ms 125.2 ms +89.05%
test_cancel_build_failure 258.5 ms 142.9 ms +80.85%
test_cancel_build_success 243.5 ms 122.6 ms +98.54%
test_cancel_build_unexpected_error 771.5 ms 614.3 ms +25.59%
test_cancel_build_with_cancelled_error 257.5 ms 141.3 ms +82.25%
test_cancel_nonexistent_build 8.9 ms 14 ms -36.45%
test_starter_projects 1.4 s 1.3 s +10.76%
test_successful_run_with_input_type_any 181.1 ms 111.9 ms +61.87%
test_successful_run_with_input_type_text 171.5 ms 112.1 ms +53.07%
test_successful_run_with_output_type_any 162.4 ms 109.4 ms +48.49%
test_successful_run_with_output_type_debug 171 ms 111.5 ms +53.41%

codspeed-hq[bot] avatar Apr 19 '25 17:04 codspeed-hq[bot]

We noticed some of the GitHub unit test jobs were failing because of resource issues. Screenshot 2025-04-19 at 12 32 42 PM

Looking at one random test, we can see an issue with the inspect.getsource in the component.py.

We reworked that function to work without the inspect.getsource, and we can see major performance improvements. Screenshot 2025-04-19 at 1 05 01 PM

The tests are passing after this update.

patrykattc avatar Apr 19 '25 18:04 patrykattc

I hope it will merged soon, we are waiting for this

barnuri avatar Apr 23 '25 09:04 barnuri

Will other implementations of SSO be support besides Keycloak?

frostronic avatar May 16 '25 15:05 frostronic

Will other implementations of SSO be support besides Keycloak?

What others do you require? Can't Keycloak provide support for the ones you need?

ogabrielluiz avatar May 16 '25 16:05 ogabrielluiz

Will other implementations of SSO be support besides Keycloak?

What others do you require? Can't Keycloak provide support for the ones you need?

If by "Keycloak" you mean "standardized SSO/OIDC/Oauth/SAML support" then yes it probably would. However, as you may know Keycloak is just one of many popular SSO platform, but there are many popular platforms in use such as Authentik, Authelia, Okta, etc.

frostronic avatar May 16 '25 17:05 frostronic

I see. Maybe it should be SAML directly instead, right? @patrykattc any reasons you picked Keycloak out of the gate?

ogabrielluiz avatar May 16 '25 20:05 ogabrielluiz

I see. Maybe it should be SAML directly instead, right? @patrykattc any reasons you picked Keycloak out of the gate?

@ogabrielluiz @frostronic We are a full-stack development shop, covering everything from AI to Cloud to DevOps to backend to frontend. We provide complete solutions. Ex we build Varcel for clients. Part of our solution is SSO with keycloak. We deployed everything in Kubernetes and provided all the tools for the business application to be production-ready. I understand that many businesses utilize services like Varcel and other services, such as Clerk, for SSO. However, we would like to provide a comprehensive solution. The way we added this SSO feature means it can be extended to other providers with minimal effort. Additionally, this SSO PR is not just SSO; we also provided production-ready Dockerfiles, updated Prometheus observability, and addressed multiple bug fixes. We also have a brand-new, production-ready Helm chart that we are ready to share. We also have many additional features that are waiting for this merge. One of the features is the ability to provide user groups that allow for sharing global variables between users. This means instead of having an API key per user, you have an API key per group of users. We also added an agent-to-agent protocol. We are also iterating fast with additional features. We move fast. (We might also share Terraform/Terragrunt code to deploy the helm charts with databases to build your complete system)

patrykattc avatar May 21 '25 10:05 patrykattc

To help with the why, here are a few images. Keycloak is also an identity broker. Screenshot 2025-05-21 at 6 51 41 AM

We use Google login for dev. Screenshot 2025-05-21 at 6 51 46 AM

And we use many others in production, including different social media platforms and SAML. Screenshot 2025-05-21 at 6 51 51 AM

patrykattc avatar May 21 '25 10:05 patrykattc

Since I see the tests running, here is a sneak peek at the group features that we use to manage global variables. We also updated the frontend project to use React Query 5, and you can see the React Query dev tool there at the bottom, which we also added to the project. Screenshot 2025-05-21 at 7 00 04 AM

patrykattc avatar May 21 '25 11:05 patrykattc

Hi, I didn't see any infor concerning MCP authentification. Will there be some integration with MCP server ie. auth and tool calling using the caller's token ? Thank you for all your work :D

Baptiste-ms avatar May 22 '25 11:05 Baptiste-ms

@Baptiste-ms, this PR is about integrating SSO with Langflow authentication. The SSO auth creates the same JWT token as the basic login form in Langflow. We have another PR waiting for this one to merge, which addresses additional authentication with the MCP server. Ex. We have an MCP for executing Kubernetes commands, and we only want DevOps users to have specific access to the kubectl commands. Additionally, we have two distinct DevOps roles: administrators and regular users. We control which commands DevOps can execute through the MCP using authentication, and we have different authentication methods based on the role as well. So once this is merged, we will push a lot more code for auth that is related to MCP. The basic mechanism is that we enhanced the LangFlow JWT token.

patrykattc avatar May 22 '25 12:05 patrykattc

Ok got it! Thank you for the clarification. Other changes will cascade after this one, good news :hugs:

Baptiste-ms avatar May 22 '25 13:05 Baptiste-ms

Hey there, Do you know how effort is due to integrate this feature? Do you plan to integrate it for a specific release? We are in 1.4.2

Thanks for your involvement

flefevre avatar Jun 01 '25 21:06 flefevre

I was just wondering if there have been any updates

ncecere avatar Jun 12 '25 16:06 ncecere

Hey @ncecere

This will be merged soon. We are focused on fixing some bugs but we will look at this very soon.

By the way, do you all have a guide on how to integrate this implementation with Okta?

cc: @patrykattc @flefevre

ogabrielluiz avatar Jun 12 '25 16:06 ogabrielluiz

Hey @ncecere

This will be merged soon. We are focused on fixing some bugs but we will look at this very soon.

By the way, do you all have a guide on how to integrate this implementation with Okta?

cc: @patrykattc @flefevre

Hey @ncecere

This will be merged soon. We are focused on fixing some bugs but we will look at this very soon.

By the way, do you all have a guide on how to integrate this implementation with Okta?

cc: @patrykattc @flefevre

Just wanted to see if there have been any updates or movement on this

ncecere avatar Jun 26 '25 17:06 ncecere

If I do understand, The primary goal of this PR is to release the new authentication feature using Keycloak with OAuth2. That said, this implementation could also be a good foundation for improving OAuth-based authentication in MCP (Model Context Protocol) servers, following the official spec here: https://modelcontextprotocol.io/specification/draft/basic/authorization Definitely something worth considering for future enhancements with the capacity of langflow to generate already Mcp server from flows on demand !

Feature: Support OAuth2 (Keycloak) Authentication for Generated MCP Servers #8883

flefevre avatar Jul 05 '25 00:07 flefevre