diff --git a/mozilla_django_oidc_db/admin.py b/mozilla_django_oidc_db/admin.py index 1406422..a0e40d7 100644 --- a/mozilla_django_oidc_db/admin.py +++ b/mozilla_django_oidc_db/admin.py @@ -51,6 +51,7 @@ class OpenIDConnectConfigAdmin(DynamicArrayMixin, SingletonModelAdmin): "sync_groups_glob_pattern", "default_groups", "make_users_staff", + "superuser_group_names", ) }, ), diff --git a/mozilla_django_oidc_db/backends.py b/mozilla_django_oidc_db/backends.py index 4c97687..aeab936 100644 --- a/mozilla_django_oidc_db/backends.py +++ b/mozilla_django_oidc_db/backends.py @@ -145,11 +145,30 @@ def update_user(self, user, claims): user.save(update_fields=values.keys()) + self.update_user_superuser_status(user, claims) + self.update_user_groups(user, claims) self.update_user_default_groups(user) return user + def update_user_superuser_status(self, user, claims): + """ + Assigns superuser status to the user if the user is a member of at least one + specific group. Superuser status is explicitly removed if the user is not or + no longer member of at least one of these groups. + """ + groups_claim = self.config.groups_claim + superuser_group_names = self.config.superuser_group_names + + if superuser_group_names: + claim_groups = glom(claims, groups_claim, default=[]) + if set(superuser_group_names) & set(claim_groups): + user.is_superuser = True + else: + user.is_superuser = False + user.save() + def update_user_groups(self, user, claims): """ Updates user group memberships based on the group_claim setting. diff --git a/mozilla_django_oidc_db/migrations/0012_openidconnectconfig_superuser_group_names.py b/mozilla_django_oidc_db/migrations/0012_openidconnectconfig_superuser_group_names.py new file mode 100644 index 0000000..f31df73 --- /dev/null +++ b/mozilla_django_oidc_db/migrations/0012_openidconnectconfig_superuser_group_names.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.18 on 2023-12-21 11:59 + +from django.db import migrations, models + +import django_better_admin_arrayfield.models.fields + + +class Migration(migrations.Migration): + dependencies = [ + ( + "mozilla_django_oidc_db", + "0011_alter_openidconnectconfig_userinfo_claims_source", + ), + ] + + operations = [ + migrations.AddField( + model_name="openidconnectconfig", + name="superuser_group_names", + field=django_better_admin_arrayfield.models.fields.ArrayField( + base_field=models.CharField( + max_length=50, verbose_name="Superuser group name" + ), + blank=True, + default=list, + help_text="If any of these group names are present in the claims upon login, the user will be marked as a superuser. If none of these groups are present the user will lose superuser permissions.", + size=None, + verbose_name="Superuser group names", + ), + ), + ] diff --git a/mozilla_django_oidc_db/models.py b/mozilla_django_oidc_db/models.py index a40a1b5..8789df8 100644 --- a/mozilla_django_oidc_db/models.py +++ b/mozilla_django_oidc_db/models.py @@ -293,6 +293,17 @@ class OpenIDConnectConfig(CachingMixin, OpenIDConnectConfigBase): "users to login to the admin interface. By default they have no permissions, even if they are staff." ), ) + superuser_group_names = ArrayField( + verbose_name=_("Superuser group names"), + base_field=models.CharField(_("Superuser group name"), max_length=50), + default=list, + blank=True, + help_text=_( + "If any of these group names are present in the claims upon login, " + "the user will be marked as a superuser. If none of these groups are present " + "the user will lose superuser permissions." + ), + ) class Meta: verbose_name = _("OpenID Connect configuration") diff --git a/tests/test_backend.py b/tests/test_backend.py index d7659c2..ecceadd 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -284,8 +284,13 @@ def test_backend_update_user(mock_get_solo): User = get_user_model() # Create two users with the same email address, this shouldn't cause problems + # is_superuser should not be affected if `superuser_group_names` is not set user1 = User.objects.create( - username="123456", email="admin@localhost", first_name="John", last_name="Doe" + username="123456", + email="admin@localhost", + first_name="John", + last_name="Doe", + is_superuser=True, ) user2 = User.objects.create( username="654321", email="admin@localhost", first_name="Jane", last_name="Doe" @@ -307,6 +312,7 @@ def test_backend_update_user(mock_get_solo): assert user.email == "modified@localhost" assert user.first_name == "Name" assert user.last_name == "Modified" + assert user.is_superuser @pytest.mark.django_db @@ -604,3 +610,134 @@ def test_backend_init_cache_not_called(mock_get_solo): # `OpenIDConnectConfig.get_solo` should not be called when initializing the backend assert mock_get_solo.call_count == 0 + + +@pytest.mark.django_db +@patch("mozilla_django_oidc_db.models.OpenIDConnectConfig.get_solo") +def test_backend_update_user_superuser(mock_get_solo): + oidc_config = OpenIDConnectConfig( + id=1, + enabled=True, + oidc_rp_client_id="testid", + oidc_rp_client_secret="secret", + oidc_rp_sign_algo="HS256", + oidc_rp_scopes_list=["openid", "email"], + oidc_op_jwks_endpoint="http://some.endpoint/v1/jwks", + oidc_op_authorization_endpoint="http://some.endpoint/v1/auth", + oidc_op_token_endpoint="http://some.endpoint/v1/token", + oidc_op_user_endpoint="http://some.endpoint/v1/user", + groups_claim="roles", + sync_groups=False, + superuser_group_names=["superuser"], + ) + mock_get_solo.return_value = oidc_config + + claims = { + "sub": "123456", + "roles": ["superuser", "groupadmin"], + } + + backend = OIDCAuthenticationBackend() + + user = backend.create_user(claims) + + # Verify that the groups were created + assert Group.objects.count() == 0 + + # Verify that a user is created with the correct values + assert user.username == "123456" + assert user.is_superuser + + +@pytest.mark.django_db +@patch("mozilla_django_oidc_db.models.OpenIDConnectConfig.get_solo") +def test_backend_update_user_remove_superuser(mock_get_solo): + oidc_config = OpenIDConnectConfig( + id=1, + enabled=True, + oidc_rp_client_id="testid", + oidc_rp_client_secret="secret", + oidc_rp_sign_algo="HS256", + oidc_rp_scopes_list=["openid", "email"], + oidc_op_jwks_endpoint="http://some.endpoint/v1/jwks", + oidc_op_authorization_endpoint="http://some.endpoint/v1/auth", + oidc_op_token_endpoint="http://some.endpoint/v1/token", + oidc_op_user_endpoint="http://some.endpoint/v1/user", + groups_claim="roles", + sync_groups=False, + superuser_group_names=["superuser"], + ) + mock_get_solo.return_value = oidc_config + + User = get_user_model() + user = User.objects.create( + username="123456", + email="admin@localhost", + first_name="John", + last_name="Doe", + is_superuser=True, + ) + + claims = { + "sub": "123456", + "roles": ["nosuperuser", "groupadmin"], + } + + backend = OIDCAuthenticationBackend() + + user = backend.update_user(user, claims) + + # Verify that the groups were created + assert Group.objects.count() == 0 + + # Verify that a user is created with the correct values + assert user.username == "123456" + assert not user.is_superuser + + +@pytest.mark.django_db +@patch("mozilla_django_oidc_db.models.OpenIDConnectConfig.get_solo") +def test_backend_update_user_no_superuser_group_names( + mock_get_solo, +): + oidc_config = OpenIDConnectConfig( + id=1, + enabled=True, + oidc_rp_client_id="testid", + oidc_rp_client_secret="secret", + oidc_rp_sign_algo="HS256", + oidc_rp_scopes_list=["openid", "email"], + oidc_op_jwks_endpoint="http://some.endpoint/v1/jwks", + oidc_op_authorization_endpoint="http://some.endpoint/v1/auth", + oidc_op_token_endpoint="http://some.endpoint/v1/token", + oidc_op_user_endpoint="http://some.endpoint/v1/user", + groups_claim="roles", + sync_groups=False, + superuser_group_names=[], + ) + mock_get_solo.return_value = oidc_config + + User = get_user_model() + user = User.objects.create( + username="123456", + email="admin@localhost", + first_name="John", + last_name="Doe", + is_superuser=True, + ) + + claims = { + "sub": "123456", + "roles": ["nosuperuser", "groupadmin"], + } + + backend = OIDCAuthenticationBackend() + + user = backend.update_user(user, claims) + + # Verify that the groups were created + assert Group.objects.count() == 0 + + # Verify that a user is created with the correct values + assert user.username == "123456" + assert user.is_superuser