Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions changelog/8213-remove-libpbac-env-var.yaml
Original file line number Diff line number Diff line change
@@ -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: []
37 changes: 25 additions & 12 deletions src/fides/service/pbac/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from __future__ import annotations

import ctypes
import importlib.util
import json
import os
import platform
Expand All @@ -44,36 +45,48 @@ 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
pkg_bin = Path(__file__).parent.parent.parent / "bin" / filename
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)
)


Expand All @@ -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.
Expand Down
46 changes: 14 additions & 32 deletions tests/fides/service/pbac/test_engine_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()
Loading