Adding an opa plugin#427
Conversation
|
As for security scoring - OPA relies on Low score ≠ vulnerability. We can engage with the OPA community about them if we choose. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds an OPA-backed authorization plugin to AuthBridge: configuration, SDK lifecycle and bundle activation, four decision paths for inbound/outbound request/response, include-set-driven input shaping for extensions, comprehensive tests and docs, and compile-time registration in envoy/lite/proxy with go.mod updates. ChangesOPA Authorization Plugin
Sequence Diagram(s)sequenceDiagram
participant Pipeline as AuthBridgePipeline
participant OPAPlugin as OPA plugin
participant OPASDK as OPASDK
participant BundleServer as BundleServer
Pipeline->>OPAPlugin: Configure / Init
OPAPlugin->>OPASDK: startOPA(bundle: bundles/<agentID>.tar.gz)
OPASDK->>BundleServer: GET bundle_url/bundles/<agentID>.tar.gz
BundleServer-->>OPASDK: bundle tar.gz
OPASDK-->>OPAPlugin: Ready
Pipeline->>OPAPlugin: OnRequest/OnResponse(input)
OPAPlugin->>OPASDK: Evaluate(path, input)
OPASDK-->>OPAPlugin: decision (allow|deny|undefined|error)
OPAPlugin-->>Pipeline: continue|reject (record outcome)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 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 `@authbridge/authlib/go.mod`:
- Around line 34-40: Remove the duplicate require entry for
github.com/goccy/go-json v0.10.3 in the go.mod (the repeated
"github.com/goccy/go-json v0.10.3 // indirect" lines); keep a single entry and
then run go mod tidy to reconcile module metadata. Locate the duplicate in the
authlib go.mod, delete the redundant line, and verify with go mod tidy that no
other duplicates remain.
In `@authbridge/authlib/plugins/opa/plugin.go`:
- Around line 247-250: The OnRequest branch that returns pipeline.DenyStatus
when p.decider == nil must also emit a session event so not-ready denials are
observable; locate the OPA.OnRequest method, and before returning
pipeline.DenyStatus(503, "upstream.unreachable", "opa policy engine not
initialized") add a call to the plugin's session-event emitter (use the existing
observability helper used elsewhere in this plugin, e.g., p.emitSessionEvent /
p.EmitSessionEvent or the module's session event helper) with action "deny" and
the same reason/metadata so the denial is recorded in session observability.
- Around line 198-208: The code in buildOPAConfig constructs the bundle resource
using p.agentID directly ("bundles/%s.tar.gz"), which allows path traversal or
invalid characters; update buildOPAConfig to validate or sanitize p.agentID
before use (e.g., enforce a strict whitelist regexp like /^[A-Za-z0-9._-]+$/ or
similar) and reject or error on invalid values, or alternatively
percent-encode/URL-escape the agent id when interpolating into the resource
string; ensure the check/escape is applied to p.agentID before creating the
fmt.Sprintf("bundles/%s.tar.gz", p.agentID) value so the OPA bundle resource
cannot be manipulated by malicious input.
- Around line 93-100: There is a data race because OPA fields p.decider and
p.agentID are written in background goroutines (Init/startOPA) and read in
OnRequest/OnResponse; fix by protecting these fields with a sync.Mutex on the
OPA struct or replacing them with an atomic.Value and always read/write via the
chosen concurrency primitive (update Init, startOPA, OnRequest, OnResponse to
use it consistently). In buildOPAConfig validate/sanitize the agentID before
using it to build the bundle path (reject any agentID containing '/', '\\', ".."
or path separators or return an error) so fmt.Sprintf("bundles/%s.tar.gz",
p.agentID) cannot escape directories. Finally ensure the “not-ready” path in
OnRequest emits observability like OnResponse does (call
pctx.Skip("opa_not_ready") or pctx.Record(...) before returning
pipeline.DenyStatus(...)) so session events are recorded when p.decider == nil.
In `@authbridge/authlib/plugins/opa/README.md`:
- Around line 388-395: The README.md contains multiple fenced code blocks
lacking language identifiers (causing MD040); update each backtick fence in the
shown sections (around the example file trees at lines referenced) to include a
language tag such as text (e.g., change ``` to ```text) for every tree/list
block (the three blocks at 388-395 and the other blocks around 414-419 and
423-432) so markdownlint and docs CI pass; ensure you only add the language
identifier to the opening fences and leave the inner content unchanged.
In `@authbridge/authlib/plugins/README.md`:
- Line 30: The table row for the `opa` plugin is in the wrong section; remove
the `| `opa` | OPA policy evaluation on inbound and outbound requests via bundle
download |` row from the "Reusable building blocks for plugin authors" table and
add the same row into the "Built-in plugins" table (preserving table formatting
and column order), and then run the repository markdown spellcheck action to
ensure no MD lint errors remain; locate these edits by searching for the `opa`
plugin entry and the table headers "Reusable building blocks for plugin authors"
and "Built-in plugins" in README.md.
In `@authbridge/cmd/authbridge-lite/go.mod`:
- Around line 65-69: The go.mod contains duplicate require entries for the same
golang.org/x/* modules; open go.mod and remove the duplicated require lines so
each module (golang.org/x/crypto, golang.org/x/net, golang.org/x/sync,
golang.org/x/sys, golang.org/x/text) appears only once with the intended
version, then run `go mod tidy` to verify and update the file; ensure you edit
the duplicate entries in the go.mod require block (look for the repeated module
names) rather than changing other files.
In `@authbridge/cmd/authbridge-lite/main.go`:
- Around line 41-44: The file comment about "Auth gates only: drop the parsers
and token-broker." is out of sync with the imports: jwtvalidation
(github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/jwtvalidation),
opa (github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/opa) and
tokenexchange
(github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/tokenexchange)
are still being compiled; either update the comment to accurately list the
included plugins (jwtvalidation, opa, tokenexchange) or remove the tokenexchange
import if token-broker/tokenexchange should indeed be dropped—locate the import
block in main.go and make the corresponding change so the comment and the
imported plugin set match.
In `@authbridge/cmd/authbridge-proxy/go.mod`:
- Around line 65-69: The go.mod contains duplicate require entries for the
golang.org/x modules (golang.org/x/crypto, golang.org/x/net, golang.org/x/sync,
golang.org/x/sys, golang.org/x/text); remove the redundant duplicates so each of
those module requirements appears only once in the require block, keeping the
desired version (e.g., v0.48.0, v0.49.0, v0.19.0, v0.41.0, v0.34.0) and running
`go mod tidy` afterwards to ensure module graph consistency (look for the
require lines referencing golang.org/x/crypto, golang.org/x/net,
golang.org/x/sync, golang.org/x/sys, golang.org/x/text).
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 4d76c28e-2767-411d-b54a-f7d213fa3645
⛔ Files ignored due to path filters (4)
authbridge/authlib/go.sumis excluded by!**/*.sumauthbridge/cmd/authbridge-envoy/go.sumis excluded by!**/*.sumauthbridge/cmd/authbridge-lite/go.sumis excluded by!**/*.sumauthbridge/cmd/authbridge-proxy/go.sumis excluded by!**/*.sum
📒 Files selected for processing (12)
authbridge/authlib/go.modauthbridge/authlib/plugins/README.mdauthbridge/authlib/plugins/opa/README.mdauthbridge/authlib/plugins/opa/plugin.goauthbridge/authlib/plugins/opa/plugin_test.goauthbridge/authlib/plugins/plugins_test.goauthbridge/cmd/authbridge-envoy/go.modauthbridge/cmd/authbridge-envoy/main.goauthbridge/cmd/authbridge-lite/go.modauthbridge/cmd/authbridge-lite/main.goauthbridge/cmd/authbridge-proxy/go.modauthbridge/cmd/authbridge-proxy/main.go
| ``` | ||
| bundles/my-agent.tar.gz | ||
| authbridge/ | ||
| inbound/ | ||
| request.rego # package authbridge.inbound.request | ||
| outbound/ | ||
| request.rego # package authbridge.outbound.request | ||
| ``` |
There was a problem hiding this comment.
Add language identifiers to fenced code blocks to satisfy markdownlint.
These fences are missing a language tag (MD040) and can fail docs CI.
Proposed fix
-```
+```text
bundles/my-agent.tar.gz
authbridge/
inbound/
request.rego # package authbridge.inbound.request
outbound/
request.rego # package authbridge.outbound.request@@
- +text
bundles/my-agent.tar.gz
authbridge/
inbound/
request.rego
@@
-```
+```text
bundles/my-agent.tar.gz
authbridge/
inbound/
request.rego # package authbridge.inbound.request
response.rego # package authbridge.inbound.response (optional)
outbound/
request.rego # package authbridge.outbound.request (optional)
response.rego # package authbridge.outbound.response (optional)
</details>
As per coding guidelines, "`**/*.md`: Run spellcheck on all markdown files (enforced by spellcheck_action.yml)".
Also applies to: 414-419, 423-432
<details>
<summary>🧰 Tools</summary>
<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>
[warning] 388-388: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
</details>
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @authbridge/authlib/plugins/opa/README.md around lines 388 - 395, The
README.md contains multiple fenced code blocks lacking language identifiers
(causing MD040); update each backtick fence in the shown sections (around the
example file trees at lines referenced) to include a language tag such as text
(e.g., change totext) for every tree/list block (the three blocks at
388-395 and the other blocks around 414-419 and 423-432) so markdownlint and
docs CI pass; ensure you only add the language identifier to the opening fences
and leave the inner content unchanged.
</details>
<!-- fingerprinting:phantom:triton:hawk -->
<!-- This is an auto-generated comment by CodeRabbit -->
Signed-off-by: David Hadas <david.hadas@gmail.com>
… fix observability Signed-off-by: David Hadas <david.hadas@gmail.com>
|
@davidhadas can you do a conflict resolution before I start reviewing this PR? I think some of the changes in this PR has already been merged in earlier PRs |
Summary
This is an OPA Plugin that is working against an OPA bundle service using the standard OPA SDK
Related issue(s)
kagenti/kagenti#792
opa
OPA (Open Policy Agent) plugin for AuthBridge. Downloads policy bundles from a
Kagenti Bundle Server based on the agent's identity and evaluates requests and
responses against the loaded policy using four fixed decision paths.
How it works
/shared/client-id.txt(mounted by the kagenti-operator from a Keycloak-credentials Secret).
fetch
bundles/<agent-id>.tar.gzfrom the bundle server.polling for updates (respecting
ETag/If-None-Matchfor lightweight304 responses).
path based on traffic direction and phase. If the policy denies, the plugin
returns HTTP 403. If the path is undefined (rule not present in the bundle),
the plugin skips evaluation — treating the absence of a rule as "no opinion".
The plugin reports not-ready (
Ready() == false) until the first bundle issuccessfully loaded. The Kubernetes readiness probe holds traffic off the pod
until then, so requests never arrive before the policy is active.
Decision paths
The plugin uses four fixed OPA decision paths, one for each evaluation point:
authbridge/inbound/requestauthbridge/inbound/responseauthbridge/outbound/requestauthbridge/outbound/responseEach path is independent. A bundle only needs to include the rules it cares
about — undefined paths are skipped (treated as allow). Most deployments will
only define
authbridge/inbound/request.Configuration
Default configuration
Most deployments only need
bundle_url. The lean default input is sufficientfor tool-access-control and model-restriction policies:
With this config, policies can decide based on caller identity, tool names,
model names, hosts, and methods — without any bulk content crossing the OPA
evaluation boundary.
Example: content-filtering policy
If a policy needs to inspect the actual user prompt (e.g., block tasks
containing sensitive keywords), extend the input with
a2a.content:This adds
a2a.parts[].content,a2a.artifact, anda2a.error_messagetothe OPA input so the policy can match against user-submitted text.
Example: full MCP arguments + LLM conversation audit
A deployment that needs to write policies against tool argument values and
the full conversation history:
Config fields
bundle_urlagent_id_file/shared/client-id.txtagent_idagent_id_fileis ignoredpolling_min_delay10polling_max_delay120include[]The
includemechanismThe OPA input document is lean by default — it contains only structural
metadata needed for authorization decisions (method, path, host, identity, tool
names, model names). Bulk content fields (conversation history, full tool
arguments, prompt text) are excluded unless explicitly requested via the
includeconfig list.Each entry in
includeis a named group key that unlocks additional fields:mcp.params.nameparams.namein MCP inputmcp.params.uriparams.uriin MCP inputmcp.params.<key>mcp.params.cursor)mcp.paramsparamsmap (all keys)mcp.resultresultmap (response path)mcp.errorerrorobject (code + message + data)a2a.contentparts[].content,artifact,error_messageinference.messagesmessages[]arrayinference.completioncompletiontextinference.tools.detaildescription+parametersinference.tool_callstool_calls[]withargumentsDefault-on keys (
mcp.params.name,mcp.params.uri) are always included evenwith an empty
includelist.OPA input document
Default (lean) input
{ "direction": "inbound", "method": "POST", "path": "/api/v1/invoke", "host": "my-agent", "headers": { "authorization": "Bearer eyJ...", "content-type": "application/json" }, "identity": { "subject": "user-123", "client_id": "caller-agent", "scopes": ["openid", "profile"] }, "agent": { "client_id": "my-agent" }, "a2a": { "method": "invoke", "session_id": "sess-123", "task_id": "task-789", "role": "user" }, "mcp": { "method": "tools/call", "params": { "name": "create_issue", "uri": "file:///workspace/main.go" } }, "inference": { "model": "gpt-4", "stream": false, "max_tokens": 4000, "tools": ["create_issue", "list_issues"] } }Notes:
mcp.paramscontains only the default-on keys (name,uri) that arepresent in the original params. Add more via
include.inference.toolsis a string array of tool names only (not full objects).Use
inference.tools.detailinincludefor descriptions and parameters.With
include: ["a2a.content", "mcp.params", "inference.messages", "inference.tools.detail"]{ "a2a": { "method": "invoke", "session_id": "sess-123", "task_id": "task-789", "role": "user", "parts": [ { "kind": "text", "content": "Create a GitHub issue for bug XYZ" } ], "artifact": "Issue #42 created", "error_message": "" }, "mcp": { "method": "tools/call", "params": { "name": "create_issue", "arguments": {"title": "Bug XYZ", "body": "..."} } }, "inference": { "model": "gpt-4", "stream": false, "max_tokens": 4000, "messages": [ {"role": "user", "content": "Help me create an issue"} ], "tools": [ {"name": "create_issue", "description": "Creates a GitHub issue", "parameters": {"type": "object"}} ] } }On the response path the document also includes:
{ "response": { "status_code": 200, "headers": { "content-type": "application/json" } } }Input field reference
direction"inbound"or"outbound"methodGET,POST, ...)pathhostHostheader valueheaders,)identityagenta2amcpinferenceresponsePolicy contract
Each decision path evaluates to an
allowrule. The plugin supports tworeturn shapes:
Boolean -- the simplest form:
Object with reason -- for detailed deny messages:
Example policies
Tool access control (MCP) — inbound request
LLM model restriction (Inference) — outbound request
Dangerous tool combination block (Inference) — outbound request
Task content filtering (A2A) — requires
include: ["a2a.content"]Multi-path bundle example
A single bundle can contain rules for multiple decision paths:
The plugin interprets the decision as follows:
truefalse{"allow": true}{"allow": false}{"allow": false, "reason": "..."}Bundle layout
The bundle server must serve a standard OPA bundle at the path
bundles/<agent-id>.tar.gz. A minimal bundle contains a single.regofilefor the inbound request path:
A full bundle covering all four decision paths:
Only include the paths you need — the plugin skips evaluation for any
undefined path.
See the OPA bundle documentation
for the full specification.
Pipeline ordering
The plugin declares
After: ["jwt-validation", "a2a-parser", "mcp-parser", "inference-parser"](soft ordering). When these plugins are present in the same pipeline, OPA runs
after them so
input.identity,input.a2a,input.mcp, andinput.inferenceare populated. If any are absent, OPA still runs — the corresponding input
fields will be missing and the policy must handle that case.
Deny behavior
request arrives anyway (e.g. in tests), the plugin denies with 503.
Session events
The plugin records
Invocationentries for every decision:allowpolicy_allowed/response_policy_alloweddenypolicy_denied/response_policy_denieddenydecision_error/response_decision_errorskipopa_not_readyskipno_policy_ruleThese appear in the session events API (
:9094) and inabctl.Summary by CodeRabbit
New Features
Documentation
Tests
Chores