Commit a044fcc6 authored by Sergio Gimenez's avatar Sergio Gimenez
Browse files

feat(fm): outbound EWBI client with partner bearer token

parent d6f5b85d
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -22,4 +22,5 @@ class PostgresPartnerRepo:
            our_client_id=row.our_client_id,
            our_client_secret_ref=row.our_client_secret_ref,
            token_endpoint=row.token_endpoint,
            base_url=row.base_url,
        )
+1 −0
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ partner_ops = Table(
    Column("id", PGUUID(as_uuid=True), primary_key=True, server_default=func.gen_random_uuid()),
    Column("mcc_mnc", String(10), nullable=False, unique=True),
    Column("oauth2_client_id", String(255), nullable=False, unique=True),
    Column("base_url", Text, nullable=False),
    Column("our_client_id", String(255)),
    Column("our_client_secret_ref", Text),
    Column("token_endpoint", Text),
+0 −0

Empty file added.

+68 −0
Original line number Diff line number Diff line
from uuid import UUID

import httpx

from federation_manager.domain.errors import (
    PartnerEndpointConfigurationError,
    PartnerRequestFailed,
)
from federation_manager.domain.models import EwbiResponse, PartnerOP
from federation_manager.domain.ports import PartnerTokenProviderPort


class HttpxEwbiClient:
    def __init__(
        self,
        client: httpx.AsyncClient,
        token_provider: PartnerTokenProviderPort,
    ) -> None:
        self._client = client
        self._token_provider = token_provider

    async def post(
        self,
        partner: PartnerOP,
        path: str,
        payload: dict[str, object],
        federation_correlation_id: UUID,
    ) -> EwbiResponse:
        url = self._url(partner, path)
        token = await self._token_provider.token_for(partner, scope="fed-mgmt")

        try:
            response = await self._client.post(
                url,
                headers={
                    "Accept": "application/json",
                    "Authorization": f"Bearer {token}",
                    "X-Federation-Correlation-ID": str(federation_correlation_id),
                },
                json=payload,
            )
        except httpx.HTTPError:
            raise PartnerRequestFailed(partner.id) from None

        if not response.content:
            body: object | None = None
        else:
            try:
                body = response.json()
            except ValueError:
                raise PartnerRequestFailed(partner.id) from None
        return EwbiResponse(status_code=response.status_code, body=body)

    @staticmethod
    def _url(partner: PartnerOP, path: str) -> str:
        if not path.startswith("/") or httpx.URL(path).is_absolute_url:
            raise ValueError("EWBI path must be an absolute path without a host")

        if not partner.base_url:
            raise PartnerEndpointConfigurationError(partner.id)
        try:
            base_url = httpx.URL(partner.base_url)
        except httpx.InvalidURL:
            raise PartnerEndpointConfigurationError(partner.id) from None
        if base_url.scheme != "https" or not base_url.host:
            raise PartnerEndpointConfigurationError(partner.id)

        return f"{partner.base_url.rstrip('/')}/{path.lstrip('/')}"
+6 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from federation_manager.adapters.database.partner_repo import PostgresPartnerRepo
from federation_manager.application.authentication import PartnerAuthenticator
from federation_manager.domain.ports import (
    EwbiClientPort,
    JwtValidatorPort,
    PartnerRepositoryPort,
    PartnerTokenProviderPort,
@@ -34,6 +35,11 @@ def get_partner_token_provider(request: Request) -> PartnerTokenProviderPort:
    return provider


def get_ewbi_client(request: Request) -> EwbiClientPort:
    client: EwbiClientPort = request.app.state.ewbi_client
    return client


def get_partner_authenticator(
    repo: Annotated[PartnerRepositoryPort, Depends(get_partner_repo)],
    jwt_validator: Annotated[JwtValidatorPort, Depends(get_jwt_validator)],
Loading