From 9b97237aa9d436e019320ef086625e10f3dbf87a Mon Sep 17 00:00:00 2001 From: Richard Glosner <xglosner@fi.muni.cz> Date: Tue, 28 May 2024 14:36:33 +0200 Subject: [PATCH] Resolve "Regenerate credentials" --- rolling-changelog.txt | 1 + user/schema/mutation.py | 79 ++++++++++++++++++++++++++------------- user/schema/query.py | 2 +- user/schema/validators.py | 62 ++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 28 deletions(-) diff --git a/rolling-changelog.txt b/rolling-changelog.txt index fa0d327c..16a7f033 100644 --- a/rolling-changelog.txt +++ b/rolling-changelog.txt @@ -41,3 +41,4 @@ fix: update performance testing tools to the newest API fix: fix SendEmailInput authorization checks feat: addition of INJECT_SECRET_KEY env variable #141 change: set csrf cookie for `/version` endpoint +feat: endpoint for re-generation of user login credentials #202 diff --git a/user/schema/mutation.py b/user/schema/mutation.py index 5636e58b..840dbeeb 100644 --- a/user/schema/mutation.py +++ b/user/schema/mutation.py @@ -1,8 +1,8 @@ from typing import List import graphene -from django.contrib.auth.models import Group -from rest_framework.exceptions import PermissionDenied +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 @@ -19,7 +19,10 @@ from user.schema.validators import ( validate_instructor_removing, validate_definition_access_assinging, validate_definition_access_removing, + validate_credentials_regeneration, + execute_change_userdata, ) +from user.email.email_sender import send_credentials class AssignUsersToTeamMutation(graphene.Mutation): @@ -225,38 +228,57 @@ class ChangeUserDataMutation(graphene.Mutation): change_user_input = graphene.Argument(ChangeUserInput, required=True) @classmethod - @protected(Perms.update_user) + @protected(Perms.update_user.full_name) def mutate( cls, root, info, change_user_input: ChangeUserInput ) -> graphene.Mutation: user = get_model(User, id=change_user_input.user_id) - if change_user_input.group is not None: - if ( - change_user_input.group == "admin" - and not info.context.user.is_superuser - ): - raise PermissionDenied( - "Permission denied - Only admin can change user to admin group" - ) - user.group = Group.objects.get(name=change_user_input.group) - if change_user_input.group == "admin": - user.is_superuser = True - user.is_staff = True - elif change_user_input.group == "instructor": - user.is_staff = True - user.is_superuser = False - elif change_user_input.group == "trainee": - user.is_staff = False - user.is_superuser = False - if ( - change_user_input.active is not None and not user.is_imported - ): # can not change is_active of imported user (should be always False) - user.is_active = change_user_input.active - - user.save() + changes = execute_change_userdata( + change_user_input, user, info.context.user + ) + logger.info( + log_user_msg(info.context, info.context.user) + + f"updated user: {user} changed data: {changes}" + ) return ChangeUserDataMutation(user=user) +class RegenerateCredentialsMutation(graphene.Mutation): + class Arguments: + user_ids = graphene.List( + graphene.ID, + required=True, + description="IDs of the users to have re-generated credentials", + ) + + operation_done = graphene.Boolean() + + @classmethod + @protected(Perms.update_user.full_name) + def mutate(cls, root, info, user_ids: List[str]) -> graphene.Mutation: + users = User.objects.filter(id__in=user_ids, is_active=True) + regenerated_users = [] + validate_credentials_regeneration(users, info.context.user) + + for user in users: + password = get_random_string(length=15) + user.set_password(password) + regenerated_users.append((user, password)) + + User.objects.bulk_update(users, ["password"]) + if ( + not settings.DEBUG + and not settings.TESTING_MODE + and regenerated_users + ): + send_credentials(regenerated_users) + logger.info( + log_user_msg(info.context, info.context.user) + + f"re-generated credentials for users: {users}" + ) + return RegenerateCredentialsMutation(operation_done=True) + + class Mutation(graphene.ObjectType): assign_users_to_team = AssignUsersToTeamMutation.Field( description="Mutation for assigning users to the specific team of the exercise" @@ -279,3 +301,6 @@ class Mutation(graphene.ObjectType): change_user_data = ChangeUserDataMutation.Field( description="Mutation for changing user data" ) + regenerate_credentials = RegenerateCredentialsMutation.Field( + description="Mutation for re-generating credentials for users" + ) diff --git a/user/schema/query.py b/user/schema/query.py index 33d01554..fa9b729b 100644 --- a/user/schema/query.py +++ b/user/schema/query.py @@ -64,7 +64,7 @@ class Query(graphene.ObjectType): def resolve_user(self, info, user_id: str) -> User: return get_model(User, id=user_id) - @protected(Perms.view_user) + @protected(Perms.view_user.full_name) def resolve_tags(self, info) -> List[Tag]: return Tag.objects.all() diff --git a/user/schema/validators.py b/user/schema/validators.py index 69d75ad4..abe1a1c9 100644 --- a/user/schema/validators.py +++ b/user/schema/validators.py @@ -1,10 +1,15 @@ from typing import List, Type, TypeVar, Union 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) @@ -62,6 +67,18 @@ def __create_users_msg_format(users: List[User]) -> str: return ", ".join(invalid_group_users) +def _validate_group_change(requester: User, changed_user: User): + if settings.NOAUTH: + return + if ( + changed_user.group == UserGroup.ADMIN + and requester.group != UserGroup.ADMIN + ): + raise PermissionDenied( + "Permission denied - Only admin can change user to admin group" + ) + + def validate_team_assigning(user_ids: List[str], team: Team) -> List[User]: users = _check_nonexistent_users(user_ids) @@ -147,3 +164,48 @@ def validate_definition_access_removing( definition_id=definition.id, ) return users + + +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() + ): + raise PermissionDenied( + "Permission denied - Only admin can re-generate credentials for admin users" + ) + + +def execute_change_userdata( + change_user_input: ChangeUserInput, changing_user: User, requester: User +) -> List[str]: + changes = [] + old_group = changing_user.group + old_status = changing_user.is_active + + if change_user_input.group is not None: + _validate_group_change(requester, changing_user) + changing_user.group = Group.objects.get(name=change_user_input.group) + if change_user_input.group == "admin": + changing_user.is_superuser = True + changing_user.is_staff = True + changes.append(f"group=(old: {old_group}, new: admin)") + elif change_user_input.group == "instructor": + changing_user.is_staff = True + changing_user.is_superuser = False + changes.append(f"group=(old: {old_group}, new: instructor)") + elif change_user_input.group == "trainee": + changing_user.is_staff = False + changing_user.is_superuser = False + changes.append(f"group=(old: {old_group}, new: trainee)") + if ( + change_user_input.active is not None and not changing_user.is_imported + ): # can not change is_active of imported user (should be always False) + changing_user.is_active = change_user_input.active + changes.append( + f"active=(old: {old_status}, new: {change_user_input.active})" + ) + changing_user.save() + return changes -- GitLab