Changes for decisions/0051-oeg-fronts-federation-lifecycle-ewbi-byte-for-byte.md: 21 added lines, 3 removed lines.
Original line number
Diff line number
Diff line
@@ -9,8 +9,8 @@ status: "accepted"
# ADR-0051: OEG fronts eight GSMA federation-lifecycle operations on its own platform namespace, scoped by partner id, forwarding the raw payload byte-for-byte to FM (operator-triggered outbound only)
**Status:** accepted — amended 2026-09-15 after FM-side review of the first version, and 2026-09-24 to add partner registration to the OEG proxy (see "Partner registration and our identity"), and 2026-10-13 to add the identity read. Originally drafted as ADR-0048 (and its error-model companion as ADR-0047); renumbered to 0051/0050 at 2026-09-16 to clear the collision with `develop`'s ADR-0047 (terminal-record-read) and `docs/platform-roles`'s ADR-0048 (platform-roles) — content unchanged by the renumbering.
**Status:** accepted — amended 2026-09-15 after FM-side review of the first version, and 2026-09-24 to add partner registration to the OEG proxy (see "Partner registration and our identity"), 2026-10-13 to add the identity read, and 2026-10-15 to add agreements and our zone list (see "Agreements and our zones"), and 2026-10-16 so a partner's one agreement is edited in place (the `PATCH` row there). Originally drafted as ADR-0048 (and its error-model companion as ADR-0047); renumbered to 0051/0050 at 2026-09-16 to clear the collision with `develop`'s ADR-0047 (terminal-record-read) and `docs/platform-roles`'s ADR-0048 (platform-roles) — content unchanged by the renumbering.
**Affects:**[oeg/architecture.md](/oeg/architecture.md), [oeg/interface-contract.md](/oeg/interface-contract.md), [fm/requirements-and-design.md](/fm/requirements-and-design.md), [fm/implementation-and-delivery.md](/fm/implementation-and-delivery.md), [fm/overview-and-architecture.md](/fm/overview-and-architecture.md), [platform/architecture.md](/platform/architecture.md), amends this ADR's own first version, restores the namespace part of [ADR-0046](/decisions/0046-oeg-fronts-federation-lifecycle-ewbi-as-sync-proxy.md) it had superseded, builds on [ADR-0050](/decisions/0050-oeg-federation-lifecycle-proxy-errors-are-gsma-problem-json.md), [ADR-0042](/decisions/0042-ewbi-oauth2-client-credentials-mtls-deferred.md)
## Context
@@ -80,12 +80,30 @@ so OEG proxies it on the same namespace and under the same rules as the eight op
There is no `DELETE`: partners are retired via `status`, never removed (FM §L.2). The body is FM's
own L.2 partner schema, not a GSMA schema — it is forwarded byte-for-byte like every other operation
here, and since it can carry the partner's client secret (ADR-0067), the no-body-logging rule below
applies to it unchanged. Federation agreements and routing rules stay on FM's L.2 API.
applies to it unchanged. Routing rules stay on FM's L.2 API.
`GET /identity` exists because FM accepts `CreateFederation` only with our own identity
(`partnerStatusLink`, `origOPFederationId`, `origOPCountryCode`, and MNCs among ours) and fills in
the optional fields left out; the operator reads those values here instead of typing them.
### Agreements and our zones (added 2026-10-15)
A partner sees none of our zones and can deploy nothing until an agreement grants them (FM §L.2,
REQ-FED-05). The hosting operator records that agreement, so it belongs in the same operator
workflow as registering the partner; without a relay the only way to write one was `kubectl exec`
into FM. OEG proxies it the same way, plus the read the operator needs to pick zones:
| OEG path (under `/platform/v1/federation`) / FM path (under `/internal`) | FM action |
|---|---|
| `POST /agreements` | record what a partner may use here: our zones, the GSMA terms offered in each, the requests allowed, validity |
| `PATCH /agreements/{agreementId}` | change any of its terms in place: a partner has one agreement (a second `POST` answers `409`) |
| `GET /local-zones` | our own zones, the ones an agreement can offer (partner zones left out) |
Agreements are read embedded in `GET /partners/{partnerOpId}`, as FM §L.2 already had it; there is
no agreement list or `DELETE`, since agreements are retired through `status`. The bodies are FM's
L.2 schema, forwarded byte-for-byte; FM validates them and answers a bad one with a `400`
`ProblemDetails` naming the field, like a partner registration.
### The eight operations
OEG serves these under its own platform namespace, `/platform/v1/federation`, gated by an operator
**Amends:** ADR-0001 and ADR-0022, where they make SRM the only consumer of `OOP_TASKS`; ADR-0046, where it keeps the DataBus to `command.srm.*` / `event.srm.*`
@@ -120,8 +120,17 @@ isn't built for request/reply.
| `command.fm.service.terminate` | `OOP_TASKS` | the `SrmServiceTerminateV1` SRM received, unshaped | the instance is absent from SRM's local inventory (partner-hosted; SRM never wrote a partner `service_instance`) |
The envelope keeps the original `operation_id` and `correlation_id`. SRM does no GSMA shaping; FM
alone maps to `InstallApp` and `RemoveApp` (ADR-0044). Terminate has no lookup step: FM resolves the
instance from its own `federation_transactions` records.
alone maps to `InstallApp` and `RemoveApp` (ADR-0044). FM resolves the partner instance from
its own `federation_transactions` records.
**2026-10-15 amendment:** before accepting a terminate for an instance absent locally, SRM
reads `GET /internal/federated-service-instances/{service_instance_id}` to retain its `zone_id`
in the order-local hand-off. This lets the terminal SRM event satisfy §C.2 without a partner
`service_instance` row. If FM doesn't know the instance, SRM fails before starting; if FM can't be reached, the command
is redelivered, not failed. The order's
`target_service_instance_id` stays null because that column references local inventory. The
request to FM remains the original command; SRM does not select an EWBI target or store partner
Changes for decisions/0068-fm-onboards-apps-at-partners-over-ewbi-before-installapp.md: 18 added lines, 5 removed lines.
Original line number
Diff line number
Diff line
@@ -9,8 +9,8 @@ status: "proposed"
# ADR-0068: FM onboards an application at a partner over the EWBI before the first `InstallApp`, and the partner resolves `InstallApp` from that onboarding
**Status:** proposed
**Date:** 2026-09-25
**Status:** proposed — amended 2026-10-15 to onboard Helm units (see "Helm units")
Changes for decisions/index.md: 3 added lines, 3 removed lines.
Original line number
Diff line number
Diff line
@@ -51,8 +51,9 @@ the reasoning lives here. Never renumber; supersede instead (see the record-deci
*[ADR-0046](0046-oeg-fronts-federation-lifecycle-ewbi-as-sync-proxy.md) - operator-triggered federation establishment and zone subscription proxy synchronously through OEG to FM (no DataBus, no internal schema); scope and payload unchanged from ADR-0043, inbound EWBI stays entirely with FM (supersedes ADR-0043's transport; scope/payload/namespace corrected by ADR-0051)
*[ADR-0047](0047-terminal-record-read-follows-camara-schema.md) - terminal-record read behaviour follows each family's own vendored CAMARA schema: Traffic Influence returns 200 with `state: deleted`, QoD stays 404 (amends ADR-0034's blanket 404 rule and its 204 delete code)
*[ADR-0048](0048-platform-roles.md) - three realm roles (app-provider, operator, platform-admin); new users get app-provider but call only the API products granted to them (each CAMARA scope gated by a product role); tenancy is the `organization` claim, else `sub`; development mode switches login off on localhost (amends ADR-0006's tenant identity and ADR-0035's entitlement note)
*[ADR-0049](0049-data-lab-standalone-profile-shared-ingress-and-iam.md) - Data Lab is a standalone compose profile behind the platform Nginx and Keycloak (realm `dataspace`); no DataBus/SRM/OEG coupling in the catalogue release
*[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; also proxies partner registration (`POST`/`GET`/`PATCH``/platform/v1/federation/partners[/{partnerOpId}]` onto FM's L.2, amended 2026-09-24) and our own federation identity (`GET /platform/v1/federation/identity`, amended 2026-10-13); 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-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; also proxies partner registration (`POST`/`GET`/`PATCH``/platform/v1/federation/partners[/{partnerOpId}]` onto FM's L.2, amended 2026-09-24) our own federation identity (`GET /platform/v1/federation/identity`, amended 2026-10-13), and agreements with the zones they can offer (`/platform/v1/federation/agreements[/{agreementId}]`, `/platform/v1/federation/local-zones`, amended 2026-10-15); 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` and tracks pending partner targets in order-local `federated_handoff` (no partner zone/instance inventory); `payload_snapshot` stays frozen command audit; completes the order from FM's answer. 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
*[ADR-0055](0055-topology-discovery-is-a-separate-adapter-interface.md) - topology discovery is a separate TopologyDiscoveryAdapter interface, not optional methods on CapabilityExecutionAdapter (amends ADR-0027)
@@ -68,8 +69,7 @@ the reasoning lives here. Never renumber; supersede instead (see the record-deci
*[ADR-0065](0065-portal-sessions-keyed-by-the-cookie.md) - portal login sessions live in PostgreSQL, tokens encrypted with a per-session key only the cookie holds; login attempts are single-use rows; a session lasts as long as Keycloak accepts its refresh token
*[ADR-0066](0066-api-clients-through-keycloak-client-registration.md) - developers' API clients are Keycloak clients registered by a narrow provisioner, products fixed at creation within the creator's grants, secret shown once, registration token encrypted under one app-wide key; a refused token on delete is reported; no reconciliation with Keycloak in Release 2
*[ADR-0067](0067-partner-client-secret-encrypted-in-fm-db.md) - proposed: FM accepts the partner client secret typed in through the registry and stores it AES-GCM encrypted in fm_db (`partner_op_secrets`) under one app-wide key, never returned; `our_client_secret_ref` files stay as the other source, one per partner; no key → 501 (amends FM #21's no-secret-accepted criterion)
*[ADR-0068](0068-fm-onboards-apps-at-partners-over-ewbi-before-installapp.md) - FM onboards an application at a partner over the EWBI (`UploadFile`, `UploadArtefact`, `OnboardApplication`) on the first `InstallApp` of that specification version, with derived GSMA ids, public repositories and container units only for now, and `flavourId` from the zone subscription; the partner creates the specification in its SRM under the originating partner as tenant and resolves `InstallApp` from that onboarding, replacing ADR-0018's manual app mapping. Adds `onApplicationOnboardStatusEvent` to ADR-0054's router
*[ADR-0049](0049-data-lab-standalone-profile-shared-ingress-and-iam.md) - Data Lab is a standalone compose profile behind the platform Nginx and Keycloak (realm `dataspace`); no DataBus/SRM/OEG coupling in the catalogue release
*[ADR-0068](0068-fm-onboards-apps-at-partners-over-ewbi-before-installapp.md) - FM onboards an application at a partner over the EWBI (`UploadFile`, `UploadArtefact`, `OnboardApplication`) on the first `InstallApp` of that specification version, with derived GSMA ids, public repositories only for now, container units and Helm charts (images read from the chart's `values.yaml`, amended 2026-10-15), and `flavourId` from the zone subscription; the partner creates the specification in its SRM under the originating partner as tenant and resolves `InstallApp` from that onboarding, replacing ADR-0018's manual app mapping. Adds `onApplicationOnboardStatusEvent` to ADR-0054's router
*[ADR-0069](0069-data-lab-helm-chart-alternative-to-compose.md) - The Data Lab can also be deployed with the OOP Helm chart: off by default, UI on a NodePort, one Virtuoso, hub and search cluster-internal
*[ADR-0070](0070-data-lab-piveau-hub-and-6gdali-map.md) - Data Lab adopts piveau-hub as catalogue engine and the 6G-DALI Metadata Application Profile (DCAT-AP 3.0 + DQV + PROV-O) as normative metadata profile
*[ADR-0071](0071-data-lab-own-version-outside-release-pin.md) - data-lab/ specs are released as 0.1.0 and are excluded from the Release 2 pin (extends ADR-0021 exceptions)
| `FM_KEYCLOAK_JWKS_URL` | unset | Where FM fetches the signing keys when it can't reach the issuer URL; unset means the issuer's JWKS |
| `FM_KEYCLOAK_AUDIENCE` | `federation-manager` | Audience partner tokens must carry in `aud`. Not built yet |
| `FM_FEDERATION_ID`, `FM_COUNTRY_CODE`, `FM_MCC`, `FM_MNCS` | `oop-local`, `ES`, `214`, `07` | Our OPG.04 identity, sent on `CreateFederation` and served on `GET /internal/identity` |
| `FM_PLATFORM_CAPS` | `serviceAPIs` | Platform capabilities we advertise |
| `FM_PARTNER_STATUS_LINK` | `http://127.0.0.1:8082/operatorplatform/federation/v1/callbacks/partner-status` | Callback URL handed to partners; a deployment sets the URL partners can reach |
| `FM_INSTANCE_STATUS_LINK` | `http://127.0.0.1:8082/operatorplatform/federation/v1/callbacks/instance-status` | Same, for `onInstanceStatusEvent` after our `InstallApp` |
| `FM_AVAIL_ZONE_NOTIF_LINK` | unset | `availZoneNotifLink` for zone notifications |
| `FM_ALLOW_INSECURE_PARTNER_ENDPOINTS` | `false` | Allow plain-HTTP partner endpoints; development only |
| `FM_ZONE_OFFER_SWEEP_SECONDS` | `600` | How often every federated partner's zone offer is re-checked, besides each agreement write and each `valid_from`/`valid_until` (REQ-FED-04). Not built yet |
| `FM_BOOTSTRAP_FEDERATION` | `false` | At startup, federate with every active partner that has no outbound context (ADR-0043) |
| `FM_PARTNER_SECRET_KEY` | unset | Key (base64url, 32 bytes) that encrypts typed-in partner secrets; unset, FM accepts only secret files (ADR-0067) |
@@ -115,7 +117,7 @@ database (`adapters/database/`) and DataBus (`adapters/databus/`) modules.
FM authorizes E/WBI requests with OAuth2 Client-Credentials + signed JWT. Our Keycloak `federation` realm issues tokens for inbound partner calls; each partner's Authorization Server issues tokens for calls from our FM to that partner. FastAPI dependencies validate inbound Bearer tokens; `httpx` fetches and caches outbound tokens.
**Inbound:** a FastAPI dependency (`api/security.py`) reads the `Authorization: Bearer` header and `PartnerAuthenticator` validates the JWT signature against Keycloak's JWKS endpoint (cached, refreshed on `kid` miss), and checks issuer, expiry, and the `fed-mgmt` scope. The validated token's client identity resolves to a partner via `partner_ops`.
**Inbound:** a FastAPI dependency (`api/security.py`) reads the `Authorization: Bearer` header and `PartnerAuthenticator` validates the JWT signature against Keycloak's JWKS endpoint (cached, refreshed on `kid` miss), and checks issuer, expiry, audience (`FM_KEYCLOAK_AUDIENCE`, not built yet) and the `fed-mgmt` scope. The validated token's client identity resolves to a partner via `partner_ops`.
**Outbound:** Callers depend only on `PartnerTokenProviderPort`. The current adapter reads the partner secret from the mounted file named by `our_client_secret_ref` or, when the operator typed the secret in through the registry, decrypts it from `partner_op_secrets` (ADR-0067); it then requests a token from `token_endpoint`, caches it until near expiry, and the EWBI client attaches it as a Bearer header:
@@ -262,7 +264,7 @@ remove the FM fleet from load balancing. Tracking it (`fm_partner_federation_hea
### S.2 Integration Tests
-**FM → SRM flow:** Use Docker Compose with FM, SRM (MockAdapter), PostgreSQL, and NATS. Verify that an inbound EWBI deploy request flows through to `event.srm.operation.completed`.
-**Inbound OAuth2:** Verify JWT signature, issuer, expiry, `fed-mgmt` scope, and `azp` → `oauth2_client_id` partner resolution against a test Keycloak realm.
-**Inbound OAuth2:** Verify JWT signature, issuer, expiry, audience, `fed-mgmt` scope, and `azp` → `oauth2_client_id` partner resolution against a test Keycloak realm.
-**Outbound OAuth2:** Verify token acquisition, near-expiry caching, concurrent refresh serialization, mounted-secret rotation, token acquisition with a stored encrypted secret, and Bearer-header injection against a mock partner Authorization Server.
-**DataBus integration:** Verify `command.srm.*` publishing and `event.srm.operation.completed` consumption round-trip.