"""FastAPI handlers for Infra endpoints.""" import logging from typing import Any, Dict, List, Optional from uuid import UUID from cerbos.sdk.client import AsyncCerbosClient from fastapi import APIRouter, Depends, status from fastapi.responses import PlainTextResponse from pydantic import UUID4 from pdp import config from pdp.connectors.features import BooleanFeature, SplitioClient from pdp.connectors.ows_account import ( LookupVendorsRequest, LookupVendorsResponse, OwsAccountClient, ) from pdp.connectors.ows_participant import ( LookupParticipantsRequest, LookupParticipantsResponse, OwsParticipantClient, ) from pdp.connectors.ows_permissions import ( DEFAULT_LIMIT as DEFAULT_OWS_PERMISSIONS_LIMIT, ) from pdp.connectors.ows_permissions import ( DEFAULT_OFFSET as DEFAULT_OWS_PERMISSIONS_OFFSET, ) from pdp.connectors.ows_permissions import ( AdminableResourcesResponse, OwsPermissionsClient, ) from pdp.connectors.redis_client import RedisConnector from pdp.constants.constants import TenantType from pdp.constants.features import ( FEATURE_FLAG_PP_SAY_BONJOUR, FEATURE_FLAG_PP_SEND_IMPERSONATED_BY_IDENTITY_UUID, ) from pdp.fastapi.auth import ( check_authorization_infra, get_principal_pdp_tenant_roles_from_scope, identity_uuid_from_scope, impersonated_by_identity_uuid_from_scope, user_type_from_scope, ) from pdp.fastapi.datasources import ( get_ows_account_client, get_ows_participant_client, get_ows_permissions_client, get_redis_connector, get_splitio_client, ) from pdp.fastapi.schemas import check_resources as check_resources_schema from pdp.fastapi.schemas import identity as identity_schema from pdp.fastapi.schemas import tenant as tenant_schema from pdp.fastapi.schemas.infra import ( BludgeonCacheRequest, BludgeonCacheResponse, BustCacheRequest, BustCacheResponse, ListCacheRequest, ListCacheResponse, ) from pdp.fastapi.schemas.principal import Principal from pdp.logic import cerbos, infra logger = logging.getLogger(__name__) router = APIRouter(tags=["infra"]) @router.get( "/", status_code=status.HTTP_200_OK, ) def hello_world( name: str = "world", identity_uuid: UUID4 = Depends(identity_uuid_from_scope) ) -> PlainTextResponse: """Hello World with an optional GET param 'name'. Args: name: Str passed as URL parameter identity_uuid: orchardIdentityId from JWT Returns: Response: PlainTextResponse containing "hello 'name'" text """ msg = f"Hello {name} from fastapi! identity_uuid='{identity_uuid}'." return PlainTextResponse(msg) @router.get( config.HEALTH_CHECK, description="Check the health of the application.", response_model=identity_schema.HelloResponse, ) def health() -> Dict[str, Any]: """Check the health of the application. Args: None Returns: Response: JSON response containing OK status """ return {"status": "ok"} @router.get( config.CONNECTIVITY_SPLITIO, description="Endpoint to check splitio connectivity.", response_model=identity_schema.BonjourResponse, ) def check_connectivity_splitio( identity_uuid: UUID4 = Depends(identity_uuid_from_scope), splitio_client: SplitioClient = Depends(get_splitio_client), ) -> Dict[str, Any]: """Endpoint to check splitio connectivity. Feature Flags: - pp_say_bonjour Args: identity_uuid: orchardIdentityId from JWT splitio_client: Split.io client connection Returns: Response: JSON response containing message """ bonjour_feature = BooleanFeature( client=splitio_client, feature_name=FEATURE_FLAG_PP_SAY_BONJOUR ) if bonjour_feature.is_on_for_identity(str(identity_uuid)): return {"message": "bonjour"} return {"message": "hello"} @router.get( config.CONNECTIVITY_OWS_PERMISSIONS, description="Endpoint to check ows-permissions connectivity.", ) async def ows_permissions_my_adminable_resources( ows_permissions_client: OwsPermissionsClient = Depends(get_ows_permissions_client), offset: int = DEFAULT_OWS_PERMISSIONS_OFFSET, limit: int = DEFAULT_OWS_PERMISSIONS_LIMIT, ) -> AdminableResourcesResponse: """Endpoint to check ows-permissions connectivity.""" result = await ows_permissions_client.get_my_adminable_resources( offset=offset, limit=limit ) return result @router.get( config.CONNECTIVITY_REDIS, description="Endpoint to check redis connectivity.", response_model=identity_schema.CachePingResponse, ) async def redis_ping() -> identity_schema.CachePingResponse: """Endpoint to check redis connectivity via ping.""" # Force use_redis_cache = True, ignore the CACHE_USE_REDIS env client = RedisConnector(redis_url=config.REDIS_URL, use_redis_cache=True) response = await client.ping() return identity_schema.CachePingResponse( status=response, redis_url=config.REDIS_URL ) @router.post( config.CONNECTIVITY_OWS_ACCOUNT, description="Endpoint to check ows-account connectivity.", response_model=LookupVendorsResponse, ) async def ows_account_test( lookup_vendor_request: LookupVendorsRequest, ows_account_client: OwsAccountClient = Depends(get_ows_account_client), ) -> LookupVendorsResponse: """Endpoint to check ows-account connectivity.""" result = await ows_account_client.lookup_vendors_by_uuids( uuids=lookup_vendor_request.uuids, fetch_flags=lookup_vendor_request.fetch_flags, ) return result @router.post( "/infra/connectivity/ows-participant/", description="Endpoint to check ows-participant connectivity.", response_model=LookupParticipantsResponse, ) async def ows_participant_test( request: LookupParticipantsRequest, ows_participant_client: OwsParticipantClient = Depends(get_ows_participant_client), ) -> LookupParticipantsResponse: """Endpoint to check ows-participant connectivity.""" result = await ows_participant_client.lookup_participants_by_uuids( uuids=request.uuids ) return result @router.post( "/cache/list/", description="List items from the cache", response_model=ListCacheResponse, dependencies=[Depends(check_authorization_infra)], ) async def cache_list( list_cache_request: ListCacheRequest, redis_connector: RedisConnector = Depends(get_redis_connector), ) -> ListCacheResponse: """List entries from the cache.""" cache_response = await infra.cache_list( list_cache_request=list_cache_request, redis_connector=redis_connector, ) return cache_response @router.post( "/cache/bust/", description="Bust items from the cache", response_model=BustCacheResponse, dependencies=[Depends(check_authorization_infra)], ) async def cache_bust( bust_cache_request: BustCacheRequest, redis_connector: RedisConnector = Depends(get_redis_connector), ) -> BustCacheResponse: """Bust entries from the cache.""" cache_response = await infra.cache_bust( bust_cache_request=bust_cache_request, redis_connector=redis_connector, ) return cache_response @router.post( "/cache/bludgeon/", description="Delete all cached entries for cache_entry_type", response_model=BludgeonCacheResponse, dependencies=[Depends(check_authorization_infra)], ) async def cache_bludgeon( bludgeon_cache_request: BludgeonCacheRequest, redis_connector: RedisConnector = Depends(get_redis_connector), ) -> BludgeonCacheResponse: """Bust entries from the cache.""" cache_response = await infra.cache_bludgeon( bludgeon_cache_request=bludgeon_cache_request, redis_connector=redis_connector, ) return cache_response @router.post( config.CONNECTIVITY_CERBOS_FARGATE, description="Endpoint to check cerbos fargate cluster connectivity", response_model=identity_schema.CheckResourcesResponse, ) async def check_connectivity_cerbos( check_resources_request: check_resources_schema.CheckResourcesRequest, identity_uuid: UUID4 = Depends(identity_uuid_from_scope), principal_identity_uuid: UUID4 = Depends(identity_uuid_from_scope), pdp_tenant_roles: Dict[UUID, identity_schema.TenantRoles] = Depends( get_principal_pdp_tenant_roles_from_scope ), impersonated_by_identity_uuid: UUID4 | None = Depends( impersonated_by_identity_uuid_from_scope ), user_type: str = Depends(user_type_from_scope), ows_account_client: OwsAccountClient = Depends(get_ows_account_client), ows_participant_client: OwsParticipantClient = Depends(get_ows_participant_client), redis_connector: RedisConnector = Depends(get_redis_connector), authenticated_identity_uuid: UUID = Depends(identity_uuid_from_scope), splitio_client: SplitioClient = Depends(get_splitio_client), ) -> identity_schema.CheckResourcesResponse: """Check cerbos fargate server health.""" cerbos_client = AsyncCerbosClient( config.CERBOS_SERVER_HOST, timeout_secs=config.CERBOS_TIMEOUT_SECS, ) logger.info("CERBOS_SERVER_HOST: %s", config.CERBOS_SERVER_HOST) pp_send_impersonated_by_identity_uuid_feature = BooleanFeature( client=splitio_client, feature_name=FEATURE_FLAG_PP_SEND_IMPERSONATED_BY_IDENTITY_UUID, ) if pp_send_impersonated_by_identity_uuid_feature.is_on_for_identity( str(impersonated_by_identity_uuid or principal_identity_uuid) ): principal = Principal( identity_uuid=principal_identity_uuid, user_type=user_type, pdp_tenant_roles=pdp_tenant_roles, impersonated_by_identity_uuid=impersonated_by_identity_uuid, ) check_response = await cerbos.check_resources( identity_uuid=str(identity_uuid), check_resources_request=check_resources_request, pdp_tenant_roles=pdp_tenant_roles, cerbos_client=cerbos_client, include_resource_attributes=check_resources_request.include_resource_attributes_in_response, # noqa: E501 ows_account_client=ows_account_client, ows_participant_client=ows_participant_client, redis_connector=redis_connector, authenticated_identity_uuid=authenticated_identity_uuid, principal=principal, splitio_client=splitio_client, ) else: check_response = await cerbos.check_resources( identity_uuid=str(identity_uuid), check_resources_request=check_resources_request, pdp_tenant_roles=pdp_tenant_roles, cerbos_client=cerbos_client, include_resource_attributes=check_resources_request.include_resource_attributes_in_response, # noqa: E501 ows_account_client=ows_account_client, ows_participant_client=ows_participant_client, redis_connector=redis_connector, authenticated_identity_uuid=authenticated_identity_uuid, splitio_client=splitio_client, ) return check_response @router.post( config.ID_TO_UUID_EXCHANGE, description="Endpoint to exchange tenant ids to UUIDs", response_model=Dict[ TenantType, Dict[str, Optional[tenant_schema.IdExchangeTenantHierarchy]], ], dependencies=[Depends(check_authorization_infra)], ) async def id_to_uuid_exchange( tenants: List[tenant_schema.IdToUuidExchangeTenant], ows_account_client: OwsAccountClient = Depends(get_ows_account_client), redis_connector: RedisConnector = Depends(get_redis_connector), ) -> Dict[TenantType, Dict[str, Optional[tenant_schema.IdExchangeTenantHierarchy]]]: """Exchange tenant ids to UUIDs.""" return await infra.gather_id_to_uuid_exchange( tenants=tenants, ows_account_client=ows_account_client, redis_connector=redis_connector, ) @router.post( config.UUID_TO_ID_EXCHANGE, description="Endpoint to exchange tenant UUIDs to ids", response_model=Dict[UUID, Optional[tenant_schema.IdExchangeTenantHierarchy]], dependencies=[Depends(check_authorization_infra)], ) async def uuid_to_id_exchange( tenants: List[tenant_schema.UuidToIdExchangeTenant], ows_account_client: OwsAccountClient = Depends(get_ows_account_client), redis_connector: RedisConnector = Depends(get_redis_connector), ) -> Dict[UUID, Optional[tenant_schema.IdExchangeTenantHierarchy]]: """Exchange tenant UUIDs to ids.""" return await infra.gather_uuid_to_id_exchange( tenants, ows_account_client=ows_account_client, redis_connector=redis_connector, )