"""Tests for the PDP authorization module.""" from unittest.mock import MagicMock, patch import pytest from users.utils import authorization @pytest.mark.parametrize( 'action', ( 'list_employees', 'validate_invitation', ), ) @patch('users.utils.authorization.g', spec=['request_context']) @patch('users.utils.authorization.base') @patch('users.utils.authorization.config') def test_pdp_authorize_action( config_mock: MagicMock, resource_getters_base_mock: MagicMock, g_mock: MagicMock, action: authorization.Action, ) -> None: """Test the pdp_authorize_action function.""" g_mock.request_context = MagicMock(jwt_identity_id='identity-id') config_mock.pdp_authorization_backend.is_authorized.return_value = True result = authorization.pdp_authorize_action(action) assert result is True config_mock.pdp_authorization_backend.is_authorized.assert_called_once_with( action=action, resource_id=0, resource_type='identity', resource_getter=resource_getters_base_mock.ForwardKwargsGetter.return_value, identity_uuid='identity-id', tenant={ 'tenant_uuid': 'stub-tenant-uuid', 'tenant_type': 'parent_company', }, ) @pytest.mark.parametrize(('action',), (('list_employees',), ('validate_invitation',))) @patch('users.utils.authorization.g', spec=['log', 'request_context']) @patch('users.utils.authorization.base') @patch('users.utils.authorization.config') def test_pdp_authorize_action_unauthorized( config_mock: MagicMock, _resource_getters_base_mock: MagicMock, g_mock: MagicMock, action: authorization.Action, ) -> None: """Test the pdp_authorize_action function.""" g_mock.request_context = MagicMock(jwt_identity_id='identity-id') config_mock.pdp_authorization_backend.is_authorized.return_value = False result = authorization.pdp_authorize_action(action) assert result is False g_mock.log.warn.assert_called_once_with( 'authorization_error', resources={ 'identity_id': 'identity-id', 'resource_id': 0, 'resource_type': 'identity', 'auth_response': False, }, ) @patch('users.utils.authorization.pdp_authorize_action', return_value=True) def test_pdp_authorize_list_employees(pdp_authorize_action_mock: MagicMock) -> None: """Test the pdp_authorize_list_employees function.""" result = authorization.pdp_authorize_list_employees() assert result is True pdp_authorize_action_mock.assert_called_once_with('list_employees') @patch('users.utils.authorization.pdp_authorize_action', return_value=True) def test_pdp_authorize_validate_invitation(pdp_authorize_action_mock: MagicMock) -> None: """Test the pdp_authorize_validate_invitation function.""" result = authorization.pdp_authorize_validate_invitation() assert result is True pdp_authorize_action_mock.assert_called_once_with('validate_invitation') def _make_ctx(jwt_identity_id=None): """Build a request_context stub with only the attribute the helper reads.""" ctx = MagicMock(spec=['jwt_identity_id']) ctx.jwt_identity_id = jwt_identity_id return ctx @patch('users.utils.authorization.identities_model') @patch('users.utils.authorization.g', spec=['request_context', 'log']) def test_authorize_missing_jwt_denied(g_mock, identities_mock): """A request with no verified JWT is denied before any identity lookup.""" g_mock.request_context = _make_ctx(jwt_identity_id=None) assert ( authorization.authorize_email_change( 'auth0|1', new_email='new@gmail.com' ) is False ) identities_mock.get_identity_by_auth0_id_dict.assert_not_called() @pytest.mark.parametrize( ('caller', 'identity', 'new_email', 'authorized'), ( pytest.param( 'id-1', None, 'new@gmail.com', False, id='target-not-found-denied', ), pytest.param( 'id-1', {'id': 'id-1', 'email': 'me@gmail.com', 'is_employee': False}, 'me-new@gmail.com', True, id='owner-can-change-to-regular-domain', ), pytest.param( 'id-1', {'id': 'id-1', 'email': 'me@gmail.com', 'is_employee': False}, 'me@employee.com', False, id='owner-cannot-change-to-employee-domain', ), pytest.param( 'admin-1', {'id': 'id-2', 'email': 'user@gmail.com', 'is_employee': False}, 'changed@gmail.com', False, id='non-owner-cannot-change-others-email', ), pytest.param( 'id-1', {'id': 'id-1', 'email': 'someone@example.com', 'is_employee': True}, 'new@gmail.com', False, id='employee-email-immutable-even-for-owner', ), pytest.param( 'admin-1', {'id': 'id-2', 'email': 'user@gmail.com', 'is_employee': False}, 'user@gmail.com', True, id='non-owner-unchanged-email-is-noop-allowed', ), pytest.param( 'id-1', {'id': 'id-1', 'email': 'employee@example.com', 'is_employee': True}, 'Employee@example.com', True, id='unchanged-email-is-case-insensitive-noop-even-for-employee', ), ), ) @patch('users.utils.authorization.email_utils') @patch('users.utils.authorization.identities_model') @patch('users.utils.authorization.g', spec=['request_context', 'log']) def test_authorize_email_change_policy( g_mock, identities_mock, email_mock, caller, identity, new_email, authorized ): """Policy for authorize_email_change (rules live in _can_change_email).""" g_mock.request_context = _make_ctx(jwt_identity_id=caller) identities_mock.get_identity_by_auth0_id_dict.return_value = identity email_mock.is_employee_email.side_effect = lambda e: e.endswith('@employee.com') assert ( authorization.authorize_email_change('auth0|target', new_email=new_email) is authorized ) @patch('users.utils.authorization.identities_model') @patch('users.utils.authorization.g', spec=['request_context', 'log']) def test_authorize_denial_logs_a_warning(g_mock, identities_mock): """A denial is logged as a warning before returning False.""" g_mock.request_context = _make_ctx(jwt_identity_id='id-1') identities_mock.get_identity_by_auth0_id_dict.return_value = { 'id': 'id-2', 'email': 'user@gmail.com', 'is_employee': False, } assert ( authorization.authorize_email_change( 'auth0|2', new_email='new@gmail.com' ) is False ) g_mock.log.warning.assert_called_once()