From f677506b6f8080225c4e713b7f5dcef913cb7364 Mon Sep 17 00:00:00 2001 From: David Paul Graham <43794491+dpgraham4401@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:13:00 -0400 Subject: [PATCH] Proxy permission and role model (#749) * new proxy Permission model * Role Model --- server/apps/core/admin.py | 4 +- .../apps/core/migrations/0002_permission.py | 32 +++++++++++++ server/apps/core/migrations/0003_role.py | 26 ++++++++++ server/apps/core/models.py | 47 ++++++++++++++++++- server/apps/core/tests/conftest.py | 22 +++++++++ server/apps/core/tests/test_models.py | 42 +++++++++++++++++ 6 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 server/apps/core/migrations/0002_permission.py create mode 100644 server/apps/core/migrations/0003_role.py create mode 100644 server/apps/core/tests/test_models.py diff --git a/server/apps/core/admin.py b/server/apps/core/admin.py index 34b57d46..50288e80 100644 --- a/server/apps/core/admin.py +++ b/server/apps/core/admin.py @@ -5,7 +5,7 @@ from apps.profile.models import Profile, RcrainfoProfile, RcrainfoSiteAccess -from .models import TrakUser +from .models import Permission, Role, TrakUser class HiddenListView(admin.ModelAdmin): @@ -73,3 +73,5 @@ def api_user(self, profile: RcrainfoProfile) -> bool: admin.site.register(Profile) admin.site.unregister(DRFToken) +admin.site.register(Permission) +admin.site.register(Role) diff --git a/server/apps/core/migrations/0002_permission.py b/server/apps/core/migrations/0002_permission.py new file mode 100644 index 00000000..4b2223ee --- /dev/null +++ b/server/apps/core/migrations/0002_permission.py @@ -0,0 +1,32 @@ +# Generated by Django 5.0.6 on 2024-07-11 19:06 + +import django.contrib.auth.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Permission', + fields=[ + ], + options={ + 'verbose_name': 'Permission', + 'verbose_name_plural': 'Permissions', + 'ordering': ['name'], + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('auth.permission',), + managers=[ + ('objects', django.contrib.auth.models.PermissionManager()), + ], + ), + ] diff --git a/server/apps/core/migrations/0003_role.py b/server/apps/core/migrations/0003_role.py new file mode 100644 index 00000000..305c6b12 --- /dev/null +++ b/server/apps/core/migrations/0003_role.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.6 on 2024-07-11 19:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_permission'), + ] + + operations = [ + migrations.CreateModel( + name='Role', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=150, unique=True, verbose_name='name')), + ('permissions', models.ManyToManyField(to='core.permission', verbose_name='permissions')), + ], + options={ + 'verbose_name': 'Role', + 'verbose_name_plural': 'Roles', + 'ordering': ['name'], + }, + ), + ] diff --git a/server/apps/core/models.py b/server/apps/core/models.py index e49adc54..ba370b19 100644 --- a/server/apps/core/models.py +++ b/server/apps/core/models.py @@ -1,7 +1,9 @@ import uuid -from django.contrib.auth.models import AbstractUser +from django.contrib.auth.models import AbstractUser, Group +from django.contrib.auth.models import Permission as DjangoPermission from django.db import models +from django.utils.translation import gettext_lazy as _ class TrakUser(AbstractUser): @@ -17,3 +19,46 @@ class Meta: editable=False, default=uuid.uuid4, ) + + +class Permission(DjangoPermission): + """Haztrak proxy permission model used for our custom object level permissions.""" + + class Meta: + proxy = True + verbose_name = "Permission" + verbose_name_plural = "Permissions" + ordering = ["name"] + + @property + def app_label(self): + return self.content_type.app_label + + @property + def model_name(self): + return self.content_type.model + + def __str__(self): + return f"{self.content_type.name} | {self.name}" + + +class Role(models.Model): + """A job/function within the system that can assigned to users to grant them permissions.""" + + class Meta: + verbose_name = _("Role") + verbose_name_plural = _("Roles") + ordering = ["name"] + + name = models.CharField( + _("name"), + max_length=150, + unique=True, + ) + permissions = models.ManyToManyField( + Permission, + verbose_name=_("permissions"), + ) + + def __str__(self): + return f"{self.name}" diff --git a/server/apps/core/tests/conftest.py b/server/apps/core/tests/conftest.py index ab40962d..babf8a27 100644 --- a/server/apps/core/tests/conftest.py +++ b/server/apps/core/tests/conftest.py @@ -2,8 +2,10 @@ from typing import Dict, List, Optional import pytest +from django.contrib.contenttypes.models import ContentType from faker import Faker +from apps.core.models import Permission from apps.rcrasite.models import RcraSiteType @@ -40,3 +42,23 @@ def create_quicker_sign( } return create_quicker_sign + + +@pytest.fixture +def permission_factory(faker: Faker): + """ + Factory for creating dynamic permission data + """ + + def create_permission( + name: str = faker.word(), + content_type_id: int = faker.random_int(min=1), + ) -> Permission: + content_type = ContentType.objects.create(app_label=faker.word(), model=faker.word()) + return Permission.objects.create( + name=name, + content_type=content_type, + content_type_id=content_type_id, + ) + + return create_permission diff --git a/server/apps/core/tests/test_models.py b/server/apps/core/tests/test_models.py new file mode 100644 index 00000000..c00a0baf --- /dev/null +++ b/server/apps/core/tests/test_models.py @@ -0,0 +1,42 @@ +import pytest +from django.contrib.contenttypes.models import ContentType + +from apps.core.models import Permission, Role + + +class TestPermissionModel: + mock_app_label = "test_app" + mock_model = "test_model" + + @pytest.fixture + def mock_content_type(self): + return ContentType.objects.create(app_label=self.mock_app_label, model=self.mock_model) + + @pytest.mark.django_db + def test_saves_new_permissions(self, mock_content_type): + permission_name = "test_permission" + permission = Permission.objects.create( + content_type=mock_content_type, content_type_id=1, name=permission_name + ) + + saved_permission = Permission.objects.get(name=permission_name) + + assert saved_permission.name == permission_name + assert saved_permission.id == permission.id + + +class TestRoleModel: + @pytest.mark.django_db + def test_role_with_multiple_permissions(self, permission_factory): + permission1 = permission_factory(content_type_id=1) + permission2 = permission_factory(content_type_id=2) + role = Role.objects.create(name="test_role") + role.permissions.add(permission1, permission2) + assert role.permissions.count() == 2 + assert permission1 in role.permissions.all() + assert permission2 in role.permissions.all() + + @pytest.mark.django_db + def test_role_str_representation(self): + role = Role.objects.create(name="test_role") + assert str(role) == "test_role"