"""Logic for querying identities that admins can administer.""" from permissions.connectors import neo4j as neo4j_connector from permissions.constants import constants, error from permissions.types import Tenant def get_identity_ids_for_external_admin( admin_identity_id: str, limit: int = constants.DEFAULT_USERS_LIMIT, offset: int = constants.DEFAULT_OFFSET, search_term: str | None = None, active: str | None = None, pending: str | None = None, tenant_access: list[Tenant] | None = None, ) -> dict[str, int | list[str]]: """Get the ids of identities that an admin can administer. This function finds identities that have access to tenants the admin can manage. Traverses the full vendor hierarchy to include users with access to: - Vendors - LabelParticipants (under Vendor or under Subaccount) - Collaborators (under Vendor) - Subaccounts (under Vendor) Args: admin_identity_id: Identity ID of the admin. limit: Maximum number of records to return. Default 50. offset: Number of records to skip. Default 0. search_term: Optional case-insensitive search on name/email. active: Optional filter - 'Y' for active, 'N' for inactive users. pending: Optional filter - 'Y' for awaiting invite acceptance, 'N' for accepted. tenant_access: Optional list of Tenant objects to filter by. Returns: Dict with 'total' (int) and 'identity_ids' (list of str). """ with neo4j_connector.db_session(access_mode=constants.NEO4j_READ_ACCESS) as session: # Determine relationship type based on active status # In real life, if you had an admin access relationship, you'd have the other one too, # but including both types out of paranoia if active == 'N': relationship_type = 'DELETED_HAS_ACCESS_TO|DELETED_HAS_ADMIN_ACCESS_TO' else: relationship_type = 'HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO' # Build optional tenant access filter tenant_access_filter = '' tenant_filters = None if tenant_access: tenant_filters = [ { 'uuid': t.tenant_uuid, 'label': constants.TENANT_TYPE_TO_NEO_MAPPING[t.tenant_type], } for t in tenant_access ] tenant_access_filter = ( 'AND any(tf IN $tenant_filters ' 'WHERE tf.uuid = tenant.uuid AND tf.label IN labels(tenant))' ) # Filter out caller from results user_conditions = ['user.id <> $admin_identity_id'] # Normalize and add search term filter normalized_search_term = None if search_term: normalized_search_term = ' '.join(search_term.lower().split()) user_conditions.append( '(' 'toLower(user.email) CONTAINS $search_term OR ' 'toLower(user.name) CONTAINS $search_term OR ' 'toLower(user.firstName) CONTAINS $search_term OR ' 'toLower(user.lastName) CONTAINS $search_term OR ' 'toLower(user.firstName + " " + user.lastName) CONTAINS $search_term OR ' 'toLower(user.lastName + " " + user.firstName) CONTAINS $search_term' ')' ) # Add pending filter (checks if user has accepted invite) if pending == 'Y': user_conditions.append('user.id = user.auth0UserId') elif pending == 'N': user_conditions.append('NOT user.id = user.auth0UserId') # Add active filter if active in ['Y', 'N']: user_conditions.append('user.active = $active') user_where = ' AND '.join(user_conditions) # Build the main query # Traverse the vendor hierarchy to find all tenants. # The variable-length path *0..2 allows admin to have HAS_ADMIN_ACCESS_TO: # - 0 hops: admin_tenant itself (Vendor, Subaccount, LabelParticipant, or Collaborator) # - 1 hop: Vendor-[:HAS_LABEL_PARTICIPANT]->LabelParticipant # Vendor-[:OWNS]->Collaborator # Vendor-[:OWNS]->Subaccount # Subaccount-[:HAS_LABEL_PARTICIPANT]->LabelParticipant # - 2 hops: Vendor-[:OWNS]->Subaccount-[:HAS_LABEL_PARTICIPANT]->LabelParticipant base_query = f""" MATCH (admin:Identity)-[:HAS_PROFILE]-> (admin_profile:Profile {{profileType: 'SettingsProfile'}}) -[:HAS_ADMIN_ACCESS_TO]->(admin_tenant) -[:OWNS|HAS_LABEL_PARTICIPANT*0..2]->(tenant) WHERE admin.id = $admin_identity_id {tenant_access_filter} AND (tenant:Vendor OR tenant:LabelParticipant OR tenant:Collaborator OR tenant:Subaccount) WITH DISTINCT tenant MATCH (tenant)<-[:{relationship_type}]-(user_profile:Profile) <-[:HAS_PROFILE]-(user:Identity) WHERE {user_where} WITH DISTINCT user.id AS identity_id WHERE identity_id IS NOT NULL """ # Count query count_query = f'{base_query} RETURN count(identity_id) AS total' # Data query with pagination data_query = ( f'{base_query} ORDER BY identity_id SKIP $offset LIMIT $limit RETURN identity_id' ) params = { 'admin_identity_id': admin_identity_id, 'active': active, 'search_term': normalized_search_term, 'tenant_filters': tenant_filters, 'offset': offset, 'limit': limit, } # Execute count query count_result = session.run(count_query, **params).single() if not count_result: raise RuntimeError(error.MESSAGE_GET_IDENTITIES) total = count_result['total'] if total == 0: return {'total': 0, 'identity_ids': []} # Execute data query data_result = session.run(data_query, **params) identity_ids = [record['identity_id'] for record in data_result] return {'total': total, 'identity_ids': identity_ids}