"""Helper functions.""" from datetime import date, datetime from decimal import Decimal from functools import reduce import re from typing import Any, Dict, List from collaborator.connectors import mysql from collaborator.constants import error, header from collaborator.constants.header import COLLABORATOR_RESOURCE, LABEL_RESOURCE from collaborator.models.ows import ows_permissions from collaborator.models.rds.collaborator_persister import CollaboratorPersister from collaborator.utils.error import OwsError from collaborator.utils.typing import AuthorizedResources def batch(input_list, batch_size): """Create batches given a batch_size. Args: input_list (list): The input iterable list batch_size (int): The number of batches Returns: [list]: with the batches """ def reducer(batch_list, item): """Create batches. Args: batch_list ([list]): In the first iteration the batch_list value would be [[]]. As the iteration progress when a batch is completed, it go on and append to the batch list a [items] for the next batch and so on. item (dict): Dictionary in the case reports but but it could be any data type. Returns: [[]]: either an updated batch or a new one. """ if len(batch_list[-1]) < batch_size: batch_list[-1].append(item) return batch_list else: batch_list.append([item]) return batch_list return reduce(reducer, input_list, [[]]) def sanitize_data(data: Any) -> Any: """Sanitize data for a response. This turns the data into a serializable format which can be converted to JSON and returned in an HTTP response. Args: data (*): Data to sanitize Returns: *: sanitized data """ if isinstance(data, date) or isinstance(data, datetime): return data.isoformat() elif isinstance(data, Decimal): return float(data) elif isinstance(data, list): return [sanitize_data(item) for item in data] elif isinstance(data, dict): return {key: sanitize_data(value) for key, value in data.items()} return data def sanitize_text(text: str) -> str: """Sanitizies the text. Removes whitespace and replaces all non-word (including _ -) characters with _. Example: "Hello, world " => "Hello_world" "Total: 12.34" => "Total__12_34" "<^_^>" => "______" Args: text (str): Text to sanitize Returns: str: Sanitized text """ return re.sub(r"[^\w-]", "_", text.replace(" ", "")) def check_vendors_authorization( authorized_resources: AuthorizedResources, vendor_ids: List[int], throw_if_unauthorized=True, allow_access_via_collaborator=False, ) -> List[int]: """Check if authorized resources give access to specified vendors. Args: authorized_resources (AuthorizedResources): List of resources to which the requestor has access. vendor_ids (List[int]): List of IDs of vendors to check authorization for. throw_if_unauthorized (bool, optional): Whether to throw if any vendor fails authorization check. Defaults to True. allow_access_via_collaborator (bool, optional): Whether to grant access to vendor if requestor has access to any collaborator belonging to that vendor. Raises: OwsError.forbidden Returns: List[int]: List of IDs of vendors to which the requestor has access. """ if authorized_resources.full_catalog_access: return vendor_ids authorized_resources_set = { (resource.type, resource.id) for resource in authorized_resources } authorized_vendor_ids_from_collaborators = set() authorized_collaborator_ids = [ resource.id for resource in authorized_resources if resource.type == COLLABORATOR_RESOURCE ] if allow_access_via_collaborator and len(authorized_collaborator_ids) > 0: collaborators = CollaboratorPersister.get_by_ids( authorized_collaborator_ids, throw_if_unauthorized ) authorized_vendor_ids_from_collaborators = { collaborator["vendor_id"] for collaborator in collaborators } authorized_vendor_ids = [ vendor_id for vendor_id in vendor_ids if (LABEL_RESOURCE, str(vendor_id)) in authorized_resources_set or vendor_id in authorized_vendor_ids_from_collaborators ] unauthorized_vendor_ids = [ vendor_id for vendor_id in vendor_ids if vendor_id not in authorized_vendor_ids ] if throw_if_unauthorized and len(unauthorized_vendor_ids) > 0: raise OwsError.forbidden( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, ) return authorized_vendor_ids def check_collaborators_authorization( authorized_resources: AuthorizedResources, collaborator_ids: List, throw_if_unauthorized=True, ) -> Dict: """Check if authorized resources give access to specified collaborators. Args: authorized_resources (AuthorizedResources): List of resources to which the requestor has access. collaborator_ids (List[int]): List of IDs of collaborators to check authorization for. throw_if_unauthorized (bool, optional): Whether to throw if any collaborator fails authorization check. Defaults to True. Raises: OwsError.forbidden Returns: Dict: Dictionary of collaborators to which the requestor has access, keyed by ID. """ authorized_resources_set = { (resource.type, resource.id) for resource in authorized_resources } collaborators = CollaboratorPersister.get_by_ids( collaborator_ids, throw_if_unauthorized ) if authorized_resources.full_catalog_access: authorized_collaborators = collaborators else: authorized_collaborators = [ collaborator for collaborator in collaborators if (COLLABORATOR_RESOURCE, str(collaborator["id"])) in authorized_resources_set or (LABEL_RESOURCE, str(collaborator["vendor_id"])) in authorized_resources_set ] unauthorized_collaborators = [ collaborator for collaborator in collaborators if collaborator not in authorized_collaborators ] if throw_if_unauthorized and len(unauthorized_collaborators) > 0: raise OwsError.forbidden( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, ) collaborators_by_id = { collaborator["id"]: collaborator for collaborator in authorized_collaborators } return collaborators_by_id def monetary_value(currency, amount=0): """Convert to monetary_value. Args: currency (str): Currency 3 letter code amount (float): amount of the monetary value """ return {"amount": amount, "currency": currency} def get_column_by_name(table: mysql.BaseModel, column_name: str) -> Any: """Get sqlalchemy column by its _actual_ name.""" for column in vars(table).values(): if column.__class__.__name__ != "InstrumentedAttribute": continue if column.name == column_name: return column return None def get_from_response(res: dict, attr_name: str): """Get attribute from a response dict or else raise a descriptive error.""" attr = res.get(attr_name) if attr is None: raise OwsError( code=error.ERROR_CODE_NONETYPE_ACCESS_ATTEMPT, message=error.ERROR_MESSAGE_NONETYPE_ACCESS_ATTEMPT.format(attr=attr_name), ) return attr def check_admin_access_to_vendor( identity_id: str, vendor_id: int, throw_if_unauthorized=True, ) -> bool: """Check if identity has admin access to a specific vendor. Args: identity_id (str): Identity to check authorization for. vendor_id (int): ID of vendor to check authorization for. throw_if_unauthorized (bool, optional): Whether to throw if any collaborator fails authorization check. Defaults to True. Raises: OwsError.forbidden Returns: bool: If the user is authorized or not. """ resource_types = [ header.VENDOR_RESOURCE, header.SUBACCOUNT_RESOURCE, header.VENDOR_STAR_RESOURCE, ] resource_ids = set() for typ in resource_types: try: result = ows_permissions.get_admin_access_by_resource_type(identity_id, typ) resource_ids.update([str(item["id"]) for item in result["items"]]) except Exception: pass authorized = "*" in resource_ids or str(vendor_id) in resource_ids if throw_if_unauthorized and not authorized: raise OwsError.forbidden( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, ) return authorized