Loading portal-gui/src/features/api-clients/api-clients.tsx +1 −1 Changes for portal-gui/src/features/api-clients/api-clients.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -48,7 +48,7 @@ export function ApiClientsPage() { /> ) : ( <> <ApiClientsNotice /> {unavailable === false && <ApiClientsNotice />} <PageState loading={loading} error={error} Loading src/portal_service/adapters/keycloak/provisioner_token.py +7 −0 Changes for src/portal_service/adapters/keycloak/provisioner_token.py: 7 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,8 +4,12 @@ import time from collections.abc import Callable import httpx import structlog from portal_service.adapters.keycloak import _http from portal_service.domain.errors import ProvisionerRejectedError logger: structlog.BoundLogger = structlog.get_logger(__name__) _RENEW_BEFORE_EXPIRY_SECONDS = 30 _WHAT = "Provisioner token request" Loading Loading @@ -45,6 +49,9 @@ class ProvisionerToken: "client_secret": self._client_secret, }, ) if response.status_code in (400, 401): logger.error("provisioner credentials rejected", status=response.status_code) raise ProvisionerRejectedError("Keycloak rejected the provisioner credentials") _http.ensure_ok(response, what=_WHAT) parsed = _http.parse(_http.TokenResponse, response, what=_WHAT) self._token = parsed.access_token Loading src/portal_service/api/api_clients/routes.py +6 −0 Changes for src/portal_service/api/api_clients/routes.py: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ from portal_service.domain.errors import ( CreatorNotAllowedError, IdentityProviderUnavailableError, ProductNotHeldError, ProvisionerRejectedError, ) api_clients_router = APIRouter(prefix="/api/api-clients", tags=["api-clients"]) Loading @@ -46,6 +47,11 @@ def _answering_refusals() -> Iterator[None]: yield except tuple(_REFUSALS) as exc: raise HTTPException(_REFUSALS[type(exc)], str(exc)) from exc except ProvisionerRejectedError as exc: raise HTTPException( status.HTTP_503_SERVICE_UNAVAILABLE, "API client provisioning is misconfigured: ask the operator to check it", ) from exc except IdentityProviderUnavailableError as exc: raise HTTPException( status.HTTP_503_SERVICE_UNAVAILABLE, "The identity provider is unavailable" Loading src/portal_service/api/dependencies.py +4 −2 Changes for src/portal_service/api/dependencies.py: 4 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -269,10 +269,11 @@ def get_api_client_service( ApiClientServiceDep = Annotated[ApiClientService, Depends(get_api_client_service)] def get_dev_user() -> TokenClaims: def get_dev_user(catalog: ApiProductsDep) -> TokenClaims: """ Stands in for `get_current_user` when AUTH__SECURITY_MODE=none: every request acts as this user, with every role, and no login or Keycloak is needed. acts as this user, with every role and every API product, and no login or Keycloak is needed. """ return TokenClaims( sub="dev-user", Loading @@ -280,5 +281,6 @@ def get_dev_user() -> TokenClaims: exp=0, iat=0, preferred_username="dev", scope=" ".join(sorted(catalog.scopes_for(catalog.names))), realm_access={"roles": [role.value for role in Role]}, ) src/portal_service/domain/errors.py +5 −0 Changes for src/portal_service/domain/errors.py: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -40,6 +40,11 @@ class IdentityProviderUnavailableError(Exception): pass # Keycloak refused the portal's own provisioner credentials: a setting is wrong, not Keycloak down. class ProvisionerRejectedError(Exception): pass # Keycloak refused a client's registration access token: the client was deleted, disabled in # the console, or its token was replaced. Keycloak does not say which. class RegistrationTokenRejectedError(Exception): Loading Loading
portal-gui/src/features/api-clients/api-clients.tsx +1 −1 Changes for portal-gui/src/features/api-clients/api-clients.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -48,7 +48,7 @@ export function ApiClientsPage() { /> ) : ( <> <ApiClientsNotice /> {unavailable === false && <ApiClientsNotice />} <PageState loading={loading} error={error} Loading
src/portal_service/adapters/keycloak/provisioner_token.py +7 −0 Changes for src/portal_service/adapters/keycloak/provisioner_token.py: 7 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,8 +4,12 @@ import time from collections.abc import Callable import httpx import structlog from portal_service.adapters.keycloak import _http from portal_service.domain.errors import ProvisionerRejectedError logger: structlog.BoundLogger = structlog.get_logger(__name__) _RENEW_BEFORE_EXPIRY_SECONDS = 30 _WHAT = "Provisioner token request" Loading Loading @@ -45,6 +49,9 @@ class ProvisionerToken: "client_secret": self._client_secret, }, ) if response.status_code in (400, 401): logger.error("provisioner credentials rejected", status=response.status_code) raise ProvisionerRejectedError("Keycloak rejected the provisioner credentials") _http.ensure_ok(response, what=_WHAT) parsed = _http.parse(_http.TokenResponse, response, what=_WHAT) self._token = parsed.access_token Loading
src/portal_service/api/api_clients/routes.py +6 −0 Changes for src/portal_service/api/api_clients/routes.py: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ from portal_service.domain.errors import ( CreatorNotAllowedError, IdentityProviderUnavailableError, ProductNotHeldError, ProvisionerRejectedError, ) api_clients_router = APIRouter(prefix="/api/api-clients", tags=["api-clients"]) Loading @@ -46,6 +47,11 @@ def _answering_refusals() -> Iterator[None]: yield except tuple(_REFUSALS) as exc: raise HTTPException(_REFUSALS[type(exc)], str(exc)) from exc except ProvisionerRejectedError as exc: raise HTTPException( status.HTTP_503_SERVICE_UNAVAILABLE, "API client provisioning is misconfigured: ask the operator to check it", ) from exc except IdentityProviderUnavailableError as exc: raise HTTPException( status.HTTP_503_SERVICE_UNAVAILABLE, "The identity provider is unavailable" Loading
src/portal_service/api/dependencies.py +4 −2 Changes for src/portal_service/api/dependencies.py: 4 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -269,10 +269,11 @@ def get_api_client_service( ApiClientServiceDep = Annotated[ApiClientService, Depends(get_api_client_service)] def get_dev_user() -> TokenClaims: def get_dev_user(catalog: ApiProductsDep) -> TokenClaims: """ Stands in for `get_current_user` when AUTH__SECURITY_MODE=none: every request acts as this user, with every role, and no login or Keycloak is needed. acts as this user, with every role and every API product, and no login or Keycloak is needed. """ return TokenClaims( sub="dev-user", Loading @@ -280,5 +281,6 @@ def get_dev_user() -> TokenClaims: exp=0, iat=0, preferred_username="dev", scope=" ".join(sorted(catalog.scopes_for(catalog.names))), realm_access={"roles": [role.value for role in Role]}, )
src/portal_service/domain/errors.py +5 −0 Changes for src/portal_service/domain/errors.py: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -40,6 +40,11 @@ class IdentityProviderUnavailableError(Exception): pass # Keycloak refused the portal's own provisioner credentials: a setting is wrong, not Keycloak down. class ProvisionerRejectedError(Exception): pass # Keycloak refused a client's registration access token: the client was deleted, disabled in # the console, or its token was replaced. Keycloak does not say which. class RegistrationTokenRejectedError(Exception): Loading