"""Logic for profiles.""" import collections from copy import deepcopy from typing import List from ddtrace import tracer from flask import g from owsresponse import response from pythonfeatures import pythonfeatures from pythonfeatures.constants import split as split_constants from permissions.constants import constants, error, v2_role_constants from permissions.logic import identity as identity_logic from permissions.logic.vendor_star import ( is_allowed_to_receive_vendor_star, resource_access_contains_vendor_star, ) from permissions.models import ( auth0, label, profile, resource as resource_model, ) def _map_resources_to_profile_types(resources, master_contact=False): """Add list of profile_types that supports this resource.""" final_list = [] profile_roles = {} all_uuids = [] brand_counter = {} resources_with_brand = resource_model.get_resources_brand(resources) attributes = {'identity_id': g.request_context.identity_id} is_moneyhub_ff_enabled = ( pythonfeatures.get_single_feature_by_attributes( 'moneyhub_subaccount_profile_access', attributes ).message == split_constants.FEATURE_ENABLED ) if not resources_with_brand: return resources_with_brand for resource in resources_with_brand: new_resource = deepcopy(resource) explicit_profile_types = [] implicit_profile_types = [] all_uuids.append(resource['uuid']) migrated_to_abacus = False if resource['vendor_id']: migrated_to_abacus = label.get_migrated_to_abacus(resource['vendor_id']) brand = resource['brand'] brand_counter[brand] = brand_counter.get(brand, 0) + 1 for each in resource['roles']: resource_types: set = {constants.VENDOR_RESOURCE_TYPE} if should_add_moneyhub_subaccount(resource, each) and is_moneyhub_ff_enabled: resource_types.add(constants.SUBACCOUNT_RESOURCE_TYPE) if ( resource['resource_type'] in resource_types and migrated_to_abacus and brand != constants.KNR_BRAND ): explicit_profile_types.extend( constants.MIGRATED_PROFILES_EXPLICITLY_CREATED.get( f"{resource['resource_type']}_{each}", [] ) ) implicit_profile_types.extend( constants.MIGRATED_PROFILES_IMPLICITLY_CREATED.get( f"{resource['resource_type']}_{each}", [] ) ) elif brand == constants.AWAL_BRAND: explicit_profile_types.extend( constants.AWAL_RESOURCE_PROFILES_EXPLICITLY_CREATED.get( f"{resource['resource_type']}_{each}", [] ) ) implicit_profile_types.extend( constants.AWAL_RESOURCE_PROFILES_IMPLICITLY_CREATED.get( f"{resource['resource_type']}_{each}", [] ) ) elif brand == constants.KNR_BRAND: explicit_profile_types.extend( constants.KNR_RESOURCE_PROFILES_EXPLICITLY_CREATED.get( f"{resource['resource_type']}_{each}", [] ) ) implicit_profile_types.extend( constants.KNR_RESOURCE_PROFILES_IMPLICITLY_CREATED.get( f"{resource['resource_type']}_{each}", [] ) ) else: explicit_profile_types.extend( constants.RESOURCE_PROFILES_EXPLICIT_CREATED.get( f"{resource['resource_type']}_{each}", [] ) ) implicit_profile_types.extend( constants.RESOURCE_PROFILES_IMPLICITLY_CREATED.get( f"{resource['resource_type']}_{each}", [] ) ) if not explicit_profile_types and not implicit_profile_types: return response.create_error_response( error.ERROR_CODE_VALIDATION_ERROR, f"{resource['resource_type']} not supported" ) if all( [ resource['uuid'] == constants.VENDOR_STAR_UUID, constants.COLLABORATORSPROFILE in explicit_profile_types, ] ): explicit_profile_types.remove(constants.COLLABORATORSPROFILE) new_resource['explicit_profile_types'] = sorted(set(explicit_profile_types)) new_resource['implicit_profile_types'] = sorted(set(implicit_profile_types)) final_list.append(new_resource) # profile_roles= { InsightsProfile: [analytics, accounting] .. } for profile_type in explicit_profile_types: roles = profile_roles.setdefault(profile_type, []) roles.extend(resource['roles']) if all( [ resource['resource_type'] == constants.COLLABORATOR_RESOURCE_TYPE, profile_type == constants.DOCUMENTSPROFILE, ] ): roles.append(constants.PAYEE_MANAGEMENT_ROLE) elif all( [ profile_type == constants.COLLABORATORSPROFILE, ( constants.ACCOUNTING_ROLE in resource['roles'] or constants.ADMINISTRATOR_ROLE in resource['roles'] ), ] ): roles.append(constants.ROYALTIES_ROLE) # the brand all resources share, otherwise is set to None aggregate_resources_brand = list(brand_counter.keys())[0] if len(brand_counter) == 1 else None return response.Response( message={ 'resources': final_list, 'profile_roles': profile_roles, 'all_uuids': all_uuids, 'aggregate_brand': aggregate_resources_brand, } ) def should_add_moneyhub_subaccount(resource: dict, role: str) -> bool: """Check if moneyhub subaccount should be added. Args: resource (dict): the resource. role (str): the role of the resource Returns: Bool """ if 'resource_type' not in resource: return False if resource['resource_type'] != constants.SUBACCOUNT_RESOURCE_TYPE: return False if role not in [constants.ACCOUNTING_ROLE, constants.ADMINISTRATOR_ROLE]: return False return True def _check_vend_star_resource_requested(resource_type, resource_uuid): """Check if a vendor * catalog resource was requested. Args: resource_type: (str) type of resource (LabelParticipant / SubAccount / Vendor, etc.) resource_uuid: (str) uuid of resource to check Returns: Boolean """ return ( resource_type == constants.VENDOR_RESOURCE_TYPE and resource_uuid == constants.VENDOR_STAR_UUID ) def _check_vend_star_requested(resources): """Check if a vendor * catalog resource was requested. Args: resources: resource_access list unserialized from schema. Returns: Boolean """ vendor_uuids = [ r['uuid'] for r in resources if r['resource_type'] == constants.VENDOR_RESOURCE_TYPE ] return constants.VENDOR_STAR_UUID in vendor_uuids def create_identity_profiles_resources( admin_context, identity, resource_access=[], send_password_reset=True, set_email_verified=True, overwrite_existing_access=True, brand=None, create_auth0_user=True, master_contact=False, user_metadata={}, ): """Create new identity, profile and resource access if one does not exist. Args: admin_context (dict): Admin Identity data. identity (dict): User Identity details. resource_access (list): List of resource object with roles. send_password_reset (bool): send password_reset. set_email_verified (bool): set email verified. create_auth0_user (bool): determine if we should create user in auth0 or just neo4j. overwrite_existing_access (bool): delete old and replace with new access. brand (string): brand associated with the identity. master_contact (bool): new identity is a master contact from gda-account-creation step function. Return: response (obj) """ edit_super_admins_enabled = ( pythonfeatures.get_single_feature(constants.EDIT_SUPER_ADMINS, g.request_context).message == split_constants.FEATURE_ENABLED ) resources = _map_resources_to_profile_types(resource_access, master_contact=master_contact) if not resources: return resources resource_data = resources.message identity_brand = resources.message['aggregate_brand'] or constants.THEORCHARD_BRAND if identity_brand == constants.SONY_BRAND: brand = constants.SONY_BRAND create_auth0_user = False send_password_reset = False if not brand: if not resource_data['aggregate_brand']: admin_identity = identity_logic.get_identity_by_id(admin_context['identity_id']) if not admin_identity: # this should never happen return admin_identity brand = admin_identity.message.get('default_brand') or constants.THEORCHARD_BRAND # if the resource brand is mass appeal or drm, set the user's default brand to theorchard. elif resource_data['aggregate_brand'] not in constants.PROFILE_BRANDS: brand = constants.THEORCHARD_BRAND else: brand = resource_data['aggregate_brand'] # if this is an AWAL, KNR, or Orchard user, # don't create user in Auth0 here. they will be created # when they click the invite link emailed to them. if brand in [constants.AWAL_BRAND, constants.KNR_BRAND, constants.THEORCHARD_BRAND]: create_auth0_user = False set_email_verified = False send_password_reset = False can_access_vendor_star = profile.check_vendor_star_access(**admin_context) if not can_access_vendor_star: can_administer = profile.has_admin_access_to_resources( admin_context, resources.message['all_uuids'] ) if not can_administer: return can_administer if not edit_super_admins_enabled and (_check_vend_star_requested(resource_access)): return response.create_error_response( error.ERROR_CODE_AUTHORIZATION, error.ERROR_MESSAGE_SUPER_ADMIN ) if admin_context['profile_type'] == constants.SETTINGSPROFILE: # when creating user from settings app, modify password reset email to show additional # information for artists. app_metadata = {} resource_types = list(set([r['resource_type'] for r in resources.message['resources']])) if resource_types == [constants.LABELPARTICIPANT_RESOURCE_TYPE]: label_participant_uuids = [ r['uuid'] for r in resources.message['resources'] if r['resource_type'] == constants.LABELPARTICIPANT_RESOURCE_TYPE ] label_participants = profile.get_label_for_label_participant(label_participant_uuids) if not label_participants: return label_participants app_metadata = { 'should_send_welcome_as_password_reset': True, 'label_participants': label_participants.message, } identity['app_metadata'] = app_metadata identity['user_types'] = _get_user_types(resource_types) if resource_access_contains_vendor_star( resources.message['resources'] ) and not is_allowed_to_receive_vendor_star(identity['email']): g.log.info( error.INTERNAL_LOGGING_BLOCKED_VENDOR_STAR, resources={ 'admin_identity_id': admin_context.get('identity_id'), 'identity_email': identity['email'], }, ) return response.create_error_response( error.ERROR_CODE_BAD_REQUEST, error.ERROR_MESSAGE_CANNOT_ASSIGN_RESOURCE, status=403, ) result = profile.add_resources_to_identity( identity=identity, resources=resources.message['resources'], profile_roles=resources.message['profile_roles'], admin_context=admin_context, can_access_vendor_star=bool(can_access_vendor_star), set_email_verified=set_email_verified, overwrite_existing_access=overwrite_existing_access, brand=brand, create_auth0_user=create_auth0_user, master_contact=master_contact, user_metadata=user_metadata, ) if not result: return result # if everything was success, send password reset email. if create_auth0_user: _reset_auth0_user_metadata(result.message['profiles_affected']) if send_password_reset and result.message['identity_created']: auth0.bulk_send_password_reset( [identity['email']], constants.ARTIST_USER_TYPE in identity['user_types'], user_metadata, ) return result def _get_user_types(resource_types): """Set user type depending on resource access.""" user_types = [] if ( constants.VENDOR_RESOURCE_TYPE in resource_types or constants.SUBACCOUNT_RESOURCE_TYPE in resource_types ): # noqa user_types.append(constants.LABEL_USER_TYPE) if constants.LABELPARTICIPANT_RESOURCE_TYPE in resource_types: user_types.append(constants.ARTIST_USER_TYPE) return user_types def _reset_auth0_user_metadata(profiles): """Reset vend_contact in user_metadata.""" new_label_profile = {} for p in profiles: if p['profile_type'] == constants.LABELPROFILE: new_label_profile[p['user_identity']['auth0_user_id']] = dict( vend_contact_id=p['profile_id'], type='alw' ) # update vend_contact and auth0 both so they are in sync. label.update_auth0_primary(new_label_profile) auth0.update_user_metadata(new_label_profile) def edit_identity_profiles_resources( admin_context, identity_id, resource_access=None, overwrite_existing_access=False ): """Edit identity's profile and resource access. Args: admin_context (dict): Admin Identity data. identity_id (str): User Identity id. resource_access (list): List of resource object with roles. Return: response (obj) """ edit_super_admins_enabled = ( pythonfeatures.get_single_feature(constants.EDIT_SUPER_ADMINS, g.request_context).message == split_constants.FEATURE_ENABLED ) resource_access = resource_access or [] resources = _map_resources_to_profile_types(resource_access) if not resources: return resources if not edit_super_admins_enabled and (_check_vend_star_requested(resource_access)): return response.create_error_response( error.ERROR_CODE_AUTHORIZATION, error.ERROR_MESSAGE_FORBIDDEN_USER ) can_access_vendor_star = profile.check_vendor_star_access(**admin_context) if can_access_vendor_star: existing_identity = identity_logic.get_identity_by_id(identity_id) else: existing_identity = profile.get_identity_id_for_admin(admin_context, identity_id) if not existing_identity: return existing_identity if resource_access_contains_vendor_star( resource_access ) and not is_allowed_to_receive_vendor_star(existing_identity.message['email']): g.log.info( error.INTERNAL_LOGGING_BLOCKED_VENDOR_STAR, resources={ 'admin_identity_id': admin_context.get('identity_id'), 'identity_email': existing_identity.message['email'], }, ) return response.create_error_response( error.ERROR_CODE_BAD_REQUEST, error.ERROR_MESSAGE_CANNOT_ASSIGN_RESOURCE, status=403, ) if existing_identity.message.get('active') and existing_identity.message.get('active') == 'N': return response.create_error_response( error.ERROR_CODE_BAD_REQUEST, 'Cannot edit an inactive user.' ) brands = list(set([r['brand'] for r in resources.message['resources']])) existing_identity.message['all_brands'] = brands resource_types = list(set([r['resource_type'] for r in resources.message['resources']])) existing_identity.message['user_types'] = _get_user_types(resource_types) if not can_access_vendor_star: can_administer = profile.has_admin_access_to_resources( admin_context, resources.message['all_uuids'] ) if not can_administer: return can_administer result = profile.add_resources_to_identity( existing_identity.message, resources.message['resources'], resources.message['profile_roles'], admin_context, bool(can_access_vendor_star), overwrite_existing_access=overwrite_existing_access, ) if not result: return result # reset is case some accounts were deactivated. _reset_auth0_user_metadata(result.message['profiles_affected']) return existing_identity @tracer.wrap('get_identities_by_profile', service='neo4j') def get_identities_by_profile( profile_type: str, profile_id: int, profile_uuid: str = None, ) -> List[str]: """Find all identities with access to a Profile by profile type and id, or by uuid. profile_type: the type of profile, e.g. SettingsProfile. profile_id: the id of the profile, different from the UUID of the profile. profile_uuid: the uuid of the profile. Returns a list of identity_uuids. """ return profile.get_identities_by_profile(profile_type, profile_id, profile_uuid=profile_uuid) def v2_roles_to_profiles_and_roles_dict(v2_roles: list[str]) -> dict[str, list[str]]: """Take a list of v2 roles and return a dict with profile type keys and role list values.""" # Deduplicate just in case v2_role_names = list(set(v2_roles)) profiles_and_roles_to_add = collections.defaultdict(list) for v2_role_name in v2_role_names: profile_type = v2_role_constants.V2_ROLE_TO_PROFILE_MAP[v2_role_name] if profile_type == constants.LABELPROFILE: to_add = [v2_role_constants.V2_ROLE_TO_LABEL_PROFILE_ROLE_MAP[v2_role_name]] elif profile_type in constants.PROFILE_TYPE_TO_ROLES_MAPPING: # Profiles with more than one role type are label profile (handled above) # plus content profile and distribution profile (not administered in settings) to_add = [v2_role_constants.PROFILE_TO_NEO4J_ROLE_MAP[profile_type]] elif profile_type == constants.SETTINGSPROFILE: # Settings profiles don't store roles on em to_add = [] profiles_and_roles_to_add[profile_type] += to_add return profiles_and_roles_to_add