"""Interface for AuthorizationBackend classes.""" from __future__ import annotations import logging from dataclasses import dataclass from typing import Any, Protocol, runtime_checkable from python_pdp_sdk.backends.exceptions import ( AllowedTenantsException, AttributesException, InvalidRequestException, PdpAuthenticationError, UnauthenticatedException, UnauthorizedException, ) from python_pdp_sdk.connectors.ows_pdp.models.allowed_tenant import AllowedTenant from python_pdp_sdk.connectors.ows_pdp.models.auth_effect import AuthEffect from python_pdp_sdk.connectors.ows_pdp.models.check_resource_action import ( CheckResourceAction, ) from python_pdp_sdk.connectors.ows_pdp.models.check_resources_request import ( CheckResourcesRequest, ) from python_pdp_sdk.connectors.ows_pdp.models.get_allowed_tenants_request import ( GetAllowedTenantsRequest, ) from python_pdp_sdk.connectors.ows_pdp.models.resource import Resource from python_pdp_sdk.connectors.ows_pdp.models.resource_id import ResourceId from python_pdp_sdk.connectors.ows_pdp.ows_pdp import OwsPdpClient from python_pdp_sdk.protocols import ResourceGetter logger = logging.getLogger(__name__) @dataclass() class ResourceWithAttributes: """Dataclass used by clients to pass 2 or more resources and attributes to authorization checks.""" resource_id: int | str attributes: dict[str, Any] @dataclass() class ResourceAction(ResourceWithAttributes): """Dataclass used by clients to check authorization of a specific resource and action.""" action: str resource_type: str @runtime_checkable class AuthorizationBackend(Protocol): """AuthorizationBackend is an interface for common authorization operations.""" def is_authorized( self, action: str, resource_id: int | str, resource_type: str, resource_getter: ResourceGetter, raise_when_unauthorized: bool = False, *args: Any, **kwargs: Any, ) -> bool: """Return True when request is authorized to action on a resource. Args: action: Operation being authorized resource_id: Resource identifier resource_type: Resource type being authorized resource_getter: Used with args and kwargs to fetch resource attributes which are relevant to authorization check raise_when_unauthorized: Pass True to raise an exception when the request is not authorized *args: Arguments to use with resource_getter **kwargs: Keyword arguments to use with resource_getter """ ... def get_authorized_tenants( self, action: str, resource_type: str, ) -> list[AllowedTenant]: """Return list of Tenants which the requester is authorized to action on a resource. Args: action: Operation being authorized resource_type: Resource type being authorized """ ... def is_authorized_many( self, action: str, resource_type: str, resources_with_attributes: list[ResourceWithAttributes], raise_when_unauthorized: bool = False, ) -> list[bool]: """Lookup permissions for many resources with an identical resource type and action. Args: action: Operation being authorized resource_type: Resource type being authorized resources_with_attributes: List of resources with attributes being authorized raise_when_unauthorized: Pass True to raise an exception when the request is not authorized """ ... def is_authorized_many_resources_and_actions( self, resource_actions: list[ResourceAction], raise_when_unauthorized: bool = False, ) -> list[bool]: """Lookup permissions for many resources with different resource type/actions.""" ... class PdpAuthorizationBackend(AuthorizationBackend): """PdpAuthorizationBackend uses ows-pdp to make authorization decisions.""" def __init__(self, ows_pdp_client: OwsPdpClient) -> None: """Create PdpAuthorizationBackend instance.""" self._ows_pdp_client = ows_pdp_client def is_authorized( self, action: str, resource_id: int | str, resource_type: str, resource_getter: ResourceGetter, raise_when_unauthorized: bool = False, *args: Any, **kwargs: Any, ) -> bool: """Return True when request is authorized to action on a resource. Args: action: Operation being authorized resource_id: Resource identifier resource_type: Resource type being authorized resource_getter: Used with args and kwargs to fetch resource attributes which are relevant to authorization check raise_when_unauthorized: Pass True to raise an exception when the request is not authorized *args: Arguments to use with resource_getter **kwargs: Keyword arguments to use with resource_getter """ try: attributes = resource_getter.get_attributes(*args, **kwargs) except Exception as e: logger.warning( "Unable to get_attributes %s for resource_type %s action %s. Defaulting to empty attributes.", # noqa: E501 type(resource_getter), resource_type, action, exc_info=e, ) raise AttributesException( f"Unable to get_attributes {type(resource_getter)} for resource_type {resource_type} action {action}", # noqa: E501 ) from e # TODO PP-597 Hydrate attributes with uuids of various tenants, as needed try: result = self._ows_pdp_client.check_my_resources( CheckResourcesRequest( resources=[ CheckResourceAction( action=action, resource=Resource( resource_id=ResourceId(resource_id), resource_type=resource_type, attributes=attributes, ), ) ] ), ) except PdpAuthenticationError as e: # Always re-raise authentication errors raise UnauthenticatedException("Authentication Error") from e except Exception as e: # noqa: E722 logger.warning( "Error when checking my resource type %s action %s", resource_type, action, exc_info=e, ) # Treat any exception as Not Authorized if not raise_when_unauthorized: return False raise UnauthorizedException( "Implicitly denied by an upstream exception." ) from e if result.resources[0].effect == AuthEffect.ALLOW: return True if not raise_when_unauthorized: return False raise UnauthorizedException("Explicitly denied by PDP.") def get_authorized_tenants( self, action: str, resource_type: str, ) -> list[AllowedTenant]: """Return list of Tenants which the requester is authorized to action on a resource. Args: action: Operation being authorized resource_type: Resource type being authorized """ try: result = self._ows_pdp_client.get_allowed_tenants( GetAllowedTenantsRequest( action=action, resource_type=resource_type, ), ) except PdpAuthenticationError as e: # Always re-raise authentication errors raise UnauthenticatedException("Authentication Error") from e except Exception as e: # noqa: E722 logger.warning( "Error when getting allowed tenants for resource type %s action %s", resource_type, action, exc_info=e, ) raise AllowedTenantsException( f"Error when getting allowed tenants for resource type {resource_type} action {action}" ) from e if not result: return [] return result.tenants def is_authorized_many( self, action: str, resource_type: str, resources_with_attributes: list[ResourceWithAttributes], raise_when_unauthorized: bool = False, ) -> list[bool]: """Lookup permissions for many resources with an identical resource type and action. Args: action: Operation being authorized resource_type: Resource type being authorized resources_with_attributes: List of resources with attributes being authorized raise_when_unauthorized: Pass True to raise an exception when the request is not authorized """ check_resources_request = self._build_check_resources_request( action=action, resource_type=resource_type, resources_with_attributes=resources_with_attributes, ) try: check_resources_response = self._ows_pdp_client.check_my_resources( check_resources_request=check_resources_request ) except PdpAuthenticationError as e: # Always re-raise authentication errors raise UnauthenticatedException("Authentication Error") from e except Exception as e: # noqa: E722 logger.warning( "Error when checking many resources with type %s action %s", resource_type, action, exc_info=e, ) if not raise_when_unauthorized: return [False] * len(resources_with_attributes) raise UnauthorizedException( "Implicitly denied by an upstream exception." ) from e authorization_results = [ resource.effect == AuthEffect.ALLOW for resource in check_resources_response.resources ] if False in authorization_results and raise_when_unauthorized: raise UnauthorizedException("Unauthorized for at least one resource.") return authorization_results def _build_check_resources_request( self, action: str, resource_type: str, resources_with_attributes: list[ResourceWithAttributes], ) -> CheckResourcesRequest: """Convert the ResourceWithAttributes list into a check_resources_request.""" if not resources_with_attributes: raise InvalidRequestException( "Received a check-resources request without any resources." ) resources = [] for resource_with_attrs in resources_with_attributes: resources.append( CheckResourceAction( action=action, resource=Resource( resource_id=ResourceId(resource_with_attrs.resource_id), resource_type=resource_type, attributes=resource_with_attrs.attributes, ), ) ) return CheckResourcesRequest( resources=resources, ) def is_authorized_many_resources_and_actions( self, resource_actions: list[ResourceAction], raise_when_unauthorized: bool = False, ) -> list[bool]: """Lookup permissions for many resources with different resource type/actions.""" if not resource_actions: raise InvalidRequestException("resource_actions must be a non-empty list.") resources = [] for resource_action in resource_actions: resources.append( CheckResourceAction( action=resource_action.action, resource=Resource( resource_id=ResourceId(resource_action.resource_id), resource_type=resource_action.resource_type, attributes=resource_action.attributes, ), ) ) try: check_resources_response = self._ows_pdp_client.check_my_resources( check_resources_request=CheckResourcesRequest( resources=resources, ) ) except PdpAuthenticationError as e: # Always re-raise authentication errors raise UnauthenticatedException("Authentication Error") from e except Exception as e: logger.warning( "Error when checking many resources and actions", exc_info=e, ) if not raise_when_unauthorized: return [False] * len(resource_actions) raise UnauthorizedException( "Implicitly denied by an upstream exception." ) from e authorization_results = [ resource.effect == AuthEffect.ALLOW for resource in check_resources_response.resources ] if False in authorization_results and raise_when_unauthorized: raise UnauthorizedException("Unauthorized for at least one resource.") return authorization_results