Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.

v0.2.0

Latest

Choose a tag to compare

@github-actions github-actions released this 06 Oct 14:23
· 3 commits to main since this release
ebb3fb8

The AuthZEN and MCP binding release. A tool's declared mapping now decides what the Policy Decision Point is asked, and the enforcement point has been run against three PDPs that somebody else wrote. Those were the last two v0.2 items in ROADMAP.md.

Wire format: accepts mdt.v 1, issues mdt.v 1. Unchanged from 0.1.0.

This is a 0.x minor release, so the Go API may change, and it does in one place: mcp.Authorized.Decision became Decisions, under Changed below.

The gate, and how it was met

  • Works against three PDPs from different vendors with no implementation-specific code paths. The PDPs are Open Policy Agent through its contrib AuthZEN proxy, Cerbos, and OpenFGA. All three are on the AuthZEN working group's interop list, none shares code with this project, and the interop workflow runs them from pinned, digest-checked releases. The client and the enforcement point have no code that knows which PDP they are talking to. The per-PDP differences are a policy file each and one client option that already existed, and test/interop/README.md says why each is there.

Not a gate for this version, but held to anyway: the new fuzz target, FuzzDeclaredMapping, accumulated twenty-five hours with no crasher before the tag, about 810 million executions, over the code this tag names. The commits between the one fuzzed, 4338a42, and the tag change only documentation. That is the same bar v0.1 set for the targets it shipped, met the same way, as five concurrent campaigns. The mapping resolver parses input written by the server being authorized, and shipping it with minutes of fuzzing would not have matched how the parsers before it were treated.

Added

  • Declared mappings for tools/call, the COAZ-MCP binding's x-authzen-mapping. authzen.ParseMapping reads one and compiles its CEL up front, so a mapping that cannot work is refused when it is loaded rather than on the first call; authzen.MappingFromInputSchema finds one in a tool's inputSchema. A catalog returns it in mcp.Facts.Mapping and the enforcer asks the PDP what the mapping constructs instead of the default mapping. Both envelopes are supported: evaluations becomes one Access Evaluation request per entry, and the call is allowed only if every one is.
  • The identities in a declared mapping's request come from the chain, not the mapping. subject.id is the sponsor, subject.properties.iss the sponsor's issuer, context.agent the acting agent, and the verified chain is in context["mandatum.delegation"]; a mapping that sets any of them to something else is refused. The binding lets a mapping override subject.id for deployments that cannot carry the user in a token. A chain always carries the user, so here the override would only ever let the server being authorized say whose call it is. The divergence is in the conformance notes.
  • A declared mapping changes the question the PDP is asked and nothing else. The chain's own grant is still checked first, against the deployment's description of the tool, so a server cannot declare its way past what the sponsor delegated; TestADeclaredMappingCannotWidenTheGrant asserts the PDP is not asked.
  • mcp.StageMapping, the stage that refuses a call whose mapping could not produce a request. It runs between the grant and the PDP, and the PDP is not asked.
  • Bounds on a mapping's expressions, which are written by the server being authorized and run on every call to its tool: a CEL cost limit, authzen.MaxExpressionCost, and a separate bound on what they produce for one call, authzen.MaxResolvedValues and authzen.MaxResolvedBytes. The second exists because the first does not cover it — CEL charges for referencing a list, not for its length, so an expression mapping one argument's list onto another's evaluated well under the cost limit while allocating tens of gigabytes. Found in review before merge; TestAnAmplifyingExpressionIsStopped is the case.
  • FuzzDeclaredMapping, which holds the anchoring above as an invariant over arbitrary mappings. The nightly campaign picks it up with the others.
  • The module's first dependency, CEL (cel.dev/cel-go). The binding has no expression-free conformance level. It is imported by pkg/authzen alone; the verification core still depends only on the standard library, and the supply-chain notes say how to check that.
  • An interoperability test against three independent PDPs: Open Policy Agent through its contrib AuthZEN proxy, Cerbos, and OpenFGA. This is the v0.2 gate. One scenario, run through the enforcement point with the stock client, covers both the default and a declared mapping. Every refusal in it has to come from the PDP reading one specific field. The PDPs run from pinned, digest-checked releases on every pull request that touches the client or the enforcement point. Per PDP, only the policy file differs. test/interop/README.md records what the run found:
    • Cerbos and OpenFGA do not echo X-Request-ID, which the Authorization API requires, so the client is configured not to send one.
    • Cerbos policies cannot read the request context, so behind Cerbos nothing can be decided by the acting agent.

Changed

  • mcp.Authorized.Decision is now Decisions, one per evaluation the mapping constructed, in order. There is still exactly one unless the tool declares an evaluations mapping, so Decisions[0] is what Decision was.
  • The conformance notes record a divergence that was there before this change and was not written down: the middleware answers every refusal with code 403, where the binding specifies -32602, -32001 and -32603 by kind.

Known limitations

What 0.1.0 listed and this release does not fix still stands: no audit log, an in-process sequence store only, nothing publishing a revocation set on a schedule, no third-party security review, and rule V7 unreachable through the public API. Two of its limitations are gone, declared mappings and an untested PDP binding. What changed or is new:

  • Only tools/call is mapped to AuthZEN. Declared mappings now apply to it. Every other MCP method is still forwarded unexamined, which diverges from the binding's deny-by-default over unmapped methods. The middleware does not learn mappings from tools/list responses, so a deployment supplies them through its catalog. See docs/spec/coaz-mcp-conformance.md.
  • Two of the three tested PDPs need request correlation switched off. Cerbos and OpenFGA do not echo X-Request-ID, and the client refuses an answer it cannot tie to its question. Against them, authzen.WithRequestID(func() string { return "" }) is required, and the PEP and PDP logs then cannot be joined on a request identifier.
  • Behind Cerbos, nothing can be decided by the acting agent. Cerbos policies cannot read the evaluation context, which is where context.agent and the verified chain go. A declared mapping can project the agent into subject or resource properties, where Cerbos can read it. The default mapping does not do that.
  • Refusals carry code 403, not the binding's codes. The binding specifies -32602, -32001 and -32603 by kind. The middleware's reason is recorded where the constant is defined.

Verifying this release

cosign verify-blob \
  --bundle checksums.txt.bundle \
  --certificate-identity-regexp 'https://github.com/kanywst/mandatum/.github/workflows/release.yml@.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  checksums.txt

sha256sum -c checksums.txt

An SPDX SBOM is attached. Build provenance is attested and can be checked with gh attestation verify.

The full changelog, including which Delegation Assertion format versions every release accepts and issues, is in CHANGELOG.md.