Skip to content

fix(keycloak): propagate and verify audience mapper errors (#348) - #350

Merged
cwiklik merged 5 commits into
mainfrom
fix/348-audience-mapper-error-propagation
May 11, 2026
Merged

fix(keycloak): propagate and verify audience mapper errors (#348)#350
cwiklik merged 5 commits into
mainfrom
fix/348-audience-mapper-error-propagation

Conversation

@cwiklik

@cwiklik cwiklik commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Propagate ensureAudienceMapper errors instead of silently discarding them with _ =, so the controller sees failures and can act on them
  • Add verifyAudienceMapper defense-in-depth check that runs on every reconcile — GETs the scope's protocol mappers and re-creates the oidc-audience-mapper if missing (catches scopes broken by prior transient failures)
  • Make updateAudienceMapperIfNeeded return an error when no matching mapper is found (was a silent no-op)

Fixes #348

Changes

File What
internal/keycloak/audience.go Propagate errors on both code paths (existing + new scope); add verifyAudienceMapper; remove unused slog import
internal/keycloak/audience_test.go Add MapperFailurePropagated test (mapper 500 → error surfaces); add VerifyRecreatesMissingMapper test (missing mapper detected and re-created); update existing tests for verify GET call

Test plan

Unit tests — all 6 pass

=== RUN   TestEnsureAudienceScope
--- PASS: TestEnsureAudienceScope (0.00s)
=== RUN   TestEnsureAudienceScope_UpdatesStaleMapper
--- PASS: TestEnsureAudienceScope_UpdatesStaleMapper (0.00s)
=== RUN   TestEnsureAudienceScope_SkipsUpdateWhenCorrect
--- PASS: TestEnsureAudienceScope_SkipsUpdateWhenCorrect (0.00s)
=== RUN   TestEnsureAudienceScope_MapperFailurePropagated
--- PASS: TestEnsureAudienceScope_MapperFailurePropagated (0.00s)
=== RUN   TestEnsureAudienceScope_VerifyRecreatesMissingMapper
--- PASS: TestEnsureAudienceScope_VerifyRecreatesMissingMapper (0.00s)
=== RUN   TestEnsureAudienceScope_Disabled
--- PASS: TestEnsureAudienceScope_Disabled (0.00s)
PASS
ok  	github.com/kagenti/operator/internal/keycloak	1.051s

Kind cluster — local image build + deploy

Built operator image from this branch, loaded into Kind cluster via ctr import, rolled out kagenti-controller-manager deployment with imagePullPolicy: Never.

$ kubectl get pods -n kagenti-system -l control-plane=controller-manager
NAME                                          READY   STATUS    RESTARTS   AGE
kagenti-controller-manager-74d4cb5446-9nvt2   1/1     Running   0          27s

Kind cluster — new agent deploy validates fix

Deployed a new agent (weather-service-777) in team1 namespace via Sandbox CR with source build. The operator reconciled the new pod and created the audience scope with the correct mapper:

=== Audience scopes ===
  agent-team1-weather-service-777-aud  (id: 5d6c7190-c73e-41b4-b35b-b928bb5b9989)
  agent-team1-weather-service-aud      (id: 311fb6d9-40ec-4c9f-adf6-65704969f44f)

=== weather-service-777 mapper ===
  mapper: agent-team1-weather-service-777-aud
  type:   oidc-audience-mapper
  audience: spiffe://localtest.me/ns/team1/sa/weather-service-777

Both agents have correct audience scopes with mappers — no silent failures, no missing mappers.

Assisted-By: Claude Code

@cwiklik

cwiklik commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author

CI Note: Unit Test failure is pre-existing, not related to this PR

The failing test is mapWorkloadToAgentCards in indexers_test.go:115 — a Sandbox→AgentCard mapping test that errors with field label not supported: .spec.targetRef.name. This is unrelated to the keycloak audience mapper changes in this PR.

Main branch CI (ci.yaml) also shows this failure:

Run Branch Conclusion
25571428211 main failure
25571405507 main failure
25569991617 main success

All 6 keycloak audience tests pass (including 2 new ones added by this PR).

cwiklik added 2 commits May 11, 2026 14:20
…owing them

getOrCreateAudienceClientScope discarded errors from ensureAudienceMapper
on both code paths (existing and new scope), leaving audience scopes
without the oidc-audience-mapper and causing silent 401 failures.

Fixes #348

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: cwiklik <cwiklikj@gmail.com>
…ncile

verifyAudienceMapper runs after getOrCreateAudienceClientScope and GETs
the scope's protocol mappers to confirm the oidc-audience-mapper exists
with the correct audience. If missing (e.g. prior transient failure left
the scope without a mapper), it re-creates it. If stale, it updates it.

Also makes updateAudienceMapperIfNeeded return an error when no matching
mapper is found (previously a silent no-op).

Refs #348

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: cwiklik <cwiklikj@gmail.com>
@cwiklik
cwiklik force-pushed the fix/348-audience-mapper-error-propagation branch from 17f3429 to 581e41f Compare May 11, 2026 18:20
The fake IDP test server only handled POST to /protocol-mappers/models
but the new verifyAudienceMapper method also GETs mappers to confirm
they exist. Track created mappers and serve them on GET.

Signed-off-by: cwiklik <cwiklik@users.noreply.github.com>
Signed-off-by: cwiklik <cwiklikj@gmail.com>
@cwiklik
cwiklik force-pushed the fix/348-audience-mapper-error-propagation branch from 8818a6c to 0926d9c Compare May 11, 2026 18:44

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

Review Summary

Well-scoped fix for #348: replaces error-discarding _ = calls with proper propagation, adds a defense-in-depth verifyAudienceMapper that re-creates missing mappers on reconcile, and adds regression tests for both failure propagation and mapper re-creation. Tests look solid and all 15 CI checks pass.

Areas reviewed: Go (internal/keycloak)
Commits: 3, all DCO signed-off, conventional format, Assisted-By: per project policy
CI: all 15 checks green

Approving. Left a couple of non-blocking suggestions/nits inline.

Assisted-By: Claude Code

return nil
}
if mappers[i].Config == nil {
mappers[i].Config = make(map[string]string)

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.

suggestion: verifyAudienceMapper and updateAudienceMapperIfNeeded share ~25 lines of GET-and-parse logic (endpoint, auth header, read body, status check, unmarshal, match loop). Consider extracting a listAudienceMappers(ctx, token, realm, scopeID) ([]protocolMapperRep, error) helper — both callers would benefit and the two match-loop behaviors (update-only vs create-if-missing) become clearer. Not blocking.

}
slog.Debug("no matching audience mapper found for scope", "scope", scopeName, "scopeID", scopeID)
return nil
return fmt.Errorf("no matching audience mapper found for scope %q (scopeID %s)", scopeName, scopeID)

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.

suggestion: updateAudienceMapperIfNeeded now returns an error when no matching mapper is found. This changes behavior for the 409 Conflict path in ensureAudienceMapper: a 409 with no name match would previously be silently ignored, now it errors. This is the right direction for visibility, but worth a brief comment on the function (or in the error string) noting that "no match" is now treated as a real failure (e.g. case mismatch or Keycloak race) rather than the previous silent no-op — makes the behavior change easier to spot later.

return fmt.Errorf("keycloak update audience mapper: status %d: %s", resp.StatusCode, truncate(body, 256))
}

// verifyAudienceMapper is a defense-in-depth check that runs on every reconcile.

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.

nit: verifyAudienceMapper adds an extra GET /protocol-mappers/models on every reconcile for every audience-enabled scope. The defense-in-depth tradeoff is reasonable, but worth acknowledging the cost in the doc comment (e.g. "one extra GET per reconcile; accepted cost for catching scopes left broken by prior transient failures").

cwiklik added 2 commits May 11, 2026 18:22
… comments

Address PR #350 review feedback:
- Extract shared GET-and-parse logic into listAudienceMappers helper
- Document behavior change in updateAudienceMapperIfNeeded (no-match is
  now a real error, not a silent no-op)
- Acknowledge per-reconcile GET cost in verifyAudienceMapper doc comment

Signed-off-by: cwiklik <cwiklik@users.noreply.github.com>
Signed-off-by: cwiklik <cwiklikj@gmail.com>
The Sandbox test creates an AgentCard via direct client then queries via
the manager's cached client. On slower CI runners the cache hasn't synced
yet, causing a flaky failure. Use Eventually to poll until the cache
reflects the new object.

Signed-off-by: cwiklik <cwiklik@users.noreply.github.com>
Signed-off-by: cwiklik <cwiklikj@gmail.com>
@cwiklik
cwiklik merged commit 01f875a into main May 11, 2026
15 checks passed
cwiklik added a commit that referenced this pull request May 13, 2026
…pper (#358)

When a protocol mapper exists with the correct name but the wrong type
(not oidc-audience-mapper), the POST returns 409 and
updateAudienceMapperIfNeeded fails to find a matching mapper — entering
an infinite error loop that blocks audience scope propagation.

## Root Cause

The 409 Conflict from Keycloak means "a mapper with that name already
exists." But updateAudienceMapperIfNeeded only looks for mappers matching
BOTH Name == scopeName AND ProtocolMapper == "oidc-audience-mapper".
When the existing mapper has the right name but wrong type, the loop
skips it and falls through to "no matching audience mapper found."

This also prevents verifyAudienceMapper (defense-in-depth from PR #350)
from running, since getOrCreateAudienceClientScope returns early on the
error — no self-healing is possible.

## Fix

In updateAudienceMapperIfNeeded, after failing to find an
oidc-audience-mapper, perform a second pass looking for any mapper with
a matching name (regardless of type). If found, DELETE it via the
Keycloak Admin API, then re-POST the correct oidc-audience-mapper.

This is the minimal targeted fix — it handles the exact broken state
(wrong-type name collision) without restructuring the flow.

## Observed Symptoms

- Operator logs: "ensure audience mapper for existing scope ... no
  matching audience mapper found" repeating every few seconds
- Agent tokens lack the correct audience claim
- AuthBridge/Envoy rejects requests with 401 Unauthorized
- Affects fresh installs with operator v0.2.0-rc.4

Fixes #358

Signed-off-by: cwiklik <cwiklik@users.noreply.github.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: cwiklik <cwiklikj@gmail.com>
cwiklik added a commit that referenced this pull request May 13, 2026
…pper (#358)

When a protocol mapper exists with the correct name but the wrong type
(not oidc-audience-mapper), the POST returns 409 and
updateAudienceMapperIfNeeded fails to find a matching mapper — entering
an infinite error loop that blocks audience scope propagation.

## Root Cause

The 409 Conflict from Keycloak means "a mapper with that name already
exists." But updateAudienceMapperIfNeeded only looks for mappers matching
BOTH Name == scopeName AND ProtocolMapper == "oidc-audience-mapper".
When the existing mapper has the right name but wrong type, the loop
skips it and falls through to "no matching audience mapper found."

This also prevents verifyAudienceMapper (defense-in-depth from PR #350)
from running, since getOrCreateAudienceClientScope returns early on the
error — no self-healing is possible.

## Fix

In updateAudienceMapperIfNeeded, after failing to find an
oidc-audience-mapper, perform a second pass looking for any mapper with
a matching name (regardless of type). If found, DELETE it via the
Keycloak Admin API, then re-POST the correct oidc-audience-mapper.

This is the minimal targeted fix — it handles the exact broken state
(wrong-type name collision) without restructuring the flow.

## Observed Symptoms

- Operator logs: "ensure audience mapper for existing scope ... no
  matching audience mapper found" repeating every few seconds
- Agent tokens lack the correct audience claim
- AuthBridge/Envoy rejects requests with 401 Unauthorized
- Affects fresh installs with operator v0.2.0-rc.4

Fixes #358

Signed-off-by: cwiklik <cwiklik@users.noreply.github.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: cwiklik <cwiklikj@gmail.com>
@pdettori
pdettori deleted the fix/348-audience-mapper-error-propagation branch July 15, 2026 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

bug: ensureAudienceMapper silently swallows errors, leaving scopes without mappers

2 participants