fix: scope GET /apps by caller app_provider_id via SRM

Summary

  • Scope CAMARA GET /apps by the caller's app_provider_id (JWT sub), so each user only sees apps they registered — not the whole tenant.
  • Pass app_provider_id on the internal SRM catalog list (?app_provider_id=…); do not forward tenant_id to SRM.
  • Keep CAMARA alignment: clients still only send optional x-correlator; identity comes from the token via Caller.

Context

CAMARA getApps has no filter params and only says return apps “the user has permission to view.” For this release, that means app provider scope (JWT sub), not org/tenant_id. SRM owns provider filtering on the catalog list; OEG extracts app_provider_id from the validated token and forwards it on the internal call.

Changes

  • Router: get_apps → service.get_apps(app_provider_id=caller.app_provider_id, …)
  • Service: remove scoping TODO; forward app_provider_id to SRM
  • SRM port/client + fake: get_apps(app_provider_id, x_correlator) with query param
  • Unit/endpoint/flow/SRM client tests updated

Out of scope

  • OEG list_by_tenant / registration ∩ SRM id join
  • Adding app_provider_id column on AppRegistration
  • Ownership checks on GET/DELETE /apps/{appId}

Merge request reports

Loading
+7 −2
Changes for src/open_exposure_gateway/adapters/http/srm_client.py: 7 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -199,10 +199,15 @@ class SRMClient:
        )
        return SRMNetworkCapability.model_validate(data)

    async def get_apps(self, x_correlator: str | None = None) -> list[SRMCatalogPayload]:
    async def get_apps(
        self, app_provider_id: str, x_correlator: str | None = None
    ) -> list[SRMCatalogPayload]:
        headers = {"x-correlator": x_correlator} if x_correlator else None
        data = await self._request(
            "GET", "/internal/catalog/service-specifications", headers=headers
            "GET",
            "/internal/catalog/service-specifications",
            params={"app_provider_id": app_provider_id},
            headers=headers,
        )
        return [SRMCatalogPayload.model_validate(a) for a in data]

+4 −1
Changes for src/open_exposure_gateway/api/camara/edge_application_management/v0_1_0_alpha_1/router.py: 4 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -121,7 +121,10 @@ async def get_apps(
    service: EdgeAppService,
    caller: Caller,
) -> Any:
    return await service.get_apps(x_correlator=caller.x_correlator)
    return await service.get_apps(
        app_provider_id=caller.app_provider_id,
        x_correlator=caller.x_correlator,
    )


@router.get(
+6 −7
Changes for src/open_exposure_gateway/application/services/edge_application_management_service.py: 6 added lines, 7 removed lines.
Original line number Diff line number Diff line
@@ -322,13 +322,12 @@ class EdgeApplicationManagementService:
                details=str(exc),
            ) from exc

    async def get_apps(self, x_correlator: Optional[str] = None) -> list[AppManifest]:
        # TODO: scope by caller.tenant_id/app_provider_id once JWT auth is wired in
        # (dependencies.py currently hardcodes both to "placeholder"). Every other
        # endpoint here already threads these through; this one doesn't yet, and
        # tenant_id isn't in SRM's catalog response, so filtering must happen via
        # app_registration_repo, not srm_client.get_apps.
        catalogs = await self.srm_client.get_apps(x_correlator=x_correlator)
    async def get_apps(
        self, app_provider_id: str, x_correlator: Optional[str] = None
    ) -> list[AppManifest]:
        catalogs = await self.srm_client.get_apps(
            app_provider_id=app_provider_id, x_correlator=x_correlator
        )
        try:
            return [build_app_manifest(catalog) for catalog in catalogs]
        except (ValueError, TypeError, IndexError) as exc:
+3 −1
Changes for src/open_exposure_gateway/ports/srm_port.py: 3 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -22,7 +22,9 @@ class SRMClientPort(Protocol):
        x_correlator: str | None,
    ) -> list[SRMZone]: ...

    async def get_apps(self, x_correlator: str | None) -> list[SRMCatalogPayload]: ...
    async def get_apps(
        self, app_provider_id: str, x_correlator: str | None
    ) -> list[SRMCatalogPayload]: ...

    async def get_app(self, app_id: UUID, x_correlator: str | None) -> SRMCatalogPayload: ...

+8 −2
Changes for tests/unit/fakes.py: 8 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -153,8 +153,14 @@ class FakeSRMClient:
    ) -> list[SRMZone]:
        return list(self.zones)

    async def get_apps(self, x_correlator: str | None = None) -> list[SRMCatalogPayload]:
        return [SRMCatalogPayload.model_validate(p) for p in self.catalog.values()]
    async def get_apps(
        self, app_provider_id: str, x_correlator: str | None = None
    ) -> list[SRMCatalogPayload]:
        return [
            payload
            for payload in (SRMCatalogPayload.model_validate(p) for p in self.catalog.values())
            if payload.service_specification.app_provider_id == app_provider_id
        ]

    async def get_app(self, app_id: UUID, x_correlator: str | None = None) -> SRMCatalogPayload:
        entry = self.catalog.get(str(app_id))
Loading
Loading