"""Application Handlers. Requests are redirected to handlers, which are responsible for getting information from the URL and passing it down to the logic layer. The way each layer talks to each other is through Response objects which defines the type status of the data and the data itself. Please note: the Orchard uses the term handlers over views as convention for clarity See: owsresponse.response for more details. """ from http import HTTPStatus import botocore import sentry_sdk from flask import g, jsonify, request from marshmallow import ValidationError from owsrequest import context from owsrequest import request as owsrequest from owsrequest.constants import headers as owsrequest_headers from owsresponse import response from owsresponse.adaptors.flask import flaskify from participant import config from participant.api import app from participant.constants import error, permissions from participant.constants import service as service_constants from participant.exceptions.invalid_usage import InvalidUsageException from participant.utils import exception @app.errorhandler(InvalidUsageException) def invalid_usage_exception_handler(invalid_usage_exception): """Handle all thrown InvalidUsageExceptions. Args: invalid_usage_exception (InvalidUsageException): A thrown InvalidUsageException Returns: flask.Response: Error response. """ return flaskify(invalid_usage_exception.response) @app.errorhandler(Exception) def exception_handler(err): """Handle error when uncaught exception is raised. Default exception handler. Note: Exception will also be sent to Sentry if config.SENTRY_DSN is set. Returns: flask.Response: A 400 or 500 response with JSON 'code' & 'message' payload. """ bad_request_message = 'Oopsies, I don\'t know how to respond to that ¯\\_(ツ)_/¯' bad_request_response = response.create_error_response( HTTPStatus.BAD_REQUEST, bad_request_message ) internal_server_error_message = ( 'I have erred and am unable to complete your request.' ) internal_server_error_response = response.create_fatal_response( internal_server_error_message ) if isinstance(err, ValidationError): return flaskify( response.create_error_response( code=error.ERROR_CODE_VALIDATION_ERROR, message={'error': err.messages} ) ) if ( isinstance(err, botocore.exceptions.ClientError) and (err.response.get('Error') or {}).get('Code') == 'NoSuchKey' ): return flaskify(bad_request_response) sentry_sdk.capture_exception() g.log.exception(err) if getattr(err, 'code', None) in range( HTTPStatus.BAD_REQUEST, HTTPStatus.INTERNAL_SERVER_ERROR ): return flaskify(bad_request_response) return flaskify(internal_server_error_response) @app.route(config.HEALTH_CHECK, methods=['GET']) def health(): """Check the health of the application.""" return jsonify({'status': 'ok'}) def get_vendor_and_subaccount(profile_type, profile_id): """Get vendor and subaccount. Args: profile_type (string): Profile type. profile_id (int): Profile id. Returns: (int, int): The tuple: (vendor_id, subaccount_id). """ ows_permissions_response = owsrequest.get( service_constants.OWS_PERMISSIONS, service_constants.OWS_PERMISSIONS_PROFILE_RESOURCE.format( profile_type=profile_type, profile_id=profile_id, resource='label' ), ) if ows_permissions_response.status_code != 200: exception.raise_exception(Exception, ows_permissions_response.status_code) return resources = ows_permissions_response.json()['items'] # Pull the first reference to a vendor or subaccount resource from result resource = next( ( resource for resource in resources if resource['type'].lower() in ( permissions.SUBACCOUNT_RESOURCE_TYPE.lower(), permissions.VENDOR_RESOURCE_TYPE.lower(), ) ), None, ) # If no vendor or subaccount resources, return no vendor_id, subaccount_id if not resource: return None, None # If the resource is a vendor, return only vendor_id if resource['type'] == permissions.VENDOR_RESOURCE_TYPE: return resource['id'], None # Return vendor_id, subaccount_id return resource['vendor_id'], resource['id'] def get_artist_profile_resources(profile_type, profile_id): """Get artist profile resources. Args: profile_type (string): Profile type. profile_id (int): Profile id. Returns: [dict]: Artist info resources. """ ows_permissions_response = owsrequest.get( service_constants.OWS_PERMISSIONS, service_constants.OWS_PERMISSIONS_PROFILE_RESOURCE.format( profile_type=profile_type, profile_id=profile_id, resource='ArtistInfo' ), ) if ows_permissions_response.status_code != 200: exception.raise_exception(Exception, ows_permissions_response.status_code) return resources = ows_permissions_response.json()['items'] return resources def _is_public_request(): """Check if this request is public and it doesn't require context check.""" url_rule = request.url_rule if url_rule and url_rule.rule in [ '/service//search', '/service//artist/', ]: service = (request.view_args or {}).get('service') return service in [config.SPOTIFY_SERVICE, config.APPLE_MUSIC_SERVICE] return False def _is_artist_links_request(): url_rule = request.url_rule return url_rule and url_rule.rule == '/artist-links' @app.before_request def before_request(): """Prepare for request.""" if _is_public_request(): return if _is_artist_links_request(): return # All code proceeding past this point must have a vendor and subaccount context. # TODO: This is a temporary code path to allow non-profile access for Switchboard. user_id = request.headers.get(owsrequest_headers.ORCHARD_USER_ID, '') if user_id == f'oa:{config.SWITCHBOARD_OA_USER_ID}': g.vendor_id, g.subaccount_id = _get_query_vendor_and_subaccount() return if user_id == f'oa:{config.PODCAST_OA_USER_ID}': return request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) if not request_context.profile_type or not request_context.profile_id: return if request_context.profile_type == owsrequest_headers.PROFILE_TYPE_ORCH_ADMIN: return if request_context.profile_type == owsrequest_headers.PROFILE_TYPE_PODCAST: return if request_context.profile_type not in [ owsrequest_headers.PROFILE_TYPE_LABEL, owsrequest_headers.PROFILE_TYPE_ARTIST, owsrequest_headers.PROFILE_TYPE_INSIGHTS, owsrequest_headers.PROFILE_TYPE_CONTENT, ]: exception.raise_exception( Exception, f'Unsupported profile_type: {request_context.profile_type}' ) return if request_context.profile_type == owsrequest_headers.PROFILE_TYPE_LABEL: profile_vendor_id, profile_subaccount_id = get_vendor_and_subaccount( request_context.profile_type, request_context.profile_id ) if not profile_vendor_id: exception.raise_exception(Exception, 'Missing label context.') return query_vendor_id, query_subaccount_id = _get_query_vendor_and_subaccount() # Check for sub-account profile if ( profile_subaccount_id and profile_vendor_id == query_vendor_id and profile_subaccount_id == query_subaccount_id ): g.vendor_id = profile_vendor_id g.subaccount_id = profile_subaccount_id return # Check for D3 and vendor profile if profile_subaccount_id is None and profile_vendor_id == query_vendor_id: g.vendor_id = profile_vendor_id g.subaccount_id = 0 return exception.raise_exception(Exception, 'Not in requested label context.') if request_context.profile_type == owsrequest_headers.PROFILE_TYPE_ARTIST: artist_info_resources = get_artist_profile_resources( request_context.profile_type, request_context.profile_id ) if not artist_info_resources: exception.raise_exception(Exception, 'No resources available.') g.artist_info_resources = [ai['id'] for ai in artist_info_resources] return def _get_query_vendor_and_subaccount(): """Get vendor and subaccount from request params.""" request_params = { **request.args.to_dict(), **(request.get_json(silent=True) or {}), } vendor_id = int(request_params.get('vendor_id', 0)) subaccount_id = int(request_params.get('subaccount_id', 0)) return vendor_id, subaccount_id @app.after_request def after_request(resp): """Prepare the response.""" return resp