"""Handlers for internal endpoints. This module contains the request handlers for endpoints under the "/internal" sub-root of the application. These endpoints are designed to be inaccessible from external networks (i.e. they are not proxied or exposed by ows-grass). They can only be called from within the trusted private network. """ from flask import g from owsresponse import response from owsresponse.adaptors.flask import flaskify import permissions.validations.schemas.identity as identity_schemas from permissions.api import app from permissions.constants import constants, error, parent_companies from permissions.logic import ( identity as identity_logic, tenant as tenant_logic, user_invite, user_notify, user_revoke, user_update, ) from permissions.models import identity as identity_model from permissions.types import AdminIdentity, IdentityInput, Tenant, TenantRolesInput, TenantType from permissions.utils import api_utils, authorization @app.route('/internal/v2/identities', methods=['POST']) @api_utils.jwt_check @api_utils.validate_request_data(identity_schemas.CreateInternalIdentity()) def create_internal_identity(deserialize_schema): """Create an employee identity.""" if not authorization.pdp_authorize_manage_employee(): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, status=403, ) ) email = deserialize_schema['email'] if identity_logic.get_identity_by_email(email): return flaskify( response.create_error_response( code=error.ERROR_CODE_CONFLICT, message=f'Identity with email {email} already exists.', status=409, ) ) admin_identity = _admin_identity_from_g() first_name = deserialize_schema.get('first_name') last_name = deserialize_schema.get('last_name') identity_input = IdentityInput(email=email, first_name=first_name, last_name=last_name) tenant = deserialize_schema.get('tenant') roles = deserialize_schema.get('roles_to_attach', []) tenant_to_add = Tenant( tenant_type=TenantType(tenant['tenant_type']), tenant_uuid=tenant['tenant_uuid'] ) tenant_roles_input = TenantRolesInput( roles_to_attach=roles, roles_to_detach=[], tenant=tenant_to_add ) if tenant_to_add.tenant_type == constants.ACCOUNT_TENANT_TYPE: if not tenant_logic.check_compatibility_with_tenant_configuration(tenant_to_add, roles): return flaskify( response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message='Role(s) cannot be attached', status=422, ) ) brand = deserialize_schema.get('brand') identity = user_invite.create_employee( assignee_identity=identity_input, admin_identity=admin_identity, tenant_roles_input=tenant_roles_input, brand=brand, ) user_notify.notify_employee_created( admin=admin_identity, assignee=identity, tenant_roles_input=tenant_roles_input, brand=brand, ) return flaskify(response.Response({'id': identity.id}, status=201)) @app.route('/internal/v2/identities/', methods=['PATCH']) @api_utils.jwt_check @api_utils.validate_request_data(identity_schemas.UpdateInternalIdentity()) def update_internal_identity(identity_id, deserialize_schema): """Update an employee identity.""" if not authorization.pdp_authorize_manage_employee(): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, status=403, ) ) identity = identity_model.get_identity_by_id_new(identity_id) if not identity: return flaskify( response.create_error_response( code=error.ERROR_CODE_NOT_FOUND, message='User does not exist', status=404, ) ) tenant_dict = deserialize_schema.get('tenant') tenant = Tenant( tenant_type=TenantType(tenant_dict['tenant_type']), tenant_uuid=tenant_dict['tenant_uuid'] ) roles_to_attach: list[str] = deserialize_schema.get('roles_to_attach') roles_to_detach: list[str] = deserialize_schema.get('roles_to_detach') match tenant.tenant_type: case TenantType.PARENT_COMPANY: brand = deserialize_schema.get('brand') case TenantType.ACCOUNT: brand = tenant_logic.get_parent_company_brand_for_tenant(tenant) if tenant.tenant_type == constants.ACCOUNT_TENANT_TYPE: if not tenant_logic.check_compatibility_with_tenant_configuration(tenant, roles_to_attach): return flaskify( response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message='Role(s) cannot be attached', status=422, ) ) # Removing the last role of a tenant is also disallowed via UI if not roles_to_attach: try: is_removing_last_role = tenant_logic.seat_is_update_removing_last_role( identity_id=identity_id, tenant_uuid=tenant_dict['tenant_uuid'], roles_to_detach=roles_to_detach, ) except ValueError as err: return flaskify( response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message=str(err), status=400, ) ) if is_removing_last_role: return flaskify( response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message='Cannot remove all roles for a tenant via this endpoint.' ' Please use revoke access endpoint instead.', status=400, ) ) admin = _admin_identity_from_g() auth0_org = _auth0_org_from_parent_company_uuid(tenant_dict['tenant_uuid']) identity_with_auth0 = identity_model.get_identity_with_auth0_for_existing_identity( admin=admin, existing_identity=identity, email=identity.email, brand=auth0_org, ) tenant_roles_input = TenantRolesInput( tenant=tenant, roles_to_attach=roles_to_attach, roles_to_detach=roles_to_detach, ) user_update.update_employee( admin=admin, identity_with_auth0=identity_with_auth0, tenant_roles_input=tenant_roles_input, brand=brand, ) if roles_to_attach: user_notify.notify_employee_updated( admin=admin, assignee=identity_with_auth0, tenant_roles_input=tenant_roles_input, brand=auth0_org, ) return flaskify(response.Response({'id': identity_id})) @app.route('/internal/v2/identities/', methods=['DELETE']) @api_utils.jwt_check def delete_internal_identity(identity_id): """Delete an employee identity by revoking all access.""" if not authorization.pdp_authorize_manage_employee(): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, status=403, ) ) identity = identity_model.get_identity_by_id_new(identity_id) if not identity: return flaskify( response.create_error_response( code=error.ERROR_CODE_NOT_FOUND, message='User does not exist', status=404, ) ) admin = _admin_identity_from_g() user_revoke.revoke_all_access_for_employee_identity( admin_context={'identity_id': admin.id, 'profile_id': admin.settings_profile.profile_id}, identity=identity, ) return flaskify(response.Response(status=204)) @app.route( '/internal/v2/identities//tenants//', methods=['DELETE'], ) @api_utils.jwt_check @api_utils.validate_request_data(identity_schemas.RevokeInternalIdentityTenantAccess()) def delete_internal_identity_tenant_access( identity_id, tenant_type, tenant_uuid, deserialize_schema ): """Revoke an employee identity's access to a single account tenant.""" if not authorization.pdp_authorize_manage_employee(): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, status=403, ) ) identity = identity_model.get_identity_by_id_new(identity_id) if not identity: return flaskify( response.create_error_response( code=error.ERROR_CODE_NOT_FOUND, message='User does not exist', status=404, ) ) tenant = Tenant(tenant_type=TenantType(tenant_type), tenant_uuid=tenant_uuid) if not tenant_logic.does_tenant_exist(tenant): return flaskify( response.create_error_response( code=error.ERROR_CODE_NOT_FOUND, message='Tenant not found.', status=404, ) ) admin = _admin_identity_from_g() user_revoke.revoke_access_to_tenant_for_identity( admin_identity_id=admin.id, admin_profile_id=admin.settings_profile.profile_id, identity_id=identity_id, tenant=tenant, deactivate_if_last_tenant=False, ) return flaskify(response.Response(status=204)) def _admin_identity_from_g() -> AdminIdentity: """Helper to build an AdminIdentity from g.request_context.""" admin_id = g.request_context.jwt_identity_id admin = identity_model.get_identity_by_id_new(identity_id=admin_id) settings_profile = identity_model.get_identity_settings_profile(admin_id) return AdminIdentity( id=admin.id, first_name=admin.first_name, last_name=admin.last_name, name=admin.name, email=admin.email, auth0_user_id=admin.auth0_user_id, active=admin.active, user_types=admin.user_types, default_brand=admin.default_brand, settings_profile=settings_profile, ) def _auth0_org_from_parent_company_uuid(parent_company_uuid: str) -> str: """Determine auth0 org (sometimes aka brand in code, unfortunately) based on PC uuid.""" if parent_company_uuid == parent_companies.ORCHARD_PARENT_COMPANY_UUID: return constants.AUTH0_ORCHARD_ORG_NAME else: return constants.SONY_BRAND