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