"""Logic for revoking a user's permissions for a single or multiple tenants.""" import pymysql import sqlalchemy from ddtrace import tracer from flask import g from neo4j import exceptions as neo4j_exceptions from permissions.connectors import mysql, neo4j as neo4j_connector from permissions.constants import constants, parent_companies from permissions.exceptions.incomplete_result_error import IncompleteResultError from permissions.logic import ( default_brand as default_brand_logic, identity as identity_logic, vend_contact as vend_contact_logic, ) from permissions.models import ( identity as identity_model, tenant as tenant_model, vend_contact as vend_contact_model, ) from permissions.types import AdminableTenant, Identity, Tenant, TenantType def revoke_access_to_tenant_for_identity( admin_identity_id: str, admin_profile_id: int, identity_id: str, tenant: Tenant, deactivate_if_last_tenant: bool = True, ) -> None: """ Revoke access to a single tenant for identity. Soft-delete access to tenant in neo4j, Update defaultBrand in Auth0 user_metadata if tenant not longer has access to current defaultBrand, Deactivate vend_contact records if tenant is vendor or subaccount Args: admin_identity_id (str): The admin identity id. identity_id (str): The user's identity id. tenant (Tenant): Tenant object. deactivate_if_last_tenant (bool): When True (default), deactivate the identity if this was their last remaining tenant. When False, the identity is left active even with no remaining tenants. Returns: None. """ identity = identity_model.get_identity_by_id_new(identity_id) soft_delete_access( identity=identity, admin_identity_id=admin_identity_id, tenant=tenant, ) # Check if user has any remaining access to tenant and deactivate if not if tenant_model.get_identity_tenant_count(identity_id): try: default_brand_logic.update_default_brand_if_needed(identity, admin_identity_id) except Exception: g.log.error( 'Error updating default brand after tenant revocation' '--access has still been removed', resources={ 'identity_id': identity.id, 'tenant_uuid': tenant.tenant_uuid, }, ) if tenant.tenant_type in (TenantType.ACCOUNT, TenantType.SUBACCOUNT): # In case this tenant is associated with the user's primary auth0 vend_contact_id with mysql.db_session() as mysql_session: try: vend_contact_logic.set_new_auth0_vend_contact_id( session=mysql_session, identity=identity, admin_id=admin_identity_id, admin_profile_id=admin_profile_id, ) except Exception as e: g.log.error( 'Error setting new auth0 vend contact id during revocation of tenant access' '--access has still been removed', resources={ 'identity_id': identity.id, 'tenant_uuid': tenant.tenant_uuid, 'error': str(e), }, ) raise e elif deactivate_if_last_tenant: with neo4j_connector.db_session(access_mode=constants.NEO4j_WRITE_ACCESS) as neo4j_session: try: g.log.info( 'Deactivating identity because access to last tenant has been removed', resources={'identity_id': identity.id, 'tenant_uuid': tenant.tenant_uuid}, ) identity_logic.deactivate_identity( session=neo4j_session, identity=identity, admin_id=admin_identity_id, ) except Exception as e: g.log.error( 'Error deactivating identity during revocation of tenant access' '--access has still been removed', resources={ 'identity_id': identity.id, 'tenant_uuid': tenant.tenant_uuid, 'error': str(e), }, ) raise e def soft_delete_access( identity: identity_model.Identity, admin_identity_id: str, tenant: Tenant, ) -> None: """Soft delete access to a tenant for an identity. May raise.""" with mysql.db_session() as mysql_session, neo4j_connector.db_session( access_mode=constants.NEO4j_WRITE_ACCESS ) as neo4j_session: neo4j_tx = neo4j_session.begin_transaction() try: tenant_model.soft_delete_access_to_tenant_for_identity( tx=neo4j_tx, admin_identity_id=admin_identity_id, identity_id=identity.id, tenant=tenant, ) if tenant.tenant_type in (TenantType.ACCOUNT, TenantType.SUBACCOUNT): vend_contact_model.deactivate_by_tenant_for_identity( tx=mysql_session, identity=identity, tenant=tenant ) neo4j_tx.commit() except ( sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.DatabaseError, pymysql.err.DatabaseError, IncompleteResultError, ) as err: neo4j_tx.rollback() mysql_session.rollback() g.log.error( 'Error soft-deleting access to tenant', resources={ 'identity_id': identity.id, 'tenant_uuid': tenant.tenant_uuid, 'error': str(err), }, ) raise err @tracer.wrap('revoke_access_to_all_tenants_for_identity') def revoke_access_to_all_tenants_for_identity( admin_id: str, admin_profile_id: int, identity: identity_model.Identity ): """Revoke access to all tenants shared by the admin and the user. Soft-deletes access relationships in neo4j and deactivates vend_contact records if necessary. """ adminable_tenants = tenant_model.get_adminable_tenants_for_identity( admin_context={'identity_id': admin_id, 'profile_id': admin_profile_id}, identity_id=identity.id, limit=99999, offset=0, ) tenant_dicts = [at.tenant for at in adminable_tenants] # Edge case: If admin and user have vendor star, also revoke ParentCompany access # (Should really be handled via employee endpoints/SEAT but here we are) has_vendor_star = any( t.tenant.tenant_uuid == constants.VENDOR_STAR_UUID for t in adminable_tenants ) if has_vendor_star: tenant_dicts.extend( [ Tenant( tenant_uuid=parent_companies.ORCHARD_PARENT_COMPANY_UUID, tenant_type=TenantType.PARENT_COMPANY, ), Tenant( tenant_uuid=parent_companies.SME_PARENT_COMPANY_UUID, tenant_type=TenantType.PARENT_COMPANY, ), ] ) with neo4j_connector.db_session(access_mode=constants.NEO4j_WRITE_ACCESS) as neo4j_session: neo4j_tx = neo4j_session.begin_transaction() try: tenant_model.soft_delete_access_to_multiple_tenants_for_identity( tx=neo4j_tx, tenants=tenant_dicts, admin_id=admin_id, identity_id=identity.id, ) _deactivate_vend_contacts_for_tenants( tenants=adminable_tenants, admin_id=admin_id, admin_profile_id=admin_profile_id, identity=identity, ) except ( neo4j_exceptions.Neo4jError, sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.DatabaseError, pymysql.err.DatabaseError, ) as err: g.log.error( 'Error soft-deleting access to all tenants', resources={ 'identity_id': identity.id, 'tenant_uuids': [t.tenant.tenant_uuid for t in adminable_tenants], 'error': str(err), }, ) neo4j_tx.rollback() raise err neo4j_tx.commit() if tenant_model.get_identity_tenant_count(identity_id=identity.id): default_brand_logic.update_default_brand_if_needed(identity, admin_id) else: g.log.info( 'Deactivating identity because access to all tenants has been removed', resources={'identity_id': identity.id}, ) try: identity_logic.deactivate_identity( session=neo4j_session, identity=identity, admin_id=admin_id ) except Exception as e: g.log.error( 'Error deactivating identity during revocation of all tenant access' '--access has still been removed', resources={'identity_id': identity.id, 'error': str(e)}, ) raise e def revoke_all_access_for_employee_identity( admin_context: dict[str, str], identity: identity_model.Identity ) -> None: """Revoke access to all tenants for an employee identity.""" with neo4j_connector.db_session(access_mode=constants.NEO4j_WRITE_ACCESS) as neo4j_session: try: tenant_model.soft_delete_all_access_to_tenants_for_identity( session=neo4j_session, identity_id=identity.id, admin_id=admin_context['identity_id'], ) except neo4j_exceptions.Neo4jError as err: g.log.error( 'Error soft-deleting access to all tenants for employee', resources={ 'identity_id': identity.id, 'error': str(err), }, ) raise err try: identity_logic.deactivate_identity( session=neo4j_session, identity=identity, admin_id=admin_context['identity_id'] ) except Exception as e: g.log.error( 'Error deactivating identity during revocation of all tenant access' '--access has still been removed', resources={'identity_id': identity.id, 'error': str(e)}, ) raise e @tracer.wrap('_deactivate_vend_contacts_for_tenants') def _deactivate_vend_contacts_for_tenants( tenants: list[AdminableTenant], identity: Identity, admin_id: str, admin_profile_id: int, ): label_profile_tenants = [ tenant for tenant in tenants if constants.LABELPROFILE in [p.profile_type for p in tenant.profiles] ] if label_profile_tenants: # Must use a separate mysql session to deactivate vend_contacts and commit, otherwise # ows-users may encounter a locking issue when trying to set a new primary with mysql.db_session() as mysql_session: for adminable_tenant in label_profile_tenants: vend_contact_model.deactivate_by_tenant_for_identity( tx=mysql_session, identity=identity, tenant=adminable_tenant.tenant ) # If this call has revoked access to the user's primary auth0 vend_contact, # but at least one vend_contact remains (for a tenant the admin doesn't have access to), # we need to set it as the new primary so the user can still log in to workstation. # (Rare case--most of the time this will do nothing.) with mysql.db_session() as mysql_session: vend_contact_logic.set_new_auth0_vend_contact_id( session=mysql_session, identity=identity, admin_id=admin_id, admin_profile_id=admin_profile_id, )