Skip to content

Commit

Permalink
Merge pull request #67 from maykinmedia/feature/65-superuser-claim
Browse files Browse the repository at this point in the history
✨ [#65] Add functionality to make users superuser based on groups
  • Loading branch information
stevenbal authored Dec 21, 2023
2 parents 7d57717 + 0e7f066 commit e364be9
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 1 deletion.
1 change: 1 addition & 0 deletions mozilla_django_oidc_db/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class OpenIDConnectConfigAdmin(DynamicArrayMixin, SingletonModelAdmin):
"sync_groups_glob_pattern",
"default_groups",
"make_users_staff",
"superuser_group_names",
)
},
),
Expand Down
19 changes: 19 additions & 0 deletions mozilla_django_oidc_db/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
11 changes: 11 additions & 0 deletions mozilla_django_oidc_db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
139 changes: 138 additions & 1 deletion tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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

0 comments on commit e364be9

Please sign in to comment.