"""Auth0 handlers. All endpoints related to Auth0 operations. """ import re from connector_neo4j import Neo4jSession from flask import g, request from owsrequest import error_response from owsrequest.constants import headers as header_constants from owsresponse import response from owsresponse.adaptors.flask import flaskify from users import config, constants from users.app import app from users.connectors import redis from users.logic import auth0_client, profiles, user_info from users.models import identities as identities_model from users.utils import authorization from users.utils.api_utils import validate_request_data from users.validation.schemas.create_org_invitation import CreateOrgInvitation from users.validation.schemas.org_members import AddOrganizationMembers @app.route('/auth0/users//all-accounts', methods=['GET']) def get_all_accounts_with_auth0_id(auth0_id): """Get all accounts with this auth0_user_id. Args: auth0_id (str): Auth0 id. Returns: flask.Response: containing session metadata. """ if 'auth0' in auth0_id: auth0_id = auth0_id.replace('auth0|', '') include_deleted = False include_support_email = False if request.args.get('include_deleted'): include_deleted = True if request.args.get('include_support_email'): include_support_email = True return flaskify( user_info.get_accounts_with_auth0_id(auth0_id, include_deleted, include_support_email) ) @app.route('/auth0/users', methods=['GET']) def get_paginated_auth0_users(): """Get a page of Auth0 users.""" return flaskify( auth0_client.get_auth0_users_paginated( page=request.args.get('page', 0), per_page=request.args.get('per_page', 100), q=request.args.get('q', None), ) ) @app.route('/auth0/users/bulk', methods=['GET']) def get_bulk_paginated_auth0_users(): """Get a page of Auth0 emails.""" auth0_user_ids = request.args.getlist('auth0_ids') if not auth0_user_ids: return flaskify( response.create_error_response( code=constants.BAD_PARAMS_ERROR_CODE, message=constants.BAD_PARAMS_ERROR_message, status=400, ) ) return flaskify(auth0_client.get_bulk_auth0_users_paginated(auth0_user_ids)) @app.route('/auth0/users', methods=['POST']) def create_user_with_auth0(): """Create an Auth0 User.""" audit_user = g.request_context.identity_id data = request.get_json() return flaskify(auth0_client.create_user(data, audit_user)) @app.route('/auth0/users/', methods=['GET']) def read_user_with_auth0(user_id): """Fetch an Auth0 User.""" if 'auth0' not in user_id and 'google-apps' not in user_id: user_id = 'auth0|{}'.format(user_id) return flaskify(auth0_client.read_user(user_id)) @app.route('/auth0/users//picture', methods=['GET']) def read_user_picture_with_auth0_user_id(user_id): """Fetch an Auth0 User picture by Auth0 user_id, cached. If user does not exist, cache their picture as empty string because Redis does not accept None. This is to avoid hitting Auth0 multiple times for a faulty or non-existent user id. Params: user_id (str): auth0 user id. Returns: flask.Response: containing user picture. """ if not user_id.startswith(('auth0', 'google-apps')): user_id = f'auth0|{user_id}' user_picture_cache = redis.client.get(f'user_picture_{user_id}') # it could be empty string if user_picture_cache is not None: picture = user_picture_cache.decode('utf8') return flaskify(response.Response(message=None if picture == '' else picture)) user_response = auth0_client.read_user(user_id) # empty string for None if not user_response: picture = '' else: picture = user_response.message.get('picture', None) picture = '' if picture is None else picture redis.client.set(f'user_picture_{user_id}', picture, ex=config.REDIS_CACHE_TTL) return flaskify(response.Response(message=None if picture == '' else picture)) @app.route('/auth0/users/', methods=['PATCH']) @Neo4jSession(transaction=True, use_v2=True, database=constants.NEO4J_DATABASE_NAME) def update_user_with_auth0(user_id): """Patch an Auth0 User.""" if 'auth0' not in user_id: user_id = 'auth0|{}'.format(user_id) data = request.get_json() email = data.get('email') if email and not authorization.authorize_email_change(user_id, new_email=email): return flaskify(error_response.create_error_forbidden()) admin_identity_id = g.request_context.identity_id # Check that the new email is not already in use by another identity if email: existing_auth0_user = auth0_client.read_user(user_id).message if existing_auth0_user.get('email') != email: existing_identity = identities_model.get_identity_by_email(email) if existing_identity.status == 200: g.log.info( 'Attempt to change email to one already in use by another identity', resources={ 'auth0_user_id': user_id, 'email': email, 'in_use_identity_id': existing_identity.message.get('id'), 'admin_identity_id': admin_identity_id, }, ) return flaskify( response.create_error_response( status=409, code=constants.ERROR_CODE_IDENTITY_ALREADY_EXISTS, message=constants.ERROR_MESSAGE_IDENTITY_ALREADY_EXISTS_EMAIL, ) ) return flaskify(auth0_client.update_user(user_id, data)) @app.route('/auth0/users/', methods=['DELETE']) def delete_user_with_auth0(user_id): """Delete an Auth0 User.""" if 'auth0' not in user_id: user_id = 'auth0|{}'.format(user_id) return flaskify(auth0_client.delete_user(user_id)) @app.route('/auth0/reset-users/', methods=['DELETE']) def reset_user_with_auth0(auth0_id): """Reset all WStation users with this auth0 id. This is for cucumber tests, so they can be re-used like non-migrated users. Params: auth0_id (str): auth0 user id. Returns: flask.Response: containing user metadata. """ if 'auth0' in auth0_id: auth0_id = auth0_id.replace('auth0|', '') return flaskify(user_info.reset_users_auth0_details(auth0_id)) @app.route('/auth0/reset-mfa/', methods=['DELETE']) @Neo4jSession(use_v2=True, database=constants.NEO4J_DATABASE_NAME) def reset_mfa_devices_for_user(auth0_id): """Reset all mfa devices for this auth0 id. Params: auth0_id (str): auth0 user id. Returns: flask.Response: containing user metadata. """ identity = identities_model.get_identity_by_auth0_id_dict(auth0_id) if not identity: return flaskify(response.create_not_found_response()) # only frontend can call this or a lambda with M2M with proper user. admin_identity_id = g.request_context.jwt_identity_id if not admin_identity_id: return flaskify(error_response.create_error_forbidden()) if not identities_model.can_reset_mfa(admin_identity_id, identity): return flaskify(error_response.create_error_forbidden()) if 'auth0' not in auth0_id: auth0_id = 'auth0|{}'.format(auth0_id) return flaskify(auth0_client.reset_mfa_devices_for_user(auth0_id)) @app.route('/auth0/recovery', methods=['POST']) def create_user_password_reset(): """ Trigger password recovery email for a user. Args: None. Expects a JSON payload in the request body. JSON Payload: email (str, required): The email address of the user to recover the password for. connection (str, optional): The Auth0 connection to use. Defaults to "art-relations". client_id (str, optional): The Auth0 client generating this request. Returns: Flask response: 200 if password reset email was sent or 400 with an error message if not. """ data = request.get_json() # TODO: use auth0_client.get_connection_id_from_name connection = data.get('connection', config.AUTH0_CONNECTION) email = data.get('email', None) if email is None: return flaskify(response.Response({'message': 'The email key is required.'}, status=400)) client_id = data.get('client_id', None) try: # This will not fail with a bad email args = [data['email'].lower(), connection] if client_id: args.append(client_id) auth0_client.send_password_reset(*args) return flaskify(response.Response({}, status=200)) except Exception: return flaskify( response.Response({'message': 'Could not send password reset.'}, status=400) ) @app.route('/auth0/send-verification-email', methods=['POST']) def resend_verify_email(): """Trigger Verify email. Params: user_id (str): Auth0 user id. """ data = request.get_json() user_id = data.get('user_id', None) if user_id is None: return flaskify(response.Response({'message': 'The user_id key is required.'}, status=400)) if 'auth0' not in user_id: user_id = 'auth0|{}'.format(user_id) return flaskify(auth0_client.resend_verify_email(user_id)) @app.route('/auth0/organizations/', methods=['GET']) def get_auth0_organization(org_name: str): return flaskify(auth0_client.get_organization(org_name)) @app.route('/users/auth0/email/', methods=['GET']) def get_auth0_users_with_email(email): """Get user user_id with auth0 details. Params: email (str): Email address. Returns: flask.Response: containing user metadata. """ return flaskify(auth0_client.get_auth0_users(email.lower())) @app.route('/users/bulk-auth0-password-reset', methods=['POST']) def bulk_password_reset(): """Bulk password reset.""" data = request.get_json() user_ids = data.get('user_ids', None) if not user_ids: return flaskify(response.Response({'message': 'No users to reset.'}, status=204)) auth0_ids = [ 'auth0|{}'.format(user_id) if 'auth0' not in user_id else user_id for user_id in user_ids ] return flaskify(auth0_client.bulk_password_reset(auth0_ids)) @app.route('/users/auth0/single-signon', methods=['POST']) def send_auth0_single_signon(): """Send notification for Auth0 single sign on. Params: user_id (str): alw:Vend contact id for now. auth0_id (str): Auth0 user id. email (str): auth0 user email. Returns: flask.Response: containing user metadata. """ data = request.get_json() user_type, user_id = data.get('user_id').split(':') auth0_value = data.get('auth0_id') if 'auth0|' not in auth0_value: auth0_value = 'auth0|{}'.format(auth0_value) auth0_type, auth0_id = auth0_value.split('|') existing_label = user_info.get_label_names(user_type, user_id) if not existing_label: return flaskify(existing_label) already_linked_labels = user_info.get_label_names_for_auth0(auth0_id) if not already_linked_labels: return flaskify(already_linked_labels) existing_label.message.extend(already_linked_labels.message) labels = list(set(existing_label.message)) result = auth0_client.send_sqs_message( data.get('user_id'), data.get('email').lower(), data.get('auth0_id'), labels ) return flaskify(result) @app.route('/users/auth0//primary', methods=['PUT']) @Neo4jSession(transaction=True, use_v2=True, database=constants.NEO4J_DATABASE_NAME) def set_auth0_primary_user(auth0_id): """Set the primary vend contact for an auth0 user. Params: auth0_id (str): auth0 unique id. Returns: flask.Response: containing user metadata. """ if 'auth0' not in auth0_id: auth0_id = 'auth0|{}'.format(auth0_id) data = request.get_json() user_id = data.get('user_id', None) # Optional param sent to help in case where auth0 id may be set to identity id on vend_contact identity_id = data.get('identity_id', None) return flaskify( user_info.set_auth0_primary_user( auth0_id=auth0_id, user_id=user_id, identity_id=identity_id, ) ) @app.route('/users/auth0-update', methods=['POST']) @Neo4jSession(transaction=True, use_v2=True, database=constants.NEO4J_DATABASE_NAME) def update_user_with_auth0_details(): """Update user user_id with auth0 details. Params: user_id (int): Vend contact id for now. user_type (str): Alw for now. auth0_id (str): auth0 unique id. Returns: flask.Response: containing user metadata. """ from users.connectors.sentry import sentry_client creds = request.get_json() auth0_id = creds.get('auth0_id') if not auth0_id or not creds.get('user_id') or not creds.get('user_type'): return flaskify( response.create_not_found_response({'missing': 'Missing required params or empty.'}) ) if 'auth0' in auth0_id: auth0_id = auth0_id.replace('auth0|', '') db_response = user_info.update_auth0_details( creds.get('user_id'), creds.get('user_type'), auth0_id ) if not db_response: return flaskify(db_response) if db_response.message.get('active') == 'N': return flaskify(db_response) # once art_relations is updated, check role and create InsightsProfile roles = user_info.get_roles_for_user(creds.get('user_id'), 'alw').message allowed_roles = ['analytics', 'administrator'] user_roles = [role.lower() for role in roles['role_names']] insight_roles = list(set(allowed_roles) & set(user_roles)) if insight_roles: try: # create / edit INSIGHTS_PROFILE payload = { 'profile_name': db_response.message.get('login'), 'profile_type': constants.INSIGHTS_PROFILE, 'roles': insight_roles, } new_profile = profiles.create_profile_if_not_exist(auth0_id, payload).message db_response.message[constants.INSIGHTS_PROFILE] = new_profile # create relationship to Vendor / SubAccount resource = db_response.message.get('account') if resource.get('subaccount_id'): profiles.grant_access( auth0_id, new_profile, constants.RESOURCE_SUBACCOUNT, resource.get('subaccount_id'), ) else: profiles.grant_access( auth0_id, new_profile, constants.RESOURCE_VENDOR, resource.get('vendor_id') ) except Exception: sentry_client.capture_exception() return flaskify(db_response) @app.route('/users//auth0/deactivate/', methods=['DELETE']) @Neo4jSession(transaction=True, use_v2=True, database=constants.NEO4J_DATABASE_NAME) def deactivate_user(user_id, app): """Deactivate a specific user contact and block in auth0 if necessary. Params: user_id (str): vend_contact.id or orchardmin_user.id. app (str): The app to deactivate for (ex: oa or alw). Returns: flask.Response: containing user metadata. """ return flaskify(user_info.deactivate_user(user_id, app)) @app.route('/auth0/reset-user/', methods=['DELETE']) def reset_user_with_user_id(user_identifier): """Reset all WStation users with this auth0 id. This is for cucumber tests, so they can be re-used like non-migrated users. Args: user_identifier (str): User Id (ex: oa:123 or alw:123). Returns: flask.Response: containing user metadata. """ if not re.match('^(?:oa|alw):\d+$', user_identifier): # noqa return flaskify( response.create_fatal_response( message='user_identifier must be in the form oa:123 or alw:123.' ) ) user_id_parts = user_identifier.split(':') user_type = user_id_parts[0] user_id = user_id_parts[1] return flaskify(user_info.reset_user_auth0_details(user_id, user_type)) # Moved from identity.py @app.route('/auth0//organizations', methods=['GET']) def get_auth0_user_organizations(auth0_user_id): """Return a list of organizations that an auth0 user is in. Args: auth0_user_id (str): auth0 user id. """ if not g.request_context.context_type == header_constants.CONTEXT_TYPE_PROFILE: return flaskify(error_response.create_error_forbidden()) if 'auth0' not in auth0_user_id: auth0_user_id = 'auth0|{}'.format(auth0_user_id) return flaskify(auth0_client.list_user_organizations(auth0_user_id)) @app.route('/auth0/invite/organization-member', methods=['POST']) @validate_request_data(CreateOrgInvitation()) @Neo4jSession(transaction=True, use_v2=True, database=constants.NEO4J_DATABASE_NAME) def create_organization_invitation(): """Create an invitation to an organization. Params: email (str): Required. User email. brand (str): Required. Org/brand name from Auth0. This is the name not the display name. admin_name (str): Optional. Name of the admin who is inviting the user. auth0_application_name (str): Optional. Name of Auth0 SPA application. Default is Insights. user_metadata (dict): Optional. Auth0 user_metadata. Returns: Flask.response. """ if not g.request_context.context_type == header_constants.CONTEXT_TYPE_PROFILE: return flaskify(error_response.create_error_forbidden()) data = request.get_json() admin_identity_id = g.request_context.identity_id return flaskify(auth0_client.create_organization_invitation(data, admin_identity_id)) @app.route('/auth0/add/organization-members', methods=['POST']) @validate_request_data(AddOrganizationMembers()) def create_organization_members(): """Add members to an organization. Params: brand (str): Required. Org/brand name from Auth0. This is the name not the display name. members (str): Required. List of Auth0 userids. Returns: Flask.response. """ 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()) data = request.get_json() admin_identity_id = g.request_context.identity_id # Platform-3673: Force brand to lower case if data.get('brand'): data['brand'] = data.get('brand').lower() return flaskify(auth0_client.create_organization_members(data, admin_identity_id))