"""User handlers. All endpoints related to user operations - session, user listing, user details, contacts, managers. """ import re from connector_neo4j import Neo4jSession from flask import g, request from owsrequest import flask_request from owsrequest.constants import headers as header_constants from owsresponse import response from owsresponse.adaptors.flask import flaskify from pythonfeatures import pythonfeatures from pythonfeatures.constants import split as split_constants from users import constants from users.app import app from users.connectors.sentry import sentry_client from users.logic import notifications, user_info from users.utils import regex_utils from users.utils.api_utils import validate_request_data from users.utils.basic_utils import get_pagination from users.validation.schemas.dataloader import OrchAdminUsersDataloader @app.route('/users//session/', methods=['GET']) def get_user_session_metadata_for_app(user_id, app): """Get user session metadata for an app. NOTE: may be deprecated soon. Consider using /users/session/ instead. Args: user_id (str): vend_contact.id or orchardmin_user.id. app (str): The app to fetch session metadata for (ex: oa or alw). Returns: flask.Response: containing session metadata. """ return flaskify(user_info.get_user_session_metadata_for_app(user_id, app)) @app.route('/users//session//raw', methods=['GET']) @Neo4jSession(use_v2=True, database=constants.NEO4J_DATABASE_NAME) def get_user_session_metadata_for_app_raw(user_id, app): """Get user session metadata for an app using raw SQL. NOTE: may be deprecated soon. Consider using /users/session/ instead. Args: user_id (str): vend_contact.id or orchardmin_user.id. app (str): The app to fetch session metadata for (ex: oa or alw). Returns: flask.Response: containing session metadata. """ data = user_info.get_user_session_metadata_for_app_raw(user_id, app) if data and data.message.get('active') == 'N': g.log.warning(f'Inactive User login: {user_id} for app {app}') return flaskify(data) @app.route('/users//session//linked-accounts', methods=['GET']) @Neo4jSession(use_v2=True, database=constants.NEO4J_DATABASE_NAME) def get_linked_accounts(vc_id, app_name): """Get this user's linked account details. Args: vc_id (str): vend_contact.id (renamed from ambiguous user_id). app_name (str): The app to fetch session metadata for (ex: oa or alw). Returns: flask.Response: containing session metadata. """ return flaskify(user_info.get_linked_accounts(vc_id, app_name)) @app.route('/users/session/', methods=['GET']) @Neo4jSession(use_v2=True, database=constants.NEO4J_DATABASE_NAME) def get_user_session_metadata_for_app_with_grass(app): """Get user session metadata for an app using grass headers. Args: app (str): The app to fetch session metadata for (ex: oa or alw). Returns: flask.Response: containing session metadata. """ headers_response = flask_request.verify_grass_headers(request, True) if not headers_response: return flaskify(headers_response) header_account_type, header_account_id = flask_request.get_grass_headers(request) user_id = request.headers.get(constants.ORCHARD_USER_ID) if '{}:'.format(app) in user_id: user_id = user_id.replace('{}:'.format(app), '') metadata = user_info.get_account_session_data(user_id, app) if not metadata: return flaskify(metadata) # Append type-agnostic info for account identity purposes metadata.message['account_name'] = metadata.message['vendor']['name'] if metadata.message['subaccount']: metadata.message['account_name'] = metadata.message['subaccount']['subaccount_name'] metadata.message['account_type'] = header_account_type metadata.message['account_id'] = header_account_id return flaskify(metadata) @app.route('/users/roles/', methods=['GET']) def get_user_roles(app): """Get user roles for an app user. Used when calling from another microserice that has already been through grass Args: app (str): The app to fetch roles for (ex: oa or alw). Returns: flask.Response: containing roles. """ if not regex_utils.is_recognisable_app(app): return response.create_error_response( status=400, code=constants.ERROR_INVALID_APP, message='Invalid app.' ) if app == 'alw': headers_response = flask_request.verify_grass_headers(request, True) user_id = request.headers.get(constants.ORCHARD_USER_ID) if app == 'oa': headers_response = flask_request.verify_profile_headers(request) user_id = request.headers.get(header_constants.ORCHARD_PROFILE_ID) if not headers_response: return flaskify(headers_response) if '{}:'.format(app) in user_id: user_id = user_id.replace('{}:'.format(app), '') return flaskify(user_info.get_roles_for_user(user_id, app)) @app.route('/users', methods=['GET']) def get_users(): """Get users list filtered by user type. Params: type (str): User type. Returns: flask.Response: containing the oauth clients. """ from users import config user_type = request.args.get('type') if user_type not in config.USER_INFO_USER_TYPES: return flaskify( response.create_error_response( constants.UNSUPPORTED_USER_TYPE_CODE, constants.UNSUPPORTED_USER_TYPE ) ) users = user_info.get_users(user_type) if not users: return users # Wrap users list into dict before serializing it. users.message = {'items': users.message} return flaskify(users) @app.route('/users/orchadmin/dataloader', methods=['POST']) @validate_request_data(OrchAdminUsersDataloader()) def get_orchadmin_users(): """ Handler to fetch OrchAdmin user data for a list of user IDs. Expects: JSON body with a 'user_ids' field containing a list of strings. Returns: Flask response with a JSON object: { "users": [ { "user_id": "123", "first_name": "John", "last_name": "Doe", "active": true }, ... ] } Raises: Captures and reports exceptions to Sentry. """ data = request.get_json() user_ids = data['user_ids'] try: return flaskify(response.Response(user_info.get_orchadmin_users(user_ids))) except Exception as e: sentry_client.capture_exception() return flaskify(response.create_fatal_response(e.args)) @app.route('/users/', methods=['GET']) @Neo4jSession(use_v2=True, database=constants.NEO4J_DATABASE_NAME) def get_user(user_id): """Get information about a user. Args: user_id (str): User Id (ex: oa:123 or alw:123). Returns: flask.Response: containing the oauth clients. """ return flaskify(user_info.get_user_raw(user_id)) @app.route('/users/get-alw-users-by-email', methods=['GET']) def get_user_by_email(): """Get information about ALW user(s) based on email. Params: email (str): ALW user email. Returns: flask.Response: containing user(s) info. """ user_email = request.args.get('email') if not user_email: return flaskify( response.create_error_response( code=constants.BAD_PARAMS_ERROR_CODE, message=constants.BAD_PARAMS_ERROR_message, status=400, ) ) return flaskify(user_info.get_alw_users_by_email(user_email.lower())) @app.route('/users//minimum-details', methods=['GET']) def get_minimum_user_details(user_id): """Get vendor and subaccount id about a user. Args: user_id (str): User Id (ex: oa:123 or alw:123). Request params: include_roles (str): should the roles be included Returns: flask.Response: containing the oauth clients. """ if not re.match('^(?:oa|alw):\d+$', user_id): # noqa return flaskify( response.create_fatal_response(message='user_id must be in the form oa:123 or alw:123.') ) user_id_parts = user_id.split(':') user_type = user_id_parts[0] user_id_id = user_id_parts[1] include_roles = request.args.get('include_roles') if include_roles: include_roles = True else: include_roles = False return flaskify(user_info.get_users_minimum_details(user_type, user_id_id, include_roles)) @app.route('/users/document', methods=['GET']) def get_user_document(): """GET cloudsearch friendly json document for lambda-alooma-cloudsearch. Params: user_id (int): vend_contact.id contact_id (int): vend_contact.contact_id subaccount_id (int): Unique id of subaccount Returns: Response: Flask response """ user_id = request.args.get('user_id') contact_id = request.args.get('contact_id') subaccount_id = request.args.get('subaccount_id') if user_id or contact_id or subaccount_id: return flaskify( user_info.get_user_basic_info( user_id=user_id, contact_id=contact_id, subaccount_id=subaccount_id ) ) return flaskify( response.create_error_response( code=constants.BAD_PARAMS_ERROR_CODE, message=constants.BAD_PARAMS_ERROR_message, status=400, ) ) @app.route('/users/verify-login', methods=['POST']) def verify_login_user(): """Return details for user with matching login and password. Params: login (str): orchard login name. password (str): password hashed by auth0. feature (str): (Optional) Feature flag name to get its status. Returns: flask.Response: containing user metadata. """ creds = request.get_json() feature_name = request.args.get('feature') user_data = user_info.verify_alw_login(creds.get('login'), creds.get('password')) if feature_name and user_data: attributes_response = user_info.get_attributes_for_vend_contact( user_data.message.get('vend_contact_id') ) if not attributes_response: return flaskify(attributes_response) is_enabled = ( pythonfeatures.get_single_feature_by_attributes( feature_name, attributes_response.message ) ).message == split_constants.FEATURE_ENABLED user_data.message['feature'] = {feature_name: is_enabled} return flaskify(user_data) @app.route('/users//status', methods=['PUT']) @Neo4jSession(transaction=True, use_v2=True, database=constants.NEO4J_DATABASE_NAME) def update_user_status(vend_contact_id): """Update status of vend_contact as Y or N. Params: vend_contact_id (int): the vendor contact id. Returns: flask.Response: JSON object representing user data. """ resp = flask_request.verify_grass_headers(request, True) if not resp: return flaskify(resp) user_update_status = request.get_json() result = user_info.update_user_status(vend_contact_id, user_update_status) attributes_response = user_info.get_attributes_for_vend_contact(vend_contact_id) if not attributes_response: return flaskify(attributes_response) is_enabled_feature_deactivate_workstation_user = ( pythonfeatures.get_single_feature_by_attributes( constants.FEATURE_DEACTIVATE_WORKSTATION_USER, attributes_response.message ) ).message == split_constants.FEATURE_ENABLED if is_enabled_feature_deactivate_workstation_user: if not result: return flaskify(result) vendor_id = result.message.get('account', {}).get('vendor_id') subaccount_id = result.message.get('account', {}).get('subaccount_id') if user_update_status.get('active') == 'N': # deactivate user in auth0 if this is the last account auth0_result = user_info.deactivate_user(vend_contact_id, 'alw') notifications.unsubscribe_labelprofile(vend_contact_id, vendor_id, subaccount_id) if not auth0_result: return flaskify(auth0_result) else: auth0_result = user_info.reactivate_user(vend_contact_id) notifications.subscribe_labelprofile(vend_contact_id, vendor_id, subaccount_id) if not auth0_result: return flaskify(auth0_result) return flaskify(result) @app.route('///users', methods=['GET']) def get_contact_details(account_type, account_id): """Get list of users. Gets list of users for vendor or subaccount. This endpoint accepts query string '/users?status=disable'. status=disable - It will fetch only disabled users Default, it fetches only active users. Params: account_type (string): vendor or subaccount. account_id (int): Unique id of vendor_id or subaccount_id. Returns: Flask.response: contains list of dicts of user information. """ resp = flask_request.verify_grass_access(request, True, **{account_type: account_id}) if not resp: return flaskify(resp) user_id = request.headers.get(constants.ORCHARD_USER_ID) if user_id is not None and 'alw' in user_id: user_id = user_id.split('alw:')[1] status = request.args.get('status') page = get_pagination(request) return flaskify( user_info.get_contact_details( account_type, account_id, user_id, status, page.offset, page.limit ) ) @app.route('/ows///primary-contact', methods=['GET']) def get_primary_contact(account_type, account_id): """Get primary contact. Gets primary contact for vendor or subaccount. This is designated as the account with master='Y'. Params: account_type (string): vendor or subaccount. account_id (int): Unique id of vendor_id or subaccount_id. Returns: Flask.response: contains dicts of user information. """ active = request.args.get('active') return flaskify(user_info.get_primary_contact(account_type, account_id, active)) @app.route('/users/alw/', methods=['PATCH']) def update_vend_contact_user(user_id): """Patch a Vend contact User.""" data = request.get_json() return flaskify(user_info.update_vend_contact_user(user_id, data)) @app.route('/ws/users/roles/') def get_vend_contact_user_roles(user_id): """Get user roles for vend_contact users for neo4j sync. Args: user_id (int): vend_contact_id . Returns: flask.Response: containing roles. """ return flaskify(user_info.get_roles_for_user(user_id, 'alw')) @app.route('/users/product_managers', methods=['GET']) def get_product_managers(): """Get list of Product Managers. Returns: flask.Response: containing the Product Managers. """ resp = flask_request.verify_grass_headers(request) if not resp: return flaskify(resp) product_managers = user_info.get_product_managers() if not product_managers: return flaskify(product_managers) return flaskify(product_managers) @app.route('/users/account_managers', methods=['GET']) def get_account_managers(): """Get list of account managers for PM/ALR assignments. Returns: Flask.response. """ return flaskify(user_info.get_account_managers()) @app.route('/users/closers', methods=['GET']) def get_closers(): """Get list of active closers. Returns: Flask.response. """ return flaskify(user_info.get_active_closers())