"""Service for generating signed urls for Mode.""" import hmac from base64 import encodebytes from collections import OrderedDict from hashlib import md5, sha256 from urllib.parse import urlencode from reporting import config from reporting.constants import mode as mc def _generate_md5_hash(content=''): """Generate a md5 hash. Args: content (str): value to hash Returns: generated_hash: md5 hash """ generated_hash = md5() generated_hash.update(content.encode('utf-8')) return generated_hash.digest() def _generate_content_digest(): """Generate a base64 encoded digest of an md5 hash. Returns: digest (str): base64 encoded md5 hash """ content_hash = _generate_md5_hash() return encodebytes(content_hash).decode('utf-8').strip() def _build_encoded_request_string( content_digest, url, timestamp, request_type='GET', content_type='' ): """Build utf-8 encoded request string. Args: request_type (str): method of the request content_type (str): content type of the request content_digest (str): base64 encoded hash url (str): url of the request timestamp (int): timestamp of the request Returns: encoded_string (str): utf-8 encoded request string """ return ( '{request_type},{content_type},{content_digest},{url},{timestamp}'.format( request_type=request_type, content_type=content_type, content_digest=content_digest, url=url, timestamp=timestamp, ) ).encode('utf-8') def dict_from_collection(collection, new_key, old_key='name'): """Build a dictionary from a list of dicts keyed by `new_key`. Args: collection (list): list of dictionaries. new_key (str): key in the new dict `d`. old_key (str): key in the original list of dicts, its value becomes new key, defaults to `name`. Returns: (dict): the dict indexed at `new_key`. """ d = dict( (d.get(old_key), dict(d, index=index)) for (index, d) in enumerate(collection) ) return d.get(new_key) def sort_params_by_key(params): """Sort params by key name. Args: params (dict): dictionary of parameters Returns: sorted_dict (dict): dictionary with sorted keys """ return OrderedDict(sorted(params.items(), key=lambda t: t[0])) def sort_and_encode_params(params): """Sort and format params for Mode SQL. Args: params (dict): dictionary of parameters Returns: request_params (string): string of encoded url params. """ if not params: return '' sorted_params = sort_params_by_key(params) for i in sorted_params: if not sorted_params[i]: sorted_params[i] = '' elif isinstance(sorted_params[i], list) and len(sorted_params[i]) > 0: for index, item in enumerate(sorted_params[i]): sorted_params[i][index] = item.replace(',', '(_COMMA_)') sorted_params[i] = ','.join(sorted_params[i]) return urlencode(sorted_params).replace('+', '%20') def generate_signed_url(url, secret, timestamp): """Generate a signed url for interacting with the Mode api. Args: url (str): embed url to sign secret (str): mode secret key timestamp (int): timestamp of url signature Returns: signed_url (str): signed embed url """ encoded_secret = secret.encode('utf-8') content_digest = _generate_content_digest() encoded_request_string = _build_encoded_request_string( content_digest, url, timestamp ) signature = hmac.new( encoded_secret, msg=encoded_request_string, digestmod=sha256 ).hexdigest() return '{url}&signature={signature}'.format(url=url, signature=signature) def api_url(**params): """Generate a mode API url call. Args: params (kwargs): Keyword arguments Returns: mode_api_url (str): a Mode API url string that can be used for a request. """ path = params.get('path') api_url = '{url}/{path}'.format(url=mc.MODE_API_URL, path=path) query_params = params.get('query') if query_params: api_url = '{api_url}?{params}'.format( api_url=api_url, params=sort_and_encode_params(query_params) ) return api_url def prefix_with_environment(string): """Prefix a string with the current environment name. Args: string (str): a string to prefix with the current environment. Returns: (str): string prefixed by the current environment, i.e. `qa_foo`. """ if not string: return '' return '{}_{}'.format(config.ENVIRONMENT, str(string)) def query_url_from_query_run(query_run): """Generate the Mode report query url from query_run object. Args: query_run: object representation of a query_run from Mode. Returns: query_api_url (str): The url for fetching a query object from Mode. """ _links = query_run.get('_links') if not _links: return None query = _links.get('query') if not query: return query href = query.get('href') if not href: return None return '{base_uri}{href}'.format(href=href, base_uri=mc.BASE_URI) def query_run_result_url_from_query_run(query_run): """Generate the Mode report query result url from query_run object. Args: query_run: object representation of a query_run from Mode. Returns: query_result_url (str): The url for fetching a query_run_result object from Mode. """ _links = query_run.get('_links') if not _links: return None result = _links.get('result') if not result: return None href = result.get('href') if not href: return None return '{base_uri}{href}'.format(href=href, base_uri=mc.BASE_URI) def json_url_from_query_result(query_result): """Generate the Mode JSON content url from query_result object. Args: query_result: object representation of a query_result from Mode. Returns: json_url (str): The url for the JSON content of the result from Mode. """ _links = query_result.get('_links') if not _links: return None json = _links.get('json') if not json: return None return json.get('href') def is_report_query(query): """Test if a Mode query object is a report query. Args: query: object representation of a query from Mode. Returns: (bool): result of the check. """ name = query.get('name') if not name: return False return not name.startswith('filter_')