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 9 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
2 changes: 2 additions & 0 deletions backend/root/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'corsheaders',
'root', # Register to enable management.commands.
'samfundet',
'samfundet.roleauth',
robines marked this conversation as resolved.
Show resolved Hide resolved
]

MIDDLEWARE = [
Expand Down Expand Up @@ -129,6 +130,7 @@

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

# Password validation
Expand Down
38 changes: 36 additions & 2 deletions backend/root/utils/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -15,6 +15,9 @@

LOG = logging.getLogger(__name__)

if TYPE_CHECKING:
from samfundet.models import Gang, GangSection, Organization


class FieldTrackerMixin(Model):
"""
Expand Down Expand Up @@ -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.
Expand Down
54 changes: 53 additions & 1 deletion backend/root/utils/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""

############################################################
Expand Down Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
24 changes: 24 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 @@ -132,6 +134,7 @@ def group_memberships(self, obj: User) -> int:
'is_staff',
'is_superuser',
'groups',
'roles',
'user_permissions',
),
},
Expand Down Expand Up @@ -161,6 +164,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Generated by Django 5.0.6 on 2024-07-04 13:42

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'),
('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)),
('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),
),
]
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
Loading
Loading