Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DO NOT MERGE #73

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[bumpversion]
commit = False
tag = False
current_version = 0.12.0
current_version = 0.13.0

[bumpversion:file:setup.cfg]

Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
Changelog
=========

0.13.0 (2023-12-21)
===================

* [`#65`_] Add functionality to make users superuser based on groups
* [`#68`_] More clear label/helptext for sync_groups

.. _#65: https://github.com/maykinmedia/mozilla-django-oidc-db/issues/65
.. _#68: https://github.com/maykinmedia/mozilla-django-oidc-db/issues/68

0.12.0 (2022-12-14)
===================

Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
Welcome to mozilla_django_oidc_db's documentation!
==================================================

:Version: 0.12.0
:Version: 0.13.0
:Source: https://github.com/maykinmedia/mozilla-django-oidc-db
:Keywords: OIDC, django, database, authentication
:PythonVersion: 3.7
Expand Down
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,24 @@
# Generated by Django 3.2.18 on 2023-12-21 14:07

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
(
"mozilla_django_oidc_db",
"0011_alter_openidconnectconfig_userinfo_claims_source",
),
]

operations = [
migrations.AlterField(
model_name="openidconnectconfig",
name="sync_groups",
field=models.BooleanField(
default=True,
help_text="If checked, local user groups will be created for group names present in the groups claim, if they do not exist yet locally.",
verbose_name="Create local user groups if they do not exist yet",
),
),
]
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",
),
),
]
12 changes: 12 additions & 0 deletions mozilla_django_oidc_db/migrations/0013_merge_20231221_1529.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Generated by Django 3.2.18 on 2023-12-21 14:29

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("mozilla_django_oidc_db", "0012_alter_openidconnectconfig_sync_groups"),
("mozilla_django_oidc_db", "0012_openidconnectconfig_superuser_group_names"),
]

operations = []
18 changes: 14 additions & 4 deletions mozilla_django_oidc_db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,11 @@ class OpenIDConnectConfig(CachingMixin, OpenIDConnectConfigBase):
),
)
sync_groups = models.BooleanField(
_("synchronize groups"),
_("Create local user groups if they do not exist yet"),
default=True,
help_text=_(
"Synchronize the local user groups with the provided groups. Note that this "
"means a user is removed from all groups if there is no group claim. "
"Uncheck to manage groups manually."
"If checked, local user groups will be created for group names present in "
"the groups claim, if they do not exist yet locally."
),
)
sync_groups_glob_pattern = models.CharField(
Expand Down Expand Up @@ -293,6 +292,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
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# see http://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files
[metadata]
name = mozilla-django-oidc-db
version = 0.12.0
version = 0.13.0
description = A database-backed configuration for mozilla-django-oidc
long_description = file: README.rst
url = https://github.com/maykinmedia/mozilla-django-oidc-db
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
Loading