diff --git a/sdk/python/feast/offline_server.py b/sdk/python/feast/offline_server.py index e130544445f..e82b5239767 100644 --- a/sdk/python/feast/offline_server.py +++ b/sdk/python/feast/offline_server.py @@ -7,37 +7,6 @@ from datetime import datetime from typing import Any, Dict, List, Optional, cast -import click -import pyarrow as pa -import pyarrow.flight as fl -from google.protobuf.json_format import Parse - -from feast import FeatureStore, FeatureView, utils -from feast.arrow_error_handler import arrow_server_error_handling_decorator -from feast.data_source import DataSource -from feast.errors import FeatureViewNotFoundException -from feast.feature_logging import FeatureServiceLoggingSource -from feast.feature_view import DUMMY_ENTITY_NAME -from feast.infra.offline_stores.offline_utils import get_offline_store_from_config -from feast.permissions.action import AuthzedAction -from feast.permissions.security_manager import assert_permissions -from feast.permissions.server.arrow import ( - AuthorizationMiddlewareFactory, - inject_user_details_decorator, -) -from feast.permissions.server.utils import ( - AuthManagerType, - ServerType, - init_auth_manager, - init_security_manager, - str_to_auth_manager_type, -) -from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto -from feast.saved_dataset import SavedDatasetStorage - -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - _FIPS_CIPHER_SUITES = ":".join( [ "ECDHE-RSA-AES128-GCM-SHA256", @@ -55,14 +24,60 @@ def _is_fips_enabled() -> bool: with open("/proc/sys/crypto/fips_enabled") as f: return f.read().strip() == "1" except (FileNotFoundError, PermissionError, OSError): - logger.debug("Could not detect FIPS mode (Linux-only feature)") return False -def _configure_grpc_fips() -> None: +def _configure_grpc_fips() -> bool: if _is_fips_enabled() and "GRPC_SSL_CIPHER_SUITES" not in os.environ: os.environ["GRPC_SSL_CIPHER_SUITES"] = _FIPS_CIPHER_SUITES - logger.info("FIPS mode detected, configured FIPS-compliant gRPC cipher suites.") + return True + return False + + +# On FIPS-enabled systems (notably IBM Power ppc64le), gRPC reads +# GRPC_SSL_CIPHER_SUITES during shared-library initialization. The env var +# must be set before any gRPC-linked module (pyarrow.flight) is imported. +_fips_configured = _configure_grpc_fips() + +import click # noqa: E402 +import pyarrow as pa # noqa: E402 +import pyarrow.flight as fl # noqa: E402 +from google.protobuf.json_format import Parse # noqa: E402 + +from feast import FeatureStore, FeatureView, utils # noqa: E402 +from feast.arrow_error_handler import ( # noqa: E402 + arrow_server_error_handling_decorator, +) +from feast.data_source import DataSource # noqa: E402 +from feast.errors import FeatureViewNotFoundException # noqa: E402 +from feast.feature_logging import FeatureServiceLoggingSource # noqa: E402 +from feast.feature_view import DUMMY_ENTITY_NAME # noqa: E402 +from feast.infra.offline_stores.offline_utils import ( # noqa: E402 + get_offline_store_from_config, +) +from feast.permissions.action import AuthzedAction # noqa: E402 +from feast.permissions.security_manager import assert_permissions # noqa: E402 +from feast.permissions.server.arrow import ( # noqa: E402 + AuthorizationMiddlewareFactory, + inject_user_details_decorator, +) +from feast.permissions.server.utils import ( # noqa: E402 + AuthManagerType, + ServerType, + init_auth_manager, + init_security_manager, + str_to_auth_manager_type, +) +from feast.protos.feast.core.DataSource_pb2 import ( # noqa: E402 + DataSource as DataSourceProto, +) +from feast.saved_dataset import SavedDatasetStorage # noqa: E402 + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +if _fips_configured: + logger.info("FIPS mode detected, configured FIPS-compliant gRPC cipher suites.") class OfflineServer(fl.FlightServerBase): diff --git a/sdk/python/tests/unit/test_offline_server.py b/sdk/python/tests/unit/test_offline_server.py index 6ff93d02282..fa1bf85e97e 100644 --- a/sdk/python/tests/unit/test_offline_server.py +++ b/sdk/python/tests/unit/test_offline_server.py @@ -1,4 +1,7 @@ import os +import subprocess +import sys +import textwrap from unittest.mock import MagicMock, mock_open, patch import assertpy @@ -137,3 +140,55 @@ def test_configure_grpc_fips_noop_without_fips(): os.environ.pop("GRPC_SSL_CIPHER_SUITES", None) _configure_grpc_fips() assert "GRPC_SSL_CIPHER_SUITES" not in os.environ + + +def test_module_level_fips_sets_env_before_pyarrow_import(): + """GRPC_SSL_CIPHER_SUITES must be set at module load time, + before pyarrow.flight (which bundles gRPC) is imported. + + Uses a subprocess so pyarrow.flight is not already cached in + sys.modules, which lets us verify the true import ordering. + """ + script = textwrap.dedent("""\ + import io, os, sys + + # Intercept only /proc/sys/crypto/fips_enabled to simulate FIPS + _real_open = open + def _fips_open(file, *args, **kwargs): + if str(file) == "/proc/sys/crypto/fips_enabled": + return io.StringIO("1\\n") + return _real_open(file, *args, **kwargs) + + import builtins + builtins.open = _fips_open + + # Track import order to verify env var is set before pyarrow.flight + original_import = builtins.__import__ + def tracking_import(name, *args, **kwargs): + if name == "pyarrow.flight": + assert "GRPC_SSL_CIPHER_SUITES" in os.environ, ( + "GRPC_SSL_CIPHER_SUITES not set before pyarrow.flight import" + ) + return original_import(name, *args, **kwargs) + + builtins.__import__ = tracking_import + try: + import feast.offline_server + assert "GRPC_SSL_CIPHER_SUITES" in os.environ + assert "AES128-GCM-SHA256" in os.environ["GRPC_SSL_CIPHER_SUITES"] + finally: + builtins.__import__ = original_import + builtins.open = _real_open + """) + env = os.environ.copy() + env.pop("GRPC_SSL_CIPHER_SUITES", None) + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + env=env, + timeout=60, + ) + assert result.returncode == 0, ( + f"Subprocess failed:\nstdout: {result.stdout}\nstderr: {result.stderr}" + )