Skip to content

Commit

Permalink
Guardian setup (#750)
Browse files Browse the repository at this point in the history
* Bump django from 5.0.6 to 5.0.7 in /server

Bumps [django](https://github.com/django/django) from 5.0.6 to 5.0.7.
- [Commits](django/django@5.0.6...5.0.7)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>

* add Django Guardian to project

* configure django guardian and add Site and Org specific object level permission models

* remove our implementations of the Role and Permission model

* adjust site permission/role verbose name and admin representation

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Loading branch information
dpgraham4401 and dependabot[bot] authored Jul 22, 2024
1 parent 97e073e commit 00124bf
Show file tree
Hide file tree
Showing 15 changed files with 290 additions and 110 deletions.
6 changes: 3 additions & 3 deletions server/apps/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from apps.profile.models import Profile, RcrainfoProfile, RcrainfoSiteAccess

from .models import Permission, Role, TrakUser
from .models import GroupPermission, TrakUser, UserPermission


class HiddenListView(admin.ModelAdmin):
Expand Down Expand Up @@ -73,5 +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)
admin.site.register(UserPermission)
admin.site.register(GroupPermission)
47 changes: 47 additions & 0 deletions server/apps/core/migrations/0004_grouppermission_userpermission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 5.0.6 on 2024-07-22 19:25

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('contenttypes', '0002_remove_content_type_name'),
('core', '0003_role'),
]

operations = [
migrations.CreateModel(
name='GroupPermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_pk', models.CharField(max_length=255, verbose_name='object ID')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')),
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')),
],
options={
'abstract': False,
'indexes': [models.Index(fields=['content_type', 'object_pk'], name='core_groupp_content_9384ee_idx')],
'unique_together': {('group', 'permission', 'object_pk')},
},
),
migrations.CreateModel(
name='UserPermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_pk', models.CharField(max_length=255, verbose_name='object ID')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
'indexes': [models.Index(fields=['content_type', 'object_pk'], name='core_userpe_content_92a909_idx')],
'unique_together': {('user', 'permission', 'object_pk')},
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-07-22 20:03

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('core', '0004_grouppermission_userpermission'),
]

operations = [
migrations.RemoveField(
model_name='role',
name='permissions',
),
migrations.DeleteModel(
name='Permission',
),
migrations.DeleteModel(
name='Role',
),
]
55 changes: 17 additions & 38 deletions server/apps/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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 _
from guardian.models import GroupObjectPermissionAbstract, UserObjectPermissionAbstract


class TrakUser(AbstractUser):
Expand All @@ -21,44 +21,23 @@ class Meta:
)


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 UserPermission(UserObjectPermissionAbstract):
"""
User object permission model for Haztrak.
access via guardian.utils.get_user_obj_perms_model()
We define this class if we need to customize User object level permissions later.
"""

class Meta(UserObjectPermissionAbstract.Meta):
abstract = False

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"),
)
class GroupPermission(GroupObjectPermissionAbstract):
"""
Group object permission model for Haztrak.
access via guardian.utils get_group_obj_perms_model()
We define this class if we need to customize Group object level permissions later.
"""

def __str__(self):
return f"{self.name}"
class Meta(GroupObjectPermissionAbstract.Meta):
abstract = False
22 changes: 0 additions & 22 deletions server/apps/core/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
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


Expand Down Expand Up @@ -42,23 +40,3 @@ 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
42 changes: 0 additions & 42 deletions server/apps/core/tests/test_models.py

This file was deleted.

23 changes: 22 additions & 1 deletion server/apps/org/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from django.contrib import admin
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from guardian.admin import GuardedModelAdmin

from apps.org.models import Org, OrgAccess
from apps.org.models import Org, OrgAccess, OrgGroupObjectPermission, OrgUserObjectPermission
from apps.site.models import Site

admin.site.register(OrgAccess)
Expand All @@ -19,3 +22,21 @@ def rcrainfo_integrated(self, obj):

def number_of_sites(self, org: Org):
return Site.objects.filter(org=org).count()


@admin.register(OrgUserObjectPermission)
class OrgUserObjectPermissionAdmin(GuardedModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "permission":
content_type = ContentType.objects.get_for_model(Site)
kwargs["queryset"] = Permission.objects.filter(content_type=content_type)
return super().formfield_for_foreignkey(db_field, request, **kwargs)


@admin.register(OrgGroupObjectPermission)
class OrgGroupObjectPermissionAdmin(GuardedModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "permission":
content_type = ContentType.objects.get_for_model(Site)
kwargs["queryset"] = Permission.objects.filter(content_type=content_type)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 5.0.6 on 2024-07-22 19:25

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('org', '0002_rename_trakorg_org_rename_trakorgaccess_orgaccess'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='OrgGroupObjectPermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='org.org')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')),
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')),
],
options={
'abstract': False,
'unique_together': {('group', 'permission', 'content_object')},
},
),
migrations.CreateModel(
name='OrgUserObjectPermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='org.org')),
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
'unique_together': {('user', 'permission', 'content_object')},
},
),
]
13 changes: 13 additions & 0 deletions server/apps/org/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from django.conf import settings
from django.db import models
from guardian.models.models import GroupObjectPermissionBase, UserObjectPermissionBase

from apps.profile.models import RcrainfoProfile

Expand Down Expand Up @@ -86,3 +87,15 @@ class Meta:

def __str__(self):
return f"{self.user} - {self.org}"


class OrgUserObjectPermission(UserObjectPermissionBase):
"""Org object level permission."""

content_object = models.ForeignKey(Org, on_delete=models.CASCADE)


class OrgGroupObjectPermission(GroupObjectPermissionBase):
"""Org object level Group."""

content_object = models.ForeignKey(Org, on_delete=models.CASCADE)
23 changes: 22 additions & 1 deletion server/apps/site/admin.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
from django.contrib import admin
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from guardian.admin import GuardedModelAdmin

from apps.site.models import Site
from apps.site.models import Site, SiteGroupObjectPermission, SiteUserObjectPermission


@admin.register(Site)
class HaztrakSiteAdmin(admin.ModelAdmin):
list_display = ["__str__", "last_rcrainfo_manifest_sync"]
readonly_fields = ["rcra_site"]
search_fields = ["rcra_site__epa_id"]


@admin.register(SiteUserObjectPermission)
class SiteUserObjectPermissionAdmin(GuardedModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "permission":
content_type = ContentType.objects.get_for_model(Site)
kwargs["queryset"] = Permission.objects.filter(content_type=content_type)
return super().formfield_for_foreignkey(db_field, request, **kwargs)


@admin.register(SiteGroupObjectPermission)
class SiteGroupObjectPermissionAdmin(GuardedModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "permission":
content_type = ContentType.objects.get_for_model(Site)
kwargs["queryset"] = Permission.objects.filter(content_type=content_type)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
Loading

0 comments on commit 00124bf

Please sign in to comment.