diff --git a/aleksis/apps/maka/frontend/components/effort_types/EffortTypes.vue b/aleksis/apps/maka/frontend/components/effort_types/EffortTypes.vue
index b1845ebcff6649e525017bf3c78f5718d8f3fabd..215f54d8444a5a1a1ea6be8c38c1d0ceecb107f3 100644
--- a/aleksis/apps/maka/frontend/components/effort_types/EffortTypes.vue
+++ b/aleksis/apps/maka/frontend/components/effort_types/EffortTypes.vue
@@ -1,67 +1,70 @@
 <script setup>
 import ColorField from "aleksis.core/components/generic/forms/ColorField.vue";
 import InlineCRUDList from "aleksis.core/components/generic/InlineCRUDList.vue";
+import SharedSecretWrapper from "../shared_secret/SharedSecretWrapper.vue";
 </script>
 
 <template>
-  <v-container>
-    <inline-c-r-u-d-list
-      :headers="headers"
-      :i18n-key="i18nKey"
-      create-item-i18n-key="maka.effort_types.create"
-      :gql-query="gqlQuery"
-      :gql-create-mutation="gqlCreateMutation"
-      :gql-patch-mutation="gqlPatchMutation"
-      :gql-delete-mutation="gqlDeleteMutation"
-      :default-item="defaultItem"
-      :get-create-data="transformItem"
-      :get-patch-data="transformItem"
-    >
-      <!-- eslint-disable-next-line vue/valid-v-slot -->
-      <template #name.field="{ attrs, on }">
-        <div aria-required="true">
-          <v-text-field
+  <shared-secret-wrapper>
+    <v-container>
+      <inline-c-r-u-d-list
+        :headers="headers"
+        :i18n-key="i18nKey"
+        create-item-i18n-key="maka.effort_types.create"
+        :gql-query="gqlQuery"
+        :gql-create-mutation="gqlCreateMutation"
+        :gql-patch-mutation="gqlPatchMutation"
+        :gql-delete-mutation="gqlDeleteMutation"
+        :default-item="defaultItem"
+        :get-create-data="transformItem"
+        :get-patch-data="transformItem"
+      >
+        <!-- eslint-disable-next-line vue/valid-v-slot -->
+        <template #name.field="{ attrs, on }">
+          <div aria-required="true">
+            <v-text-field
+              v-bind="attrs"
+              v-on="on"
+              :rules="$rules().required.build()"
+            />
+          </div>
+        </template>
+
+        <template #color="{ item }">
+          <v-chip :color="item.color" outlined v-if="item.color">
+            {{ item.color }}
+          </v-chip>
+          <span v-else>–</span>
+        </template>
+        <!-- eslint-disable-next-line vue/valid-v-slot -->
+        <template #color.field="{ attrs, on }">
+          <color-field v-bind="attrs" v-on="on" />
+        </template>
+
+        <template #default="{ item }">
+          <v-switch
+            :input-value="item.default"
+            disabled
+            inset
+            :false-value="false"
+            :true-value="true"
+          />
+        </template>
+        <!-- eslint-disable-next-line vue/valid-v-slot -->
+        <template #default.field="{ attrs, on }">
+          <v-switch
             v-bind="attrs"
             v-on="on"
-            :rules="$rules().required.build()"
+            inset
+            :false-value="false"
+            :true-value="true"
+            :hint="$t('maka.effort_types.default_helptext')"
+            persistent-hint
           />
-        </div>
-      </template>
-
-      <template #color="{ item }">
-        <v-chip :color="item.color" outlined v-if="item.color">
-          {{ item.color }}
-        </v-chip>
-        <span v-else>–</span>
-      </template>
-      <!-- eslint-disable-next-line vue/valid-v-slot -->
-      <template #color.field="{ attrs, on }">
-        <color-field v-bind="attrs" v-on="on" />
-      </template>
-
-      <template #default="{ item }">
-        <v-switch
-          :input-value="item.default"
-          disabled
-          inset
-          :false-value="false"
-          :true-value="true"
-        />
-      </template>
-      <!-- eslint-disable-next-line vue/valid-v-slot -->
-      <template #default.field="{ attrs, on }">
-        <v-switch
-          v-bind="attrs"
-          v-on="on"
-          inset
-          :false-value="false"
-          :true-value="true"
-          :hint="$t('maka.effort_types.default_helptext')"
-          persistent-hint
-        />
-      </template>
-    </inline-c-r-u-d-list>
-  </v-container>
+        </template>
+      </inline-c-r-u-d-list>
+    </v-container>
+  </shared-secret-wrapper>
 </template>
 
 <script>
diff --git a/aleksis/apps/maka/frontend/components/efforts/Efforts.vue b/aleksis/apps/maka/frontend/components/efforts/Efforts.vue
index 7decf67ab79838bccfa05baea3343594b5bc4a03..8445d2150e3fafbade413b80b0a42dc70c8b465d 100644
--- a/aleksis/apps/maka/frontend/components/efforts/Efforts.vue
+++ b/aleksis/apps/maka/frontend/components/efforts/Efforts.vue
@@ -2,63 +2,66 @@
 import GroupChip from "aleksis.core/components/group/GroupChip.vue";
 import InlineCRUDList from "aleksis.core/components/generic/InlineCRUDList.vue";
 import EffortTypeChip from "../effort_types/EffortTypeChip.vue";
+import SharedSecretWrapper from "../shared_secret/SharedSecretWrapper.vue";
 </script>
 
 <template>
-  <v-container>
-    <inline-c-r-u-d-list
-      :headers="headers"
-      :i18n-key="i18nKey"
-      create-item-i18n-key="maka.efforts.create"
-      :gql-query="gqlQuery"
-      :gql-create-mutation="gqlCreateMutation"
-      :gql-patch-mutation="gqlPatchMutation"
-      :gql-delete-mutation="gqlDeleteMutation"
-      :default-item="defaultItem"
-    >
-      <!-- eslint-disable-next-line vue/valid-v-slot -->
-      <template #name.field="{ attrs, on }">
-        <div aria-required="true">
-          <v-text-field
-            v-bind="attrs"
-            v-on="on"
-            :rules="$rules().required.build()"
-          />
-        </div>
-      </template>
+  <shared-secret-wrapper>
+    <v-container>
+      <inline-c-r-u-d-list
+        :headers="headers"
+        :i18n-key="i18nKey"
+        create-item-i18n-key="maka.efforts.create"
+        :gql-query="gqlQuery"
+        :gql-create-mutation="gqlCreateMutation"
+        :gql-patch-mutation="gqlPatchMutation"
+        :gql-delete-mutation="gqlDeleteMutation"
+        :default-item="defaultItem"
+      >
+        <!-- eslint-disable-next-line vue/valid-v-slot -->
+        <template #name.field="{ attrs, on }">
+          <div aria-required="true">
+            <v-text-field
+              v-bind="attrs"
+              v-on="on"
+              :rules="$rules().required.build()"
+            />
+          </div>
+        </template>
 
-      <template #effortType="{ item }">
-        <effort-type-chip :effort-type="item.effortType" v-if="item.effortType" />
-        <span v-else>–</span>
-      </template>
-      <!-- eslint-disable-next-line vue/valid-v-slot -->
-      <template #effortType.field="{ attrs, on }">
-        <div aria-required="true">
-          <v-autocomplete v-bind="attrs" v-on="on" :items="effortTypes" item-text="name" item-value="id" :rules="$rules().required.build()" />
-        </div>
-      </template>
+        <template #effortType="{ item }">
+          <effort-type-chip :effort-type="item.effortType" v-if="item.effortType" />
+          <span v-else>–</span>
+        </template>
+        <!-- eslint-disable-next-line vue/valid-v-slot -->
+        <template #effortType.field="{ attrs, on }">
+          <div aria-required="true">
+            <v-autocomplete v-bind="attrs" v-on="on" :items="effortTypes" item-text="name" item-value="id" :rules="$rules().required.build()" />
+          </div>
+        </template>
 
-      <template #group="{ item }">
-        <group-chip :group="item.group"/>
-      </template>
-      <!-- eslint-disable-next-line vue/valid-v-slot -->
-      <template #group.field="{ attrs, on }">
-        <div aria-required="true">
-          <v-autocomplete v-bind="attrs" v-on="on" :items="groups" item-text="name" item-value="id" :rules="$rules().required.build()" />
-        </div>
-      </template>
+        <template #group="{ item }">
+          <group-chip :group="item.group"/>
+        </template>
+        <!-- eslint-disable-next-line vue/valid-v-slot -->
+        <template #group.field="{ attrs, on }">
+          <div aria-required="true">
+            <v-autocomplete v-bind="attrs" v-on="on" :items="groups" item-text="name" item-value="id" :rules="$rules().required.build()" />
+          </div>
+        </template>
 
-      <template #gradeSet="{ item }">
-        {{ item.gradeSet.name }}
-      </template>
-      <!-- eslint-disable-next-line vue/valid-v-slot -->
-      <template #gradeSet.field="{ attrs, on }">
-        <div aria-required="true">
-          <v-autocomplete v-bind="attrs" v-on="on" :items="gradeSets" item-text="name" item-value="id" :rules="$rules().required.build()" />
-        </div>
-      </template>
-    </inline-c-r-u-d-list>
-  </v-container>
+        <template #gradeSet="{ item }">
+          {{ item.gradeSet.name }}
+        </template>
+        <!-- eslint-disable-next-line vue/valid-v-slot -->
+        <template #gradeSet.field="{ attrs, on }">
+          <div aria-required="true">
+            <v-autocomplete v-bind="attrs" v-on="on" :items="gradeSets" item-text="name" item-value="id" :rules="$rules().required.build()" />
+          </div>
+        </template>
+      </inline-c-r-u-d-list>
+    </v-container>
+  </shared-secret-wrapper>
 </template>
 
 <script>
diff --git a/aleksis/apps/maka/frontend/components/grade_management/GradeManagement.vue b/aleksis/apps/maka/frontend/components/grade_management/GradeManagement.vue
index d9bb3cc62b4eb2fe56f6d745cd5df341a3a688ac..69fd7dfc64936e83f239456350d5d67d4ba0debe 100644
--- a/aleksis/apps/maka/frontend/components/grade_management/GradeManagement.vue
+++ b/aleksis/apps/maka/frontend/components/grade_management/GradeManagement.vue
@@ -1,52 +1,55 @@
 <script setup>
 import CRUDList from "aleksis.core/components/generic/CRUDList.vue";
+import SharedSecretWrapper from "../shared_secret/SharedSecretWrapper.vue";
 </script>
 
 <template>
-  <v-container>
-    <c-r-u-d-list
-      :headers="headers"
-      :i18n-key="i18nKey"
-      create-item-i18n-key="maka.grade_set.create"
-      :gql-query="gqlQuery"
-      :gql-create-mutation="gqlCreateMutation"
-      :gql-patch-mutation="gqlPatchMutation"
-      :gql-delete-mutation="gqlDeleteMutation"
-      :default-item="defaultItem"
-      :get-create-data="transformItem(false)"
-      :get-patch-data="transformItem(true)"
-      show-expand
-      :enable-edit="true"
-    >
-      <!-- eslint-disable-next-line vue/valid-v-slot -->
-      <template #name.field="{ attrs, on }">
-        <div aria-required="true">
-          <v-text-field
-            v-bind="attrs"
-            v-on="on"
-            :rules="$rules().required.build()"
-          />
-        </div>
-      </template>
-
-      <template #expanded-item="{ item }">
-        <v-sheet class="my-2">
-          <div
-            v-if="item.gradeChoices && item.gradeChoices.length"
-            class="d-flex flex-wrap"
-            style="gap: 0.5em"
-          >
-            <v-chip v-for="gradeChoice in item.gradeChoices">
-              {{ gradeChoice.name }}
-            </v-chip>
+  <shared-secret-wrapper>
+    <v-container>
+      <c-r-u-d-list
+        :headers="headers"
+        :i18n-key="i18nKey"
+        create-item-i18n-key="maka.grade_set.create"
+        :gql-query="gqlQuery"
+        :gql-create-mutation="gqlCreateMutation"
+        :gql-patch-mutation="gqlPatchMutation"
+        :gql-delete-mutation="gqlDeleteMutation"
+        :default-item="defaultItem"
+        :get-create-data="transformItem(false)"
+        :get-patch-data="transformItem(true)"
+        show-expand
+        :enable-edit="true"
+      >
+        <!-- eslint-disable-next-line vue/valid-v-slot -->
+        <template #name.field="{ attrs, on }">
+          <div aria-required="true">
+            <v-text-field
+              v-bind="attrs"
+              v-on="on"
+              :rules="$rules().required.build()"
+            />
           </div>
-          <template v-else>
-            TODO: you need to create choices!
-          </template>
-        </v-sheet>
-      </template>
-    </c-r-u-d-list>
-  </v-container>
+        </template>
+
+        <template #expanded-item="{ item }">
+          <v-sheet class="my-2">
+            <div
+              v-if="item.gradeChoices && item.gradeChoices.length"
+              class="d-flex flex-wrap"
+              style="gap: 0.5em"
+            >
+              <v-chip v-for="gradeChoice in item.gradeChoices">
+                {{ gradeChoice.name }}
+              </v-chip>
+            </div>
+            <template v-else>
+              TODO: you need to create choices!
+            </template>
+          </v-sheet>
+        </template>
+      </c-r-u-d-list>
+    </v-container>
+  </shared-secret-wrapper>
 </template>
 
 <script>
diff --git a/aleksis/apps/maka/frontend/components/shared_secret/SharedSecretWrapper.vue b/aleksis/apps/maka/frontend/components/shared_secret/SharedSecretWrapper.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ddf72ddceebb7a6dcde09e7ef5b541e26a452d61
--- /dev/null
+++ b/aleksis/apps/maka/frontend/components/shared_secret/SharedSecretWrapper.vue
@@ -0,0 +1,85 @@
+<template>
+  <div>
+    <slot v-if="sharedSecretStatus && !initial" />
+    <div class="d-flex justify-center align-center flex-column text-center" v-else-if="$apollo.queries.sharedSecretStatus.loading">
+      <h1 class="text-h5">{{ $t("maka.shared_secret.checking") }}</h1>
+      <v-progress-circular
+        indeterminate
+        color="primary"
+      ></v-progress-circular>
+    </div>
+    <div class="d-flex justify-center align-center flex-column text-center" v-else>
+      <h1 class="text-h5">{{ $t("maka.shared_secret.enter") }}</h1>
+      <v-text-field v-model="sharedSecret" :loading="loading" type="password" :error-messages="errorMessages">
+        <template #append>
+          <v-btn
+            :disabled="loading"
+            icon
+            @click="submitSharedSecret"
+          >
+            <v-icon color="primary">
+              mdi-send-outline
+            </v-icon>
+          </v-btn>
+        </template>
+      </v-text-field>
+    </div>
+  </div>
+</template>
+  
+  <script>
+  import { gqlSubmitSharedSecret, gqlSharedSecretStatus } from "./sharedSecret.graphql";
+  
+  export default {
+    name: "SharedSecretWrapper",
+    data() {
+      return {
+        sharedSecret: "",
+        sharedSecretStatus: false,
+        loading: false,
+        showError: false,
+        initial: true,
+      };
+    },
+    methods: {
+      submitSharedSecret() {
+        this.showError = false;
+        this.loading = true;
+        this.$apollo.mutate({
+          mutation: gqlSubmitSharedSecret,
+          variables: {
+            sharedSecret: this.sharedSecret,
+          },
+        }).then((data) => {
+          this.loading = false;
+          this.$apollo.queries.sharedSecretStatus.refetch();
+        });
+      },
+    },
+    computed: {
+      errorMessages() {
+        if (!this.loading && !this.sharedSecretStatus && this.showError) {
+          return [this.$t('maka.shared_secret.error')];
+        } else {
+          return [];
+        }
+      },
+    },
+    apollo: {
+      sharedSecretStatus: {
+        query: gqlSharedSecretStatus,
+        result ({ data, loading, networkStatus }) {
+          if (!loading && !data?.sharedSecretStatus && !this.initial) {
+            this.showError = true;
+          } else if (!loading) {
+            this.initial = false;
+          }
+        },
+      },
+    },
+  };
+  </script>
+  
+  <style scoped>
+  </style>
+  
\ No newline at end of file
diff --git a/aleksis/apps/maka/frontend/components/shared_secret/sharedSecret.graphql b/aleksis/apps/maka/frontend/components/shared_secret/sharedSecret.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..a4b39d102071ea20e5d1a741802cae53568cca54
--- /dev/null
+++ b/aleksis/apps/maka/frontend/components/shared_secret/sharedSecret.graphql
@@ -0,0 +1,9 @@
+mutation gqlSubmitSharedSecret($sharedSecret: String!) {
+  submitSharedSecret(sharedSecret: $sharedSecret) {
+    ok
+  }
+}
+
+query gqlSharedSecretStatus {
+  sharedSecretStatus
+}
diff --git a/aleksis/apps/maka/frontend/index.js b/aleksis/apps/maka/frontend/index.js
index ed5f30cb0d232d0a6a0c7ac5da71dcffa7a1ddd2..f72d3a5845875560f1e7599defb725c29544aca7 100644
--- a/aleksis/apps/maka/frontend/index.js
+++ b/aleksis/apps/maka/frontend/index.js
@@ -32,7 +32,8 @@ export default {
       name: "maka.gradeManagement",
       meta: {
         inMenu: true,
-        titleKey: "maka.grades.menu_title",
+        titleKey: "maka.grade_set.menu_title",
+        toolbarTitle: "maka.grade_set.title_plural",
         icon: "mdi-star-settings-outline",
         iconActive: "mdi-star-settings",
         permission: "",
@@ -45,6 +46,7 @@ export default {
       meta: {
         inMenu: true,
         titleKey: "maka.effort_types.menu_title",
+        toolbarTitle: "maka.effort_types.title_plural",
         icon: "mdi-folder-star-multiple-outline",
         iconActive: "mdi-folder-star-multiple",
         permission: "",
@@ -57,6 +59,7 @@ export default {
       meta: {
         inMenu: true,
         titleKey: "maka.efforts.menu_title",
+        toolbarTitle: "maka.efforts.title_plural",
         icon: "mdi-playlist-star",
         iconActive: "mdi-playlist-remove",
         permission: "",
diff --git a/aleksis/apps/maka/frontend/messages/en.json b/aleksis/apps/maka/frontend/messages/en.json
index 46361285449e68325883be0a0de9c67bb89d5562..71a955dc1e63df5b15df5ba92ed6bf894982fc8a 100644
--- a/aleksis/apps/maka/frontend/messages/en.json
+++ b/aleksis/apps/maka/frontend/messages/en.json
@@ -8,6 +8,7 @@
     },
     "grade_set": {
       "name": "Name",
+      "menu_title": "Grade Sets",
       "title_plural": "Grade Sets",
       "create": "Create Grade Set"
     },
@@ -29,6 +30,12 @@
       "icon": "Icon",
       "default": "Default Effort Type",
       "default_helptext": "Will disable previous default when enabled"
+    },
+    "shared_secret": {
+      "title": "Shared secret",
+      "enter": "Shared secret needed for accessing page",
+      "error": "Wrong shared secret entered. Please try again.",
+      "checking": "Checking for correctly entered shared secret…"
     }
   },
   "group": {
diff --git a/aleksis/apps/maka/models/__init__.py b/aleksis/apps/maka/models/__init__.py
index 9fbbe3f14616cd8901de1e9b511911f49f9d415f..27c4be4731e4a7a5eda2fc052cc986e0d874b0d4 100644
--- a/aleksis/apps/maka/models/__init__.py
+++ b/aleksis/apps/maka/models/__init__.py
@@ -68,6 +68,17 @@ class EffortType(ExtensibleModel):
 
         super().save(*args, **kwargs)
 
+    class Meta:
+        verbose_name = _("Effort type")
+        verbose_name_plural = _("Effort types")
+        constraints = [
+            models.UniqueConstraint(
+                fields=["default"],
+                condition=models.Q(default=True),
+                name="only_one_default_effort_type",
+            )
+        ]
+
 
 class Effort(ExtensibleModel):
     """Concrete occurrence of an effort type."""
diff --git a/aleksis/apps/maka/preferences.py b/aleksis/apps/maka/preferences.py
new file mode 100644
index 0000000000000000000000000000000000000000..895defd12161fa2fde9e21beb4e602a21255fac4
--- /dev/null
+++ b/aleksis/apps/maka/preferences.py
@@ -0,0 +1,47 @@
+from django.conf import settings
+from django.contrib.auth.hashers import make_password
+from django.forms import PasswordInput
+from django.forms.widgets import SelectMultiple
+from django.utils.translation import gettext_lazy as _
+
+from colorfield.widgets import ColorWidget
+from dynamic_preferences.preferences import Section
+from dynamic_preferences.serializers import StringSerializer
+from dynamic_preferences.types import (
+    BooleanPreference,
+    ChoicePreference,
+    FilePreference,
+    IntegerPreference,
+    LongStringPreference,
+    ModelMultipleChoicePreference,
+    MultipleChoicePreference,
+    StringPreference,
+)
+from oauth2_provider.models import AbstractApplication
+
+from aleksis.core.registries import person_preferences_registry, site_preferences_registry
+
+maka = Section("maka", verbose_name=_("Grade management"))
+
+
+class PasswordSerializer(StringSerializer):
+    """Serializer hashing the given string before saving it."""
+
+    @classmethod
+    def to_db(cls, value, **kwargs):
+        return str(make_password(value))
+
+
+
+@site_preferences_registry.register
+class SharedSecret(StringPreference):
+    """Shared secret for accessing grade management pages."""
+
+    section = maka
+    name = "shared_secret"
+    default = make_password("aleksis")
+    required = True
+    widget = PasswordInput
+    serializer = PasswordSerializer
+    verbose_name = _("Shared secret")
+    help_text = _("This is the global shared secret key used to access all grade management pages requiring two factor authentification. It is not possible to see the secret again after setting it; please write it down safely.")
diff --git a/aleksis/apps/maka/rules.py b/aleksis/apps/maka/rules.py
new file mode 100644
index 0000000000000000000000000000000000000000..0b167c9228112764ca7aae3ebb98de04a26ed697
--- /dev/null
+++ b/aleksis/apps/maka/rules.py
@@ -0,0 +1,10 @@
+import rules
+from rules import is_superuser
+
+from aleksis.core.util.predicates import has_person
+from .util.predicates import has_shared_secret
+
+
+# View 2FA (shared secret) protected pages
+protected_page_predicate = has_person & has_shared_secret
+rules.add_perm("maka.protected_page_rule", protected_page_predicate)
diff --git a/aleksis/apps/maka/schema/__init__.py b/aleksis/apps/maka/schema/__init__.py
index 266e40a3cb7306844161b44eaac461c6cd9da9c9..df8cea228bca6392b1a923f41e9992f8d3505154 100644
--- a/aleksis/apps/maka/schema/__init__.py
+++ b/aleksis/apps/maka/schema/__init__.py
@@ -28,6 +28,7 @@ from .grade import (
     GradeBatchPatchMutation,
     GradeType,
 )
+from .shared_secret import SubmitSharedSecretMutation
 
 
 class Query(graphene.ObjectType):
@@ -37,6 +38,10 @@ class Query(graphene.ObjectType):
     effort_types = FilterOrderList(EffortTypeType)
     efforts = FilterOrderList(EffortType)
 
+    shared_secret_status = graphene.Boolean()
+
+    def resolve_shared_secret_status(root, info) -> bool:
+        return info.context.session.get("maka_shared_secret_correct")
 
 class Mutation(graphene.ObjectType):
     create_grade_sets = GradeSetBatchCreateMutation.Field()
@@ -55,6 +60,8 @@ class Mutation(graphene.ObjectType):
     delete_effort_types = EffortTypeBatchDeleteMutation.Field()
     update_effort_types = EffortTypeBatchPatchMutation.Field()
 
-    create_efforts = EffortBatchCreateMutation.Field()
-    delete_efforts = EffortBatchDeleteMutation.Field()
-    update_efforts = EffortBatchPatchMutation.Field()
+    create_rankings = EffortBatchCreateMutation.Field()
+    delete_rankings = EffortBatchDeleteMutation.Field()
+    update_rankings = EffortBatchPatchMutation.Field()
+
+    submit_shared_secret = SubmitSharedSecretMutation.Field()
diff --git a/aleksis/apps/maka/schema/effort.py b/aleksis/apps/maka/schema/effort.py
index 42b23b560e13aebdac9696ea4383001e4381fb93..36cbdd06478279f09122122f6aceea4e85950f95 100644
--- a/aleksis/apps/maka/schema/effort.py
+++ b/aleksis/apps/maka/schema/effort.py
@@ -9,10 +9,11 @@ from aleksis.core.schema.base import (
     RulesObjectType,
 )
 
+from .shared_secret import SharedSecretObjectType, SharedSecretBatchCreateMixin, SharedSecretBatchPatchMixin, SharedSecretBatchDeleteMixin
 from ..models import Effort as EffortModel, EffortType as EffortTypeModel
 
 
-class EffortTypeType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
+class EffortTypeType(SharedSecretObjectType, PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
     class Meta:
         model = EffortTypeModel
         fields = ("id", "name", "color", "icon", "default")
@@ -22,7 +23,7 @@ class EffortTypeType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
         }
 
 
-class EffortTypeBatchCreateMutation(BaseBatchCreateMutation):
+class EffortTypeBatchCreateMutation(SharedSecretBatchCreateMixin, BaseBatchCreateMutation):
     class Meta:
         model = EffortTypeModel
         permissions = ("maka.create_efforttype_rule",)
@@ -34,13 +35,13 @@ class EffortTypeBatchCreateMutation(BaseBatchCreateMutation):
         )
 
 
-class EffortTypeBatchDeleteMutation(BaseBatchDeleteMutation):
+class EffortTypeBatchDeleteMutation(SharedSecretBatchDeleteMixin, BaseBatchDeleteMutation):
     class Meta:
         model = EffortTypeModel
         permissions = ("maka.delete_efforttype_rule",)
 
 
-class EffortTypeBatchPatchMutation(BaseBatchPatchMutation):
+class EffortTypeBatchPatchMutation(SharedSecretBatchPatchMixin, BaseBatchPatchMutation):
     class Meta:
         model = EffortTypeModel
         permissions = ("maka.edit_efforttype_rule",)
@@ -53,7 +54,7 @@ class EffortTypeBatchPatchMutation(BaseBatchPatchMutation):
         )
 
 
-class EffortType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
+class EffortType(SharedSecretObjectType, PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
     class Meta:
         model = EffortModel
         fields = ("id", "name", "weight", "effort_type", "group", "grade_set", "grades")
@@ -63,7 +64,7 @@ class EffortType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
         }
 
 
-class EffortBatchCreateMutation(BaseBatchCreateMutation):
+class EffortBatchCreateMutation(SharedSecretBatchCreateMixin, BaseBatchCreateMutation):
     class Meta:
         model = EffortModel
         permissions = ("maka.create_effort_rule",)
@@ -75,13 +76,13 @@ class EffortBatchCreateMutation(BaseBatchCreateMutation):
         )
 
 
-class EffortBatchDeleteMutation(BaseBatchDeleteMutation):
+class EffortBatchDeleteMutation(SharedSecretBatchDeleteMixin, BaseBatchDeleteMutation):
     class Meta:
         model = EffortModel
         permissions = ("maka.delete_effort_rule",)
 
 
-class EffortBatchPatchMutation(BaseBatchPatchMutation):
+class EffortBatchPatchMutation(SharedSecretBatchPatchMixin, BaseBatchPatchMutation):
     class Meta:
         model = EffortModel
         permissions = ("maka.edit_effort_rule",)
diff --git a/aleksis/apps/maka/schema/grade.py b/aleksis/apps/maka/schema/grade.py
index 56fa66785fb70eaf453414f3cbb18e1c38081d33..f12f24641645fbe7a61f6a7755f0c266a7396605 100644
--- a/aleksis/apps/maka/schema/grade.py
+++ b/aleksis/apps/maka/schema/grade.py
@@ -9,10 +9,11 @@ from aleksis.core.schema.base import (
     RulesObjectType,
 )
 
+from .shared_secret import SharedSecretObjectType, SharedSecretBatchCreateMixin, SharedSecretBatchPatchMixin, SharedSecretBatchDeleteMixin
 from ..models import Grade
 
 
-class GradeType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
+class GradeType(SharedSecretObjectType, PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
     class Meta:
         model = Grade
         fields = ("id", "person", "grade", "effort")
@@ -22,7 +23,7 @@ class GradeType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
         }
 
 
-class GradeBatchCreateMutation(BaseBatchCreateMutation):
+class GradeBatchCreateMutation(SharedSecretBatchCreateMixin, BaseBatchCreateMutation):
     class Meta:
         model = Grade
         permissions = ("maka.create_grade_rule",)
@@ -33,13 +34,13 @@ class GradeBatchCreateMutation(BaseBatchCreateMutation):
         )
 
 
-class GradeBatchDeleteMutation(BaseBatchDeleteMutation):
+class GradeBatchDeleteMutation(SharedSecretBatchDeleteMixin, BaseBatchDeleteMutation):
     class Meta:
         model = Grade
         permissions = ("maka.delete_grade_rule",)
 
 
-class GradeBatchPatchMutation(BaseBatchPatchMutation):
+class GradeBatchPatchMutation(SharedSecretBatchPatchMixin, BaseBatchPatchMutation):
     class Meta:
         model = Grade
         permissions = ("maka.edit_grade_rule",)
diff --git a/aleksis/apps/maka/schema/grade_set.py b/aleksis/apps/maka/schema/grade_set.py
index d140da402cb06f79ee5a940f022e0b987bd0ca72..391ef9677a2e3c78c6de1b82ef369e1bade6e166 100644
--- a/aleksis/apps/maka/schema/grade_set.py
+++ b/aleksis/apps/maka/schema/grade_set.py
@@ -9,10 +9,11 @@ from aleksis.core.schema.base import (
     RulesObjectType,
 )
 
+from .shared_secret import SharedSecretObjectType, SharedSecretBatchCreateMixin, SharedSecretBatchPatchMixin, SharedSecretBatchDeleteMixin
 from ..models import GradeChoice, GradeSet
 
 
-class GradeSetType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
+class GradeSetType(SharedSecretObjectType, PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
     class Meta:
         model = GradeSet
         fields = ("id", "name", "grade_choices")
@@ -22,7 +23,7 @@ class GradeSetType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
         }
 
 
-class GradeSetBatchCreateMutation(BaseBatchCreateMutation):
+class GradeSetBatchCreateMutation(SharedSecretBatchCreateMixin, BaseBatchCreateMutation):
     class Meta:
         model = GradeSet
         permissions = ("maka.create_gradeset_rule",)
@@ -31,13 +32,13 @@ class GradeSetBatchCreateMutation(BaseBatchCreateMutation):
         )
 
 
-class GradeSetBatchDeleteMutation(BaseBatchDeleteMutation):
+class GradeSetBatchDeleteMutation(SharedSecretBatchDeleteMixin, BaseBatchDeleteMutation):
     class Meta:
         model = GradeSet
         permissions = ("maka.delete_gradeset_rule",)
 
 
-class GradeSetBatchPatchMutation(BaseBatchPatchMutation):
+class GradeSetBatchPatchMutation(SharedSecretBatchPatchMixin, BaseBatchPatchMutation):
     class Meta:
         model = GradeSet
         permissions = ("maka.edit_gradeset_rule",)
diff --git a/aleksis/apps/maka/schema/shared_secret.py b/aleksis/apps/maka/schema/shared_secret.py
new file mode 100644
index 0000000000000000000000000000000000000000..50ea68316f9c03467d03afc6e808e50056fb00a5
--- /dev/null
+++ b/aleksis/apps/maka/schema/shared_secret.py
@@ -0,0 +1,75 @@
+from django.contrib.auth.hashers import check_password
+from django.core.exceptions import PermissionDenied
+
+import graphene
+from graphene_django import DjangoObjectType
+
+from aleksis.core.util.core_helpers import get_site_preferences
+
+
+class SubmitSharedSecretMutation(graphene.Mutation):
+    class Arguments:
+        shared_secret = graphene.String(required=True)  # noqa
+
+    ok = graphene.Boolean()
+
+    @classmethod
+    def mutate(cls, root, info, shared_secret):  # noqa
+        info.context.session["maka_shared_secret_correct"] = check_password(shared_secret, get_site_preferences()["maka__shared_secret"])
+        return cls(ok=True)
+
+
+class SharedSecretObjectType(DjangoObjectType):
+    class Meta:
+        abstract = True
+
+    @classmethod
+    def get_queryset(cls, queryset, info):
+        if not info.context.session.get("maka_shared_secret_correct"):
+            raise PermissionDenied()
+
+        return super().get_queryset(queryset, info)
+
+
+
+class SharedSecretBatchCreateMixin:
+    """Mixin for maka shared secret checking during batch create mutations."""
+
+    class Meta:
+        login_required = True
+
+    @classmethod
+    def check_permissions(cls, root, info, input, *args, **kwargs):  # noqa: A002
+        if info.context.session.get("maka_shared_secret_correct"):
+            return
+
+        raise PermissionDenied()
+
+
+class SharedSecretBatchPatchMixin:
+    """Mixin for maka shared secret checking during batch patch mutations."""
+
+    class Meta:
+        login_required = True
+
+    @classmethod
+    def check_permissions(cls, root, info, input, *args, **kwargs):  # noqa: A002
+        if info.context.session.get("maka_shared_secret_correct"):
+            return
+
+        raise PermissionDenied()
+
+
+class SharedSecretBatchDeleteMixin:
+    """Mixin for maka shared secret checking during batch delete mutations."""
+
+    class Meta:
+        login_required = True
+
+    @classmethod
+    def check_permissions(cls, root, info, input, *args, **kwargs):  # noqa: A002
+        if info.context.session.get("maka_shared_secret_correct"):
+            return
+
+        raise PermissionDenied()
+
diff --git a/aleksis/apps/maka/util/predicates.py b/aleksis/apps/maka/util/predicates.py
new file mode 100644
index 0000000000000000000000000000000000000000..2305daf2ed290e65a992bf8fc92d7aef8e246421
--- /dev/null
+++ b/aleksis/apps/maka/util/predicates.py
@@ -0,0 +1,9 @@
+from django.contrib.auth.models import User
+from django.http import HttpRequest
+
+from rules import predicate
+
+
+@predicate
+def has_shared_secret(user: User, request: HttpRequest) -> bool:
+    return request.session.get("maka_shared_secret_correct")