Skip to content

fix(agents): scope agent deployments to creator via on-behalf-of - #1000

Merged
benmccown merged 2 commits into
mainfrom
fix-agent-rbac
Jul 30, 2026
Merged

fix(agents): scope agent deployments to creator via on-behalf-of#1000
benmccown merged 2 commits into
mainfrom
fix-agent-rbac

Conversation

@benmccown

@benmccown benmccown commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Agent deployments previously authenticated as the service:agents principal, which resolves to the ServiceSystem role — a platform-wide wildcard (*) grant. That gave every deployed agent admin-level reach across all workspaces. This PR scopes a deployment's runtime platform access to the identity that created it, using the platform's existing on-behalf-of (OBO) delegation mechanism.

The control plane (controller reconcile loop + DeploymentConfig/Deployment entity writes) intentionally stays service:agents — only the deployed agent's own platform calls are delegated.

Motivation

service:agents was hardcoded when auth support was added to AgentDeployment. Realistically a deployed agent should only reach the workspace(s) its creator can. The OBO stack already exists platform-wide (Principal.on_behalf_of / effective_principal, X-NMP-Principal-On-Behalf-Of, and the entity store already computes access for the OBO user), and the entity created_by is server-populated from the creating principal — so we delegate to that.

PR Review Guide

The idea: a deployed agent was authenticating as service:agents (platform-wide admin). Now it delegates to its creator via on-behalf-of, so it's scoped to the creator's workspaces. 17 files, but really 3 small moves + tests + 1 regenerated artifact.

1. Sidecar learns "on behalf of (OBO)" — the core mechanism

  • packages/nmp_common/.../auth/workload_proxy/main.py — new optional NMP_AUTH_PROXY_ON_BEHALF_OF env; stamps X-NMP-Principal-On-Behalf-Of on forwarded requests. Added the inbound OBO header to the strip-set so a workload can't spoof it. This is the heart of the change.

2. Plumb the creator through to that env var

  • plugins/nemo-deployments/.../entities.py — new DeploymentConfig.auth_proxy_sidecar_on_behalf_of field.
  • plugins/nemo-deployments/.../auth_proxy.py — passes that field into the sidecar's NMP_AUTH_PROXY_ON_BEHALF_OF env (appended only when set).
  • plugins/nemo-agents/.../runner/deployments_backend.py — sets it from the deployment's created_by when auth is on (warns if no creator).
  • plugins/nemo-agents/.../runner/{backend,controller,in_memory}.py — thread created_by through the create_deployment interface; controller passes dep.created_by; subprocess backend ignores it (no sidecar).

3. Make IGW actually honor "on behalf of"

  • services/core/inference-gateway/.../api/authz.py (new)enforce_delegated_workspace_access(workspace, permission): for a delegated service principal, check the OBO user holds the required inference.gateway.*.exec perm in the workspace (PDP evaluated as that user). No-op for everyone else, so no behavior change for normal callers.
  • services/core/inference-gateway/.../api/v2/{openai,models,providers}.py — call that helper before routing. Needed because the shared route gate takes the service bypass and never narrows on OBO — without this, stamping OBO would do nothing for inference.

Not changed on purpose

  • Control plane (controller reconcile + DeploymentConfig/Deployment writes) stays service:agents.

Mechanical / low-attention

  • plugins/nemo-deployments/openapi/openapi.yaml — regenerated (make refresh-openapi); just the one new field. Root SDK/CLI unaffected.
  • Tests: test_workload_proxy.py (OBO stamp + spoof-strip), test_auth_proxy.py (env wiring), test_runner_deployments.py (created_by threading), test_authz.py (new) + test_igw_with_auth.py (delegated allow/deny).

Suggested read order: workload_proxy/main.pyinference-gateway/api/authz.py → the deployments/agents plumbing → skim tests.

Testing

  • Unit: workload-proxy OBO stamp + spoof-stripping; DeploymentConfig env wiring; created_by threading (incl. no-creator fallback); new IGW test_authz.py (no-op paths + delegated allow/deny).
  • Integration: TestIGWDelegatedServicePrincipalAccess — delegated list-models and openai-proxy denied (403) when the OBO user lacks a role, allowed when granted. Existing TestIGWServicePrincipalAccess (non-delegated bypass) still passes → no regression.
  • IGW unit suite (458) green; ty, ruff, and pre-commit hooks clean.

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Deployments can delegate platform access based on the principal that created them.
    • Added optional “on-behalf-of” support for the auth-proxy sidecar configuration.
    • Inference Gateway now enforces workspace-scoped access for delegated (“on-behalf-of”) service principals.
  • Bug Fixes

    • Hardened auth-proxy request handling to prevent spoofing of delegated identity metadata.
    • Delegated principals without required workspace permissions now receive 403.
  • Tests

    • Expanded unit and integration coverage for delegated access and spoofing-prevention behavior.

@benmccown
benmccown requested review from a team as code owners July 30, 2026 20:07
@benmccown benmccown self-assigned this Jul 30, 2026
@github-actions github-actions Bot added the fix label Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR propagates created_by into deployment auth-proxy sidecars, forwards on-behalf-of principals while stripping spoofed headers, and enforces delegated workspace permissions across inference-gateway routes.

Changes

Delegated authorization

Layer / File(s) Summary
Auth-proxy delegated header forwarding
packages/nmp_common/src/..., packages/nmp_common/tests/...
The proxy reads delegated configuration, strips inbound identity and companion metadata headers, and forwards the configured delegated principal.
Deployment creator propagation
plugins/nemo-agents/..., plugins/nemo-deployments/...
Deployment creation carries created_by into DeploymentConfig and conditionally injects NMP_AUTH_PROXY_ON_BEHALF_OF into the sidecar.
Inference-gateway delegated access enforcement
services/core/inference-gateway/src/..., services/core/inference-gateway/tests/...
Model, OpenAI, and provider routes validate workspaces and enforce delegated permissions before request processing, with unit and integration coverage for allowed and denied access.

Sequence Diagram(s)

sequenceDiagram
  participant AgentDeploymentController
  participant DeploymentsRunnerBackend
  participant auth_proxy
  participant InferenceGateway
  participant AuthClient
  AgentDeploymentController->>DeploymentsRunnerBackend: Create deployment with created_by
  DeploymentsRunnerBackend->>auth_proxy: Configure delegated principal
  auth_proxy->>InferenceGateway: Forward request with on-behalf-of header
  InferenceGateway->>AuthClient: Check workspace permission
  AuthClient-->>InferenceGateway: Allow or deny
Loading

Possibly related PRs

Suggested reviewers: mckornfield, anastasia-nesterenko, ironcommit

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly matches the main change: scoping agent deployments to the creator via on-behalf-of delegation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-agent-rbac

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/nmp_common/src/nmp/common/auth/workload_proxy/main.py (1)

64-70: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Strip the OBO email header too.

X-NMP-Principal-On-Behalf-Of-Email survives forwarding. A colocated workload can attach untrusted delegated identity metadata to the proxy-stamped OBO principal. Strip it alongside the OBO ID header and cover both configured/unconfigured paths.

Proposed fix
 _ON_BEHALF_OF_HEADER = "x-nmp-principal-on-behalf-of"
+_ON_BEHALF_OF_EMAIL_HEADER = "x-nmp-principal-on-behalf-of-email"

         _PRINCIPAL_ID_HEADER,
         _ON_BEHALF_OF_HEADER,
+        _ON_BEHALF_OF_EMAIL_HEADER,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nmp_common/src/nmp/common/auth/workload_proxy/main.py` around lines
64 - 70, Update _STRIP_REQUEST_HEADERS to include the
X-NMP-Principal-On-Behalf-Of-Email header alongside _ON_BEHALF_OF_HEADER. Ensure
request forwarding strips this email header in both configured and unconfigured
proxy paths, and add or update coverage for both cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nmp_common/src/nmp/common/auth/workload_proxy/main.py`:
- Around line 165-172: Update the startup log in the auth-proxy sidecar around
the logger.info call to remove the on_behalf_of value entirely and report only
whether delegation is enabled. Preserve the other startup context and ensure no
delegated principal or creator identifier is written to logs.

---

Outside diff comments:
In `@packages/nmp_common/src/nmp/common/auth/workload_proxy/main.py`:
- Around line 64-70: Update _STRIP_REQUEST_HEADERS to include the
X-NMP-Principal-On-Behalf-Of-Email header alongside _ON_BEHALF_OF_HEADER. Ensure
request forwarding strips this email header in both configured and unconfigured
proxy paths, and add or update coverage for both cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 4ea24ed1-6a81-45a1-8799-b77f6629266d

📥 Commits

Reviewing files that changed from the base of the PR and between 9b0f9ad and f8846f8.

📒 Files selected for processing (17)
  • packages/nmp_common/src/nmp/common/auth/workload_proxy/main.py
  • packages/nmp_common/tests/auth/test_workload_proxy.py
  • plugins/nemo-agents/src/nemo_agents_plugin/runner/backend.py
  • plugins/nemo-agents/src/nemo_agents_plugin/runner/controller.py
  • plugins/nemo-agents/src/nemo_agents_plugin/runner/deployments_backend.py
  • plugins/nemo-agents/src/nemo_agents_plugin/runner/in_memory.py
  • plugins/nemo-agents/tests/unit/test_runner_deployments.py
  • plugins/nemo-deployments/openapi/openapi.yaml
  • plugins/nemo-deployments/src/nemo_deployments_plugin/auth_proxy.py
  • plugins/nemo-deployments/src/nemo_deployments_plugin/entities.py
  • plugins/nemo-deployments/tests/unit/test_auth_proxy.py
  • services/core/inference-gateway/src/nmp/core/inference_gateway/api/authz.py
  • services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/models.py
  • services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/openai.py
  • services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/providers.py
  • services/core/inference-gateway/tests/integration/test_igw_with_auth.py
  • services/core/inference-gateway/tests/unit/test_authz.py

Comment thread packages/nmp_common/src/nmp/common/auth/workload_proxy/main.py
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 28412/36254 78.4% 62.8%
Integration Tests 17007/34972 48.6% 21.2%

@ironcommit ironcommit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@benmccown
benmccown enabled auto-merge July 30, 2026 21:51
Agent deployments previously authenticated as the service:agents principal,
which resolves to the ServiceSystem role (platform-wide wildcard access).
Scope a deployment's runtime platform access to the identity that created it
by delegating via on-behalf-of, instead of granting admin-level reach.

- workload-proxy sidecar: add optional NMP_AUTH_PROXY_ON_BEHALF_OF; stamp
  X-NMP-Principal-On-Behalf-Of on forwarded requests, and strip the inbound
  OBO header so a co-located workload cannot spoof the delegated identity.
- deployments plugin: add DeploymentConfig.auth_proxy_sidecar_on_behalf_of and
  wire it into the auth-proxy sidecar env (regenerated plugin OpenAPI spec).
- agents runner: thread the deployment creator (created_by) through the
  container backend into the sidecar OBO env at deploy time.
- inference-gateway: enforce delegated workspace access on the proxy path.
  For a delegated service principal, verify the on-behalf-of user holds the
  required inference permission in the target workspace (via the PDP evaluated
  as the delegated user). Non-delegated callers are unchanged, preserving the
  existing internal service bypass. Without this, the IGW route gate takes the
  service bypass and never narrows on OBO.

The control plane (controller reconcile + DeploymentConfig/Deployment entity
writes) intentionally remains service:agents.

Adds unit + integration coverage for the sidecar OBO stamping/spoof-stripping,
the DeploymentConfig wiring, the created_by threading, and IGW delegated
allow/deny behavior.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
Address PR review feedback on the auth-proxy sidecar:

- Strip inbound X-NMP-Principal-On-Behalf-Of-Email and
  -Groups companion headers, not just the OBO id. The platform derives the
  delegated user's effective groups/email from these headers and feeds them to
  the PDP, so a co-located workload could otherwise pair our stamped OBO id with
  attacker-chosen groups/email and be authorized as those, defeating the
  scoping. Extend the spoof-strip tests to cover both companion headers in the
  configured and unconfigured paths.
- Do not log the delegated principal id in the sidecar startup line; log a
  boolean (delegated=<bool>) instead to keep creator identifiers out of logs.

Signed-off-by: Ben McCown <bmccown@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/core/inference-gateway/src/nmp/core/inference_gateway/api/authz.py`:
- Around line 78-90: Update the authorization-denial handling around the
principal permission check to stop exposing principal.on_behalf_of. Remove it
from the logger.info arguments and use only non-identifying context such as
permission, workspace, and service; replace the HTTPException detail with a
generic inference authorization denial that does not include delegated-principal
data.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f6c1ff43-bb51-4463-88ed-2f15d9ce1857

📥 Commits

Reviewing files that changed from the base of the PR and between 916cfef and 271b7df.

📒 Files selected for processing (17)
  • packages/nmp_common/src/nmp/common/auth/workload_proxy/main.py
  • packages/nmp_common/tests/auth/test_workload_proxy.py
  • plugins/nemo-agents/src/nemo_agents_plugin/runner/backend.py
  • plugins/nemo-agents/src/nemo_agents_plugin/runner/controller.py
  • plugins/nemo-agents/src/nemo_agents_plugin/runner/deployments_backend.py
  • plugins/nemo-agents/src/nemo_agents_plugin/runner/in_memory.py
  • plugins/nemo-agents/tests/unit/test_runner_deployments.py
  • plugins/nemo-deployments/openapi/openapi.yaml
  • plugins/nemo-deployments/src/nemo_deployments_plugin/auth_proxy.py
  • plugins/nemo-deployments/src/nemo_deployments_plugin/entities.py
  • plugins/nemo-deployments/tests/unit/test_auth_proxy.py
  • services/core/inference-gateway/src/nmp/core/inference_gateway/api/authz.py
  • services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/models.py
  • services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/openai.py
  • services/core/inference-gateway/src/nmp/core/inference_gateway/api/v2/providers.py
  • services/core/inference-gateway/tests/integration/test_igw_with_auth.py
  • services/core/inference-gateway/tests/unit/test_authz.py

@benmccown
benmccown added this pull request to the merge queue Jul 30, 2026
Merged via the queue into main with commit 3ab873b Jul 30, 2026
53 of 54 checks passed
@benmccown
benmccown deleted the fix-agent-rbac branch July 30, 2026 22:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants