"""User information logic layer module. Contains functions that work with general information related to user accounts. """ from datetime import datetime, timedelta, timezone import json import re from time import time from typing import Optional import uuid from flask import g from owsresponse import response from owsresponse.adaptors.flask import flaskify from pythonfeatures import pythonfeatures from pythonfeatures.constants import context as context_constants from users import config, constants from users.logic import auth0_client from users.models import ( identities, ows_account, profiles, social_auth_item, user_info, user_info_raw, ) from users.utils import dataloader_util, regex_utils def get_users(user_type, user_ids=None): """Fetch OA and ALW user records. Args: user_type (str): Type of account to return users for (oa/alw). user_ids: (list): A list of integer user_ids. Returns: response.Response: An object containing a list of users. """ if user_type == constants.USER_INFO_USER_TYPE_OA: return user_info.fetch_oa_users(user_ids) elif user_type == constants.USER_INFO_USER_TYPE_ALW: return user_info.fetch_alw_users(user_ids) raise NotImplementedError('User type {} is not supported.'.format(user_type)) def get_orchadmin_users(user_ids: list[str]) -> dict[str, list[dict[str, str | bool] | None]]: """ Retrieves and formats OrchAdmin user data for a given list of user IDs. Args: user_ids: (list[str]): List of user ID strings to fetch. Returns: dict[str, list[dict[str, str | bool] | None]]: A dictionary with a 'users' key containing a list of user info dictionaries, in the same order as user_ids. Missing users are represented as None. """ users = user_info.get_orchadmin_users(user_ids) return { 'users': dataloader_util.format_for_dataloader( users, user_ids, 'user_id', ) } def get_user_raw(user_id): """Fetch OA and ALW user records. Args: user_id (str): User Id (ex: oa:123 or alw:123). Returns: response.Response: An object containing the user info. """ if not regex_utils.is_recognisable_user_id(user_id): return 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] if not user_id_id or int(user_id_id) < 1: return response.create_not_found_response(message='User not found.') if user_type == constants.USER_INFO_USER_TYPE_OA: return user_info_raw.fetch_oa_user_raw(user_id_id) return user_info_raw.fetch_vend_contact_details(user_id_id) def get_users_minimum_details(user_type, user_id, include_roles): """Fetch OA and ALW user records from orchadmin or vend_contact only. Args: user_type (str): Type of account to return users for (oa/alw). user_id: (int): user_id. Returns: response.Response: An object containing users details. """ if user_type == constants.USER_INFO_USER_TYPE_ALW: if include_roles: return user_info_raw.fetch_vend_contact_raw_with_roles(user_id) else: return user_info_raw.fetch_vend_contact_raw(user_id) if user_type == constants.USER_INFO_USER_TYPE_OA: return user_info_raw.fetch_oa_user_raw(user_id) def get_user_session_metadata_for_app(user_id, app): """Get user session metadata for an app. NOTE: may be deprecated soon. Consider using get_account_session_data instead. Args: user_id (str): The user id (ex: orchadmin.id or vend_contact.id). app (str): The app to fetch session metadata for (ex: oa or alw). Returns: response.Response: An object containing the user session metadata. """ if not regex_utils.is_recognisable_app(app): return response.create_error_response( status=400, code='INVALID_APP', message='Invalid app.' ) if app == 'alw': return user_info.fetch_alw_session_user_metatada(user_id) if app == 'oa': return response.create_error_response( status=501, code='NOT_IMPLEMENTED', message='Not implemented.' ) 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 get_account_session_data instead. Args: user_id (str): The user id (ex: orchadmin.id or vend_contact.id). app (str): The app to fetch session metadata for (ex: oa or alw). Returns: response.Response: An object containing the user session metadata. """ if not regex_utils.is_recognisable_app(app): return response.create_error_response( status=400, code='INVALID_APP', message='Invalid app.' ) if app == 'alw': ar_data = user_info.fetch_alw_session_user_metatada_raw(user_id) if not ar_data: return ar_data auth0_id = ar_data.message.get('auth0_user_id') if auth0_id: # use localization from neo4j for WS users. identity = identities.get_identity_by_auth0_id(auth0_id).message if not identity: # ideally this should never happen. return ar_data ar_data.message['language'] = identity.get('localization', ar_data.message['language']) feature_response = pythonfeatures.get_single_feature_by_attributes( constants.FEATURE_WS_NEO4J_NUMBER_FORMAT, {'user_id': f'alw:{user_id}'} ) if feature_response.message == 'enabled': # use number_format from neo4j for WS users. number_format = constants.NUMBER_FORMAT_MAPPING_TO_LEGACY.get( identity.get('number_format') ) or ar_data.message.get('number_format') ar_data.message['number_format'] = number_format return ar_data if app == 'oa': return response.create_error_response( status=501, code='NOT_IMPLEMENTED', message='Not implemented.' ) def get_linked_accounts(vc_id, app): """Get all different accounts that are linked to this user. Args: vc_id (str): vend_contact.id (renamed from ambiguous user_id). app (str): The app to fetch session metadata for (ex: oa or alw). Returns: response.Response: An object containing the user session metadata. """ if not regex_utils.is_recognisable_app(app): return response.create_error_response( status=400, code='INVALID_APP', message='Invalid app.' ) if app == 'alw': return _get_linked_accounts_by_identity(vc_id) if app == 'oa': return response.create_error_response( status=501, code='NOT_IMPLEMENTED', message='Not implemented.' ) def _get_linked_accounts_by_identity(vc_id): """Resolve linked accounts via Neo4j Identity -> LabelProfile traversal. Instead of matching linked accounts by auth0_user_id in MySQL (which breaks when IDs diverge for Google SSO users), this resolves them entirely through the Identity graph: vc_id (which is the LabelProfile.profileId) goes straight into Neo4j to find the parent Identity, then all sibling LabelProfiles under that Identity are the linked accounts. No auth0_user_id involved. Args: vc_id (int): vend_contact.id (= LabelProfile.profileId in Neo4j). Returns: response.Response: A list of linked account details. """ # 1. Find the Identity node that owns this LabelProfile identity_response = identities.get_identity_for_profile(int(vc_id), constants.LABEL_PROFILE) if not identity_response.message or identity_response.status != 200: g.log.warning(f'Identity not found for vc_id={vc_id}') return response.Response(message=[]) identity_id = identity_response.message.get('id') if not identity_id: return response.Response(message=[]) # 2. Get LabelProfiles with active vendor access (excludes DELETED_HAS_ACCESS_TO) label_profiles = profiles.get_linked_label_profiles(identity_id) if not label_profiles: return response.Response(message=[]) # 3. Extract profileId values (these map to vend_contact.id) profile_ids = [p.get('profile_id') for p in label_profiles if p.get('profile_id')] if not profile_ids: return response.Response(message=[]) # 4. Fetch full account details from MySQL using profile IDs result = user_info.get_linked_account_details_by_profile_ids(vc_id, profile_ids) # 5. Enrich MySQL results with profile_uuid from Neo4j if result.status == 200 and isinstance(result.message, list): uuid_by_profile_id = { p.get('profile_id'): p.get('uuid') for p in label_profiles if p.get('profile_id') } for account in result.message: account['profile_uuid'] = uuid_by_profile_id.get(account.get('vc_id')) return result def get_accounts_with_auth0_id(auth0_id, include_deleted=False, include_support_email=False): """Get all different accounts that are linked to this user. Args: auth0_id (str): Auth0 id without auth0| prefix. include_deleted (bool): Flag to decide if we include deactivated users. include_support_email (bool): Flag to decide if we include support email details. Returns: response.Response: An object containing the user session metadata. """ if include_support_email: result = user_info.get_all_account_details_with_auth0_id(auth0_id, include_deleted) else: result = user_info.get_all_accounts_with_auth0_id(auth0_id, include_deleted) if not result.message: g.log.warning( constants.WARNING_MESSAGE_EMPTY_RESPONSE.format('get_all_accounts_with_auth0_id') ) return result def get_primary_vend_contact_for_identity(identity_id): """Get the primary vend_contact_id for an identity. Resolves using Neo4j LabelProfile -> MySQL vend_contact mapping, without using auth0_user_id as glue. Resolution chain: 1. Neo4j: Get LabelProfile profileIds for the identity 2. Single profile -> return its profileId (= vend_contact_id) 3. Multiple profiles, disambiguate via MySQL: a. auth0_primary='Y' in vend_contact -> return it b. Single active vend_contact -> return it Args: identity_id (str): Identity node id (UUID). Returns: response.Response: with {'vend_contact_id': int} or 404. """ identity_data = identities.get_identity_label_profile_ids(identity_id) if not identity_data: return response.create_not_found_response( message=constants.ERROR_MESSAGE_IDENTITY_NOT_FOUND ) profile_ids = identity_data.get('profile_ids', []) if not profile_ids: return response.create_not_found_response(message='No LabelProfiles found for identity.') # Single profile — unambiguous if len(profile_ids) == 1: return response.Response(message={'vend_contact_id': profile_ids[0]}) # Multiple profiles — disambiguate via MySQL vend_contacts = user_info.get_active_vend_contacts_by_ids(profile_ids) if not vend_contacts: return response.create_not_found_response( message='Could not resolve primary vend_contact for identity.' ) primary = next((vc for vc in vend_contacts if vc['auth0_primary'] == 'Y'), None) if primary: return response.Response(message={'vend_contact_id': primary['vend_contact_id']}) # Single active vend_contact if len(vend_contacts) == 1: return response.Response(message={'vend_contact_id': vend_contacts[0]['vend_contact_id']}) # Ambiguous — cannot resolve return response.create_not_found_response( message='Could not resolve primary vend_contact for identity.' ) def get_account_session_data(user_id, app): """Get user session data with additional info such as linked accounts. Args: user_id (str): The user id (ex: orchadmin.id or vend_contact.id). app (str): The app to fetch session metadata for (ex: oa or alw). Returns: response.Response: An object containing the user session metadata. """ if not regex_utils.is_recognisable_app(app): return response.create_error_response( status=400, code='INVALID_APP', message='Invalid app.' ) if app == 'alw': user_response = user_info.fetch_alw_session_user_metatada_raw(user_id) if not user_response: return user_response linked = get_linked_accounts(user_id, app) if not linked: return linked user_response.message.update({'linked_accounts': linked.message}) auth0_id = user_response.message.get('auth0_user_id') if auth0_id: # fetch account data from neo4j neo4j_metadata = identities.get_account_session_by_identity(auth0_id, user_id).message if not neo4j_metadata: # ideally this should never happen. return user_response user_response.message['identity'] = neo4j_metadata['identity'] user_response.message['vendor']['service_tier'] = neo4j_metadata['service_tier'] user_response.message['vendor']['company_brand'] = neo4j_metadata['company_brand'] return user_response if app == 'oa': return response.create_error_response( status=501, code='NOT_IMPLEMENTED', message='Not implemented.' ) def verify_alw_login(orchard_login, password_hash): """Get user session metadata for an app. Args: orchard_login (str): vend_contact login password_hash (str): salt hashed password Returns: response.Response: An object containing the user session metadata. """ return user_info_raw.fetch_alw_user_by_login(orchard_login, password_hash) def verify_auth0_invitation(email): """ Verify if the given email has a valid Auth0 invitation. Args: email (str): Email to verify Returns: Response: Response object with valid boolean flag and status """ identity_uuid = None user = {} try: identity_req = identities.get_identity_by_email(email.lower()) try: identity = flaskify(identity_req) user = identity.json if user is None: raise except Exception as e: raise ValueError('user doesnt exists') from e try: identity_uuid = uuid.UUID(user.get('id', '')) auth0_uuid = uuid.UUID(user.get('auth0_user_id', '')) if identity_uuid != auth0_uuid: raise ValueError('user identity/auth0 are different') except ValueError as e: raise ValueError('id/auth0 are not uuid') from e updated = user.get('updated_on') or user.get('created_at') or user.get('last_modified_at') if not updated: raise ValueError('user does not have update/create time') now_dt = datetime.now(timezone.utc) updated_dt = datetime.fromisoformat(updated) if (now_dt - updated_dt) > timedelta(seconds=config.AUTH0_ORG_INVITE_TTL): raise ValueError('update_time exceded AUTH0_ORG_INVITE_TTL') created_by = user.get('auth0_user_created_by') if user.get('auth0_user_created_by') != 'invitation': raise ValueError(f'auth0_user_created_by {created_by}') if user.get('active') != 'Y': raise ValueError('user not active') # For now, supporting both auth0_invitations and invitation_id fields auth0_invitations = json.loads(user.get('auth0_invitations') or '{}') if (not user.get('invitation_id')) and (not auth0_invitations): raise ValueError('invitation_id not found') # All checks passed, this is a user with a valid invitation latest_inv_value = max( auth0_invitations.values(), key=lambda inv: inv.get('timestamp', ''), default=None, ) latest_inv_id = latest_inv_value['invitation_id'] if latest_inv_value else None latest_org_id = latest_inv_value.get('organization_id') if latest_inv_value else None validity_response = { 'valid': True, 'invitation_id': user.get('invitation_id') or latest_inv_id, 'organization_id': user.get('organization_id') or latest_org_id, } return response.Response(status=200, message=validity_response) except ValueError as e: g.log.info( 'Auth0 invitation is invalid:', resources={ 'identity_id': identity_uuid, 'user': user, 'error': str(e), }, ) return response.Response(status=200, message={'valid': False}) def update_vend_contacts(old_auth0_user_id, new_auth0_user_id): """Update all vend contact with auth0_user_id.""" return user_info.update_vend_contacts(old_auth0_user_id, new_auth0_user_id) def update_auth0_details(user_id, user_type, auth0_id): """Update user user_id with auth0 details. This is triggered when the user clicks the verify link in their email to transfer their Auth0 details into art_relations. Args: user_id (int): Vend contact id for now. user_type (str): Type of user (alw/oa). auth0_id (str): auth0 unique id. Returns: response.Response: An object containing the user session metadata. """ vend_contact = user_info.fetch_vend_contact_and_contact(user_id) if not vend_contact: return vend_contact primary_user = user_info.fetch_primary_for_auth0_user(auth0_id) is_existing_user = bool(vend_contact and vend_contact.message['auth0_user_id']) data = {'auth0_user_id': auth0_id, 'auth0_migration_date': datetime.now()} # If there is not a primary account yet, set this as the primary if not primary_user: data.update({'auth0_primary': 'Y'}) elif is_existing_user and vend_contact.message['primary']: data.update({'auth0_primary': None}) if not vend_contact.message.get('login') and vend_contact.message.get('requested_login_email'): subaccount = vend_contact.message['account'].get('subaccount_id') if not subaccount: subaccount = '' login_name = '{}_{}_{}'.format( vend_contact.message['account']['vendor_id'], subaccount, vend_contact.message['requested_login_email'], ) data.update({'login': login_name}) if user_type == constants.USER_INFO_USER_TYPE_ALW: update_msg = user_info.update_vend_contact_details(user_id, data) # In the event of an account-to-account SSO perform a cleanup if update_msg and is_existing_user: cleanup_auth0_user(vend_contact.message['auth0_user_id']) return update_msg return response.create_error_response( status=501, code='NOT_IMPLEMENTED', message='Not implemented.' ) def update_vend_contact_user(user_id, data): """Update vend_contact and contact details for user_id. Args: user_id (int): Vend contact id for now. data (dict): data to update. Returns: response.Response: An object containing the user session metadata. """ contact_data = [] if 'contact' in data: contact_data = data['contact'] del data['contact'] result = user_info.update_vend_contact_details(user_id, data) if contact_data and result and 'contact_id' in result.message: user_info.update_contact_details(result.message['contact_id'], contact_data) return result def cleanup_auth0_user(auth0_id): """Cleanup an auth0 user account to make sure it is still active. This logic ensures that every auth0 account has an active primary user. If there are no users associated with an auth0_id, that id gets blocked. Args: auth0_id (str): auth0 unique id. Returns: response.Response: An object containing update response. """ if 'auth0' not in auth0_id: raw_auth0_id = auth0_id auth0_id = 'auth0|{}'.format(auth0_id) else: raw_auth0_id = auth0_id.replace('auth0|', '') primary_user = user_info.fetch_primary_for_auth0_user(raw_auth0_id) # If there is still primary user, do nothing if primary_user: return response.Response(status=304, message='Not modified') linked_accounts = user_info.fetch_users_by_auth0_id(raw_auth0_id) if linked_accounts and len(linked_accounts.message): # Fallback to the next linked account if available fallback_user = linked_accounts.message[0] return set_auth0_primary_user(auth0_id, fallback_user) else: # If there are no active contacts on the account, block the user data = {'blocked': True} result = auth0_client.update_user(auth0_id, data) identities.activate_deactivate_identity(auth0_id.replace('auth0|', ''), True) return result def get_label_names(user_type, user_id): """Get vendor name for this vend contact user id. Args: user_type (str): alw for now. user_id (str): Vend contact id for now. Returns: flask.Response: containing list of label names. """ if user_type == constants.USER_INFO_USER_TYPE_ALW: return user_info.fetch_vendor_names(user_id) return response.create_error_response( status=501, code='NOT_IMPLEMENTED', message='Not implemented.' ) def get_label_names_for_auth0(auth0_id): """Get vendor names for this Auth0 user id. Args: auth0_id (str): Auth0 user id. Returns: flask.Response: containing list of label names. """ return user_info.fetch_vendor_names(None, auth0_id) def get_attributes_for_vend_contact(vend_contact_id): """Build the attributes dictionary for Split.io for a label user. Args: vend_contact_id (str): The vendor contact id. Returns: flask.Response: Containing the attributes. """ vend_contact_details_response = get_users_minimum_details('alw', vend_contact_id, False) if not vend_contact_details_response: return vend_contact_details_response account = vend_contact_details_response.message['account'] attributes = {context_constants.ORCHARD_USER_ID: 'alw:{}'.format(vend_contact_id)} if account[context_constants.VENDOR_ID]: attributes[context_constants.VENDOR_ID] = account[context_constants.VENDOR_ID] if account[context_constants.SUBACCOUNT_ID]: attributes[context_constants.SUBACCOUNT_ID] = account[context_constants.SUBACCOUNT_ID] return response.Response(message=attributes) def reset_users_auth0_details(auth0_id): """Reset ALW user's auth0_id and auth0_migration date. Args: auth0_id (str): auth0 user id. Returns: flask.Response: containing list of label names. """ return user_info.reset_users_auth0_details(auth0_id) def set_auth0_primary_user(auth0_id: str, user_id: str | None, identity_id: str | None = None): """Set the primary user to the new user_id. This also will make an update to auth0 itself so that the new user is set in user_metadata. Args: auth0_id (str): auth0 user id. user_id (str): Vend contact id to be made primary. Returns: flask.Response: containing user_metadata """ target_user = user_info.fetch_vend_contact_for_user(user_id) # Don't allow someone else's user to get marked as primary if not target_user: return response.create_not_found_response() target_auth0_id = target_user.message['auth0_user_id'] if target_auth0_id and 'auth0' not in target_auth0_id: target_auth0_id = 'auth0|{}'.format(target_auth0_id) if not target_user: return response.create_not_found_response() if target_auth0_id != auth0_id: if not identity_id: identity_id = identities.get_identity_for_profile( profile_id=user_id, profile_type=constants.LABEL_PROFILE ).message['id'] # This just means the vend_contact was created when the user was pending; totally cool if target_user.message['auth0_user_id'] == identity_id: g.log.info( 'vend_contact found with identity id as auth0_user_id', resources={'vend_contact_id': user_id}, ) # Real mismatch; not cool else: g.log.warn( 'auth0_user_id of given vend_contact did not match given auth0 id or identity id', resources={ 'vend_contact_id': user_id, 'vend_contact_auth0_id': target_auth0_id, 'identity_auth0_id': auth0_id, }, ) return response.create_not_found_response() app_parts = target_user.message['user_id'].split(':') app = app_parts[0] # In the db auth0 id is stored without the prefix raw_auth0_id = auth0_id.split('auth0|')[1] primary_user = user_info.fetch_primary_for_auth0_user(raw_auth0_id) # If there's an existing primary user, first clear that one if primary_user: primary_user_parts = primary_user.message['user_id'].split(':') reset_msg = user_info.update_vend_contact_details( primary_user_parts[1], {'auth0_primary': None} ) if not reset_msg: return reset_msg update_msg = user_info.update_vend_contact_details(user_id, {'auth0_primary': 'Y'}) if not update_msg: return update_msg update_user_neo4j = identities.update_auth0_primary_to_identity(raw_auth0_id, user_id) if not update_user_neo4j: return update_user_neo4j data = { 'user_metadata': { 'username': target_user.message['login'], 'vend_contact_id': user_id, 'type': app, } } identities.activate_deactivate_identity(raw_auth0_id, False) is_pending = raw_auth0_id == identity_id if identity_id and is_pending: g.log.info( 'Pending user, no auth0 user yet; skipping auth0 user_metadata update', resources={'vend_contact_id': user_id, 'identity_id': identity_id}, ) return response.Response(message=data, status=200) result = auth0_client.update_user(auth0_id, data) # auth0 update failed so propagate the error if not result: return result return response.Response(message=data, status=200) def deactivate_user(user_id, app): """Deactivate a specific user contact and block in auth0 if necessary. Args: user_id (str): The user id (ex: orchadmin.id or vend_contact.id). app (str): The app to fetch session metadata for (ex: oa or alw). Returns: response.Response: An object containing the user update response. """ if not regex_utils.is_recognisable_app(app): return response.create_error_response( status=400, code='INVALID_APP', message='Invalid app.' ) if app == 'alw': vend_contact = user_info.fetch_vend_contact_for_user(user_id) if not vend_contact: return vend_contact auth0_id = vend_contact.message['auth0_user_id'] if not auth0_id: return response.Response(message='User has not been migrated to Auth0') if 'auth0' not in auth0_id: auth0_id = 'auth0|{}'.format(auth0_id) linked_accounts = get_linked_accounts(user_id, app) if vend_contact.message['primary'] == 'Y': if linked_accounts and len(linked_accounts.message): # Fallback to the next linked account if available fallback_user = linked_accounts.message[0]['vc_id'] return set_auth0_primary_user(auth0_id, fallback_user) elif linked_accounts and len(linked_accounts.message): # If this isn't the primary account but someone else is, ignore return response.Response(status=304, message='Not modified') # If this was the only active contact on the account, block the user data = {'blocked': True} result = auth0_client.update_user(auth0_id, data) identities.activate_deactivate_identity(auth0_id.replace('auth0|', ''), True) return result if app == 'oa': oa_user = user_info.fetch_oa_users([user_id]) auth0_id = oa_user.message[0]['auth0_user_id'] if not auth0_id: return response.Response(message='User has not been migrated to Auth0') if 'auth0' not in auth0_id: auth0_id = 'auth0|{}'.format(auth0_id) data = {'blocked': True} result = auth0_client.update_user(auth0_id, data) identities.activate_deactivate_identity(auth0_id.replace('auth0|', ''), True) return result def reactivate_user(user_id): """Reactivate a blocked user in auth0. Params: user_id (str): vend_contact user id. Returns: flask.Response containing the updated user object from Auth0. """ vend_contact = user_info.fetch_vend_contact_for_user(user_id) if not vend_contact: return vend_contact auth0_id = vend_contact.message['auth0_user_id'] if not auth0_id: return response.Response(message='User has not been migrated to Auth0') if 'auth0' not in auth0_id: auth0_id = f'auth0|{auth0_id}' data = {'blocked': False} result = auth0_client.update_user(auth0_id, data) identities.activate_deactivate_identity(auth0_id.replace('auth0|', ''), False) return result def reset_user_auth0_details(user_id, app): """Reset a user's auth0 information. Args: user_id (str): The user id (ex: orchadmin.id or vend_contact.id). app (str): The app to fetch session metadata for (ex: oa or alw). Returns: response.Response: An object containing the user reset response. """ if not regex_utils.is_recognisable_app(app): return response.create_error_response( status=400, code='INVALID_APP', message='Invalid app.' ) if app == 'alw': return user_info.reset_user_auth0_details(user_id) if app == 'oa': return response.create_error_response( status=501, code='NOT_IMPLEMENTED', message='Not implemented.' ) def get_user_info_feature_fm(user_id, account_type, account_id, correlation_id=None): """Get user info with advertising permissions for feature.fm. Args: user_id (str): The user id (vend_contact.id) account_type(str): The account type: vendor or subaccount account_id (str): unique identifier for a vendor. correlation_id(str): The app correlation id Returns: response.Response: An object containing user information with advertising permissions. """ timestamp = int(time()) user_info = get_users_minimum_details('alw', user_id, True) if not user_info: return user_info user_info.message.update({'timestamp': timestamp}) user_account = user_info.message.get('account', {}) account_id_key = '{0}_id'.format(account_type) features_response = ows_account.get_enabled_features_for_vendor(account_id, correlation_id) if account_id != user_account.get(account_id_key): return response.create_not_found_response() vendor_id = user_account['vendor_id'] currency_code = ows_account.get_vendor_currency_code(vendor_id) if not currency_code: return response.create_fatal_response() return response.Response( map_feature_fm_user_info( user_info.message, features_response.message, currency_code, account_type ) ) def map_feature_fm_user_info(user_info, features_result, currency_code, account_type): """Map the user's info for FeatureFM. Args: user_info (dict): the user's info. features_result (dict): containing the enabled features. currency_code (str): The currency code e.g. USD account_type (str): vendor or subaccount Returns: dict: the mapped user's info. """ features = [item['feature_name'] for item in features_result['items']] permissions = {'action_pages': 'W', 'smart_links': 'W'} facebook_boost = False role_ids = user_info.get('role_ids') if constants.CAMPAIGNS_FEATURE_NAME in features: if constants.ADVERTISING_ROLE_ID in role_ids or constants.ADMIN_ROLE_ID in role_ids: permissions.update({'campaigns': 'W'}) else: permissions.update({'campaigns': 'N'}) else: permissions.update({'campaigns': 'N'}) if constants.FACEBOOK_BOOST_NAME in features: facebook_boost = True if account_type == constants.GRASS_ACCOUNT_TYPE_VENDOR: account_id = user_info.get('account').get('vendor_id') elif account_type == constants.GRASS_ACCOUNT_TYPE_VENDOR: account_id = user_info.get('account').get('subaccount_id') else: account_id = '' ga_flags = { 'orchard_advertising': (constants.CAMPAIGNS_FEATURE_NAME in features or False), 'orchard_advertising_tier_1': (constants.WHITELIST_FEATURE_NAME in features or False), 'facebook_campaign_boost': (constants.FACEBOOK_BOOST_NAME in features or False), 'advertising_role': ( (constants.ADVERTISING_ROLE_ID in role_ids) or (constants.ADMIN_ROLE_ID in role_ids) ) or False, } result = { 'user_id': user_info.get('user_id'), 'first_name': user_info.get('first_name'), 'last_name': user_info.get('last_name'), 'email': user_info.get('email'), 'language': user_info.get('language'), 'currency': currency_code, 'company': user_info.get('company'), 'permissions': permissions, 'timestamp': user_info.get('timestamp'), 'account_id': account_id, 'facebook_boost': facebook_boost, 'account_type': account_type, 'ga_flags': ga_flags, } return result def _make_request(request_item): """Make a request as part of a multi-thread series of requests. Args: request_item (dict): containing a function and parameters. Returns: Result of calling the given function with the given parameters. """ return request_item.get('function_to_call')(*request_item.get('params')) def get_user_basic_info(user_id=None, contact_id=None, subaccount_id=None): """Get the User's basic info. Args: user_id (int): vend_contact.id contact_id (int): vend_contact.contact_id subaccount_id (int): Unique id of subaccount Returns: A list of dict with user details. """ return user_info.fetch_user_basic_info( user_id=user_id, contact_id=contact_id, subaccount_id=subaccount_id ) def update_user_status(user_id, user_update_data): """Update status of user. Args: user_id (int): unique identifier of user. user_update_data (dict): status of user Returns: response.Response: wrapper containing the updated user data or errors. """ data = {} if 'active' in user_update_data and user_update_data['active'] in ['Y', 'N']: data['active'] = user_update_data['active'] return update_vend_contact_user(user_id, data) return response.Response(status=400, message=constants.MISSING_STATUS_MESSAGE) def get_contact_details(account_type, account_id, user_id, status, page_offset, page_limit): """Get a list of all contacts pertaining to an account_id. Args: account_type (str): Type of account. account_id (int): account id associated with the contact user_id (int) : identifier of vend_contact table status (string) : status of user page_offset (int): beginning of the list page_limit (int): Max length of the contact list Returns: Response: A response object with contact details. """ result = user_info.get_contact_details( account_type, account_id, user_id, status, page_offset, page_limit ) return result def get_auth0_connection_name_from_email(email: str) -> Optional[str]: """ Replicates the email domain validation from universal-login auth0 template. Must be kept in sync with isGoogleTenantEmail/isSonyEmail so any changes there/here should be applied into the other. https://github.com/theorchard/auth0-hosted-pages/blob/master/orch-branding/templates/markup/universal-login.html#L37 Args: email: Email address to validate Returns: Connection name if email matches known patterns, None otherwise """ try: # Not a letter, number, underscore, dot, or hyphen disallowed_specials_regex = r'[^a-zA-Z0-9_.-]' addr, domain = email.lower().split('@') if not addr or re.search(disallowed_specials_regex, addr): return None # Valid email pattern, check if it belong to a known connection for connection_name, domains in constants.AUTH0_DOMAINS_CONNECTIONS.items(): if domain in domains: return connection_name return None except ValueError: return None def get_alw_users_by_email(email): """Get a list of all alw users based on email. Args: email (str): Email of the alw user. Returns: Response: A list of dict that contains alw user(s) info. """ return user_info_raw.fetch_alw_users_by_email(email) def get_roles_for_user(user_id, app): """Get user role ids and names only. Args: user_id (str): The user id (ex: orchadmin.id or vend_contact.id). app (str): The app to fetch session metadata for (ex: oa or alw). Returns: response.Response: An object containing the user session metadata. """ if app == 'alw': return user_info.get_alw_roles_for_user(user_id) if app == 'oa': return user_info.get_oa_roles_for_user(user_id) def get_primary_contact(account_type, account_id, active): """Get the primary contact pertaining to an account_id. Args: account_type (str): Type of account. account_id (int): account id associated with the contact active (Optional[str]): the "active" status of the contact Returns: Response: A response object with contact details. """ return user_info.get_primary_contact(account_type, account_id, active) def can_unlink_participant_social_account(identity_id, participant_id, platform): """Check if the user can unlink a participant social auth. Checks whether the user with `identity_id` is authorized to unlink the social platform `platform` of participant wit id `participant_id`. This information is retrieved from Dynamodb table configured with `DYNAMODB_SOCIAL_AUTH_TABLE`. Args: identity_id (str): The identity id of the orchard user participant_id (str): The participant id of the orchard artist/performer platform (str): The social platform for which the unlink status is checked Returns: response.Response: An object containing information whether user can unlink participant or not """ participant = social_auth_item.fetch(participant_id, identity_id, platform) if not participant: return response.Response( status=200, message={ 'can_unlink': False, 'message': 'Participant "{}" not connected to social platform "{}" ' 'by user "{}"'.format(participant_id, platform, identity_id), }, ) return response.Response(status=200, message={'can_unlink': True}) def unlink_participant_social_account(identity_id, participant_id, platform): """Unlinks a participant social auth by a user. Checks whether the user with `identity_id` is authorized to unlink the social platform `platform` of participant wit id `participant_id`. This information is retrieved from Dynamodb table configured with `DYNAMODB_SOCIAL_AUTH_TABLE`. If allowed to unlink, the item is updated in the table with linked false. Args: identity_id (str): The identity id of the orchard user participant_id (str): The participant id of the orchard artist/performer platform (str): The social platform for which the unlink status is checked Returns: response.Response: An object containing information whether user can unlink participant or not """ participant = social_auth_item.fetch(participant_id, identity_id, platform) error_response = response.create_error_response( 'user_error', 'Participant {} social account for {} cannot be unlinked by {}'.format( participant_id, platform, identity_id ), ) if not participant: return error_response updated_item = social_auth_item.set_linked_to_false(participant_id, identity_id, platform) if 'linked' not in updated_item: return error_response if updated_item['linked']: return error_response updated_participant = { 'participant_id': participant_id, 'platform': platform, 'identity_id': identity_id, 'linked': updated_item['linked'], } return response.Response(status=200, message=updated_participant) def get_product_managers(): """Get product managers.""" return user_info.get_product_managers() def get_account_managers(): """Get list of PM/ALR Assignment users.""" return user_info.get_account_managers() def get_active_closers(): """Get list of active users who are also closer.""" return user_info.get_active_closers() def get_identity_vendor(identity_id, label_profile_id): """Get vendor information associated with a label profile.""" return identities.get_identity_vendor(identity_id, label_profile_id)