diff --git a/aai/apps.py b/aai/apps.py index bd5f7be7e18346b0cac7535a603ae6e39b5434dd..01fcb37054d5d5c6aa89db03a1f7fc1ed8d0f863 100644 --- a/aai/apps.py +++ b/aai/apps.py @@ -4,6 +4,3 @@ from django.apps import AppConfig class AaiConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "aai" - - def ready(self): - import aai.management.commands.assignperms diff --git a/aai/backend.py b/aai/backend.py index 40928fb54afb42f1ab1ee6f04381562803e0e370..fc247c7afb89a4bde9b0b23c1b2dbd0f4faf66d8 100644 --- a/aai/backend.py +++ b/aai/backend.py @@ -21,7 +21,7 @@ class CustomAuthBackend(ModelBackend): ) raise AuthenticationFailed("Username or password is missing") try: - user = UserModel._default_manager.get_by_natural_key(username) + user: User = UserModel._default_manager.get_by_natural_key(username) except UserModel.DoesNotExist: # Run the default password hasher once to reduce the timing # difference between an existing and a nonexistent user. diff --git a/aai/management/__init__.py b/aai/management/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/aai/management/commands/__init__.py b/aai/management/commands/__init__.py deleted file mode 100644 index 87f235c0666d98aff19c03802b176f78bbf740f4..0000000000000000000000000000000000000000 --- a/aai/management/commands/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = "aai.apps.AaiConfig" diff --git a/aai/management/commands/assignperms.py b/aai/management/commands/assignperms.py deleted file mode 100644 index 35c77c423c5c450f5462788b4b6ab473dfa3474e..0000000000000000000000000000000000000000 --- a/aai/management/commands/assignperms.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import Any -import sys - -from django.core.management.base import BaseCommand -from django.contrib.auth.models import Permission, Group -from django.db.models.signals import post_migrate -from django.dispatch import receiver - -from aai.models import UserGroup, Perms - -migrated_apps = set() -required_migrations = {"user", "aai", "django.contrib.auth"} -DONE = False - - -# Due to unit tests pipeline this has to be temporarly automatic -@receiver(post_migrate) -def assign_perms_after_migration(sender, **kwargs): - global DONE, migrated_apps, required_migrations - if DONE: - return - - migrated_apps.add(sender.name) - if required_migrations.issubset(migrated_apps): - assign_perms() - DONE = True - - -def assign_perms(): - # Initialize groups - trainee, _ = Group.objects.get_or_create(name=UserGroup.TRAINEE) - instructor, _ = Group.objects.get_or_create(name=UserGroup.INSTRUCTOR) - _, _ = Group.objects.get_or_create(name=UserGroup.ADMIN) - - #### trainee permissions assignemnt #### - trainee_perm_names = [ - Perms.view_exercise.codename, - Perms.use_tool.codename, - Perms.send_email.codename, - Perms.view_trainee_info.codename, - Perms.view_email_info.codename, - Perms.view_email.codename, - Perms.manipulate_file.codename, - ] - trainee_perms = Permission.objects.filter(codename__in=trainee_perm_names) - trainee.permissions.add(*trainee_perms) - instructor.permissions.add(*trainee_perms) - trainee.save() - - #### instructor permissions assignment #### - instructor_perm_names = [ - Perms.update_exercise.codename, - Perms.update_definition.codename, - Perms.view_definition.codename, - Perms.view_category.codename, - Perms.view_milestone.codename, - Perms.send_injectselection.codename, - Perms.view_extendtool.codename, - Perms.view_injectselection.codename, - Perms.view_analytics.codename, - Perms.update_userassignment.codename, - Perms.view_user.codename, - Perms.update_user.codename, - ] - instructor_perms = Permission.objects.filter( - codename__in=instructor_perm_names - ) - instructor.permissions.add(*instructor_perms) - instructor.save() - - sys.stderr.write("Predefined groups and permissions have been set in DB\n") - - -class Command(BaseCommand): - help = "Populate database with groups and assigns permissions" - - def handle(self, *args: Any, **options: Any): - assign_perms() diff --git a/aai/migrations/0004_delete_perms.py b/aai/migrations/0004_delete_perms.py new file mode 100644 index 0000000000000000000000000000000000000000..176e09fe9ab7018bd85e9f15d7d98ccc73473fb9 --- /dev/null +++ b/aai/migrations/0004_delete_perms.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.22 on 2024-07-10 15:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aai', '0003_alter_perms_options'), + ] + + operations = [ + migrations.DeleteModel( + name='Perms', + ), + ] diff --git a/aai/models.py b/aai/models.py deleted file mode 100644 index 3225491129fb39134fda2506f18f4559dc74cf5b..0000000000000000000000000000000000000000 --- a/aai/models.py +++ /dev/null @@ -1,118 +0,0 @@ -from typing import Optional -from enum import Enum - -from django.db import models -from django.contrib.contenttypes.models import ContentType -from django.contrib.auth.models import Group - - -class UserGroup(str, Enum): - TRAINEE = "trainee" - INSTRUCTOR = "instructor" - ADMIN = "admin" - - @staticmethod - def from_str(group: str) -> Optional["UserGroup"]: - lowered_group = group.lower() - if lowered_group == "trainee" or lowered_group == "t": - return UserGroup.TRAINEE - elif lowered_group == "instructor" or lowered_group == "i": - return UserGroup.INSTRUCTOR - elif lowered_group == "admin" or lowered_group == "a": - return UserGroup.ADMIN - return None - - def __eq__(self, other): - if isinstance(other, Group): - return self.value == other.name - return super().__eq__(other) - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self) -> int: - return super().__hash__() - - -class NameHandler: - full_name: str - codename: str - - def __init__(self, full_name): - self.full_name = full_name - self.codename = self.full_name.split(".")[1] - - def __get__(self, instance, owner): - return self - - -class Perms(models.Model): - update_exercise = NameHandler("aai.update_exercise") - view_exercise = NameHandler("aai.view_exercise") - update_definition = NameHandler("aai.update_definition") - view_definition = NameHandler("aai.view_definition") - view_category = NameHandler("aai.view_category") - view_milestone = NameHandler("aai.view_milestone") - use_tool = NameHandler("aai.use_tool") - send_injectselection = NameHandler("aai.send_injectselection") - send_email = NameHandler("aai.send_email") - view_trainee_info = NameHandler("aai.view_trainee_info") - view_extendtool = NameHandler("aai.view_extendtool") - view_injectselection = NameHandler("aai.view_injectselection") - view_email_info = NameHandler("aai.view_email_info") - view_email = NameHandler("aai.view_email") - view_analytics = NameHandler("aai.view_analytics") - update_userassignment = NameHandler("aai.update_userassignment") - view_user = NameHandler("aai.view_user") - manipulate_file = NameHandler("aai.manipulate_file") - update_user = NameHandler("aai.update_user") - delete_user = NameHandler("aai.delete_user") # only for admin - export_import = NameHandler("aai.export_import") - - class Meta: - default_permissions = () - permissions = [ - ( - "update_exercise", - "Can access instructor tools for exercise manipulation", - ), - ("view_exercise", "Can view exercise and their info"), - ("update_definition", "Can add/delete/change definition"), - ("view_definition", "Can view definition"), - ("view_category", "Can view inject categories"), - ("view_milestone", "Can view milestones"), - ("use_tool", "Can use tool of the exercise"), - ("send_injectselection", "Can pick and send inject selection"), - ( - "send_email", - "Can send email and execute email related operations", - ), - ( - "view_trainee_info", - "Can view exercise info intedned to trainees (roles, tools...)", - ), - ("view_extendtool", "Can view extend tool (with responses)"), - ("view_injectselection", "Can view inject selecion options"), - ( - "view_email_info", - "Can view info related to emails (contacts, addresses...)", - ), - ("view_email", "Can view email bodies and threads"), - ("view_analytics", "Can view data needed for analytics dashboard"), - ( - "update_userassignment", - "Can (un)assign user to exercise or team", - ), - ("view_user", "Can view users in database"), - ( - "manipulate_file", - "Can upload and download files during exercise", - ), - ("update_user", "Can add/remove/change user"), - ("delete_user", "Can delete user"), - ("export_import", "Can export and import database"), - ] - - @classmethod - def content_type(cls): - return ContentType.objects.get_for_model(Perms) diff --git a/aai/tests/aai_utils_tests.py b/aai/tests/aai_utils_tests.py index 8a48c4e8a2d345ab214102ce1e0c3bad730e4d23..ee012b71a15a793196d24157f3dcf46909ccc5d7 100644 --- a/aai/tests/aai_utils_tests.py +++ b/aai/tests/aai_utils_tests.py @@ -7,7 +7,6 @@ from rest_framework.exceptions import PermissionDenied, AuthenticationFailed from django.test import TestCase, override_settings from rest_framework.test import APIRequestFactory -from aai.models import Perms from aai.utils import protected, extra_protected, Check from common_lib.test_utils import ( internal_create_exercise, @@ -88,7 +87,6 @@ class AaiUtilsTests(TestCase): cls.factory = APIRequestFactory() cls.assignUsers() - cls.assignPerms() cls.createData() @classmethod @@ -108,15 +106,6 @@ class AaiUtilsTests(TestCase): # Assign definition to the instructor cls.instructor.definitions.add(cls.definition) - @classmethod - def assignPerms(cls): - p = Permission.objects.get(codename=Perms.view_exercise.codename) - cls.trainee1.group.permissions.add(p) - cls.instructor.group.permissions.add(p) - - p = Permission.objects.get(codename=Perms.update_exercise.codename) - cls.instructor.group.permissions.add(p) - @classmethod def createData(cls): # create action log @@ -170,9 +159,7 @@ class AaiUtilsTests(TestCase): return 0 def test_protected_admin(self): - decorated = protected("aai.non_existent")( - self.dummy - ) # admin has access + decorated = protected(User.AuthGroup.ADMIN)(self.dummy) request = self.factory.request() request.user = self.trainee1 @@ -187,7 +174,7 @@ class AaiUtilsTests(TestCase): self.assertEqual(decorated(request), 0) def test_protected_instructor(self): - decorated = protected(Perms.update_exercise.full_name)(self.dummy) + decorated = protected(User.AuthGroup.INSTRUCTOR)(self.dummy) request = self.factory.request() request.user = self.trainee1 @@ -201,7 +188,7 @@ class AaiUtilsTests(TestCase): self.assertEqual(decorated(request), 0) def test_protected_trainee(self): - decorated = protected(Perms.view_exercise.full_name)(self.dummy) + decorated = protected(User.AuthGroup.TRAINEE)(self.dummy) request = self.factory.request() request.user = self.trainee1 @@ -214,7 +201,7 @@ class AaiUtilsTests(TestCase): self.assertEqual(decorated(request), 0) def test_protected_anonoymous(self): - decorated = protected(Perms.view_exercise.full_name)(self.dummy) + decorated = protected(User.AuthGroup.TRAINEE)(self.dummy) request = self.factory.request() request.user = AnonymousUser() diff --git a/aai/utils.py b/aai/utils.py index 1ae550be47ce464535c648fa4766a88c70af5b5a..c9b7286f8ba85c57e885934a6a0f9592baa252a2 100644 --- a/aai/utils.py +++ b/aai/utils.py @@ -7,7 +7,6 @@ from graphene import ResolveInfo from rest_framework.exceptions import AuthenticationFailed, PermissionDenied from rest_framework.request import HttpRequest, Request -from aai.models import UserGroup from common_lib.logger import logger from common_lib.utils import get_model, InputObject from exercise.graphql_inputs import CreateExerciseInput @@ -168,7 +167,7 @@ def _check_thread(user: User, thread_id) -> bool: def _check_visible_only(user: User, visible_only: bool) -> bool: - return visible_only or user.group != UserGroup.TRAINEE + return visible_only or user.group != User.AuthGroup.TRAINEE CHECK_MAPPING: Dict[Check, Callable] = { @@ -191,13 +190,13 @@ def logged_in(func): return wrapper -def protected(required_permission: str): +def protected(required_group: User.AuthGroup): """ - Decorator that checks whether user of request has required permission. + Decorator that checks whether user of request has required authorization group. If not, PermissionDenied exception is raised. Args: - required_permission: name of the permission in form "app_label.codename" + required_group: minimal required authorization group to access endpoint """ def decorator(func): @@ -218,7 +217,7 @@ def protected(required_permission: str): if user.is_anonymous: raise AuthenticationFailed("Authentication failed") - if user.has_perm(required_permission): + if user.group >= required_group: return func(*args, **kwargs) raise PermissionDenied("Permission denied") diff --git a/common_lib/schema_types.py b/common_lib/schema_types.py index fa0626986c40f3b824075dcb4fa5776a4d926893..1068392c062842b4b18f90c091f4f52779b40869 100644 --- a/common_lib/schema_types.py +++ b/common_lib/schema_types.py @@ -3,7 +3,6 @@ from django.db import models from graphene_django import DjangoObjectType from django.conf import settings -from aai.models import UserGroup from exercise.models import ( Exercise, Team, @@ -45,7 +44,7 @@ from running_exercise.models import ( LogType, QuestionnaireAnswer, ) -from user.models import User, Tag, Group +from user.models import User, Tag class RestrictedUser(DjangoObjectType): @@ -110,7 +109,7 @@ class ExerciseType(DjangoObjectType): def resolve_user_set(self, info): if settings.NOAUTH: pass - elif info.context.user.group == UserGroup.TRAINEE: + elif info.context.user.group == User.AuthGroup.TRAINEE: return User.objects.none() return self.user_set.all() @@ -124,7 +123,7 @@ class TeamType(DjangoObjectType): def resolve_user_set(self, info): if settings.NOAUTH: pass - elif info.context.user.group == UserGroup.TRAINEE: + elif info.context.user.group == User.AuthGroup.TRAINEE: return User.objects.none() return self.user_set.all() @@ -181,7 +180,7 @@ class QuestionType(DjangoObjectType): if settings.NOAUTH: return self.correct - if user.group == UserGroup.TRAINEE: + if user.group == User.AuthGroup.TRAINEE: return 0 return self.correct @@ -329,7 +328,7 @@ class UserType(DjangoObjectType): return self.teams.all() def resolve_group(self, info): - return self.group + return User.AuthGroup(self.group).name.lower() class TagType(DjangoObjectType): @@ -338,12 +337,6 @@ class TagType(DjangoObjectType): exclude_fields = ("user_set",) -class GroupType(DjangoObjectType): - class Meta: - model = Group - exclude_fields = ("user_set",) - - class ExerciseEventTypeEnum(models.TextChoices): CREATE = "create" MODIFY = "modify" @@ -381,7 +374,7 @@ class QuestionnaireAnswerType(DjangoObjectType): if user.is_anonymous: return self.is_correct - if user.group == UserGroup.TRAINEE: + if user.group == User.AuthGroup.TRAINEE: return None return self.is_correct diff --git a/exercise/schema/mutation.py b/exercise/schema/mutation.py index 7d1e16dcd8384e6ee36a29b3e47498d67ea5be84..ba2f8e2cc5e5740fe38b7c842700ed8858776d78 100644 --- a/exercise/schema/mutation.py +++ b/exercise/schema/mutation.py @@ -6,10 +6,9 @@ from common_lib.schema_types import ExerciseType from exercise.graphql_inputs import CreateExerciseInput from exercise.lib.exercise_manager import ExerciseManager from exercise_definition.lib.definition_manager import DefinitionManager -from aai.models import Perms from aai.utils import protected, extra_protected, input_object_protected, Check from user.schema.validators import validate_instructor_assigning -from user.models import InstructorOfExercise +from user.models import InstructorOfExercise, User class CreateExerciseMutation(graphene.Mutation): @@ -21,7 +20,7 @@ class CreateExerciseMutation(graphene.Mutation): ) @classmethod - @protected(Perms.update_exercise.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @input_object_protected("create_exercise_input") def mutate( cls, @@ -50,7 +49,7 @@ class DeleteExerciseMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.update_exercise.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def mutate(cls, root, info, exercise_id: str): ExerciseManager.delete_exercise(int(exercise_id)) @@ -64,7 +63,7 @@ class DeleteDefinitionMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.update_definition.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.DEFINITION_ID) def mutate(cls, root, info, definition_id: str): DefinitionManager.delete_definition(int(definition_id)) diff --git a/exercise/schema/query.py b/exercise/schema/query.py index 2331a46c247293fa7705b94a8d9c4e229cfd33d3..64bc2b78c46248f4ffac8bfc76deed36ca8af528 100644 --- a/exercise/schema/query.py +++ b/exercise/schema/query.py @@ -20,7 +20,7 @@ from exercise_definition.models import ( FileInfo, Channel, ) -from aai.models import Perms, UserGroup +from user.models import User from aai.utils import protected, extra_protected, Check @@ -94,7 +94,7 @@ class Query(graphene.ObjectType): description="Retrieve all channels for an exercise", ) - @protected(Perms.view_exercise.full_name) + @protected(User.AuthGroup.TRAINEE) def resolve_exercises( self, info, @@ -108,11 +108,11 @@ class Query(graphene.ObjectType): if settings.NOAUTH: exercises = Exercise.objects.all() else: - if user.group == UserGroup.TRAINEE: + if user.group == User.AuthGroup.TRAINEE: exercises = Exercise.objects.filter(teams__users__user=user) - elif user.group == UserGroup.INSTRUCTOR: + elif user.group == User.AuthGroup.INSTRUCTOR: exercises = user.exercises.all() - elif user.group == UserGroup.ADMIN: + elif user.group == User.AuthGroup.ADMIN: exercises = Exercise.objects.all() else: return Exercise.objects.none() @@ -131,31 +131,30 @@ class Query(graphene.ObjectType): return exercises - @protected(Perms.view_exercise.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.EXERCISE_ID) def resolve_exercise_id(self, info, exercise_id: str) -> Optional[Exercise]: return Exercise.objects.filter(id=exercise_id).first() - @protected(Perms.view_definition.full_name) + @protected(User.AuthGroup.INSTRUCTOR) def resolve_definitions(self, info) -> List[Definition]: if settings.NOAUTH: return Definition.objects.all() user = info.context.user # User dependant data resolving - if user.group == UserGroup.INSTRUCTOR: + if user.group == User.AuthGroup.INSTRUCTOR: return Definition.objects.filter(maintainers__user=user) - elif user.group == UserGroup.ADMIN: + elif user.group == User.AuthGroup.ADMIN: return Definition.objects.all() - else: # UserGroup.TRAINEE and unknown groups can't see definitions - return [] + return [] - @protected(Perms.view_definition.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.DEFINITION_ID) def resolve_definition(self, info, definition_id: str) -> Definition: return get_model(Definition, id=definition_id) - @protected(Perms.view_category.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def resolve_auto_injects(self, info, exercise_id: str) -> List[Inject]: exercise = get_model( @@ -167,7 +166,7 @@ class Query(graphene.ObjectType): definition_id=exercise.definition.id, auto=True ) - @protected(Perms.view_milestone.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def resolve_milestones(self, info, exercise_id: str) -> List[Milestone]: exercise = get_model( @@ -177,20 +176,20 @@ class Query(graphene.ObjectType): return Milestone.objects.filter(definition_id=exercise.definition.id) - @protected(Perms.view_trainee_info.full_name) + @protected(User.AuthGroup.TRAINEE) def resolve_file_info(self, info, file_info_id: str) -> FileInfo: return get_model(FileInfo, id=file_info_id) - @protected(Perms.view_trainee_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.TEAM_ID) def resolve_team_uploaded_files(self, info, team_id: str) -> List[FileInfo]: return FileInfo.objects.filter(team__team_id=team_id) - @protected(Perms.view_trainee_info.full_name) + @protected(User.AuthGroup.TRAINEE) def resolve_channel(self, info, channel_id: str) -> Channel: return get_model(Channel, id=channel_id) - @protected(Perms.view_trainee_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.EXERCISE_ID) def resolve_exercise_channels( self, info, exercise_id: str diff --git a/exercise/subscription.py b/exercise/subscription.py index 433a42d04e39493c48e5fd389c3f0eb022dd8c57..9f7c91c016b2c7c2fb21175ab65a99e04b69fb67 100644 --- a/exercise/subscription.py +++ b/exercise/subscription.py @@ -3,7 +3,7 @@ import graphene from common_lib.schema_types import ExerciseEventTypeEnum from common_lib.schema_types import ExerciseType -from aai.models import Perms +from user.models import User from aai.utils import protected NOTIFICATION_QUEUE_LIMIT = 64 @@ -16,7 +16,7 @@ class ExercisesSubscription(channels_graphql_ws.Subscription): event_type = graphene.Field(graphene.Enum.from_enum(ExerciseEventTypeEnum)) @staticmethod - @protected(Perms.view_exercise.full_name) + @protected(User.AuthGroup.TRAINEE) def subscribe(root, info): return ["all"] diff --git a/exercise/views.py b/exercise/views.py index f16dda9594a46ca747a9c9bdb480e35339393189..a2f6354a4c792a5ed7e0b05c744d14da74a01075 100644 --- a/exercise/views.py +++ b/exercise/views.py @@ -6,7 +6,7 @@ from rest_framework import parsers from rest_framework.response import Response from rest_framework.views import APIView -from aai.models import Perms +from user.models import User from aai.utils import protected, extra_protected, Check from common_lib.exceptions import ApiException from exercise.lib.export_import import export_database, import_database @@ -14,7 +14,7 @@ from exercise.lib.log_manager import LogManager class RetrieveExerciseLogsView(APIView): - @protected(Perms.view_analytics.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def get(self, request, *args, **kwargs): """ @@ -44,7 +44,7 @@ class BackendVersionView(APIView): class ExportImportView(APIView): - @protected(Perms.export_import.full_name) + @protected(User.AuthGroup.ADMIN) def get(self, request, *args, **kwargs): """ Get the exported file @@ -53,7 +53,7 @@ class ExportImportView(APIView): parser_classes = [parsers.MultiPartParser] - @protected(Perms.export_import.full_name) + @protected(User.AuthGroup.ADMIN) def post(self, request, *args, **kwargs): """ Import data into the database based on the imported zip file diff --git a/exercise_definition/views.py b/exercise_definition/views.py index 32aaed2970b2d2d445e6384b5d3b7a6b1802d716..a230998f5fbf25b888ae9844da3db4582ee688bd 100644 --- a/exercise_definition/views.py +++ b/exercise_definition/views.py @@ -6,19 +6,18 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView -from aai.models import Perms from aai.utils import protected from common_lib.exceptions import ApiException from common_lib.logger import logger, log_user_msg from exercise_definition.lib import DefinitionUploader, DefinitionParser from exercise_definition.lib.definition_validator import DefinitionValidator -from user.models import DefinitionAccess +from user.models import DefinitionAccess, User class UploadDefinitionView(APIView): parser_classes = [parsers.MultiPartParser] - @protected(Perms.update_definition.full_name) + @protected(User.AuthGroup.INSTRUCTOR) def post(self, request: Request, *args, **kwargs): """Upload an exercise definition file and check it for correctness.""" uploaded_file = request.FILES.get("file") @@ -52,7 +51,7 @@ class UploadDefinitionView(APIView): class ValidateDefinition(APIView): parser_classes = [parsers.MultiPartParser] - @protected(Perms.update_definition.full_name) + @protected(User.AuthGroup.INSTRUCTOR) def post(self, request: Request, *args, **kwargs): """Validate the uploaded definition""" uploaded_file = request.FILES.get("file") diff --git a/running_exercise/schema/mutation.py b/running_exercise/schema/mutation.py index 72dd953c3059d44ea6137d2d641ffda50df56e50..a9681e4bda633d4f5203994650f310c30838a72b 100644 --- a/running_exercise/schema/mutation.py +++ b/running_exercise/schema/mutation.py @@ -3,7 +3,7 @@ from typing import List import graphene from django.conf import settings -from aai.models import Perms +from user.models import User from aai.utils import protected, extra_protected, input_object_protected, Check from common_lib.logger import logger from common_lib.schema_types import ExerciseType, EmailThreadType @@ -34,7 +34,7 @@ class UseToolMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.use_tool.full_name) + @protected(User.AuthGroup.TRAINEE) @input_object_protected("use_tool_input") def mutate( cls, @@ -56,7 +56,7 @@ class SelectTeamInjectOptionMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.send_injectselection.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @input_object_protected("select_team_inject_input") def mutate( cls, root, info, select_team_inject_input: SelectTeamInjectInput @@ -81,7 +81,7 @@ class CreateThreadMutation(graphene.Mutation): thread = graphene.Field(EmailThreadType) @classmethod - @protected(Perms.send_email.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.EXERCISE_ID) def mutate( cls, @@ -105,7 +105,7 @@ class SendEmailMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.send_email.full_name) + @protected(User.AuthGroup.TRAINEE) @input_object_protected("send_email_input") def mutate( cls, @@ -126,7 +126,7 @@ class MoveExerciseTimeMutation(graphene.Mutation): exercise = graphene.Field(ExerciseType) @classmethod - @protected(Perms.update_exercise.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def mutate(cls, root, info, exercise_id: str, time_diff: int): exercise = ExerciseLoop.move_time(int(exercise_id), time_diff) @@ -140,7 +140,7 @@ class StartExerciseMutation(graphene.Mutation): exercise = graphene.Field(ExerciseType) @classmethod - @protected(Perms.update_exercise.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def mutate(cls, root, info, exercise_id: str): exercise = ExerciseLoop.start(int(exercise_id)) @@ -154,7 +154,7 @@ class StopExerciseMutation(graphene.Mutation): exercise = graphene.Field(ExerciseType) @classmethod - @protected(Perms.update_exercise.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def mutate(cls, root, info, exercise_id: str): exercise = ExerciseLoop.stop(int(exercise_id)) @@ -174,7 +174,7 @@ class ModifyMilestoneMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.update_exercise.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.TEAM_ID) def mutate(cls, root, info, team_id: str, milestone: str, activate: bool): instructor_modify_milestone( @@ -192,9 +192,8 @@ class AnswerQuestionnaireMutation(graphene.Mutation): operation_done = graphene.Boolean() - # using send email because we do not have a "trainee action" permission @classmethod - @protected(Perms.send_email.full_name) + @protected(User.AuthGroup.TRAINEE) def mutate(cls, root, info, quest_input: QuestionnaireInput): QuestionnaireHandler.answer_questionnaire(quest_input) return AnswerQuestionnaireMutation(operation_done=True) diff --git a/running_exercise/schema/query.py b/running_exercise/schema/query.py index c7560e567edbb56d945ae0bbd095480ac997665f..503fb66e488e82b465521ceaca69395201125892 100644 --- a/running_exercise/schema/query.py +++ b/running_exercise/schema/query.py @@ -3,7 +3,7 @@ from typing import List import graphene from django.db.models import QuerySet -from aai.models import Perms +from user.models import User from aai.utils import protected, extra_protected, Check from common_lib.exceptions import RunningExerciseOperationException from common_lib.schema_types import ( @@ -206,12 +206,12 @@ class Query(graphene.ObjectType): description="Retrieve the learning objectives for the specific team", ) - @protected(Perms.view_trainee_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.TEAM_ID) def resolve_team(self, info, team_id: str) -> Team: return get_model(Team, id=int(team_id)) - @protected(Perms.view_trainee_info.full_name) + @protected(User.AuthGroup.TRAINEE) def resolve_team_roles(self, info) -> List[str]: # TODO: remove dependency on "get_running_exercise", prefer exercise_id parameter running_exercise = get_running_exercise() @@ -219,7 +219,7 @@ class Query(graphene.ObjectType): raise RunningExerciseOperationException("No exercise is running.") return [team.role for team in running_exercise.teams.all()] - @protected(Perms.view_trainee_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.TEAM_ID) def resolve_team_tools(self, info, team_id: str) -> List[Tool]: team = get_model(Team, id=int(team_id)) @@ -230,7 +230,7 @@ class Query(graphene.ObjectType): if has_role(team.role, tool.roles.split(" ")) ] - @protected(Perms.view_extendtool.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.TEAM_ID) def resolve_extended_team_tools(self, info, team_id: str) -> List[Tool]: team = get_model(Team, id=int(team_id)) @@ -241,12 +241,12 @@ class Query(graphene.ObjectType): if has_role(team.role, tool.roles.split(" ")) ] - @protected(Perms.view_trainee_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.LOG_ID) def resolve_action_log(self, info, log_id: str) -> ActionLog: return get_model(ActionLog, id=int(log_id)) - @protected(Perms.view_trainee_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.TEAM_ID) def resolve_team_action_logs( self, info, team_id: str @@ -258,7 +258,7 @@ class Query(graphene.ObjectType): ) -> QuerySet[ActionLog]: return ActionLog.objects.filter(team_id=team_id, channel_id=channel_id) - @protected(Perms.view_milestone.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.TEAM_ID) @extra_protected(Check.VISIBLE_ONLY) def resolve_team_milestones( @@ -266,76 +266,76 @@ class Query(graphene.ObjectType): ) -> List[MilestoneState]: return get_milestone_states(int(team_id), visible_only) - @protected(Perms.view_injectselection.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.TEAM_ID) def resolve_team_inject_selections( self, info, team_id: str ) -> QuerySet[InjectSelection]: return InjectSelection.objects.filter(team_id=team_id) - @protected(Perms.view_email_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.VISIBLE_ONLY) def resolve_email_contacts( self, info, visible_only: bool = True ) -> QuerySet[EmailParticipant]: return EmailClient.get_contacts(visible_only) - @protected(Perms.view_email_info.full_name) + @protected(User.AuthGroup.TRAINEE) def resolve_email_contact( self, info, participant_id: str ) -> EmailParticipant: return get_model(EmailParticipant, id=int(participant_id)) - @protected(Perms.view_email.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.TEAM_ID) def resolve_email_threads( self, info, team_id: str ) -> QuerySet[EmailThread]: return EmailClient.get_threads(int(team_id)) - @protected(Perms.view_email.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.THREAD_ID) def resolve_email_thread(self, info, thread_id: str) -> EmailThread: return get_model(EmailThread, id=int(thread_id)) - @protected(Perms.view_email_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.THREAD_ID) def resolve_email_addresses(self, info, thread_id: str) -> List[str]: return EmailClient.get_instructor_email_addresses(int(thread_id)) - @protected(Perms.view_email_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.EXERCISE_ID) def resolve_validate_email_address( self, info, exercise_id: str, address: str ) -> bool: return EmailClient.validate_email_address(int(exercise_id), address) - @protected(Perms.view_email_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.TEAM_ID) def resolve_team_email_participant( self, info, team_id: str ) -> EmailParticipant: return EmailClient.get_team_participant(int(team_id)) - @protected(Perms.view_injectselection.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def resolve_email_templates( self, info, exercise_id: str, email_addresses: List[str] ) -> List[EmailTemplate]: return EmailClient.get_email_templates(exercise_id, email_addresses) - @protected(Perms.view_injectselection.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.THREAD_ID) def resolve_thread_templates( self, info, thread_id: int ) -> List[EmailTemplate]: return EmailClient.get_thread_templates(int(thread_id)) - @protected(Perms.view_injectselection.full_name) + @protected(User.AuthGroup.INSTRUCTOR) def resolve_thread_template(self, info, template_id: str) -> EmailTemplate: return get_model(EmailTemplate, id=int(template_id)) - @protected(Perms.view_exercise.full_name) + @protected(User.AuthGroup.TRAINEE) def resolve_exercise_time_left(self, info) -> int: # TODO: remove dependency on "get_running_exercise", prefer exercise_id parameter running_exercise = get_running_exercise() @@ -348,7 +348,7 @@ class Query(graphene.ObjectType): ) return max(0, exercise_duration_s - int(running_exercise.elapsed_s)) - @protected(Perms.view_exercise.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.EXERCISE_ID) def resolve_exercise_config(self, info, exercise_id: str) -> GrapheneConfig: exercise = get_model(Exercise, id=int(exercise_id)) @@ -364,12 +364,12 @@ class Query(graphene.ObjectType): custom_email_suffix=config.custom_email_suffix, ) - @protected(Perms.view_exercise.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.EXERCISE_ID) def resolve_exercise_loop_running(self, info, exercise_id: str) -> bool: return ExerciseLoop.is_running(int(exercise_id)) - @protected(Perms.view_analytics.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def resolve_analytics_milestones( self, info, exercise_id: str @@ -378,7 +378,7 @@ class Query(graphene.ObjectType): team_state__exercise_id=int(exercise_id) ) - @protected(Perms.view_analytics.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def resolve_analytics_action_logs( self, info, exercise_id: str @@ -387,7 +387,7 @@ class Query(graphene.ObjectType): return ActionLog.objects.filter(team__in=exercise.teams.all()) - @protected(Perms.view_analytics.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def resolve_analytics_email_threads( self, info, exercise_id: str @@ -396,14 +396,14 @@ class Query(graphene.ObjectType): exercise_id=exercise_id ).prefetch_related("emails") - @protected(Perms.view_analytics.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def resolve_exercise_tools(self, info, exercise_id: str) -> List[Tool]: exercise = get_model(Exercise, id=int(exercise_id)) return exercise.definition.tools.all() - @protected(Perms.view_trainee_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.TEAM_ID) def resolve_questionnaire_state( self, info, team_id: str, questionnaire_id: str @@ -414,14 +414,14 @@ class Query(graphene.ObjectType): questionnaire_id=questionnaire_id, ) - @protected(Perms.view_analytics.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.TEAM_ID) def resolve_team_questionnaires( self, info, team_id: str ) -> List[TeamQuestionnaireState]: return TeamQuestionnaireState.objects.filter(team_id=team_id) - @protected(Perms.view_analytics.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def resolve_exercise_questionnaires( self, info, exercise_id: str @@ -430,7 +430,7 @@ class Query(graphene.ObjectType): team__exercise_id=exercise_id ) - @protected(Perms.view_analytics.full_name) + @protected(User.AuthGroup.INSTRUCTOR) def resolve_team_learning_objectives( self, info, team_id: str ) -> List[TeamLearningObjective]: diff --git a/running_exercise/subscription.py b/running_exercise/subscription.py index d439ced28ecde08ab6f70e52dbf19b19abb5c678..5e4a93badd9b18b9373f2345c589d75206312662 100644 --- a/running_exercise/subscription.py +++ b/running_exercise/subscription.py @@ -1,7 +1,7 @@ import channels_graphql_ws import graphene -from aai.models import Perms +from user.models import User from aai.utils import protected, extra_protected, Check from common_lib.schema_types import ( ActionLogType, @@ -25,7 +25,7 @@ class ExerciseLoopRunningSubscription(channels_graphql_ws.Subscription): exercise_id = graphene.ID() @staticmethod - @protected(Perms.view_exercise.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.EXERCISE_ID) def subscribe(root, info, exercise_id: str): get_model(Exercise, id=int(exercise_id)) @@ -45,7 +45,7 @@ class ActionLogsSubscription(channels_graphql_ws.Subscription): team_id = graphene.ID() @staticmethod - @protected(Perms.view_trainee_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.TEAM_ID) def subscribe(root, info, team_id: str): get_model(Team, id=int(team_id)) @@ -69,7 +69,7 @@ class MilestonesSubscription(channels_graphql_ws.Subscription): ) @staticmethod - @protected(Perms.view_milestone.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.TEAM_ID) @extra_protected(Check.VISIBLE_ONLY) def subscribe(root, info, team_id: str, visible_only: bool = True): @@ -89,7 +89,7 @@ class InjectSelectionsSubscription(channels_graphql_ws.Subscription): team_id = graphene.ID() @staticmethod - @protected(Perms.send_injectselection.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.TEAM_ID) def subscribe(root, info, team_id: str): get_model(Team, id=int(team_id)) @@ -108,7 +108,7 @@ class EmailThreadSubscription(channels_graphql_ws.Subscription): team_id = graphene.ID() @staticmethod - @protected(Perms.view_email.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.TEAM_ID) def subscribe(root, info, team_id: str): get_model(Team, id=int(team_id)) @@ -127,7 +127,7 @@ class AnalyticsMilestonesSubscription(channels_graphql_ws.Subscription): exercise_id = graphene.ID() @staticmethod - @protected(Perms.view_analytics.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def subscribe(root, info, exercise_id: str): get_model(Exercise, id=int(exercise_id)) @@ -146,7 +146,7 @@ class AnalyticsActionLogsSubscription(channels_graphql_ws.Subscription): exercise_id = graphene.ID() @staticmethod - @protected(Perms.view_analytics.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def subscribe(root, info, exercise_id: str): get_model(Exercise, id=int(exercise_id)) @@ -165,7 +165,7 @@ class AnalyticsEmailThreadSubscription(channels_graphql_ws.Subscription): exercise_id = graphene.ID() @staticmethod - @protected(Perms.view_analytics.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def subscribe(root, info, exercise_id: str): get_model(Exercise, id=int(exercise_id)) @@ -186,7 +186,7 @@ class TeamQuestionnaireStateSubscription(channels_graphql_ws.Subscription): team_id = graphene.ID(required=True) @staticmethod - @protected(Perms.view_trainee_info.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.TEAM_ID) def subscribe(root, info, team_id: str): get_model(Team, id=int(team_id)) diff --git a/running_exercise/views.py b/running_exercise/views.py index db8da9218cc5deb0073e14f299e16246507fe82a..230321bfca72757c05810ad7927201d6cfbb0167 100644 --- a/running_exercise/views.py +++ b/running_exercise/views.py @@ -3,7 +3,7 @@ from rest_framework import parsers from rest_framework.response import Response from rest_framework.views import APIView -from aai.models import Perms +from user.models import User from aai.utils import protected, extra_protected, Check from common_lib.exceptions import ApiException from running_exercise.lib.file_handler import ( @@ -13,7 +13,7 @@ from running_exercise.lib.file_handler import ( class GetFileView(APIView): - @protected(Perms.manipulate_file.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.TEAM_ID) def get(self, request, *args, **kwargs): """ @@ -40,7 +40,7 @@ class GetFileView(APIView): class UploadFileView(APIView): parser_classes = [parsers.MultiPartParser] - @protected(Perms.manipulate_file.full_name) + @protected(User.AuthGroup.TRAINEE) @extra_protected(Check.TEAM_ID) def post(self, request, *args, **kwargs): """Upload a file as a team.""" diff --git a/user/lib/user_uploader.py b/user/lib/user_uploader.py index 83406ab08f02da7bd9e717bae0f8c412e4a2d5a1..d4df050f4af3440d065b7b302e28b9cc806877b7 100644 --- a/user/lib/user_uploader.py +++ b/user/lib/user_uploader.py @@ -14,7 +14,6 @@ from user.lib.user_validator import ( ValidatedUserData, ) from user.models import User, Tag, UserTag -from aai.models import UserGroup from user.email.email_sender import send_credentials @@ -23,17 +22,17 @@ Password = str CreatedUser = Tuple[User, Password] -def _group_to_create_func(group: UserGroup) -> Callable: - if group == UserGroup.TRAINEE: +def _group_to_create_func(group: User.AuthGroup) -> Callable: + if group == User.AuthGroup.TRAINEE: return User.objects.create_user - elif group == UserGroup.INSTRUCTOR: + if group == User.AuthGroup.INSTRUCTOR: return User.objects.create_staffuser - elif group == UserGroup.ADMIN: + if group == User.AuthGroup.ADMIN: return User.objects.create_superuser - raise ValueError(f"Unexpected UserGroup ({group}) in _group_to_create_func") + raise ValueError(f"Unexpected Authorization Group ({group}).") def _create_and_tag_user( diff --git a/user/lib/user_validator.py b/user/lib/user_validator.py index c219d58b029a58e7f0014a7c1f7cede06a0ad76b..a9bfacbf8838d31d6b1033bf005541c9340c9cf6 100644 --- a/user/lib/user_validator.py +++ b/user/lib/user_validator.py @@ -5,7 +5,6 @@ from enum import Enum from rest_framework.response import Response from rest_framework import status -from aai.models import UserGroup from user.models import User, EMAIL_REGEX @@ -36,7 +35,7 @@ class ValidatedUserData: def __init__( self, username: str, - group: UserGroup, + group: User.AuthGroup, tags: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, @@ -142,14 +141,14 @@ class UserValidator: # For convenience of users "Empty" is taken as TRAINEE and thus valid return True - if UserGroup.from_str(group) is None: + if User.AuthGroup.from_str(group) is None: self.result_handler.add_error( f"User with username:{username} has definined invalid role: {group}." ) create = False elif ( - UserGroup.from_str(group) == UserGroup.ADMIN + User.AuthGroup.from_str(group) == User.AuthGroup.ADMIN and not self.admin_initiator ): self.result_handler.add_error( @@ -178,9 +177,9 @@ class UserValidator: ResultAction.ERROR, ): typed_group = ( - UserGroup.from_str(user_data.group) + User.AuthGroup.from_str(user_data.group) if user_data.group is not None and user_data.group != "" - else UserGroup.TRAINEE + else User.AuthGroup.TRAINEE ) if typed_group is None: # validation was success -> can't be None diff --git a/user/management/commands/initadmins.py b/user/management/commands/initadmins.py index 2ee1d629332e98068b6ef3c9e5e64ed8660c5217..669019c02ecfa5edf9f5465cf1ef050096103410 100644 --- a/user/management/commands/initadmins.py +++ b/user/management/commands/initadmins.py @@ -9,7 +9,6 @@ from user.models import EMAIL_REGEX, User from user.lib.user_uploader import _create_and_tag_user from user.lib.user_validator import ValidatedUserData from user.email.email_sender import send_credentials -from aai.models import UserGroup def _validate_admins(admins: str) -> List[ValidatedUserData]: @@ -28,7 +27,7 @@ def _validate_admins(admins: str) -> List[ValidatedUserData]: ) else: valid_users.append( - ValidatedUserData(username=email, group=UserGroup.ADMIN) + ValidatedUserData(username=email, group=User.AuthGroup.ADMIN) ) return valid_users diff --git a/user/migrations/0005_alter_user_group.py b/user/migrations/0005_alter_user_group.py new file mode 100644 index 0000000000000000000000000000000000000000..aa8611350d251b6f79c92497310d0b2247340331 --- /dev/null +++ b/user/migrations/0005_alter_user_group.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.22 on 2024-07-10 15:36 + +from django.db import migrations, models +import user.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0004_auto_20240625_1246'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='group', + field=models.IntegerField(choices=[(1, 'trainee'), (3, 'instructor'), (5, 'admin')], default=user.models.User.AuthGroup['TRAINEE']), + ), + ] diff --git a/user/models.py b/user/models.py index b502e6e62f8893af978660a58e531936adad0961..335df7f3a4a9cde22eb4cc11c0d9c4344185ed81 100644 --- a/user/models.py +++ b/user/models.py @@ -1,14 +1,13 @@ import re import uuid +from enum import IntEnum +from typing import Optional from django.contrib.auth.hashers import make_password -from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, Group +from django.contrib.auth.models import BaseUserManager, AbstractBaseUser from django.db import models, transaction -from django.db.models.signals import post_save, m2m_changed -from django.dispatch import receiver from django.utils import timezone -from aai.models import UserGroup from exercise.models import Team, Exercise, Definition EMAIL_REGEX = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b" @@ -18,7 +17,7 @@ PERM_CACHE = "_perm_cache" class UserManager(BaseUserManager): use_in_migrations = True - def _create_user(self, username, password, group=None, **extra_fields): + def _create_user(self, username, password, group, **extra_fields): """ Create and save a user with the given username and password. """ @@ -32,14 +31,7 @@ class UserManager(BaseUserManager): username = self.normalize_email(username) user: User = self.model(username=username, **extra_fields) user.password = make_password(password) - user.save( - using=self._db - ) # user must have assigned id when adding group - - if group is not None: - group, _ = Group.objects.get_or_create(name=group) - user.group = group - user.save() + user.group = group if (first_name := extra_fields.get("first_name", None)) is not None: user.first_name = first_name @@ -47,27 +39,28 @@ class UserManager(BaseUserManager): if (last_name := extra_fields.get("last_name", None)) is not None: user.last_name = last_name + user.save(using=self._db) return user def create_superuser(self, username, password, **extra_fields): extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) return self._create_user( - username, password, UserGroup.ADMIN, **extra_fields + username, password, User.AuthGroup.ADMIN, **extra_fields ) def create_staffuser(self, username, password, **extra_fields): extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", False) return self._create_user( - username, password, UserGroup.INSTRUCTOR, **extra_fields + username, password, User.AuthGroup.INSTRUCTOR, **extra_fields ) def create_user(self, username, password, **extra_fields): extra_fields.setdefault("is_staff", False) extra_fields.setdefault("is_superuser", False) return self._create_user( - username, password, UserGroup.TRAINEE, **extra_fields + username, password, User.AuthGroup.TRAINEE, **extra_fields ) def get_by_natural_key(self, username): @@ -109,6 +102,26 @@ class UserTagManager(models.Manager): class User(AbstractBaseUser): + class AuthGroup(IntEnum): + TRAINEE = 1 + INSTRUCTOR = 3 + ADMIN = 5 + + @staticmethod + def from_str(group: str) -> Optional["User.AuthGroup"]: + lowered_group = group.lower() + if lowered_group == "trainee" or lowered_group == "t": + return User.AuthGroup.TRAINEE + elif lowered_group == "instructor" or lowered_group == "i": + return User.AuthGroup.INSTRUCTOR + elif lowered_group == "admin" or lowered_group == "a": + return User.AuthGroup.ADMIN + return None + + @classmethod + def choices(cls): + return [(group.value, group.name.lower()) for group in cls] + id = models.UUIDField( primary_key=True, default=uuid.uuid4, @@ -119,7 +132,9 @@ class User(AbstractBaseUser): first_name = models.TextField(blank=True, null=True) last_name = models.TextField(blank=True, null=True) date_joined = models.DateTimeField(default=timezone.now) - group = models.ForeignKey(Group, on_delete=models.SET_NULL, null=True) + group = models.IntegerField( + choices=AuthGroup.choices(), default=AuthGroup.TRAINEE + ) is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) is_active = models.BooleanField(default=True) @@ -149,36 +164,6 @@ class User(AbstractBaseUser): def __str__(self): return self.get_username() - def has_perm(self, perm): - if self.is_superuser: - return True - - if not self.is_active or self.is_anonymous or self.group is None: - return False - - if not hasattr(self, PERM_CACHE): - perms = self.group.permissions.all().values_list( - "content_type__app_label", "codename" - ) - setattr( - self, - PERM_CACHE, - {f"{ct}.{name}" for ct, name in perms}, - ) - return perm in getattr(self, PERM_CACHE) - - @classmethod - def invalidate_cache(cls): - for user in cls.objects.all(): - if hasattr(user, PERM_CACHE): - delattr(user, PERM_CACHE) - - -@receiver(post_save, sender=Group) -@receiver(m2m_changed, sender=Group.permissions.through) -def _user_perm_cache_invalidator(signal, instance, **kwargs): - User.invalidate_cache() - class Tag(models.Model): name = models.CharField(max_length=20, blank=False, null=False) diff --git a/user/schema/mutation.py b/user/schema/mutation.py index 66224f49ac2a7851dbd596c9b9d728ced71f6b6d..3f186ffab666766dafb6976772403485f6a96220 100644 --- a/user/schema/mutation.py +++ b/user/schema/mutation.py @@ -4,7 +4,6 @@ import graphene from django.utils.crypto import get_random_string from django.conf import settings -from aai.models import Perms from aai.utils import protected, extra_protected, Check from common_lib.logger import logger, log_user_msg from common_lib.schema_types import UserType @@ -40,7 +39,7 @@ class AssignUsersToTeamMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.update_userassignment.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.TEAM_ID) def mutate( cls, root, info, user_ids: List[str], team_id: str @@ -73,7 +72,7 @@ class RemoveUsersFromTeamMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.update_userassignment.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.TEAM_ID) def mutate( cls, root, info, user_ids: List[str], team_id: str @@ -104,7 +103,7 @@ class AssignInstructorsToExercise(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.update_userassignment.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def mutate( cls, root, info, user_ids: List[str], exercise_id: str @@ -137,7 +136,7 @@ class RemoveInstructorsFromExerciseMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.update_userassignment.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.EXERCISE_ID) def mutate( cls, root, info, user_ids: List[str], exercise_id: str @@ -170,7 +169,7 @@ class AddDefinitionAccessMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.update_definition.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.DEFINITION_ID) def mutate( cls, root, info, user_ids: List[str], definition_id: str @@ -203,7 +202,7 @@ class RemoveDefinitionAccessMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.update_definition.full_name) + @protected(User.AuthGroup.INSTRUCTOR) @extra_protected(Check.DEFINITION_ID) def mutate( cls, root, info, user_ids: List[str], definition_id: str @@ -228,7 +227,7 @@ class ChangeUserDataMutation(graphene.Mutation): change_user_input = graphene.Argument(ChangeUserInput, required=True) @classmethod - @protected(Perms.update_user.full_name) + @protected(User.AuthGroup.INSTRUCTOR) def mutate( cls, root, info, change_user_input: ChangeUserInput ) -> graphene.Mutation: @@ -254,7 +253,7 @@ class RegenerateCredentialsMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.update_user.full_name) + @protected(User.AuthGroup.INSTRUCTOR) def mutate(cls, root, info, user_ids: List[str]) -> graphene.Mutation: users = User.objects.filter(id__in=user_ids, is_active=True) regenerated_users = [] @@ -290,7 +289,7 @@ class DeleteUsersMutation(graphene.Mutation): operation_done = graphene.Boolean() @classmethod - @protected(Perms.delete_user.full_name) + @protected(User.AuthGroup.ADMIN) def mutate(cls, root, info, user_ids: List[str]) -> graphene.Mutation: users = User.objects.filter(id__in=user_ids) if not settings.NOAUTH or not info.context.user.is_anonymous: diff --git a/user/schema/query.py b/user/schema/query.py index 8d186dc28241295e73cb0c051c39a1b4fb313df9..4cdffcc62e83bbb6676b94a75ed6621f6251c651 100644 --- a/user/schema/query.py +++ b/user/schema/query.py @@ -1,11 +1,9 @@ from typing import Optional, List import graphene -from django.contrib.auth.models import Group -from aai.models import Perms -from aai.utils import protected -from common_lib.schema_types import UserType, TagType, GroupType +from aai.utils import protected, logged_in +from common_lib.schema_types import UserType, TagType from common_lib.utils import get_model from user.graphql_inputs import FilterUsersInput from user.models import User, Tag @@ -26,7 +24,7 @@ class Query(graphene.ObjectType): TagType, description="Retrieve all tags (for filtering)" ) groups = graphene.List( - GroupType, description="Retrieve all groups (for filtering)" + graphene.String, description="Retrieve all groups (for filtering)" ) def resolve_who_am_i(self, info) -> Optional[User]: @@ -34,7 +32,7 @@ class Query(graphene.ObjectType): return None return info.context.user - @protected(Perms.view_user.full_name) + @protected(User.AuthGroup.INSTRUCTOR) def resolve_users( self, info, filter_users_input: Optional[FilterUsersInput] = None ) -> List[User]: @@ -75,14 +73,14 @@ class Query(graphene.ObjectType): users = users[: filter_users_input.limit] return users - @protected(Perms.view_user.full_name) + @protected(User.AuthGroup.INSTRUCTOR) def resolve_user(self, info, user_id: str) -> User: return get_model(User, id=user_id) - @protected(Perms.view_user.full_name) + @protected(User.AuthGroup.INSTRUCTOR) def resolve_tags(self, info) -> List[Tag]: return Tag.objects.all() - # TODO: for now leave open to anyone - def resolve_groups(self, info) -> List[Group]: - return Group.objects.all() + @logged_in + def resolve_groups(self, info) -> List[str]: + return [name for _, name in User.AuthGroup.choices()] diff --git a/user/schema/validators.py b/user/schema/validators.py index c73fbbe7816f9b6a104eb7c576fbafd93c4b4e6f..b66d92adbf6495ecc1ebf912f8bdeb463000b452 100644 --- a/user/schema/validators.py +++ b/user/schema/validators.py @@ -4,11 +4,9 @@ from django.db.models import Model from django.conf import settings from rest_framework.exceptions import PermissionDenied from django.db.models import QuerySet -from django.contrib.auth.models import Group from user.models import UserInTeam, User, InstructorOfExercise, DefinitionAccess from exercise.models import Team, Exercise, Definition -from aai.models import UserGroup from user.graphql_inputs import ChangeUserInput ModelType = TypeVar("ModelType", bound=Model) @@ -27,12 +25,12 @@ def _check_nonexistent_users(user_ids: List[str]) -> List[User]: raise ValueError(f"Users with ids:{invalid_ids} don't exist") -def _check_group(users: List[User], required_groups: List[UserGroup]): +def _check_group(users: List[User], required_groups: List[User.AuthGroup]): + # checks AuthGroup with exact match (not hierarchical) invalid_group_users = [ user for user in users - if user.group is None - or not set([user.group.name]).intersection(set(required_groups)) + if not set([user.group]).intersection(set(required_groups)) ] if not invalid_group_users: return @@ -40,7 +38,7 @@ def _check_group(users: List[User], required_groups: List[UserGroup]): msg_format_users = __create_users_msg_format(invalid_group_users) raise ValueError( f"Users {msg_format_users} don't have one of the groups:{required_groups}. " - f"If you want to assign them, set them one of the metioned group." + f"If you want to assign them, set them one of the mentioned group." ) @@ -72,13 +70,16 @@ def _validate_group_change( ): if settings.NOAUTH: return - if new_group == UserGroup.ADMIN and requester.group != UserGroup.ADMIN: + if ( + new_group == User.AuthGroup.ADMIN + and requester.group != User.AuthGroup.ADMIN + ): raise PermissionDenied( "Permission denied - Only admin can change user to admin group" ) if ( - requester.group != UserGroup.ADMIN - and changing_user.group == UserGroup.ADMIN + requester.group != User.AuthGroup.ADMIN + and changing_user.group == User.AuthGroup.ADMIN ): raise PermissionDenied( "Permission denied - Only admin can change user from admin group" @@ -99,8 +100,8 @@ def _validate_active_change( "Permission denied - Can't change own active status" ) if ( - requester.group != UserGroup.ADMIN - and changing_user.group == UserGroup.ADMIN + requester.group != User.AuthGroup.ADMIN + and changing_user.group == User.AuthGroup.ADMIN ): raise PermissionDenied( "Permission denied - Only admin can change active status of admin user" @@ -124,7 +125,7 @@ def validate_team_assigning(user_ids: List[str], team: Team) -> List[User]: f"to a team of the same exercise" ) - _check_group(users, [UserGroup.TRAINEE]) + _check_group(users, [User.AuthGroup.TRAINEE]) return users @@ -156,7 +157,7 @@ def validate_instructor_assigning( f"Users {msg_format_users} were already assigned to exercise (id:{exercise.id})" ) - _check_group(users, [UserGroup.INSTRUCTOR, UserGroup.ADMIN]) + _check_group(users, [User.AuthGroup.INSTRUCTOR, User.AuthGroup.ADMIN]) return users @@ -176,7 +177,7 @@ def validate_instructor_removing( def validate_definition_access_assinging(user_ids: List[str]) -> List[User]: users = _check_nonexistent_users(user_ids) - _check_group(users, [UserGroup.INSTRUCTOR, UserGroup.ADMIN]) + _check_group(users, [User.AuthGroup.INSTRUCTOR, User.AuthGroup.ADMIN]) return users @@ -198,8 +199,8 @@ def validate_credentials_regeneration(users: QuerySet[User], requester: User): if settings.NOAUTH: return if ( - requester.group != UserGroup.ADMIN - and users.filter(group__name=UserGroup.ADMIN).exists() + requester.group != User.AuthGroup.ADMIN + and users.filter(group=User.AuthGroup.ADMIN).exists() ): raise PermissionDenied( "Permission denied - Only admin can re-generate credentials for admin users" @@ -219,7 +220,7 @@ def execute_change_userdata( _validate_group_change( requester, changing_user, change_user_input.group ) - changing_user.group = Group.objects.get(name=change_user_input.group) + changing_user.group = User.AuthGroup.from_str(change_user_input.group) if change_user_input.group == "admin": changing_user.is_superuser = True changing_user.is_staff = True diff --git a/user/tests/csv_onboarding_tests.py b/user/tests/csv_onboarding_tests.py index 9992248fd2bd7eb5706ba34bbe32790de35baccc..148517516db9cacc859556f7ae49b9b8ca9f97e8 100644 --- a/user/tests/csv_onboarding_tests.py +++ b/user/tests/csv_onboarding_tests.py @@ -6,7 +6,6 @@ from django.test import TestCase from rest_framework.response import Response from user.lib.user_uploader import UserUploader, TAG_DELIMITER -from aai.models import UserGroup from user.models import User, Tag @@ -41,7 +40,7 @@ class UserTests(TestCase): ] uploaded_file = self.create_file(data) response: Response = UserUploader.validate_and_upload( - uploaded_file, UserGroup.ADMIN + uploaded_file, User.AuthGroup.ADMIN ) self.assertEqual( response.data["status"], "ok", msg=response.data["detail"] @@ -65,7 +64,7 @@ class UserTests(TestCase): User.objects.create_user(username="b@uni.mail.com", password="b") response: Response = UserUploader.validate_and_upload( - uploaded_file, UserGroup.ADMIN + uploaded_file, User.AuthGroup.ADMIN ) self.assertEqual(response.data["status"], "ok") self.assertIn( @@ -81,7 +80,7 @@ class UserTests(TestCase): ] uploaded_file = self.create_file(data) response: Response = UserUploader.validate_and_upload( - uploaded_file, UserGroup.ADMIN + uploaded_file, User.AuthGroup.ADMIN ) self.assertEqual(response.data["status"], "ok") self.assertIn( @@ -97,7 +96,7 @@ class UserTests(TestCase): ] uploaded_file = self.create_file(data) response: Response = UserUploader.validate_and_upload( - uploaded_file, UserGroup.ADMIN + uploaded_file, User.AuthGroup.ADMIN ) self.assertEqual(response.data["status"], "error") self.assertIn( @@ -112,7 +111,7 @@ class UserTests(TestCase): ] uploaded_file = self.create_file(data) response: Response = UserUploader.validate_and_upload( - uploaded_file, UserGroup.ADMIN + uploaded_file, User.AuthGroup.ADMIN ) self.assertEqual(response.data["status"], "error") self.assertIn( @@ -127,7 +126,7 @@ class UserTests(TestCase): ] uploaded_file = self.create_file(data) response: Response = UserUploader.validate_and_upload( - uploaded_file, UserGroup.ADMIN + uploaded_file, User.AuthGroup.ADMIN ) self.assertEqual(response.data["status"], "error") self.assertIn( @@ -159,7 +158,7 @@ class UserTests(TestCase): ] uploaded_file = self.create_file(data) response: Response = UserUploader.validate_and_upload( - uploaded_file, UserGroup.INSTRUCTOR + uploaded_file, User.AuthGroup.INSTRUCTOR ) self.assertEqual( @@ -189,7 +188,7 @@ class UserTests(TestCase): uploaded_file = self.create_file(data) response: Response = UserUploader.validate_and_upload( - uploaded_file, UserGroup.INSTRUCTOR + uploaded_file, User.AuthGroup.INSTRUCTOR ) self.assertEqual( response.data["status"], "ok", msg=response.data["detail"] @@ -221,7 +220,7 @@ class UserTests(TestCase): uploaded_file = self.create_file(data) response: Response = UserUploader.validate_and_upload( - uploaded_file, UserGroup.INSTRUCTOR + uploaded_file, User.AuthGroup.INSTRUCTOR ) self.assertEqual( response.data["status"], "ok", msg=response.data["detail"] @@ -252,7 +251,7 @@ class UserTests(TestCase): ] uploaded_file = self.create_file(data) response: Response = UserUploader.validate_and_upload( - uploaded_file, UserGroup.INSTRUCTOR + uploaded_file, User.AuthGroup.INSTRUCTOR ) self.assertEqual( response.data["status"], "ok", msg=response.data["detail"] @@ -267,7 +266,7 @@ class UserTests(TestCase): with self.assertRaises(ValueError): UserUploader.validate_and_upload( - uploaded_file, UserGroup.INSTRUCTOR + uploaded_file, User.AuthGroup.INSTRUCTOR ) def test_empty_username(self): @@ -276,7 +275,7 @@ class UserTests(TestCase): with self.assertRaises(ValueError): UserUploader.validate_and_upload( - uploaded_file, UserGroup.INSTRUCTOR + uploaded_file, User.AuthGroup.INSTRUCTOR ) def test_multiple_separators(self): @@ -284,7 +283,7 @@ class UserTests(TestCase): uploaded_file = self.create_file(data) response: Response = UserUploader.validate_and_upload( - uploaded_file, UserGroup.INSTRUCTOR + uploaded_file, User.AuthGroup.INSTRUCTOR ) self.assertEqual( response.data["status"], "error", msg=response.data["detail"] diff --git a/user/tests/mutation_validators_tests.py b/user/tests/mutation_validators_tests.py index f4dac319bc058eab5318fcba079de324626badb2..0c6d470b816bd263a5bffd4195104744bf8f2b01 100644 --- a/user/tests/mutation_validators_tests.py +++ b/user/tests/mutation_validators_tests.py @@ -6,7 +6,6 @@ from django.test import TestCase, override_settings from django.conf import settings from user.models import User, UserInTeam, InstructorOfExercise -from aai.models import UserGroup from exercise.models import Exercise, Team from user.schema.validators import ( _check_nonexistent_users, @@ -74,15 +73,15 @@ class UserMutationValidatorsTest(TestCase): get_model(Team, id="10") def test_group_checker(self): - self.assertIsNone(_check_group([self.user1], [UserGroup.TRAINEE])) + self.assertIsNone(_check_group([self.user1], [User.AuthGroup.TRAINEE])) with self.assertRaises(ValueError): - _check_group([self.user1], [UserGroup.INSTRUCTOR]) + _check_group([self.user1], [User.AuthGroup.INSTRUCTOR]) self.assertIsNone( - _check_group([self.instructor], [UserGroup.INSTRUCTOR]) + _check_group([self.instructor], [User.AuthGroup.INSTRUCTOR]) ) with self.assertRaises(ValueError): - _check_group([self.instructor], [UserGroup.ADMIN]) + _check_group([self.instructor], [User.AuthGroup.ADMIN]) def test_presence_checker(self): UserInTeam.objects.create(user=self.user1, team=self.team) diff --git a/user/views.py b/user/views.py index 93fc560cded495df7bbc06f45faf7530b0542296..4af5ca88b045e7920c7b9622edb9fa9ce3d41369 100644 --- a/user/views.py +++ b/user/views.py @@ -3,7 +3,7 @@ from rest_framework import parsers from rest_framework.request import Request from rest_framework.views import APIView -from aai.models import Perms +from user.models import User from aai.utils import protected from common_lib.exceptions import ApiException from common_lib.logger import logger, log_user_msg @@ -13,7 +13,7 @@ from user.lib import UserUploader class UploadUserFile(APIView): parser_classes = [parsers.MultiPartParser] - @protected(Perms.update_user.full_name) + @protected(User.AuthGroup.INSTRUCTOR) def post(self, request: Request, *args, **kwargs): """ Uploads a new users file and checks for its correctness.