diff --git a/passbook/admin/forms/users.py b/passbook/admin/forms/users.py index c3a640396..2e9f51eb2 100644 --- a/passbook/admin/forms/users.py +++ b/passbook/admin/forms/users.py @@ -12,7 +12,7 @@ class UserForm(forms.ModelForm): class Meta: model = User - fields = ["username", "name", "email", "is_staff", "is_active", "attributes"] + fields = ["username", "name", "email", "is_active", "attributes"] widgets = { "name": forms.TextInput, "attributes": CodeMirrorWidget, diff --git a/passbook/admin/templates/administration/group/list.html b/passbook/admin/templates/administration/group/list.html index c9ddbc929..44a498c74 100644 --- a/passbook/admin/templates/administration/group/list.html +++ b/passbook/admin/templates/administration/group/list.html @@ -50,7 +50,7 @@ - {{ group.user_set.all|length }} + {{ group.users.all|length }} diff --git a/passbook/admin/tests.py b/passbook/admin/tests.py index a7017775a..13ced0c9c 100644 --- a/passbook/admin/tests.py +++ b/passbook/admin/tests.py @@ -8,7 +8,7 @@ from django.test import Client, TestCase from django.urls.exceptions import NoReverseMatch from passbook.admin.urls import urlpatterns -from passbook.core.models import User +from passbook.core.models import Group, User from passbook.lib.utils.reflection import get_apps @@ -16,7 +16,9 @@ class TestAdmin(TestCase): """Generic admin tests""" def setUp(self): - self.user = User.objects.create_superuser(username="test") + self.user = User.objects.create_user(username="test") + self.user.pb_groups.add(Group.objects.filter(is_superuser=True).first()) + self.user.save() self.client = Client() self.client.force_login(self.user) diff --git a/passbook/core/api/groups.py b/passbook/core/api/groups.py index fbd9fc7e0..1af8ab425 100644 --- a/passbook/core/api/groups.py +++ b/passbook/core/api/groups.py @@ -11,7 +11,7 @@ class GroupSerializer(ModelSerializer): class Meta: model = Group - fields = ["pk", "name", "parent", "user_set", "attributes"] + fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"] class GroupViewSet(ModelViewSet): diff --git a/passbook/core/api/users.py b/passbook/core/api/users.py index d583fbf5e..dc04e6deb 100644 --- a/passbook/core/api/users.py +++ b/passbook/core/api/users.py @@ -1,5 +1,5 @@ """User API Views""" -from rest_framework.serializers import ModelSerializer +from rest_framework.serializers import BooleanField, ModelSerializer from rest_framework.viewsets import ModelViewSet from passbook.core.models import User @@ -8,10 +8,12 @@ from passbook.core.models import User class UserSerializer(ModelSerializer): """User Serializer""" + is_superuser = BooleanField(read_only=True) + class Meta: model = User - fields = ["pk", "username", "name", "email"] + fields = ["pk", "username", "name", "is_superuser", "email"] class UserViewSet(ModelViewSet): diff --git a/passbook/core/forms/groups.py b/passbook/core/forms/groups.py index 3c8dae507..ffb85dab2 100644 --- a/passbook/core/forms/groups.py +++ b/passbook/core/forms/groups.py @@ -18,21 +18,19 @@ class GroupForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance.pk: - self.initial["members"] = self.instance.user_set.values_list( - "pk", flat=True - ) + self.initial["members"] = self.instance.users.values_list("pk", flat=True) def save(self, *args, **kwargs): instance = super().save(*args, **kwargs) if instance.pk: - instance.user_set.clear() - instance.user_set.add(*self.cleaned_data["members"]) + instance.users.clear() + instance.users.add(*self.cleaned_data["members"]) return instance class Meta: model = Group - fields = ["name", "parent", "members", "attributes"] + fields = ["name", "is_superuser", "parent", "members", "attributes"] widgets = { "name": forms.TextInput(), "attributes": CodeMirrorWidget, diff --git a/passbook/core/migrations/0003_default_user.py b/passbook/core/migrations/0003_default_user.py index 63af2c780..56f9edd9c 100644 --- a/passbook/core/migrations/0003_default_user.py +++ b/passbook/core/migrations/0003_default_user.py @@ -1,7 +1,7 @@ # Generated by Django 3.0.6 on 2020-05-23 16:40 from django.apps.registry import Apps -from django.db import migrations +from django.db import migrations, models from django.db.backends.base.schema import BaseDatabaseSchemaEditor @@ -15,8 +15,6 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): username="pbadmin", email="root@localhost", name="passbook Default Admin" ) pbadmin.set_password("pbadmin") # noqa # nosec - pbadmin.is_superuser = True - pbadmin.is_staff = True pbadmin.save() @@ -27,5 +25,15 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RemoveField(model_name="user", name="is_superuser",), + migrations.RemoveField(model_name="user", name="is_staff",), migrations.RunPython(create_default_user), + migrations.AddField( + model_name="user", + name="is_superuser", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="user", name="is_staff", field=models.BooleanField(default=False) + ), ] diff --git a/passbook/core/migrations/0009_group_is_superuser.py b/passbook/core/migrations/0009_group_is_superuser.py new file mode 100644 index 000000000..95fbe9f9a --- /dev/null +++ b/passbook/core/migrations/0009_group_is_superuser.py @@ -0,0 +1,44 @@ +# Generated by Django 3.1.1 on 2020-09-15 19:53 +from django.apps.registry import Apps +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +def create_default_admin_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): + db_alias = schema_editor.connection.alias + Group = apps.get_model("passbook_core", "Group") + User = apps.get_model("passbook_core", "User") + + # Creates a default admin group + group, _ = Group.objects.using(db_alias).get_or_create( + is_superuser=True, defaults={"name": "passbook Admins",} + ) + group.users.add(User.objects.get(username="pbadmin")) + group.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_core", "0008_auto_20200824_1532"), + ] + + operations = [ + migrations.RemoveField(model_name="user", name="is_superuser",), + migrations.RemoveField(model_name="user", name="is_staff",), + migrations.AlterField( + model_name="user", + name="pb_groups", + field=models.ManyToManyField( + related_name="users", to="passbook_core.Group" + ), + ), + migrations.AddField( + model_name="group", + name="is_superuser", + field=models.BooleanField( + default=False, help_text="Users added to this group will be superusers." + ), + ), + migrations.RunPython(create_default_admin_group), + ] diff --git a/passbook/core/models.py b/passbook/core/models.py index 43cdbe0ce..d58a53856 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -4,6 +4,7 @@ from typing import Any, Optional, Type from uuid import uuid4 from django.contrib.auth.models import AbstractUser +from django.contrib.auth.models import UserManager as DjangoUserManager from django.db import models from django.db.models import Q, QuerySet from django.forms import ModelForm @@ -34,7 +35,12 @@ class Group(models.Model): """Custom Group model which supports a basic hierarchy""" group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) + name = models.CharField(_("name"), max_length=80) + is_superuser = models.BooleanField( + default=False, help_text=_("Users added to this group will be superusers.") + ) + parent = models.ForeignKey( "Group", blank=True, @@ -52,6 +58,14 @@ class Group(models.Model): unique_together = (("name", "parent",),) +class UserManager(DjangoUserManager): + """Custom user manager that doesn't assign is_superuser and is_staff""" + + def create_user(self, username, email=None, password=None, **extra_fields): + """Custom user manager that doesn't assign is_superuser and is_staff""" + return self._create_user(username, email, password, **extra_fields) + + class User(GuardianUserMixin, AbstractUser): """Custom User model to allow easier adding o f user-based settings""" @@ -59,11 +73,23 @@ class User(GuardianUserMixin, AbstractUser): name = models.TextField(help_text=_("User's display name.")) sources = models.ManyToManyField("Source", through="UserSourceConnection") - pb_groups = models.ManyToManyField("Group") + pb_groups = models.ManyToManyField("Group", related_name="users") password_change_date = models.DateTimeField(auto_now_add=True) attributes = models.JSONField(default=dict, blank=True) + objects = UserManager() + + @property + def is_superuser(self) -> bool: + """Get supseruser status based on membership in a group with superuser status""" + return self.pb_groups.filter(is_superuser=True).exists() + + @property + def is_staff(self) -> bool: + """superuser == staff user""" + return self.is_superuser + def set_password(self, password): if self.pk: password_changed.send(sender=self, user=self, password=password) diff --git a/passbook/core/tests/test_views_overview.py b/passbook/core/tests/test_views_overview.py index 4a857e2d4..0edae05db 100644 --- a/passbook/core/tests/test_views_overview.py +++ b/passbook/core/tests/test_views_overview.py @@ -13,7 +13,7 @@ class TestOverviewViews(TestCase): def setUp(self): super().setUp() - self.user = User.objects.create_superuser( + self.user = User.objects.create_user( username="unittest user", email="unittest@example.com", password="".join( diff --git a/passbook/core/tests/test_views_user.py b/passbook/core/tests/test_views_user.py index 88e984f85..c38a601f3 100644 --- a/passbook/core/tests/test_views_user.py +++ b/passbook/core/tests/test_views_user.py @@ -13,7 +13,7 @@ class TestUserViews(TestCase): def setUp(self): super().setUp() - self.user = User.objects.create_superuser( + self.user = User.objects.create_user( username="unittest user", email="unittest@example.com", password="".join( diff --git a/passbook/policies/group_membership/models.py b/passbook/policies/group_membership/models.py index 3c8dbdafd..55726be1a 100644 --- a/passbook/policies/group_membership/models.py +++ b/passbook/policies/group_membership/models.py @@ -30,7 +30,7 @@ class GroupMembershipPolicy(Policy): return GroupMembershipPolicyForm def passes(self, request: PolicyRequest) -> PolicyResult: - return PolicyResult(self.group.user_set.filter(pk=request.user.pk).exists()) + return PolicyResult(self.group.users.filter(pk=request.user.pk).exists()) class Meta: diff --git a/passbook/policies/group_membership/tests.py b/passbook/policies/group_membership/tests.py index d0e7c43ea..1948942bc 100644 --- a/passbook/policies/group_membership/tests.py +++ b/passbook/policies/group_membership/tests.py @@ -24,7 +24,7 @@ class TestGroupMembershipPolicy(TestCase): def test_valid(self): """user in group""" group = Group.objects.create(name="test") - group.user_set.add(get_anonymous_user()) + group.users.add(get_anonymous_user()) group.save() policy: GroupMembershipPolicy = GroupMembershipPolicy.objects.create( group=group diff --git a/passbook/sources/ldap/connector.py b/passbook/sources/ldap/connector.py index 646ba8e49..54c6f14e6 100644 --- a/passbook/sources/ldap/connector.py +++ b/passbook/sources/ldap/connector.py @@ -151,7 +151,7 @@ class Connector: group_cache[group_dn] = groups.first() group = group_cache[group_dn] users = User.objects.filter(attributes__ldap_uniq=uniq) - group.user_set.add(*list(users)) + group.users.add(*list(users)) # Now that all users are added, lets write everything for _, group in group_cache.items(): group.save() diff --git a/swagger.yaml b/swagger.yaml index 241018062..ac57ced14 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -5922,7 +5922,7 @@ definitions: required: - name - parent - - user_set + - users type: object properties: pk: @@ -5935,11 +5935,15 @@ definitions: type: string maxLength: 80 minLength: 1 + is_superuser: + title: Is superuser + description: Users added to this group will be superusers. + type: boolean parent: title: Parent type: string format: uuid - user_set: + users: type: array items: type: integer @@ -5970,6 +5974,10 @@ definitions: description: User's display name. type: string minLength: 1 + is_superuser: + title: Is superuser + type: boolean + readOnly: true email: title: Email address type: string