diff --git a/Dockerfile b/Dockerfile index 163c071541e..d8bb44b4f0c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -196,6 +196,10 @@ USER root RUN cd /fides && uv build --sdist && \ uv pip install --python /opt/fides/bin/python dist/ethyca_fides-*.tar.gz +# Copy libpbac to a path that survives volume mounts over /fides/ +RUN mkdir -p /opt/fides/lib && \ + cp /fides/src/fides/bin/libpbac.so /opt/fides/lib/libpbac.so + # Remove this directory to prevent issues with catch all RUN rm -r /fides/src/fides/ui-build USER fidesuser diff --git a/changelog/8213-remove-libpbac-env-var.yaml b/changelog/8213-remove-libpbac-env-var.yaml new file mode 100644 index 00000000000..9b5d2425872 --- /dev/null +++ b/changelog/8213-remove-libpbac-env-var.yaml @@ -0,0 +1,4 @@ +type: Developer Experience +description: Remove FIDES_PBAC_LIB_PATH env var; libpbac is now discovered automatically via importlib fallback +pr: 8213 +labels: [] diff --git a/src/fides/service/pbac/engine.py b/src/fides/service/pbac/engine.py index 4c68112b53a..bfd9393a92a 100644 --- a/src/fides/service/pbac/engine.py +++ b/src/fides/service/pbac/engine.py @@ -21,6 +21,7 @@ from __future__ import annotations import ctypes +import importlib.util import json import os import platform @@ -44,20 +45,14 @@ def _lib_filename() -> str: return "libpbac.so" -def _find_library() -> Path: +def find_library() -> Path: """Search for libpbac in standard locations. Order: - 1. FIDES_PBAC_LIB_PATH env var (explicit override) - 2. fides/bin/ inside the installed package (wheel distribution) + 1. fides/bin/ inside the installed package (wheel distribution) + 2. fides/bin/ via importlib (when __file__ is a hot-reload mount) 3. policy-engine/ build output (local dev) """ - env_path = os.environ.get("FIDES_PBAC_LIB_PATH") - if env_path: - p = Path(env_path) - if p.is_file(): - return p - filename = _lib_filename() # Wheel location: fides/bin/libpbac.so @@ -65,15 +60,33 @@ def _find_library() -> Path: if pkg_bin.is_file(): return pkg_bin + # Site-packages fallback — needed when __file__ is a volume mount + # (e.g. fidesplus dev containers hot-reloading fides source). + spec = importlib.util.find_spec("fides") + if spec and spec.origin: + site_bin = Path(spec.origin).parent / "bin" / filename + if site_bin.is_file() and site_bin != pkg_bin: + return site_bin + + # Docker fallback — the Dockerfile copies libpbac here so it + # survives volume mounts that shadow /fides/src/. + docker_bin = Path("/opt/fides/lib") / filename + if docker_bin.is_file(): + return docker_bin + # Local dev: policy-engine/ build output repo_root = Path(__file__).parent.parent.parent.parent.parent dev_path = repo_root / "policy-engine" / filename if dev_path.is_file(): return dev_path + searched = [str(pkg_bin)] + if spec and spec.origin: + searched.append(str(Path(spec.origin).parent / "bin" / filename)) + searched.append(str(dev_path)) raise RuntimeError( - f"Could not find {filename}. Set FIDES_PBAC_LIB_PATH or build with: " - f"cd policy-engine && go build -buildmode=c-shared -o {filename} ./cmd/libpbac/" + f"Could not find {filename}. Searched:\n" + + "\n".join(f" - {p}" for p in searched) ) @@ -90,7 +103,7 @@ def _get_lib() -> ctypes.CDLL: # pragma: no cover — requires built Go library if _lib is not None: return _lib # another thread won the race - path = _find_library() + path = find_library() lib = ctypes.cdll.LoadLibrary(str(path)) # All exported functions take a C string and return a C string. diff --git a/tests/fides/service/pbac/test_engine_unit.py b/tests/fides/service/pbac/test_engine_unit.py index 9270629e06c..69424201c5c 100644 --- a/tests/fides/service/pbac/test_engine_unit.py +++ b/tests/fides/service/pbac/test_engine_unit.py @@ -7,13 +7,12 @@ from __future__ import annotations -import os from pathlib import Path from unittest.mock import patch import pytest -from fides.service.pbac.engine import _find_library, _lib_filename +from fides.service.pbac.engine import _lib_filename, find_library class TestLibFilename: @@ -31,33 +30,16 @@ def test_windows(self): class TestFindLibrary: - def test_env_var_override(self, tmp_path): - """FIDES_PBAC_LIB_PATH pointing to a real file wins.""" - lib_file = tmp_path / "libpbac.so" - lib_file.write_text("fake") - with patch.dict(os.environ, {"FIDES_PBAC_LIB_PATH": str(lib_file)}): - assert _find_library() == lib_file - - def test_env_var_nonexistent_file_falls_through(self, tmp_path): - """FIDES_PBAC_LIB_PATH pointing to missing file is ignored.""" - with patch.dict(os.environ, {"FIDES_PBAC_LIB_PATH": str(tmp_path / "nope")}): - # Mock out the fallback paths so we get RuntimeError - with patch.object(Path, "is_file", return_value=False): - with pytest.raises(RuntimeError, match="Could not find"): - _find_library() - - def test_raises_when_no_library_found(self): - """RuntimeError when library isn't at any search location.""" - with patch.dict(os.environ, {}, clear=False): - os.environ.pop("FIDES_PBAC_LIB_PATH", None) - with patch.object(Path, "is_file", return_value=False): - with pytest.raises(RuntimeError, match="Could not find"): - _find_library() - - def test_error_message_includes_build_command(self): - """Error message tells users how to build the library.""" - with patch.dict(os.environ, {}, clear=False): - os.environ.pop("FIDES_PBAC_LIB_PATH", None) - with patch.object(Path, "is_file", return_value=False): - with pytest.raises(RuntimeError, match="go build -buildmode=c-shared"): - _find_library() + def test_finds_installed_binary(self): + """find_library locates a real libpbac binary.""" + result = find_library() + assert result.is_file() + assert result.name.startswith("libpbac") + + def test_raises_with_searched_paths_when_not_found(self, monkeypatch): + """RuntimeError lists searched paths when library isn't anywhere.""" + monkeypatch.setattr(Path, "is_file", lambda self: False) + monkeypatch.setattr("importlib.util.find_spec", lambda name: None) + + with pytest.raises(RuntimeError, match="Searched"): + find_library()