"""Utility Functions for Handlers.""" from collections import namedtuple import datetime from functools import wraps import json from flask import g from flask import request from oto import response from oto.adaptors.flask import flaskify from owsrequest import flask_request from werkzeug.exceptions import BadRequest from werkzeug.exceptions import UnsupportedMediaType from backend.constants import error from backend.constants import field from backend.constants import header from backend.exceptions import RequestError from backend.exceptions import ValidationError from backend.models import ows_product from backend.validation import json_schema Account = namedtuple('Account', ['type', 'id']) User = namedtuple('User', ['type', 'id']) Profile = namedtuple('Profile', ['type', 'id']) def get_account_from_grass(function): """Get account info from GRASS headers. Args: Function (func): the function to be called after grabbing the headers. Returns: Function: The decorated function. """ @wraps(function) def call_function_with_headers(*args, **kwargs): account_type, account_id = \ flask_request.get_grass_headers(request) kwargs['account'] = Account(type=account_type, id=account_id) return function(*args, **kwargs) return call_function_with_headers def get_user_from_grass(function): """Get user info from GRASS headers. Args: Function (func): the function to be called after grabbing the headers. Returns: Function: The decorated function. """ @wraps(function) def call_function_with_headers(*args, **kwargs): user_id = None user_type = None orchard_user_id = request.headers.get(header.ORCHARD_USER_ID, '') if orchard_user_id and ':' in orchard_user_id: user_type, user_id = orchard_user_id.split(':') kwargs['user'] = User(type=user_type, id=user_id) return function(*args, **kwargs) return call_function_with_headers def get_user_from_profile_headers(function): """Get profile info from headers. Args: Function (func): the function to be called after grabbing the headers. Returns: Function: The decorated function. """ @wraps(function) def call_function_with_headers(*args, **kwargs): orchard_profile_type = request.headers.get(header.ORCHARD_PROFILE_TYPE, '') orchard_profile_id = request.headers.get(header.ORCHARD_PROFILE_ID, '') kwargs['profile'] = Profile(type=orchard_profile_type, id=orchard_profile_id) return function(*args, **kwargs) return call_function_with_headers def _is_admin_request(): profile_type = request.headers.get(header.ORCHARD_PROFILE_TYPE) profile_id = request.headers.get(header.ORCHARD_PROFILE_ID) # TODO: Check profile label access is_profile_based_admin = ( profile_id and profile_type == header.values.ORCHARD_PROFILE_TYPE_CONTENT ) orchard_user_id = request.headers.get(header.ORCHARD_USER_ID) is_legacy_admin = orchard_user_id and orchard_user_id.startswith('oa:') return bool(is_profile_based_admin or is_legacy_admin) def get_account_info(function): """Decorator to get account info from request headers or query params.""" @wraps(function) def wrapper(*args, **kwargs): if _is_admin_request(): # call ows-product to get product data product_response = ows_product.get_product_by_product_id( kwargs.get('product_id')) if not product_response: return product_response vendor_id = product_response.message['vendor_id'] subaccount_id = product_response.message['subaccount_id'] if subaccount_id: kwargs[field.ACCOUNT_TYPE] = 'subaccount' kwargs[field.ACCOUNT_ID] = str(subaccount_id) else: kwargs[field.ACCOUNT_TYPE] = 'vendor' kwargs[field.ACCOUNT_ID] = str(vendor_id) return function(*args, **kwargs) account_type, account_id = flask_request.get_grass_headers(request) alt_account_type = request.args.get(field.ACCOUNT_TYPE) alt_account_id = request.args.get(field.ACCOUNT_ID) # If no account data from headers, check query parameters if not account_type and not account_id: account_type = alt_account_type account_id = alt_account_id elif alt_account_type or alt_account_id: # More than one method was used to pass in account data return flaskify(response.create_error_response( code=error.ERROR_ACCOUNT_DATA_CODE, message=error.ERROR_ACCOUNT_DATA_REQUIRED_MSG)) # Could not find complete account data if not account_type or not account_id: return flaskify(response.create_error_response( code=error.ERROR_ACCOUNT_DATA_CODE, message=error.ERROR_ACCOUNT_DATA_REQUIRED_MSG)) kwargs[field.ACCOUNT_TYPE] = account_type kwargs[field.ACCOUNT_ID] = account_id return function(*args, **kwargs) return wrapper def validate_header(validator): """A decorator to call when validating HTTP request headers. Args: validator (Draft3Validator): the JSON Draft3 Schema to validate against. Returns: callable: the wrapped function. """ def decorator(f): @wraps(f) def _validate_header(*args, **kwargs): """Validate the request headers against a validator. Args: request (werkzeug.local.LocalProxy): the request object from the handler. validator (Draft3Validator): the JSON Draft3 Schema to validate against. Returns: Response: error HTTP response, or continue to the calling function if successful. """ validation_response = json_schema.validate( dict(request.headers), validator) if validation_response.status != 200: return flaskify(validation_response) return f(*args, **kwargs) return _validate_header return decorator def validate_body(validator): """A decorator to call when validating HTTP request body. Args: validator (Draft3Validator): the JSON Draft3 Schema to validate against. Returns: callable: the wrapped function. """ def decorator(f): """Decorator for validate_body method.""" @wraps(f) def _validate_body(*args, **kwargs): """Validate the request body against a validator. Args: request (werkzeug.local.LocalProxy): the request object from the handler. validator (Draft3Validator): the JSON Draft3 Schema to validate against. Returns: Response: error HTTP response, or continue to the calling function if successful. """ validation_response = json_schema.validate( request.get_json(), validator) if validation_response.status != 200: return flaskify(validation_response) return f(*args, **kwargs) return _validate_body return decorator def parse_request_json(schema=None, partial=False): """Parse and optionally validate request JSON.""" try: request_json = request.get_json() except (BadRequest, UnsupportedMediaType): raise RequestError('Invalid JSON') # Verify request data is enveloped in dict (Marshmallow doesn't catch this) if not isinstance(request_json, dict): raise RequestError('Request envelope must be an object.') if not schema: return request_json result = schema().load(request_json, partial=partial) if result.errors: raise ValidationError(result.errors) if partial and not result.data: raise RequestError('Empty request') return result.data def explode_query_param_to_ids(key): """Expand command separated list of ids for request query params. Args: key (str): Name of query param Returns: list: List of ints Raises: ValueError: String could not be converted """ if key not in request.args: raise ValueError( error.VALIDATION_ERROR_MISSING_FIELD_MSG.format(key)) comma_joined_ids = request.args.get(key) try: ids = list(map(int, comma_joined_ids.split(','))) except ValueError: raise ValueError('Could not convert `{}` param to an int'.format(key)) for i in ids: if i <= 0: raise ValueError( 'Each number in `{}` param must be a positive int'.format( key)) return ids def is_jwt_identity_authorized(jwt_identity_id): """Check if the given JWT identity UUID is authorized. Args: jwt_identity_id (str): The identity UUID. """ return jwt_identity_id in header.AUTHORIZED_IDENTITIES def persist_and_strip_suggestions(result, tuid, attribute_id_key, write_func, attr_type): """Persist attribute suggestions to the DB and strip reasons from response items. Reads persist/user_uuid/review_queue_id from the current request context. Mutates result.message['items'] in-place to remove the 'reasons' field. """ persist = request.args.get('persist', '').lower() == 'true' user_uuid = request.headers.get(header.ORCHARD_IDENTITY_ID) review_queue_id = request.args.get('review_queue_id', type=int) suggested_items = result.message.get('items', []) if isinstance(result.message, dict) else [] if persist and suggested_items: if not user_uuid or not review_queue_id: g.log.warning( 'Skipping %s suggestion persistence for tuid=%s: ' 'user_uuid=%s, review_queue_id=%s', attr_type, tuid, user_uuid, review_queue_id, ) else: try: write_func( unique_track_id=tuid, suggestions=[ {'attribute_id': item[attribute_id_key], 'reasons': item['reasons']} for item in suggested_items ], user_uuid=user_uuid, review_queue_id=review_queue_id, ) except Exception: g.log.exception( 'Failed to persist %s suggestions for tuid=%s', attr_type, tuid ) for item in suggested_items: item.pop('reasons', None) class DatetimeEncoder(json.JSONEncoder): """Custom JSON encoder.""" def default(self, o): """Method converting datetime to string.""" if isinstance(o, datetime.datetime) or isinstance(o, datetime.date): return o.isoformat() return super().default(o)