|
76 | 76 | from core.users.models import UserProfile |
77 | 77 | from core.users.tests.factories import UserProfileFactory |
78 | 78 | from .backends import OCLOIDCAuthenticationBackend |
79 | | -from .exceptions import SSOIdentityMismatchRefused |
| 79 | +from .exceptions import SSOIdentityMismatchRefused, SSOUsernameCaseMismatchRefused |
80 | 80 | from .checksums import Checksum |
81 | 81 | from .serializers import IdentifierSerializer |
82 | 82 | from .validators import URIValidator |
@@ -2802,20 +2802,71 @@ def test_update_user_keeps_a_stored_sso_id(self): |
2802 | 2802 | self.backend.update_user(user, {**self.claim, 'sub': 'sub-2'}) |
2803 | 2803 | self.assertEqual(user.sso_id, 'sub-1') |
2804 | 2804 |
|
2805 | | - def test_filter_users_by_claims_matches_username_exactly(self): |
2806 | | - """Keycloak sends usernames in lowercase, and a mixed-case account is never matched, active or not""" |
| 2805 | + def test_filter_users_by_claims_prefers_the_exact_username(self): |
2807 | 2806 | UserProfileFactory(username='Batman', email='batman@gotham.com') |
| 2807 | + batman = UserProfileFactory(username='batman', email='batman@gotham.com') |
| 2808 | + |
| 2809 | + with self.assertNumQueries(1): |
| 2810 | + users = self.backend.filter_users_by_claims(self.claim) |
| 2811 | + self.assertEqual(list(users), [batman]) |
| 2812 | + |
| 2813 | + def test_filter_users_by_claims_matches_username_ignoring_case_with_its_verified_email(self): |
| 2814 | + batman = UserProfileFactory(username='Batman', email=' Batman@Gotham.com') |
| 2815 | + |
| 2816 | + users = self.backend.filter_users_by_claims({**self.claim, 'sub': 'sub-1'}) |
| 2817 | + self.assertEqual(list(users), [batman]) |
| 2818 | + self.backend.update_user(users[0], {**self.claim, 'sub': 'sub-1'}) |
| 2819 | + |
| 2820 | + batman.refresh_from_db() |
| 2821 | + self.assertEqual(batman.username, 'Batman') |
| 2822 | + self.assertEqual(batman.sso_id, 'sub-1') |
| 2823 | + self.assertEqual(UserProfile.objects.filter(username__iexact='batman').count(), 1) |
| 2824 | + |
| 2825 | + def test_filter_users_by_claims_refuses_username_ignoring_case_without_its_verified_email(self): |
| 2826 | + batman = UserProfileFactory(username='Batman', email='batman@gotham.com', first_name='Bat') |
| 2827 | + without_email_verified = {key: value for key, value in self.claim.items() if key != 'email_verified'} |
| 2828 | + |
| 2829 | + for claims in [ |
| 2830 | + {**self.claim, 'email_verified': False}, |
| 2831 | + {**self.claim, 'email_verified': 'true'}, |
| 2832 | + without_email_verified, |
| 2833 | + {**self.claim, 'email': 'joker@gotham.com'}, |
| 2834 | + {**self.claim, 'email': None}, |
| 2835 | + {**self.claim, 'email': ''}, |
| 2836 | + ]: |
| 2837 | + with self.subTest(claims=claims): |
| 2838 | + with patch('core.users.models.UserProfile.save') as save_mock: |
| 2839 | + with self.assertRaises(SSOUsernameCaseMismatchRefused): |
| 2840 | + self.backend.filter_users_by_claims(claims) |
| 2841 | + save_mock.assert_not_called() |
| 2842 | + |
| 2843 | + batman.refresh_from_db() |
| 2844 | + self.assertEqual(batman.first_name, 'Bat') |
| 2845 | + self.assertIsNone(batman.sso_id) |
| 2846 | + |
| 2847 | + def test_filter_users_by_claims_refuses_several_accounts_matching_username_ignoring_case(self): |
| 2848 | + UserProfileFactory(username='Batman', email='batman@gotham.com') |
| 2849 | + UserProfileFactory(username='BatMan', email='joker@gotham.com') |
| 2850 | + |
| 2851 | + with self.assertRaises(SSOUsernameCaseMismatchRefused): |
| 2852 | + self.backend.filter_users_by_claims(self.claim) |
| 2853 | + |
| 2854 | + def test_filter_users_by_claims_refuses_username_ignoring_case_linked_to_a_different_sub(self): |
| 2855 | + UserProfileFactory(username='Batman', email='batman@gotham.com', sso_id='sub-1') |
| 2856 | + |
| 2857 | + with self.assertRaises(SSOIdentityMismatchRefused): |
| 2858 | + self.backend.filter_users_by_claims({**self.claim, 'sub': 'sub-2'}) |
| 2859 | + |
| 2860 | + def test_filter_users_by_claims_reactivates_deactivated_user_ignoring_case_with_its_verified_email(self): |
2808 | 2861 | robin = UserProfileFactory(username='Robin', email='robin@gotham.com') |
2809 | 2862 | robin.deactivate() |
2810 | 2863 |
|
2811 | | - self.assertEqual(self.backend.filter_users_by_claims(self.claim).count(), 0) |
2812 | | - self.assertEqual( |
2813 | | - self.backend.filter_users_by_claims( |
2814 | | - {**self.claim, 'preferred_username': 'robin', 'email': 'robin@gotham.com'}).count(), |
2815 | | - 0 |
2816 | | - ) |
| 2864 | + users = self.backend.filter_users_by_claims( |
| 2865 | + {**self.claim, 'preferred_username': 'robin', 'email': 'robin@gotham.com'}) |
| 2866 | + |
| 2867 | + self.assertEqual(list(users), [robin]) |
2817 | 2868 | robin.refresh_from_db() |
2818 | | - self.assertFalse(robin.is_active) |
| 2869 | + self.assertTrue(robin.is_active) |
2819 | 2870 |
|
2820 | 2871 | def test_filter_users_by_claims_reactivates_deactivated_user_with_its_verified_email(self): |
2821 | 2872 | batman = UserProfileFactory(username='batman', email='Batman@Gotham.com ') |
@@ -2936,19 +2987,24 @@ def test_get_or_create_user_for_deactivated_user(self, get_userinfo_mock): |
2936 | 2987 | self.assertEqual(batman.first_name, 'Bruce') |
2937 | 2988 |
|
2938 | 2989 | @patch('core.common.backends.OCLOIDCAuthenticationBackend.get_userinfo') |
2939 | | - def test_get_or_create_user_leaves_mixed_case_deactivated_user(self, get_userinfo_mock): |
2940 | | - get_userinfo_mock.return_value = self.claim |
| 2990 | + def test_get_or_create_user_for_mixed_case_user(self, get_userinfo_mock): |
2941 | 2991 | old_batman = UserProfileFactory(username='Batman', email='batman@gotham.com', first_name='Bat') |
2942 | | - old_batman.deactivate() |
2943 | 2992 |
|
| 2993 | + get_userinfo_mock.return_value = {**self.claim, 'email': 'joker@gotham.com'} |
| 2994 | + with patch.object(self.backend, 'create_user') as create_user_mock: |
| 2995 | + with self.assertRaises(SSOUsernameCaseMismatchRefused): |
| 2996 | + self.backend.get_or_create_user('access-token', None, None) |
| 2997 | + create_user_mock.assert_not_called() |
| 2998 | + self.assertEqual(UserProfile.objects.filter(username__iexact='batman').count(), 1) |
| 2999 | + |
| 3000 | + get_userinfo_mock.return_value = self.claim |
2944 | 3001 | user = self.backend.get_or_create_user('access-token', None, None) |
2945 | 3002 |
|
2946 | | - self.assertNotEqual(user, old_batman) |
2947 | | - self.assertEqual(user.username, 'batman') |
2948 | | - self.assertTrue(user.is_active) |
| 3003 | + self.assertEqual(user, old_batman) |
| 3004 | + self.assertEqual(UserProfile.objects.filter(username__iexact='batman').count(), 1) |
2949 | 3005 | old_batman.refresh_from_db() |
2950 | | - self.assertFalse(old_batman.is_active) |
2951 | | - self.assertEqual(old_batman.first_name, 'Bat') |
| 3006 | + self.assertEqual(old_batman.username, 'Batman') |
| 3007 | + self.assertEqual(old_batman.first_name, 'Bruce') |
2952 | 3008 |
|
2953 | 3009 | @patch('mozilla_django_oidc.auth.OIDCAuthenticationBackend.authenticate') |
2954 | 3010 | def test_authenticate_fails_and_drops_stored_tokens_when_sign_in_is_refused(self, authenticate_mock): |
|
0 commit comments