Add CEL actor matching for trusted issuers - #6364
Conversation
Trusted external issuers previously authorized delegation only when a single configured actor claim matched an exact allowedActors entry. That model cannot express issuer-signed delegation classes or policies based on multiple verified claims, while accepting a valid external token without explicit consent would create a confused-deputy risk. Add actorMatcher as an optional CEL predicate over the complete signature-verified subject-token claims map. A true matcher result is additive to allowedActors; both paths still require the authenticated ToolHive client to match allowedDelegateClients. may_act remains opt-in, authoritative when present, and is evaluated before actor authorization. Expose actorMatcher through the shared embedded-auth-server CRD configuration, convert it into the auth-server RunConfig, and validate/compile it before server startup. Keep runtime matcher failures fail-closed, preserve generic OAuth client errors, and log only safe DEBUG diagnostics. Document CEL has() guards for optional claims, strengthen matcher unit/integration coverage, and add kind E2E coverage for successful and rejected CRD-configured matchers. Fixes #6322
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6364 +/- ##
==========================================
+ Coverage 72.97% 73.05% +0.07%
==========================================
Files 742 744 +2
Lines 78398 78651 +253
==========================================
+ Hits 57208 57455 +247
+ Misses 17201 17172 -29
- Partials 3989 4024 +35 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
samuv
left a comment
There was a problem hiding this comment.
Summary
This is a thoughtful, well-tested addition that keeps the existing allowlist path and adds matcher authorization with fail-closed evaluation. I left three inline comments. The main concern is that raw CEL evaluation errors can include claim values and are currently logged. The other two comments cover configuration validation and generated API documentation.
Checklist
- Tests: Unit, integration, operator E2E, lint, and the reported CI checks are green.
- Docs: Architecture, Swagger, and CRD docs are included. One CRD description still reflects the pre-matcher behavior.
- Registry impact: None.
- Security: Authorization remains fail closed, but evaluation errors should be redacted before logging.
- Backwards compatibility: The fields are optional and additive, and CRD schema compatibility checks pass.
CEL evaluation errors can embed raw claim values (e.g. timestamp() on a malformed string quotes it verbatim), so the actor matcher's evaluation error is no longer logged or returned to the caller in its raw form. The AllowedActors/AllowMayAct doc comments are also updated to describe actorMatcher as an additional authorization signal, since the generated CRD schema and operator API docs still described the pre-matcher, allowlist-only behavior. Addresses review feedback on #6364.
samuv
left a comment
There was a problem hiding this comment.
Follow-up review
The CEL error redaction is correct and the regression test now exercises the actual value-leaking operator. The CRD descriptions are also updated consistently. I left one inline comment for the equivalent standalone auth-server description, which still feeds stale text into Swagger. The non-boolean result-type validation remains safe to defer to a tracked toolhive-core follow-up because runtime evaluation fails closed.
Checklist
- Tests: Focused regression coverage is appropriate. Several latest CI jobs are still running; completed lint, codegen, docs, security, and CRD compatibility checks pass.
- Docs: CRD docs are fixed. One standalone Swagger source comment still needs the matching update.
- Registry impact: None.
- Security: Raw CEL errors and claim values are now redacted.
- Backwards compatibility: No new compatibility concern found.
The AllowMayAct comment on the TrustedIssuer wire type still said may_act only bypasses AllowedActors, which fed stale text into docs/server/swagger.*. Addresses follow-up review feedback on #6364.
samuv
left a comment
There was a problem hiding this comment.
Approved. The CEL error redaction is safe and covered by a regression test, the CRD and standalone Swagger documentation are aligned, and the static boolean result-type check is tracked in stacklok/toolhive-core#239 as a non-blocking follow-up.
Checklist:
- Tests: The required check passes. Unit tests, lint, documentation, code generation, security, and compatibility checks are green.
- Docs: Architecture, CRD, and Swagger documentation are aligned.
- Registry impact: None.
- Security: CEL evaluation errors no longer expose claim values.
- Backwards compatibility: The new matcher field remains optional and additive.
Summary
Trusted issuers currently authorize external-actor delegation only through
AllowedActors, a literal allowlist of one claim's values. That doesn'tscale to IdPs where the trust signal is a role, group, or combination of
claims rather than a single client identifier — every new value requires an
operator to edit and re-apply config. This adds
TrustedIssuer.ActorMatcher,an admin-authored CEL expression evaluated against the subject token's
complete signature-verified claims, as an additional (not replacement)
consent signal alongside
AllowedActors.TrustedIssuer.ActorMatcher/ CRD fieldTrustedIssuerConfig.actorMatcher,compiled once at validator construction via
toolhive-core's CEL engine(existing cost/length limits apply, so no new DoS surface).
resolveAllowedActorcall sites withresolveActorAuthorization,which tries the allowlist first and falls back to the matcher; either
signal is sufficient.
ValidatedClaims.ExternalActorAuthorizedand switchcheckDelegationConsent's external-actor case to key on it instead ofExternalActor != "", since a matcher-only authorization leavesExternalActorempty (a matcher need not resolve a single actor claim).RunConfig.Validate) andvalidator construction (defence in depth), matching the existing
ActorClaim/AllowedDelegateClientschecks.docs/arch/17-token-exchange-delegation.mdwith the new consentpath, CEL variable shape, and a misconfiguration warning (write the
predicate against an issuer-controlled identifier claim, not a
user-editable profile attribute).
Part of #6322 (actor matching only; client-assertion auth is separate
follow-up work).
Type of change
Test plan
task test)task test-e2e)task lint-fix)Added unit tests for CEL compilation (accepts any well-typed expression,
denies a non-bool result only at evaluation time), the multi-issuer
validator's authorization precedence (allowlist bypasses a false matcher,
matcher-only authorization with no allowlist, matcher evaluation failure
denies and doesn't leak token/claims into logs), and a new kind e2e suite
(
virtualmcp_trusted_issuer_actormatcher_test.go) proving both amatcher-only grant and a fail-closed rejection through a real
VirtualMCPServer/CRD deployment.
API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.ActorMatcheris a new optional field; no existing field changes shape.Does this introduce a user-facing change?
Yes — operators can now set
trustedIssuers[].actorMatcher(a CELexpression) on
MCPExternalAuthConfig/VirtualMCPServerembedded authserver config to authorize external-actor delegation by claim predicate
instead of (or in addition to) a literal
allowedActorslist.Special notes for reviewers
An expression that compiles but doesn't return
bool(e.g.claims.foowhere
foois a string) is only caught the first time a real tokenevaluates it — every token is then denied, since the expression can never
return
bool. This is a runtime error, not a config error, so it does notfail reconciliation; it's surfaced only via the per-token auth-server debug
log. Flagged this tradeoff explicitly in code comments since there's no
static way to check a CEL AST's result type through
toolhive-core'scel.Engine.🤖 Generated with Claude Code