"""Profile handlers. All endpoints related to profile CRUD operations. """ from connector_neo4j import Neo4jSession from flask import g, request from owsrequest import access as flask_access, error_response, flask_request from owsrequest.constants import headers as header_constants from owsresponse import response from owsresponse.adaptors.flask import flaskify from users import constants from users.app import app from users.logic import profiles from users.utils.api_utils import validate_request_data from users.utils.basic_utils import get_mock_response from users.validation.schemas.application import GetApplicationsRequestSchema from users.validation.schemas.identity_to_profile import ( IdentityToProfile as IdentityToProfileSchema, ) from users.validation.schemas.profile import Profile as ProfileSchema @app.route('/users/identity//application//profiles', methods=['GET']) @Neo4jSession(use_v2=True, database=constants.NEO4J_DATABASE_NAME) def get_profiles(orchard_identity, app_name): """GET all profiles for this orchard_identity that this app can support. Platform-3347: Non-Settings Profiles will only be returned if they have Access or Admin Access to at least one Resource. This is meant to be called from Auth0 rules. Extra checks in handler as this endpoint is excluded from rules check. Args: orchard_identity (str): Identity (auth0 id). app_name (str): Application name eg: workstation, orchardgo etc. """ profile_types = request.args.get('types').split(',') if request.args.get('types') else [] if orchard_identity in constants.MOCK_USER_IDENTITIES: if app_name.lower() == 'switchboard': result = get_mock_response(orchard_identity, 'get_switchboard_profiles') elif app_name.lower() == 'frontend-podcast': result = get_mock_response(orchard_identity, 'get_podcast_profiles') else: result = get_mock_response(orchard_identity, 'get_profiles') return flaskify( response.Response( {'items': result, 'pagination': {'type': 'none', 'total_records': len(result)}} ) ) verify = flask_request.verify_profile_headers(request) if not verify: return flaskify(verify) header_identity = request.headers.get(header_constants.ORCHARD_IDENTITY_ID) header_uuid = request.headers.get(header_constants.ORCHARD_IDENTITY_UUID) # We allow CONTEXT_TYPE_NONE when there are no headers at all but if some profile headers are # there, then it should have ORCHARD_IDENTITY_ID. if ( not header_identity and not g.request_context.context_type == header_constants.CONTEXT_TYPE_NONE ): # noqa return flaskify(error_response.create_error_forbidden()) if ( header_identity and not header_identity == orchard_identity and not header_uuid == orchard_identity ): return flaskify( response.create_error_response( code=flask_access.ERROR_CODE_BAD_HEADERS, message="Cannot access other user's profile.", ) ) return flaskify( profiles.get_profiles_for_identity(orchard_identity, app_name.lower(), profile_types) ) @app.route('/users/identity//applications', methods=['GET']) @validate_request_data(GetApplicationsRequestSchema(), source='args') @Neo4jSession(use_v2=True, database=constants.NEO4J_DATABASE_NAME) def get_applications(orchard_identity_id): """GET all applications for given orchard_identity. Params: orchard_identity_id (str): The identity whose application information we are retrieving role (str): Optional query parameter to retrieve only applications where identity has certain role. Returns: flask.Response: containing list of application information including role and audit information """ if ( g.request_context.context_type != constants.PROFILE_CONTEXT_TYPE or not g.request_context.profile_type ): return flaskify(error_response.create_error_incomplete_profile_headers()) role = request.args.get('role') resource_type = request.args.get('resource_type') resource_uuid = request.args.get('resource_uuid') # get brand for this user. identity_object = profiles.get_identity_from_graph_tx(orchard_identity_id) if not identity_object: return flaskify(identity_object) brand = ( request.args.get('brand') or identity_object.message.get('default_brand') or constants.ORCHARD_BRAND ) return flaskify( profiles.get_applications_for_identity_tx( orchard_identity_id, role, brand, resource_type, resource_uuid, ) ) @app.route('/profile/identity/', methods=['POST']) @validate_request_data(ProfileSchema()) @Neo4jSession(transaction=True, use_v2=True, database=constants.NEO4J_DATABASE_NAME) def create_profile_for_identity(orchard_identity_id): """POST profile json body to create a new Profile for an Identity. Body: profile (json): object representing the profile to be created - profile_id (int): the id of the Profile relative to `profile_type` - profile_name (str): the name of the Profile - profile_type (str): the type of profile e.g. (LabelProfile, ArtistProfile, etc) - roles (list): list of strings reprenting roles e.g. ['catalog', 'analytics'] Args: orchard_identity_id (str): Identity id (auth0 id). """ return flaskify(profiles.create_profile(orchard_identity_id, request.json)) @app.route('/profile/profile_id//profile_type/', methods=['GET']) @app.route( '/profile/profile_id//profile_type/', methods=['GET'], defaults={'profile_id': None}, ) @app.route( '/profile/profile_uuid/', methods=['GET'], defaults={'profile_id': None, 'profile_type': None}, ) @Neo4jSession(use_v2=True, database=constants.NEO4J_DATABASE_NAME) def get_profile_by_id_and_type(profile_id, profile_type, profile_uuid=None): """GET a Profile by type and id. Args: - profile_id (int): the id of the Profile - profile_type (str): the type of profile e.g. (LabelProfile, ArtistProfile, etc) - profile_uuid (str): Optionally you can send only profile's 'uuid. """ if profile_uuid: return flaskify(profiles.get_profile_by_uuid(profile_uuid)) g.log.info(f'Using get_profile_by_id_and_type with profile_id: {profile_type}:{profile_id}') return flaskify(profiles.get_profile(profile_id, profile_type)) @app.route('/profile/profile_id//profile_type/', methods=['DELETE']) @app.route( '/profile/profile_id//profile_type/', methods=['DELETE'], defaults={'profile_id': None}, ) @app.route( '/profile/profile_uuid/', methods=['DELETE'], defaults={'profile_id': None, 'profile_type': None}, ) @Neo4jSession(transaction=True, use_v2=True, database=constants.NEO4J_DATABASE_NAME) def delete_profile_by_id_and_type(profile_id, profile_type, profile_uuid=None): """DELETE a Profile by type and id. Args: - profile_id (int): the id of the Profile - profile_type (str): the type of profile e.g. (LabelProfile, ArtistProfile, etc) - profile_uuid (str): Optionally you can send only profile's 'uuid. """ if profile_uuid: return flaskify(profiles.delete_profile_by_uuid(profile_uuid)) g.log.info(f'Using delete_profile_by_id_and_type with profile_id: {profile_type}:{profile_id}') return flaskify(profiles.delete_profile(profile_id, profile_type)) @app.route('/profile/profile_id//profile_type/', methods=['PATCH']) @app.route( '/profile/profile_id//profile_type/', methods=['PATCH'], defaults={'profile_id': 0}, ) @app.route( '/profile/profile_uuid/', methods=['PATCH'], defaults={'profile_id': 0, 'profile_type': 'ArtistProfile'}, ) @validate_request_data(ProfileSchema(), partial=True) @Neo4jSession(transaction=True, use_v2=True, database=constants.NEO4J_DATABASE_NAME) def update_profile_by_id_and_type(profile_id, profile_type, uuid=None): """PATCH a Profile by type and id. Body: profile (json): object with fields to be updated - profile_name (str): the name of the Profile - roles (list): list of strings reprenting roles e.g. ['catalog', 'analytics'] Args: - profile_id (int): the id of the Profile - profile_type (str): the type of profile e.g. (LabelProfile, ArtistProfile, etc) - uuid (str): Optionally you can send only profile's 'uuid. """ if uuid: return flaskify(profiles.update_profile_by_uuid(uuid, request.json)) g.log.info(f'Using update_profile_by_id_and_type with profile_id: {profile_type}:{profile_id}') return flaskify(profiles.update_profile(profile_id, profile_type, request.json)) @app.route( '/users/identity//profile//', # NOQA methods=['POST'], ) @app.route( '/users/identity//profile//', methods=['POST'], defaults={'profile_id': 0}, ) @app.route( '/users/identity//profile/', methods=['POST'], defaults={'profile_id': 0, 'profile_type': 'InsightsProfile'}, ) @validate_request_data(IdentityToProfileSchema()) @Neo4jSession(transaction=True, use_v2=True, database=constants.NEO4J_DATABASE_NAME) def add_profile_to_identity(identity_id, profile_type, profile_id, profile_uuid=None): """Link an existing identity to an existing profile. Args: identity_id (str): Unique identity identifier. profile_type (str): Type of profile eg: ArtistProfile, LabelProfile. profile_id (int): Profile Identifier. profile_uuid (str): Optionally you can send only profile's 'uuid. """ if profile_uuid: return flaskify(profiles.add_profile_uuid_to_identity(identity_id, profile_uuid)) g.log.info(f'Using add_profile_to_identity with profile_id: {profile_type}:{profile_id}') return flaskify(profiles.add_profile_to_identity(identity_id, profile_id, profile_type)) @app.route( '/users/identity//profile//', methods=['DELETE'] ) @app.route( '/users/identity//profile//', methods=['DELETE'], defaults={'profile_id': 0}, ) @app.route( '/users/identity//profile/', methods=['DELETE'], defaults={'profile_id': 0, 'profile_type': 'InsightsProfile'}, ) @validate_request_data(IdentityToProfileSchema()) @Neo4jSession(transaction=True, use_v2=True, database=constants.NEO4J_DATABASE_NAME) def delete_profile_to_identity(identity_id, profile_id, profile_type, profile_uuid=None): """Delete an existing identity to profile relationship. Args: identity_id (str): Unique identity identifier. profile_id (int): Profile identifier. profile_type (str): Type of profile eg: ArtistProfile, LabelProfile. profile_uuid (str): Optionally you can send only profile's 'uuid. """ if profile_uuid: return flaskify(profiles.delete_profile_uuid_to_identity(identity_id, profile_uuid)) g.log.info(f'Using delete_profile_to_identity with profile_id: {profile_type}:{profile_id}') return flaskify(profiles.delete_profile_to_identity(identity_id, profile_id, profile_type)) # Aliases for the original /profile/... routes below, added so callers behind the # ows-users-proxy ALB (allowlist requires /auth0/users*) can reach this handler. # NOTE: exclude_paths only covers the form (same gap on the original route), # so uuid callers with profile headers hit the rules check. Revisit if uuid callers # start using these routes behind the proxy. @app.route( '/auth0/users/profile/profile_id//profile_type//identity', methods=['GET'], endpoint='get_identity_for_profile_auth0_users_id', ) @app.route( '/auth0/users/profile/profile_id//profile_type//identity', methods=['GET'], defaults={'profile_id': None}, endpoint='get_identity_for_profile_auth0_users_id_uuid', ) @app.route( '/auth0/users/profile/profile_uuid//identity', methods=['GET'], defaults={'profile_id': None, 'profile_type': None}, endpoint='get_identity_for_profile_auth0_users_uuid', ) @app.route( '/profile/profile_id//profile_type//identity', methods=['GET'] ) @app.route( '/profile/profile_id//profile_type//identity', methods=['GET'], defaults={'profile_id': None}, ) @app.route( '/profile/profile_uuid//identity', methods=['GET'], defaults={'profile_id': None, 'profile_type': None}, ) @Neo4jSession(use_v2=True, database=constants.NEO4J_DATABASE_NAME) def get_identity_for_profile(profile_id, profile_type, profile_uuid=None): """Get identity from graphdb. Extra checks in handler as this endpoint is excluded from rules check. Args: - profile_id (int): the id of the Profile - profile_type (str): the type of profile e.g. (LabelProfile, ArtistProfile, etc) - profile_uuid (str): Optionally you can send only profile's 'uuid. """ # Called by ows-grass auth/session with None headers. This check ensures that it is only called # from backend microservice or from frontend with profile headers that match url. if g.request_context.context_type not in [ header_constants.CONTEXT_TYPE_NONE, header_constants.CONTEXT_TYPE_PROFILE, ]: return flaskify(error_response.create_error_forbidden()) verify = flask_request.verify_profile_headers_match_route(request, profile_type, profile_id) if not verify: return flaskify(verify) if profile_uuid: return flaskify(profiles.get_identity_for_profile_uuid(profile_uuid)) g.log.info(f'Using get_identity_for_profile with profile_id: {profile_type}:{profile_id}') return flaskify(profiles.get_identity_for_profile(profile_id, profile_type)) @app.route('/profile/uuid/', methods=['GET']) @Neo4jSession(use_v2=True, database=constants.NEO4J_DATABASE_NAME) def get_profile_by_uuid(uuid): """Get a profile by uuid from graphdb. Args: uuid (str): uuid """ return flaskify(profiles.get_profile_by_uuid(uuid))