Commit 3d5ad76c authored by Sergio Gimenez's avatar Sergio Gimenez
Browse files

docs(fm): receive GSMA callbacks through one router (ADR-0054)

FM hands every partner a partnerStatusLink but has no route behind it,
so a partner's onPartnerStatusEvent gets a 404, and ADR-0053's deploy
flow needs FM to receive onInstanceStatusEvent.

All OPG.04 callbacks share one shape (POST to a URL we chose, token with
fed-mgmt-notif), so FM gets one router under
/operatorplatform/federation/v1/callbacks/{kind}: check the scope, check
that federationContextId is an outbound context of the calling partner,
then run an idempotent per-kind handler. onPartnerStatusEvent goes
first.
The federation realm needs fed-mgmt-notif as an optional scope on
partner
clients, in FM's dev realm and in the platform Keycloak chart
parent 41f718ef
Loading
Loading
Loading
Loading
+93 −0
Changes for decisions/0054-fm-receives-gsma-callbacks-through-one-router.md: 93 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Decision Record
title: "ADR-0054: FM receives every GSMA callback through one router under /operatorplatform/federation/v1/callbacks/{kind}"
description: "FM receives every GSMA callback through one router under /operatorplatform/federation/v1/callbacks/{kind}"
tags: [adr]
timestamp: 2026-09-22T00:00:00Z
status: "proposed"
---

# ADR-0054: FM receives every GSMA callback through one router under `/operatorplatform/federation/v1/callbacks/{kind}`

**Status:** proposed
**Date:** 2026-09-22
**Affects:** [fm/requirements-and-design.md](/fm/requirements-and-design.md) (new §L.1a, callbacks FM receives), [fm/overview-and-architecture.md](/fm/overview-and-architecture.md) (partner authentication)

## Context

When FM federates with a partner as the originating OP, several OPG.04 operations make the partner
call FM back later: `CreateFederation` hands over a `partnerStatusLink`, `InstallApp` an
`appInstCallbackLink`, `ZoneSubscribe` an `availZoneNotifLink`. OPG.04 v6.0 (artifact v1.4.0)
defines eleven such callbacks, and every one has the same shape: a `POST` to a URL the originating
OP chose, authorised with the `notifClientCredentials` scheme and the `fed-mgmt-notif` scope.

FM already sends callbacks as the partner (`onInstanceStatusEvent` after an inbound `InstallApp`),
but it receives none. It hands every partner a `partnerStatusLink` today (`FM_PARTNER_STATUS_LINK`,
default `/operatorplatform/federation/v1/partner-status`) and nothing listens there, so a partner's
`onPartnerStatusEvent` gets a `404`. The deploy hand-off in ADR-0053 depends on FM receiving
`onInstanceStatusEvent` too.

Two more gaps turned up while checking this. REQ-FED-01 says FM validates `fed-mgmt-notif` on
inbound calls, but `PartnerAuthenticator` hardcodes `fed-mgmt`. And no `federation` realm defines a
`fed-mgmt-notif` scope, neither FM's dev realm nor the platform Keycloak chart in helm MR
oop/deploy/helm!12, so a partner can't obtain a callback token from us, and FM's own callback client
can't obtain one from a partner running the same setup.

## Decision

FM receives all GSMA callbacks through one router, and every callback goes through the same three
steps before a per-kind handler runs.

```mermaid
flowchart LR
    P[Partner OP] -->|"POST /operatorplatform/federation/v1/callbacks/{kind}<br/>Bearer token, scope fed-mgmt-notif"| R
    subgraph FM ["FM, originating side"]
        R[Callback router] --> A["Authenticate<br/>scope fed-mgmt-notif<br/>azp to an active partner"]
        A --> C["Resolve context<br/>federationContextId must be an<br/>outbound context of that partner"]
        C --> D{kind}
        D -->|partner-status| H1["onPartnerStatusEvent<br/>federation_contexts.status"]
        D -->|instance-status| H2["onInstanceStatusEvent<br/>federation_transactions"]
        D -->|zone-resource| H3["onZoneResourceUpdateEvent<br/>shared_resource_catalogues"]
    end
```

**URLs.** Callbacks live under `/operatorplatform/federation/v1/callbacks/{kind}`, with one `kind`
per callback: `partner-status`, `instance-status`, `zone-resource`, and more as FM starts calling the
operations that trigger them. OPG.04 leaves the URL to the originating OP; putting it under the
partner-facing prefix means the ingress already routes it to FM, with no new rule. The
`FM_PARTNER_STATUS_LINK` default moves to `/operatorplatform/federation/v1/callbacks/partner-status`.
Nothing received on the old URL, so the move breaks nothing.

**Authentication.** A callback needs a token from our `federation` realm carrying `fed-mgmt-notif`
(REQ-FED-01). A `fed-mgmt` token alone gets `401`: the two scopes separate "you may call our API"
from "you may notify us", which is the distinction OPG.04 draws. The `azp` claim resolves to an
active partner exactly as for other EWBI calls.

**Context check.** Every callback payload carries `federationContextId`. It must name an
**outbound** federation context belonging to the calling partner, meaning one FM created with them.
Anything else gets `404`, so no partner can touch a federation it isn't party to.

**Handlers** are idempotent, because partners may retry, and answer with the status code OPG.04 gives
each callback (`204` for `onPartnerStatusEvent` and `onInstanceStatusEvent`, `200` for
`onZoneResourceUpdateEvent`).

**Order of work.** `onPartnerStatusEvent` comes first: partners can already send it, and its effect
is specified (it updates `federation_contexts.status`, whose values mirror the callback's `Status`
enum). `onInstanceStatusEvent` follows with FM's outbound `InstallApp`, which creates the transaction
it updates, and publishing `event.fm.operation.completed` from it waits on ADR-0053.
`onZoneResourceUpdateEvent` waits for `shared_resource_catalogues`.

**Keycloak.** The `federation` realm defines a `fed-mgmt-notif` client scope and attaches it to each
partner client as an **optional** scope, beside the default `fed-mgmt`. Optional means a token only
carries it when the client asks, so an ordinary API token can't double as a callback token. This
applies to FM's dev realm and to the platform Keycloak chart.

## Consequences

Easier: each new callback is one handler, not a new endpoint with its own auth and ownership checks.
A partner can finally report federation status changes to us. The receiving side of ADR-0053's
deploy flow has a place to live.

Harder: the platform Keycloak chart needs a second client scope per partner client, which its
current template (one `clientScope` value) doesn't support. Partners federating with us have to
request `fed-mgmt-notif` when they call us back, which OPG.04 already requires of them.
+1 −0
Changes for decisions/index.md: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -53,3 +53,4 @@ the reasoning lives here. Never renumber; supersede instead (see the record-deci
* [ADR-0050](0050-oeg-federation-lifecycle-proxy-errors-are-gsma-problem-json.md) - OEG's federation-lifecycle proxy endpoints (§N.7, ADR-0046) return OPG.04 `application/problem+json` `ProblemDetails` for every error - FM's errors relayed unchanged, OEG's own 400/401/403/502/503 from a fixed template; ADR-0030's `ErrorInfo` rule is scoped to OEG's CAMARA northbound surface
* [ADR-0051](0051-oeg-fronts-federation-lifecycle-ewbi-byte-for-byte.md) - OEG fronts eight GSMA federation-lifecycle operations on its own platform namespace, `/platform/v1/federation/partners/{partnerOpId}/federations/...`, scoped by a registered `partnerOpId` (create/read/update/delete federation, fed-context-id lookup, zone list/subscribe/details/unsubscribe), forwarding the raw GSMA request/response byte-for-byte to FM including the query string, pinned to GSMA OPG.04 v6.0 / artifact v1.4.0; operator-triggered outbound only, separated from inbound EWBI at the ingress; keeps ADR-0046's sync-HTTP/stateless/no-DataBus transport and ADR-0050's error model (amends this ADR's own first version, which used the bare canonical OPG.04 paths and lost the partner addressing GSMA gets for free from the URL)
* [ADR-0053](0053-srm-fm-federated-lifecycle-handoff-over-the-databus.md) - SRM hands federated deploys and terminates to FM as `command.fm.service.deploy` / `.terminate` on `OOP_TASKS`, and FM reports the outcome as `event.fm.operation.completed` on `OOP_EVENTS`; zone lookups stay synchronous HTTP reads. `completion_basis` separates a confirmed deploy from a terminate the partner only accepted. SRM writes a `service_order` row for federated work but no `service_instance` row; FM waits 30 minutes by default for a deploy callback. Amends ADR-0001/0022 (FM consumes `command.fm.>`) and ADR-0046's DataBus scope; resolves G4
* [ADR-0054](0054-fm-receives-gsma-callbacks-through-one-router.md) - FM receives every OPG.04 callback on `/operatorplatform/federation/v1/callbacks/{kind}`: one router that checks the `fed-mgmt-notif` scope and that `federationContextId` is an outbound context of the calling partner, then a per-kind idempotent handler. `onPartnerStatusEvent` first; `FM_PARTNER_STATUS_LINK` moves to `/callbacks/partner-status`. The `federation` realm gets `fed-mgmt-notif` as an optional scope on partner clients
+27 −0
Changes for fm/requirements-and-design.md: 27 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -418,6 +418,33 @@ Likewise, `federation_transactions.federation_operation_id` is internal audit/co
must return the operation-specific identifiers defined by OPG.04 rather than a universal custom
`operationId` response.

### L.1a Callbacks FM Receives (ADR-0054)

As the originating OP, FM gives partners URLs to call back on. OPG.04 defines every callback as a
`POST` to that URL with a `notifClientCredentials` token carrying `fed-mgmt-notif`, so FM receives
them all through one router under `/operatorplatform/federation/v1/callbacks/{kind}`.

| Kind | OPG.04 callback | FM hands out the URL in | Handler effect | Success | Status |
|---|---|---|---|---|---|
| `partner-status` | `onPartnerStatusEvent` | `CreateFederation.partnerStatusLink` | updates `federation_contexts.status` from `federationStatus` | `204` | first |
| `instance-status` | `onInstanceStatusEvent` | `InstallApp.appInstCallbackLink` | updates `federation_transactions`; feeds `event.fm.operation.completed` (ADR-0053) | `204` | with outbound `InstallApp` |
| `zone-resource` | `onZoneResourceUpdateEvent` | `ZoneSubscribe.availZoneNotifLink` | updates `shared_resource_catalogues` | `200` | with the zone catalogue |

Before any handler runs, FM:

1. validates the Bearer JWT from our `federation` realm and requires the `fed-mgmt-notif` scope; a
   token with only `fed-mgmt` gets `401`;
2. resolves `azp` to an active partner, as for every EWBI call;
3. requires the payload's `federationContextId` to name an **outbound** federation context of that
   partner, and answers `404` otherwise.

Handlers are idempotent, since partners may retry. Errors use the OPG.04 `ProblemDetails` model like
the rest of the EWBI surface. `FM_PARTNER_STATUS_LINK` defaults to
`/operatorplatform/federation/v1/callbacks/partner-status`.

The `federation` realm must define `fed-mgmt-notif` and attach it to each partner client as an
optional client scope, next to the default `fed-mgmt`, so only tokens that ask for it carry it.

### L.2 Internal Admin API (Operator Management)

These endpoints are for operator/admin use (via Portal or internal tools). They are NOT exposed on the EWBI path. **Since ADR-0051,** the operator-triggered create/read/update/decommission lifecycle for partners and agreements normally arrives via OEG's synchronous federation-lifecycle proxy (§L.3b, REQ-FED-20) or, for establishment, FM's own bootstrap configuration; `POST`/`GET`/`PATCH`/`DELETE` on `/internal/partners` and `/internal/agreements` remain available for break-glass/migration use. All of `/internal/routing-rules` (create included: routing rules are outside that scope) is unaffected and stays the primary path.