From c40063b4c00c2a6c4a6a5c79ce4a4156174e09b9 Mon Sep 17 00:00:00 2001 From: robines Date: Tue, 2 Jul 2024 23:14:47 +0200 Subject: [PATCH 01/32] Create user roles for organizations/gangs/sections --- backend/root/settings/base.py | 2 + backend/root/utils/mixins.py | 38 +++++++- backend/root/utils/permissions.py | 54 +++++++++++- backend/samfundet/admin.py | 24 ++++++ ...ergangrole_usergangsectionrole_and_more.py | 86 +++++++++++++++++++ backend/samfundet/models/__init__.py | 4 + backend/samfundet/models/general.py | 13 +++ backend/samfundet/models/recruitment.py | 14 +++ backend/samfundet/models/role.py | 36 ++++++++ backend/samfundet/roleauth/__init__.py | 0 backend/samfundet/roleauth/backend.py | 37 ++++++++ backend/samfundet/utils.py | 11 ++- 12 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 backend/samfundet/migrations/0002_role_user_roles_usergangrole_usergangsectionrole_and_more.py create mode 100644 backend/samfundet/models/role.py create mode 100644 backend/samfundet/roleauth/__init__.py create mode 100644 backend/samfundet/roleauth/backend.py diff --git a/backend/root/settings/base.py b/backend/root/settings/base.py index 529c08d34..1e658107b 100644 --- a/backend/root/settings/base.py +++ b/backend/root/settings/base.py @@ -87,6 +87,7 @@ 'corsheaders', 'root', # Register to enable management.commands. 'samfundet', + 'samfundet.roleauth' ] MIDDLEWARE = [ @@ -129,6 +130,7 @@ AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend', # default + 'samfundet.roleauth.backend.RoleAuthBackend', ] # Password validation diff --git a/backend/root/utils/mixins.py b/backend/root/utils/mixins.py index 116e1ae09..09dbde463 100644 --- a/backend/root/utils/mixins.py +++ b/backend/root/utils/mixins.py @@ -3,7 +3,7 @@ import sys import copy import logging -from typing import Any +from typing import TYPE_CHECKING, Any from rest_framework import serializers @@ -15,6 +15,9 @@ LOG = logging.getLogger(__name__) +if TYPE_CHECKING: + from samfundet.models import Gang, GangSection, Organization + class FieldTrackerMixin(Model): """ @@ -180,7 +183,38 @@ def save(self, *args: Any, **kwargs: Any) -> None: super().save(*args, **kwargs) -class CustomBaseModel(FullCleanSaveMixin): +class RoleMixin: + """To be a part of the Role system, at least one of these functions must be implemented.""" + + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + """ + There are often multiple paths to requested resource, typically in many-to-many relationships. + Resolving must be done manually + + :param return_id: May be used to implement resolver without redundant db instance fetching + """ + raise NotImplementedError('Intentionally not implemented: ambiguous resource') + + def resolve_gang(self, *, return_id: bool = False) -> Gang | int: + """ + There are often multiple paths to requested resource, typically in many-to-many relationships. + Resolving must be done manually + + :param return_id: May be used to implement resolver without redundant db instance fetching + """ + raise NotImplementedError('Intentionally not implemented: ambiguous resource') + + def resolve_section(self, *, return_id: bool = False) -> GangSection | int: + """ + There are often multiple paths to requested resource, typically in many-to-many relationships. + Resolving must be done manually + + :param return_id: May be used to implement resolver without redundant db instance fetching + """ + raise NotImplementedError('Intentionally not implemented: ambiguous resource') + + +class CustomBaseModel(RoleMixin, FullCleanSaveMixin): """ Basic model which will contains necessary version info of a model: With by who and when it was updated and created. diff --git a/backend/root/utils/permissions.py b/backend/root/utils/permissions.py index 8d5db0d7f..9af60278f 100644 --- a/backend/root/utils/permissions.py +++ b/backend/root/utils/permissions.py @@ -4,7 +4,7 @@ DO NOT WRITE IN THIS FILE, AS IT WILL BE OVERWRITTEN ON NEXT UPDATE. THIS FILE WAS GENERATED BY: root.management.commands.generate_permissions -LAST UPDATE: 2023-12-31 13:44:38.878050+00:00 +LAST UPDATE: 2024-07-01 13:53:22.576148+00:00 """ ############################################################ @@ -49,6 +49,13 @@ GUARDIAN_VIEW_USEROBJECTPERMISSION = 'guardian.view_userobjectpermission' ### End: guardian ### +### roleauth ### +ROLEAUTH_ADD_ROLE = 'roleauth.add_role' +ROLEAUTH_CHANGE_ROLE = 'roleauth.change_role' +ROLEAUTH_DELETE_ROLE = 'roleauth.delete_role' +ROLEAUTH_VIEW_ROLE = 'roleauth.view_role' +### End: roleauth ### + ### samfundet ### SAMFUNDET_ADD_BILLIGEVENT = 'samfundet.add_billigevent' SAMFUNDET_CHANGE_BILLIGEVENT = 'samfundet.change_billigevent' @@ -75,6 +82,11 @@ SAMFUNDET_DELETE_BOOKING = 'samfundet.delete_booking' SAMFUNDET_VIEW_BOOKING = 'samfundet.view_booking' +SAMFUNDET_ADD_CAMPUS = 'samfundet.add_campus' +SAMFUNDET_CHANGE_CAMPUS = 'samfundet.change_campus' +SAMFUNDET_DELETE_CAMPUS = 'samfundet.delete_campus' +SAMFUNDET_VIEW_CAMPUS = 'samfundet.view_campus' + SAMFUNDET_ADD_CLOSEDPERIOD = 'samfundet.add_closedperiod' SAMFUNDET_CHANGE_CLOSEDPERIOD = 'samfundet.change_closedperiod' SAMFUNDET_DELETE_CLOSEDPERIOD = 'samfundet.delete_closedperiod' @@ -115,6 +127,11 @@ SAMFUNDET_DELETE_GANG = 'samfundet.delete_gang' SAMFUNDET_VIEW_GANG = 'samfundet.view_gang' +SAMFUNDET_ADD_GANGSECTION = 'samfundet.add_gangsection' +SAMFUNDET_CHANGE_GANGSECTION = 'samfundet.change_gangsection' +SAMFUNDET_DELETE_GANGSECTION = 'samfundet.delete_gangsection' +SAMFUNDET_VIEW_GANGSECTION = 'samfundet.view_gangsection' + SAMFUNDET_ADD_GANGTYPE = 'samfundet.add_gangtype' SAMFUNDET_CHANGE_GANGTYPE = 'samfundet.change_gangtype' SAMFUNDET_DELETE_GANGTYPE = 'samfundet.delete_gangtype' @@ -205,21 +222,51 @@ SAMFUNDET_DELETE_RECRUITMENTAPPLICATION = 'samfundet.delete_recruitmentapplication' SAMFUNDET_VIEW_RECRUITMENTAPPLICATION = 'samfundet.view_recruitmentapplication' +SAMFUNDET_ADD_RECRUITMENTCAMPUSSTAT = 'samfundet.add_recruitmentcampusstat' +SAMFUNDET_CHANGE_RECRUITMENTCAMPUSSTAT = 'samfundet.change_recruitmentcampusstat' +SAMFUNDET_DELETE_RECRUITMENTCAMPUSSTAT = 'samfundet.delete_recruitmentcampusstat' +SAMFUNDET_VIEW_RECRUITMENTCAMPUSSTAT = 'samfundet.view_recruitmentcampusstat' + +SAMFUNDET_ADD_RECRUITMENTDATESTAT = 'samfundet.add_recruitmentdatestat' +SAMFUNDET_CHANGE_RECRUITMENTDATESTAT = 'samfundet.change_recruitmentdatestat' +SAMFUNDET_DELETE_RECRUITMENTDATESTAT = 'samfundet.delete_recruitmentdatestat' +SAMFUNDET_VIEW_RECRUITMENTDATESTAT = 'samfundet.view_recruitmentdatestat' + +SAMFUNDET_ADD_RECRUITMENTINTERVIEWAVAILABILITY = 'samfundet.add_recruitmentinterviewavailability' +SAMFUNDET_CHANGE_RECRUITMENTINTERVIEWAVAILABILITY = 'samfundet.change_recruitmentinterviewavailability' +SAMFUNDET_DELETE_RECRUITMENTINTERVIEWAVAILABILITY = 'samfundet.delete_recruitmentinterviewavailability' +SAMFUNDET_VIEW_RECRUITMENTINTERVIEWAVAILABILITY = 'samfundet.view_recruitmentinterviewavailability' + SAMFUNDET_ADD_RECRUITMENTPOSITION = 'samfundet.add_recruitmentposition' SAMFUNDET_CHANGE_RECRUITMENTPOSITION = 'samfundet.change_recruitmentposition' SAMFUNDET_DELETE_RECRUITMENTPOSITION = 'samfundet.delete_recruitmentposition' SAMFUNDET_VIEW_RECRUITMENTPOSITION = 'samfundet.view_recruitmentposition' +SAMFUNDET_ADD_RECRUITMENTSEPERATEPOSITION = 'samfundet.add_recruitmentseperateposition' +SAMFUNDET_CHANGE_RECRUITMENTSEPERATEPOSITION = 'samfundet.change_recruitmentseperateposition' +SAMFUNDET_DELETE_RECRUITMENTSEPERATEPOSITION = 'samfundet.delete_recruitmentseperateposition' +SAMFUNDET_VIEW_RECRUITMENTSEPERATEPOSITION = 'samfundet.view_recruitmentseperateposition' + SAMFUNDET_ADD_RECRUITMENTSTATISTICS = 'samfundet.add_recruitmentstatistics' SAMFUNDET_CHANGE_RECRUITMENTSTATISTICS = 'samfundet.change_recruitmentstatistics' SAMFUNDET_DELETE_RECRUITMENTSTATISTICS = 'samfundet.delete_recruitmentstatistics' SAMFUNDET_VIEW_RECRUITMENTSTATISTICS = 'samfundet.view_recruitmentstatistics' +SAMFUNDET_ADD_RECRUITMENTTIMESTAT = 'samfundet.add_recruitmenttimestat' +SAMFUNDET_CHANGE_RECRUITMENTTIMESTAT = 'samfundet.change_recruitmenttimestat' +SAMFUNDET_DELETE_RECRUITMENTTIMESTAT = 'samfundet.delete_recruitmenttimestat' +SAMFUNDET_VIEW_RECRUITMENTTIMESTAT = 'samfundet.view_recruitmenttimestat' + SAMFUNDET_ADD_RESERVATION = 'samfundet.add_reservation' SAMFUNDET_CHANGE_RESERVATION = 'samfundet.change_reservation' SAMFUNDET_DELETE_RESERVATION = 'samfundet.delete_reservation' SAMFUNDET_VIEW_RESERVATION = 'samfundet.view_reservation' +SAMFUNDET_ADD_ROLE = 'samfundet.add_role' +SAMFUNDET_CHANGE_ROLE = 'samfundet.change_role' +SAMFUNDET_DELETE_ROLE = 'samfundet.delete_role' +SAMFUNDET_VIEW_ROLE = 'samfundet.view_role' + SAMFUNDET_ADD_SAKSDOKUMENT = 'samfundet.add_saksdokument' SAMFUNDET_CHANGE_SAKSDOKUMENT = 'samfundet.change_saksdokument' SAMFUNDET_DELETE_SAKSDOKUMENT = 'samfundet.delete_saksdokument' @@ -247,6 +294,11 @@ SAMFUNDET_IMPERSONATE = 'samfundet.impersonate' SAMFUNDET_VIEW_USER = 'samfundet.view_user' +SAMFUNDET_ADD_USERFEEDBACKMODEL = 'samfundet.add_userfeedbackmodel' +SAMFUNDET_CHANGE_USERFEEDBACKMODEL = 'samfundet.change_userfeedbackmodel' +SAMFUNDET_DELETE_USERFEEDBACKMODEL = 'samfundet.delete_userfeedbackmodel' +SAMFUNDET_VIEW_USERFEEDBACKMODEL = 'samfundet.view_userfeedbackmodel' + SAMFUNDET_ADD_USERPREFERENCE = 'samfundet.add_userpreference' SAMFUNDET_CHANGE_USERPREFERENCE = 'samfundet.change_userpreference' SAMFUNDET_DELETE_USERPREFERENCE = 'samfundet.delete_userpreference' diff --git a/backend/samfundet/admin.py b/backend/samfundet/admin.py index 00a75e421..2add5817c 100644 --- a/backend/samfundet/admin.py +++ b/backend/samfundet/admin.py @@ -21,6 +21,8 @@ CustomGuardedModelAdmin, ) +from samfundet.models.role import Role, UserOrgRole, UserGangRole, UserGangSectionRole + from .models.event import Event, EventGroup, EventRegistration from .models.general import ( Tag, @@ -132,6 +134,7 @@ def group_memberships(self, obj: User) -> int: 'is_staff', 'is_superuser', 'groups', + 'roles', 'user_permissions', ), }, @@ -161,6 +164,27 @@ def members(self, obj: Group) -> int: return n +@admin.register(Role) +class RoleAdmin(admin.ModelAdmin): + list_display = ['name', 'content_type'] + filter_horizontal = ['permissions'] + + +@admin.register(UserOrgRole) +class UserOrgRole(admin.ModelAdmin): + list_display = ('user', 'role', 'obj') + + +@admin.register(UserGangRole) +class UserGangRole(admin.ModelAdmin): + list_display = ('user', 'role', 'obj') + + +@admin.register(UserGangSectionRole) +class UserGangSectionRole(admin.ModelAdmin): + list_display = ('user', 'role', 'obj') + + @admin.register(Permission) class PermissionAdmin(CustomGuardedModelAdmin): # ordering = [] diff --git a/backend/samfundet/migrations/0002_role_user_roles_usergangrole_usergangsectionrole_and_more.py b/backend/samfundet/migrations/0002_role_user_roles_usergangrole_usergangsectionrole_and_more.py new file mode 100644 index 000000000..fb7108efb --- /dev/null +++ b/backend/samfundet/migrations/0002_role_user_roles_usergangrole_usergangsectionrole_and_more.py @@ -0,0 +1,86 @@ +# Generated by Django 5.0.2 on 2024-07-02 21:08 + +import django.db.models.deletion +import root.utils.mixins +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'), + ('samfundet', '0001_initial'), + ] + + 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=255)), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('permissions', models.ManyToManyField(to='auth.permission')), + ], + ), + migrations.AddField( + model_name='user', + name='roles', + field=models.ManyToManyField(blank=True, related_name='users', to='samfundet.role'), + ), + migrations.CreateModel( + name='UserGangRole', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.PositiveIntegerField(blank=True, default=0, editable=False, null=True)), + ('created_at', models.DateTimeField(blank=True, editable=False, null=True)), + ('updated_at', models.DateTimeField(blank=True, editable=False, null=True)), + ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('obj', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='samfundet.gang')), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='samfundet.role')), + ('updated_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(root.utils.mixins.RoleMixin, models.Model), + ), + migrations.CreateModel( + name='UserGangSectionRole', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.PositiveIntegerField(blank=True, default=0, editable=False, null=True)), + ('created_at', models.DateTimeField(blank=True, editable=False, null=True)), + ('updated_at', models.DateTimeField(blank=True, editable=False, null=True)), + ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('obj', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='samfundet.gangsection')), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='samfundet.role')), + ('updated_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(root.utils.mixins.RoleMixin, models.Model), + ), + migrations.CreateModel( + name='UserOrgRole', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.PositiveIntegerField(blank=True, default=0, editable=False, null=True)), + ('created_at', models.DateTimeField(blank=True, editable=False, null=True)), + ('updated_at', models.DateTimeField(blank=True, editable=False, null=True)), + ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('obj', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='samfundet.organization')), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='samfundet.role')), + ('updated_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(root.utils.mixins.RoleMixin, models.Model), + ), + ] diff --git a/backend/samfundet/models/__init__.py b/backend/samfundet/models/__init__.py index 9b8a85b70..8e427388b 100644 --- a/backend/samfundet/models/__init__.py +++ b/backend/samfundet/models/__init__.py @@ -12,12 +12,16 @@ User, Image, Profile, + GangSection, + Organization, UserPreference, ) __all__ = [ 'User', 'Gang', + 'GangSection', + 'Organization', 'Event', 'Image', 'Profile', diff --git a/backend/samfundet/models/general.py b/backend/samfundet/models/general.py index 28c0cf9ce..5a642432f 100644 --- a/backend/samfundet/models/general.py +++ b/backend/samfundet/models/general.py @@ -22,6 +22,7 @@ from root.utils import permissions from root.utils.mixins import CustomBaseModel, FullCleanSaveMixin +from samfundet.models.role import Role from samfundet.models.model_choices import ReservationOccasion, UserPreferenceTheme, SaksdokumentCategory from .utils.fields import LowerCaseField, PhoneNumberField @@ -132,6 +133,8 @@ class User(AbstractUser): on_delete=models.PROTECT, ) + roles = models.ManyToManyField(Role, blank=True, related_name='users') + class Meta: permissions = [ ('debug', 'Can view debug mode'), @@ -329,6 +332,16 @@ class Meta: verbose_name = 'Gang' verbose_name_plural = 'Gangs' + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + if return_id: + return self.organization_id + return self.organization + + def resolve_gang(self, *, return_id: bool = False) -> Gang | int: + if return_id: + return self.id + return self + def __str__(self) -> str: return f'{self.gang_type} - {self.name_nb}' diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index 0172af548..0c661dd61 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -32,6 +32,9 @@ class Recruitment(CustomBaseModel): max_applications = models.PositiveIntegerField(null=True, blank=True, verbose_name='Max applications per applicant') + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.organization.resolve_org(return_id=return_id) + def is_active(self) -> bool: return self.visible_from < timezone.now() < self.actual_application_deadline @@ -128,6 +131,14 @@ class RecruitmentPosition(CustomBaseModel): # TODO: Implement interviewer functionality interviewers = models.ManyToManyField(to=User, help_text='Interviewers for the position', blank=True, related_name='interviewers') + def resolve_gang(self, *, return_id: bool = False) -> Gang | int: + if return_id: + return self.gang_id + return self.gang + + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.gang.resolve_org(return_id=return_id) + def __str__(self) -> str: return f'Position: {self.name_en} in {self.recruitment}' @@ -351,6 +362,9 @@ class RecruitmentInterviewAvailability(CustomBaseModel): end_time = models.TimeField(help_text='Last possible time of day for interviews', default='23:00:00', null=False, blank=False) timeslot_interval = models.PositiveSmallIntegerField(help_text='The time interval (in minutes) between each timeslot', default=30) + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment.resolve_org(return_id=return_id) + class OccupiedTimeslot(FullCleanSaveMixin): user = models.ForeignKey( diff --git a/backend/samfundet/models/role.py b/backend/samfundet/models/role.py new file mode 100644 index 000000000..14414711a --- /dev/null +++ b/backend/samfundet/models/role.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from django.db import models +from django.conf import settings +from django.contrib.contenttypes.models import ContentType + +from root.utils.mixins import CustomBaseModel + + +class Role(models.Model): + name = models.CharField(max_length=255) + permissions = models.ManyToManyField('auth.Permission') + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + + def __str__(self): + return self.name + + +class UserRoleBase(CustomBaseModel): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + role = models.ForeignKey(Role, on_delete=models.CASCADE) + + class Meta: + abstract = True + + +class UserOrgRole(UserRoleBase): + obj = models.ForeignKey('samfundet.Organization', on_delete=models.CASCADE) + + +class UserGangRole(UserRoleBase): + obj = models.ForeignKey('samfundet.Gang', on_delete=models.CASCADE) + + +class UserGangSectionRole(UserRoleBase): + obj = models.ForeignKey('samfundet.GangSection', on_delete=models.CASCADE) diff --git a/backend/samfundet/roleauth/__init__.py b/backend/samfundet/roleauth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/samfundet/roleauth/backend.py b/backend/samfundet/roleauth/backend.py new file mode 100644 index 000000000..3820db671 --- /dev/null +++ b/backend/samfundet/roleauth/backend.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from typing import Any + +from django.contrib.auth.backends import BaseBackend + +from samfundet.utils import get_perm +from samfundet.models import User +from samfundet.models.role import UserOrgRole, UserGangRole, UserGangSectionRole + + +class RoleAuthBackend(BaseBackend): + def has_perm(self, user_obj: User, perm: str, obj: Any = None) -> bool: # noqa: C901 + if not user_obj.is_active or obj is None: + return False + + if user_obj.is_superuser: + return True + + permission = get_perm(perm=perm, model=obj) + + if hasattr(obj, 'resolve_org'): + org_id = obj.resolve_org(return_id=True) + if org_id is not None: + return UserOrgRole.objects.filter(user=user_obj, obj__id=org_id, role__permissions=permission).exists() + + if hasattr(obj, 'resolve_gang'): + gang_id = obj.resolve_gang(return_id=True) + if gang_id is not None: + return UserGangRole.objects.filter(user=user_obj, obj__id=gang_id, role__permissions=permission).exists() + + if hasattr(obj, 'resolve_section'): + section_id = obj.resolve_section(return_id=True) + if section_id is not None: + return UserGangSectionRole.objects.filter(user=user_obj, obj__id=section_id, role__permissions=permission).exists() + + return False diff --git a/backend/samfundet/utils.py b/backend/samfundet/utils.py index 0d4fda08b..dd1159e1d 100644 --- a/backend/samfundet/utils.py +++ b/backend/samfundet/utils.py @@ -3,10 +3,12 @@ import datetime from django.http import QueryDict -from django.db.models import Q +from django.db.models import Q, Model from django.utils.timezone import make_aware from django.core.exceptions import ValidationError from django.db.models.query import QuerySet +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType from .models import User from .models.event import Event @@ -90,3 +92,10 @@ def get_occupied_timeslots_from_request( occupied_timeslots.append(OccupiedTimeslot(user=user, recruitment=recruitment, start_dt=start_date, end_dt=end_date)) return occupied_timeslots + + +def get_perm(*, perm: str, model: type[Model]) -> Permission: + codename = perm.split('.')[1] + content_type = ContentType.objects.get_for_model(model=model) + permission = Permission.objects.get(codename=codename, content_type=content_type) + return permission From 294cc28a9320eca61fee27c2847c9db9854aded2 Mon Sep 17 00:00:00 2001 From: robines Date: Tue, 2 Jul 2024 23:17:17 +0200 Subject: [PATCH 02/32] Fix user role admin names --- backend/samfundet/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/samfundet/admin.py b/backend/samfundet/admin.py index 2add5817c..82051c218 100644 --- a/backend/samfundet/admin.py +++ b/backend/samfundet/admin.py @@ -171,17 +171,17 @@ class RoleAdmin(admin.ModelAdmin): @admin.register(UserOrgRole) -class UserOrgRole(admin.ModelAdmin): +class UserOrgRoleAdmin(admin.ModelAdmin): list_display = ('user', 'role', 'obj') @admin.register(UserGangRole) -class UserGangRole(admin.ModelAdmin): +class UserGangRoleAdmin(admin.ModelAdmin): list_display = ('user', 'role', 'obj') @admin.register(UserGangSectionRole) -class UserGangSectionRole(admin.ModelAdmin): +class UserGangSectionRoleAdmin(admin.ModelAdmin): list_display = ('user', 'role', 'obj') From 8e808309b83ffddb10936d474cbd9d6c5115673f Mon Sep 17 00:00:00 2001 From: robines Date: Tue, 2 Jul 2024 23:17:57 +0200 Subject: [PATCH 03/32] Type annotations --- backend/samfundet/models/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/samfundet/models/role.py b/backend/samfundet/models/role.py index 14414711a..a4a49b2f8 100644 --- a/backend/samfundet/models/role.py +++ b/backend/samfundet/models/role.py @@ -12,7 +12,7 @@ class Role(models.Model): permissions = models.ManyToManyField('auth.Permission') content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - def __str__(self): + def __str__(self) -> str: return self.name From 6c55a790e27d1de5940df6d6b94797e13ef126b6 Mon Sep 17 00:00:00 2001 From: robines Date: Wed, 3 Jul 2024 00:32:40 +0200 Subject: [PATCH 04/32] Ruff format --- backend/root/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/root/settings/base.py b/backend/root/settings/base.py index 1e658107b..09636cb80 100644 --- a/backend/root/settings/base.py +++ b/backend/root/settings/base.py @@ -87,7 +87,7 @@ 'corsheaders', 'root', # Register to enable management.commands. 'samfundet', - 'samfundet.roleauth' + 'samfundet.roleauth', ] MIDDLEWARE = [ From b33fd058f28a356751f1c8b26e15c04962c5fd34 Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 5 Jul 2024 01:57:28 +0200 Subject: [PATCH 05/32] Remove content_type from Role --- backend/samfundet/admin.py | 2 +- ...le_user_roles_usergangrole_usergangsectionrole_and_more.py | 4 +--- backend/samfundet/models/role.py | 2 -- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/backend/samfundet/admin.py b/backend/samfundet/admin.py index 82051c218..f844d71a3 100644 --- a/backend/samfundet/admin.py +++ b/backend/samfundet/admin.py @@ -166,7 +166,7 @@ def members(self, obj: Group) -> int: @admin.register(Role) class RoleAdmin(admin.ModelAdmin): - list_display = ['name', 'content_type'] + list_display = ('name',) filter_horizontal = ['permissions'] diff --git a/backend/samfundet/migrations/0002_role_user_roles_usergangrole_usergangsectionrole_and_more.py b/backend/samfundet/migrations/0002_role_user_roles_usergangrole_usergangsectionrole_and_more.py index fb7108efb..5eb892b01 100644 --- a/backend/samfundet/migrations/0002_role_user_roles_usergangrole_usergangsectionrole_and_more.py +++ b/backend/samfundet/migrations/0002_role_user_roles_usergangrole_usergangsectionrole_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.2 on 2024-07-02 21:08 +# Generated by Django 5.0.6 on 2024-07-04 13:42 import django.db.models.deletion import root.utils.mixins @@ -10,7 +10,6 @@ class Migration(migrations.Migration): dependencies = [ ('auth', '0012_alter_user_first_name_max_length'), - ('contenttypes', '0002_remove_content_type_name'), ('samfundet', '0001_initial'), ] @@ -20,7 +19,6 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=255)), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), ('permissions', models.ManyToManyField(to='auth.permission')), ], ), diff --git a/backend/samfundet/models/role.py b/backend/samfundet/models/role.py index a4a49b2f8..826ffab14 100644 --- a/backend/samfundet/models/role.py +++ b/backend/samfundet/models/role.py @@ -2,7 +2,6 @@ from django.db import models from django.conf import settings -from django.contrib.contenttypes.models import ContentType from root.utils.mixins import CustomBaseModel @@ -10,7 +9,6 @@ class Role(models.Model): name = models.CharField(max_length=255) permissions = models.ManyToManyField('auth.Permission') - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) def __str__(self) -> str: return self.name From da4dc4511fe7b1d151139597f6dfb38793914edc Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 5 Jul 2024 01:59:52 +0200 Subject: [PATCH 06/32] Add remaining resolvers for recruitment, and org/gang/section --- backend/samfundet/models/general.py | 20 +++++++++++ backend/samfundet/models/recruitment.py | 45 ++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/backend/samfundet/models/general.py b/backend/samfundet/models/general.py index 5a642432f..89702d946 100644 --- a/backend/samfundet/models/general.py +++ b/backend/samfundet/models/general.py @@ -282,6 +282,11 @@ class Meta: verbose_name = 'Organization' verbose_name_plural = 'Organizations' + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + if return_id: + return self.id + return self + def __str__(self) -> str: return self.name @@ -334,6 +339,7 @@ class Meta: def resolve_org(self, *, return_id: bool = False) -> Organization | int: if return_id: + # noinspection PyTypeChecker return self.organization_id return self.organization @@ -352,6 +358,20 @@ class GangSection(CustomBaseModel): logo = models.ForeignKey(Image, on_delete=models.PROTECT, blank=True, null=True, verbose_name='Logo') gang = models.ForeignKey(Gang, blank=False, null=False, related_name='gang', on_delete=models.PROTECT, verbose_name='Gjeng') + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.gang.resolve_org(return_id=return_id) + + def resolve_gang(self, *, return_id: bool = False) -> Gang | int: + if return_id: + # noinspection PyTypeChecker + return self.gang_id + return self.gang + + def resolve_section(self, *, return_id: bool = False) -> GangSection | int: + if return_id: + return self.id + return self + def __str__(self) -> str: return f'{self.gang.name_nb} - {self.name_nb}' diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index 0c661dd61..8133f39bd 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -33,7 +33,10 @@ class Recruitment(CustomBaseModel): max_applications = models.PositiveIntegerField(null=True, blank=True, verbose_name='Max applications per applicant') def resolve_org(self, *, return_id: bool = False) -> Organization | int: - return self.organization.resolve_org(return_id=return_id) + if return_id: + # noinspection PyTypeChecker + return self.organization_id + return self.organization def is_active(self) -> bool: return self.visible_from < timezone.now() < self.actual_application_deadline @@ -133,6 +136,7 @@ class RecruitmentPosition(CustomBaseModel): def resolve_gang(self, *, return_id: bool = False) -> Gang | int: if return_id: + # noinspection PyTypeChecker return self.gang_id return self.gang @@ -166,6 +170,9 @@ class RecruitmentSeperatePosition(CustomBaseModel): blank=True, ) + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment.resolve_org(return_id=return_id) + def __str__(self) -> str: return f'Seperate recruitment: {self.name_nb} ({self.recruitment})' @@ -181,6 +188,15 @@ class InterviewRoom(CustomBaseModel): def __str__(self) -> str: return self.name + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment.resolve_org(return_id=return_id) + + def resolve_gang(self, *, return_id: bool = False) -> Gang | int: + if return_id: + # noinspection PyTypeChecker + return self.gang_id + return self.gang + def clean(self) -> None: super().clean() @@ -205,6 +221,12 @@ class Interview(CustomBaseModel): interviewers = models.ManyToManyField(to='samfundet.User', help_text='Interviewers for this interview', blank=True, related_name='interviews') notes = models.TextField(help_text='Notes for the interview', null=True, blank=True) + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.room.resolve_org(return_id=return_id) + + def resolve_gang(self, *, return_id: bool = False) -> Gang | int: + return self.room.resolve_gang(return_id=return_id) + class RecruitmentApplication(CustomBaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -236,6 +258,12 @@ class RecruitmentApplication(CustomBaseModel): choices=RecruitmentApplicantStates.choices, default=RecruitmentApplicantStates.NOT_SET, help_text='The state of the applicant for the recruiter' ) + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment.resolve_org(return_id=return_id) + + def resolve_gang(self, *, return_id: bool = False) -> Gang | int: + return self.recruitment_position.resolve_gang(return_id=return_id) + def organize_priorities(self) -> None: """Organizes priorites from 1 to n, so that it is sequential with no gaps""" applications_for_user = RecruitmentApplication.objects.filter(recruitment=self.recruitment, user=self.user).order_by('applicant_priority') @@ -385,6 +413,9 @@ class OccupiedTimeslot(FullCleanSaveMixin): class Meta: constraints = [models.UniqueConstraint(fields=['user', 'recruitment', 'start_dt', 'end_dt'], name='occupied_UNIQ')] + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment.resolve_org(return_id=return_id) + class RecruitmentStatistics(FullCleanSaveMixin): recruitment = models.OneToOneField(Recruitment, on_delete=models.CASCADE, blank=True, null=True, related_name='statistics') @@ -392,6 +423,9 @@ class RecruitmentStatistics(FullCleanSaveMixin): total_applicants = models.PositiveIntegerField(null=True, blank=True, verbose_name='Total applicants') total_applications = models.PositiveIntegerField(null=True, blank=True, verbose_name='Total applications') + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment.resolve_org(return_id=return_id) + def save(self, *args: tuple, **kwargs: dict) -> None: self.total_applications = self.recruitment.applications.count() self.total_applicants = self.recruitment.applications.values('user').distinct().count() @@ -432,6 +466,9 @@ class RecruitmentTimeStat(models.Model): def __str__(self) -> str: return f'{self.recruitment_stats} {self.hour} {self.count}' + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment_stats.resolve_org(return_id=return_id) + def save(self, *args: tuple, **kwargs: dict) -> None: count = 0 for application in self.recruitment_stats.recruitment.applications.all(): @@ -449,6 +486,9 @@ class RecruitmentDateStat(models.Model): def __str__(self) -> str: return f'{self.recruitment_stats} {self.date} {self.count}' + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment_stats.resolve_org(return_id=return_id) + def save(self, *args: tuple, **kwargs: dict) -> None: count = 0 for application in self.recruitment_stats.recruitment.applications.all(): @@ -467,6 +507,9 @@ class RecruitmentCampusStat(models.Model): def __str__(self) -> str: return f'{self.recruitment_stats} {self.campus} {self.count}' + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment_stats.resolve_org(return_id=return_id) + def save(self, *args: tuple, **kwargs: dict) -> None: self.count = User.objects.filter( id__in=self.recruitment_stats.recruitment.applications.values_list('user', flat=True).distinct(), campus=self.campus From a6537dc5373077b118b7bee2e2bb8bba24bb4146 Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 5 Jul 2024 03:40:21 +0200 Subject: [PATCH 07/32] Add working examples for Interview and InterviewRoom views --- backend/samfundet/views.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index dcfde85b4..95d7d003a 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -34,6 +34,7 @@ GITHUB_SIGNATURE_HEADER, REQUESTED_IMPERSONATE_USER, ) +from root.utils.permissions import SAMFUNDET_VIEW_INTERVIEW, SAMFUNDET_VIEW_INTERVIEWROOM from .utils import event_query, generate_timeslots, get_occupied_timeslots_from_request from .homepage import homepage @@ -973,12 +974,21 @@ class InterviewRoomView(ModelViewSet): serializer_class = InterviewRoomSerializer queryset = InterviewRoom.objects.all() - def list(self, request: Request) -> Response: + # noinspection PyMethodOverriding + def retrieve(self, request: Request, pk: int) -> Response: + room = get_object_or_404(InterviewRoom, pk=pk) + if not request.user.has_perm(SAMFUNDET_VIEW_INTERVIEWROOM, room): + raise PermissionDenied + return super().retrieve(request=request, pk=pk) + + def list(self, request: Request, *args, **kwargs) -> Response: recruitment = request.query_params.get('recruitment') if not recruitment: return Response({'error': 'A recruitment parameter is required'}, status=status.HTTP_400_BAD_REQUEST) - filtered_rooms = InterviewRoom.objects.filter(recruitment__id=recruitment) + filtered_rooms = [ + room for room in InterviewRoom.objects.filter(recruitment__id=recruitment) if request.user.has_perm(SAMFUNDET_VIEW_INTERVIEWROOM, room) + ] serialized_rooms = self.get_serializer(filtered_rooms, many=True) return Response(serialized_rooms.data) @@ -1003,6 +1013,18 @@ class InterviewView(ModelViewSet): serializer_class = InterviewSerializer queryset = Interview.objects.all() + # noinspection PyMethodOverriding + def retrieve(self, request: Request, pk: int) -> Response: + interview = get_object_or_404(Interview, pk=pk) + if not request.user.has_perm(SAMFUNDET_VIEW_INTERVIEW, interview): + raise PermissionDenied + return super().retrieve(request=request, pk=pk) + + def list(self, request: Request, **kwargs) -> Response: + interviews = [interview for interview in self.get_queryset() if request.user.has_perm(SAMFUNDET_VIEW_INTERVIEW, interview)] + serializer = self.get_serializer(interviews, many=True) + return Response(serializer.data) + class RecruitmentInterviewAvailabilityView(ListCreateAPIView): model = RecruitmentInterviewAvailability @@ -1011,12 +1033,12 @@ class RecruitmentInterviewAvailabilityView(ListCreateAPIView): class RecruitmentAvailabilityView(APIView): + permission_classes = [IsAuthenticated] model = RecruitmentInterviewAvailability serializer_class = RecruitmentInterviewAvailabilitySerializer def get(self, request: Request, **kwargs: int) -> Response: - recruitment = kwargs.get('id') - availability = get_object_or_404(RecruitmentInterviewAvailability, recruitment__id=recruitment) + availability = get_object_or_404(RecruitmentInterviewAvailability, recruitment__id=kwargs.get('id')) start_time = availability.start_time end_time = availability.end_time From d60f7badc342d92e7fef463445c596a8254a365e Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 5 Jul 2024 03:40:44 +0200 Subject: [PATCH 08/32] ruff method order --- backend/samfundet/models/recruitment.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index 8133f39bd..638c6f492 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -423,9 +423,6 @@ class RecruitmentStatistics(FullCleanSaveMixin): total_applicants = models.PositiveIntegerField(null=True, blank=True, verbose_name='Total applicants') total_applications = models.PositiveIntegerField(null=True, blank=True, verbose_name='Total applications') - def resolve_org(self, *, return_id: bool = False) -> Organization | int: - return self.recruitment.resolve_org(return_id=return_id) - def save(self, *args: tuple, **kwargs: dict) -> None: self.total_applications = self.recruitment.applications.count() self.total_applicants = self.recruitment.applications.values('user').distinct().count() @@ -437,6 +434,9 @@ def save(self, *args: tuple, **kwargs: dict) -> None: def __str__(self) -> str: return f'{self.recruitment} stats' + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment.resolve_org(return_id=return_id) + def generate_time_stats(self) -> None: for h in range(0, 24): time_stat, created = RecruitmentTimeStat.objects.get_or_create(recruitment_stats=self, hour=h) @@ -466,9 +466,6 @@ class RecruitmentTimeStat(models.Model): def __str__(self) -> str: return f'{self.recruitment_stats} {self.hour} {self.count}' - def resolve_org(self, *, return_id: bool = False) -> Organization | int: - return self.recruitment_stats.resolve_org(return_id=return_id) - def save(self, *args: tuple, **kwargs: dict) -> None: count = 0 for application in self.recruitment_stats.recruitment.applications.all(): @@ -477,6 +474,9 @@ def save(self, *args: tuple, **kwargs: dict) -> None: self.count = count super().save(*args, **kwargs) + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment_stats.resolve_org(return_id=return_id) + class RecruitmentDateStat(models.Model): recruitment_stats = models.ForeignKey(RecruitmentStatistics, on_delete=models.CASCADE, blank=False, null=False, related_name='date_stats') @@ -486,9 +486,6 @@ class RecruitmentDateStat(models.Model): def __str__(self) -> str: return f'{self.recruitment_stats} {self.date} {self.count}' - def resolve_org(self, *, return_id: bool = False) -> Organization | int: - return self.recruitment_stats.resolve_org(return_id=return_id) - def save(self, *args: tuple, **kwargs: dict) -> None: count = 0 for application in self.recruitment_stats.recruitment.applications.all(): @@ -497,6 +494,9 @@ def save(self, *args: tuple, **kwargs: dict) -> None: self.count = count super().save(*args, **kwargs) + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment_stats.resolve_org(return_id=return_id) + class RecruitmentCampusStat(models.Model): recruitment_stats = models.ForeignKey(RecruitmentStatistics, on_delete=models.CASCADE, blank=False, null=False, related_name='campus_stats') @@ -507,11 +507,11 @@ class RecruitmentCampusStat(models.Model): def __str__(self) -> str: return f'{self.recruitment_stats} {self.campus} {self.count}' - def resolve_org(self, *, return_id: bool = False) -> Organization | int: - return self.recruitment_stats.resolve_org(return_id=return_id) - def save(self, *args: tuple, **kwargs: dict) -> None: self.count = User.objects.filter( id__in=self.recruitment_stats.recruitment.applications.values_list('user', flat=True).distinct(), campus=self.campus ).count() super().save(*args, **kwargs) + + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.recruitment_stats.resolve_org(return_id=return_id) From c6889feb0ef4a875f6c8430a7c6e59da3e308582 Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 5 Jul 2024 03:46:11 +0200 Subject: [PATCH 09/32] mypy --- backend/samfundet/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index 95d7d003a..df4a60afe 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -981,7 +981,8 @@ def retrieve(self, request: Request, pk: int) -> Response: raise PermissionDenied return super().retrieve(request=request, pk=pk) - def list(self, request: Request, *args, **kwargs) -> Response: + # noinspection PyMethodOverriding + def list(self, request: Request) -> Response: recruitment = request.query_params.get('recruitment') if not recruitment: return Response({'error': 'A recruitment parameter is required'}, status=status.HTTP_400_BAD_REQUEST) @@ -1020,7 +1021,8 @@ def retrieve(self, request: Request, pk: int) -> Response: raise PermissionDenied return super().retrieve(request=request, pk=pk) - def list(self, request: Request, **kwargs) -> Response: + # noinspection PyMethodOverriding + def list(self, request: Request) -> Response: interviews = [interview for interview in self.get_queryset() if request.user.has_perm(SAMFUNDET_VIEW_INTERVIEW, interview)] serializer = self.get_serializer(interviews, many=True) return Response(serializer.data) From 76a696899b27fd8b4f6ad8ec433e28f29484eb86 Mon Sep 17 00:00:00 2001 From: robines Date: Sun, 7 Jul 2024 21:18:17 +0200 Subject: [PATCH 10/32] Remove roles from user model --- ...2_role_usergangrole_usergangsectionrole_userorgrole.py} | 7 +------ backend/samfundet/models/general.py | 2 -- 2 files changed, 1 insertion(+), 8 deletions(-) rename backend/samfundet/migrations/{0002_role_user_roles_usergangrole_usergangsectionrole_and_more.py => 0002_role_usergangrole_usergangsectionrole_userorgrole.py} (95%) diff --git a/backend/samfundet/migrations/0002_role_user_roles_usergangrole_usergangsectionrole_and_more.py b/backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py similarity index 95% rename from backend/samfundet/migrations/0002_role_user_roles_usergangrole_usergangsectionrole_and_more.py rename to backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py index 5eb892b01..2f2a3d956 100644 --- a/backend/samfundet/migrations/0002_role_user_roles_usergangrole_usergangsectionrole_and_more.py +++ b/backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.6 on 2024-07-04 13:42 +# Generated by Django 5.0.6 on 2024-07-07 19:18 import django.db.models.deletion import root.utils.mixins @@ -22,11 +22,6 @@ class Migration(migrations.Migration): ('permissions', models.ManyToManyField(to='auth.permission')), ], ), - migrations.AddField( - model_name='user', - name='roles', - field=models.ManyToManyField(blank=True, related_name='users', to='samfundet.role'), - ), migrations.CreateModel( name='UserGangRole', fields=[ diff --git a/backend/samfundet/models/general.py b/backend/samfundet/models/general.py index 89702d946..fda51aaac 100644 --- a/backend/samfundet/models/general.py +++ b/backend/samfundet/models/general.py @@ -133,8 +133,6 @@ class User(AbstractUser): on_delete=models.PROTECT, ) - roles = models.ManyToManyField(Role, blank=True, related_name='users') - class Meta: permissions = [ ('debug', 'Can view debug mode'), From 95c915fcade4994ff2165133dcb30d3c28abc5a2 Mon Sep 17 00:00:00 2001 From: robines Date: Sat, 3 Aug 2024 14:47:29 +0200 Subject: [PATCH 11/32] Move to samfundet app --- backend/root/settings/base.py | 3 +-- backend/root/utils/permissions.py | 22 +++++++++++------ backend/root/utils/routes.py | 26 +++++++++++++++++++-- backend/samfundet/admin.py | 1 - backend/samfundet/{roleauth => }/backend.py | 0 backend/samfundet/roleauth/__init__.py | 0 frontend/src/routes/backend.ts | 24 +++++++++++++++++++ 7 files changed, 64 insertions(+), 12 deletions(-) rename backend/samfundet/{roleauth => }/backend.py (100%) delete mode 100644 backend/samfundet/roleauth/__init__.py diff --git a/backend/root/settings/base.py b/backend/root/settings/base.py index 09636cb80..f9c94866d 100644 --- a/backend/root/settings/base.py +++ b/backend/root/settings/base.py @@ -87,7 +87,6 @@ 'corsheaders', 'root', # Register to enable management.commands. 'samfundet', - 'samfundet.roleauth', ] MIDDLEWARE = [ @@ -130,7 +129,7 @@ AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend', # default - 'samfundet.roleauth.backend.RoleAuthBackend', + 'samfundet.backend.RoleAuthBackend', ] # Password validation diff --git a/backend/root/utils/permissions.py b/backend/root/utils/permissions.py index ad1cef0d2..5d43da3bf 100644 --- a/backend/root/utils/permissions.py +++ b/backend/root/utils/permissions.py @@ -47,13 +47,6 @@ GUARDIAN_VIEW_USEROBJECTPERMISSION = 'guardian.view_userobjectpermission' ### End: guardian ### -### roleauth ### -ROLEAUTH_ADD_ROLE = 'roleauth.add_role' -ROLEAUTH_CHANGE_ROLE = 'roleauth.change_role' -ROLEAUTH_DELETE_ROLE = 'roleauth.delete_role' -ROLEAUTH_VIEW_ROLE = 'roleauth.view_role' -### End: roleauth ### - ### samfundet ### SAMFUNDET_ADD_BILLIGEVENT = 'samfundet.add_billigevent' SAMFUNDET_CHANGE_BILLIGEVENT = 'samfundet.change_billigevent' @@ -297,6 +290,21 @@ SAMFUNDET_DELETE_USERFEEDBACKMODEL = 'samfundet.delete_userfeedbackmodel' SAMFUNDET_VIEW_USERFEEDBACKMODEL = 'samfundet.view_userfeedbackmodel' +SAMFUNDET_ADD_USERGANGROLE = 'samfundet.add_usergangrole' +SAMFUNDET_CHANGE_USERGANGROLE = 'samfundet.change_usergangrole' +SAMFUNDET_DELETE_USERGANGROLE = 'samfundet.delete_usergangrole' +SAMFUNDET_VIEW_USERGANGROLE = 'samfundet.view_usergangrole' + +SAMFUNDET_ADD_USERGANGSECTIONROLE = 'samfundet.add_usergangsectionrole' +SAMFUNDET_CHANGE_USERGANGSECTIONROLE = 'samfundet.change_usergangsectionrole' +SAMFUNDET_DELETE_USERGANGSECTIONROLE = 'samfundet.delete_usergangsectionrole' +SAMFUNDET_VIEW_USERGANGSECTIONROLE = 'samfundet.view_usergangsectionrole' + +SAMFUNDET_ADD_USERORGROLE = 'samfundet.add_userorgrole' +SAMFUNDET_CHANGE_USERORGROLE = 'samfundet.change_userorgrole' +SAMFUNDET_DELETE_USERORGROLE = 'samfundet.delete_userorgrole' +SAMFUNDET_VIEW_USERORGROLE = 'samfundet.view_userorgrole' + SAMFUNDET_ADD_USERPREFERENCE = 'samfundet.add_userpreference' SAMFUNDET_CHANGE_USERPREFERENCE = 'samfundet.change_userpreference' SAMFUNDET_DELETE_USERPREFERENCE = 'samfundet.delete_userpreference' diff --git a/backend/root/utils/routes.py b/backend/root/utils/routes.py index b313885c8..5a0f3e5d9 100644 --- a/backend/root/utils/routes.py +++ b/backend/root/utils/routes.py @@ -43,6 +43,30 @@ admin__auth_group_delete = 'admin:auth_group_delete' admin__auth_group_change = 'admin:auth_group_change' adminauthgroup__objectId = '' +admin__samfundet_role_changelist = 'admin:samfundet_role_changelist' +admin__samfundet_role_add = 'admin:samfundet_role_add' +admin__samfundet_role_history = 'admin:samfundet_role_history' +admin__samfundet_role_delete = 'admin:samfundet_role_delete' +admin__samfundet_role_change = 'admin:samfundet_role_change' +adminsamfundetrole__objectId = '' +admin__samfundet_userorgrole_changelist = 'admin:samfundet_userorgrole_changelist' +admin__samfundet_userorgrole_add = 'admin:samfundet_userorgrole_add' +admin__samfundet_userorgrole_history = 'admin:samfundet_userorgrole_history' +admin__samfundet_userorgrole_delete = 'admin:samfundet_userorgrole_delete' +admin__samfundet_userorgrole_change = 'admin:samfundet_userorgrole_change' +adminsamfundetuserorgrole__objectId = '' +admin__samfundet_usergangrole_changelist = 'admin:samfundet_usergangrole_changelist' +admin__samfundet_usergangrole_add = 'admin:samfundet_usergangrole_add' +admin__samfundet_usergangrole_history = 'admin:samfundet_usergangrole_history' +admin__samfundet_usergangrole_delete = 'admin:samfundet_usergangrole_delete' +admin__samfundet_usergangrole_change = 'admin:samfundet_usergangrole_change' +adminsamfundetusergangrole__objectId = '' +admin__samfundet_usergangsectionrole_changelist = 'admin:samfundet_usergangsectionrole_changelist' +admin__samfundet_usergangsectionrole_add = 'admin:samfundet_usergangsectionrole_add' +admin__samfundet_usergangsectionrole_history = 'admin:samfundet_usergangsectionrole_history' +admin__samfundet_usergangsectionrole_delete = 'admin:samfundet_usergangsectionrole_delete' +admin__samfundet_usergangsectionrole_change = 'admin:samfundet_usergangsectionrole_change' +adminsamfundetusergangsectionrole__objectId = '' admin__auth_permission_permissions = 'admin:auth_permission_permissions' admin__auth_permission_permissions_manage_user = 'admin:auth_permission_permissions_manage_user' admin__auth_permission_permissions_manage_group = 'admin:auth_permission_permissions_manage_group' @@ -547,8 +571,6 @@ samfundet__recruitment_positions = 'samfundet:recruitment_positions' samfundet__recruitment_positions_gang = 'samfundet:recruitment_positions_gang' samfundet__recruitment_set_interview = 'samfundet:recruitment_set_interview' -samfundet__active_recruitment_positions = 'samfundet:active_recruitment_positions' -samfundet__applicants_without_interviews = 'samfundet:applicants_without_interviews' samfundet__recruitment_application_states_choices = 'samfundet:recruitment_application_states_choices' samfundet__recruitment_application_update_state_gang = 'samfundet:recruitment_application_update_state_gang' samfundet__recruitment_application_update_state_position = 'samfundet:recruitment_application_update_state_position' diff --git a/backend/samfundet/admin.py b/backend/samfundet/admin.py index f844d71a3..d96a0827d 100644 --- a/backend/samfundet/admin.py +++ b/backend/samfundet/admin.py @@ -134,7 +134,6 @@ def group_memberships(self, obj: User) -> int: 'is_staff', 'is_superuser', 'groups', - 'roles', 'user_permissions', ), }, diff --git a/backend/samfundet/roleauth/backend.py b/backend/samfundet/backend.py similarity index 100% rename from backend/samfundet/roleauth/backend.py rename to backend/samfundet/backend.py diff --git a/backend/samfundet/roleauth/__init__.py b/backend/samfundet/roleauth/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/routes/backend.ts b/frontend/src/routes/backend.ts index a52d2070f..1c481c2e4 100644 --- a/frontend/src/routes/backend.ts +++ b/frontend/src/routes/backend.ts @@ -42,6 +42,30 @@ export const ROUTES_BACKEND = { admin__auth_group_delete: '/admin/auth/group/:objectId/delete/', admin__auth_group_change: '/admin/auth/group/:objectId/change/', adminauthgroup__objectId: '/admin/auth/group/:objectId/', + admin__samfundet_role_changelist: '/admin/samfundet/role/', + admin__samfundet_role_add: '/admin/samfundet/role/add/', + admin__samfundet_role_history: '/admin/samfundet/role/:objectId/history/', + admin__samfundet_role_delete: '/admin/samfundet/role/:objectId/delete/', + admin__samfundet_role_change: '/admin/samfundet/role/:objectId/change/', + adminsamfundetrole__objectId: '/admin/samfundet/role/:objectId/', + admin__samfundet_userorgrole_changelist: '/admin/samfundet/userorgrole/', + admin__samfundet_userorgrole_add: '/admin/samfundet/userorgrole/add/', + admin__samfundet_userorgrole_history: '/admin/samfundet/userorgrole/:objectId/history/', + admin__samfundet_userorgrole_delete: '/admin/samfundet/userorgrole/:objectId/delete/', + admin__samfundet_userorgrole_change: '/admin/samfundet/userorgrole/:objectId/change/', + adminsamfundetuserorgrole__objectId: '/admin/samfundet/userorgrole/:objectId/', + admin__samfundet_usergangrole_changelist: '/admin/samfundet/usergangrole/', + admin__samfundet_usergangrole_add: '/admin/samfundet/usergangrole/add/', + admin__samfundet_usergangrole_history: '/admin/samfundet/usergangrole/:objectId/history/', + admin__samfundet_usergangrole_delete: '/admin/samfundet/usergangrole/:objectId/delete/', + admin__samfundet_usergangrole_change: '/admin/samfundet/usergangrole/:objectId/change/', + adminsamfundetusergangrole__objectId: '/admin/samfundet/usergangrole/:objectId/', + admin__samfundet_usergangsectionrole_changelist: '/admin/samfundet/usergangsectionrole/', + admin__samfundet_usergangsectionrole_add: '/admin/samfundet/usergangsectionrole/add/', + admin__samfundet_usergangsectionrole_history: '/admin/samfundet/usergangsectionrole/:objectId/history/', + admin__samfundet_usergangsectionrole_delete: '/admin/samfundet/usergangsectionrole/:objectId/delete/', + admin__samfundet_usergangsectionrole_change: '/admin/samfundet/usergangsectionrole/:objectId/change/', + adminsamfundetusergangsectionrole__objectId: '/admin/samfundet/usergangsectionrole/:objectId/', admin__auth_permission_permissions: '/admin/auth/permission/:objectPk/permissions/', admin__auth_permission_permissions_manage_user: '/admin/auth/permission/:objectPk/permissions/user-manage/:userId/', admin__auth_permission_permissions_manage_group: '/admin/auth/permission/:objectPk/permissions/group-manage/:groupId/', From b71140a2d262b7177e4fd514d9a97eaf5f12e65a Mon Sep 17 00:00:00 2001 From: robines Date: Sat, 3 Aug 2024 14:51:26 +0200 Subject: [PATCH 12/32] ruff --- backend/samfundet/models/general.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/samfundet/models/general.py b/backend/samfundet/models/general.py index fda51aaac..670149fce 100644 --- a/backend/samfundet/models/general.py +++ b/backend/samfundet/models/general.py @@ -22,7 +22,6 @@ from root.utils import permissions from root.utils.mixins import CustomBaseModel, FullCleanSaveMixin -from samfundet.models.role import Role from samfundet.models.model_choices import ReservationOccasion, UserPreferenceTheme, SaksdokumentCategory from .utils.fields import LowerCaseField, PhoneNumberField From ae46e34a9233ad8cf477cb5eb10353573f640a8a Mon Sep 17 00:00:00 2001 From: robines Date: Sat, 10 Aug 2024 21:51:33 +0200 Subject: [PATCH 13/32] get_perm: support permissions without period --- backend/samfundet/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/samfundet/utils.py b/backend/samfundet/utils.py index dd1159e1d..6755ad78d 100644 --- a/backend/samfundet/utils.py +++ b/backend/samfundet/utils.py @@ -95,7 +95,7 @@ def get_occupied_timeslots_from_request( def get_perm(*, perm: str, model: type[Model]) -> Permission: - codename = perm.split('.')[1] + codename = perm.split('.')[1] if '.' in perm else perm content_type = ContentType.objects.get_for_model(model=model) permission = Permission.objects.get(codename=codename, content_type=content_type) return permission From 9ebfc2af670f8b2f400156d204de977729a87a20 Mon Sep 17 00:00:00 2001 From: robines Date: Wed, 14 Aug 2024 00:35:16 +0200 Subject: [PATCH 14/32] Fix hierarchy permission checking --- backend/samfundet/backend.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/backend/samfundet/backend.py b/backend/samfundet/backend.py index 3820db671..b87a9ee82 100644 --- a/backend/samfundet/backend.py +++ b/backend/samfundet/backend.py @@ -20,18 +20,30 @@ def has_perm(self, user_obj: User, perm: str, obj: Any = None) -> bool: # noqa: permission = get_perm(perm=perm, model=obj) if hasattr(obj, 'resolve_org'): - org_id = obj.resolve_org(return_id=True) - if org_id is not None: - return UserOrgRole.objects.filter(user=user_obj, obj__id=org_id, role__permissions=permission).exists() + try: + org_id = obj.resolve_org(return_id=True) + if org_id is not None and UserOrgRole.objects.filter(user=user_obj, obj__id=org_id, + role__permissions=permission).exists(): + return True + except NotImplementedError: + pass if hasattr(obj, 'resolve_gang'): - gang_id = obj.resolve_gang(return_id=True) - if gang_id is not None: - return UserGangRole.objects.filter(user=user_obj, obj__id=gang_id, role__permissions=permission).exists() + try: + gang_id = obj.resolve_gang(return_id=True) + if gang_id is not None and UserGangRole.objects.filter(user=user_obj, obj__id=gang_id, + role__permissions=permission).exists(): + return True + except NotImplementedError: + pass if hasattr(obj, 'resolve_section'): - section_id = obj.resolve_section(return_id=True) - if section_id is not None: - return UserGangSectionRole.objects.filter(user=user_obj, obj__id=section_id, role__permissions=permission).exists() + try: + section_id = obj.resolve_section(return_id=True) + if section_id is not None and UserGangSectionRole.objects.filter(user=user_obj, obj__id=section_id, + role__permissions=permission).exists(): + return True + except NotImplementedError: + pass return False From bde4d11ef7cd53636bd3d4a2158eda86eac21069 Mon Sep 17 00:00:00 2001 From: robines Date: Wed, 14 Aug 2024 00:39:08 +0200 Subject: [PATCH 15/32] Add some tests --- backend/samfundet/backend.py | 9 ++-- backend/samfundet/conftest.py | 64 +++++++++++++++++++++++- backend/samfundet/tests/test_roles.py | 72 +++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 backend/samfundet/tests/test_roles.py diff --git a/backend/samfundet/backend.py b/backend/samfundet/backend.py index b87a9ee82..0f00e3e61 100644 --- a/backend/samfundet/backend.py +++ b/backend/samfundet/backend.py @@ -22,8 +22,7 @@ def has_perm(self, user_obj: User, perm: str, obj: Any = None) -> bool: # noqa: if hasattr(obj, 'resolve_org'): try: org_id = obj.resolve_org(return_id=True) - if org_id is not None and UserOrgRole.objects.filter(user=user_obj, obj__id=org_id, - role__permissions=permission).exists(): + if org_id is not None and UserOrgRole.objects.filter(user=user_obj, obj__id=org_id, role__permissions=permission).exists(): return True except NotImplementedError: pass @@ -31,8 +30,7 @@ def has_perm(self, user_obj: User, perm: str, obj: Any = None) -> bool: # noqa: if hasattr(obj, 'resolve_gang'): try: gang_id = obj.resolve_gang(return_id=True) - if gang_id is not None and UserGangRole.objects.filter(user=user_obj, obj__id=gang_id, - role__permissions=permission).exists(): + if gang_id is not None and UserGangRole.objects.filter(user=user_obj, obj__id=gang_id, role__permissions=permission).exists(): return True except NotImplementedError: pass @@ -40,8 +38,7 @@ def has_perm(self, user_obj: User, perm: str, obj: Any = None) -> bool: # noqa: if hasattr(obj, 'resolve_section'): try: section_id = obj.resolve_section(return_id=True) - if section_id is not None and UserGangSectionRole.objects.filter(user=user_obj, obj__id=section_id, - role__permissions=permission).exists(): + if section_id is not None and UserGangSectionRole.objects.filter(user=user_obj, obj__id=section_id, role__permissions=permission).exists(): return True except NotImplementedError: pass diff --git a/backend/samfundet/conftest.py b/backend/samfundet/conftest.py index 206cfed73..167a34c2a 100644 --- a/backend/samfundet/conftest.py +++ b/backend/samfundet/conftest.py @@ -11,12 +11,15 @@ from django.test import Client, TestCase from django.utils import timezone from django.core.files.images import ImageFile -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType import root.management.commands.seed_scripts.billig as billig_seed from root.settings import BASE_DIR +from samfundet.backend import RoleAuthBackend from samfundet.constants import DEV_PASSWORD +from samfundet.models.role import Role from samfundet.models.event import Event from samfundet.models.billig import BilligEvent from samfundet.models.general import ( @@ -29,6 +32,7 @@ Campus, BlogPost, TextItem, + GangSection, Reservation, Organization, MerchVariation, @@ -249,6 +253,64 @@ def fixture_gang2(fixture_organization: Organization) -> Iterator[Gang]: organization.delete() +@pytest.fixture +def fixture_gang_section(fixture_gang: Gang) -> Iterator[GangSection]: + gang_section = GangSection.objects.create( + name_nb='Test Gang Section', + name_en='Test Gang Section', + gang=fixture_gang, + ) + yield gang_section + gang_section.delete() + + +@pytest.fixture +def fixture_role() -> Iterator[Role]: + role = Role.objects.create( + name='Test Role', + ) + yield role + role.delete() + + +@pytest.fixture +def fixture_role_auth_backend() -> RoleAuthBackend: + return RoleAuthBackend() + + +@pytest.fixture +def fixture_org_permission() -> Iterator[Permission]: + permission = Permission.objects.create( + name='Test Org Permission', + codename='test_org_permission', + content_type=ContentType.objects.get_for_model(Organization), + ) + yield permission + permission.delete() + + +@pytest.fixture +def fixture_gang_permission() -> Iterator[Permission]: + permission = Permission.objects.create( + name='Test Gang Permission', + codename='test_gang_permission', + content_type=ContentType.objects.get_for_model(Gang), + ) + yield permission + permission.delete() + + +@pytest.fixture +def fixture_gang_section_permission() -> Iterator[Permission]: + permission = Permission.objects.create( + name='Test Gang Section Permission', + codename='test_gang_section_permission', + content_type=ContentType.objects.get_for_model(GangSection), + ) + yield permission + permission.delete() + + @pytest.fixture def fixture_text_item() -> Iterator[TextItem]: text_item = TextItem.objects.create( diff --git a/backend/samfundet/tests/test_roles.py b/backend/samfundet/tests/test_roles.py new file mode 100644 index 000000000..7591f7af6 --- /dev/null +++ b/backend/samfundet/tests/test_roles.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from django.contrib.auth.models import Permission + +from samfundet.models import Gang, User, GangSection, Organization +from samfundet.backend import RoleAuthBackend +from samfundet.models.role import Role, UserOrgRole, UserGangRole, UserGangSectionRole + + +def test_has_perm_superuser( + fixture_role_auth_backend: RoleAuthBackend, fixture_superuser: User, fixture_organization: Organization, fixture_org_permission: Permission +): + assert fixture_role_auth_backend.has_perm(fixture_superuser, fixture_org_permission.codename, fixture_organization) + + +def test_has_perm_user_with_orgrole( + fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, fixture_organization: Organization, fixture_org_permission: Permission +): + fixture_role.permissions.add(fixture_org_permission) + + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + + org_role = UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) + org_role.save() + + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + + +def test_has_perm_user_with_gangrole( + fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, fixture_gang: Gang, fixture_gang_permission: Permission +): + fixture_role.permissions.add(fixture_gang_permission) + + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + + gang_role = UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) + gang_role.save() + + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + + +def test_has_perm_user_with_sectionrole( + fixture_role_auth_backend: RoleAuthBackend, + fixture_user: User, + fixture_role: Role, + fixture_gang_section: GangSection, + fixture_gang_section_permission: Permission, +): + fixture_role.permissions.add(fixture_gang_section_permission) + + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + + section_role = UserGangSectionRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang_section) + section_role.save() + + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + + +def test_has_perm_inactive_user( + fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_organization: Organization, fixture_org_permission: Permission +): + fixture_user.is_active = False + fixture_user.save() + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + + +def test_has_perm_no_obj(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_org_permission: Permission): + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, None) + + +def test_has_perm_superuser_no_obj(fixture_role_auth_backend: RoleAuthBackend, fixture_superuser: User, fixture_org_permission: Permission): + assert not fixture_role_auth_backend.has_perm(fixture_superuser, fixture_org_permission.codename, None) From e3bf094bf440a1e8c4f9f4f41578dd533b4af939 Mon Sep 17 00:00:00 2001 From: robines Date: Wed, 14 Aug 2024 00:54:02 +0200 Subject: [PATCH 16/32] Remove RoleMixin to avoid unnecessary method calls and exception raising/catching This means when the obj hasattr check is True, we can assume the method is implemented. --- backend/root/utils/mixins.py | 38 +------------------ backend/samfundet/backend.py | 27 +++++-------- ...angrole_usergangsectionrole_userorgrole.py | 6 +-- 3 files changed, 12 insertions(+), 59 deletions(-) diff --git a/backend/root/utils/mixins.py b/backend/root/utils/mixins.py index 09dbde463..116e1ae09 100644 --- a/backend/root/utils/mixins.py +++ b/backend/root/utils/mixins.py @@ -3,7 +3,7 @@ import sys import copy import logging -from typing import TYPE_CHECKING, Any +from typing import Any from rest_framework import serializers @@ -15,9 +15,6 @@ LOG = logging.getLogger(__name__) -if TYPE_CHECKING: - from samfundet.models import Gang, GangSection, Organization - class FieldTrackerMixin(Model): """ @@ -183,38 +180,7 @@ def save(self, *args: Any, **kwargs: Any) -> None: super().save(*args, **kwargs) -class RoleMixin: - """To be a part of the Role system, at least one of these functions must be implemented.""" - - def resolve_org(self, *, return_id: bool = False) -> Organization | int: - """ - There are often multiple paths to requested resource, typically in many-to-many relationships. - Resolving must be done manually - - :param return_id: May be used to implement resolver without redundant db instance fetching - """ - raise NotImplementedError('Intentionally not implemented: ambiguous resource') - - def resolve_gang(self, *, return_id: bool = False) -> Gang | int: - """ - There are often multiple paths to requested resource, typically in many-to-many relationships. - Resolving must be done manually - - :param return_id: May be used to implement resolver without redundant db instance fetching - """ - raise NotImplementedError('Intentionally not implemented: ambiguous resource') - - def resolve_section(self, *, return_id: bool = False) -> GangSection | int: - """ - There are often multiple paths to requested resource, typically in many-to-many relationships. - Resolving must be done manually - - :param return_id: May be used to implement resolver without redundant db instance fetching - """ - raise NotImplementedError('Intentionally not implemented: ambiguous resource') - - -class CustomBaseModel(RoleMixin, FullCleanSaveMixin): +class CustomBaseModel(FullCleanSaveMixin): """ Basic model which will contains necessary version info of a model: With by who and when it was updated and created. diff --git a/backend/samfundet/backend.py b/backend/samfundet/backend.py index 0f00e3e61..7146afc59 100644 --- a/backend/samfundet/backend.py +++ b/backend/samfundet/backend.py @@ -20,27 +20,18 @@ def has_perm(self, user_obj: User, perm: str, obj: Any = None) -> bool: # noqa: permission = get_perm(perm=perm, model=obj) if hasattr(obj, 'resolve_org'): - try: - org_id = obj.resolve_org(return_id=True) - if org_id is not None and UserOrgRole.objects.filter(user=user_obj, obj__id=org_id, role__permissions=permission).exists(): - return True - except NotImplementedError: - pass + org_id = obj.resolve_org(return_id=True) + if org_id is not None and UserOrgRole.objects.filter(user=user_obj, obj__id=org_id, role__permissions=permission).exists(): + return True if hasattr(obj, 'resolve_gang'): - try: - gang_id = obj.resolve_gang(return_id=True) - if gang_id is not None and UserGangRole.objects.filter(user=user_obj, obj__id=gang_id, role__permissions=permission).exists(): - return True - except NotImplementedError: - pass + gang_id = obj.resolve_gang(return_id=True) + if gang_id is not None and UserGangRole.objects.filter(user=user_obj, obj__id=gang_id, role__permissions=permission).exists(): + return True if hasattr(obj, 'resolve_section'): - try: - section_id = obj.resolve_section(return_id=True) - if section_id is not None and UserGangSectionRole.objects.filter(user=user_obj, obj__id=section_id, role__permissions=permission).exists(): - return True - except NotImplementedError: - pass + section_id = obj.resolve_section(return_id=True) + if section_id is not None and UserGangSectionRole.objects.filter(user=user_obj, obj__id=section_id, role__permissions=permission).exists(): + return True return False diff --git a/backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py b/backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py index 2f2a3d956..6e48a1d68 100644 --- a/backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py +++ b/backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py @@ -1,7 +1,6 @@ -# Generated by Django 5.0.6 on 2024-07-07 19:18 +# Generated by Django 5.0.6 on 2024-08-13 22:51 import django.db.models.deletion -import root.utils.mixins from django.conf import settings from django.db import migrations, models @@ -38,7 +37,6 @@ class Migration(migrations.Migration): options={ 'abstract': False, }, - bases=(root.utils.mixins.RoleMixin, models.Model), ), migrations.CreateModel( name='UserGangSectionRole', @@ -56,7 +54,6 @@ class Migration(migrations.Migration): options={ 'abstract': False, }, - bases=(root.utils.mixins.RoleMixin, models.Model), ), migrations.CreateModel( name='UserOrgRole', @@ -74,6 +71,5 @@ class Migration(migrations.Migration): options={ 'abstract': False, }, - bases=(root.utils.mixins.RoleMixin, models.Model), ), ] From 5dd260e2354461b23954dfaf8e62e613f341b4b9 Mon Sep 17 00:00:00 2001 From: robines Date: Thu, 22 Aug 2024 21:53:51 +0200 Subject: [PATCH 17/32] Begin docs --- docs/technical/backend/rolesystem.md | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/technical/backend/rolesystem.md diff --git a/docs/technical/backend/rolesystem.md b/docs/technical/backend/rolesystem.md new file mode 100644 index 000000000..38d1daa14 --- /dev/null +++ b/docs/technical/backend/rolesystem.md @@ -0,0 +1,43 @@ +# Role System + +The role system in Samfundet4 builds on the Django "authentication backend" concept. Our system adds +a [custom auth backend](https://docs.djangoproject.com/en/5.0/topics/auth/customizing/). The goal of the system is to +enable us to answer queries like: + +> Does Bob have access to edit case document X, which belongs to the UKA organization? +> +>Does Bob have access to view recruitment position X, which belongs to Web (a section of MG in the Samfundet +> organization) + +The system uses hierarchical permission checking. It first checks if a user has a permission to a specific object on the +Organizational level, then Gang level, and finally the Gang Section level. This means that if a user has a specific +permission for an object on the Organizational level, they also have it on the Gang and Gang Section levels. And if a +user has it on the Gang level, they also have it on the Gang Section level. + +## Organization/Gang/Section resolvers + +For the auth backend to know what organization an object belongs to, models need to implement +the `resolve_org`/`resolve_gang`/`resolve_section` methods. The purpose of these resolvers is to return the +org/gang/section the object belongs to. Models implementing these methods may not have ambiguous ownership, or be owned +by multiple orgs/gangs/sections. + +> Example: The Tag model doesn't implement these methods, as they aren't "owned" by anybody +> +> Example: A Recruitment is owned by an organization, therefore `resolve_org` returns that organization. + +## Role + +A Role simply contains a name and a list of permissions. An "Interviewer" Role may for example contain permissions +related to interviews, interview rooms, applications, etc. + +## Organization/Gang/Section User Roles + +To tie users together to roles, we use either `UserOrgRole`/`UserGangRole`/`UserGangSectionRole`. These models contain +three fields: user, role, and object. For `UserOrgRole`, the obj will be an instance of Organization, for `UserGangRole` +it'll be an instance of Gang, and for `UserGangSectionRole` it'll be an instance of a GangSection. + +## Inheritance + +There is no inheritance in our system, as that often leads to unnecessary complexity, both in the code and in our mental +understanding of how a specific role – or a set of roles – operates. If something is wrong with a role's permissions, +you can simply fix it then and there, instead of looking up and down the inheritance tree to see where the issue is. From d1ebf7fa6ed0ca41eed616c0851c7877b17f848a Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 23 Aug 2024 00:22:44 +0200 Subject: [PATCH 18/32] Update migration --- ...0004_role_usergangrole_usergangsectionrole_userorgrole.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename backend/samfundet/migrations/{0002_role_usergangrole_usergangsectionrole_userorgrole.py => 0004_role_usergangrole_usergangsectionrole_userorgrole.py} (97%) diff --git a/backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py b/backend/samfundet/migrations/0004_role_usergangrole_usergangsectionrole_userorgrole.py similarity index 97% rename from backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py rename to backend/samfundet/migrations/0004_role_usergangrole_usergangsectionrole_userorgrole.py index 6e48a1d68..a24f0ca57 100644 --- a/backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py +++ b/backend/samfundet/migrations/0004_role_usergangrole_usergangsectionrole_userorgrole.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.6 on 2024-08-13 22:51 +# Generated by Django 5.0.6 on 2024-08-22 22:22 import django.db.models.deletion from django.conf import settings @@ -9,7 +9,7 @@ class Migration(migrations.Migration): dependencies = [ ('auth', '0012_alter_user_first_name_max_length'), - ('samfundet', '0001_initial'), + ('samfundet', '0003_recruitmentseparateposition_and_more'), ] operations = [ From 7117698c5dd4eba374bd3ad144e32741b597d543 Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 23 Aug 2024 00:39:35 +0200 Subject: [PATCH 19/32] Add code examples to docs --- docs/technical/backend/rolesystem.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/technical/backend/rolesystem.md b/docs/technical/backend/rolesystem.md index 38d1daa14..80d037fe2 100644 --- a/docs/technical/backend/rolesystem.md +++ b/docs/technical/backend/rolesystem.md @@ -25,6 +25,30 @@ by multiple orgs/gangs/sections. > > Example: A Recruitment is owned by an organization, therefore `resolve_org` returns that organization. +Here's an example implementation of `resolve_org` for the Recruitment model: + +```python +class Recruitment(CustomBaseModel): + ... + organization = models.ForeignKey(to=Organization) + + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + if return_id: + return self.organization_id + return self.organization +``` + +And another example, showing how models can just call each other's resolvers to greatly simplify things: + +```python +class RecruitmentPosition(CustomBaseModel): + ... + gang = models.ForeignKey(to=Gang) + + def resolve_org(self, *, return_id: bool = False) -> Organization | int: + return self.gang.resolve_org(return_id=return_id) +``` + ## Role A Role simply contains a name and a list of permissions. An "Interviewer" Role may for example contain permissions @@ -36,6 +60,9 @@ To tie users together to roles, we use either `UserOrgRole`/`UserGangRole`/`User three fields: user, role, and object. For `UserOrgRole`, the obj will be an instance of Organization, for `UserGangRole` it'll be an instance of Gang, and for `UserGangSectionRole` it'll be an instance of a GangSection. +To reiterate: if we create a `UserOrgRole` instance, it gives the user all of the role's permissions for the given +organization. + ## Inheritance There is no inheritance in our system, as that often leads to unnecessary complexity, both in the code and in our mental From 57c21ba44e7dbdfe16aa0d7f962b7e854373f20e Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 23 Aug 2024 00:42:09 +0200 Subject: [PATCH 20/32] Add link to docs --- docs/technical/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/technical/README.md b/docs/technical/README.md index 1d5752bbf..feb7553cd 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -14,6 +14,7 @@ - [Billig (payment system)](/docs/technical/backend/billig.md) - [Seed scripts](/docs/technical/backend/seed.md) +- [Role System](/docs/technical/backend/rolesystem.md) ### Pipelines & Deployment - [Pipeline (mypy, eslint, tsc, ...)](/docs/technical/pipeline.md) From fa42582a2b670592a59b8741e877b9d5fb822089 Mon Sep 17 00:00:00 2001 From: robines Date: Thu, 5 Sep 2024 21:39:38 +0200 Subject: [PATCH 21/32] Fix import merge --- backend/samfundet/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/samfundet/admin.py b/backend/samfundet/admin.py index bfb73fc87..0e622c9e9 100644 --- a/backend/samfundet/admin.py +++ b/backend/samfundet/admin.py @@ -23,7 +23,7 @@ from .models.role import Role, UserOrgRole, UserGangRole, UserGangSectionRole -from .models.event import Event, EventGroup, EventRegistration +from .models.event import Event, EventGroup, EventRegistration, PurchaseFeedbackModel from .models.general import ( Tag, Gang, From ba2ef049be4d0932d30e05b65c358e9329878f0d Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 6 Sep 2024 02:36:57 +0200 Subject: [PATCH 22/32] Update migrations --- ...0008_role_usergangrole_usergangsectionrole_userorgrole.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename backend/samfundet/migrations/{0004_role_usergangrole_usergangsectionrole_userorgrole.py => 0008_role_usergangrole_usergangsectionrole_userorgrole.py} (97%) diff --git a/backend/samfundet/migrations/0004_role_usergangrole_usergangsectionrole_userorgrole.py b/backend/samfundet/migrations/0008_role_usergangrole_usergangsectionrole_userorgrole.py similarity index 97% rename from backend/samfundet/migrations/0004_role_usergangrole_usergangsectionrole_userorgrole.py rename to backend/samfundet/migrations/0008_role_usergangrole_usergangsectionrole_userorgrole.py index a24f0ca57..792d3694d 100644 --- a/backend/samfundet/migrations/0004_role_usergangrole_usergangsectionrole_userorgrole.py +++ b/backend/samfundet/migrations/0008_role_usergangrole_usergangsectionrole_userorgrole.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.6 on 2024-08-22 22:22 +# Generated by Django 5.0.7 on 2024-09-05 23:59 import django.db.models.deletion from django.conf import settings @@ -9,7 +9,7 @@ class Migration(migrations.Migration): dependencies = [ ('auth', '0012_alter_user_first_name_max_length'), - ('samfundet', '0003_recruitmentseparateposition_and_more'), + ('samfundet', '0007_recruitmentgangstat'), ] operations = [ From b8a4269c29ad10ebc86ae00d411cc4b8d2ce1fac Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 6 Sep 2024 02:37:06 +0200 Subject: [PATCH 23/32] Create fixture_organization2, update fixture_gang2 to use it This lets us more easily test different organizational hierarchies --- backend/samfundet/conftest.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/samfundet/conftest.py b/backend/samfundet/conftest.py index 167a34c2a..b9ce5e299 100644 --- a/backend/samfundet/conftest.py +++ b/backend/samfundet/conftest.py @@ -229,6 +229,13 @@ def fixture_organization() -> Iterator[Organization]: organization.delete() +@pytest.fixture +def fixture_organization2() -> Iterator[Organization]: + organization = Organization.objects.create(name='UKA') + yield organization + organization.delete() + + @pytest.fixture def fixture_gang(fixture_organization: Organization) -> Iterator[Gang]: organization = Gang.objects.create( @@ -242,12 +249,12 @@ def fixture_gang(fixture_organization: Organization) -> Iterator[Gang]: @pytest.fixture -def fixture_gang2(fixture_organization: Organization) -> Iterator[Gang]: +def fixture_gang2(fixture_organization2: Organization) -> Iterator[Gang]: organization = Gang.objects.create( name_nb='Gang 2', name_en='Gang 2', abbreviation='G2', - organization=fixture_organization, + organization=fixture_organization2, ) yield organization organization.delete() From 75675f4bb43ec77f549d6f30af01293542bf375a Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 6 Sep 2024 02:44:41 +0200 Subject: [PATCH 24/32] Add fixture_gang_section2 --- backend/samfundet/conftest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/samfundet/conftest.py b/backend/samfundet/conftest.py index b9ce5e299..b6fb4aaea 100644 --- a/backend/samfundet/conftest.py +++ b/backend/samfundet/conftest.py @@ -271,6 +271,17 @@ def fixture_gang_section(fixture_gang: Gang) -> Iterator[GangSection]: gang_section.delete() +@pytest.fixture +def fixture_gang_section2(fixture_gang2: Gang) -> Iterator[GangSection]: + gang_section = GangSection.objects.create( + name_nb='Test Gang Section 2', + name_en='Test Gang Section 2', + gang=fixture_gang2, + ) + yield gang_section + gang_section.delete() + + @pytest.fixture def fixture_role() -> Iterator[Role]: role = Role.objects.create( From b98493805839a79583b16ccbfeab288f12ad3405 Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 6 Sep 2024 03:40:36 +0200 Subject: [PATCH 25/32] Update tests, and add hierarchy testing --- backend/samfundet/tests/test_roles.py | 229 ++++++++++++++++++++++---- 1 file changed, 198 insertions(+), 31 deletions(-) diff --git a/backend/samfundet/tests/test_roles.py b/backend/samfundet/tests/test_roles.py index 7591f7af6..863067e58 100644 --- a/backend/samfundet/tests/test_roles.py +++ b/backend/samfundet/tests/test_roles.py @@ -8,65 +8,232 @@ def test_has_perm_superuser( - fixture_role_auth_backend: RoleAuthBackend, fixture_superuser: User, fixture_organization: Organization, fixture_org_permission: Permission + fixture_role_auth_backend: RoleAuthBackend, fixture_superuser: User, fixture_organization: Organization, + fixture_org_permission: Permission ): + """Test that superusers have permissions to all resources even without any roles.""" assert fixture_role_auth_backend.has_perm(fixture_superuser, fixture_org_permission.codename, fixture_organization) -def test_has_perm_user_with_orgrole( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, fixture_organization: Organization, fixture_org_permission: Permission +def test_has_perm_inactive_user( + fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_organization: Organization, + fixture_org_permission: Permission, +): + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + + +def test_has_perm_inactive_user_with_role( + fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_organization: Organization, + fixture_org_permission: Permission, fixture_role: Role ): + """Test that inactive users who would otherwise have access to a resource, don't.""" + fixture_role.permissions.add(fixture_org_permission) + UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) + fixture_user.is_active = False + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + + +def test_has_perm_no_obj(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, + fixture_org_permission: Permission): + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, None) + + +def test_has_perm_superuser_no_obj(fixture_role_auth_backend: RoleAuthBackend, fixture_superuser: User, + fixture_org_permission: Permission): + assert not fixture_role_auth_backend.has_perm(fixture_superuser, fixture_org_permission.codename, None) + + +def test_has_perm_user_with_no_roles(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, + fixture_organization: Organization, fixture_gang: Gang, + fixture_gang_section: GangSection, fixture_org_permission: Permission, + fixture_gang_permission: Permission, fixture_gang_section_permission: Permission): + """Sanity check. Within the scope of our auth backend, a user with no roles should have no permissions, + on any hierarchical level.""" + + # Create a role with permission to our example resources (org/gang/section), but don't attach it to user. fixture_role.permissions.add(fixture_org_permission) + fixture_role.permissions.add(fixture_gang_permission) + fixture_role.permissions.add(fixture_gang_section_permission) assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, + fixture_gang_section) - org_role = UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) - org_role.save() + +def test_has_perm_user_with_org_role(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, + fixture_organization: Organization, fixture_org_permission: Permission): + """Test that giving user an OrgRole with permission to access a resource, actually gives them access.""" + fixture_role.permissions.add(fixture_org_permission) + UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) assert fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) -def test_has_perm_user_with_gangrole( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, fixture_gang: Gang, fixture_gang_permission: Permission -): +def test_has_perm_user_with_gang_role(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, + fixture_role: Role, fixture_gang: Gang, fixture_gang_permission: Permission): + """Test that giving user a GangRole with permission to access a resource, actually gives them access.""" + fixture_role.permissions.add(fixture_gang_permission) + UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) + + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + + +def test_has_perm_user_with_section_role(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, + fixture_role: Role, fixture_gang_section: GangSection, + fixture_gang_section_permission: Permission): + """Test that giving user a GangSectionRole with permission to access a resource, actually gives them access.""" + fixture_role.permissions.add(fixture_gang_section_permission) + UserGangSectionRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang_section) + + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, + fixture_gang_section) + + +def test_has_perm_different_orgs(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, + fixture_organization: Organization, fixture_organization2: Organization, + fixture_org_permission: Permission, fixture_role: Role): + """Test that giving user a role to a specific org, does not give it to other orgs""" + fixture_role.permissions.add(fixture_org_permission) + + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + + UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) + + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization2) + + +def test_has_perm_different_gangs(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, + fixture_gang: Gang, fixture_gang2: Gang, + fixture_gang_permission: Permission, fixture_role: Role): + """Test that giving user a role to a specific gang, does not give it to other gangs""" fixture_role.permissions.add(fixture_gang_permission) assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) - gang_role = UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) - gang_role.save() + UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang2) -def test_has_perm_user_with_sectionrole( - fixture_role_auth_backend: RoleAuthBackend, - fixture_user: User, - fixture_role: Role, - fixture_gang_section: GangSection, - fixture_gang_section_permission: Permission, -): +def test_has_perm_different_gang_sections(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, + fixture_gang_section: GangSection, fixture_gang_section2: Gang, + fixture_gang_section_permission: Permission, fixture_role: Role): + """Test that giving user a role to a specific gang section, does not give it to other gang sections""" fixture_role.permissions.add(fixture_gang_section_permission) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, + fixture_gang_section) - section_role = UserGangSectionRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang_section) - section_role.save() + UserGangSectionRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang_section) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, + fixture_gang_section) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, + fixture_gang_section2) -def test_has_perm_inactive_user( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_organization: Organization, fixture_org_permission: Permission -): - fixture_user.is_active = False - fixture_user.save() - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) +def test_has_perm_different_users(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_user2: User, + fixture_organization: Organization, fixture_gang: Gang, + fixture_gang_section: GangSection, fixture_org_permission: Permission, + fixture_gang_permission: Permission, fixture_gang_section_permission: Permission, + fixture_role: Role): + """Test that giving user a role, does not give it to other users""" + fixture_role.permissions.add(fixture_org_permission) + fixture_role.permissions.add(fixture_gang_permission) + fixture_role.permissions.add(fixture_gang_section_permission) + UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) + UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) + UserGangSectionRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang_section) -def test_has_perm_no_obj(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_org_permission: Permission): - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, None) + assert not fixture_role_auth_backend.has_perm(fixture_user2, fixture_org_permission.codename, fixture_organization) + assert not fixture_role_auth_backend.has_perm(fixture_user2, fixture_gang_permission.codename, fixture_gang) + assert not fixture_role_auth_backend.has_perm(fixture_user2, fixture_gang_section_permission.codename, + fixture_gang_section) -def test_has_perm_superuser_no_obj(fixture_role_auth_backend: RoleAuthBackend, fixture_superuser: User, fixture_org_permission: Permission): - assert not fixture_role_auth_backend.has_perm(fixture_superuser, fixture_org_permission.codename, None) +def test_has_perm_org_downward(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, + fixture_organization: Organization, fixture_organization2: Organization, + fixture_gang: Gang, fixture_gang2: Gang, fixture_gang_section: GangSection, + fixture_role: Role, fixture_org_permission: Permission, + fixture_gang_permission: Permission, fixture_gang_section_permission: Permission): + """Test that giving permission on org/gang level, also gives it downwards (gang/section).""" + fixture_role.permissions.add(fixture_gang_section_permission) + fixture_role.permissions.add(fixture_gang_permission) + + fixture_gang.organization = fixture_organization + fixture_gang_section.gang = fixture_gang + + # Giving a user an Org role should give the same permissions on Gang and Section levels + + org_role = UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) + + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, + fixture_gang_section) + + org_role.delete() + + # Permissions should be gone after deleting org role + + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, + fixture_gang_section) + + # Giving a user a Gang role should give the same permissions on Section level + + gang_role = UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) + + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, + fixture_gang_section) + + gang_role.delete() + + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, + fixture_gang_section) + + # Give the user the Org role again, and ensure that after we detach Gang and Section from the Organization, + # we no longer have permissions. + UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) + + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, + fixture_gang_section) + + fixture_gang_section.gang = fixture_gang2 + + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, + fixture_gang_section) + + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + + fixture_gang.organization = fixture_organization2 + + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + + +def test_has_perm_section_upward(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, + fixture_organization: Organization, fixture_gang: Gang, + fixture_gang_section: GangSection, fixture_role: Role, + fixture_org_permission: Permission, fixture_gang_permission: Permission, + fixture_gang_section_permission: Permission): + """Test that giving permission on section/gang level, does not give it upwards (gang/org).""" + fixture_role.permissions.add(fixture_org_permission) + fixture_role.permissions.add(fixture_gang_permission) + fixture_role.permissions.add(fixture_gang_section_permission) + + fixture_gang.organization = fixture_organization + fixture_gang_section.gang = fixture_gang + + section_role = UserGangSectionRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang_section) + + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + + section_role.delete() + + UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) + + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) From 3626529a5077818554bb5ca52846f401df9e1648 Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 6 Sep 2024 03:55:03 +0200 Subject: [PATCH 26/32] Revert changes in views --- backend/samfundet/views.py | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index 0d1ef3f22..b24e32e34 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -1065,22 +1065,12 @@ class InterviewRoomView(ModelViewSet): serializer_class = InterviewRoomSerializer queryset = InterviewRoom.objects.all() - # noinspection PyMethodOverriding - def retrieve(self, request: Request, pk: int) -> Response: - room = get_object_or_404(InterviewRoom, pk=pk) - if not request.user.has_perm(SAMFUNDET_VIEW_INTERVIEWROOM, room): - raise PermissionDenied - return super().retrieve(request=request, pk=pk) - - # noinspection PyMethodOverriding def list(self, request: Request) -> Response: recruitment = request.query_params.get('recruitment') if not recruitment: return Response({'error': 'A recruitment parameter is required'}, status=status.HTTP_400_BAD_REQUEST) - filtered_rooms = [ - room for room in InterviewRoom.objects.filter(recruitment__id=recruitment) if request.user.has_perm(SAMFUNDET_VIEW_INTERVIEWROOM, room) - ] + filtered_rooms = InterviewRoom.objects.filter(recruitment__id=recruitment) serialized_rooms = self.get_serializer(filtered_rooms, many=True) return Response(serialized_rooms.data) @@ -1105,19 +1095,6 @@ class InterviewView(ModelViewSet): serializer_class = InterviewSerializer queryset = Interview.objects.all() - # noinspection PyMethodOverriding - def retrieve(self, request: Request, pk: int) -> Response: - interview = get_object_or_404(Interview, pk=pk) - if not request.user.has_perm(SAMFUNDET_VIEW_INTERVIEW, interview): - raise PermissionDenied - return super().retrieve(request=request, pk=pk) - - # noinspection PyMethodOverriding - def list(self, request: Request) -> Response: - interviews = [interview for interview in self.get_queryset() if request.user.has_perm(SAMFUNDET_VIEW_INTERVIEW, interview)] - serializer = self.get_serializer(interviews, many=True) - return Response(serializer.data) - class RecruitmentInterviewAvailabilityView(ListCreateAPIView): model = RecruitmentInterviewAvailability @@ -1126,12 +1103,12 @@ class RecruitmentInterviewAvailabilityView(ListCreateAPIView): class RecruitmentAvailabilityView(APIView): - permission_classes = [IsAuthenticated] model = RecruitmentInterviewAvailability serializer_class = RecruitmentInterviewAvailabilitySerializer def get(self, request: Request, **kwargs: int) -> Response: - availability = get_object_or_404(RecruitmentInterviewAvailability, recruitment__id=kwargs.get('id')) + recruitment = kwargs.get('id') + availability = get_object_or_404(RecruitmentInterviewAvailability, recruitment__id=recruitment) start_time = availability.start_time end_time = availability.end_time From d9b76574fd3dbf3eec131f64b589c97c6c09131e Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 6 Sep 2024 03:59:35 +0200 Subject: [PATCH 27/32] ruff --- backend/samfundet/admin.py | 1 - backend/samfundet/tests/test_roles.py | 175 +++++++++++++++----------- backend/samfundet/views.py | 1 - 3 files changed, 105 insertions(+), 72 deletions(-) diff --git a/backend/samfundet/admin.py b/backend/samfundet/admin.py index 0e622c9e9..3df2c2de2 100644 --- a/backend/samfundet/admin.py +++ b/backend/samfundet/admin.py @@ -22,7 +22,6 @@ ) from .models.role import Role, UserOrgRole, UserGangRole, UserGangSectionRole - from .models.event import Event, EventGroup, EventRegistration, PurchaseFeedbackModel from .models.general import ( Tag, diff --git a/backend/samfundet/tests/test_roles.py b/backend/samfundet/tests/test_roles.py index 863067e58..41320e517 100644 --- a/backend/samfundet/tests/test_roles.py +++ b/backend/samfundet/tests/test_roles.py @@ -8,23 +8,23 @@ def test_has_perm_superuser( - fixture_role_auth_backend: RoleAuthBackend, fixture_superuser: User, fixture_organization: Organization, - fixture_org_permission: Permission + fixture_role_auth_backend: RoleAuthBackend, fixture_superuser: User, fixture_organization: Organization, fixture_org_permission: Permission ): """Test that superusers have permissions to all resources even without any roles.""" assert fixture_role_auth_backend.has_perm(fixture_superuser, fixture_org_permission.codename, fixture_organization) def test_has_perm_inactive_user( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_organization: Organization, + fixture_role_auth_backend: RoleAuthBackend, + fixture_user: User, + fixture_organization: Organization, fixture_org_permission: Permission, ): assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) def test_has_perm_inactive_user_with_role( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_organization: Organization, - fixture_org_permission: Permission, fixture_role: Role + fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_organization: Organization, fixture_org_permission: Permission, fixture_role: Role ): """Test that inactive users who would otherwise have access to a resource, don't.""" fixture_role.permissions.add(fixture_org_permission) @@ -33,20 +33,25 @@ def test_has_perm_inactive_user_with_role( assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) -def test_has_perm_no_obj(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, - fixture_org_permission: Permission): +def test_has_perm_no_obj(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_org_permission: Permission): assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, None) -def test_has_perm_superuser_no_obj(fixture_role_auth_backend: RoleAuthBackend, fixture_superuser: User, - fixture_org_permission: Permission): +def test_has_perm_superuser_no_obj(fixture_role_auth_backend: RoleAuthBackend, fixture_superuser: User, fixture_org_permission: Permission): assert not fixture_role_auth_backend.has_perm(fixture_superuser, fixture_org_permission.codename, None) -def test_has_perm_user_with_no_roles(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, - fixture_organization: Organization, fixture_gang: Gang, - fixture_gang_section: GangSection, fixture_org_permission: Permission, - fixture_gang_permission: Permission, fixture_gang_section_permission: Permission): +def test_has_perm_user_with_no_roles( + fixture_role_auth_backend: RoleAuthBackend, + fixture_user: User, + fixture_role: Role, + fixture_organization: Organization, + fixture_gang: Gang, + fixture_gang_section: GangSection, + fixture_org_permission: Permission, + fixture_gang_permission: Permission, + fixture_gang_section_permission: Permission, +): """Sanity check. Within the scope of our auth backend, a user with no roles should have no permissions, on any hierarchical level.""" @@ -57,12 +62,12 @@ def test_has_perm_user_with_no_roles(fixture_role_auth_backend: RoleAuthBackend, assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, - fixture_gang_section) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) -def test_has_perm_user_with_org_role(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, - fixture_organization: Organization, fixture_org_permission: Permission): +def test_has_perm_user_with_org_role( + fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, fixture_organization: Organization, fixture_org_permission: Permission +): """Test that giving user an OrgRole with permission to access a resource, actually gives them access.""" fixture_role.permissions.add(fixture_org_permission) UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) @@ -70,8 +75,9 @@ def test_has_perm_user_with_org_role(fixture_role_auth_backend: RoleAuthBackend, assert fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) -def test_has_perm_user_with_gang_role(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, - fixture_role: Role, fixture_gang: Gang, fixture_gang_permission: Permission): +def test_has_perm_user_with_gang_role( + fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, fixture_gang: Gang, fixture_gang_permission: Permission +): """Test that giving user a GangRole with permission to access a resource, actually gives them access.""" fixture_role.permissions.add(fixture_gang_permission) UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) @@ -79,20 +85,28 @@ def test_has_perm_user_with_gang_role(fixture_role_auth_backend: RoleAuthBackend assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) -def test_has_perm_user_with_section_role(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, - fixture_role: Role, fixture_gang_section: GangSection, - fixture_gang_section_permission: Permission): +def test_has_perm_user_with_section_role( + fixture_role_auth_backend: RoleAuthBackend, + fixture_user: User, + fixture_role: Role, + fixture_gang_section: GangSection, + fixture_gang_section_permission: Permission, +): """Test that giving user a GangSectionRole with permission to access a resource, actually gives them access.""" fixture_role.permissions.add(fixture_gang_section_permission) UserGangSectionRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang_section) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, - fixture_gang_section) + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) -def test_has_perm_different_orgs(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, - fixture_organization: Organization, fixture_organization2: Organization, - fixture_org_permission: Permission, fixture_role: Role): +def test_has_perm_different_orgs( + fixture_role_auth_backend: RoleAuthBackend, + fixture_user: User, + fixture_organization: Organization, + fixture_organization2: Organization, + fixture_org_permission: Permission, + fixture_role: Role, +): """Test that giving user a role to a specific org, does not give it to other orgs""" fixture_role.permissions.add(fixture_org_permission) @@ -104,9 +118,14 @@ def test_has_perm_different_orgs(fixture_role_auth_backend: RoleAuthBackend, fix assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization2) -def test_has_perm_different_gangs(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, - fixture_gang: Gang, fixture_gang2: Gang, - fixture_gang_permission: Permission, fixture_role: Role): +def test_has_perm_different_gangs( + fixture_role_auth_backend: RoleAuthBackend, + fixture_user: User, + fixture_gang: Gang, + fixture_gang2: Gang, + fixture_gang_permission: Permission, + fixture_role: Role, +): """Test that giving user a role to a specific gang, does not give it to other gangs""" fixture_role.permissions.add(fixture_gang_permission) @@ -118,28 +137,37 @@ def test_has_perm_different_gangs(fixture_role_auth_backend: RoleAuthBackend, fi assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang2) -def test_has_perm_different_gang_sections(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, - fixture_gang_section: GangSection, fixture_gang_section2: Gang, - fixture_gang_section_permission: Permission, fixture_role: Role): +def test_has_perm_different_gang_sections( + fixture_role_auth_backend: RoleAuthBackend, + fixture_user: User, + fixture_gang_section: GangSection, + fixture_gang_section2: Gang, + fixture_gang_section_permission: Permission, + fixture_role: Role, +): """Test that giving user a role to a specific gang section, does not give it to other gang sections""" fixture_role.permissions.add(fixture_gang_section_permission) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, - fixture_gang_section) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) UserGangSectionRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang_section) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, - fixture_gang_section) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, - fixture_gang_section2) + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section2) -def test_has_perm_different_users(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_user2: User, - fixture_organization: Organization, fixture_gang: Gang, - fixture_gang_section: GangSection, fixture_org_permission: Permission, - fixture_gang_permission: Permission, fixture_gang_section_permission: Permission, - fixture_role: Role): +def test_has_perm_different_users( + fixture_role_auth_backend: RoleAuthBackend, + fixture_user: User, + fixture_user2: User, + fixture_organization: Organization, + fixture_gang: Gang, + fixture_gang_section: GangSection, + fixture_org_permission: Permission, + fixture_gang_permission: Permission, + fixture_gang_section_permission: Permission, + fixture_role: Role, +): """Test that giving user a role, does not give it to other users""" fixture_role.permissions.add(fixture_org_permission) fixture_role.permissions.add(fixture_gang_permission) @@ -151,15 +179,22 @@ def test_has_perm_different_users(fixture_role_auth_backend: RoleAuthBackend, fi assert not fixture_role_auth_backend.has_perm(fixture_user2, fixture_org_permission.codename, fixture_organization) assert not fixture_role_auth_backend.has_perm(fixture_user2, fixture_gang_permission.codename, fixture_gang) - assert not fixture_role_auth_backend.has_perm(fixture_user2, fixture_gang_section_permission.codename, - fixture_gang_section) - - -def test_has_perm_org_downward(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, - fixture_organization: Organization, fixture_organization2: Organization, - fixture_gang: Gang, fixture_gang2: Gang, fixture_gang_section: GangSection, - fixture_role: Role, fixture_org_permission: Permission, - fixture_gang_permission: Permission, fixture_gang_section_permission: Permission): + assert not fixture_role_auth_backend.has_perm(fixture_user2, fixture_gang_section_permission.codename, fixture_gang_section) + + +def test_has_perm_org_downward( + fixture_role_auth_backend: RoleAuthBackend, + fixture_user: User, + fixture_organization: Organization, + fixture_organization2: Organization, + fixture_gang: Gang, + fixture_gang2: Gang, + fixture_gang_section: GangSection, + fixture_role: Role, + fixture_org_permission: Permission, + fixture_gang_permission: Permission, + fixture_gang_section_permission: Permission, +): """Test that giving permission on org/gang level, also gives it downwards (gang/section).""" fixture_role.permissions.add(fixture_gang_section_permission) fixture_role.permissions.add(fixture_gang_permission) @@ -172,40 +207,34 @@ def test_has_perm_org_downward(fixture_role_auth_backend: RoleAuthBackend, fixtu org_role = UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, - fixture_gang_section) + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) org_role.delete() # Permissions should be gone after deleting org role assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, - fixture_gang_section) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) # Giving a user a Gang role should give the same permissions on Section level gang_role = UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, - fixture_gang_section) + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) gang_role.delete() - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, - fixture_gang_section) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) # Give the user the Org role again, and ensure that after we detach Gang and Section from the Organization, # we no longer have permissions. UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, - fixture_gang_section) + assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) fixture_gang_section.gang = fixture_gang2 - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, - fixture_gang_section) + assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) @@ -214,11 +243,17 @@ def test_has_perm_org_downward(fixture_role_auth_backend: RoleAuthBackend, fixtu assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) -def test_has_perm_section_upward(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, - fixture_organization: Organization, fixture_gang: Gang, - fixture_gang_section: GangSection, fixture_role: Role, - fixture_org_permission: Permission, fixture_gang_permission: Permission, - fixture_gang_section_permission: Permission): +def test_has_perm_section_upward( + fixture_role_auth_backend: RoleAuthBackend, + fixture_user: User, + fixture_organization: Organization, + fixture_gang: Gang, + fixture_gang_section: GangSection, + fixture_role: Role, + fixture_org_permission: Permission, + fixture_gang_permission: Permission, + fixture_gang_section_permission: Permission, +): """Test that giving permission on section/gang level, does not give it upwards (gang/org).""" fixture_role.permissions.add(fixture_org_permission) fixture_role.permissions.add(fixture_gang_permission) diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index b24e32e34..221606853 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -35,7 +35,6 @@ GITHUB_SIGNATURE_HEADER, REQUESTED_IMPERSONATE_USER, ) -from root.utils.permissions import SAMFUNDET_VIEW_INTERVIEW, SAMFUNDET_VIEW_INTERVIEWROOM from .utils import event_query, generate_timeslots, get_occupied_timeslots_from_request from .homepage import homepage From cf7edda602a7f0d09c051e04dd62d01f2a5c1f34 Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 6 Sep 2024 04:13:23 +0200 Subject: [PATCH 28/32] Remove RoleAuthBackend fixture for more readable code --- backend/samfundet/conftest.py | 6 -- backend/samfundet/tests/test_roles.py | 116 +++++++++++++------------- 2 files changed, 57 insertions(+), 65 deletions(-) diff --git a/backend/samfundet/conftest.py b/backend/samfundet/conftest.py index b6fb4aaea..8efc8ff6e 100644 --- a/backend/samfundet/conftest.py +++ b/backend/samfundet/conftest.py @@ -17,7 +17,6 @@ import root.management.commands.seed_scripts.billig as billig_seed from root.settings import BASE_DIR -from samfundet.backend import RoleAuthBackend from samfundet.constants import DEV_PASSWORD from samfundet.models.role import Role from samfundet.models.event import Event @@ -291,11 +290,6 @@ def fixture_role() -> Iterator[Role]: role.delete() -@pytest.fixture -def fixture_role_auth_backend() -> RoleAuthBackend: - return RoleAuthBackend() - - @pytest.fixture def fixture_org_permission() -> Iterator[Permission]: permission = Permission.objects.create( diff --git a/backend/samfundet/tests/test_roles.py b/backend/samfundet/tests/test_roles.py index 41320e517..859ecee98 100644 --- a/backend/samfundet/tests/test_roles.py +++ b/backend/samfundet/tests/test_roles.py @@ -7,42 +7,41 @@ from samfundet.models.role import Role, UserOrgRole, UserGangRole, UserGangSectionRole -def test_has_perm_superuser( - fixture_role_auth_backend: RoleAuthBackend, fixture_superuser: User, fixture_organization: Organization, fixture_org_permission: Permission -): +def test_has_perm_superuser(fixture_superuser: User, fixture_organization: Organization, fixture_org_permission: Permission): """Test that superusers have permissions to all resources even without any roles.""" - assert fixture_role_auth_backend.has_perm(fixture_superuser, fixture_org_permission.codename, fixture_organization) + backend = RoleAuthBackend() + assert backend.has_perm(fixture_superuser, fixture_org_permission.codename, fixture_organization) def test_has_perm_inactive_user( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_organization: Organization, fixture_org_permission: Permission, ): - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + backend = RoleAuthBackend() + assert not backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) -def test_has_perm_inactive_user_with_role( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_organization: Organization, fixture_org_permission: Permission, fixture_role: Role -): +def test_has_perm_inactive_user_with_role(fixture_user: User, fixture_organization: Organization, fixture_org_permission: Permission, fixture_role: Role): """Test that inactive users who would otherwise have access to a resource, don't.""" + backend = RoleAuthBackend() fixture_role.permissions.add(fixture_org_permission) UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) fixture_user.is_active = False - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + assert not backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) -def test_has_perm_no_obj(fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_org_permission: Permission): - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, None) +def test_has_perm_no_obj(fixture_user: User, fixture_org_permission: Permission): + backend = RoleAuthBackend() + assert not backend.has_perm(fixture_user, fixture_org_permission.codename, None) -def test_has_perm_superuser_no_obj(fixture_role_auth_backend: RoleAuthBackend, fixture_superuser: User, fixture_org_permission: Permission): - assert not fixture_role_auth_backend.has_perm(fixture_superuser, fixture_org_permission.codename, None) +def test_has_perm_superuser_no_obj(fixture_superuser: User, fixture_org_permission: Permission): + backend = RoleAuthBackend() + assert not backend.has_perm(fixture_superuser, fixture_org_permission.codename, None) def test_has_perm_user_with_no_roles( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, fixture_organization: Organization, @@ -52,6 +51,7 @@ def test_has_perm_user_with_no_roles( fixture_gang_permission: Permission, fixture_gang_section_permission: Permission, ): + backend = RoleAuthBackend() """Sanity check. Within the scope of our auth backend, a user with no roles should have no permissions, on any hierarchical level.""" @@ -60,104 +60,101 @@ def test_has_perm_user_with_no_roles( fixture_role.permissions.add(fixture_gang_permission) fixture_role.permissions.add(fixture_gang_section_permission) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert not backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + assert not backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert not backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) -def test_has_perm_user_with_org_role( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, fixture_organization: Organization, fixture_org_permission: Permission -): +def test_has_perm_user_with_org_role(fixture_user: User, fixture_role: Role, fixture_organization: Organization, fixture_org_permission: Permission): + backend = RoleAuthBackend() """Test that giving user an OrgRole with permission to access a resource, actually gives them access.""" fixture_role.permissions.add(fixture_org_permission) UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + assert backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) -def test_has_perm_user_with_gang_role( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, fixture_gang: Gang, fixture_gang_permission: Permission -): +def test_has_perm_user_with_gang_role(fixture_user: User, fixture_role: Role, fixture_gang: Gang, fixture_gang_permission: Permission): + backend = RoleAuthBackend() """Test that giving user a GangRole with permission to access a resource, actually gives them access.""" fixture_role.permissions.add(fixture_gang_permission) UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) def test_has_perm_user_with_section_role( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_role: Role, fixture_gang_section: GangSection, fixture_gang_section_permission: Permission, ): + backend = RoleAuthBackend() """Test that giving user a GangSectionRole with permission to access a resource, actually gives them access.""" fixture_role.permissions.add(fixture_gang_section_permission) UserGangSectionRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang_section) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) def test_has_perm_different_orgs( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_organization: Organization, fixture_organization2: Organization, fixture_org_permission: Permission, fixture_role: Role, ): + backend = RoleAuthBackend() """Test that giving user a role to a specific org, does not give it to other orgs""" fixture_role.permissions.add(fixture_org_permission) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + assert not backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization2) + assert backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + assert not backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization2) def test_has_perm_different_gangs( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_gang: Gang, fixture_gang2: Gang, fixture_gang_permission: Permission, fixture_role: Role, ): + backend = RoleAuthBackend() """Test that giving user a role to a specific gang, does not give it to other gangs""" fixture_role.permissions.add(fixture_gang_permission) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert not backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang2) + assert backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert not backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang2) def test_has_perm_different_gang_sections( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_gang_section: GangSection, fixture_gang_section2: Gang, fixture_gang_section_permission: Permission, fixture_role: Role, ): + backend = RoleAuthBackend() """Test that giving user a role to a specific gang section, does not give it to other gang sections""" fixture_role.permissions.add(fixture_gang_section_permission) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert not backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) UserGangSectionRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang_section) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section2) + assert backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert not backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section2) def test_has_perm_different_users( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_user2: User, fixture_organization: Organization, @@ -168,6 +165,7 @@ def test_has_perm_different_users( fixture_gang_section_permission: Permission, fixture_role: Role, ): + backend = RoleAuthBackend() """Test that giving user a role, does not give it to other users""" fixture_role.permissions.add(fixture_org_permission) fixture_role.permissions.add(fixture_gang_permission) @@ -177,13 +175,12 @@ def test_has_perm_different_users( UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) UserGangSectionRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang_section) - assert not fixture_role_auth_backend.has_perm(fixture_user2, fixture_org_permission.codename, fixture_organization) - assert not fixture_role_auth_backend.has_perm(fixture_user2, fixture_gang_permission.codename, fixture_gang) - assert not fixture_role_auth_backend.has_perm(fixture_user2, fixture_gang_section_permission.codename, fixture_gang_section) + assert not backend.has_perm(fixture_user2, fixture_org_permission.codename, fixture_organization) + assert not backend.has_perm(fixture_user2, fixture_gang_permission.codename, fixture_gang) + assert not backend.has_perm(fixture_user2, fixture_gang_section_permission.codename, fixture_gang_section) def test_has_perm_org_downward( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_organization: Organization, fixture_organization2: Organization, @@ -195,6 +192,7 @@ def test_has_perm_org_downward( fixture_gang_permission: Permission, fixture_gang_section_permission: Permission, ): + backend = RoleAuthBackend() """Test that giving permission on org/gang level, also gives it downwards (gang/section).""" fixture_role.permissions.add(fixture_gang_section_permission) fixture_role.permissions.add(fixture_gang_permission) @@ -206,45 +204,44 @@ def test_has_perm_org_downward( org_role = UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) org_role.delete() # Permissions should be gone after deleting org role - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert not backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert not backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) # Giving a user a Gang role should give the same permissions on Section level gang_role = UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) gang_role.delete() - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert not backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) # Give the user the Org role again, and ensure that after we detach Gang and Section from the Organization, # we no longer have permissions. UserOrgRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_organization) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) fixture_gang_section.gang = fixture_gang2 - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) + assert not backend.has_perm(fixture_user, fixture_gang_section_permission.codename, fixture_gang_section) - assert fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) fixture_gang.organization = fixture_organization2 - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert not backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) def test_has_perm_section_upward( - fixture_role_auth_backend: RoleAuthBackend, fixture_user: User, fixture_organization: Organization, fixture_gang: Gang, @@ -254,6 +251,7 @@ def test_has_perm_section_upward( fixture_gang_permission: Permission, fixture_gang_section_permission: Permission, ): + backend = RoleAuthBackend() """Test that giving permission on section/gang level, does not give it upwards (gang/org).""" fixture_role.permissions.add(fixture_org_permission) fixture_role.permissions.add(fixture_gang_permission) @@ -264,11 +262,11 @@ def test_has_perm_section_upward( section_role = UserGangSectionRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang_section) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + assert not backend.has_perm(fixture_user, fixture_gang_permission.codename, fixture_gang) + assert not backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) section_role.delete() UserGangRole.objects.create(user=fixture_user, role=fixture_role, obj=fixture_gang) - assert not fixture_role_auth_backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) + assert not backend.has_perm(fixture_user, fixture_org_permission.codename, fixture_organization) From dc3e9401d07a63312d76c013d08a41727960f734 Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 6 Sep 2024 04:17:29 +0200 Subject: [PATCH 29/32] Add note about return_id to docs --- docs/technical/backend/rolesystem.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/technical/backend/rolesystem.md b/docs/technical/backend/rolesystem.md index 80d037fe2..f9fd717b5 100644 --- a/docs/technical/backend/rolesystem.md +++ b/docs/technical/backend/rolesystem.md @@ -25,6 +25,10 @@ by multiple orgs/gangs/sections. > > Example: A Recruitment is owned by an organization, therefore `resolve_org` returns that organization. +These resolver methods only have a single parameter: `return_id`. The purpose of this argument is to avoid having to +unnecessarily fetch a whole instance from the database, when we only need the ID. All models which implement the +resolvers must respect this argument if possible. + Here's an example implementation of `resolve_org` for the Recruitment model: ```python From 0a456b12ac76b621eb74b6536a4f48811527932d Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 6 Sep 2024 04:24:12 +0200 Subject: [PATCH 30/32] [skip ci] Add real-world example to docs --- docs/technical/backend/rolesystem.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/technical/backend/rolesystem.md b/docs/technical/backend/rolesystem.md index f9fd717b5..aa20b89a4 100644 --- a/docs/technical/backend/rolesystem.md +++ b/docs/technical/backend/rolesystem.md @@ -14,6 +14,17 @@ Organizational level, then Gang level, and finally the Gang Section level. This permission for an object on the Organizational level, they also have it on the Gang and Gang Section levels. And if a user has it on the Gang level, they also have it on the Gang Section level. +## Real-world example + +Before we get into the technical details of the system, it's important to know how the system is used, so here's a +real-world example. + +Say we have a "Interviewer" role. This role gives permissions to view and manage interviews in a recruitment. If the +user is given this role on the Organization level, it means they are able to manage absolutely all interviews for gangs +and sections which belong to the organization. If they are given the role on the Gang level, they are able to manage all +interviews for the gang and the gang's sections. And finally, if they are given the role on the Gang Section level, they +are only able to manage interviews belonging to the gang section. + ## Organization/Gang/Section resolvers For the auth backend to know what organization an object belongs to, models need to implement From 6809a78d3420144674b2c202980a044431bbf1b6 Mon Sep 17 00:00:00 2001 From: robines Date: Fri, 6 Sep 2024 04:29:01 +0200 Subject: [PATCH 31/32] Update example to a more "normal" model --- docs/technical/backend/rolesystem.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/technical/backend/rolesystem.md b/docs/technical/backend/rolesystem.md index aa20b89a4..833e73633 100644 --- a/docs/technical/backend/rolesystem.md +++ b/docs/technical/backend/rolesystem.md @@ -32,7 +32,7 @@ the `resolve_org`/`resolve_gang`/`resolve_section` methods. The purpose of these org/gang/section the object belongs to. Models implementing these methods may not have ambiguous ownership, or be owned by multiple orgs/gangs/sections. -> Example: The Tag model doesn't implement these methods, as they aren't "owned" by anybody +> Example: The Venue model doesn't implement these methods, as they aren't "owned" by anybody > > Example: A Recruitment is owned by an organization, therefore `resolve_org` returns that organization. From 064d1818de10267d06553b34eb3001b76e953d2b Mon Sep 17 00:00:00 2001 From: robines Date: Tue, 17 Sep 2024 20:29:28 +0200 Subject: [PATCH 32/32] Update migration --- ...0002_role_usergangrole_usergangsectionrole_userorgrole.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename backend/samfundet/migrations/{0008_role_usergangrole_usergangsectionrole_userorgrole.py => 0002_role_usergangrole_usergangsectionrole_userorgrole.py} (97%) diff --git a/backend/samfundet/migrations/0008_role_usergangrole_usergangsectionrole_userorgrole.py b/backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py similarity index 97% rename from backend/samfundet/migrations/0008_role_usergangrole_usergangsectionrole_userorgrole.py rename to backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py index 792d3694d..8f72f9cd5 100644 --- a/backend/samfundet/migrations/0008_role_usergangrole_usergangsectionrole_userorgrole.py +++ b/backend/samfundet/migrations/0002_role_usergangrole_usergangsectionrole_userorgrole.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.7 on 2024-09-05 23:59 +# Generated by Django 5.0.7 on 2024-09-17 18:27 import django.db.models.deletion from django.conf import settings @@ -9,7 +9,7 @@ class Migration(migrations.Migration): dependencies = [ ('auth', '0012_alter_user_first_name_max_length'), - ('samfundet', '0007_recruitmentgangstat'), + ('samfundet', '0001_initial'), ] operations = [