"""Route handlers for Identity endpoints.""" from flask import g, request from neo4j.exceptions import TransientError from owsrequest import error_response from owsresponse import response from owsresponse.adaptors.flask import flaskify from permissions import config from permissions.api import app from permissions.connectors import redis from permissions.constants import constants from permissions.logic import profile, resource from permissions.models import owsusers from permissions.utils.api_utils import validate_request_data from permissions.utils.cache_utils import bust_by_identity, get_resources_key from permissions.validations.schemas.add_identity_profile_resources import ( AddIdentityProfileResourcesSchema, ) from permissions.validations.schemas.edit_profile_resources import ( EditProfileResourceSchema, EditProfileResourcesSchema, ) from permissions.validations.schemas.get_profiles_for_admin import GetProfilesForAdminSchema from permissions.validations.schemas.get_user_resources_for_admin import ( GetUserResourcesForAdminSchema, ) from pythonfeatures import pythonfeatures from pythonfeatures.constants import split as split_constants @app.route('/identity//direct-access/resources/', methods=['GET']) @validate_request_data(GetUserResourcesForAdminSchema()) def get_user_resources_for_admin(identity_id, resource_type, deserialize_schema): """GET resources that identity_id has direct access to and the admin can administer. When Profile headers are sent for admin user, then it will filter the resources to what is common with this admin. Args: identity_id(str): Identity uuid for user that is being viewed/edited. resource_type(str): Type of resource. eg: vendor, label, subaccount, vendor star. Params: limit (int): Limit number of records. Default 200. offset (int): Offset result set. Default 0. active (bool): Users active state. Default True. pending (bool):Users pending state. Default False. """ if any( [ g.request_context.context_type != constants.PROFILE_CONTEXT_TYPE, not g.request_context.profile_type, ] ): return flaskify(error_response.create_error_incomplete_profile_headers()) admin_context = { 'identity_id': g.request_context.identity_id, 'profile_type': g.request_context.profile_type, 'profile_id': int(g.request_context.profile_id), } if admin_context['profile_type'] == constants.COLLABORATORSPROFILE: settings_profile = owsusers.get_settings_profile_for_identity(admin_context['identity_id']) if settings_profile is not None: admin_context['profile_type'] = settings_profile['profile_type'] admin_context['profile_id'] = settings_profile['profile_id'] if admin_context['profile_type'] not in constants.SETTINGS_SUPPORT_MAPPING['adminProfileTypes']: return flaskify(error_response.create_error_forbidden_user()) limit = int(request.args.get('limit', constants.DEFAULT_LIMIT)) offset = int(request.args.get('offset', constants.DEFAULT_OFFSET)) active = request.args.get('active', constants.DEFAULT_ACTIVE_STATUS) if active == 'false': active = False result = resource.get_user_resources_for_admin( admin_context, identity_id, resource_type, limit, offset, active ) if not result: return flaskify(result) return flaskify( response.Response( { 'items': result.message['data'], 'pagination': { 'type': constants.PAGINATION_TYPE_STANDARD, 'total_records': result.message['total'], 'limit': limit, 'offset': offset, 'active': active, }, } ) ) @app.route('/identity/admin/resources/all/', methods=['GET']) def get_my_adminable_resources(): """GET all resources which this authenticated user has admin access.""" identity_uuid = g.request_context.jwt_identity_id limit = int(request.args.get('limit', constants.DEFAULT_LIMIT)) offset = int(request.args.get('offset', constants.DEFAULT_OFFSET)) if not identity_uuid: return flaskify( response.create_error_response( code='UNAUTHORIZED', message='Request context has no identity uuid.', status=401 ) ) redis_key = get_resources_key('identity', identity_uuid, resource_type='all_admin') cached_data = redis.get(redis_key) if cached_data and limit is constants.DEFAULT_LIMIT and offset is constants.DEFAULT_OFFSET: return flaskify(response.Response(cached_data)) result = resource.get_resources_for_identity_uuid(identity_uuid, limit, offset) if result and limit is constants.DEFAULT_LIMIT and offset is constants.DEFAULT_OFFSET: redis.set(redis_key, result.message, config.REDIS_CACHE_TTL) return flaskify(result) @app.route('/identity/admin/profiles', methods=['GET']) @validate_request_data(GetProfilesForAdminSchema()) def get_profiles_for_admin(deserialize_schema): """GET all profiles an admin can administer. This will return all profiles that have access to the resources the given identity has admin rights to. Ex: /identity/admin/profiles?profile_type=LabelProfile returns profiles that have access to the resources the given Identity's LabelProfile(s) have admin access to. /identity/admin/profiles returns profiles that have access to the resources the given Identity (and all of its profiles) has admin access to. Params: profile_type (str): Optional. Type of profile the given identity has access to eg. LabelProfile, InsightsProfile, etc. limit (int): Optional. Limit number of records. Default 50. offset (int): Optional. Offset result set. Default 0. term (str): Optional. Search term. Search is case-insensitive. active (str): Optional. Filter by active/inactive users. Eg. Y or N. pending (str): Optional. Filter by awaiting auth0 org invite accepted. Eg. Y (waiting for invite accpetion) or N (invite already accepted). label_participant (int): Optional. Filter by access to LabelParticipant node ids. @deprecated Use resource_access parameter. resource_access (str): Optional. Filter by uuid of a node. parent_vendor_filter(list[str]): Optional. Filter results to parent vendor uuids. include_subaccount_users (bool): Optional. Whether to include subaccount users for D3 admins when "resource_access" is specified. Default is True. """ if any( [ g.request_context.context_type != constants.PROFILE_CONTEXT_TYPE, not g.request_context.profile_type, ] ): return flaskify(error_response.create_error_incomplete_profile_headers()) if g.request_context.profile_type not in constants.SETTINGS_SUPPORT_MAPPING['profileTypes']: return flaskify(error_response.create_error_forbidden_user()) edit_super_admins_enabled = ( pythonfeatures.get_single_feature(constants.EDIT_SUPER_ADMINS, g.request_context).message == split_constants.FEATURE_ENABLED ) admin_context = { 'identity_id': g.request_context.identity_id, 'profile_type': g.request_context.profile_type, 'profile_id': int(g.request_context.profile_id), } profile_types = request.args.getlist('profile_type') limit = int(request.args.get('limit', constants.DEFAULT_USERS_LIMIT)) offset = int(request.args.get('offset', constants.DEFAULT_OFFSET)) search_term = request.args.get('term') active = request.args.get('active', None) pending = request.args.get('pending', None) label_participants = request.args.getlist('label_participant') resource_access = request.args.getlist('resource_access') parent_vendor_filter = deserialize_schema.get('parent_vendor_filter') include_subaccount_users = deserialize_schema.get('include_subaccount_users', [True])[0] include_requester = deserialize_schema.get('include_requester', [False])[0] result = resource.get_user_profiles_for_admin( admin_context, profile_types, limit, offset, search_term, label_participants, active, pending, resource_access, parent_vendor_filter, edit_super_admins_enabled, include_subaccount_users, include_requester, ) if not result: return flaskify(result) return flaskify( response.Response( { 'items': result.message['data'], 'pagination': { 'type': constants.PAGINATION_TYPE_STANDARD, 'total_records': result.message['total'], 'limit': limit, 'offset': offset, }, } ) ) @app.route('/deactivate/identity/', methods=['DELETE']) def deactivate_user(identity_id): """Deactivate a user and delete access to its resources. When Profile headers are sent for admin user, it will delete resources access that are common with this admin. If all the resources are deleted, then it disables/blocks the user in auth0. Args: identity_id(str): Identity uuid for user that is being viewed/edited. """ if any( [ g.request_context.context_type != constants.PROFILE_CONTEXT_TYPE, not g.request_context.profile_type, ] ): return flaskify(error_response.create_error_incomplete_profile_headers()) admin_context = { 'identity_id': g.request_context.identity_id, 'profile_type': g.request_context.profile_type, 'profile_id': int(g.request_context.profile_id), } result = resource.deactivate_resources_common_with_admin(admin_context, identity_id) return flaskify(result) @app.route('/activate/identity/', methods=['PATCH']) def activate_user(identity_id): """Activate a user and unblock the auth0 user too. When Profile headers are sent for admin user, if that admin is a super admin (vendor * access) only then they can activate users. Old admins no longer apply as deactivate removed all those access. Args: identity_id(str): Identity uuid for user that is being viewed/edited. """ if any( [ g.request_context.context_type != constants.PROFILE_CONTEXT_TYPE, not g.request_context.profile_type, ] ): return flaskify(error_response.create_error_incomplete_profile_headers()) admin_context = { 'identity_id': g.request_context.identity_id, 'profile_type': g.request_context.profile_type, 'profile_id': int(g.request_context.profile_id), } result = resource.activate_user(admin_context, identity_id) return flaskify(result) @app.route('/identity/add-resources-profiles', methods=['POST']) @validate_request_data(AddIdentityProfileResourcesSchema()) def add_identity_profiles(deserialize_schema): """Add identity, profiles and add a list of resources to each. Json body: dict: containing emails, profileTypes, resources and roles. Eg: { "identity": { "name": "foo@bar.com", # will be also be reused as profile name. "email": "foo@bar.com", "first_name": "Foo", # optional "last_name": "Bar" # optional }, "resource_access": [ # same resources will get added to all supported profile_types. { "resource_type": "Vendor", # required "uuid": "dffedd4d-b88d-444d-a9eb-6ce89aa4d2f6", # required "roles": ["analytics"], # required atleast one. }, { "resource_type": "LabelParticipant", "uuid": "5eb8caaf-78df-4a99-929b-887170ede534", "roles": ["analytics"], } ], "send_password_reset": true, # optional. Default is true. "set_email_verified": true, # optional. Default is true. "overwrite_existing_access": true, # optional. Default is true. "create_auth0_user": true, # optional. Default is true. This param is used to determine if settings should create the auth0 user or will the user be created via auth0 invite email. "brand": # optional. Ex. awal or theorchard. "master_contact": false # optional. Default is false. This param is used for gda users created from the gda-account-creation step function. "user_metadata": # optional. Ex. {"should_send_collaborator_access_email": True} } """ raise_neo4j_error = ( pythonfeatures.get_single_feature( 'test_raise_neo4j_error_for_ows_permissions', g.request_context ).message == split_constants.FEATURE_ENABLED ) if raise_neo4j_error: raise TransientError if g.request_context.profile_id is None: return flaskify(error_response.create_error_forbidden_user()) admin_context = { 'identity_id': g.request_context.identity_id, 'profile_type': g.request_context.profile_type, 'profile_id': int(g.request_context.profile_id), } if admin_context['profile_type'] == constants.COLLABORATORSPROFILE: settings_profile = owsusers.get_settings_profile_for_identity(admin_context['identity_id']) if settings_profile is not None: admin_context['profile_type'] = settings_profile['profile_type'] admin_context['profile_id'] = settings_profile['profile_id'] if admin_context['profile_type'] not in constants.SETTINGS_SUPPORT_MAPPING['adminProfileTypes']: return flaskify(error_response.create_error_forbidden_user()) result = profile.create_identity_profiles_resources(admin_context, **deserialize_schema) if result: try: identity_uuids = [] for identity in result.message.get('identities_affected', []): if 'id' in identity: identity_uuids.append(identity['id']) if identity_uuids: bust_by_identity(identity_uuids, resource_types=['all_admin']) except Exception as e: g.log.warning(f'Failed to bust identity all_admin cache for {identity_uuids}: {e}') return flaskify(result) @app.route('/identity//edit-resources-profiles', methods=['POST']) @validate_request_data(EditProfileResourcesSchema()) def edit_identity_profiles(identity_id, deserialize_schema): """Edit identity access to resources. Json body: dict: containing resources and roles. Eg: { "resource_access": [ # same resources will get added to all supported profile_types. { "resource_type": "Vendor", # required "uuid": "497611", # required "roles": ["analytics"], # required atleast one., "overwrite_existing_access": false, # optional. Default is false. }, { "resource_type": "LabelParticipant", "uuid": "497611", "roles": ["analytics"], "overwrite_existing_access": false, # optional. Default is false. } ], } """ if g.request_context.profile_id is None: return flaskify(error_response.create_error_forbidden_user()) admin_context = { 'identity_id': g.request_context.identity_id, 'profile_type': g.request_context.profile_type, 'profile_id': int(g.request_context.profile_id), } if admin_context['profile_type'] == constants.COLLABORATORSPROFILE: settings_profile = owsusers.get_settings_profile_for_identity(admin_context['identity_id']) if settings_profile is not None: admin_context['profile_type'] = settings_profile['profile_type'] admin_context['profile_id'] = settings_profile['profile_id'] if admin_context['profile_type'] not in constants.SETTINGS_SUPPORT_MAPPING['adminProfileTypes']: return flaskify(error_response.create_error_forbidden_user()) result = profile.edit_identity_profiles_resources(admin_context, **deserialize_schema) if result: try: bust_by_identity([identity_id], resource_types=['all_admin']) except Exception as e: g.log.warning(f'Failed to bust identity all_admin cache for {identity_id}: {e}') return flaskify(result) @app.route('/identity//edit-identity-profile-resource', methods=['POST']) @validate_request_data(EditProfileResourceSchema()) def edit_identity_profile_resource(identity_id, deserialize_schema): """Edit identity access to single resource. Json body: { "resource": (dict): type and uuid of resource to edit access to { "resource_type": "Vendor", # required "resource_uuid": "497611", # required } "profile_role_access": (list): # required at least one [ # profile-roles combinations to use to connect identity_id to specified resource(s) { "profile_type": "LabelProfile", "roles_to_add": ["accounting", "analytics"], "roles_to_remove": ["catalog"] }, { "profile_type": "InsightsProfile", "roles_to_add": ["analytics"], "roles_to_remove": [] } ], } Returns: (obj): { identity_id, resource_type, resource_uuid, profile_role_access } """ if g.request_context.profile_id is None: return flaskify(error_response.create_error_forbidden_user()) admin_context = { 'identity_id': g.request_context.identity_id, 'profile_type': g.request_context.profile_type, 'profile_id': int(g.request_context.profile_id), } if admin_context['profile_type'] != constants.SETTINGSPROFILE: return flaskify(error_response.create_error_forbidden_user()) result = flaskify(profile.edit_identity_profile_resource(admin_context, **deserialize_schema)) if result: try: bust_by_identity([identity_id], resource_types=['all_admin']) except Exception as e: g.log.warning(f'Failed to bust identity all_admin cache for {identity_id}: {e}') return result @app.route('/identity//remove-profile-resource', methods=['POST']) @validate_request_data(EditProfileResourceSchema()) def delete_identity_profile_resource(identity_id, deserialize_schema): """Soft delete identity access to single resource. Json body: { "resource": (dict): type and uuid of resource to delete access to { "resource_type": "Vendor", # required "resource_uuid": "497611", # required } "profile_role_access": (list): # required at least one [ # profile-roles combinations to use to connect identity_id to specified resource(s) { "profile_type": "LabelProfile", "roles_to_add": ["accounting", "analytics"], "roles_to_remove": ["catalog"] }, { "profile_type": "InsightsProfile", "roles_to_add": ["analytics"], "roles_to_remove": [] } ], } Returns: (obj): { identity_id, resource_type, resource_uuid, profile_role_access } """ if g.request_context.profile_id is None: return flaskify(error_response.create_error_forbidden_user()) admin_context = { 'identity_id': g.request_context.identity_id, 'profile_type': g.request_context.profile_type, 'profile_id': int(g.request_context.profile_id), } if admin_context['profile_type'] != constants.SETTINGSPROFILE: return flaskify(error_response.create_error_forbidden_user()) result = flaskify(profile.edit_identity_profile_resource(admin_context, **deserialize_schema)) if result: try: bust_by_identity([identity_id], resource_types=['all_admin']) except Exception as e: g.log.warning(f'Failed to bust identity all_admin cache for {identity_id}: {e}') return result