"""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 for more details. """ import json from json import JSONDecodeError from connector_neo4j import Neo4jSession from ddtrace import tracer from flask import Response, g, request from owsrequest import error_response, flask_request from owsrequest.constants import headers as header_constants from owsresponse import response, status from owsresponse.adaptors.flask import flaskify from pythonfeatures import pythonfeatures from pythonfeatures.constants import split as split_constants from werkzeug.exceptions import HTTPException from account import config from account.api import app, authorization_backend from account.connectors.sentry import sentry_client from account.constants import error, header from account.constants.connectors import NEO4J_DATABASE_NAME from account.constants.constants import ( FEATURE_USE_NEO4J_MANAGED_TX, WARNING_MESSAGE_VENDOR_ACCESS_CHECK, ) from account.constants.tenants import TenantType from account.logic import ( country, feature, genre, identity as identity_logic, owner, region, sme, subaccount, supplier, vendor, ) from account.logic.application import ( application as app_logic, collaborator as collaborator_app_logic, label_participant as label_participant_logic, subaccount as subaccount_app_logic, vendor as vendor_app_logic, ) from account.logic.resource_getters.account import AccountByIdResourceGetter from account.logic.resource_getters.company_brand import CompanyBrandByNameResourceGetter from account.models import neo4j_vendor from account.utils import handler_util, pagination from account.utils.api_utils import validate_request_data from account.validation import validation from account.validation.schemas import create_vendor from account.validation.schemas.create_subaccount import CreateSubaccountSchema from account.validation.schemas.lookup import ( LookupSubaccountsBySubaccountIds, LookupSubaccountsByUuids, LookupVendorsByUuids, LookupVendorsByVendorIds, ) from account.validation.schemas.update_vendor import UpdateVendorSchema from account.validation.schemas.vendor_restricted_features import ( BulkAddVendorRestrictedFeaturesSchema, BulkRemoveVendorRestrictedFeaturesSchema, ) @app.route(config.HEALTH_CHECK) def health(): """Check the health of the application.""" return flaskify(response.Response({'status': 'ok'})) @app.route('/identity', methods=['GET']) def identity(): """Get identity of account with a grass session. Returns: Flask.response: a dict of account info. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) grass_account_type, grass_account_id = flask_request.get_grass_headers(request) user_id = request.headers.get('Orchard-user-id') if not grass_account_type and not user_id: return flaskify(response.Response(status=401)) identity_response = identity_logic.get_identity(grass_account_type, grass_account_id) if not identity_response and user_id: identity_response = identity_logic.get_user_details(user_id) return flaskify(identity_response) @app.route('/subaccount', methods=['HEAD']) def is_subaccount_type(): """Get if user in Grass headers is a subaccount. Returns: Flask.response """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) grass_account_type, grass_account_id = flask_request.get_grass_headers(request) if grass_account_type == header.GRASS_ACCOUNT_TYPE_SUBACCOUNT: return flaskify(response.Response(error.SUCCESS_CODE)) return flaskify( response.create_error_response( error.ERROR_CODE_NOT_TYPE, error.ERROR_MESSAGE_NOT_SUBACCOUNT ) ) @app.route('/subaccounts', methods=['GET']) def get_subaccounts(): """Get list of subaccounts. Gets list of subaccounts for vendor_id in Grass header. This endpoint can also accept query string. '/subaccounts?status=deactivated' status=deactivated - It will fetch only disabled subaccounts Default, it fetches only active subaccounts. Returns: Flask.response: contains list of dicts of subaccount information. When a vendor id has no subaccounts, this vendor is considered to not be a D3 and should have no concept of subaccounts. ows-account returns a 404 response to obfuscate the subaccounts concept. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) grass_account_type, grass_account_id = flask_request.get_grass_headers(request) page = pagination.get_pagination(request) validation = flask_request.verify_grass_access(request, required=True, vendor=grass_account_id) if not validation: return flaskify(validation) status = request.args.get('status') data = subaccount.get_subaccounts( grass_account_id, status, page_offset=page.offset, page_limit=page.limit ) return flaskify(data) @app.route('//subaccounts', methods=['GET']) def get_vendor_subaccounts(vendor_id): """Get list of subaccounts. Gets list of subaccounts given . Args: vendor_id (int): unique identifier for vendor. Returns: Flask.response: contains list of dicts of subaccount information. When a vendor id has no subaccounts, this vendor is considered to not be a D3 and should have no concept of subaccounts. ows-account returns a 404 response to obfuscate the subaccounts concept. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) page = pagination.get_pagination(request) validation = flask_request.verify_grass_access(request, required=False, vendor=vendor_id) if not validation: return flaskify(validation) data = subaccount.get_subaccounts(vendor_id, page_offset=page.offset, page_limit=page.limit) return flaskify(data) @app.route('/subaccount/', methods=['GET']) @tracer.wrap() def get_subaccount(subaccount_id): """Get a subaccount. Gets a subaccount, given . Args: subaccount_id (int): unique identifier for subaccount. Returns: Flask.response: contains dict of subaccount information. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) with tracer.trace('get_grass_headers_from_request'): grass_account_type, grass_account_id = flask_request.get_grass_headers(request) with tracer.trace('verify_grass_access_from_request'): validation = flask_request.verify_grass_access( request, required=False, vendor=grass_account_id, subaccount=subaccount_id ) if not validation: return flaskify(response=validation) # Validate this Vendor has permission to access this subaccount if grass_account_type == header.GRASS_ACCOUNT_TYPE_VENDOR: with tracer.trace('validate_vendor_has_permission'): data = subaccount.is_subaccount_for_vendor( subaccount_id, grass_account_id, return_result=True ) if not data: return flaskify(data) else: with tracer.trace('get_subaccount_with_subaccount_id'): data = subaccount.get_subaccount(subaccount_id) return flaskify(data) @app.route('//subaccount/', methods=['HEAD']) def is_subaccount_for_vendor(vendor_id, subaccount_id): """Validate subaccount belongs to vendor. Given vendor with , validates subaccount with is owned by vendor. Args: vendor_id (int): unique identifier for a vendor. subaccount_id (int): unique identifier for a subaccount. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) validation = flask_request.verify_grass_access(request, required=False, vendor=vendor_id) if not validation: return flaskify(validation) data = subaccount.is_subaccount_for_vendor(subaccount_id, vendor_id) return flaskify(data) @app.route('/distributor', methods=['HEAD']) def is_distributor_type(): """Validate Grass user is a distributor. Gets if vendor in Grass headers is a distributor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) grass_account_type, grass_account_id = flask_request.get_grass_headers(request) validation = flask_request.verify_grass_access( request, required=True, vendor=grass_account_id, subaccount=grass_account_id ) if not validation: return flaskify(response=validation) if grass_account_type == header.GRASS_ACCOUNT_TYPE_SUBACCOUNT: return flaskify( response.create_error_response( error.ERROR_CODE_NOT_FOUND, error.ERROR_MESSAGE_NO_MATCH, status=204 ) ) data = vendor.is_distributor(grass_account_id) return flaskify(data) @app.route('//distributor', methods=['HEAD']) def is_vendor_a_distributor(vendor_id): """Validate vendor is a distributor. Given vendor with , gets if vendor is a distributor. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) validation = flask_request.verify_grass_access(request, required=False, vendor=vendor_id) if not validation: return flaskify(response=validation) data = vendor.is_distributor(vendor_id) return flaskify(data) @app.route('/sony/vendor/', methods=['HEAD']) def is_vendor_sme(vendor_id): """Validate vendor is Sony distributed. Given vendor with , returns status code 200 if vendor is Sony distributed, otherwise returns 404. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) validation = flask_request.verify_grass_access(request, required=False, vendor=vendor_id) if not validation: return flaskify(validation) data = sme.is_sme_vendor(vendor_id) return flaskify(data) @app.route('/sony/subaccount/', methods=['HEAD']) def is_subaccount_sme(subaccount_id): """Validate subaccount is Sony distributed. Given subaccount with , returns status code 200 if subaccount is Sony distributed, otherwise returns 404. Args: subaccount_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) grass_account_type, grass_account_id = flask_request.get_grass_headers(request) validation = flask_request.verify_grass_access( request, required=False, subaccount=subaccount_id, vendor=grass_account_id ) if not validation: return flaskify(validation) # If Vendor is passed from GRASS, validate that this has permission # to access this subaccount if grass_account_type == header.GRASS_ACCOUNT_TYPE_VENDOR: data = subaccount.is_subaccount_for_vendor(subaccount_id, grass_account_id) if not data: return flaskify(data) data = sme.is_sme_subaccount(subaccount_id) return flaskify(data) @app.route('/vendor/', methods=['HEAD']) def is_valid_vendor(vendor_id): """Validate if vendor_id is a valid vendor. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(vendor.is_vendor(vendor_id)) @app.route('/vendor/', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_vendor(vendor_id): """Return basic information about vendor. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) pdp_check = False if not access_rule_decision: pdp_check = authorization_backend.is_authorized( action='view', resource_id=vendor_id, resource_type='account', resource_getter=AccountByIdResourceGetter(vendor_id), ) if not pdp_check: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) use_managed_tx = ( pythonfeatures.get_single_feature(FEATURE_USE_NEO4J_MANAGED_TX, g.request_context) ).message == split_constants.FEATURE_ENABLED if not pdp_check: profile_vendor_access_call = ( vendor.profile_has_access_tx if use_managed_tx else vendor.profile_has_access ) # Grass-Account-Type and Grass-Id are not required, but if they are passed # then they are verified grass_account_type, grass_id = flask_request.get_grass_headers(request) validation_response = response.Response() if grass_account_type == header.GRASS_ACCOUNT_TYPE_SUBACCOUNT: if not subaccount.is_subaccount_for_vendor(grass_id, vendor_id): validation_response = response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, status=403, ) else: validation_response = flask_request.verify_grass_access( request, required=False, vendor=vendor_id ) if not validation_response: return flaskify(validation_response) request_profile_id = g.request_context.profile_id request_profile_type = g.request_context.profile_type if config.ENVIRONMENT == config.QA_ENVIRONMENT and ( request_profile_id is None and request_profile_type is None ): pass # Suppress warnings due to curl requests in QA that omit headers elif not profile_vendor_access_call(request_profile_id, request_profile_type, vendor_id): g.log.warning( WARNING_MESSAGE_VENDOR_ACCESS_CHECK.format( request_profile_id, request_profile_type, vendor_id ) ) if use_managed_tx: return flaskify(vendor.get_vendor_label_info(vendor_id, neo_tx=True)) return flaskify(vendor.get_vendor_label_info(vendor_id)) @app.route('/vendors/dataloader', methods=['POST']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_vendors() -> Response: """Handler to get bulk vendors Expects JSON payload with a list of vendor uuids. Example payload: ['uuid1', 'uuid2'] Returns: Flask.response. """ vendor_uuids = request.get_json() if not isinstance(vendor_uuids, list) or not all( isinstance(item, str) for item in vendor_uuids ): return flaskify( response.create_error_response( error.ERROR_CODE_INVALID_INPUT, error.ERROR_MESSAGE_INVALID_REQUEST ) ) # temporary logging to understand the behavior of the clients headers = request.headers.copy() authorization_header = next( (key for key in headers.keys() if key.lower() == 'authorization'), None ) if authorization_header: headers.pop(authorization_header, None) identity_id = g.request_context.identity_id profile_id = g.request_context.profile_id profile_type = g.request_context.profile_type # for oa / alw users, the check will work as these legacy headers # get converted to profile headers. access_rule_decision = flask_request.verify_rules_access_standalone(request) try: if not access_rule_decision: # Get accessible vendors through pdp auth backend. accessible_vendor_uuids = vendor.get_pp_accessible_vendors(vendor_uuids=vendor_uuids) else: # Get accessible vendors through neo4j accessible_vendor_uuids = neo4j_vendor.accessible_vendors( profile_id=profile_id, profile_type=profile_type, vendor_uuids=vendor_uuids, identity_id=identity_id, ) # Log warning if any vendors are inaccessible if len(accessible_vendor_uuids) != len(vendor_uuids): g.ows.log.warn( 'vendor-dataloader: called to access inaccessible vendor', resources={ 'profile': { 'profile_id': profile_id, 'profile_type': profile_type, 'identity_id': identity_id, }, 'headers': { 'profile_id': headers.get('Orchard-Profile-Id'), 'profile_type': headers.get('Orchard-Profile-Type'), 'identity_id': headers.get('Orchard-Identity-Id'), 'user_id': headers.get('Orchard-User-Id'), }, 'vendors': set(vendor_uuids) - set(accessible_vendor_uuids), 'input_vendors': vendor_uuids, 'accessible_vendors': accessible_vendor_uuids, }, ) return flaskify( response.Response( vendor.get_vendors( accessible_vendor_uuids=accessible_vendor_uuids, vendor_uuids=vendor_uuids ) ) ) except Exception as e: return flaskify(response.create_fatal_response(e.args)) @app.route('/vendors/names/dataloader', methods=['POST']) def get_vendors_names() -> Response: """Handler to get vendor names in bulk Expects JSON payload with a list of vendor uuids. Example payload: ['uuid1', 'uuid2'] Returns: Flask.response. """ vendor_uuids = request.get_json() if not isinstance(vendor_uuids, list) or not all( isinstance(item, str) for item in vendor_uuids ): return flaskify( response.create_error_response( error.ERROR_CODE_INVALID_INPUT, error.ERROR_MESSAGE_INVALID_REQUEST ) ) try: return flaskify(response.Response(vendor.get_vendor_names(vendor_uuids))) except Exception as e: return flaskify(response.create_fatal_response(e.args)) @app.route('/subaccounts/names/dataloader', methods=['POST']) def get_subaccounts_names() -> Response: """Handler to get subaccount names in bulk Expects JSON payload with a list of vendor uuids. Example payload: ['uuid1', 'uuid2'] Returns: Flask.response. """ subaccount_uuids = request.get_json() if not isinstance(subaccount_uuids, list) or not all( isinstance(item, str) for item in subaccount_uuids ): return flaskify( response.create_error_response( error.ERROR_CODE_INVALID_INPUT, error.ERROR_MESSAGE_INVALID_REQUEST ) ) try: return flaskify(response.Response(subaccount.get_subaccount_names(subaccount_uuids))) except Exception as e: return flaskify(response.create_fatal_response(e.args)) @app.route('/vendor', methods=['PATCH']) @validate_request_data(create_vendor.CreateVendorSchema()) def create_update_vendor(): """Create a new vendor or update an existing vendor. Params: vendor_id (int): Required for updates. Unique identifier for a vendor. vendor_name (str): Required for creates. email (str): Required. email + vendor_name = unique identifier for new vendor create. owner (str | None): Optional. owner name. Default is orchard. label_identifier (str): Optional. label identifier. status (str | None): Optional. label status. Default is signed. company_brand (str | None): Optional. The Company's brand this vendor belongs to. service_tier_uuid (str): Required. uuid of the service tier the vendor will be in. migrated_to_abacus (bool | None): Optional. country (str | None): Optional. Country affiliated with brand, iso alpha 3 format. assigned_to (int | None): Optional. orchardadmin_user id the vendor is assigned to. assigned_reviewer (int | None): Optional. Foreign key to orchadmin_users table. source (str): Required. The source that is creating the vendor Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) if not g.request_context.context_type == header_constants.CONTEXT_TYPE_PROFILE: return flaskify(error_response.create_error_forbidden()) @Neo4jSession(transaction=True, use_v2=True, database=NEO4J_DATABASE_NAME) def call_logic(): data = request.get_json() return flaskify(vendor.create_or_update_vendor(data)) return call_logic() @tracer.wrap() @app.route('/v2/vendors', methods=['POST']) @Neo4jSession(transaction=True, use_v2=True, database=NEO4J_DATABASE_NAME) @validate_request_data(create_vendor.V2CreateVendorSchema()) def create_vendor(): """Create a vendor with minimal attributes. Properties: -name (str):Full name of the Vendor -owner (str): Owner of this Vendor (what business unit it maps back to) -company_brand (str): Company's brand name. eg: awal, columbia, theorchard, hrs -service_tier_uuid (str): Unique id for service tier -payment_currency (str): 3 char currency code. Not validated! Returns: Flask.response """ request_data = request.json company_brand = request_data['company_brand'] # Use Authorization Backend to verify # identity has permissions to create an account # under the desired company_brand if not authorization_backend.is_authorized( action='create', resource_id=0, resource_type='account', resource_getter=CompanyBrandByNameResourceGetter(company_brand), ): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) result_response = vendor.create_vendor(request_data) return flaskify(result_response) @app.route('/v2/vendor/', methods=['PATCH']) @validate_request_data(UpdateVendorSchema()) def update_vendor(vendor_id): """Update partial vendor information. Args: -vendor_id (int): unique identifier for a vendor. Properties: -name (str): full name of the Vendor -company_brand (str): Company's brand name. eg: awal, columbia, theorchard, hrs -company_brand_uuid (str): unique id for company brand. -service_tier_uuid (str): unique id for service tier. -label_identifier (enum): 'Frontline','Client Services','Catalog','D3','Film','TV','Test'. . . . for full list of supported updatable properties see update_vendor validation schema. Returns: Flask.response: field(s) changed and update timestamp """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) if not g.request_context.context_type == header_constants.CONTEXT_TYPE_PROFILE: return flaskify(error_response.create_error_forbidden()) @Neo4jSession(transaction=True, use_v2=True, database=NEO4J_DATABASE_NAME) def call_logic(): data = request.get_json() user_id = request.headers.get(header_constants.ORCHARD_USER_ID) if user_id: if user_id.startswith('oa:'): data['last_modified_by'] = user_id.lstrip('oa:') else: return flaskify( response.Response( status=401, message=(f'Unauthorized user with user_id = {user_id}'), ) ) return flaskify(vendor.update_vendor(vendor_id, data)) return call_logic() @app.route('/vendor/brand/', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_company_brand(vendor_id): """Return company brand associated with given label. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ use_managed_tx = ( pythonfeatures.get_single_feature(FEATURE_USE_NEO4J_MANAGED_TX, g.request_context) ).message == split_constants.FEATURE_ENABLED access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) if use_managed_tx: return flaskify(vendor.get_vendor_company_brand_tx(vendor_id)) return flaskify(vendor.get_vendor_company_brand(vendor_id)) @app.route('/vendor//suppliers', methods=['GET']) def get_phys_suppliers(vendor_id): """Return vendor suppliers default selection. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(supplier.get_suppliers(vendor_id)) @app.route('/vendor//suppliers', methods=['PUT']) def set_phys_suppliers(vendor_id): """Set suppliers default selection. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) data = request.get_json(silent=True, force=True) or {} store_ids = data.get('store_ids') if store_ids is None: store_ids = [] return flaskify(supplier.set_suppliers(vendor_id, store_ids)) @app.route('/vendor//features', methods=['GET']) def get_enabled_features_for_vendor(vendor_id): """Get a list of enabled feature controls for a vendor. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response: containing the list of enabled feature controls. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) validation = flask_request.verify_grass_access(request, vendor=vendor_id) if not validation: return flaskify(response=validation) return flaskify(feature.get_enabled_features_for_vendor(vendor_id)) @app.route('/vendor//restricted_features/add', methods=['POST']) @validate_request_data(BulkAddVendorRestrictedFeaturesSchema()) def bulk_add_restricted_features_for_vendor(vendor_id): """Bulk add restricted features for a vendor. POST Body: feature_ids (list[int]): list of feature IDs. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) data = request.get_json() return flaskify(feature.bulk_add_restricted_features_for_vendor(vendor_id, data['feature_ids'])) @app.route('/vendor//restricted_features/remove', methods=['POST']) @validate_request_data(BulkRemoveVendorRestrictedFeaturesSchema()) def bulk_remove_restricted_features_for_vendor(vendor_id): """Bulk remove restricted features for a vendor. POST Body: feature_ids (list[int]): list of feature IDs. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) data = request.get_json() return flaskify( feature.bulk_remove_restricted_features_for_vendor(vendor_id, data['feature_ids']) ) @app.route('/vendor-currency-codes', methods=['GET']) @validation.reject_grass_headers def get_all_vendor_currency_codes(): """Get a list of all currency codes for vendors. Returns: Flask.response: containing the list of all vendor currency codes. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(vendor.get_all_vendor_currency_codes()) @app.route('/subaccount//document', methods=['GET']) @validation.reject_grass_headers def get_subaccount_document(subaccount_id): """Get single subaccount document for cloudsearch corpus. Should have no grass headers since this is intended for internal use Args: subaccount_id (int): subaccount id Returns: Response: Flask response """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(subaccount.get_subaccount_document(subaccount_id)) @app.route('/vendor//assigned-to', methods=['GET']) def get_vendor_assigned_to(vendor_id): """Get a single vendor's assigned_to property.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) result = vendor.get_vendor_assigned_to(vendor_id) return flaskify(result) @app.route('/vendor//secondary-internal-contact', methods=['GET']) def get_vendor_secondary_internal_contact(vendor_id): """Get a single vendor's secondary internal contact.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) result = vendor.get_vendor_secondary_internal_contact(vendor_id) return flaskify(result) @app.route('/vendor//document', methods=['GET']) @validation.reject_grass_headers def get_vendor_document(vendor_id): """Get single vendor document for cloudsearch corpus. Should have no grass headers since this is intended for internal use Args: vendor_id (int): vendor id Returns: Response: Flask response """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(vendor.get_vendor_document(vendor_id)) @app.route('/vendor//assigned-reviewer', methods=['GET']) def get_vendor_assigned_reviewer(vendor_id): """Get a single vendor's assigned_reviewer property.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) result = vendor.get_vendor_assigned_reviewer(vendor_id) return flaskify(result) @app.route('/subaccount//status', methods=['PUT']) def update_subaccount_status(subaccount_id): """Update subaccount status for given subaccout_id. Arge: subaccount_id (int): subaccount id Returns: Flask.response: Returns updated status of subaccount. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) is_active = request.get_json().get('active', None) if is_active is None or not isinstance(is_active, bool): return flaskify( response.create_error_response( code=error.ERROR_CODE_INVALID_INPUT, message=error.ERROR_MESSAGE_INVALID_SUBACCOUNT_STATUS.format(subaccount_id), status=400, ) ) headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_subaccount_ownership, subaccount_id=subaccount_id ) if not ownership_response: return flaskify(ownership_response) subaccount_status = subaccount.update_subaccount_status(subaccount_id, is_active) return flaskify(subaccount_status) @app.route('/vendor//service-details', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_vendor_service_details(vendor_id): """Get vendor service details.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) validation = flask_request.verify_grass_access(request, required=False, vendor=vendor_id) if not validation: return flaskify(validation) vendor_service_details = vendor.get_vendor_service_details(vendor_id) return flaskify(vendor_service_details) @app.route('/vendor//distributor', methods=['PATCH']) def set_vendor_distributor(vendor_id): """Set is_distributor field from N to Y. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(vendor.update_is_distributor_in_vendor(vendor_id)) @app.route('/owners', methods=['GET']) def get_owners(): """Get list of owners.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(owner.get_owners()) @app.route('/genres', methods=['GET']) def get_genres(): """Get list of genres from genre table. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(genre.get_genres()) @app.route('/countries', methods=['GET']) def get_countries(): """Get list of countries from country table. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(country.get_countries()) @app.route('/regions', methods=['GET']) def get_regions(): """Get list of regions from region table. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(region.get_regions()) @app.route('/service-tiers', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_service_tiers(): """Get list of service tiers and uuids from service tier nodes in neo4j. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(vendor.get_service_tiers()) @app.route('/parent_companies', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_parent_companies(): """Get list of parent companies from neo4j. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(vendor.get_parent_companies()) @app.route('/brands', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_company_brands(): """Get list of company brands from neo4j. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(vendor.get_company_brands()) @app.route('/lookup/vendors/uuids/', methods=['POST']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) @validate_request_data(LookupVendorsByUuids()) def lookup_vendors_by_uuids(): """Get list of vendor ids using their uuids. NOTE: This endpoint is configured to require NO access rule checks. This is because it is serving as a Policy Information Point for Permission Platform. Do not expose other properties or attributes via this endpoint. Returns: Flask.response. """ schema = LookupVendorsByUuids() request_payload = schema.load(request.get_json()) uuids = request_payload['uuids'] fetch_flags = request_payload.get('fetch_flags', []) if not uuids: return flaskify( response.create_error_response( error.ERROR_CODE_INVALID_INPUT, error.ERROR_MESSAGE_INVALID_REQUEST ) ) return flaskify(vendor.lookup_vendors_by_uuids(uuids, fetch_flags)) @app.route('/lookup/vendors/vendor-ids/', methods=['POST']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) @validate_request_data(LookupVendorsByVendorIds()) def lookup_vendors_by_vendor_ids(): """Get list of vendor uuids using their vendor_ids. NOTE: This endpoint is configured to require NO access rule checks. This is because it is serving as a Policy Information Point for Permission Platform. Do not expose other properties or attributes via this endpoint. Returns: Flask.response. """ schema = LookupVendorsByVendorIds() request_payload = schema.load(request.get_json()) vendor_ids = request_payload['vendor_ids'] fetch_flags = request_payload.get('fetch_flags', []) if not vendor_ids: return flaskify( response.create_error_response( error.ERROR_CODE_INVALID_INPUT, error.ERROR_MESSAGE_INVALID_REQUEST ) ) return flaskify(vendor.lookup_vendors_by_vendor_ids(vendor_ids, fetch_flags)) @app.route('/vendors/by-external-identifier-1/', methods=['GET']) def get_vendors_by_external_identifier(external_identifier_1): """Get a list of vendors by external_identifier_1. Note: This endpoint is only for internal use in process of creation SME vendors. Args: external_identifier_1 (str): external_identifier_1. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) owner_name = request.args.get('owner') result = vendor.get_vendors_by_external_identifier(external_identifier_1, owner_name) return flaskify(result) @app.route('/subaccount', methods=['POST']) @validate_request_data(CreateSubaccountSchema()) def create_subaccount(): """Create a new subaccount for given vendor_id. Params: vendor_id (int): Required for subaccount creation. subaccount_name (str): Required for creates. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) if not g.request_context.context_type == header_constants.CONTEXT_TYPE_PROFILE: return flaskify(error_response.create_error_forbidden()) data = request.get_json() return flaskify(subaccount.create_subaccount(data)) @app.errorhandler(Exception) def exception_handler(error): """Handle error when uncaught exception is raised. Default exception handler. Returns: flask.Response: An error response. """ g.log.exception(error) # pass through HTTP errors if isinstance(error, HTTPException): sentry_client.capture_exception() return error # now you're handling non-HTTP exceptions only parsed_error = str(error) http_response_status = status.INTERNAL_ERROR response_code = 'bad_request' message = 'Bad Request' try: parsed_error = json.loads(parsed_error) http_response_status = parsed_error.get('status') or http_response_status response_code = parsed_error.get('code') or response_code message = parsed_error.get('message') or message except JSONDecodeError: message = parsed_error pass if http_response_status == status.INTERNAL_ERROR: sentry_client.capture_exception() return flaskify( response.create_error_response(response_code, message, status=http_response_status) ) @app.route('/lookup/subaccounts/uuids/', methods=['POST']) @validate_request_data(LookupSubaccountsByUuids()) def lookup_subaccounts_by_uuids(): """Get list of subaccount ids using their uuids. NOTE: This endpoint is configured to require NO access rule checks. This is because it is serving as a Policy Information Point for Permission Platform. Do not expose other properties or attributes via this endpoint. Returns: Flask.response. """ schema = LookupSubaccountsByUuids() request_payload = schema.load(request.get_json()) uuids = request_payload['uuids'] fetch_flags = request_payload.get('fetch_flags', []) if not uuids: return flaskify( response.create_error_response( error.ERROR_CODE_INVALID_INPUT, error.ERROR_MESSAGE_INVALID_REQUEST ) ) return flaskify(subaccount.lookup_subaccounts_by_uuids(uuids, fetch_flags)) @app.route('/lookup/subaccounts/subaccount-ids/', methods=['POST']) @validate_request_data(LookupSubaccountsBySubaccountIds()) def lookup_subaccounts_by_ids(): """Get list of subaccount uuids using their subaccount_ids. NOTE: This endpoint is configured to require NO access rule checks. This is because it is serving as a Policy Information Point for Permission Platform. Do not expose other properties or attributes via this endpoint. Returns: Flask.response. """ schema = LookupSubaccountsBySubaccountIds() request_payload = schema.load(request.get_json()) subaccount_ids = request_payload.get('subaccount_ids', []) fetch_flags = request_payload.get('fetch_flags', []) if not subaccount_ids: return flaskify( response.create_error_response( error.ERROR_CODE_INVALID_INPUT, error.ERROR_MESSAGE_INVALID_REQUEST ) ) return flaskify(subaccount.lookup_subaccounts_by_subaccount_ids(subaccount_ids, fetch_flags)) @app.route('/vendor//applications', methods=['GET']) @handler_util.check_admin_access(tenant_type=TenantType.ACCOUNT, tenant_uuid_key='vendor_uuid') def get_vendor_applications(vendor_uuid): """Get applications and roles for a Vendor.""" return flaskify(vendor_app_logic.get_vendor_applications(vendor_uuid)) @app.route('/subaccount//applications', methods=['GET']) @handler_util.check_admin_access( tenant_type=TenantType.SUBACCOUNT, tenant_uuid_key='subaccount_uuid' ) def get_subaccount_applications(subaccount_uuid): """Get applications and roles for a Subaccount.""" return flaskify(subaccount_app_logic.get_subaccount_applications(subaccount_uuid)) @app.route('/collaborator//applications', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) @handler_util.check_admin_access( tenant_type=TenantType.COLLABORATOR, tenant_uuid_key='collaborator_uuid' ) def get_collaborator_applications(collaborator_uuid): """Get applications and roles for a Collaborator.""" return flaskify(collaborator_app_logic.get_collaborator_applications(collaborator_uuid)) @app.route('/label-participant//applications', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) @handler_util.check_admin_access( tenant_type=TenantType.LABEL_PARTICIPANT, tenant_uuid_key='lp_uuid' ) def get_label_participant_applications(lp_uuid): """Get applications and roles for a label participant.""" applications = label_participant_logic.get_lp_applications(lp_uuid) return flaskify(response.Response({'applications': applications})) @app.route('/applications/dataloader', methods=['POST']) def get_applications_by_ids(): """Endpoint to get applications by ids.""" application_ids = request.get_json() return flaskify(app_logic.get_applications_by_ids(application_ids)) @app.route('/roles/dataloader', methods=['POST']) def get_roles_by_ids(): """Endpoint to get roles by ids.""" role_ids = request.get_json() return flaskify(app_logic.get_roles_by_ids(role_ids)) @app.route('/vendor//master-contact', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_master_contact(vendor_id): """Get master contact for vendor.""" if not authorization_backend.is_authorized( action='view_account_info', resource_id=vendor_id, resource_type='account', resource_getter=AccountByIdResourceGetter(vendor_id), ): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) return flaskify(vendor.get_master_contact(vendor_id))