Skip to content

Add CEL actor matching for trusted issuers - #6364

Merged
jhrozek merged 4 commits into
mainfrom
6322-actor-matcher
Aug 19, 2026
Merged

Add CEL actor matching for trusted issuers#6364
jhrozek merged 4 commits into
mainfrom
6322-actor-matcher

Conversation

@jhrozek

@jhrozek jhrozek commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Trusted issuers currently authorize external-actor delegation only through
AllowedActors, a literal allowlist of one claim's values. That doesn't
scale 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.

  • Add TrustedIssuer.ActorMatcher / CRD field TrustedIssuerConfig.actorMatcher,
    compiled once at validator construction via toolhive-core's CEL engine
    (existing cost/length limits apply, so no new DoS surface).
  • Replace resolveAllowedActor call sites with resolveActorAuthorization,
    which tries the allowlist first and falls back to the matcher; either
    signal is sufficient.
  • Add ValidatedClaims.ExternalActorAuthorized and switch
    checkDelegationConsent's external-actor case to key on it instead of
    ExternalActor != "", since a matcher-only authorization leaves
    ExternalActor empty (a matcher need not resolve a single actor claim).
  • Validate the expression at both config-time (RunConfig.Validate) and
    validator construction (defence in depth), matching the existing
    ActorClaim/AllowedDelegateClients checks.
  • Update docs/arch/17-token-exchange-delegation.md with the new consent
    path, 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

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

  • Unit tests (task test)
  • E2E tests (task test-e2e)
  • Linting (task lint-fix)
  • Manual testing (describe below)

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 a
matcher-only grant and a fail-closed rejection through a real
VirtualMCPServer/CRD deployment.

API Compatibility

  • This PR does not break the v1beta1 API, OR the api-break-allowed label is applied and the migration guidance is described above.

ActorMatcher is a new optional field; no existing field changes shape.

Does this introduce a user-facing change?

Yes — operators can now set trustedIssuers[].actorMatcher (a CEL
expression) on MCPExternalAuthConfig / VirtualMCPServer embedded auth
server config to authorize external-actor delegation by claim predicate
instead of (or in addition to) a literal allowedActors list.

Special notes for reviewers

An expression that compiles but doesn't return bool (e.g. claims.foo
where foo is a string) is only caught the first time a real token
evaluates 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 not
fail 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's
cel.Engine.

🤖 Generated with Claude Code

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
@github-actions github-actions Bot added the size/XL Extra large PR: 1000+ lines changed label Aug 18, 2026
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.05%. Comparing base (7a0c0a5) to head (a8dbcc1).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
...ver/server/tokenexchange/multi_issuer_validator.go 95.83% 1 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 18, 2026

@samuv samuv 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.

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.

Comment thread pkg/authserver/server/tokenexchange/multi_issuer_validator.go Outdated
Comment thread pkg/authserver/server/tokenexchange/multi_issuer_validator.go
Comment thread cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go
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.
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 19, 2026

@samuv samuv 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.

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.

Comment thread pkg/authserver/server/tokenexchange/multi_issuer_validator.go
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.
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 19, 2026

@samuv samuv 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.

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.

@jhrozek
jhrozek merged commit 3c41b20 into main Aug 19, 2026
53 checks passed
@jhrozek
jhrozek deleted the 6322-actor-matcher branch August 19, 2026 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra large PR: 1000+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants