import abc import logging from collections.abc import Sequence from typing import Any from cachelib import BaseCache as Cache from fansifter_common.adapters.ows_account import OwsAccountClient, VendorLookup from fansifter_common.adapters.ows_pdp import ( AuthEffect, CheckResourceAction, CheckResourceActionResult, CheckResourcesInput, OwsPdpClient, Resource, TenantType, ) from fansifter_common.auth.account import Account, AccountAccess from fansifter_common.auth.exceptions import PermissionDenied from fansifter_common.auth.types import JointVentureParticipant from fansifter_common.utils.cache import cached logger = logging.getLogger(__name__) class AuthorizationBackend(abc.ABC): @abc.abstractmethod def get_account_access( self, identity_id: str, /, *, resource_type: str, action: str, ) -> AccountAccess: ... def authorize_for_resource_type_action( self, identity_id: str, /, *, resource_type: str, action: str, ) -> AccountAccess: account_access = self.get_account_access( identity_id, resource_type=resource_type, action=action ) if not account_access.authorized: raise PermissionDenied("Account access denied.") return account_access def authorize_account( self, identity_id: str, /, *, resource_type: str, action: str, account: Account ) -> AccountAccess: account_access = self.get_account_access( identity_id, resource_type=resource_type, action=action ) if not account_access.has_access(account): raise PermissionDenied( f"You are not allowed to perform `{action}` action " f"on `{resource_type}` resource for account `{account.vendor_id}`" ) return account_access @abc.abstractmethod def is_allowed_account_resource( self, identity_id: str, /, *, account: Account, resource_id: str, resource_type: str, action: str, joint_ventures: list[JointVentureParticipant] | None = None, ) -> bool: pass @abc.abstractmethod def is_allowed_owned_resource( self, identity_id: str, /, *, resource_id: str, resource_type: str, resource_identity_id: str, action: str, account: Account | None, ) -> bool: pass class PdpAuthorizationBackend(AuthorizationBackend): def __init__( self, ows_pdp_client: OwsPdpClient, ows_account_client: OwsAccountClient, cache: Cache, cache_timeout: int, ) -> None: self.ows_pdp_client = ows_pdp_client self.ows_account_client = ows_account_client self.cache = cache self.cache_timeout = cache_timeout def is_allowed_owned_resource( self, identity_id: str, /, *, resource_id: str, resource_type: str, action: str, resource_identity_id: str, account: Account | None, ) -> bool: try: check_resources = self._get_check_owned_resource( resource_id=resource_id, resource_type=resource_type, resource_identity_id=resource_identity_id, action=action, account=account, ) check_resource = check_resources[0] except Exception as exc: logger.warning("Failed to check resource permission", exc_info=exc) return False return check_resource.effect == AuthEffect.ALLOW def is_allowed_account_resource( self, identity_id: str, /, *, account: Account, resource_id: str, resource_type: str, action: str, joint_ventures: list[JointVentureParticipant] | None = None, ) -> bool: if joint_ventures is None: joint_ventures = [] vendors = self._lookup_vendors_by_vendor_ids([account.vendor_id]) joint_venture_vendors = self._lookup_vendors_by_vendor_ids( [participant.vendor_id for participant in joint_ventures] ) try: check_resources = self._get_check_vendors_resource( vendors=vendors, resource_id=resource_id, resource_type=resource_type, action=action, ) check_resource = check_resources[0] if check_resource.effect == AuthEffect.ALLOW: return True if joint_ventures: check_joint_venture_resources = self._get_check_joint_venture_resource( joint_ventures=joint_ventures, vendors=joint_venture_vendors, resource_id=resource_id, resource_type=resource_type, action=action, ) check_joint_venture_resource = check_joint_venture_resources[0] if check_joint_venture_resource.effect == AuthEffect.ALLOW: return True return False except Exception as exc: logger.warning("Failed to check resource permission", exc_info=exc) return False @cached("auth:account-access", timeout_param="cache_timeout") def get_account_access( self, identity_id: str, /, *, resource_type: str, action: str, ) -> AccountAccess: tenants = self.ows_pdp_client.get_allowed_tenants( resource_type=resource_type, action=action ) accounts = [] for tenant in tenants: if tenant.tenant_id is not None: accounts.append( Account( vendor_id=tenant.tenant_id, vendor_uuid=tenant.tenant_uuid, subaccount_id=0, # subaccount_id is not supported yet ) ) return AccountAccess(accounts=accounts) def _get_check_vendors_resource( self, *, vendors: Sequence[VendorLookup | Account], resource_id: str, resource_type: str, action: str, use_vendor_uuid_as_resource_id: bool = False, ) -> list[CheckResourceActionResult]: return self.ows_pdp_client.check_resources( "self", resources=CheckResourcesInput( resources=[ CheckResourceAction( resource=Resource( # TODO: pdp should return tenant_uuid within attributes # This is a workaround for now resource_id=( vendor.vendor_uuid if use_vendor_uuid_as_resource_id else resource_id ), resource_type=resource_type, attributes={ "tenant": { "tenant_uuid": vendor.vendor_uuid, "tenant_type": TenantType.ACCOUNT, } }, ), action=action, ) for vendor in vendors ] ), ) def _get_check_joint_venture_resource( self, *, joint_ventures: list[JointVentureParticipant], vendors: Sequence[VendorLookup | Account], resource_id: str, resource_type: str, action: str, ) -> list[CheckResourceActionResult]: vendor_id_to_uuid = {vendor.vendor_id: vendor.vendor_uuid for vendor in vendors} return self.ows_pdp_client.check_resources( "self", resources=CheckResourcesInput( resources=[ CheckResourceAction( resource=Resource( resource_id=resource_id, resource_type=resource_type, attributes={ "joint_venture": { "tenants": [ { "tenant_uuid": vendor_id_to_uuid[ participant.vendor_id ], "tenant_type": TenantType.ACCOUNT, "is_provider": participant.is_provider, "is_consumer": participant.is_consumer, } for participant in joint_ventures ] } }, ), action=action, ) ] ), ) def _get_check_owned_resource( self, *, resource_id: str, resource_type: str, resource_identity_id: str, action: str, account: Account | None, ) -> list[CheckResourceActionResult]: resource_attributes: dict[str, Any] = {"identity_uuid": resource_identity_id} if account: vendors = self._lookup_vendors_by_vendor_ids([account.vendor_id]) vendor = vendors[0] resource_attributes["tenant"] = { "tenant_uuid": vendor.vendor_uuid, "tenant_type": TenantType.ACCOUNT, } return self.ows_pdp_client.check_resources( "self", resources=CheckResourcesInput( resources=[ CheckResourceAction( resource=Resource( resource_id=resource_id, resource_type=resource_type, attributes=resource_attributes, ), action=action, ) ] ), ) @cached("auth:vendors-by-uuids") def _lookup_vendors_by_uuids(self, uuids: list[str]) -> list[VendorLookup]: if not uuids: return [] return self.ows_account_client.lookup_vendors_by_uuids(uuids) @cached("auth:vendors-by-vendor-ids") def _lookup_vendors_by_vendor_ids( self, vendor_ids: list[int] ) -> list[VendorLookup]: if not vendor_ids: return [] return self.ows_account_client.lookup_vendors_by_vendor_ids(vendor_ids) class AccessAuthorizationBackendStub(AuthorizationBackend): def __init__(self, account_access: AccountAccess) -> None: self.account_access = account_access def get_account_access( self, identity_id: str, /, *, resource_type: str, action: str, ) -> AccountAccess: return self.account_access def is_allowed_account_resource( self, identity_id: str, /, *, account: Account, resource_id: str, resource_type: str, action: str, joint_ventures: list[JointVentureParticipant] | None = None, ) -> bool: return True def is_allowed_owned_resource( self, identity_id: str, /, *, resource_id: str, resource_type: str, action: str, resource_identity_id: str, account: Account | None, ) -> bool: return True