fix(fm): refuse partner tokens not issued for us

sequenceDiagram
  participant P as Partner FM
  participant KC as our Keycloak (realm federation)
  participant FM as our FM (EWBI)
  P->>KC: client_credentials, scope fed-mgmt
  KC-->>P: access token, aud "federation-manager" (audience mapper from the chart)
  P->>FM: EWBI call, Bearer token
  Note over FM: checks signature, issuer, expiry, fed-mgmt scope<br/>and now aud contains FM_KEYCLOAK_AUDIENCE
  FM-->>P: 2xx, or 401 authentication-failed when aud is missing or names someone else

FM validated inbound EWBI tokens for signature, issuer, expiry and the fed-mgmt scope, but never looked at aud. Any token our Keycloak minted for another client in the federation realm would have passed. The Helm chart already puts an audience mapper (aud: federation-manager) on every partner client and its docs say FM checks it; until now FM didn't.

FM_KEYCLOAK_AUDIENCE sets the expected audience and defaults to federation-manager, the value the chart's mapper writes, so existing deployments need no new setting. A token without that audience gets the same 401 authentication-failed as a bad scope. The CI realm (keycloak/realm-import.json) and the test fixture realm get the mapper too, so the integration tests run against tokens shaped like a deployment's.

One extra test pins that a refused InstallApp leaves no install record. Deleting a refused attempt depends on FM answering 404 to SRM's instance read.

Result
full suite, Postgres + NATS + Keycloak as in CI 437 passed, 1 skipped (432 on develop + 5 new)
mypy, ruff clean
testbed probe, ns oop: client in the federation realm without the mapper token has no aud; EWBI GET /{ctx}/health → 401 authentication-failed
same client after adding the mapper aud: federation-manager; FM gets past the token and answers 401 partner-unknown (the probe isn't a registered partner)
testbed, OP-1 portal → app on OP-2's Paris zone, all three operators running develop + this MR + feat/one-agreement-per-partner partner calls in both directions accepted; instance ready

D-paris-instance-ready

The unknown row is an older partner instance, deleted on OP-2 earlier. OEG !43 drops those rows from the list.

🤖 Generated with Claude Code

Merge request reports

Loading
Loading