Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 0 additions & 8 deletions .github/actions/build-policy-wasm/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,6 @@ description: >
runs:
using: composite
steps:
- name: Install OPA
shell: bash
env:
OPA_VERSION: v1.8.0
run: |
curl -fsSL -o /usr/local/bin/opa \
"https://github.com/open-policy-agent/opa/releases/download/${OPA_VERSION}/opa_linux_amd64_static"
chmod +x /usr/local/bin/opa
- name: Build policy WASM
shell: bash
run: ./script/build_policy_wasm.sh
7 changes: 7 additions & 0 deletions docs/auth/authorization/policy-engine.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,23 @@ The embedded PDP uses a WASM-compiled Rego policy engine built into the auth ser

### How It Works

- Rego policy files are compiled to `policy.wasm` with `make build-policy`
Comment thread
mckornfield marked this conversation as resolved.
Outdated
- Policy data (role bindings, workspace visibility, endpoint permissions) is served by the auth service
- Data is refreshed on a configurable interval (default: 30 seconds)

In source checkouts, startup with `auth.enabled: true` and
`policy_decision_point_provider: embedded` automatically builds a missing or
stale `policy.wasm` using the pinned OPA version from `script/build_policy_wasm.sh`.
Packaged wheels and container images should include `policy.wasm` at build time.

### Configuration

```yaml
auth:
enabled: true
policy_decision_point_provider: "embedded"
policy_decision_point_base_url: "http://auth:8000"
embedded_pdp_auto_build_wasm: false
policy_data_refresh_interval: 30 # seconds
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

Expand Down
23 changes: 23 additions & 0 deletions docs/auth/deployment/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ NMP_AUTH_ENABLED=true
NMP_AUTH_POLICY_DECISION_POINT_BASE_URL=http://auth:8000
NMP_AUTH_POLICY_DECISION_POINT_PROVIDER=embedded
NMP_AUTH_ADMIN_EMAIL=admin@example.com
NMP_AUTH_EMBEDDED_PDP_AUTO_BUILD_WASM=true
```

Nested keys (e.g., OIDC) use double underscore: `NMP_AUTH_OIDC__ISSUER`, `NMP_AUTH_OIDC__CLIENT_ID`.
Expand All @@ -93,16 +94,38 @@ auth:
enabled: true
policy_decision_point_provider: embedded
policy_decision_point_base_url: "http://localhost:8080"
embedded_pdp_auto_build_wasm: true
admin_email: "admin@example.com"
```

When running from a source checkout, embedded PDP startup automatically builds a
missing or stale `policy.wasm` with the pinned OPA version used by
`make build-policy`. Packaged deployments should include `policy.wasm` at image
or wheel build time; set `embedded_pdp_auto_build_wasm: false` to fail fast if
that artifact is missing.

In offline development environments, provide the pinned OPA binary explicitly:

```bash
OPA_BIN=/path/to/opa_linux_amd64_static uv run nemo services run --host 127.0.0.1 --port 8080
```

Or seed the local cache used by `script/build_policy_wasm.sh`:

```bash
mkdir -p .cache/opa/v1.8.0
cp /path/to/opa_linux_amd64_static .cache/opa/v1.8.0/opa_linux_amd64_static
chmod +x .cache/opa/v1.8.0/opa_linux_amd64_static
```

### Production with embedded PDP

```yaml
auth:
enabled: true
policy_decision_point_base_url: "http://auth:8000"
policy_decision_point_provider: embedded
embedded_pdp_auto_build_wasm: false
policy_data_refresh_interval: 30
admin_email: "platform-admin@company.com"
oidc:
Expand Down
2 changes: 2 additions & 0 deletions docs/set-up/config-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ auth:
propagation_poll_interval_seconds: 1.0
# Allow unsigned JWTs (`alg=none`) for local development/testing. Disabled by default and should not be enabled in production. | default: False
allow_unsigned_jwt: false
# When auth is enabled with the embedded PDP and policy.wasm is missing or stale, build it automatically from a local NeMo Platform source checkout. Packaged deployments should include policy.wasm at build time and can disable this for fail-fast startup. | default: True
embedded_pdp_auto_build_wasm: true
# OIDC configuration for native token validation.
oidc:
# Enable native OIDC token validation. | default: False
Expand Down
9 changes: 9 additions & 0 deletions packages/nmp_common/src/nmp/common/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ class AuthConfig(create_service_config_class("auth")):
),
)

embedded_pdp_auto_build_wasm: bool = Field(
default=True,
description=(
"When auth is enabled with the embedded PDP and policy.wasm is missing or stale, "
"build it automatically from a local NeMo Platform source checkout. Packaged deployments "
"should include policy.wasm at build time and can disable this for fail-fast startup."
),
)

oidc: OIDCConfig = Field(
default_factory=OIDCConfig,
description="OIDC configuration for native token validation.",
Expand Down
23 changes: 23 additions & 0 deletions packages/nmp_platform_runner/src/nmp/platform_runner/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

logger = logging.getLogger(__name__)
console = Console()
error_console = Console(stderr=True)


def _startup_phase(name: str, t0: float) -> None:
Expand All @@ -52,6 +53,25 @@ def _database_display(db_url: str) -> str:
return db_type


def _is_policy_wasm_error(error: Exception) -> bool:
return (
error.__class__.__name__ == "PolicyWasmError"
and error.__class__.__module__ == "nmp.core.auth.app.embedded_pdp.policy_wasm"
Comment thread
mckornfield marked this conversation as resolved.
Outdated
)


def _display_policy_wasm_error(error: Exception) -> None:
error_console.print()
error_console.print(
Panel(
str(error),
title="Embedded Auth Policy WASM Startup Failed",
border_style="red",
expand=False,
)
)


def run_controllers_in_threads(
controller_run_funcs: dict[str, Callable],
stop_signal: threading.Event,
Expand Down Expand Up @@ -163,6 +183,9 @@ def signal_handler(signum: int, _frame: object) -> None:
except KeyboardInterrupt:
logger.info("Received keyboard interrupt, shutting down")
except Exception as error:
if _is_policy_wasm_error(error):
_display_policy_wasm_error(error)
raise SystemExit(1) from None
logger.exception("Fatal error occurred")
raise SystemExit(1) from error
finally:
Expand Down
18 changes: 18 additions & 0 deletions packages/nmp_platform_runner/src/nmp/platform_runner/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ async def dispatch(self, request: Request, call_next) -> Response:
return await call_next(request)


def preflight_embedded_auth_policy_wasm(auth_config) -> None:
"""Ensure local embedded auth PDP has a loadable policy.wasm before serving traffic."""
if not auth_config.enabled or auth_config.policy_decision_point_provider != "embedded":
return

try:
from nmp.core.auth.app.embedded_pdp.policy_wasm import ensure_embedded_policy_wasm
except ImportError as exc:
raise RuntimeError(
"Auth is enabled with the embedded PDP, but the nmp-auth package is not installed. "
"Install nmp-auth or set auth.policy_decision_point_provider='opa'."
) from exc
Comment thread
mckornfield marked this conversation as resolved.

ensure_embedded_policy_wasm(auto_build=getattr(auth_config, "embedded_pdp_auto_build_wasm", True))


def create_platform_openapi_app() -> FastAPI:
"""Create the platform app used for aggregate OpenAPI generation."""
services = []
Expand Down Expand Up @@ -196,13 +212,15 @@ async def root_handler() -> Response:

def run_server(services: list[Service] | None = None, host: str = "0.0.0.0", port: int = 8080) -> None:
"""Run the platform API server."""
preflight_embedded_auth_policy_wasm(get_auth_config())
app = create_app(services or [])
setup_fastapi_instrumentations(app)
uvicorn.run(app, host=host, port=port, log_config=None)


def run_server_with_reload(app_factory: str, host: str = "0.0.0.0", port: int = 8080) -> None:
"""Run the platform API server with uvicorn reload enabled."""
preflight_embedded_auth_policy_wasm(get_auth_config())
reload_dirs = [
"packages/nmp_platform/src",
"services/core",
Expand Down
38 changes: 38 additions & 0 deletions packages/nmp_platform_runner/tests/test_run_policy_wasm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from nmp.platform_runner import run
from rich.console import Console


class PolicyWasmError(Exception):
pass


PolicyWasmError.__module__ = "nmp.core.auth.app.embedded_pdp.policy_wasm"


def test_policy_wasm_error_is_expected_startup_error():
assert run._is_policy_wasm_error(PolicyWasmError("boom"))
assert not run._is_policy_wasm_error(RuntimeError("boom"))


def test_policy_wasm_error_renders_as_panel(tmp_path, monkeypatch):
stderr = tmp_path / "stderr.txt"
console = Console(file=stderr.open("w"), force_terminal=False, width=100)
monkeypatch.setattr(run, "error_console", console)

run._display_policy_wasm_error(
PolicyWasmError(
"Failed to build embedded auth PDP policy.wasm.\n\n"
"Command:\n"
" script/build_policy_wasm.sh\n\n"
"Offline options:\n"
" OPA_BIN=/path/to/opa ./script/build_policy_wasm.sh"
)
)

output = stderr.read_text()
assert "Embedded Auth Policy WASM Startup Failed" in output
assert "script/build_policy_wasm.sh" in output
assert "OPA_BIN=/path/to/opa" in output
55 changes: 55 additions & 0 deletions packages/nmp_platform_runner/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,61 @@ def fake_create_app(services, controller_run_funcs=None, _http_client=None):
assert captured["controller_run_funcs"] == {"agents-deployment": plugin_controller}


def test_embedded_auth_preflight_invokes_policy_wasm_helper(monkeypatch):
calls: list[bool] = []
auth_cfg = AuthConfig(
enabled=True,
policy_decision_point_provider="embedded",
embedded_pdp_auto_build_wasm=False,
)

from nmp.core.auth.app.embedded_pdp import policy_wasm

monkeypatch.setattr(policy_wasm, "ensure_embedded_policy_wasm", lambda *, auto_build: calls.append(auto_build))

server.preflight_embedded_auth_policy_wasm(auth_cfg)

assert calls == [False]


@pytest.mark.parametrize(
"auth_cfg",
[
AuthConfig(enabled=False, policy_decision_point_provider="embedded"),
AuthConfig(enabled=True, policy_decision_point_provider="opa"),
],
)
def test_embedded_auth_preflight_skips_when_not_needed(auth_cfg, monkeypatch):
calls: list[bool] = []

from nmp.core.auth.app.embedded_pdp import policy_wasm

monkeypatch.setattr(policy_wasm, "ensure_embedded_policy_wasm", lambda *, auto_build: calls.append(auto_build))

server.preflight_embedded_auth_policy_wasm(auth_cfg)

assert calls == []


def test_run_server_runs_embedded_auth_preflight():
auth_cfg = _make_auth_config(enabled=True)
calls: list[AuthConfig] = []
with (
patch("nmp.platform_runner.server.get_auth_config", return_value=auth_cfg),
patch(
"nmp.platform_runner.server.preflight_embedded_auth_policy_wasm", side_effect=lambda cfg: calls.append(cfg)
),
patch("nmp.platform_runner.server.create_app", return_value=FastAPI()) as create_app,
patch("nmp.platform_runner.server.setup_fastapi_instrumentations"),
patch("nmp.platform_runner.server.uvicorn.run") as uvicorn_run,
):
server.run_server(services=[], host="127.0.0.1", port=9999)

assert calls == [auth_cfg]
create_app.assert_called_once_with([])
uvicorn_run.assert_called_once()


def test_create_default_app_raises_for_unknown_service_from_env(monkeypatch):
monkeypatch.setattr(server, "_obs_initialized", True)
monkeypatch.setenv("NMP_SERVICES", "missing-service")
Expand Down
Loading
Loading