from datetime import date from unittest import mock import pytest from django.contrib.sessions.middleware import SessionMiddleware from django.test.utils import override_settings from awx.main.models import User from awx.api.versioning import reverse # # user creation # EXAMPLE_USER_DATA = {"username": "affable", "first_name": "a", "last_name": "a", "email": "a@a.com", "is_superuser": False, "password": "r$TyKiOCb#ED"} @pytest.mark.django_db def test_user_create(post, admin): response = post(reverse('api:user_list'), EXAMPLE_USER_DATA, admin, middleware=SessionMiddleware(mock.Mock())) assert response.status_code == 201 assert not response.data['is_superuser'] assert not response.data['is_system_auditor'] # Disable local password checks to ensure that any ValidationError originates from the Django validators. @override_settings( LOCAL_PASSWORD_MIN_LENGTH=1, LOCAL_PASSWORD_MIN_DIGITS=0, LOCAL_PASSWORD_MIN_UPPER=0, LOCAL_PASSWORD_MIN_SPECIAL=0, ) @pytest.mark.django_db def test_user_create_with_django_password_validation_basic(post, admin): """Test if the Django password validators are applied correctly.""" with override_settings( AUTH_PASSWORD_VALIDATORS=[ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': { 'min_length': 3, }, }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ], ): # This user should fail the UserAttrSimilarity, MinLength and CommonPassword validators. user_attrs = ( { "password": "Password", # NOSONAR "username": "Password", "is_superuser": False, }, ) print(f"Create user with invalid password {user_attrs=}") response = post(reverse('api:user_list'), user_attrs, admin, middleware=SessionMiddleware(mock.Mock())) assert response.status_code == 400 # This user should pass all Django validators. user_attrs = { "password": "r$TyKiOCb#ED", # NOSONAR "username": "TestUser", "is_superuser": False, } print(f"Create user with valid password {user_attrs=}") response = post(reverse('api:user_list'), user_attrs, admin, middleware=SessionMiddleware(mock.Mock())) assert response.status_code == 201 @pytest.mark.parametrize( "user_attrs,validators,expected_status_code", [ # Test password similarity with username. ( {"password": "TestUser1", "username": "TestUser1", "is_superuser": False}, # NOSONAR [ {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, ], 400, ), ( {"password": "abc", "username": "TestUser1", "is_superuser": False}, # NOSONAR [ {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, ], 201, ), # Test password min length criterion. ( {"password": "TooShort", "username": "TestUser1", "is_superuser": False}, # NOSONAR [ {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 9}}, ], 400, ), ( {"password": "LongEnough", "username": "TestUser1", "is_superuser": False}, # NOSONAR [ {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 9}}, ], 201, ), # Test password is too common criterion. ( {"password": "Password", "username": "TestUser1", "is_superuser": False}, # NOSONAR [ {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, ], 400, ), ( {"password": "aEArV$5Vkdw", "username": "TestUser1", "is_superuser": False}, # NOSONAR [ {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, ], 201, ), # Test if password is only numeric. ( {"password": "1234567890", "username": "TestUser1", "is_superuser": False}, # NOSONAR [ {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, ], 400, ), ( {"password": "abc4567890", "username": "TestUser1", "is_superuser": False}, # NOSONAR [ {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, ], 201, ), ], ) # Disable local password checks to ensure that any ValidationError originates from the Django validators. @override_settings( LOCAL_PASSWORD_MIN_LENGTH=1, LOCAL_PASSWORD_MIN_DIGITS=0, LOCAL_PASSWORD_MIN_UPPER=0, LOCAL_PASSWORD_MIN_SPECIAL=0, ) @pytest.mark.django_db def test_user_create_with_django_password_validation_ext(post, delete, admin, user_attrs, validators, expected_status_code): """Test the functionality of the single Django password validators.""" # default_parameters = { # Default values for input parameters which are None. "user_attrs": { "password": "r$TyKiOCb#ED", # NOSONAR "username": "DefaultUser", "is_superuser": False, }, "validators": [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': { 'min_length': 8, }, }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ], } user_attrs = user_attrs if user_attrs is not None else default_parameters["user_attrs"] validators = validators if validators is not None else default_parameters["validators"] with override_settings(AUTH_PASSWORD_VALIDATORS=validators): response = post(reverse('api:user_list'), user_attrs, admin, middleware=SessionMiddleware(mock.Mock())) assert response.status_code == expected_status_code # Delete user if it was created succesfully. if response.status_code == 201: response = delete(reverse('api:user_detail', kwargs={'pk': response.data['id']}), admin, middleware=SessionMiddleware(mock.Mock())) assert response.status_code == 204 else: # Catch the unexpected behavior that sometimes the user is written # into the database before the validation fails. This actually can # happen if UserSerializer.validate instantiates User(**attrs)! username = user_attrs['username'] assert not User.objects.filter(username=username) @pytest.mark.django_db def test_fail_double_create_user(post, admin): response = post(reverse('api:user_list'), EXAMPLE_USER_DATA, admin, middleware=SessionMiddleware(mock.Mock())) assert response.status_code == 201 response = post(reverse('api:user_list'), EXAMPLE_USER_DATA, admin, middleware=SessionMiddleware(mock.Mock())) assert response.status_code == 400 @pytest.mark.django_db def test_creating_user_retains_session(post, admin): ''' Creating a new user should not refresh a new session id for the current user. ''' with mock.patch('awx.api.serializers.update_session_auth_hash') as update_session_auth_hash: response = post(reverse('api:user_list'), EXAMPLE_USER_DATA, admin) assert response.status_code == 201 assert not update_session_auth_hash.called @pytest.mark.django_db def test_updating_own_password_refreshes_session(patch, admin): ''' Updating your own password should refresh the session id. ''' with mock.patch('awx.api.serializers.update_session_auth_hash') as update_session_auth_hash: # Attention: If the Django password validator `CommonPasswordValidator` # is active, this test case will fail because this validator raises on # password 'newpassword'. Consider changing the hard-coded password to # something uncommon. patch(reverse('api:user_detail', kwargs={'pk': admin.pk}), {'password': 'newpassword'}, admin, middleware=SessionMiddleware(mock.Mock())) assert update_session_auth_hash.called @pytest.mark.django_db def test_create_delete_create_user(post, delete, admin): response = post(reverse('api:user_list'), EXAMPLE_USER_DATA, admin, middleware=SessionMiddleware(mock.Mock())) assert response.status_code == 201 response = delete(reverse('api:user_detail', kwargs={'pk': response.data['id']}), admin, middleware=SessionMiddleware(mock.Mock())) assert response.status_code == 204 response = post(reverse('api:user_list'), EXAMPLE_USER_DATA, admin, middleware=SessionMiddleware(mock.Mock())) print(response.data) assert response.status_code == 201 @pytest.mark.django_db def test_user_cannot_update_last_login(patch, admin): assert admin.last_login is None patch(reverse('api:user_detail', kwargs={'pk': admin.pk}), {'last_login': '2020-03-13T16:39:47.303016Z'}, admin, middleware=SessionMiddleware(mock.Mock())) assert User.objects.get(pk=admin.pk).last_login is None @pytest.mark.django_db def test_user_verify_attribute_created(admin, get): assert admin.created == admin.date_joined resp = get(reverse('api:user_detail', kwargs={'pk': admin.pk}), admin) assert resp.data['created'] == admin.date_joined past = date(2020, 1, 1).isoformat() for op, count in (('gt', 1), ('lt', 0)): resp = get(reverse('api:user_list') + f'?created__{op}={past}', admin) assert resp.data['count'] == count @pytest.mark.django_db def test_org_not_shown_in_admin_user_sublists(admin_user, get, organization): for view_name in ('user_admin_of_organizations_list', 'user_organizations_list'): url = reverse(f'api:{view_name}', kwargs={'pk': admin_user.pk}) r = get(url, user=admin_user, expect=200) assert organization.pk not in [org['id'] for org in r.data['results']] @pytest.mark.django_db def test_admin_user_not_shown_in_org_users(admin_user, get, organization): for view_name in ('organization_users_list', 'organization_admins_list'): url = reverse(f'api:{view_name}', kwargs={'pk': organization.pk}) r = get(url, user=admin_user, expect=200) assert admin_user.pk not in [u['id'] for u in r.data['results']]