Commit 2e4616c6 authored by Sergio Gimenez's avatar Sergio Gimenez
Browse files

feat(federation): relay FM's 501 for a partner secret it can't store

FM answers 501 when it has no key to store a typed our_client_secret. Allow it
on register and update, and document our_client_secret_set on Partner.
parent 17a4e8f8
Loading
Loading
Loading
Loading
Loading
+7 −5
Changes for src/open_exposure_gateway/api/gsma/federation_manager/v1_4_0/router.py: 7 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -85,6 +85,7 @@ _ERROR_RESPONSES: dict[int | str, dict[str, Any]] = {
    409: {"model": ProblemDetails, "description": "Conflict"},
    422: {"model": ProblemDetails, "description": "Unprocessable entity"},
    500: {"model": ProblemDetails, "description": "Internal server error"},
    501: {"model": ProblemDetails, "description": "Not implemented"},
    502: {"model": ProblemDetails, "description": "Bad gateway"},
    503: {"model": ProblemDetails, "description": "Service unavailable"},
    520: {"model": ProblemDetails, "description": "Unknown error"},
@@ -106,11 +107,12 @@ _FM_IDENTITY_STATUSES = frozenset({200, 500})

# The partner registry is FM's own API. FM answers a bad body with 400 and a registration
# with incomplete outbound credentials with 422; 409 means the mcc_mnc or oauth2_client_id
# is already taken. OEG checks partnerOpId itself, so FM never sees a malformed one.
# is already taken, and 501 that FM has no key to store a typed our_client_secret.
# OEG checks partnerOpId itself, so FM never sees a malformed one.
_FM_PARTNER_LIST_STATUSES = frozenset({200, 500})
_FM_PARTNER_REGISTER_STATUSES = frozenset({201, 400, 409, 422, 500})
_FM_PARTNER_REGISTER_STATUSES = frozenset({201, 400, 409, 422, 500, 501})
_FM_PARTNER_GET_STATUSES = frozenset({200, 404, 500})
_FM_PARTNER_UPDATE_STATUSES = frozenset({200, 400, 404, 409, 422, 500})
_FM_PARTNER_UPDATE_STATUSES = frozenset({200, 400, 404, 409, 422, 500, 501})

_PROBLEM_JSON = "application/problem+json"

@@ -286,7 +288,7 @@ async def list_partners(
    summary="Register a partner operator platform before federating with it",
    operation_id="register_partner",
    response_model=Partner,
    responses=_responses(400, 401, 409, 422, 500, 502, 503),
    responses=_responses(400, 401, 409, 422, 500, 501, 502, 503),
)
async def register_partner(
    request: Request,
@@ -333,7 +335,7 @@ async def get_partner(
    summary="Update a registered partner; fields left out are unchanged",
    operation_id="update_partner",
    response_model=Partner,
    responses=_responses(400, 401, 404, 409, 422, 500, 502, 503),
    responses=_responses(400, 401, 404, 409, 422, 500, 501, 502, 503),
)
async def update_partner(
    partnerOpId: PartnerOpId,
+2 −0
Changes for src/open_exposure_gateway/api/gsma/federation_manager/v1_4_0/schemas.py: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -245,6 +245,8 @@ class Partner(BaseModel):
    our_client_secret_ref: Optional[str] = None
    token_endpoint: Optional[Uri] = None
    status: PartnerStatus
    # FM stores a typed our_client_secret encrypted and only ever reports that one is set
    our_client_secret_set: bool = False


class PartnerList(RootModel[list[Partner]]):
+28 −1
Changes for tests/unit/test_federation_manager_endpoints.py: 28 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -60,7 +60,8 @@ _PARTNER_BODY = (
    b'{"id": "5f0c2a8e-3b1d-4c6e-9a7f-1d2e3f4a5b6c", "name": "Partner A", '
    b'"mcc_mnc": "208-01", "oauth2_client_id": "partner-a", '
    b'"base_url": "https://partner-a.example", "our_client_id": null, '
    b'"our_client_secret_ref": null, "token_endpoint": null, "status": "pending"}'
    b'"our_client_secret_ref": null, "token_endpoint": null, "status": "pending", '
    b'"our_client_secret_set": false}'
)
_REGISTRATION = (
    b'{"name": "Partner A", "mcc_mnc": "208-01", "oauth2_client_id": "partner-a", '
@@ -751,6 +752,32 @@ def test_partner_registration_is_not_validated_by_oeg(
    assert fake_fm_client.calls[0]["content"] == b'{"mcc_mnc": "x"}'


@pytest.mark.parametrize(
    ("http_method", "path"), [("post", "/partners"), ("patch", f"/partners/{_PARTNER}")]
)
def test_partner_secret_storage_unavailable_is_relayed(
    client: TestClient, fake_fm_client: _FakeFmClient, http_method: str, path: str
) -> None:
    """FM has no key to store a typed our_client_secret; the operator must see why."""
    problem = (
        b'{"type": "urn:oop:ewbi:error:partner-secret-storage-unavailable", '
        b'"title": "Partner Secret Storage Not Configured", "status": 501}'
    )
    fake_fm_client.response = httpx.Response(
        501, content=problem, headers={"content-type": "application/problem+json"}
    )

    response = client.request(
        http_method,
        f"{BASE_PATH}{path}",
        content=b'{"our_client_secret": "bbb222"}',
        headers={"content-type": "application/json"},
    )

    assert response.status_code == 501
    assert response.content == problem


@pytest.mark.parametrize(
    ("http_method", "path", "status"),
    [