diff --git a/aleksis/apps/lesrooster/frontend/components/helper.graphql b/aleksis/apps/lesrooster/frontend/components/helper.graphql
index 2c32fae1c109d2a8d86e3484ce9821405754a9f3..417870dfca049ce5c85c082555459e379b3ed274 100644
--- a/aleksis/apps/lesrooster/frontend/components/helper.graphql
+++ b/aleksis/apps/lesrooster/frontend/components/helper.graphql
@@ -29,14 +29,6 @@ query gqlClasses {
   }
 }
 
-query gqlClassesByGrade($grade: ID!) {
-  groups: classesByGrade(grade: $grade) {
-    id
-    name
-    shortName
-  }
-}
-
 query gqlCourses {
   courses {
     id
diff --git a/aleksis/apps/lesrooster/frontend/components/supervision/supervision.graphql b/aleksis/apps/lesrooster/frontend/components/supervision/supervision.graphql
index b79998797602067598d0666fc3c5cba938111c77..88ccf9a4ba495178ca5e91080ca746d1318246c2 100644
--- a/aleksis/apps/lesrooster/frontend/components/supervision/supervision.graphql
+++ b/aleksis/apps/lesrooster/frontend/components/supervision/supervision.graphql
@@ -70,7 +70,7 @@ mutation deleteSupervisions($ids: [ID]!) {
   }
 }
 
-mutation updateSupervisions($input: [BatchPatchBreakInput]!) {
+mutation updateSupervisions($input: [BatchPatchSupervisionInput]!) {
   batchMutation: updateSupervisions(input: $input) {
     items: supervisions {
       ...supervisionFields
diff --git a/aleksis/apps/lesrooster/frontend/index.js b/aleksis/apps/lesrooster/frontend/index.js
index 67bcfca300b3193ee118311baa4c645246775cb3..1a82aec7706253dd24cc8dd41d0f29c0995abf73 100644
--- a/aleksis/apps/lesrooster/frontend/index.js
+++ b/aleksis/apps/lesrooster/frontend/index.js
@@ -11,14 +11,14 @@ export default {
   },
   children: [
     {
-      path: "validity-ranges/",
+      path: "validity_ranges/",
       component: () => import("./components/validity_range/ValidityRange.vue"),
       name: "lesrooster.validity_ranges",
       meta: {
         inMenu: true,
         titleKey: "lesrooster.validity_range.menu_title",
         icon: "mdi-calendar-expand-horizontal-outline",
-        permission: "lesrooster.view_validity_ranges_rule",
+        permission: "lesrooster.view_validityranges_rule",
       },
     },
     {
@@ -30,7 +30,21 @@ export default {
         titleKey: "lesrooster.lesson_raster.menu_title",
         toolbarTitle: "lesrooster.lesson_raster.menu_title",
         icon: "mdi-grid-large",
-        permission: "lesrooster.view_lesson_raster_rule",
+        permission: "lesrooster.manage_lesson_raster_rule",
+      },
+    },
+    {
+      path: "timebound_course_configs/plan_courses/",
+      component: () =>
+        import(
+          "./components/timebound_course_config/TimeboundCourseConfigRaster.vue"
+        ),
+      name: "lesrooster.planCourses",
+      meta: {
+        inMenu: true,
+        titleKey: "lesrooster.timebound_course_config.raster_menu_title",
+        icon: "mdi-clock-edit-outline",
+        permission: "lesrooster.view_timeboundcourseconfigs_rule",
       },
     },
     {
@@ -43,7 +57,7 @@ export default {
         titleKey: "lesrooster.timetable_management.menu_title",
         toolbarTitle: "lesrooster.timetable_management.menu_title",
         icon: "mdi-magnet",
-        permission: "lesrooster.view_timetable_creation_rule",
+        permission: "lesrooster.plan_timetables_rule",
       },
       children: [
         {
@@ -53,11 +67,22 @@ export default {
           name: "lesrooster.timetable_management",
           props: true,
           meta: {
-            permission: "lesrooster.view_timetable_creation_rule",
+            permission: "lesrooster.plan_timetables_rule",
           },
         },
       ],
     },
+    {
+      path: "supervisions/",
+      component: () => import("./components/supervision/Supervision.vue"),
+      name: "lesrooster.supervisions",
+      meta: {
+        inMenu: true,
+        titleKey: "lesrooster.supervision.menu_title",
+        icon: "mdi-seesaw",
+        permission: "lesrooster.view_supervisions_rule",
+      },
+    },
     {
       path: "slots/",
       component: () =>
@@ -78,7 +103,7 @@ export default {
         inMenu: true,
         titleKey: "lesrooster.break.menu_title",
         icon: "mdi-timer-sand-paused",
-        permission: "lesrooster.view_break_slots_rule",
+        permission: "lesrooster.view_breakslots_rule",
       },
     },
     {
@@ -92,32 +117,7 @@ export default {
         inMenu: true,
         titleKey: "lesrooster.timebound_course_config.crud_table_menu_title",
         icon: "mdi-timetable",
-        permission: "lesrooster.view_timebound_course_configs_rule",
-      },
-    },
-    {
-      path: "timebound_course_configs/plan_courses/",
-      component: () =>
-        import(
-          "./components/timebound_course_config/TimeboundCourseConfigRaster.vue"
-        ),
-      name: "lesrooster.planCourses",
-      meta: {
-        inMenu: true,
-        titleKey: "lesrooster.timebound_course_config.raster_menu_title",
-        icon: "mdi-clock-edit-outline",
-        permission: "lesrooster.view_timebound_course_configs_rule",
-      },
-    },
-    {
-      path: "supervisions/",
-      component: () => import("./components/supervision/Supervision.vue"),
-      name: "lesrooster.supervisions",
-      meta: {
-        inMenu: true,
-        titleKey: "lesrooster.supervision.menu_title",
-        icon: "mdi-seesaw",
-        permission: "lesrooster.view_supervisions_rule",
+        permission: "lesrooster.view_timeboundcourseconfigs_rule",
       },
     },
   ],
diff --git a/aleksis/apps/lesrooster/models.py b/aleksis/apps/lesrooster/models.py
index 7d9ffafc72ef8c1930fbf2d6d3632894a8a5cc45..33b92ae379c89305567c1351bc5a549709259c70 100644
--- a/aleksis/apps/lesrooster/models.py
+++ b/aleksis/apps/lesrooster/models.py
@@ -93,7 +93,8 @@ class ValidityRange(ExtensibleModel):
             if qs.exists():
                 raise ValidationError(
                     _(
-                        "There is already a published validity range for this time or a part of this time."
+                        "There is already a published validity range "
+                        "for this time or a part of this time."
                     )
                 )
 
@@ -692,6 +693,6 @@ class LesroosterGlobalPermissions(GlobalPermissionModel):
     class Meta:
         managed = False
         permissions = (
-            ("view_lesson_raster", _("Can view lesson raster")),
-            ("view_timetable_creation", _("Can view timetable creation")),
+            ("manage_lesson_raster", _("Can manage lesson raster")),
+            ("plan_timetables", _("Can plan timetables")),
         )
diff --git a/aleksis/apps/lesrooster/rules.py b/aleksis/apps/lesrooster/rules.py
index 973eeb7ad043b3e8c2a893b092f7cf3275548b94..ca93ba3f1e935ce6aa9482dce016c81c7cad7057 100644
--- a/aleksis/apps/lesrooster/rules.py
+++ b/aleksis/apps/lesrooster/rules.py
@@ -18,76 +18,123 @@ from .models import (
     ValidityRange,
 )
 
+manage_lesson_raster_predicate = has_person | has_global_perm("lesrooster.manage_lesson_raster")
+add_perm("lesrooster.manage_lesson_raster_rule", manage_lesson_raster_predicate)
+
+plan_timetables_predicate = has_person | has_global_perm("lesrooster.plan_timetables")
+add_perm("lesrooster.plan_timetables_rule", plan_timetables_predicate)
+
+
+# Slots
+view_slots_predicate = has_person & (
+    has_global_perm("lesrooster.view_slot")
+    | has_any_object("lesrooster.view_slot", Slot)
+    | manage_lesson_raster_predicate
+    | plan_timetables_predicate
+)
+add_perm("lesrooster.view_slots_rule", view_slots_predicate)
+
+view_slot_predicate = has_person & (
+    has_global_perm("lesrooster.view_slot")
+    | has_object_perm("lesrooster.view_slot")
+    | manage_lesson_raster_predicate
+    | plan_timetables_predicate
+)
+add_perm("lesrooster.view_slot_rule", view_slot_predicate)
+
+create_slot_predicate = has_person & (
+    has_global_perm("lesrooster.add_slot") | manage_lesson_raster_predicate
+)
+add_perm("lesrooster.create_slot_rule", create_slot_predicate)
+
+edit_slot_predicate = view_slot_predicate & (
+    has_global_perm("lesrooster.change_slot")
+    | has_object_perm("lesrooster.change_slot")
+    | manage_lesson_raster_predicate
+)
+add_perm("lesrooster.edit_slot_rule", edit_slot_predicate)
+
+delete_slot_predicate = view_slot_predicate & (
+    has_global_perm("lesrooster.delete_slot")
+    | has_object_perm("lesrooster.delete_slot")
+    | manage_lesson_raster_predicate
+)
+add_perm("lesrooster.delete_slot_rule", delete_slot_predicate)
+
+# Break slots
+
 view_break_slots_predicate = has_person & (
     has_global_perm("lesrooster.view_breakslot")
     | has_any_object("lesrooster.view_breakslot", BreakSlot)
+    | manage_lesson_raster_predicate
+    | plan_timetables_predicate
 )
-add_perm("lesrooster.view_break_slots_rule", view_break_slots_predicate)
+add_perm("lesrooster.view_breakslots_rule", view_break_slots_predicate)
 
 view_break_slot_predicate = has_person & (
-    has_global_perm("lesrooster.view_breakslot") | has_object_perm("lesrooster.view_breakslot")
+    has_global_perm("lesrooster.view_breakslot")
+    | has_object_perm("lesrooster.view_breakslot")
+    | manage_lesson_raster_predicate
+    | plan_timetables_predicate
 )
-add_perm("lesrooster.view_break_slot_rule", view_break_slot_predicate)
+add_perm("lesrooster.view_breakslot_rule", view_break_slot_predicate)
 
-create_break_slot_predicate = has_person & has_global_perm("lesrooster.add_breakslot")
-add_perm("lesrooster.create_break_slot_rule", create_break_slot_predicate)
+create_break_slot_predicate = has_person & (
+    has_global_perm("lesrooster.add_breakslot") | manage_lesson_raster_predicate
+)
+add_perm("lesrooster.create_breakslot_rule", create_break_slot_predicate)
 
 edit_break_slot_predicate = view_break_slot_predicate & (
-    has_global_perm("lesrooster.change_breakslot") | has_object_perm("lesrooster.change_breakslot")
+    has_global_perm("lesrooster.change_breakslot")
+    | has_object_perm("lesrooster.change_breakslot")
+    | manage_lesson_raster_predicate
 )
-add_perm("lesrooster.edit_break_slot_rule", edit_break_slot_predicate)
+add_perm("lesrooster.edit_breakslot_rule", edit_break_slot_predicate)
 
 delete_break_slot_predicate = view_break_slot_predicate & (
-    has_global_perm("lesrooster.delete_breakslot") | has_object_perm("lesrooster.delete_breakslot")
+    has_global_perm("lesrooster.delete_breakslot")
+    | has_object_perm("lesrooster.delete_breakslot")
+    | manage_lesson_raster_predicate
 )
-add_perm("lesrooster.delete_break_slot_rule", delete_break_slot_predicate)
+add_perm("lesrooster.delete_breakslot_rule", delete_break_slot_predicate)
 
+
+# Lessons
 view_lessons_predicate = has_person & (
-    has_global_perm("lesrooster.view_lesson") | has_any_object("lesrooster.view_lesson", Lesson)
+    has_global_perm("lesrooster.view_lesson")
+    | has_any_object("lesrooster.view_lesson", Lesson)
+    | plan_timetables_predicate
 )
 add_perm("lesrooster.view_lessons_rule", view_lessons_predicate)
 
 view_lesson_predicate = has_person & (
-    has_global_perm("lesrooster.view_lesson") | has_object_perm("lesrooster.view_lesson")
+    has_global_perm("lesrooster.view_lesson")
+    | has_object_perm("lesrooster.view_lesson")
+    | plan_timetables_predicate
 )
 add_perm("lesrooster.view_lesson_rule", view_lesson_predicate)
 
-create_lesson_predicate = has_person & has_global_perm("lesrooster.add_lesson")
+create_lesson_predicate = has_person & (
+    has_global_perm("lesrooster.add_lesson") | plan_timetables_predicate
+)
 add_perm("lesrooster.create_lesson_rule", create_lesson_predicate)
 
 edit_lesson_predicate = view_lesson_predicate & (
-    has_global_perm("lesrooster.change_lesson") | has_object_perm("lesrooster.change_lesson")
+    has_global_perm("lesrooster.change_lesson")
+    | has_object_perm("lesrooster.change_lesson")
+    | plan_timetables_predicate
 )
 add_perm("lesrooster.edit_lesson_rule", edit_lesson_predicate)
 
 delete_lesson_predicate = view_lesson_predicate & (
-    has_global_perm("lesrooster.delete_lesson") | has_object_perm("lesrooster.delete_lesson")
+    has_global_perm("lesrooster.delete_lesson")
+    | has_object_perm("lesrooster.delete_lesson")
+    | plan_timetables_predicate
 )
 add_perm("lesrooster.delete_lesson_rule", delete_lesson_predicate)
 
-view_slots_predicate = has_person & (
-    has_global_perm("lesrooster.view_slot") | has_any_object("lesrooster.view_slot", Slot)
-)
-add_perm("lesrooster.view_slots_rule", view_slots_predicate)
-
-view_slot_predicate = has_person & (
-    has_global_perm("lesrooster.view_slot") | has_object_perm("lesrooster.view_slot")
-)
-add_perm("lesrooster.view_slot_rule", view_slot_predicate)
-
-create_slot_predicate = has_person & has_global_perm("lesrooster.add_slot")
-add_perm("lesrooster.create_slot_rule", create_slot_predicate)
-
-edit_slot_predicate = view_slot_predicate & (
-    has_global_perm("lesrooster.change_slot") | has_object_perm("lesrooster.change_slot")
-)
-add_perm("lesrooster.edit_slot_rule", edit_slot_predicate)
-
-delete_slot_predicate = view_slot_predicate & (
-    has_global_perm("lesrooster.delete_slot") | has_object_perm("lesrooster.delete_slot")
-)
-add_perm("lesrooster.delete_slot_rule", delete_slot_predicate)
 
+# Supervisions
 view_supervisions_predicate = has_person & (
     has_global_perm("lesrooster.view_supervision")
     | has_any_object("lesrooster.view_supervision", Supervision)
@@ -114,135 +161,138 @@ delete_supervision_predicate = view_supervision_predicate & (
 )
 add_perm("lesrooster.delete_supervision_rule", delete_supervision_predicate)
 
+
+# Supervision substitutions
 view_supervision_substitutions_predicate = has_person & (
     has_global_perm("lesrooster.view_supervisionsubstitution")
     | has_any_object("lesrooster.view_supervisionsubstitution", SupervisionSubstitution)
 )
-add_perm("lesrooster.view_supervision_substitutions_rule", view_supervision_substitutions_predicate)
+add_perm("lesrooster.view_supervisionsubstitutions_rule", view_supervision_substitutions_predicate)
 
 view_supervision_substitution_predicate = has_person & (
     has_global_perm("lesrooster.view_supervisionsubstitution")
     | has_object_perm("lesrooster.view_supervisionsubstitution")
 )
-add_perm("lesrooster.view_supervision_substitution_rule", view_supervision_substitution_predicate)
+add_perm("lesrooster.view_supervisionsubstitution_rule", view_supervision_substitution_predicate)
 
 create_supervision_substitution_predicate = has_person & has_global_perm(
     "lesrooster.add_supervisionsubstitution"
 )
 add_perm(
-    "lesrooster.create_supervision_substitution_rule", create_supervision_substitution_predicate
+    "lesrooster.create_supervisionsubstitution_rule", create_supervision_substitution_predicate
 )
 
 edit_supervision_substitution_predicate = view_supervision_substitution_predicate & (
     has_global_perm("lesrooster.change_supervisionsubstitution")
     | has_object_perm("lesrooster.change_supervisionsubstitution")
 )
-add_perm("lesrooster.edit_supervision_substitution_rule", edit_supervision_substitution_predicate)
+add_perm("lesrooster.edit_supervisionsubstitution_rule", edit_supervision_substitution_predicate)
 
 delete_supervision_substitution_predicate = view_supervision_substitution_predicate & (
     has_global_perm("lesrooster.delete_supervisionsubstitution")
     | has_object_perm("lesrooster.delete_supervisionsubstitution")
 )
 add_perm(
-    "lesrooster.delete_supervision_substitution_rule", delete_supervision_substitution_predicate
+    "lesrooster.delete_supervisionsubstitution_rule", delete_supervision_substitution_predicate
 )
 
+# Timebound course configs
+
 view_timebound_course_configs_predicate = has_person & (
     has_global_perm("lesrooster.view_timeboundcourseconfig")
     | has_any_object("lesrooster.view_timeboundcourseconfig", TimeboundCourseConfig)
 )
-add_perm("lesrooster.view_timebound_course_configs_rule", view_timebound_course_configs_predicate)
+add_perm("lesrooster.view_timeboundcourseconfigs_rule", view_timebound_course_configs_predicate)
 
 view_timebound_course_config_predicate = has_person & (
     has_global_perm("lesrooster.view_timeboundcourseconfig")
     | has_object_perm("lesrooster.view_timeboundcourseconfig")
+    | plan_timetables_predicate
 )
-add_perm("lesrooster.view_timebound_course_config_rule", view_timebound_course_config_predicate)
+add_perm("lesrooster.view_timeboundcourseconfig_rule", view_timebound_course_config_predicate)
 
 create_timebound_course_config_predicate = has_person & has_global_perm(
     "lesrooster.add_timeboundcourseconfig"
 )
-add_perm("lesrooster.create_timebound_course_config_rule", create_timebound_course_config_predicate)
+add_perm("lesrooster.create_timeboundcourseconfig_rule", create_timebound_course_config_predicate)
 
 edit_timebound_course_config_predicate = view_timebound_course_config_predicate & (
     has_global_perm("lesrooster.change_timeboundcourseconfig")
     | has_object_perm("lesrooster.change_timeboundcourseconfig")
 )
-add_perm("lesrooster.edit_timebound_course_config_rule", edit_timebound_course_config_predicate)
+add_perm("lesrooster.edit_timeboundcourseconfig_rule", edit_timebound_course_config_predicate)
 
 delete_timebound_course_config_predicate = view_timebound_course_config_predicate & (
     has_global_perm("lesrooster.delete_timeboundcourseconfig")
     | has_object_perm("lesrooster.delete_timeboundcourseconfig")
 )
-add_perm("lesrooster.delete_timebound_course_config_rule", delete_timebound_course_config_predicate)
-
-view_time_grids_predicate = has_person & (
-    has_global_perm("lesrooster.view_timegrid")
-    | has_any_object("lesrooster.view_timegrid", TimeGrid)
-)
-add_perm("lesrooster.view_time_grids_rule", view_time_grids_predicate)
-
-view_time_grid_predicate = has_person & (
-    has_global_perm("lesrooster.view_timegrid") | has_object_perm("lesrooster.view_timegrid")
-)
-add_perm("lesrooster.view_time_grid_rule", view_time_grid_predicate)
-
-create_time_grid_predicate = has_person & has_global_perm("lesrooster.add_timegrid")
-add_perm("lesrooster.create_time_grid_rule", create_time_grid_predicate)
+add_perm("lesrooster.delete_timeboundcourseconfig_rule", delete_timebound_course_config_predicate)
 
-edit_time_grid_predicate = view_time_grid_predicate & (
-    has_global_perm("lesrooster.change_timegrid") | has_object_perm("lesrooster.change_timegrid")
-)
-add_perm("lesrooster.edit_time_grid_rule", edit_time_grid_predicate)
 
-delete_time_grid_predicate = view_time_grid_predicate & (
-    has_global_perm("lesrooster.delete_timegrid") | has_object_perm("lesrooster.delete_timegrid")
-)
-add_perm("lesrooster.delete_time_grid_rule", delete_time_grid_predicate)
+# Validity ranges
 
 view_validity_ranges_predicate = has_person & (
     has_global_perm("lesrooster.view_validityrange")
     | has_any_object("lesrooster.view_validityrange", ValidityRange)
 )
-add_perm("lesrooster.view_validity_ranges_rule", view_validity_ranges_predicate)
+add_perm("lesrooster.view_validityranges_rule", view_validity_ranges_predicate)
 
 view_validity_range_predicate = has_person & (
     has_global_perm("lesrooster.view_validityrange")
     | has_object_perm("lesrooster.view_validityrange")
+    | plan_timetables_predicate
 )
-add_perm("lesrooster.view_validity_range_rule", view_validity_range_predicate)
+add_perm("lesrooster.view_validityrange_rule", view_validity_range_predicate)
 
 create_validity_range_predicate = has_person & has_global_perm("lesrooster.add_validityrange")
-add_perm("lesrooster.create_validity_range_rule", create_validity_range_predicate)
+add_perm("lesrooster.create_validityrange_rule", create_validity_range_predicate)
 
 edit_validity_range_predicate = view_validity_range_predicate & (
     has_global_perm("lesrooster.change_validityrange")
     | has_object_perm("lesrooster.change_validityrange")
 )
-add_perm("lesrooster.edit_validity_range_rule", edit_validity_range_predicate)
+add_perm("lesrooster.edit_validityrange_rule", edit_validity_range_predicate)
 
 delete_validity_range_predicate = view_validity_range_predicate & (
     has_global_perm("lesrooster.delete_validityrange")
     | has_object_perm("lesrooster.delete_validityrange")
 )
-add_perm("lesrooster.delete_validity_range_rule", delete_validity_range_predicate)
+add_perm("lesrooster.delete_validityrange_rule", delete_validity_range_predicate)
+
+# Time grids
+view_time_grids_predicate = has_person & (
+    has_global_perm("lesrooster.view_timegrid")
+    | has_any_object("lesrooster.view_timegrid", TimeGrid)
+)
+add_perm("lesrooster.view_timegrids_rule", view_time_grids_predicate)
+
+view_time_grid_predicate = has_person & (
+    has_global_perm("lesrooster.view_timegrid")
+    | has_object_perm("lesrooster.view_timegrid")
+    | plan_timetables_predicate
+)
+add_perm("lesrooster.view_timegrid_rule", view_time_grid_predicate)
+
+create_time_grid_predicate = has_person & has_global_perm("lesrooster.add_timegrid")
+add_perm("lesrooster.create_timegrid_rule", create_time_grid_predicate)
+
+edit_time_grid_predicate = view_time_grid_predicate & (
+    has_global_perm("lesrooster.change_timegrid") | has_object_perm("lesrooster.change_timegrid")
+)
+add_perm("lesrooster.edit_timegrid_rule", edit_time_grid_predicate)
 
-view_lesson_raster_predicate = has_person | has_global_perm(
-    "lesrooster.view_lesson_raster"
-)  # FIXME
-add_perm("lesrooster.view_lesson_raster_rule", view_lesson_raster_predicate)
+delete_time_grid_predicate = view_time_grid_predicate & (
+    has_global_perm("lesrooster.delete_timegrid") | has_object_perm("lesrooster.delete_timegrid")
+)
+add_perm("lesrooster.delete_timegrid_rule", delete_time_grid_predicate)
 
-view_timetable_creation_predicate = has_person | has_global_perm(
-    "lesrooster.view_timetable_creation"
-)  # FIXME
-add_perm("lesrooster.view_timetable_creation_rule", view_timetable_creation_predicate)
 
 view_lesrooster_menu_predicate = (
     view_validity_ranges_predicate
     | view_slots_predicate
     | view_break_slots_predicate
     | view_timebound_course_configs_predicate
-    | view_lesson_raster_predicate
-    | view_timetable_creation_predicate
+    | manage_lesson_raster_predicate
+    | plan_timetables_predicate
 )
 add_perm("lesrooster.view_lesrooster_menu_rule", view_lesrooster_menu_predicate)
diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py
index 4e79ee518214f65cda7e50b4579686a597861df9..a1fe2e1c16fd83fdc5621740ba73516b105df8ae 100644
--- a/aleksis/apps/lesrooster/schema/__init__.py
+++ b/aleksis/apps/lesrooster/schema/__init__.py
@@ -7,12 +7,19 @@ from guardian.shortcuts import get_objects_for_user
 
 from aleksis.apps.cursus.models import Course, Subject
 from aleksis.apps.cursus.schema import CourseInterface
-from aleksis.apps.cursus.schema import Query as CursusSchemaQuery
 from aleksis.core.models import Group
 from aleksis.core.schema.base import FilterOrderList
 from aleksis.core.schema.group import GroupType
 
-from ..models import Lesson, Slot, TimeboundCourseConfig, ValidityRange
+from ..models import (
+    BreakSlot,
+    Lesson,
+    Slot,
+    Supervision,
+    TimeboundCourseConfig,
+    TimeGrid,
+    ValidityRange,
+)
 from .break_slot import (
     BreakSlotBatchCreateMutation,
     BreakSlotBatchDeleteMutation,
@@ -47,7 +54,6 @@ from .supervision import (
 )
 from .time_grid import (
     TimeGridBatchDeleteMutation,
-    TimeGridBatchPatchMutation,
     TimeGridCreateMutation,
     TimeGridDeleteMutation,
     TimeGridType,
@@ -105,24 +111,56 @@ class Query(graphene.ObjectType):
         LesroosterExtendedSubjectType, groups=graphene.List(graphene.ID)
     )
 
-    classes_by_grade = graphene.List(GroupType, grade=graphene.ID())
-
     groups_by_time_grid = graphene.List(GroupType, time_grid=graphene.ID(required=True))
 
+    @staticmethod
+    def resolve_break_slots(root, info):
+        if not info.context.user.has_perm("lesrooster.view_breakslot_rule"):
+            return get_objects_for_user(info.context.user, "lesrooster.view_breakslot", BreakSlot)
+        return BreakSlot.objects.all()
+
     @staticmethod
     def resolve_slots(root, info):
         # Note: This does also return `Break` objects (but with type set to Slot). This is intended
-        return Slot.objects.non_polymorphic()
+        slots = Slot.objects.non_polymorphic()
+        if not info.context.user.has_perm("lesrooster.view_slot_rule"):
+            return get_objects_for_user(info.context.user, "lesrooster.view_slot", slots)
+        return slots
+
+    @staticmethod
+    def resolve_timebound_course_configs(root, info):
+        tccs = TimeboundCourseConfig.objects.all()
+        if not info.context.user.has_perm("lesrooster.view_timeboundcourseconfig_rule"):
+            return get_objects_for_user(
+                info.context.user, "lesrooster.view_timeboundcourseconfig", tccs
+            )
+        return tccs
 
     @staticmethod
-    def resolve_time_course_configs(root, info):
-        return
+    def resolve_validity_ranges(root, info):
+        if not info.context.user.has_perm("lesrooster.view_validityrange_rule"):
+            return get_objects_for_user(
+                info.context.user, "lesrooster.view_validityrange", ValidityRange
+            )
+        return ValidityRange.objects.all()
+
+    @staticmethod
+    def resolve_time_grids(root, info):
+        if not info.context.user.has_perm("lesrooster.view_timegrid_rule"):
+            return get_objects_for_user(info.context.user, "lesrooster.view_timegrid", TimeGrid)
+        return TimeGrid.objects.all()
+
+    @staticmethod
+    def resolve_supervisions(root, info):
+        if not info.context.user.has_perm("lesrooster.view_supervision_rule"):
+            return get_objects_for_user(
+                info.context.user, "lesrooster.view_supervision", Supervision
+            )
+        return Supervision.objects.all()
 
     @staticmethod
     def resolve_lesrooster_extended_subjects(root, info, groups):
-        return get_objects_for_user(
-            info.context.user, "cursus.view_subject", Subject.objects.all()
-        ).prefetch_related(
+        subjects = Subject.objects.all().prefetch_related(
             Prefetch(
                 "courses",
                 queryset=get_objects_for_user(
@@ -130,21 +168,20 @@ class Query(graphene.ObjectType):
                 ).filter(groups__in=groups),
             )
         )
+        if not info.context.user.has_perm("lesrooster.view_subject_rule"):
+            return get_objects_for_user(info.context.user, "cursus.view_subject", subjects)
+        return subjects
 
     @staticmethod
     def resolve_current_validity_range(root, info):
-        return ValidityRange.current
-
-    @staticmethod
-    def resolve_classes_by_grade(root, info, grade):
-        grade_group = Group.objects.prefetch_related("child_groups").get(id=grade)
-        return get_objects_for_user(
-            info.context.user,
-            "core.view_group",
-            grade_group.child_groups.filter(group_type__name="School class"),
-        )
+        validity_range = ValidityRange.current
+        if info.context.user.has_perm("lesrooster.view_validityrange_rule", validity_range):
+            return validity_range
 
     def resolve_course_objects_for_group(root, info, group, time_grid):
+        if not info.context.user.has_perm("lesrooster.plan_timetables_rule"):
+            return []
+
         group = Group.objects.get(pk=group)
 
         if not group:
@@ -164,6 +201,9 @@ class Query(graphene.ObjectType):
 
     @staticmethod
     def resolve_lesson_objects_for_group(root, info, group, time_grid):
+        if not info.context.user.has_perm("lesrooster.plan_timetables_rule"):
+            return []
+
         group = Group.objects.get(pk=group)
 
         if not group:
@@ -179,6 +219,9 @@ class Query(graphene.ObjectType):
 
     @staticmethod
     def resolve_lesson_objects_for_teacher(root, info, teacher, time_grid):
+        if not info.context.user.has_perm("lesrooster.plan_timetables_rule"):
+            return []
+
         return Lesson.objects.filter(
             Q(teachers=teacher) | Q(course__teachers=teacher),
             slot_start__time_grid_id=time_grid,
@@ -187,6 +230,9 @@ class Query(graphene.ObjectType):
 
     @staticmethod
     def resolve_lesson_objects_for_room(root, info, room, time_grid):
+        if not info.context.user.has_perm("lesrooster.plan_timetables_rule"):
+            return []
+
         return Lesson.objects.filter(
             rooms=room,
             slot_start__time_grid_id=time_grid,
@@ -195,7 +241,9 @@ class Query(graphene.ObjectType):
 
     @staticmethod
     def resolve_groups_by_time_grid(root, info, time_grid=None, **kwargs):
-        # FIXME Permissions
+        if not info.context.user.has_perm("lesrooster.plan_timetables_rule"):
+            return []
+
         return (
             Group.objects.filter(school_term__lr_validity_ranges__time_grids__id=time_grid)
             .annotate(has_cg=Q(child_groups__isnull=False))
diff --git a/aleksis/apps/lesrooster/schema/break_slot.py b/aleksis/apps/lesrooster/schema/break_slot.py
index 167b8ca590f4cfebdb34c746b6a6820498338ba0..c2d39f182db373ad620810c47a0f7cd2c712f5be 100644
--- a/aleksis/apps/lesrooster/schema/break_slot.py
+++ b/aleksis/apps/lesrooster/schema/break_slot.py
@@ -6,6 +6,7 @@ from graphene_django_cud.mutations import (
     DjangoBatchPatchMutation,
     DjangoCreateMutation,
 )
+from guardian.shortcuts import get_objects_for_user
 
 from aleksis.core.schema.base import (
     DeleteMutation,
@@ -40,7 +41,9 @@ class BreakSlotType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
 
     @classmethod
     def get_queryset(cls, queryset, info):
-        return queryset  # FIXME filter this queryset based on permissions
+        if not info.context.user.has_perm("lesrooster.view_breakslot_rule"):
+            return get_objects_for_user(info.context.user, "lesrooster.view_breakslot", queryset)
+        return queryset
 
 
 class BreakSlotCreateMutation(DjangoCreateMutation):
@@ -48,13 +51,20 @@ class BreakSlotCreateMutation(DjangoCreateMutation):
         model = BreakSlot
         return_field_name = "breakSlot"
         field_types = {"weekday": graphene.Int()}
-        exclude = ("managed_by_app_label",)
-        permissions = ("lesrooster.create_break_slot_rule",)
+        only_fields = (
+            "time_grid",
+            "name",
+            "weekday",
+            "period_after",
+            "time_start",
+            "time_end",
+        )
+        permissions = ("lesrooster.create_breakslot_rule",)
 
 
 class BreakSlotDeleteMutation(DeleteMutation):
     klass = BreakSlot
-    permission_required = "lesrooster.delete_breakslot"
+    permission_required = "lesrooster.delete_breakslot_rule"
 
 
 class BreakSlotBatchCreateMutation(PermissionBatchPatchMixin, DjangoBatchCreateMutation):
@@ -62,14 +72,21 @@ class BreakSlotBatchCreateMutation(PermissionBatchPatchMixin, DjangoBatchCreateM
         model = BreakSlot
         return_field_name = "breakSlots"
         field_types = {"weekday": graphene.Int()}
-        exclude = ("managed_by_app_label",)
-        permissions = ("lesrooster.create_break_slot_rule",)
+        only_fields = (
+            "time_grid",
+            "name",
+            "weekday",
+            "period_after",
+            "time_start",
+            "time_end",
+        )
+        permissions = ("lesrooster.create_breakslot_rule",)
 
 
 class BreakSlotBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMutation):
     class Meta:
         model = BreakSlot
-        permissions = ("lesrooster.delete_breakslot",)
+        permissions = ("lesrooster.delete_breakslot_rule",)
 
 
 class BreakSlotBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation):
@@ -77,4 +94,12 @@ class BreakSlotBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMut
         model = BreakSlot
         return_field_name = "breakSlots"
         field_types = {"weekday": graphene.Int()}
-        permissions = ("lesrooster.change_breakslot",)
+        permissions = ("lesrooster.edit_breakslot_rule",)
+        only_fields = (
+            "time_grid",
+            "name",
+            "weekday",
+            "period_after",
+            "time_start",
+            "time_end",
+        )
diff --git a/aleksis/apps/lesrooster/schema/lesson.py b/aleksis/apps/lesrooster/schema/lesson.py
index ee8edf3aec5a83ff238c32b3bf4a366913470542..c01743072fe3d6236e48bdbc7b6318d0c22e9be1 100644
--- a/aleksis/apps/lesrooster/schema/lesson.py
+++ b/aleksis/apps/lesrooster/schema/lesson.py
@@ -6,6 +6,7 @@ from graphene_django_cud.mutations import (
     DjangoCreateMutation,
     DjangoPatchMutation,
 )
+from guardian.shortcuts import get_objects_for_user
 from recurrence import Recurrence, deserialize, serialize
 
 from aleksis.core.schema.base import (
@@ -37,7 +38,8 @@ class LessonType(
 
     @classmethod
     def get_queryset(cls, queryset, info):
-        # FIXME filter this queryset based on permissions
+        if not info.context.user.has_perm("lesrooster.view_lesson_rule"):
+            return get_objects_for_user(info.context.user, "lesrooster.view_lesson", queryset)
         return queryset
 
     @staticmethod
@@ -48,8 +50,7 @@ class LessonType(
 class LessonCreateMutation(DjangoCreateMutation):
     class Meta:
         model = Lesson
-        fields = (
-            "id",
+        only_fields = (
             "course",
             "slot_start",
             "slot_end",
@@ -59,7 +60,7 @@ class LessonCreateMutation(DjangoCreateMutation):
             "recurrence",
         )
         field_types = {"recurrence": graphene.String()}
-        permissions = ("lesrooster.create_lesson",)
+        permissions = ("lesrooster.create_lesson_rule",)
 
     @classmethod
     def handle_recurrence(cls, value: str, name, info) -> Recurrence:
@@ -80,8 +81,7 @@ class LessonBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMut
 class LessonBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation):
     class Meta:
         model = Lesson
-        fields = (
-            "id",
+        only_fields = (
             "course",
             "slot_start",
             "slot_end",
@@ -91,7 +91,7 @@ class LessonBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutati
             "recurrence",
         )
         field_types = {"recurrence": graphene.String()}
-        permissions = ("lesrooster.change_lesson",)
+        permissions = ("lesrooster.edit_lesson_rule",)
 
     @classmethod
     def handle_recurrence(cls, value: str, name, info) -> Recurrence:
@@ -101,8 +101,7 @@ class LessonBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutati
 class LessonPatchMutation(PermissionPatchMixin, DjangoPatchMutation):
     class Meta:
         model = Lesson
-        fields = (
-            "id",
+        only_fields = (
             "course",
             "slot_start",
             "slot_end",
@@ -112,7 +111,7 @@ class LessonPatchMutation(PermissionPatchMixin, DjangoPatchMutation):
             "recurrence",
         )
         field_types = {"recurrence": graphene.String()}
-        permissions = ("lesrooster.change_lesson",)
+        permissions = ("lesrooster.edit_lesson_rule",)
 
     @classmethod
     def handle_recurrence(cls, value: str, name, info) -> Recurrence:
diff --git a/aleksis/apps/lesrooster/schema/slot.py b/aleksis/apps/lesrooster/schema/slot.py
index 9e7e4e190e4bf551b4c391aacdd769d7c6949e39..f9d62530b58107ba454537089656644ca35d3979 100644
--- a/aleksis/apps/lesrooster/schema/slot.py
+++ b/aleksis/apps/lesrooster/schema/slot.py
@@ -9,6 +9,7 @@ from graphene_django_cud.mutations import (
     DjangoBatchPatchMutation,
     DjangoCreateMutation,
 )
+from guardian.shortcuts import get_objects_for_user
 
 from aleksis.core.schema.base import (
     DeleteMutation,
@@ -42,7 +43,9 @@ class SlotType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
 
     @classmethod
     def get_queryset(cls, queryset, info):
-        return queryset  # FIXME filter this queryset based on permissions
+        if not info.context.user.has_perm("lesrooster.view_slot_rule"):
+            return get_objects_for_user(info.context.user, "lesrooster.view_slot", queryset)
+        return queryset
 
     @staticmethod
     def resolve_model(root, info):
@@ -57,7 +60,7 @@ class SlotCreateMutation(DjangoCreateMutation):
     class Meta:
         model = Slot
         field_types = {"weekday": graphene.Int()}
-        exclude = ("managed_by_app_label",)
+        only_fields = ("time_grid", "name", "weekday", "period", "time_start", "time_end")
         permissions = ("lesrooster.create_slot_rule",)
 
 
@@ -70,7 +73,7 @@ class SlotBatchCreateMutation(PermissionBatchPatchMixin, DjangoBatchCreateMutati
     class Meta:
         model = Slot
         field_types = {"weekday": graphene.Int()}
-        exclude = ("managed_by_app_label",)
+        only_fields = ("time_grid", "name", "weekday", "period", "time_start", "time_end")
         permissions = ("lesrooster.create_slot_rule",)
 
 
@@ -85,6 +88,7 @@ class SlotBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation
         model = Slot
         field_types = {"weekday": graphene.Int()}
         permissions = ("lesrooster.change_slot",)
+        only_fields = ("time_grid", "name", "weekday", "period", "time_start", "time_end")
 
 
 class CarryOverSlotsMutation(graphene.Mutation):
@@ -100,7 +104,7 @@ class CarryOverSlotsMutation(graphene.Mutation):
 
     @classmethod
     def mutate(cls, root, info, time_grid, from_day, to_day, only=None):
-        if not info.context.user.has_perm("lesrooster.change_slot"):
+        if not info.context.user.has_perm("lesrooster.edit_slot_rule"):
             raise PermissionDenied()
 
         if only is None:
@@ -164,7 +168,7 @@ class CopySlotsFromDifferentRangeMutation(graphene.Mutation):
 
     @classmethod
     def mutate(cls, root, info, time_grid, from_time_grid):
-        if not info.context.user.has_perm("lesrooster.change_slot"):
+        if not info.context.user.has_perm("lesrooster.edit_slot_rule"):
             raise PermissionDenied()
 
         time_grid = TimeGrid.objects.get(id=time_grid)
diff --git a/aleksis/apps/lesrooster/schema/supervision.py b/aleksis/apps/lesrooster/schema/supervision.py
index b0c249f1ad1d4e6e23e0172f4de30748d82897c7..ac1cb89ebc6badcffd9a40a231caa891c891f438 100644
--- a/aleksis/apps/lesrooster/schema/supervision.py
+++ b/aleksis/apps/lesrooster/schema/supervision.py
@@ -5,6 +5,7 @@ from graphene_django_cud.mutations import (
     DjangoBatchPatchMutation,
     DjangoCreateMutation,
 )
+from guardian.shortcuts import get_objects_for_user
 from recurrence import Recurrence, deserialize, serialize
 
 from aleksis.core.schema.base import (
@@ -41,7 +42,9 @@ class SupervisionType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType)
 
     @classmethod
     def get_queryset(cls, queryset, info):
-        return queryset  # FIXME filter this queryset based on permissions
+        if not info.context.user.has_perm("lesrooster.view_supervision_rule"):
+            return get_objects_for_user(info.context.user, "lesrooster.view_supervision", queryset)
+        return queryset
 
     @staticmethod
     def resolve_recurrence(root, info, **kwargs):
@@ -51,16 +54,14 @@ class SupervisionType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType)
 class SupervisionCreateMutation(DjangoCreateMutation):
     class Meta:
         model = Supervision
-        fields = (
-            "id",
+        field_types = {"recurrence": graphene.String()}
+        only_fields = (
             "rooms",
             "teachers",
             "break_slot",
             "recurrence",
         )
-        field_types = {"recurrence": graphene.String()}
-        exclude = ("managed_by_app_label",)
-        permissions = ("lesrooster.create_supervision",)
+        permissions = ("lesrooster.create_supervision_rule",)
 
     @classmethod
     def handle_recurrence(cls, value: str, name, info) -> Recurrence:
@@ -69,20 +70,19 @@ class SupervisionCreateMutation(DjangoCreateMutation):
 
 class SupervisionDeleteMutation(DeleteMutation):
     klass = Supervision
-    permission_required = "lesrooster.delete_supervision"
+    permission_required = "lesrooster.delete_supervision_rule"
 
 
 class SupervisionBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMutation):
     class Meta:
         model = Supervision
-        permissions = ("lesrooster.delete_supervision",)
+        permissions = ("lesrooster.delete_supervision_rule",)
 
 
 class SupervisionBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation):
     class Meta:
         model = Supervision
-        fields = (
-            "id",
+        only_fields = (
             "rooms",
             "teachers",
             "break_slot",
@@ -90,7 +90,7 @@ class SupervisionBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchM
         )
         field_types = {"recurrence": graphene.String()}
         exclude = ("managed_by_app_label",)
-        permissions = ("lesrooster.create_supervision",)
+        permissions = ("lesrooster.create_supervision_rule",)
 
     @classmethod
     def handle_recurrence(cls, value: str, name, info) -> Recurrence:
diff --git a/aleksis/apps/lesrooster/schema/time_grid.py b/aleksis/apps/lesrooster/schema/time_grid.py
index 6c0796dab8e98219f679add3b6b50e2acc39e9b1..b9bfbaa9c2a3f9ea78e9818063548b5045bd6b31 100644
--- a/aleksis/apps/lesrooster/schema/time_grid.py
+++ b/aleksis/apps/lesrooster/schema/time_grid.py
@@ -4,6 +4,7 @@ from graphene_django_cud.mutations import (
     DjangoBatchPatchMutation,
     DjangoCreateMutation,
 )
+from guardian.shortcuts import get_objects_for_user
 
 from aleksis.core.schema.base import (
     DeleteMutation,
@@ -34,30 +35,37 @@ class TimeGridType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
 
     @classmethod
     def get_queryset(cls, queryset, info):
-        # FIXME filter this queryset based on permissions
-        return queryset
+        if not info.context.user.has_perm("lesrooster.view_timegrid_rule"):
+            return get_objects_for_user(info.context.user, "lesrooster.view_timegrid", queryset)
+        return []
 
 
 class TimeGridCreateMutation(DjangoCreateMutation):
     class Meta:
         model = TimeGrid
-        permissions = ("lesrooster.create_time_grid_rule",)
-        exclude = ("managed_by_app_label",)
+        permissions = ("lesrooster.create_timegrid_rule",)
+        only_fields = (
+            "validity_range",
+            "group",
+        )
 
 
 class TimeGridDeleteMutation(DeleteMutation):
     klass = TimeGrid
-    permission_required = "lesrooster.delete_timegrid"
+    permission_required = "lesrooster.delete_timegrid_rule"
 
 
 class TimeGridBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMutation):
     class Meta:
         model = TimeGrid
-        permissions = ("lesrooster.delete_timegrid",)
+        permissions = ("lesrooster.delete_timegrid_rule",)
 
 
 class TimeGridBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation):
     class Meta:
         model = TimeGrid
-        permissions = ("lesrooster.change_timegrid",)
-        exclude = ("managed_by_app_label",)
+        permissions = ("lesrooster.edit_timegrid_rule",)
+        only_fields = (
+            "validity_range",
+            "group",
+        )
diff --git a/aleksis/apps/lesrooster/schema/timebound_course_config.py b/aleksis/apps/lesrooster/schema/timebound_course_config.py
index d5710da79106f3337f549b25e449c92c05fdf8a5..dc5e37487829762633197d80cd079fed42d8cb5e 100644
--- a/aleksis/apps/lesrooster/schema/timebound_course_config.py
+++ b/aleksis/apps/lesrooster/schema/timebound_course_config.py
@@ -1,5 +1,4 @@
 import graphene
-from graphene_django import DjangoListField
 from graphene_django.types import DjangoObjectType
 from graphene_django_cud.mutations import (
     DjangoBatchCreateMutation,
@@ -31,7 +30,13 @@ class TimeboundCourseConfigType(PermissionsTypeMixin, DjangoFilterMixin, DjangoO
 
     @classmethod
     def get_queryset(cls, queryset, info):
-        return queryset  # FIXME filter this queryset based on permissions
+        if not info.context.user.has_perm("lesrostter.view_timeboundcourseconfig_rule"):
+            return get_objects_for_user(
+                info.context.user,
+                "lesrooster.view_timeboundcourseconfig",
+                root.lr_timebound_course_configs.all(),
+            )
+        return queryset
 
     @staticmethod
     def resolve_name(root, info, **kwargs):
@@ -66,11 +71,12 @@ class LesroosterExtendedCourseType(CourseType):
 
     @staticmethod
     def resolve_lr_timebound_course_configs(root, info, **kwargs):
-        return get_objects_for_user(
-            info.context.user,
-            "lesrooster.view_timeboundcourseconfig",
-            root.lr_timebound_course_configs.all(),
-        )
+        if not info.context.user.has_perm("lesrostter.view_timeboundcourseconfig_rule"):
+            return get_objects_for_user(
+                info.context.user,
+                "lesrooster.view_timeboundcourseconfig",
+                root.lr_timebound_course_configs.all(),
+            )
 
 
 class LesroosterExtendedSubjectType(SubjectType):
@@ -84,23 +90,23 @@ class TimeboundCourseConfigCreateMutation(DjangoCreateMutation):
     class Meta:
         model = TimeboundCourseConfig
         fields = ("id", "course", "validity_range", "lesson_quota", "teachers")
-        permissions = ("lesrooster.create_timebound_course_config_rule",)
+        permissions = ("lesrooster.create_timeboundcourseconfig_rule",)
 
 
 class TimeboundCourseConfigBatchCreateMutation(DjangoBatchCreateMutation):
     class Meta:
         model = TimeboundCourseConfig
         fields = ("id", "course", "validity_range", "lesson_quota", "teachers")
-        permissions = ("lesrooster.create_timebound_course_config_rule",)
+        permissions = ("lesrooster.create_timeboundcourseconfig_rule",)
 
 
 class TimeboundCourseConfigDeleteMutation(DeleteMutation):
     klass = TimeboundCourseConfig
-    permission_required = "lesrooster.delete_timeboundcourseconfig"
+    permission_required = "lesrooster.delete_timeboundcourseconfig_rule"
 
 
 class TimeboundCourseConfigBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation):
     class Meta:
         model = TimeboundCourseConfig
         fields = ("id", "course", "validity_range", "lesson_quota", "teachers")
-        permissions = ("lesrooster.change_timeboundcourseconfig",)
+        permissions = ("lesrooster.change_timeboundcourseconfig_rule",)
diff --git a/aleksis/apps/lesrooster/schema/validity_range.py b/aleksis/apps/lesrooster/schema/validity_range.py
index 0d38943ee4c6ed9cb48825863b6e7f2e16d915e5..526a2a61272ca24c2d033566a72b618fafead87c 100644
--- a/aleksis/apps/lesrooster/schema/validity_range.py
+++ b/aleksis/apps/lesrooster/schema/validity_range.py
@@ -4,6 +4,7 @@ from graphene_django_cud.mutations import (
     DjangoBatchPatchMutation,
     DjangoCreateMutation,
 )
+from guardian.shortcuts import get_objects_for_user
 
 from aleksis.core.schema.base import (
     DeleteMutation,
@@ -31,30 +32,33 @@ class ValidityRangeType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
 
     @classmethod
     def get_queryset(cls, queryset, info):
-        # FIXME filter this queryset based on permissions
+        if not info.context.user.has_perm("lesrooster.view_validityrange_rule"):
+            return get_objects_for_user(
+                info.context.user, "lesrooster.view_validityrange", queryset
+            )
         return queryset
 
 
 class ValidityRangeCreateMutation(DjangoCreateMutation):
     class Meta:
         model = ValidityRange
-        permissions = ("lesrooster.create_validity_range_rule",)
-        exclude = ("managed_by_app_label",)
+        permissions = ("lesrooster.create_validityrange_rule",)
+        only_fields = ("school_term", "name", "date_start", "date_end", "status", "time_grids")
 
 
 class ValidityRangeDeleteMutation(DeleteMutation):
     klass = ValidityRange
-    permission_required = "lesrooster.delete_validityrange"
+    permission_required = "lesrooster.delete_validityrange_rule"
 
 
 class ValidityRangeBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMutation):
     class Meta:
         model = ValidityRange
-        permissions = ("lesrooster.delete_validityrange",)
+        permissions = ("lesrooster.delete_validityrange_rule",)
 
 
 class ValidityRangeBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation):
     class Meta:
         model = ValidityRange
-        permissions = ("lesrooster.change_validityrange",)
-        exclude = ("managed_by_app_label",)
+        permissions = ("lesrooster.edit_validityrange_rule",)
+        only_fields = ("school_term", "name", "date_start", "date_end", "status", "time_grids")
diff --git a/aleksis/apps/lesrooster/util/signal_handlers.py b/aleksis/apps/lesrooster/util/signal_handlers.py
index 5ef815e552658636ec97796e5df904e27b5fc023..d4e7652460ab5529c014bd70b26eb6e3b43efb34 100644
--- a/aleksis/apps/lesrooster/util/signal_handlers.py
+++ b/aleksis/apps/lesrooster/util/signal_handlers.py
@@ -1,6 +1,7 @@
-from django.db.models import Q
 import logging
 
+from django.db.models import Q
+
 
 def post_save_handler(sender, instance, created, **kwargs):
     """Sync the instance with Chronos after it has been saved."""
@@ -25,7 +26,8 @@ def pre_delete_handler(sender, instance, **kwargs):
         del_obj = instance.lesson_event.delete()
     elif hasattr(instance, "supervision_event"):
         logging.debug(
-            f"Delete supervision event {instance.supervision_event} after deletion of lesson {instance}"
+            f"Delete supervision event {instance.supervision_event} "
+            f"after deletion of lesson {instance}"
         )
         del_obj = instance.supervision_event.delete()