Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add user roles for organizations/gangs/sections #1257

Merged
merged 40 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c40063b
Create user roles for organizations/gangs/sections
robines Jul 2, 2024
294cc28
Fix user role admin names
robines Jul 2, 2024
8e80830
Type annotations
robines Jul 2, 2024
6c55a79
Ruff format
robines Jul 2, 2024
b33fd05
Remove content_type from Role
robines Jul 4, 2024
da4dc45
Add remaining resolvers for recruitment, and org/gang/section
robines Jul 4, 2024
a6537dc
Add working examples for Interview and InterviewRoom views
robines Jul 5, 2024
d60f7ba
ruff method order
robines Jul 5, 2024
c6889fe
mypy
robines Jul 5, 2024
76a6968
Remove roles from user model
robines Jul 7, 2024
a3749f6
Merge branch 'master' into robin/permissions
robines Aug 3, 2024
95c915f
Move to samfundet app
robines Aug 3, 2024
b71140a
ruff
robines Aug 3, 2024
ae46e34
get_perm: support permissions without period
robines Aug 10, 2024
9ebfc2a
Fix hierarchy permission checking
robines Aug 13, 2024
bde4d11
Add some tests
robines Aug 13, 2024
e3bf094
Remove RoleMixin to avoid unnecessary method calls and exception rais…
robines Aug 13, 2024
5dd260e
Begin docs
robines Aug 22, 2024
37ca575
Merge branch 'master' into robin/permissions
robines Aug 22, 2024
d1ebf7f
Update migration
robines Aug 22, 2024
7117698
Add code examples to docs
robines Aug 22, 2024
57c21ba
Add link to docs
robines Aug 22, 2024
69cd012
Merge branch 'master' into robin/permissions
robines Sep 5, 2024
fa42582
Fix import merge
robines Sep 5, 2024
6f186ca
Merge branch 'master' into robin/permissions
robines Sep 5, 2024
ba2ef04
Update migrations
robines Sep 6, 2024
b8a4269
Create fixture_organization2, update fixture_gang2 to use it
robines Sep 6, 2024
75675f4
Add fixture_gang_section2
robines Sep 6, 2024
b984938
Update tests, and add hierarchy testing
robines Sep 6, 2024
3626529
Revert changes in views
robines Sep 6, 2024
d9b7657
ruff
robines Sep 6, 2024
cf7edda
Remove RoleAuthBackend fixture for more readable code
robines Sep 6, 2024
dc3e940
Add note about return_id to docs
robines Sep 6, 2024
0a456b1
[skip ci] Add real-world example to docs
robines Sep 6, 2024
6809a78
Update example to a more "normal" model
robines Sep 6, 2024
f9b8ca3
Merge branch 'master' into robin/permissions
robines Sep 13, 2024
6e0f6dd
Merge branch 'master' into robin/permissions
robines Sep 17, 2024
9bc286b
Merge branch 'master' into robin/permissions
robines Sep 17, 2024
064d181
Update migration
robines Sep 17, 2024
debe2f6
Merge branch 'master' into robin/permissions
robines Sep 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/root/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@

AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend', # default
'samfundet.backend.RoleAuthBackend',
]

# Password validation
Expand Down
20 changes: 20 additions & 0 deletions backend/root/utils/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@
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'
Expand Down Expand Up @@ -285,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'
Expand Down
24 changes: 24 additions & 0 deletions backend/root/utils/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
23 changes: 23 additions & 0 deletions backend/samfundet/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -161,6 +163,27 @@ def members(self, obj: Group) -> int:
return n


@admin.register(Role)
class RoleAdmin(admin.ModelAdmin):
list_display = ('name',)
filter_horizontal = ['permissions']


@admin.register(UserOrgRole)
class UserOrgRoleAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'obj')


@admin.register(UserGangRole)
class UserGangRoleAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'obj')


@admin.register(UserGangSectionRole)
class UserGangSectionRoleAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'obj')


@admin.register(Permission)
class PermissionAdmin(CustomGuardedModelAdmin):
# ordering = []
Expand Down
37 changes: 37 additions & 0 deletions backend/samfundet/backend.py
Original file line number Diff line number Diff line change
@@ -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 and UserOrgRole.objects.filter(user=user_obj, obj__id=org_id, role__permissions=permission).exists():
return True

if hasattr(obj, 'resolve_gang'):
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'):
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
64 changes: 63 additions & 1 deletion backend/samfundet/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -29,6 +32,7 @@
Campus,
BlogPost,
TextItem,
GangSection,
Reservation,
Organization,
MerchVariation,
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Generated by Django 5.0.6 on 2024-08-22 22:22

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


class Migration(migrations.Migration):

dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('samfundet', '0003_recruitmentseparateposition_and_more'),
]

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)),
('permissions', models.ManyToManyField(to='auth.permission')),
],
),
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,
},
),
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,
},
),
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,
},
),
]
4 changes: 4 additions & 0 deletions backend/samfundet/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
User,
Image,
Profile,
GangSection,
Organization,
UserPreference,
)

__all__ = [
'User',
'Gang',
'GangSection',
'Organization',
'Event',
'Image',
'Profile',
Expand Down
30 changes: 30 additions & 0 deletions backend/samfundet/models/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,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

Expand Down Expand Up @@ -329,6 +334,17 @@ class Meta:
verbose_name = 'Gang'
verbose_name_plural = 'Gangs'

def resolve_org(self, *, return_id: bool = False) -> Organization | int:
if return_id:
# noinspection PyTypeChecker
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}'

Expand All @@ -339,6 +355,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}'

Expand Down
Loading
Loading