"""Logic Tier for User logic.""" import uuid as python_uuid from flask import g from pythonfeatures import pythonfeatures from pythonfeatures.constants import split as split_constants from podcast import config from podcast.connectors import mysql from podcast.constants import error as error from podcast.constants import feature_flag as feature_flag_constants from podcast.constants import user as user_constants from podcast.logic import user_v2 as user_v2_logic from podcast.models import ad_action as ad_action_model from podcast.models import episode as episode_model from podcast.models import network as network_model from podcast.models import order as order_model from podcast.models import ows_users from podcast.models import podcast as podcast_model from podcast.models import user as user_model from podcast.utils import api_utils from podcast.utils import feature_flag_utils from podcast.utils.exc import OwsError def create_user(data): """Create a user. Raise exception if user exists in user table. Get user identity by email. Get auth0 art-relation auth0 user by email. Existing auth0 users Old login flow: If identity doesn't exist then create identity. Raise exception if profile exists or else create profile. New login flow: If identity doesn't exist then create identity. Raise exception if profile exists or else create profile. Add user as member in the organization in auth0, no email sent. Non-existing auth0 users Old login flow: Create user in auth0, sends password_reset email as welcome email. Create profile in neo4j using uuid from get identity or from create auth0 user response. New login flow: If identity doesn't exist then create identity from generated uuid. Create profile in neo4j using uuid from get identity or generated uuid. Create user invitation in organization in auth0, sends user_invitation email as welcome email. Args: data (dict): The data to create - network_ids (array of int) - podcast_ids (array of int) - name (string): user's name - email (string): user's email - role (string) - organization (string): e.g.: 'sme' Returns: dict: containing a dict with the created user. """ current_user = get_current_user() current_user_role = current_user['role'] current_user_is_admin_or_raise(current_user_role) role = _get_role_from_input(data) is_role_input_not_producer = role not in (user_constants.PRODUCER, user_constants.READ_ONLY_PRODUCER) if current_user_is_network_admin(current_user_role): if current_user['all_networks'] is False and data.get('all_networks') is True: raise OwsError.forbidden(error.ERROR_MESSAGE_USER_ALL_NETWORKS) if data.get('network_ids'): current_user_owns_network_ids_or_raise(data['network_ids'], current_user=current_user) if data.get('podcast_ids'): current_user_owns_podcast_ids_or_raise(data['podcast_ids'], current_user) if data['organization'] != current_user['organization'] or is_role_input_not_producer: raise OwsError.forbidden() if data.get('podcast_ids') and is_role_input_not_producer: raise OwsError.forbidden() if user_model.get_user_by_email(data['email']): raise OwsError.bad_request(error.ERROR_MESSAGE_USER_ALREADY_EXISTS) default_brand = user_constants.THEORCHARD \ if data['organization'] == user_constants.ORCHARD else data['organization'] # try to get orchard-identity-id from the identity graph node user_identity = ows_users.get_identity_user_by_email(data['email']) uuid = user_identity.get('id') if user_identity is not None else None user = ows_users.get_auth0_user_by_email(data['email']) podcast_organization_login = (pythonfeatures.get_single_feature( feature_flag_constants.FEATURE_PODCAST_ORGANIZATION_LOGIN, g.request_context ).message == split_constants.FEATURE_ENABLED) if user: identity = _get_identity(user) if not user_identity: user_identity = ows_users.create_user_identity( identity, data['name'], data['email'], default_brand) uuid = user_identity['id'] if ows_users.check_for_existing_profile(uuid): raise OwsError.bad_request('User already exists') profile = ows_users.create_auth0_profile(uuid, role, default_brand) if podcast_organization_login: ows_users.create_organization_members(data['organization'], [user]) else: if podcast_organization_login: if not uuid: uuid = str(python_uuid.uuid4()) user_identity = ows_users.create_user_identity( uuid, data['name'], data['email'], default_brand) profile = ows_users.create_auth0_profile(uuid, role, default_brand) ows_users.create_organization_invitation(uuid, data['email'], data['organization']) else: new_auth0_user = ows_users.create_auth0_user(data['name'], data['email'], data['organization']) identity = new_auth0_user['identities'][0]['user_id'] uuid = uuid if uuid else new_auth0_user['user_metadata']['orchardIdentityId'] profile = ows_users.create_auth0_profile(uuid, role, default_brand) data['profile_id'] = profile['profile_id'] data['uuid'] = uuid return user_model.create_user(data) def _get_role_from_input(input_data): if 'role' in input_data: return input_data['role'] else: return user_constants.PRODUCER def _remove_unshared_networks(users_response): user_network_ids = network_ids_for_current_user() for user in users_response['items']: filtered_networks = [network for network in user['networks'] if network['id'] in user_network_ids] filtered_podcasts = [podcast for podcast in user['podcasts'] if podcast['network_id'] in user_network_ids] user['networks'] = filtered_networks user['podcasts'] = filtered_podcasts user['email'] = None return users_response def get_users(limit, offset, organization, network_ids): """Get users. Args: limit (int): number of records to return offset (int): offset organization (str): organization filter network_ids (list of ints): network ids filter Returns: dict: containing a dict with all users. """ if current_user_is_org_admin(): podcast_ids = podcast_model.get_podcast_ids_by_network_ids( network_ids) if network_ids else None return user_model.get_users( limit, offset, network_ids, podcast_ids, organization) elif current_user_is_network_admin() and organization is None: if network_ids: current_user_owns_network_ids_or_raise(network_ids) podcast_ids = podcast_model.get_podcast_ids_by_network_ids(network_ids) else: network_ids = network_ids_for_current_user() podcast_ids = podcast_ids_owned_by_current_user() users = user_model.get_users(limit, offset, network_ids, podcast_ids, exclude_admins=True) return _remove_unshared_networks(users) raise OwsError.forbidden() def get_current_user(): """Get current user. Returns: dict: current user. """ return api_utils.get_current_user() def update_user(user_id, data): """Get users. Returns: dict: containing a dict with all users. """ current_user_is_org_admin_or_raise() return user_model.update_user(user_id, data) def update_user_last_login(user_uuid, data): """Update user last login. Returns: dict: containing a dict with user data. """ user_id = current_user_is_valid_or_raise(user_uuid) return user_model.update_user_last_login(user_id, data) def delete_user(user_id): """Delete a user. Args: user_id: primary id of user Returns: dict: the deleted user """ current_user_is_org_admin_or_raise() with mysql.pod_db_session() as session: deleted_user = user_model.delete_user(user_id, session) if not ows_users.delete_profile(deleted_user['profile_id']): raise OwsError('Auth0 delete profile failed: profile_id {}'.format(deleted_user['profile_id'])) return deleted_user def users_for_network_ids(network_ids): """Get users for network id. Returns: dict: containing a dict with all users. """ if not current_user_is_org_admin(): current_user_owns_network_ids_or_raise(network_ids) return user_model.users_for_network_ids(network_ids) def get_user_favorites(): """Get current users favorite podcasts. Returns: list: containing a dict with all podcast ids. """ return user_model.get_user_favorites(get_current_user()['id']) def toggle_user_podcast_favorite(data): """Toggle favorite for podcast id.""" return user_model.toggle_user_podcast_favorite(get_current_user()['id'], data['podcast_id']) def toggle_user_chart_favorite(data): """Toggle favorite for chart.""" return user_model.toggle_user_chart_favorite( get_current_user()['id'], data['store'], data['category'], data['country'], data['chart_type']) def get_users_by_ids(ids): """Get users by ids. Returns: dict: containing a dict with users. """ current_user = get_current_user() is_producer = current_user['role'] == user_constants.PRODUCER results = user_model.get_users_by_ids(ids) if current_user['all_networks']: return results # no need to filter down users filtered_users = [] if feature_flag_utils.get_feature_flag(feature_flag_constants.FEATURE_PODCAST_IA_RESTRUCTURE): current_user_all_podcast_ids = user_v2_logic.specific_podcast_ids_for_current_user(current_user) else: current_user_all_podcast_ids = specific_podcast_ids_for_user(current_user) current_user_all_network_ids = all_network_ids_for_user( current_user, current_user_all_podcast_ids, is_producer) # all podcast ids owned by users including ones in user podcast or user show family. current_user_all_podcast_ids += podcast_model.get_podcast_ids_by_network_ids( current_user_all_network_ids) for user in results['items']: user_network_ids = set([n['id'] for n in user['networks']]) user_podcast_ids = set([p['id'] for p in user['podcasts']]) has_network_overlap = bool(set(current_user_all_network_ids) & user_network_ids) has_podcast_overlap = bool(set(current_user_all_podcast_ids) & user_podcast_ids) # check if current user has access to requested user at network or podcast level. if has_network_overlap or has_podcast_overlap or user['all_networks']: filtered_users.append(user) return {'items': filtered_users} def all_network_ids_for_user(user, podcast_ids, podcast_level=False): """Get all networks for a user. Networks assigned to user. In case of producer, they can have access to it's podcast's networks. """ all_owned_network_ids = _network_ids_for_user(user) if podcast_level and podcast_ids: podcasts = podcast_model.get_podcasts_by_ids(podcast_ids)['items'] all_owned_network_ids = all_owned_network_ids + [ podcast['network_id'] for podcast in podcasts ] return all_owned_network_ids def _network_ids_for_user(user): if user['all_networks']: return [network['id'] for network in network_model.get_networks()['items']] return [network['id'] for network in user['networks']] def network_ids_for_current_user(current_user=None): """Get the network ids the user has access to.""" return _network_ids_for_user(current_user or get_current_user()) def network_ids_for_user(user_id): """Get the network ids the user has access to.""" user = user_model.get_user_by_id(user_id) return _network_ids_for_user(user) def current_user_owns_network_ids_or_raise(network_ids, podcast_level=False, current_user=None): """Raise error if user does not have access to passed in networks.""" current_user = current_user or get_current_user() all_owned_network_ids = network_ids_for_current_user(current_user) podcast_ids = specific_podcast_ids_for_current_user(current_user) if podcast_level and podcast_ids: podcasts = podcast_model.get_podcasts_by_ids(podcast_ids)['items'] all_owned_network_ids = all_owned_network_ids + [ podcast['network_id'] for podcast in podcasts ] if not set(network_ids).issubset(set(all_owned_network_ids)): raise OwsError.forbidden() def specific_podcast_ids_for_user(user): """Get the podcast ids the user has access to at the podcast level.""" return [podcast['id'] for podcast in user['podcasts']] def specific_podcast_ids_for_user_id(user_id): """Get the podcast ids the user id has access to at the podcast level.""" user = user_model.get_user_by_id(user_id) return specific_podcast_ids_for_user(user) def specific_podcast_ids_for_current_user(current_user=None): """Get the podcast ids the current user has access to at the podcast level.""" return specific_podcast_ids_for_user(current_user or get_current_user()) def podcast_ids_owned_by_current_user(current_user=None): """Get the podcast ids the user has access to.""" current_user = current_user or get_current_user() network_ids = network_ids_for_current_user(current_user) podcast_ids = specific_podcast_ids_for_current_user(current_user) return podcast_model.get_podcast_ids_by_ids_and_network_ids( podcast_ids, network_ids) def user_owns_podcast_id(user_id, podcast_id): """Return whether a user has access to a podcast.""" podcast = podcast_model.get_podcast_by_id(podcast_id) return user_owns_podcast(user_id, podcast) def current_user_owns_podcast_id(podcast_id): """Return whether the user owns a podcast.""" podcast = podcast_model.get_podcast_by_id(podcast_id) return current_user_owns_podcast(podcast) def current_user_owns_podcast(podcast): """Return whether the current user owns a podcast.""" return podcast['network_id'] in network_ids_for_current_user() or \ podcast['id'] in specific_podcast_ids_for_current_user() def user_owns_podcast(user_id, podcast): """Return whether the user owns a podcast.""" user = user_model.get_user_by_id(user_id) return podcast['network_id'] in _network_ids_for_user(user) or \ podcast['id'] in specific_podcast_ids_for_user(user) def current_user_owns_episodes_or_raise(episode_ids): """Raise if user does not own all episode ids.""" if not current_user_owns_episodes(episode_ids): raise OwsError.forbidden() def current_user_owns_episodes(episode_ids): """Raise if user does not own all episode ids.""" episodes = episode_model.get_episodes_by_ids(episode_ids) podcasts_ids = [episode['podcast_id'] for episode in episodes['items']] all_owned_podcast_ids = podcast_ids_owned_by_current_user() return set(podcasts_ids).issubset(set(all_owned_podcast_ids)) def current_user_owns_podcast_ids_or_raise(podcast_ids, current_user=None): """Raise if user does not own all podcast ids.""" current_user = current_user or get_current_user() all_owned_podcast_ids = podcast_ids_owned_by_current_user(current_user) if not set(podcast_ids).issubset(set(all_owned_podcast_ids)): raise OwsError.forbidden() def user_owns_podcast_id_or_raise(user_id, podcast_id): """Return whether the user owns a podcast.""" if not user_owns_podcast_id(user_id, podcast_id): raise OwsError.forbidden() def current_user_owns_podcast_id_or_raise(podcast_id): """Raise an error if the current user does not own the podcast by id.""" if not current_user_owns_podcast_id(podcast_id): raise OwsError.forbidden() def current_user_owns_podcast_or_raise(podcast): """Raise an error if the current user does not own the podcast.""" if not current_user_owns_podcast(podcast): raise OwsError.forbidden() def current_user_owns_podcasts_or_raise(podcasts): """Raise an error if the current user does not own all the podcasts.""" for podcast in podcasts: current_user_owns_podcast_or_raise(podcast) def current_user_is_admin_or_raise(role=None): """Raise an error if the current user is not admin.""" role = role or get_current_user()['role'] if role not in (user_constants.NETWORK_ADMIN, user_constants.ADMIN): raise OwsError.forbidden() def current_user_is_org_admin_or_raise(): """Raise an error if the current user is not an org admin.""" role = get_current_user()['role'] if role != user_constants.ADMIN: raise OwsError.forbidden() def current_user_is_valid_or_raise(user_uuid): """Raise an error if the current user uuid is not equal to passed user uuid.""" current_user = get_current_user() if user_uuid != current_user['uuid']: raise OwsError.forbidden() return current_user['id'] def current_user_is_org_admin(): """Check if user is org admin.""" role = get_current_user()['role'] return role == user_constants.ADMIN def current_user_has_read_only_access_then_raise(role=None): """Raise an error if the current user is a read only producer.""" role = role if role is not None else get_current_user()['role'] if role == user_constants.READ_ONLY_PRODUCER: raise OwsError.forbidden() def current_user_is_network_admin(role=None): """Check if user is network admin.""" role = role or get_current_user()['role'] return role == user_constants.NETWORK_ADMIN def current_user_is_automation_org_admin(): """Check if user is cypress automation org admin.""" uuid = get_current_user()['uuid'] return uuid == config.AUTOMATION_ADMIN_UUID def current_user_owns_object(object_type, object_id): """Raise an exception if the current user doesn't have access to object.""" is_authorized = False if object_type == 'network': is_authorized = object_id in network_ids_for_current_user() if object_type == 'podcast': is_authorized = current_user_owns_podcast_id(object_id) if object_type == 'episode': episode = episode_model.get_episode_by_id(object_id) is_authorized = current_user_owns_podcast_id(episode['podcast_id']) if object_type == 'adupload': ad_action = ad_action_model.get_ad_action_no_assets(object_id) is_authorized = current_user_owns_ad_action(ad_action) return {'authorized': is_authorized} def users_with_access_to_podcast(podcast): """Get users with access to a podcast.""" if not current_user_is_org_admin(): current_user_owns_podcast_ids_or_raise([podcast['id']]) return user_model.users_for_podcast_and_network_ids( [podcast['id']], [podcast['network_id']])['items'] def all_access_users(): """Get all access users.""" return user_model.all_access_users()['items'] def _get_identity(identity_with_auth): return identity_with_auth.split('|')[1] def current_user_owns_ad_action(ad_action, current_user=None, is_network_admin=False): """Check that current user has access to ad action.""" order = order_model.get_order_by_megaphone_id(ad_action['order_id']) network_ids = set(order['network_ids']) overlap = set(network_ids_for_current_user(current_user)) & network_ids is_network_overlap = len(overlap) > 0 if is_network_overlap: return True elif not is_network_overlap and is_network_admin: return False # Check if user owns episodes at the show level podcast_ids = set(order['podcast_ids']) owned_podcast_ids = specific_podcast_ids_for_current_user(current_user) podcast_overlap = set(owned_podcast_ids) & podcast_ids episode_ids = set(order['episode_ids']) owned_episode_ids = episode_model.get_episode_ids_by_podcast_ids(owned_podcast_ids) episode_overlap = set(owned_episode_ids) & episode_ids return len(podcast_overlap) > 0 or len(episode_overlap) > 0 def current_user_owns_ad_action_or_raise(ad_action, current_user=None, is_network_admin=False): """Check that current user has access to ad action or raise.""" if not current_user_owns_ad_action(ad_action, current_user, is_network_admin): raise OwsError.forbidden() def current_user_has_access_to_ad_action_or_raise(ad_action_id, current_user): """Check that ad action is accessible to users like network admins and producers.""" current_user_role = current_user['role'] is_network_admin = current_user_role == user_constants.NETWORK_ADMIN if not current_user['all_networks']: ad_action = ad_action_model.get_ad_action_no_assets(ad_action_id) current_user_owns_ad_action_or_raise(ad_action, current_user, is_network_admin) def users_for_order(order_id): """Get users for campaign order id. Returns: dict: containing a dict with all users. """ current_user_is_admin_or_raise() order = order_model.get_order_by_megaphone_id(order_id) order_network_ids = set(order['network_ids']) order_podcast_ids = order['podcast_ids'] order_episodes = episode_model.get_episodes_by_ids(order['episode_ids'])['items'] order_podcast_ids += [episode['podcast_id'] for episode in order_episodes] if current_user_is_network_admin(): current_user_network_ids = set(network_ids_for_current_user()) current_user_podcast_ids = set(podcast_ids_owned_by_current_user()) order_network_ids = list(order_network_ids & current_user_network_ids) order_podcast_ids = list(set(order_podcast_ids) & current_user_podcast_ids) if len(order_network_ids) == 0 or len(order_podcast_ids) == 0: raise OwsError.forbidden() return user_model.users_for_podcast_and_network_ids( order_podcast_ids, order_network_ids) def check_ownership(object_type, object_id): """Check that user can access provided object. When FF is on, it checks for show family access. When FF is off, it checks for podcast access. """ if feature_flag_utils.get_feature_flag(feature_flag_constants.FEATURE_PODCAST_IA_RESTRUCTURE): return user_v2_logic.current_user_owns_object(object_type, object_id) else: return current_user_owns_object(object_type, object_id)