Commit 20156ab3 authored by Sergio Gimenez's avatar Sergio Gimenez
Browse files

docs(fm): one agreement per partner, validity pushed to partners, token audience

Brings the FM spec in line with FM !39, !40 and !41.

Agreements (requirements-and-design, the agreement entity and its
internal API section; ADR-0051's relay table): a partner has one
agreement, edited in place. POST /internal/agreements answers 409
agreement-exists when the partner already has one, and PATCH changes
any field but the partner, with the create checks run on the merged
result. Partners that had several keep the one FM applied; the rest
become expired. Replaces "zones and terms take a new agreement; the
newest valid one applies".

Zone sync (REQ-FED-04): an agreement offers nothing outside valid_from
and valid_until. FM records what each federated partner was last told
and sends ZONES ADD/REMOVE for the difference on every agreement write,
when a validity boundary passes, and on a periodic check that also
resends missed callbacks (FM_ZONE_OFFER_SWEEP_SECONDS). A withdrawn zone
releases its subscription in both directions. The partner's platformCaps
are kept on partner_ops.supported_apis, which is no longer "not built
yet".

Persistence model: the one-agreement-per-partner index (migration 0008),
and partner_ops.supported_apis and federation_contexts.offered_zone_ids
(migration 0009).

Inbound EWBI authentication also checks the token audience: aud must
name federation-manager (FM_KEYCLOAK_AUDIENCE), which our Keycloak's
audience mapper adds to partner tokens. Overview, implementation and
Workflow 1 updated.

Co-Authored-By: default avatarClaude Opus 5.5 <noreply@anthropic.com>
parent 7a3f7278
Loading
Loading
Loading
Loading
+2 −2
Changes for decisions/0051-oeg-fronts-federation-lifecycle-ewbi-byte-for-byte.md: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -9,7 +9,7 @@ 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"), 2026-10-13 to add the identity read, and 2026-10-15 to add agreements and our zone list (see "Agreements and our zones"). 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.
**Date:** 2026-09-08 (amended 2026-09-15, 2026-09-24, 2026-10-13, 2026-10-15)
**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)

@@ -96,7 +96,7 @@ into FM. OEG proxies it the same way, plus the read the operator needs to pick z
| 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 its `status`, usage limits or validity; other terms take a new agreement |
| `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
+3 −2
Changes for fm/implementation-and-delivery.md: 3 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -70,6 +70,7 @@ Settings are environment variables with the `FM_` prefix (`core/config.py`; a `.
| `FM_SRM_INTERNAL_URL` | `http://localhost:8081` | SRM internal HTTP base URL |
| `FM_KEYCLOAK_ISSUER` | `http://localhost:8090/realms/federation` | Issuer partners' tokens must carry |
| `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` |
| `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 |
@@ -115,7 +116,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`) 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 +263,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.

+2 −2
Changes for fm/overview-and-architecture.md: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -121,7 +121,7 @@ sequenceDiagram
    P->>F: EWBI request<br/>Authorization: Bearer JWT
    F->>K: Fetch JWKS on cache miss / key rotation
    K-->>F: Public signing keys
    Note over F: Verify signature, issuer, expiry, and fed-mgmt locally
    Note over F: Verify signature, issuer, expiry, audience and fed-mgmt locally
    F->>D: Find partner by JWT azp = oauth2_client_id
    D-->>F: Partner profile and status
    Note over F: Agreement authorization happens after authentication
@@ -215,7 +215,7 @@ A partner operator requests deployment of an edge application on our infrastruct
- **Trigger:** Partner OP calls OPG.04 `POST /operatorplatform/federation/v1/{federationContextId}/application/lcm` (`InstallApp`).
- **Main flow:**
  1. The partner-facing ingress routes `/operatorplatform/federation/v1/*` to FM.
  2. FM validates the Bearer JWT (signature, issuer, expiry, `fed-mgmt` scope) and maps its `azp` claim to an active partner in `partner_ops`.
  2. FM validates the Bearer JWT (signature, issuer, expiry, audience, `fed-mgmt` scope) and maps its `azp` claim to an active partner in `partner_ops`.
  3. FM resolves `federationContextId` to an inbound federation context of that partner. A repeated `Idempotency-Key` with the same request returns the original `202`; with a different request it is rejected.
  4. FM checks that the agreement permits deploying on the requested zone, and resolves the application to a local service specification: from the partner's EWBI onboarding first (ADR-0068), otherwise from the agreement's app mapping (ADR-0018).
  5. FM writes a `federation_transactions` row (`pending`) and publishes `command.srm.service.deploy` with `source=federation`, `federation_partner_ref` set, and the partner's id as `app_provider_id` (ADR-0018).
+8 −4
Changes for fm/persistence-model.md: 8 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -60,7 +60,7 @@ references; it never stores SRM inventory, placement, or capacity.

**PartnerOP** — an external operator platform FM federates with over the EWBI. Holds EWBI base
URL, the partner's OAuth2 client id (the `azp` claim we match inbound tokens against), the client
credentials and token endpoint we use for outbound calls, supported API families and SLA limits (not built yet), and
credentials and token endpoint we use for outbound calls, the platform capabilities the partner states (OPG.04 `platformCaps`), SLA limits (not built yet), and
connection status. Exactly one current row per partner in `partner_ops`; updates mutate in place
(no immutable version history in Release 2.0).

@@ -93,7 +93,7 @@ status, and sanitized request/response summaries. For asynchronous commands it i
### Relationships

```text
partner_ops            1 ──── * federation_agreements
partner_ops            1 ──── * federation_agreements   (at most one not expired)
partner_ops            1 ──── * federation_contexts
partner_ops            1 ──── * routing_rules
partner_ops            1 ──── * shared_resource_catalogues
@@ -152,7 +152,7 @@ anything owned by SRM is held only as a UUID reference.
| `our_client_secret_ref` | TEXT | | Mounted-file reference for the secret backing `our_client_id`. Mutually exclusive with a row in `partner_op_secrets` (ADR-0067) |
| `token_endpoint` | TEXT | | Partner Authorization Server token endpoint used for outbound Client-Credentials |
| `partner_ca_cert_path` | TEXT | | Optional CA bundle for ordinary HTTPS server-certificate verification; not an mTLS identity field |
| `supported_apis` | JSONB | NOT NULL, DEFAULT '[]' | Array of API family identifiers the partner supports |
| `supported_apis` | JSONB | NOT NULL, DEFAULT '[]' | The partner's OPG.04 `platformCaps`, from its `CreateFederation` answer and refreshed by every `GetFederationDetails`; zones it offers carry them |
| `sla_config` | JSONB | DEFAULT '{}' | SLA parameters (max requests/sec, max concurrent) |
| `status` | VARCHAR(20) | NOT NULL, DEFAULT 'pending' | `pending`, `active`, `suspended`, `decommissioned` |
| `last_health_check_at` | TIMESTAMPTZ | | Last successful OPG.04 `GetFederationHealth` timestamp |
@@ -185,6 +185,7 @@ The secret backing `our_client_id` when the operator typed it in through the par
| `direction` | VARCHAR(10) | NOT NULL | `inbound` when we issued the context; `outbound` when partner issued it |
| `federation_context_id` | VARCHAR(255) | NOT NULL | Opaque OPG.04 `FederationContextId`; never assume UUID format |
| `status_callback_url` | TEXT | | `partnerStatusLink` used for required OPG.04 status callbacks |
| `offered_zone_ids` | JSONB | | Inbound only: our zone ids the partner was last told it is offered (by the `CreateFederation` answer, `GetFederationDetails`, or a delivered `ZONES` callback). `NULL` until FM first records it |
| `status` | VARCHAR(20) | NOT NULL | Lowercase mirror of the OPG.04 `Status` enum carried by the `onPartnerStatusEvent` callback: `available`, `locked`, `not_available`, `temporary_failure`, `failed`; plus `terminated` once `DeleteFederationDetails` has run (row kept for audit). No pending state: `CreateFederation` returns the `federationContextId` synchronously |
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | |
@@ -211,6 +212,7 @@ Constraint: `UNIQUE (partner_op_id, direction, federation_context_id)`.
**Indexes:**
- `idx_federation_agreements_partner` on `partner_op_id`
- `idx_federation_agreements_status_validity` on `(status, valid_from, valid_until)`
- `uq_federation_agreements_one_per_partner`, unique on `partner_op_id` where `status <> 'expired'`: one live agreement per partner; older ones stay as `expired` history

#### `routing_rules`

@@ -352,7 +354,7 @@ What FM `develop` has today; update as the schema changes.

| Table | In code | Notes |
|---|---|---|
| `partner_ops` | Yes, except `supported_apis`, `sla_config`, `last_health_check_at` and `partner_ca_cert_path` (not built yet) | |
| `partner_ops` | Yes, except `sla_config`, `last_health_check_at` and `partner_ca_cert_path` (not built yet) | |
| `partner_op_secrets` | Yes | ADR-0067 |
| `federation_contexts` | Yes | |
| `federation_agreements` | Yes | `service_spec_mappings` is the `InstallApp` fallback after EWBI onboarding (ADR-0068) |
@@ -370,4 +372,6 @@ What FM `develop` has today; update as the schema changes.
| 2.1-draft | 2026-09-08 | `federation_contexts` landed in code; status vocabulary enumerated from the OPG.04 `Status` enum. |
| 2.1-draft | 2026-09-08 | `external_resource_id` records the wire spelling of the external identifier; LCM audit columns (idempotency key, request fingerprint, callback) landed in code. |
| 2.1-draft | 2026-10-13 | Single source for FM's schema: §J.1 takes over the table definitions from requirements-and-design §K.1, adding `partner_op_secrets` (ADR-0067) and `federation_agreements.zone_resources`. §J.2 subjects corrected for ADR-0051 and ADR-0053. Implementation status brought up to date with FM `develop`. |
| 2.1-draft | 2026-10-16 | `federation_agreements`: partial unique index `uq_federation_agreements_one_per_partner`, so a partner has one agreement that isn't `expired` (migration `0008_one_agreement_per_partner` marks the extra ones `expired`). |
| 2.1-draft | 2026-10-16 | `partner_ops.supported_apis` built (the partner's `platformCaps`); `federation_contexts.offered_zone_ids` added (migration `0009_zone_offer_state`). |
| 2.1-draft | 2026-10-13 | Full audit against FM `develop`: shared-resource types, synchronous-query audit order, secret storage, Alembic, operator access to `/internal/*` and topology-hiding names corrected; module tree moved to implementation-and-delivery §O; partner SLA/API columns and rate limiting marked not built yet. |
+23 −11

File changed.

Preview size limit exceeded, changes collapsed.