diff --git a/aai/access.py b/aai/access.py
new file mode 100644
index 0000000000000000000000000000000000000000..4aade1fe4c3e3ae0e34b1593aa29293e9f46b51e
--- /dev/null
+++ b/aai/access.py
@@ -0,0 +1,84 @@
+from typing import Optional
+
+from django.conf import settings
+from django.contrib.auth.models import AnonymousUser
+from rest_framework.exceptions import PermissionDenied, AuthenticationFailed
+from rest_framework.request import Request
+
+from user.models import User, UserInTeam, DefinitionAccess, InstructorOfExercise
+
+
+def user_from_context(context: Request) -> Optional[User]:
+    user: Optional[User] = None if settings.NOAUTH else context.user
+    if isinstance(user, AnonymousUser):
+        raise AuthenticationFailed("Authentication failed")
+    return user
+
+
+def team_access(context: Request, team_id: int):
+    user = user_from_context(context)
+    if user is None:
+        return
+
+    if user.group == User.AuthGroup.ADMIN:
+        return
+
+    condition = False
+    if user.group == User.AuthGroup.INSTRUCTOR:
+        condition = InstructorOfExercise.objects.filter(
+            user_id=user.id, exercise__teams__in=[team_id]
+        ).exists()
+    if user.group == User.AuthGroup.TRAINEE:
+        condition = UserInTeam.objects.filter(
+            user_id=user.id, team_id=team_id
+        ).exists()
+
+    if not condition:
+        raise PermissionDenied(
+            f"User does not have access to this team ({team_id})"
+        )
+
+
+def exercise_access(context: Request, exercise_id: int):
+    user = user_from_context(context)
+    if user is None:
+        return
+
+    if user.group == User.AuthGroup.ADMIN:
+        return
+
+    condition = False
+    if user.group == User.AuthGroup.INSTRUCTOR:
+        condition = InstructorOfExercise.objects.filter(
+            user_id=user.id, exercise_id=exercise_id
+        ).exists()
+    if user.group == User.AuthGroup.TRAINEE:
+        condition = UserInTeam.objects.filter(
+            user_id=user.id, user__teams__exercise_id=exercise_id
+        ).exists()
+
+    if not condition:
+        raise PermissionDenied(
+            f"User does not have access to this exercise ({exercise_id})"
+        )
+
+
+def definition_access(context: Request, definition_id: int):
+    user = user_from_context(context)
+    if user is None:
+        return
+
+    if user.group == User.AuthGroup.ADMIN:
+        return
+
+    condition = False
+    if user.group == User.AuthGroup.INSTRUCTOR:
+        condition = DefinitionAccess.objects.filter(
+            user_id=user.id, definition_id=definition_id
+        ).exists()
+
+    # trainee should never reach this, but just to be sure
+    if not condition:
+        raise PermissionDenied(
+            f"User does not have access to this definition ({definition_id})"
+        )
diff --git a/aai/decorators.py b/aai/decorators.py
new file mode 100644
index 0000000000000000000000000000000000000000..33236de040abc2e56429196f648bbccb7c3783ff
--- /dev/null
+++ b/aai/decorators.py
@@ -0,0 +1,117 @@
+from typing import Optional
+
+from django.conf import settings
+from graphene import ResolveInfo
+from rest_framework.exceptions import AuthenticationFailed, PermissionDenied
+from rest_framework.request import HttpRequest, Request
+
+from aai.access import user_from_context
+from common_lib.logger import logger
+from user.models import User
+
+
+# This function is probably significantly overcomplicated
+def _get_action_name(func, *args) -> str:
+    try:
+        # Should never happen
+        if len(args) == 0:
+            return "Unknown"
+
+        # I don't know a name for this variable
+        tmp = args[0]
+
+        # This case should happen when the decorator is on a Query
+        if tmp is None:
+            # A query function should always have qualname, but just in case
+            if hasattr(func, "__qualname__"):
+                # qualname should give us a name in the format Query.resolve<name>
+                return getattr(func, "__qualname__")
+            # Should never happen
+            return "Unknown"
+
+        # This case works for mutations and subscriptions where it just gives us the class name
+        if hasattr(tmp, "__name__"):
+            return getattr(tmp, "__name__")
+
+        # Last case, should only happen for views
+        tmp_t = type(tmp)
+        if hasattr(tmp_t, "__name__"):
+            return getattr(tmp_t, "__name__")
+
+        # Again, should never happen as all of our API options are covered
+        return "Unknown"
+    # we CANNOT throw an exception in here
+    except Exception:
+        return "Unknown"
+
+
+def _get_user(*args, **kwargs) -> Optional[User]:
+    """
+    Gets user object from known request types.
+
+    Args:
+        *args - non-keyworded arguments of request
+        **kwargs - keyword arguments of request
+    Returns:
+        Instance of User if request is authenticated else instance of AnonymousUser
+    """
+    if len(args) == 0:
+        return None
+
+    request = args[-1]  # get request
+    context: Optional[Request] = None
+
+    if isinstance(request, ResolveInfo):
+        context = request.context
+
+    if isinstance(request, HttpRequest) or isinstance(request, Request):
+        context = request
+
+    return user_from_context(context) if context is not None else None
+
+
+def logged_in(func):
+    def wrapper(*args, **kwargs):
+        user = _get_user(*args, **kwargs)
+        if user is not None:
+            return func(*args, **kwargs)
+        raise AuthenticationFailed("Authentication failed")
+
+    return wrapper
+
+
+def protected(required_group: User.AuthGroup):
+    """
+    Decorator that checks whether user of request has required authorization group.
+    If not, PermissionDenied exception is raised.
+
+    Args:
+        required_group: minimal required authorization group to access endpoint
+    """
+
+    def decorator(func):
+        def wrapper(*args, **kwargs):
+            action_name = _get_action_name(func, *args)
+            log_text = f"action: {action_name} | arguments: {kwargs}"
+
+            if settings.NOAUTH:
+                if action_name is not None:
+                    logger.info(log_text)
+                return func(*args, **kwargs)
+
+            user = _get_user(*args, **kwargs)
+            if user is None:
+                log_text += f" | username: Unauthenticated"
+                logger.info(log_text)
+                raise AuthenticationFailed("Authentication failed")
+
+            log_text += f" | username: {user.username}"
+            logger.info(log_text)
+
+            if user.group >= required_group:
+                return func(*args, **kwargs)
+            raise PermissionDenied("Permission denied")
+
+        return wrapper
+
+    return decorator
diff --git a/aai/schema/mutation.py b/aai/schema/mutation.py
index 50485fbb3652e0e5043ebcc0784f8c8660169be3..2feea84ba2dfd855ebdbae3b25486b2b63c81a1c 100644
--- a/aai/schema/mutation.py
+++ b/aai/schema/mutation.py
@@ -3,7 +3,7 @@ from django.conf import settings
 from django.contrib.auth import authenticate, logout, login
 from django.core.exceptions import ValidationError
 
-from aai.utils import logged_in
+from aai.decorators import logged_in
 from common_lib.logger import logger, log_user_msg
 from common_lib.schema_types import UserType
 from user.email.email_sender import send_password_change_notification
diff --git a/aai/tests/aai_utils_tests.py b/aai/tests/authorization_tests.py
similarity index 56%
rename from aai/tests/aai_utils_tests.py
rename to aai/tests/authorization_tests.py
index ee012b71a15a793196d24157f3dcf46909ccc5d7..e19239bca8dbcd9d3e3a972f8530d3405d694c41 100644
--- a/aai/tests/aai_utils_tests.py
+++ b/aai/tests/authorization_tests.py
@@ -2,12 +2,13 @@ import os
 import shutil
 
 from django.conf import settings
-from django.contrib.auth.models import AnonymousUser, Permission
-from rest_framework.exceptions import PermissionDenied, AuthenticationFailed
+from django.contrib.auth.models import AnonymousUser
 from django.test import TestCase, override_settings
+from rest_framework.exceptions import PermissionDenied, AuthenticationFailed
 from rest_framework.test import APIRequestFactory
 
-from aai.utils import protected, extra_protected, Check
+from aai.access import team_access, exercise_access, definition_access
+from aai.decorators import protected
 from common_lib.test_utils import (
     internal_create_exercise,
     internal_upload_definition,
@@ -25,14 +26,14 @@ from running_exercise.models import (
 )
 from user.models import User
 
-TEST_DATA_STORAGE = "aai_tests_test_data"
+TEST_DATA_STORAGE = "authorization_test_data"
 
 
 @override_settings(
     DATA_STORAGE=TEST_DATA_STORAGE,
     FILE_STORAGE=os.path.join(TEST_DATA_STORAGE, "files"),
 )
-class AaiUtilsTests(TestCase):
+class AuthorizationTests(TestCase):
     definition: Definition
     exercise: Exercise
     exercise2: Exercise
@@ -200,7 +201,7 @@ class AaiUtilsTests(TestCase):
         request.user = self.admin
         self.assertEqual(decorated(request), 0)
 
-    def test_protected_anonoymous(self):
+    def test_protected_anonymous(self):
         decorated = protected(User.AuthGroup.TRAINEE)(self.dummy)
         request = self.factory.request()
 
@@ -208,148 +209,64 @@ class AaiUtilsTests(TestCase):
         with self.assertRaises(AuthenticationFailed):
             decorated(request)
 
-    def test_team_protected(self):
+    def test_team_access(self):
         request = self.factory.request()
-        decorated = extra_protected(Check.TEAM_ID)(self.team_dummy)
+        # access
+        # team1: trainee1, trainee2
+        # team2: empty
+        # team3: trainee3
 
         request.user = self.trainee1
-        self.assertEqual(decorated(request, team_id=self.team1.id), 0)
+        self.assertIsNone(team_access(request, self.team1.id))
         with self.assertRaises(PermissionDenied):
-            decorated(request, team_id=self.team2.id)
+            team_access(request, self.team2.id)
 
         request.user = self.trainee2
-        self.assertEqual(decorated(request, team_id=self.team1.id), 0)
+        self.assertIsNone(team_access(request, self.team1.id))
         with self.assertRaises(PermissionDenied):
-            decorated(request, team_id=self.team2.id)
+            team_access(request, self.team2.id)
 
         request.user = self.trainee3
-        self.assertEqual(decorated(request, team_id=self.team3.id), 0)
+        self.assertIsNone(team_access(request, self.team3.id))
         with self.assertRaises(PermissionDenied):
-            decorated(request, team_id=self.team2.id)
-
-        request.user = AnonymousUser()
-        with self.assertRaises(AuthenticationFailed):
-            decorated(request, team_id=self.team2.id)
+            team_access(request, self.team2.id)
 
     def test_exercise_protected(self):
         request = self.factory.request()
-        decorated = extra_protected(Check.EXERCISE_ID)(self.exercise_dummy)
+
+        # access
+        # exercise: instructor, trainee1-3
+        # exercise2: empty
 
         request.user = self.instructor
-        self.assertEqual(decorated(request, exercise_id=self.exercise.id), 0)
+        self.assertIsNone(exercise_access(request, self.exercise.id))
         with self.assertRaises(PermissionDenied):
-            decorated(request, exercise_id=self.exercise2.id)
+            exercise_access(request, self.exercise2.id)
 
         request.user = self.admin
-        self.assertEqual(decorated(request, exercise_id=self.exercise.id), 0)
-        self.assertEqual(decorated(request, exercise_id=self.exercise2.id), 0)
+        self.assertIsNone(exercise_access(request, self.exercise.id))
+        self.assertIsNone(exercise_access(request, self.exercise2.id))
 
         request.user = self.trainee1
-        self.assertEqual(decorated(request, exercise_id=self.exercise.id), 0)
+        self.assertIsNone(exercise_access(request, self.exercise.id))
         with self.assertRaises(PermissionDenied):
-            decorated(request, exercise_id=self.exercise2.id)
-
-        request.user = AnonymousUser()
-        with self.assertRaises(AuthenticationFailed):
-            decorated(request, team_id=self.team2.id)
+            exercise_access(request, self.exercise2.id)
 
     def test_definition_protected(self):
         request = self.factory.request()
-        decorated = extra_protected(Check.DEFINITION_ID)(self.definition_dummy)
+        # access
+        # definition: instructor
 
         request.user = self.instructor
-        self.assertEqual(
-            decorated(request, definition_id=self.definition.id), 0
-        )  # instructor with access to definition
+        self.assertIsNone(definition_access(request, self.definition.id))
 
         request.user = self.instructor2
-        # instructor without access to definition
         with self.assertRaises(PermissionDenied):
-            decorated(request, definition_id=self.definition.id)
+            definition_access(request, self.definition.id)
 
         request.user = self.admin
-        # admin should see all definition
-        self.assertEqual(
-            decorated(request, definition_id=self.definition.id), 0
-        )
-
-        request.user = self.trainee1
-        # trainee should't see the definition
-        with self.assertRaises(PermissionDenied):
-            decorated(request, definition_id=self.definition.id)
-
-    def test_log_protected(self):
-        request = self.factory.request()
-        decorated = extra_protected(Check.LOG_ID)(self.log_dummy)
-
-        request.user = self.trainee1
-        # trainee1 created log -> can access it
-        self.assertEqual(decorated(request, log_id=self.log.id), 0)
-
-        request.user = self.trainee2
-        # trainee1_1 is in the same team as trainee1 who created the log thus
-        # can read the log as well
-        self.assertEqual(decorated(request, log_id=self.log.id), 0)
-
-        request.user = self.instructor
-        # instructor is assigned to the exercise where log was created thus can access it
-        self.assertEqual(decorated(request, log_id=self.log.id), 0)
-
-        request.user = self.admin
-        # admin can see all logs
-        self.assertEqual(decorated(request, log_id=self.log.id), 0)
-
-        request.user = self.trainee3
-        # trainee3 belongs to different team of the exercise thus can't read other teams logs
-        with self.assertRaises(PermissionDenied):
-            decorated(request, log_id=self.log.id)
-
-        request.user = self.instructor2
-        # instructor2 is not assigned to the exercise thus can't access the log
-        with self.assertRaises(PermissionDenied):
-            decorated(request, log_id=self.log.id)
-
-    def test_thread_protected(self):
-        request = self.factory.request()
-        decorated = extra_protected(Check.THREAD_ID)(self.thread_dummy)
-
-        request.user = self.trainee1
-        # trainee1 created thread thus can access it
-        self.assertEqual(decorated(request, thread_id=self.thread.id), 0)
-
-        request.user = self.trainee2
-        # trainee2 is in the same team as trainee1 thus can access it
-        self.assertEqual(decorated(request, thread_id=self.thread.id), 0)
-
-        request.user = self.instructor
-        # instructor is assigned to the exercise thus can access all threads of exercise
-        self.assertEqual(decorated(request, thread_id=self.thread.id), 0)
-
-        request.user = self.admin
-        # admin can access all thread
-        self.assertEqual(decorated(request, thread_id=self.thread.id), 0)
-
-        request.user = self.instructor2
-        # instructor2 is not assigned to the exercise where thread belongs thus cant access it
-        with self.assertRaises(PermissionDenied):
-            decorated(request, thread_id=self.thread.id)
-
-        request.user = self.trainee3
-        # trainee3 does not belong to any participating teams of the thread
-        with self.assertRaises(PermissionDenied):
-            decorated(request, thread_id=self.thread.id)
-
-    def test_visible_protected(self):
-        request = self.factory.request()
-        decorated = extra_protected(Check.VISIBLE_ONLY)(self.visibility_dummy)
-
-        request.user = self.instructor
-        # instructor can see both visible only and all data
-        self.assertEqual(decorated(request, visible_only=False), 0)
-        self.assertEqual(decorated(request, visible_only=True), 0)
+        self.assertIsNone(definition_access(request, self.definition.id))
 
         request.user = self.trainee1
-        # trainee can see only visible_only=True data
-        self.assertEqual(decorated(request, visible_only=True), 0)
         with self.assertRaises(PermissionDenied):
-            decorated(request, visible_only=False)
+            definition_access(request, self.definition.id)
diff --git a/aai/tests/special_case_tests.py b/aai/tests/special_case_tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9a27f32bfc96a10cd05b1d420dd401565444afb
--- /dev/null
+++ b/aai/tests/special_case_tests.py
@@ -0,0 +1,136 @@
+import os
+import shutil
+
+from django.conf import settings
+from django.test import override_settings
+
+from common_lib.graphql import (
+    GraphQLApiTestCase,
+    EmailThreadAction,
+    SendEmailAction,
+)
+from common_lib.test_utils import (
+    internal_upload_definition,
+    internal_create_exercise,
+)
+from exercise.models import Exercise
+from exercise_definition.models import Definition
+from running_exercise.lib.email_client import EmailClient
+from running_exercise.models import EmailThread
+from user.models import User
+
+TEST_DATA_STORAGE = "special_case_tests_data"
+
+
+@override_settings(
+    DATA_STORAGE=TEST_DATA_STORAGE,
+    FILE_STORAGE=os.path.join(TEST_DATA_STORAGE, "files"),
+)
+class SpecialCaseTests(GraphQLApiTestCase):
+    # These tests are temporary (Clueless) and should be removed
+    # after we add exercise/team context to all graphql requests
+    definition: Definition
+
+    exercise: Exercise
+
+    instructor1: User
+    instructor2: User
+    trainee1: User
+    trainee2: User
+
+    thread: EmailThread
+    all_thread: EmailThread
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.definition = internal_upload_definition("emails_definition")
+
+        cls.exercise = internal_create_exercise(cls.definition.id, 2)
+
+        cls.instructor1 = User.objects.create_staffuser(
+            "instructor1@instructor.com", "instructor"
+        )
+        cls.instructor2 = User.objects.create_staffuser(
+            "instructor2@instructor.com", "instructor"
+        )
+        cls.trainee1 = User.objects.create_user("user1@user.com", "user")
+        cls.trainee2 = User.objects.create_user("user2@user.com", "user")
+
+        cls.instructor1.definitions.add(cls.definition)
+
+        cls.instructor1.exercises.add(cls.exercise)
+
+        cls.trainee1.teams.add(cls.exercise.teams.first())
+
+        cls.thread = EmailClient.create_thread(
+            ["team-1@mail.com", "doe@mail.ex"], "test", cls.exercise.id
+        )
+        cls.all_thread = EmailClient.create_thread(
+            ["team-1@mail.com", "team-2@mail.com", "doe@mail.ex"],
+            "test",
+            cls.exercise.id,
+        )
+
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(settings.DATA_STORAGE)
+        super().tearDownClass()
+
+    def test_send_email(self):
+        action = SendEmailAction(
+            {
+                "thread_id": self.thread.id,
+                "sender_address": "team-1@mail.com",
+                "content": "hello",
+            },
+            {},
+        )
+
+        # valid
+        self.run_action(action, user=self.trainee1)
+
+        # trainee2 does not have access to the exercise
+        self.run_action(action, user=self.trainee2, should_throw=True)
+
+        self.trainee2.teams.add(self.exercise.teams.last())
+        # trainee2 impersonating team-1 without access to thread
+        self.run_action(action, user=self.trainee2, should_throw=True)
+
+        action.variables["thread_id"] = self.all_thread.id
+        self.run_action(action, user=self.trainee1)
+
+        # trainee2 impersonating team-1 with access to thread
+        self.run_action(action, user=self.trainee2, should_throw=True)
+
+        # trainee1 impersonating a definition address
+        action.variables["sender_address"] = "doe@mail.ex"
+        self.run_action(action, user=self.trainee1, should_throw=True)
+
+        # trainee2 impersonating a definition address without access to thread
+        action.variables["thread_id"] = self.thread.id
+        self.run_action(action, user=self.trainee2, should_throw=True)
+
+        self.run_action(action, user=self.instructor1)
+        self.run_action(action, user=self.instructor2, should_throw=True)
+
+    def test_email_thread(self):
+        action = EmailThreadAction({"thread_id": self.thread.id})
+
+        self.run_action(action, user=self.trainee1)
+        self.assertEqual(int(action.result["id"]), self.thread.id)
+
+        # trainee2 does not have access to the exercise
+        self.run_action(action, user=self.trainee2, should_throw=True)
+
+        # trainee2 now has access to the exercise, but not to this thread
+        self.trainee2.teams.add(self.exercise.teams.last())
+        self.run_action(action, user=self.trainee2, should_throw=True)
+
+        # both trainees can access this thread
+        action.variables["thread_id"] = self.all_thread.id
+        self.run_action(action, user=self.trainee1)
+        self.run_action(action, user=self.trainee2)
+
+        # instructor2 does not have access to this exercise
+        self.run_action(action, user=self.instructor1)
+        self.run_action(action, user=self.instructor2, should_throw=True)
diff --git a/aai/utils.py b/aai/utils.py
deleted file mode 100644
index c9b7286f8ba85c57e885934a6a0f9592baa252a2..0000000000000000000000000000000000000000
--- a/aai/utils.py
+++ /dev/null
@@ -1,337 +0,0 @@
-from enum import Enum
-from typing import Union, Dict, Callable, Optional
-
-from django.conf import settings
-from django.contrib.auth.models import AnonymousUser
-from graphene import ResolveInfo
-from rest_framework.exceptions import AuthenticationFailed, PermissionDenied
-from rest_framework.request import HttpRequest, Request
-
-from common_lib.logger import logger
-from common_lib.utils import get_model, InputObject
-from exercise.graphql_inputs import CreateExerciseInput
-from exercise.models import Team, Exercise
-from running_exercise.graphql_inputs import (
-    UseToolInput,
-    SendEmailInput,
-    SelectTeamInjectInput,
-)
-from running_exercise.models import EmailThread, ActionLog, EmailParticipant
-from user.models import User
-
-INSTRUCTOR = -1
-
-
-class Check(str, Enum):
-    TEAM_ID = "team_id"
-    EXERCISE_ID = "exercise_id"
-    DEFINITION_ID = "definition_id"
-    LOG_ID = "log_id"
-    THREAD_ID = "thread_id"
-    VISIBLE_ONLY = "visible_only"
-
-
-# This function is probably significantly overcomplicated
-def _get_action_name(func, *args) -> Optional[str]:
-    try:
-        # Should never happen
-        if len(args) == 0:
-            return "Unknown"
-
-        # This should correctly check whether the provided `func`
-        # is a decorator or the actual function
-        if hasattr(func, "__qualname__") and "decorator" in getattr(
-            func, "__qualname__"
-        ):
-            return None
-
-        # I don't know a name for this variable
-        tmp = args[0]
-
-        # This case should happen when the decorator is on a Query
-        if tmp is None:
-            # A query function should always have qualname, but just in case
-            if hasattr(func, "__qualname__"):
-                # qualname should give us a name in the format Query.resolve<name>
-                return getattr(func, "__qualname__")
-            # Should never happen
-            return "Unknown"
-
-        # This case works for mutations and subscriptions where it just gives us the class name
-        if hasattr(tmp, "__name__"):
-            return getattr(tmp, "__name__")
-
-        # Last case, should only happen for views
-        tmp_t = type(tmp)
-        if hasattr(tmp_t, "__name__"):
-            return getattr(tmp_t, "__name__")
-
-        # Again, should never happen as all of our API options are covered
-        return "Unknown"
-    # we CANNOT throw an exception in here
-    except Exception:
-        return "Unknown"
-
-
-def _get_user(*args, **kwargs) -> Union[AnonymousUser, User]:
-    """
-    Gets user object from known request types.
-
-    Args:
-        *args - non-keyworded arguments of request
-        **kwargs - keyword arguments of request
-    Returns:
-        Instance of User if request is authenticated else instance of AnonymousUser
-    """
-    if len(args) == 0:
-        return AnonymousUser()
-
-    request = args[-1]  # get request
-
-    if isinstance(request, ResolveInfo):
-        return request.context.user
-
-    if isinstance(request, HttpRequest) or isinstance(request, Request):
-        return request.user
-    return AnonymousUser()
-
-
-def _get_param_id_from_input_obj(
-    input_obj: InputObject, *args, **kwargs
-) -> int:
-    """
-    Helper function for retrieving id parameter from the current input objects.
-
-    Returns:
-        Id parameter needed for authorization from input object.
-    """
-    id_parameter = None
-    if isinstance(input_obj, (SelectTeamInjectInput, UseToolInput)):
-        id_parameter = input_obj.team_id
-
-    elif isinstance(input_obj, SendEmailInput):
-        # TODO: this needs rework
-        thread = get_model(EmailThread, id=input_obj.thread_id)
-        participant_team = EmailParticipant.objects.get(
-            address=input_obj.sender_address, exercise_id=thread.exercise_id
-        ).team
-        id_parameter = (
-            participant_team.id if participant_team is not None else INSTRUCTOR
-        )
-
-    elif isinstance(input_obj, CreateExerciseInput):
-        id_parameter = input_obj.definition_id
-
-    if id_parameter is None:
-        raise ValueError("Unexpected input object argument")
-    return int(id_parameter)
-
-
-def _check_team(user: User, team_id: int) -> bool:
-    if user.teams.filter(id=team_id).exists():
-        # user is in team which he wants to get data of
-        return True
-    exercises = user.exercises.all()
-    # user is instructor of the exercise where given team belongs
-    return Team.objects.filter(exercise__in=exercises, id=team_id).exists()
-
-
-def _check_exercise(user: User, exercise_id) -> bool:
-    if user.exercises.filter(id=exercise_id).exists():
-        # user is instructor of the exercise
-        return True
-    # user is trainee of the exercise
-    return user.teams.filter(exercise_id=exercise_id).exists()
-
-
-def _check_definition(user: User, definition_id) -> bool:
-    # user is maintainer of the definition
-    return user.definitions.filter(id=definition_id).exists()
-
-
-def _check_log(user: User, log_id) -> bool:
-    return _check_team(user, int(get_model(ActionLog, id=log_id).team_id))
-
-
-def _check_thread(user: User, thread_id) -> bool:
-    thread = get_model(EmailThread, id=thread_id)
-    if user.exercises.filter(id=thread.exercise_id).exists():
-        # user is instructor of the exercise with the thread
-        return True
-
-    participant_teams = thread.participants.exclude(team_id=None).values_list(
-        "team_id", flat=True
-    )
-    # user is trainee of the team participating in the thread
-    return user.teams.filter(id__in=participant_teams).exists()
-
-
-def _check_visible_only(user: User, visible_only: bool) -> bool:
-    return visible_only or user.group != User.AuthGroup.TRAINEE
-
-
-CHECK_MAPPING: Dict[Check, Callable] = {
-    Check.TEAM_ID: _check_team,
-    Check.EXERCISE_ID: _check_exercise,
-    Check.DEFINITION_ID: _check_definition,
-    Check.LOG_ID: _check_log,
-    Check.THREAD_ID: _check_thread,
-    Check.VISIBLE_ONLY: _check_visible_only,
-}
-
-
-def logged_in(func):
-    def wrapper(*args, **kwargs):
-        user = _get_user(*args, **kwargs)
-        if not user.is_anonymous:
-            return func(*args, **kwargs)
-        raise AuthenticationFailed("Authentication failed")
-
-    return wrapper
-
-
-def protected(required_group: User.AuthGroup):
-    """
-    Decorator that checks whether user of request has required authorization group.
-    If not, PermissionDenied exception is raised.
-
-    Args:
-        required_group: minimal required authorization group to access endpoint
-    """
-
-    def decorator(func):
-        def wrapper(*args, **kwargs):
-            action_name = _get_action_name(func, *args)
-            log_text = f"action: {action_name} | arguments: {kwargs}"
-
-            if settings.NOAUTH:
-                if action_name is not None:
-                    logger.info(log_text)
-                return func(*args, **kwargs)
-
-            user = _get_user(*args, **kwargs)
-            if action_name is not None:
-                log_text += f" | username: {user.username}"
-                logger.info(log_text)
-
-            if user.is_anonymous:
-                raise AuthenticationFailed("Authentication failed")
-
-            if user.group >= required_group:
-                return func(*args, **kwargs)
-            raise PermissionDenied("Permission denied")
-
-        return wrapper
-
-    return decorator
-
-
-def _input_object_checks(
-    user: User, param_id: int, input_obj: InputObject
-) -> bool:
-    """
-    Helper function for checking authorization restrications on input objects.
-    Returns:
-        True if one of the condition to access the data is met otherwise False.
-    """
-    if isinstance(input_obj, (UseToolInput, SelectTeamInjectInput)):
-        if isinstance(input_obj, UseToolInput):
-            exercise = get_model(Exercise, teams__in=[param_id])
-            # check if user has access to given tool (is in exercise
-            # containing the tool)
-            if not exercise.definition.tools.filter(
-                id=input_obj.tool_id
-            ).exists():
-                return False
-        return _check_team(user, param_id)
-
-    elif isinstance(input_obj, SendEmailInput):
-        if param_id == INSTRUCTOR:
-            return user.is_staff
-        return _check_team(user, param_id)
-
-    elif isinstance(input_obj, CreateExerciseInput):
-        return _check_definition(user, param_id)
-    return False
-
-
-def input_object_protected(obj_name: str):
-    """
-    Decorator for checking InputObject types evaluates whether user has access
-    to requested data based on the data from InputObject type.
-    Input objects MUST HAVE parmater with name given in "obj_name" parameter to
-    work properly.
-    Args:
-        obj_name: name of keyword argument of the input object of the resolver
-    """
-
-    def decorator(func):
-        def wrapper(*args, **kwargs):
-            action_name = _get_action_name(func, *args)
-            log_text = f"action: {action_name} | arguments: {kwargs}"
-            if settings.NOAUTH:
-                if action_name is not None:
-                    logger.info(log_text)
-                return func(*args, **kwargs)
-
-            user = _get_user(*args, **kwargs)
-
-            if action_name is not None:
-                log_text += f" | username: {user.username}"
-                logger.info(log_text)
-
-            if user.is_anonymous:
-                raise AuthenticationFailed("Authentication failed")
-
-            if user.is_superuser:
-                return func(*args, **kwargs)
-
-            input_obj = kwargs.get(obj_name, None)
-            if input_obj is None:
-                raise PermissionDenied("Permission denied")
-
-            # create_exercise_input: definition_id
-            # other input objects: team_id
-            param_id = _get_param_id_from_input_obj(input_obj, *args, **kwargs)
-
-            if _input_object_checks(user, param_id, input_obj):
-                return func(*args, **kwargs)
-            raise PermissionDenied("Permission denied")
-
-        return wrapper
-
-    return decorator
-
-
-def extra_protected(check: Check):
-    def decorator(func):
-        def wrapper(*args, **kwargs):
-            action_name = _get_action_name(func, *args)
-            log_text = f"action: {action_name} | arguments: {kwargs}"
-
-            if settings.NOAUTH:
-                if action_name is not None:
-                    logger.info(log_text)
-                return func(*args, **kwargs)
-
-            check_function = CHECK_MAPPING[check]
-            user = _get_user(*args, **kwargs)
-
-            if action_name is not None:
-                log_text += f" | username: {user.username}"
-                logger.info(log_text)
-
-            if user.is_anonymous:
-                raise AuthenticationFailed("Authentication failed")
-
-            param_id: Optional[int] = kwargs.get(check.value, None)
-            if param_id is None:
-                raise PermissionDenied("Permission denied")
-
-            if user.is_superuser or check_function(user, int(param_id)):
-                return func(*args, **kwargs)
-            raise PermissionDenied("Permission denied")
-
-        return wrapper
-
-    return decorator
diff --git a/common_lib/graphql/__init__.py b/common_lib/graphql/__init__.py
index 5e2cd30e6434a3d61d2cf738f7e1d88862eb3c93..4b320faa81e778d4a386d152c977423734e2212a 100644
--- a/common_lib/graphql/__init__.py
+++ b/common_lib/graphql/__init__.py
@@ -3,6 +3,7 @@ from .queries import (
     EmailContactsAction,
     ExerciseIdAction,
     EmailThreadsAction,
+    EmailThreadAction,
     TeamActionLogsAction,
     ExtendedTeamToolsAction,
     ExerciseConfigAction,
@@ -13,7 +14,6 @@ from .mutations import (
     CreateExerciseAction,
     CreateThreadAction,
     UseToolAction,
-    SelectTeamInjectAction,
     StartExerciseAction,
     DeleteExerciseAction,
     StopExerciseAction,
diff --git a/common_lib/graphql/mutations.py b/common_lib/graphql/mutations.py
index 8d405d9280bea35d7b62f9492052373f229d8d9c..2f8907cdd895d42ef19af7c1ca06d6e49b9c4241 100644
--- a/common_lib/graphql/mutations.py
+++ b/common_lib/graphql/mutations.py
@@ -12,7 +12,6 @@ from common_lib.utils import create_input
 from exercise.graphql_inputs import CreateExerciseInput, ConfigOverrideInput
 from running_exercise.graphql_inputs import (
     UseToolInput,
-    SelectTeamInjectInput,
     SendEmailInput,
     CustomInjectInput,
     OverlayInput,
@@ -80,25 +79,6 @@ class SendEmailAction(APIAction):
         create_input(SendEmailInput, **self.variables)
 
 
-class SelectTeamInjectAction(APIAction):
-    query_name = "selectTeamInject"
-
-    def __init__(self, variables: Dict[str, Any]):
-        self.variables = variables
-        self.query = GraphQLQuery(
-            "mutation",
-            self.query_name,
-            [("selectTeamInjectInput", "SelectTeamInjectInput!")],
-            Fragments.operation_done(),
-        )
-
-    def get_variables(self) -> Dict[str, Any]:
-        return {"selectTeamInjectInput": self.variables}
-
-    def validate(self) -> None:
-        create_input(SelectTeamInjectInput, **self.variables)
-
-
 class CreateExerciseAction(APIAction):
     query_name = "createExercise"
 
diff --git a/common_lib/graphql/queries.py b/common_lib/graphql/queries.py
index 18a71397407231b94d0cb8b67458c6ee8ae7eebb..f72d84692daaf20f250f7e0b0b25669909bd6575 100644
--- a/common_lib/graphql/queries.py
+++ b/common_lib/graphql/queries.py
@@ -40,6 +40,19 @@ class EmailThreadsAction(APIAction):
         )
 
 
+class EmailThreadAction(APIAction):
+    query_name = "emailThread"
+
+    def __init__(self, variables: Dict[str, Any]):
+        self.variables = variables
+        self.query = GraphQLQuery(
+            "query",
+            self.query_name,
+            [id_argument("thread")],
+            Fragments.email_thread(),
+        )
+
+
 class EmailContactsAction(APIAction):
     query_name = "emailContacts"
 
diff --git a/common_lib/subscription_handler.py b/common_lib/subscription_handler.py
index 7b40692acdaf53944681cda6a9a0bb04eda65e29..73d7570206ac4499deef3e70f31ad8dd2b2dac06 100644
--- a/common_lib/subscription_handler.py
+++ b/common_lib/subscription_handler.py
@@ -79,8 +79,7 @@ class SubscriptionHandler:
             milestone_states, team.exercise
         )
 
-        group = "all" if team is None else str(team.id)
-        cls._milestones.broadcast(payload=milestone_states, group=group)
+        cls._milestones.broadcast(payload=milestone_states, group=str(team.id))
 
     @classmethod
     def broadcast_team_visible_milestones(
diff --git a/exercise/schema/mutation.py b/exercise/schema/mutation.py
index ba2f8e2cc5e5740fe38b7c842700ed8858776d78..73034a790e7358f89dcdf36179009476d7a79754 100644
--- a/exercise/schema/mutation.py
+++ b/exercise/schema/mutation.py
@@ -1,14 +1,14 @@
 import graphene
-
 from django.conf import settings
 
+from aai.access import exercise_access, definition_access
+from aai.decorators import protected
 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.utils import protected, extra_protected, input_object_protected, Check
-from user.schema.validators import validate_instructor_assigning
 from user.models import InstructorOfExercise, User
+from user.schema.validators import validate_instructor_assigning
 
 
 class CreateExerciseMutation(graphene.Mutation):
@@ -21,13 +21,15 @@ class CreateExerciseMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @input_object_protected("create_exercise_input")
     def mutate(
         cls,
         root,
         info,
         create_exercise_input: CreateExerciseInput,
     ) -> graphene.Mutation:
+        definition_access(
+            info.context, int(create_exercise_input.definition_id)
+        )
         user = info.context.user
         exercise = ExerciseManager.create_exercise(
             create_exercise_input, info.context
@@ -50,8 +52,8 @@ class DeleteExerciseMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def mutate(cls, root, info, exercise_id: str):
+        exercise_access(info.context, int(exercise_id))
         ExerciseManager.delete_exercise(int(exercise_id))
         return DeleteExerciseMutation(operation_done=True)
 
@@ -64,8 +66,8 @@ class DeleteDefinitionMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.DEFINITION_ID)
     def mutate(cls, root, info, definition_id: str):
+        definition_access(info.context, int(definition_id))
         DefinitionManager.delete_definition(int(definition_id))
         return DeleteDefinitionMutation(operation_done=True)
 
diff --git a/exercise/schema/query.py b/exercise/schema/query.py
index 31d36ce8f8c52c99ebd61989ad7a45c0fd0bfa22..01db958707cafb72d570d578149a69492f8fa684 100644
--- a/exercise/schema/query.py
+++ b/exercise/schema/query.py
@@ -3,7 +3,8 @@ from typing import Optional, List
 import graphene
 from django.conf import settings
 
-from aai.utils import protected, extra_protected, Check
+from aai.access import team_access, exercise_access, definition_access
+from aai.decorators import protected
 from common_lib.schema_types import (
     ExerciseType,
     DefinitionType,
@@ -132,8 +133,8 @@ class Query(graphene.ObjectType):
         return exercises
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_exercise_id(self, info, exercise_id: str) -> Optional[Exercise]:
+        exercise_access(info.context, int(exercise_id))
         return Exercise.objects.filter(id=exercise_id).first()
 
     @protected(User.AuthGroup.INSTRUCTOR)
@@ -150,18 +151,18 @@ class Query(graphene.ObjectType):
         return []
 
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.DEFINITION_ID)
     def resolve_definition(self, info, definition_id: str) -> Definition:
+        definition_access(info.context, int(definition_id))
         return get_model(Definition, id=definition_id)
 
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_injects(self, info, exercise_id: str) -> List[Inject]:
+        exercise_access(info.context, int(exercise_id))
         return Inject.objects.filter(definition__exercises__in=[exercise_id])
 
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_milestones(self, info, exercise_id: str) -> List[Milestone]:
+        exercise_access(info.context, int(exercise_id))
         exercise = get_model(
             Exercise,
             id=int(exercise_id),
@@ -174,17 +175,18 @@ class Query(graphene.ObjectType):
         return get_model(FileInfo, id=file_info_id)
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.TEAM_ID)
     def resolve_team_uploaded_files(self, info, team_id: str) -> List[FileInfo]:
+        team_access(info.context, int(team_id))
         return FileInfo.objects.filter(team__team_id=team_id)
 
     @protected(User.AuthGroup.TRAINEE)
     def resolve_channel(self, info, channel_id: str) -> Channel:
+        # TODO: Add resolving of channel based on exercise_id (including access check)
         return get_model(Channel, id=channel_id)
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_exercise_channels(
         self, info, exercise_id: str
     ) -> List[Channel]:
+        exercise_access(info.context, int(exercise_id))
         return Channel.objects.filter(definition__exercises__in=[exercise_id])
diff --git a/exercise/subscription.py b/exercise/subscription.py
index 9f7c91c016b2c7c2fb21175ab65a99e04b69fb67..05f4a0775eac9923e65be85f0fb0b55a4953b60b 100644
--- a/exercise/subscription.py
+++ b/exercise/subscription.py
@@ -1,10 +1,10 @@
 import channels_graphql_ws
 import graphene
-from common_lib.schema_types import ExerciseEventTypeEnum
 
+from aai.decorators import protected
+from common_lib.schema_types import ExerciseEventTypeEnum
 from common_lib.schema_types import ExerciseType
 from user.models import User
-from aai.utils import protected
 
 NOTIFICATION_QUEUE_LIMIT = 64
 
diff --git a/exercise/views.py b/exercise/views.py
index a2f6354a4c792a5ed7e0b05c744d14da74a01075..321a949cb840b3f5794797734fda70267c813c8b 100644
--- a/exercise/views.py
+++ b/exercise/views.py
@@ -6,25 +6,27 @@ from rest_framework import parsers
 from rest_framework.response import Response
 from rest_framework.views import APIView
 
-from user.models import User
-from aai.utils import protected, extra_protected, Check
+from aai.access import exercise_access
+from aai.decorators import protected
 from common_lib.exceptions import ApiException
 from exercise.lib.export_import import export_database, import_database
 from exercise.lib.log_manager import LogManager
+from user.models import User
 
 
 class RetrieveExerciseLogsView(APIView):
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def get(self, request, *args, **kwargs):
         """
         Get logs of all teams in a game.
         """
-        exercise_id = self.kwargs.get("exercise_id")
+        exercise_id = int(self.kwargs.get("exercise_id"))
         anonymize = request.GET.get("anonymize") is not None
         force = request.GET.get("force") is not None
+
+        exercise_access(request, exercise_id)
         archive_path = LogManager(
-            exercise_id=int(exercise_id), anonymize=anonymize
+            exercise_id=exercise_id, anonymize=anonymize
         ).create_exercise_logs(force)
         return FileResponse(open(archive_path, "rb"), as_attachment=True)
 
diff --git a/exercise_definition/views.py b/exercise_definition/views.py
index a230998f5fbf25b888ae9844da3db4582ee688bd..5f85ece025c696a9bc01d24e41c8246c8b294083 100644
--- a/exercise_definition/views.py
+++ b/exercise_definition/views.py
@@ -6,7 +6,7 @@ from rest_framework.request import Request
 from rest_framework.response import Response
 from rest_framework.views import APIView
 
-from aai.utils import protected
+from aai.decorators import protected
 from common_lib.exceptions import ApiException
 from common_lib.logger import logger, log_user_msg
 from exercise_definition.lib import DefinitionUploader, DefinitionParser
diff --git a/running_exercise/graphql_inputs.py b/running_exercise/graphql_inputs.py
index f523b178777c19eec392424fe40e0f2860672ff0..164886b4f78c791fb1f635e48e7529850a6fa157 100644
--- a/running_exercise/graphql_inputs.py
+++ b/running_exercise/graphql_inputs.py
@@ -19,27 +19,6 @@ class UseToolInput(UseToolBase, graphene.InputObjectType):
     pass
 
 
-class SelectTeamInjectBase:
-    team_id = graphene.ID(required=True)
-    selection_id = graphene.ID(required=True)
-    option_email = graphene.Boolean(required=True)
-    content = graphene.String(required=True)
-    activate_milestone = graphene.String(required=False, default_value="")
-    deactivate_milestone = graphene.String(required=False, default_value="")
-    file_id = graphene.UUID(required=False, default_value="")
-    sender = graphene.String(required=False, default_value="")
-    subject = graphene.String(required=False, default_value="")
-    repeat = graphene.Int(required=False, default_value=0)
-
-
-class SelectTeamInjectType(SelectTeamInjectBase, graphene.ObjectType):
-    pass
-
-
-class SelectTeamInjectInput(SelectTeamInjectBase, graphene.InputObjectType):
-    pass
-
-
 class OverlayInput(graphene.InputObjectType):
     duration = graphene.Int(required=True)
 
diff --git a/running_exercise/lib/email_client.py b/running_exercise/lib/email_client.py
index 6311ab467312159a9267454a9bedd8383045390e..7bf5392a9abd7d23a85b0d55c924817a3cf39499 100644
--- a/running_exercise/lib/email_client.py
+++ b/running_exercise/lib/email_client.py
@@ -252,26 +252,6 @@ class EmailClient:
             templates.extend(email_address.templates.all())
         return templates
 
-    @staticmethod
-    def get_thread_templates(thread_id: int) -> List[EmailTemplate]:
-        thread = get_model(EmailThread, id=thread_id)
-        definition_participant = thread.get_definition_participant()
-        if definition_participant is None:
-            return []
-
-        return list(
-            definition_participant.get_definition_address().templates.all()
-        )
-
-    @staticmethod
-    def get_instructor_email_addresses(thread_id: int) -> List[str]:
-        thread = get_model(EmailThread, id=thread_id)
-        return list(
-            thread.participants.filter(team_id__isnull=True).values_list(
-                "address", flat=True
-            )
-        )
-
     @staticmethod
     def validate_email_address(exercise_id: int, address: str) -> bool:
         return EmailParticipant.objects.filter(
diff --git a/running_exercise/lib/loop_thread.py b/running_exercise/lib/loop_thread.py
index 3cf68e90294aa6f398f039359ff8406f23b79c5c..4d19297d173580d58fe7800271692dbcaf227849 100644
--- a/running_exercise/lib/loop_thread.py
+++ b/running_exercise/lib/loop_thread.py
@@ -71,7 +71,8 @@ class LoopThread(Thread):
             )
         finally:
             SubscriptionHandler.broadcast_exercises(
-                self.updater.exercise, ExerciseEventTypeEnum.modify()
+                get_model(Exercise, id=self.updater.exercise.id),
+                ExerciseEventTypeEnum.modify(),
             )
             SubscriptionHandler.broadcast_exercise_loop(
                 self.updater.exercise, False
diff --git a/running_exercise/lib/team_action_handler.py b/running_exercise/lib/team_action_handler.py
index b1a2fa9d3800616565a808cf6a3e5964be87d397..50f24dac5c43872283b0308bc3a9ca42be30e1e0 100644
--- a/running_exercise/lib/team_action_handler.py
+++ b/running_exercise/lib/team_action_handler.py
@@ -30,6 +30,7 @@ from user.models import User
 
 
 def _parse_selected_tool(team: Team, tool_id: int) -> Tool:
+    # TODO: check if tool is in the exercise
     tool = get_model(Tool, id=tool_id)
 
     if not has_role(team.role, tool.roles.split(" ")):
diff --git a/running_exercise/schema/mutation.py b/running_exercise/schema/mutation.py
index eb86b1268c0aca6d54b35e72250d8bc4ba24b523..f6059ddfe3f3bcc235b8545c4a3422d672e6d351 100644
--- a/running_exercise/schema/mutation.py
+++ b/running_exercise/schema/mutation.py
@@ -1,11 +1,14 @@
 from typing import List
 
 import graphene
-from django.conf import settings
+from rest_framework.exceptions import PermissionDenied
 
-from aai.utils import protected, extra_protected, input_object_protected, Check
+from aai.access import user_from_context, team_access, exercise_access
+from aai.decorators import protected
 from common_lib.logger import logger
 from common_lib.schema_types import ExerciseType, EmailThreadType
+from common_lib.utils import get_model
+from exercise.models import EmailParticipant
 from running_exercise.graphql_inputs import (
     UseToolInput,
     SendEmailInput,
@@ -19,6 +22,7 @@ from running_exercise.lib.instructor_action_handler import (
 )
 from running_exercise.lib.milestone_handler import instructor_modify_milestone
 from running_exercise.lib.team_action_handler import TeamActionHandler
+from running_exercise.models import EmailThread
 from running_exercise.questionnaire_handler import QuestionnaireHandler
 from user.models import User
 
@@ -31,15 +35,17 @@ class UseToolMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.TRAINEE)
-    @input_object_protected("use_tool_input")
     def mutate(
         cls,
         root,
         info,
         use_tool_input: UseToolInput,
     ) -> graphene.Mutation:
-        user = None if settings.NOAUTH else info.context.user
-        TeamActionHandler.perform_action(use_tool_input, user)
+        team_access(info.context, int(use_tool_input.team_id))
+
+        TeamActionHandler.perform_action(
+            use_tool_input, user_from_context(info.context)
+        )
         return UseToolMutation(operation_done=True)
 
 
@@ -59,7 +65,6 @@ class CreateThreadMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.EXERCISE_ID)
     def mutate(
         cls,
         root,
@@ -68,6 +73,7 @@ class CreateThreadMutation(graphene.Mutation):
         subject: str,
         exercise_id: str,
     ):
+        exercise_access(info.context, int(exercise_id))
         return CreateThreadMutation(
             thread=EmailClient.create_thread(
                 participant_addresses, subject, exercise_id
@@ -83,14 +89,30 @@ class SendEmailMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.TRAINEE)
-    @input_object_protected("send_email_input")
     def mutate(
         cls,
         root,
         info,
         send_email_input: SendEmailInput,
     ):
-        user = None if settings.NOAUTH else info.context.user
+        thread = get_model(EmailThread, id=send_email_input.thread_id)
+        participant = get_model(
+            EmailParticipant,
+            address=send_email_input.sender_address,
+            exercise_id=thread.exercise_id,
+        )
+        user = user_from_context(info.context)
+        if user is None:
+            # should never happen
+            raise PermissionDenied("Unauthorized access")
+        if participant.is_definition():
+            if user.group == User.AuthGroup.TRAINEE:
+                raise PermissionDenied(
+                    f"User cannot send email as this address ({send_email_input.sender_address})"
+                )
+            exercise_access(info.context, thread.exercise_id)
+        else:
+            team_access(info.context, participant.team_id)
         EmailClient.send_email(send_email_input, user)
         return SendEmailMutation(operation_done=True)
 
@@ -104,8 +126,8 @@ class MoveExerciseTimeMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def mutate(cls, root, info, exercise_id: str, time_diff: int):
+        exercise_access(info.context, int(exercise_id))
         exercise = ExerciseLoop.move_time(int(exercise_id), time_diff)
         return MoveExerciseTimeMutation(exercise=exercise)
 
@@ -118,8 +140,8 @@ class StartExerciseMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def mutate(cls, root, info, exercise_id: str):
+        exercise_access(info.context, int(exercise_id))
         exercise = ExerciseLoop.start(int(exercise_id))
         return StartExerciseMutation(exercise=exercise)
 
@@ -132,8 +154,8 @@ class StopExerciseMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def mutate(cls, root, info, exercise_id: str):
+        exercise_access(info.context, int(exercise_id))
         exercise = ExerciseLoop.stop(int(exercise_id))
         return StopExerciseMutation(exercise=exercise)
 
@@ -152,8 +174,8 @@ class ModifyMilestoneMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.TEAM_ID)
     def mutate(cls, root, info, team_id: str, milestone: str, activate: bool):
+        team_access(info.context, int(team_id))
         instructor_modify_milestone(
             team_id=int(team_id), milestone=milestone, activate=activate
         )
@@ -172,6 +194,7 @@ class AnswerQuestionnaireMutation(graphene.Mutation):
     @classmethod
     @protected(User.AuthGroup.TRAINEE)
     def mutate(cls, root, info, quest_input: QuestionnaireInput):
+        team_access(info.context, int(quest_input.team_id))
         QuestionnaireHandler.answer_questionnaire(quest_input)
         return AnswerQuestionnaireMutation(operation_done=True)
 
@@ -187,7 +210,8 @@ class SendCustomInjectMutation(graphene.Mutation):
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
     def mutate(cls, root, info, custom_inject_input: CustomInjectInput):
-        user = None if settings.NOAUTH else info.context.user
+        exercise_access(info.context, int(custom_inject_input.exercise_id))
+        user = user_from_context(info.context)
         InstructorActionHandler.send_custom_inject(custom_inject_input, user)
         return SendCustomInjectMutation(operation_done=True)
 
diff --git a/running_exercise/schema/query.py b/running_exercise/schema/query.py
index 82efe25eb5cfe614c0edfeb907b4322181acfae8..f4609cacc8b6110ab99f32d92fabcc40b15dbc06 100644
--- a/running_exercise/schema/query.py
+++ b/running_exercise/schema/query.py
@@ -2,9 +2,10 @@ from typing import List
 
 import graphene
 from django.db.models import QuerySet
+from rest_framework.exceptions import PermissionDenied
 
-from user.models import User
-from aai.utils import protected, extra_protected, Check
+from aai.access import team_access, exercise_access, user_from_context
+from aai.decorators import protected
 from common_lib.exceptions import RunningExerciseOperationException
 from common_lib.schema_types import (
     ActionLogType,
@@ -39,6 +40,7 @@ from running_exercise.lib.exercise_loop import ExerciseLoop
 from running_exercise.lib.milestone_handler import get_milestone_states
 from running_exercise.lib.utils import get_running_exercise
 from running_exercise.models import ActionLog, EmailThread
+from user.models import User
 
 
 class Query(graphene.ObjectType):
@@ -201,8 +203,9 @@ class Query(graphene.ObjectType):
     )
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.TEAM_ID)
     def resolve_team(self, info, team_id: str) -> Team:
+        team_access(info.context, int(team_id))
+
         return get_model(Team, id=int(team_id))
 
     @protected(User.AuthGroup.TRAINEE)
@@ -211,11 +214,12 @@ class Query(graphene.ObjectType):
         running_exercise = get_running_exercise()
         if running_exercise is None:
             raise RunningExerciseOperationException("No exercise is running.")
+        exercise_access(info.context, running_exercise.id)
         return [team.role for team in running_exercise.teams.all()]
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.TEAM_ID)
     def resolve_team_tools(self, info, team_id: str) -> List[Tool]:
+        team_access(info.context, int(team_id))
         team = get_model(Team, id=int(team_id))
 
         return [
@@ -225,8 +229,8 @@ class Query(graphene.ObjectType):
         ]
 
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.TEAM_ID)
     def resolve_extended_team_tools(self, info, team_id: str) -> List[Tool]:
+        team_access(info.context, int(team_id))
         team = get_model(Team, id=int(team_id))
 
         return [
@@ -236,90 +240,123 @@ class Query(graphene.ObjectType):
         ]
 
     @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))
+        log = get_model(ActionLog, id=int(log_id))
+        team_access(info.context, log.team_id)
+        return log
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.TEAM_ID)
     def resolve_team_action_logs(
         self, info, team_id: str
     ) -> QuerySet[ActionLog]:
+        team_access(info.context, int(team_id))
         return ActionLog.objects.filter(team_id=team_id)
 
+    @protected(User.AuthGroup.TRAINEE)
     def resolve_team_channel_logs(
         self, info, team_id: str, channel_id: str
     ) -> QuerySet[ActionLog]:
+        team_access(info.context, int(team_id))
         return ActionLog.objects.filter(team_id=team_id, channel_id=channel_id)
 
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.TEAM_ID)
-    @extra_protected(Check.VISIBLE_ONLY)
     def resolve_team_milestones(
         self, info, team_id: str, visible_only: bool = True
     ) -> List[MilestoneState]:
+        team_access(info.context, int(team_id))
         return get_milestone_states(int(team_id), visible_only)
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.VISIBLE_ONLY)
     def resolve_email_contacts(
         self, info, visible_only: bool = True
     ) -> QuerySet[EmailParticipant]:
+        if (
+            user := user_from_context(info.context)
+        ) and user.group == User.AuthGroup.TRAINEE:
+            return EmailClient.get_contacts(True)
         return EmailClient.get_contacts(visible_only)
 
     @protected(User.AuthGroup.TRAINEE)
     def resolve_email_contact(
         self, info, participant_id: str
     ) -> EmailParticipant:
-        return get_model(EmailParticipant, id=int(participant_id))
+        participant = get_model(EmailParticipant, id=int(participant_id))
+        exercise_access(info.context, participant.exercise_id)
+        return participant
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.TEAM_ID)
     def resolve_email_threads(
         self, info, team_id: str
     ) -> QuerySet[EmailThread]:
+        team_access(info.context, int(team_id))
         return EmailClient.get_threads(int(team_id))
 
     @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))
+        thread = get_model(EmailThread, id=int(thread_id))
+        user = user_from_context(info.context)
+        exercise_access(info.context, thread.exercise_id)
+        # :)
+        if user is not None and user.group == User.AuthGroup.TRAINEE:
+            in_team = thread.participants.filter(
+                team__users__user_id__in=[user.id]
+            ).exists()
+            if not in_team:
+                raise PermissionDenied(
+                    f"User does not have access to this email thread ({thread_id})"
+                )
+
+        return thread
 
-    @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.THREAD_ID)
+    @protected(User.AuthGroup.INSTRUCTOR)
     def resolve_email_addresses(self, info, thread_id: str) -> List[str]:
-        return EmailClient.get_instructor_email_addresses(int(thread_id))
+        thread = get_model(EmailThread, id=thread_id)
+        exercise_access(info.context, thread.exercise_id)
+        return list(
+            thread.participants.filter(team_id__isnull=True).values_list(
+                "address", flat=True
+            )
+        )
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_validate_email_address(
         self, info, exercise_id: str, address: str
     ) -> bool:
+        exercise_access(info.context, int(exercise_id))
         return EmailClient.validate_email_address(int(exercise_id), address)
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.TEAM_ID)
     def resolve_team_email_participant(
         self, info, team_id: str
     ) -> EmailParticipant:
+        team_access(info.context, int(team_id))
         return EmailClient.get_team_participant(int(team_id))
 
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_email_templates(
         self, info, exercise_id: str, email_addresses: List[str]
     ) -> List[EmailTemplate]:
+        exercise_access(info.context, int(exercise_id))
         return EmailClient.get_email_templates(exercise_id, email_addresses)
 
     @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))
+        thread = get_model(EmailThread, id=thread_id)
+        exercise_access(info.context, thread.exercise_id)
+
+        definition_participant = thread.get_definition_participant()
+        if definition_participant is None:
+            return []
+
+        return list(
+            definition_participant.get_definition_address().templates.all()
+        )
 
     @protected(User.AuthGroup.INSTRUCTOR)
     def resolve_thread_template(self, info, template_id: str) -> EmailTemplate:
+        # TODO: not secured
         return get_model(EmailTemplate, id=int(template_id))
 
     @protected(User.AuthGroup.TRAINEE)
@@ -336,8 +373,8 @@ class Query(graphene.ObjectType):
         return max(0, exercise_duration_s - int(running_exercise.elapsed_s))
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_exercise_config(self, info, exercise_id: str) -> GrapheneConfig:
+        exercise_access(info.context, int(exercise_id))
         exercise = get_model(Exercise, id=int(exercise_id))
 
         config: Config = exercise.config
@@ -352,49 +389,49 @@ class Query(graphene.ObjectType):
         )
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_exercise_loop_running(self, info, exercise_id: str) -> bool:
+        exercise_access(info.context, int(exercise_id))
         return ExerciseLoop.is_running(int(exercise_id))
 
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_analytics_milestones(
         self, info, exercise_id: str
     ) -> QuerySet[MilestoneState]:
+        exercise_access(info.context, int(exercise_id))
         return MilestoneState.objects.filter(
             team_state__exercise_id=int(exercise_id)
         )
 
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_analytics_action_logs(
         self, info, exercise_id: str
     ) -> QuerySet[ActionLog]:
+        exercise_access(info.context, int(exercise_id))
         exercise = get_model(Exercise, id=int(exercise_id))
 
         return ActionLog.objects.filter(team__in=exercise.teams.all())
 
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_analytics_email_threads(
         self, info, exercise_id: str
     ) -> QuerySet[EmailThread]:
+        exercise_access(info.context, int(exercise_id))
         return EmailThread.objects.filter(
             exercise_id=exercise_id
         ).prefetch_related("emails")
 
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_exercise_tools(self, info, exercise_id: str) -> List[Tool]:
+        exercise_access(info.context, int(exercise_id))
         exercise = get_model(Exercise, id=int(exercise_id))
 
         return exercise.definition.tools.all()
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.TEAM_ID)
     def resolve_questionnaire_state(
         self, info, team_id: str, questionnaire_id: str
     ) -> TeamQuestionnaireState:
+        team_access(info.context, int(team_id))
         return get_model(
             TeamQuestionnaireState,
             team_id=team_id,
@@ -402,17 +439,17 @@ class Query(graphene.ObjectType):
         )
 
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.TEAM_ID)
     def resolve_team_questionnaires(
         self, info, team_id: str
     ) -> List[TeamQuestionnaireState]:
+        team_access(info.context, int(team_id))
         return TeamQuestionnaireState.objects.filter(team_id=team_id)
 
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def resolve_exercise_questionnaires(
         self, info, exercise_id: str
     ) -> List[TeamQuestionnaireState]:
+        exercise_access(info.context, int(exercise_id))
         return TeamQuestionnaireState.objects.filter(
             team__exercise_id=exercise_id
         )
@@ -421,6 +458,7 @@ class Query(graphene.ObjectType):
     def resolve_team_learning_objectives(
         self, info, team_id: str
     ) -> List[TeamLearningObjective]:
+        team_access(info.context, int(team_id))
         return TeamLearningObjective.objects.filter(
             team_state__teams__in=[team_id]
         ).prefetch_related("objective", "activities")
diff --git a/running_exercise/subscription.py b/running_exercise/subscription.py
index 68f1078dd62b2e7edd8f7a9992fb4f59007b0089..44a1584a8c1e34993ea6179b0f670ba891efcbb0 100644
--- a/running_exercise/subscription.py
+++ b/running_exercise/subscription.py
@@ -1,8 +1,8 @@
 import channels_graphql_ws
 import graphene
 
-from user.models import User
-from aai.utils import protected, extra_protected, Check
+from aai.access import exercise_access, team_access
+from aai.decorators import protected
 from common_lib.schema_types import (
     ActionLogType,
     MilestoneStateType,
@@ -11,6 +11,7 @@ from common_lib.schema_types import (
 )
 from common_lib.utils import get_model, get_subscription_group
 from exercise.models import Team, Exercise
+from user.models import User
 
 NOTIFICATION_QUEUE_LIMIT = 64
 
@@ -25,8 +26,8 @@ class ExerciseLoopRunningSubscription(channels_graphql_ws.Subscription):
 
     @staticmethod
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.EXERCISE_ID)
     def subscribe(root, info, exercise_id: str):
+        exercise_access(info.context, int(exercise_id))
         get_model(Exercise, id=int(exercise_id))
         return [exercise_id]
 
@@ -45,8 +46,8 @@ class ActionLogsSubscription(channels_graphql_ws.Subscription):
 
     @staticmethod
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.TEAM_ID)
     def subscribe(root, info, team_id: str):
+        team_access(info.context, int(team_id))
         get_model(Team, id=int(team_id))
         return [team_id]
 
@@ -69,11 +70,10 @@ class MilestonesSubscription(channels_graphql_ws.Subscription):
 
     @staticmethod
     @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):
+        team_access(info.context, int(team_id))
         get_model(Team, id=int(team_id))
-        return ["all", get_subscription_group(int(team_id), visible_only)]
+        return [get_subscription_group(int(team_id), visible_only)]
 
     @staticmethod
     def publish(payload, info, team_id: str, visible_only: bool = True):
@@ -89,8 +89,8 @@ class EmailThreadSubscription(channels_graphql_ws.Subscription):
 
     @staticmethod
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.TEAM_ID)
     def subscribe(root, info, team_id: str):
+        team_access(info.context, int(team_id))
         get_model(Team, id=int(team_id))
         return [team_id]
 
@@ -108,8 +108,8 @@ class AnalyticsMilestonesSubscription(channels_graphql_ws.Subscription):
 
     @staticmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def subscribe(root, info, exercise_id: str):
+        exercise_access(info.context, int(exercise_id))
         get_model(Exercise, id=int(exercise_id))
         return [exercise_id]
 
@@ -127,8 +127,8 @@ class AnalyticsActionLogsSubscription(channels_graphql_ws.Subscription):
 
     @staticmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def subscribe(root, info, exercise_id: str):
+        exercise_access(info.context, int(exercise_id))
         get_model(Exercise, id=int(exercise_id))
         return [exercise_id]
 
@@ -146,8 +146,8 @@ class AnalyticsEmailThreadSubscription(channels_graphql_ws.Subscription):
 
     @staticmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def subscribe(root, info, exercise_id: str):
+        exercise_access(info.context, int(exercise_id))
         get_model(Exercise, id=int(exercise_id))
         return [exercise_id]
 
@@ -167,8 +167,8 @@ class TeamQuestionnaireStateSubscription(channels_graphql_ws.Subscription):
 
     @staticmethod
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.TEAM_ID)
     def subscribe(root, info, team_id: str):
+        team_access(info.context, int(team_id))
         get_model(Team, id=int(team_id))
         return [team_id]
 
diff --git a/running_exercise/views.py b/running_exercise/views.py
index 230321bfca72757c05810ad7927201d6cfbb0167..0ce464b5174b400a9739c779990904465aec0314 100644
--- a/running_exercise/views.py
+++ b/running_exercise/views.py
@@ -3,18 +3,18 @@ from rest_framework import parsers
 from rest_framework.response import Response
 from rest_framework.views import APIView
 
-from user.models import User
-from aai.utils import protected, extra_protected, Check
+from aai.access import team_access
+from aai.decorators import protected
 from common_lib.exceptions import ApiException
 from running_exercise.lib.file_handler import (
     upload_file,
     get_uploaded_file,
 )
+from user.models import User
 
 
 class GetFileView(APIView):
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.TEAM_ID)
     def get(self, request, *args, **kwargs):
         """
         Get the requested file. If the exercise is running and parameter 'instructor' is not specified,
@@ -27,6 +27,7 @@ class GetFileView(APIView):
         if file_id is None or team_id is None:
             raise ApiException("Invalid request, missing required parameters")
 
+        team_access(request, team_id)
         opened_file, file_name = get_uploaded_file(team_id, file_id, instructor)
 
         # according to Django docs, the `opened_file`
@@ -41,7 +42,6 @@ class UploadFileView(APIView):
     parser_classes = [parsers.MultiPartParser]
 
     @protected(User.AuthGroup.TRAINEE)
-    @extra_protected(Check.TEAM_ID)
     def post(self, request, *args, **kwargs):
         """Upload a file as a team."""
         file = request.FILES.get("file")
@@ -49,7 +49,8 @@ class UploadFileView(APIView):
         if file is None or team_id is None:
             raise ApiException("Invalid request, missing required parameters")
 
-        file_id = upload_file(file, team_id)
+        team_access(request, int(team_id))
+        file_id = upload_file(file, int(team_id))
 
         return Response(
             {
diff --git a/ttxbackend/schema.py b/ttxbackend/schema.py
index ec153b141cfb3f260663750e69c73ade75ced33c..2351197c7d15b7b015cc9d39a78ac00e2fd5983a 100644
--- a/ttxbackend/schema.py
+++ b/ttxbackend/schema.py
@@ -9,7 +9,6 @@ from exercise.schema import (
 from exercise.subscription import Subscription as ExerciseSubscription
 from running_exercise.graphql_inputs import (
     UseToolType,
-    SelectTeamInjectType,
     SendEmailType,
     CustomInjectType,
 )
@@ -43,7 +42,6 @@ schema = graphene.Schema(
     subscription=Subscription,
     types=[
         UseToolType,
-        SelectTeamInjectType,
         SendEmailType,
         ConfigOverrideType,
         CreateExerciseType,
diff --git a/user/schema/mutation.py b/user/schema/mutation.py
index 3f186ffab666766dafb6976772403485f6a96220..f9447aafbf48847c2a8bf41a6f33606bb4aa9352 100644
--- a/user/schema/mutation.py
+++ b/user/schema/mutation.py
@@ -1,14 +1,16 @@
 from typing import List
 
 import graphene
-from django.utils.crypto import get_random_string
 from django.conf import settings
+from django.utils.crypto import get_random_string
 
-from aai.utils import protected, extra_protected, Check
+from aai.access import team_access, exercise_access, definition_access
+from aai.decorators import protected
 from common_lib.logger import logger, log_user_msg
 from common_lib.schema_types import UserType
 from common_lib.utils import get_model
 from exercise.models import Team, Exercise, Definition
+from user.email.email_sender import send_credentials
 from user.graphql_inputs import ChangeUserInput
 from user.models import UserInTeam, InstructorOfExercise, DefinitionAccess, User
 from user.schema.validators import (
@@ -21,7 +23,6 @@ from user.schema.validators import (
     validate_credentials_regeneration,
     execute_change_userdata,
 )
-from user.email.email_sender import send_credentials
 
 
 class AssignUsersToTeamMutation(graphene.Mutation):
@@ -40,11 +41,14 @@ class AssignUsersToTeamMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.TEAM_ID)
     def mutate(
         cls, root, info, user_ids: List[str], team_id: str
     ) -> graphene.Mutation:
+        # TODO: allow assigning instructors and when assigning an instructor,
+        # create entry in InstructorOfExercise instead of UserInTeam
         team = get_model(Team, id=team_id)
+        exercise_access(info.context, team.exercise_id)
+
         users = validate_team_assigning(user_ids, team)
         UserInTeam.objects.bulk_create(
             UserInTeam(user=user, team=team) for user in users
@@ -73,10 +77,10 @@ class RemoveUsersFromTeamMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.TEAM_ID)
     def mutate(
         cls, root, info, user_ids: List[str], team_id: str
     ) -> graphene.Mutation:
+        team_access(info.context, int(team_id))
         team = get_model(Team, id=team_id)
         users = validate_team_removing(user_ids, team)
         UserInTeam.objects.filter(user__in=users, team=team).delete()
@@ -104,10 +108,10 @@ class AssignInstructorsToExercise(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def mutate(
         cls, root, info, user_ids: List[str], exercise_id: str
     ) -> graphene.Mutation:
+        exercise_access(info.context, int(exercise_id))
         exercise = get_model(Exercise, id=exercise_id)
         users = validate_instructor_assigning(user_ids, exercise)
         InstructorOfExercise.objects.bulk_create(
@@ -137,10 +141,10 @@ class RemoveInstructorsFromExerciseMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.EXERCISE_ID)
     def mutate(
         cls, root, info, user_ids: List[str], exercise_id: str
     ) -> graphene.Mutation:
+        exercise_access(info.context, int(exercise_id))
         exercise = get_model(Exercise, id=exercise_id)
         users = validate_instructor_removing(user_ids, exercise)
         InstructorOfExercise.objects.filter(
@@ -170,10 +174,10 @@ class AddDefinitionAccessMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.DEFINITION_ID)
     def mutate(
         cls, root, info, user_ids: List[str], definition_id: str
     ) -> graphene.Mutation:
+        definition_access(info.context, int(definition_id))
         definition = get_model(Definition, id=int(definition_id))
         users = validate_definition_access_assinging(user_ids)
         DefinitionAccess.objects.bulk_create(
@@ -203,10 +207,10 @@ class RemoveDefinitionAccessMutation(graphene.Mutation):
 
     @classmethod
     @protected(User.AuthGroup.INSTRUCTOR)
-    @extra_protected(Check.DEFINITION_ID)
     def mutate(
         cls, root, info, user_ids: List[str], definition_id: str
     ) -> graphene.Mutation:
+        definition_access(info.context, int(definition_id))
         definition = get_model(Definition, id=int(definition_id))
         users = validate_definition_access_removing(user_ids, definition)
         DefinitionAccess.objects.filter(
diff --git a/user/schema/query.py b/user/schema/query.py
index 4cdffcc62e83bbb6676b94a75ed6621f6251c651..71fcf1e718a4f3094730da20e979283b754795de 100644
--- a/user/schema/query.py
+++ b/user/schema/query.py
@@ -2,7 +2,7 @@ from typing import Optional, List
 
 import graphene
 
-from aai.utils import protected, logged_in
+from aai.decorators 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
diff --git a/user/views.py b/user/views.py
index 4af5ca88b045e7920c7b9622edb9fa9ce3d41369..7dab809c6e3d8608457f79c7b8d765c868b9dcd4 100644
--- a/user/views.py
+++ b/user/views.py
@@ -3,11 +3,11 @@ from rest_framework import parsers
 from rest_framework.request import Request
 from rest_framework.views import APIView
 
-from user.models import User
-from aai.utils import protected
+from aai.decorators import protected
 from common_lib.exceptions import ApiException
 from common_lib.logger import logger, log_user_msg
 from user.lib import UserUploader
+from user.models import User
 
 
 class UploadUserFile(APIView):