import copy from typing import Any, Dict, List, Mapping, Optional, Sequence from flask import Request, abort from owsrequest import request as owsrequest_request from owsrequest.context import get_request_context_from_headers from requests import Response from requests.structures import CaseInsensitiveDict from playlist.connectors.redis import redis_client from playlist.constants.cache import OWS_PERMISSIONS_TTL, ows_permissions_key from playlist.utils.cache import cached SUBACCOUNT_RESOURCE_TYPE = "subaccount" VENDOR_RESOURCE_TYPE = "vendor" LABEL_PARTICIPANT_RESOURCE_TYPE = "labelparticipant" ALLOWED_ROLES = ["analytics", "administrator"] OWS_PERMISSIONS_SERVICE_NAME = "ows-permissions" OWS_PERMISSIONS_PROFILE_URL = ( "/admin/profile-type/{profile_type}/profile/{profile_id}/resource/all" ) PERMISSION_LABEL_IDS_KEY = "permission_label_ids" PERMISSION_SUBACCOUNT_IDS_KEY = "permission_subaccount_ids" PERMISSION_LABEL_PARTICIPANT_IDS_KEY = "permission_label_participant_ids" # access to everything DEFAULT_PERMISSIONS = { PERMISSION_LABEL_IDS_KEY: [], PERMISSION_SUBACCOUNT_IDS_KEY: [], PERMISSION_LABEL_PARTICIPANT_IDS_KEY: [], } def call_ows_permissions_get_profile_resources( profile_type: str, profile_id: int ) -> Optional[List[dict]]: endpoint: str = OWS_PERMISSIONS_PROFILE_URL.format( profile_type=profile_type, profile_id=profile_id ) response: Response = owsrequest_request.get(OWS_PERMISSIONS_SERVICE_NAME, endpoint) if response.status_code >= 500: raise ValueError(f"ows-permissions returned {response.status_code}") if response.status_code != 200: return abort(401) return response.json().get("items") def get_permissions(request: Request) -> Optional[Dict[str, List[str]]]: req = get_request_context_from_headers(CaseInsensitiveDict(request.headers)) items = cached( fn=call_ows_permissions_get_profile_resources, key=ows_permissions_key(req.profile_type, req.profile_id), ttl=OWS_PERMISSIONS_TTL, profile_type=req.profile_type, profile_id=req.profile_id, ) return _parse_accessible_resources(items) def _parse_accessible_resources( resources: Sequence[Any], ) -> Optional[Mapping[str, List[str]]]: """Parses the response received from ows-permissions. Parses the response received from ows-permissions and returns a dictionary indicating those. Args: resources (Sequence[Any]): A sequence of objects which describe the resource the profile has access to. Returns: A dictionary-like object containing the resource ids the profile has access to. The dictionary-like object may contain the following keys - 1. label_ids 2. subaccount_ids 3. label_participant_ids """ if not resources: return abort(401) resource_dict: Mapping[str, List[str]] = copy.deepcopy(DEFAULT_PERMISSIONS) for resource in resources: resource_type = resource.get("type").lower() resource_roles = resource.get("roles") if any(role.lower() in ALLOWED_ROLES for role in resource_roles): if resource_type == VENDOR_RESOURCE_TYPE: # Has access to all resources if resource.get("id") == "*": for key in resource_dict: resource_dict[key].clear() return resource_dict resource_dict[PERMISSION_LABEL_IDS_KEY].append(str(resource.get("id"))) elif resource_type == SUBACCOUNT_RESOURCE_TYPE: resource_dict[PERMISSION_SUBACCOUNT_IDS_KEY].append( str(resource.get("id")) ) elif resource_type == LABEL_PARTICIPANT_RESOURCE_TYPE: resource_dict[PERMISSION_LABEL_PARTICIPANT_IDS_KEY].append( int(resource.get("id")) ) # Does not have access to any resources but user is authenticated if not any(resource_dict.values()): return abort(403) return resource_dict