"""Permissions Platform (PP) authorization.""" from typing import Any, Callable, NamedTuple from urllib.parse import urlparse from flask import g, request from python_pdp_sdk import ( ForwardKwargsGetter, MigrationAuthorizationBackend, WouldDenyMetadata, ) from python_pdp_sdk.connectors.ows_pdp.models.tenant_type import TenantType from assets import config from assets.api import authorization_backend from assets.constants import api from assets.exceptions import AssetUploadNotFound from assets.models import asset_upload, ows_product RESOURCE_TYPE = "product" ACTION_VIEW = "view" ACTION_VIEW_ASSET = "view:asset" ACTION_REVIEW = "review:asset" ACTION_UPDATE = "update:asset" ACTION_UPDATE_STATUS = "update:asset:status" ACTION_UPDATE_AI_DETECTION = "update:asset:ai_detection" ACTION_APPLY_REVISION = "apply_asset_revision" _TENANT_TYPE_ACCOUNT = TenantType.ACCOUNT.value _TENANT_TYPE_SUBACCOUNT = TenantType.SUBACCOUNT.value _VENDOR_UUID_KEY = "vendorUUID" _SUBACCOUNT_UUID_KEY = "subaccountUUID" class Tenant(NamedTuple): tenant_type: str tenant_uuid: str def request_tags() -> list[str]: """Datadog tags describing the active request, for use as extra_tags_getter.""" return [ f"method:{request.method}", f"endpoint:{request.url_rule or request.path}", f"has_authorization_header:{str(bool(request.headers.get('Authorization'))).lower()}", f"profile_type:{(request.headers.get('Orchard-Profile-Type') or 'none').lower()}", f"referrer:{urlparse(request.referrer or '').hostname or 'unknown'}", ] migration_authorization_backend = MigrationAuthorizationBackend( inner_backend=authorization_backend, service_name=config.SERVICE_NAME, environment=config.ENVIRONMENT, extra_tags_getter=request_tags, metrics_enabled=config.ENVIRONMENT in (config.QA_ENVIRONMENT, config.PROD_ENVIRONMENT), ) def _tenant_from_product_dict(product: dict[str, Any]) -> Tenant | None: """Subaccount is preferred over vendor when both are present.""" subaccount_uuid = product.get(_SUBACCOUNT_UUID_KEY) if subaccount_uuid: return Tenant(_TENANT_TYPE_SUBACCOUNT, subaccount_uuid) vendor_uuid = product.get(_VENDOR_UUID_KEY) if vendor_uuid: return Tenant(_TENANT_TYPE_ACCOUNT, vendor_uuid) return None def tenant_from_product(product_id: int) -> Tenant | None: return _tenant_from_product_dict( ows_product.get_product_by_id(product_id, with_tenant_uuids=True) ) def tenant_from_filename(filename: str) -> Tenant | None: try: product_id = asset_upload.get_asset_upload( filename, api_version=api.API_VERSION_V2 )["product_id"] except AssetUploadNotFound: return None if not product_id: return None return tenant_from_product(product_id) def _is_authorized( backend: MigrationAuthorizationBackend, action: str, tenant: Tenant | None, on_would_deny: Callable[[WouldDenyMetadata], None] | None = None, ) -> bool: kwargs: dict[str, Any] = {} if tenant is not None: kwargs["tenant"] = { "tenant_type": tenant.tenant_type, "tenant_uuid": tenant.tenant_uuid, } return bool( backend.is_authorized( action=action, resource_id=0, resource_type=RESOURCE_TYPE, resource_getter=ForwardKwargsGetter(), on_would_deny=on_would_deny, **kwargs, ) ) def _log_would_deny(metadata: WouldDenyMetadata, tenant: Tenant | None) -> None: """Log the JWT identity PP would deny, to build the grant list.""" identity_id = g.request_context.jwt_identity_id if not identity_id: return tenant_note = ( f"tenant_type={tenant.tenant_type} tenant_uuid={tenant.tenant_uuid}" if tenant is not None else "tenant=none" ) g.log.info( f"pp_would_deny identity={identity_id} action={metadata.action} " f"resource={metadata.resource_type} reason={metadata.reason} {tenant_note}" ) def shadow_authorization( action: str, resolver: Callable[..., Tenant | None] | None = None, *resolver_args: Any, ) -> None: """Shadow the PP decision for `action`, side-effect only (always allows).""" try: if resolver is not None: tenant = resolver(*resolver_args) if tenant is None: g.log.warning( f"PP shadow authorization can't resolve tenant for {action}" ) return else: tenant = None _is_authorized( migration_authorization_backend, action, tenant, on_would_deny=lambda metadata: _log_would_deny(metadata, tenant), ) except Exception as exc: g.log.exception(f"PP shadow authorization failed for {action}: {exc}")