diff --git a/aleksis/apps/chronos/menus.py b/aleksis/apps/chronos/menus.py
index 879cfa9adb7e667fd3f1c7c9b144d0532ecce529..592e6df90319d6165615e8ab3551101ce6e0fbbd 100644
--- a/aleksis/apps/chronos/menus.py
+++ b/aleksis/apps/chronos/menus.py
@@ -16,25 +16,33 @@ MENUS = {
                     "name": _("My timetable"),
                     "url": "my_timetable",
                     "icon": "person",
-                    "validators": ["menu_generator.validators.is_authenticated"],
+                    "validators": [
+                        ("aleksis.core.util.predicates.permission_validator", "chronos.view_my_timetable"),
+                    ],
                 },
                 {
                     "name": _("All timetables"),
                     "url": "all_timetables",
                     "icon": "grid_on",
-                    "validators": ["menu_generator.validators.is_authenticated"],
+                    "validators": [
+                        ("aleksis.core.util.predicates.permission_validator", "chronos.view_timetable_overview"),
+                    ],
                 },
                 {
                     "name": _("Daily lessons"),
                     "url": "lessons_day",
                     "icon": "calendar_today",
-                    "validators": ["menu_generator.validators.is_authenticated"],
+                    "validators": [
+                        ("aleksis.core.util.predicates.permission_validator", "chronos.view_lessons_day"),
+                    ],
                 },
                 {
                     "name": _("Substitutions"),
                     "url": "substitutions",
                     "icon": "update",
-                    "validators": ["menu_generator.validators.is_authenticated"],
+                    "validators": [
+                        ("aleksis.core.util.predicates.permission_validator", "chronos.view_substitutions"),
+                    ],
                 },
             ],
         }
diff --git a/aleksis/apps/chronos/migrations/0016_add_globalpermissions.py b/aleksis/apps/chronos/migrations/0016_add_globalpermissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..785e2782c95b53321ee9940f289564ad2b495065
--- /dev/null
+++ b/aleksis/apps/chronos/migrations/0016_add_globalpermissions.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.0.5 on 2020-04-30 19:03
+
+import django.contrib.postgres.fields.jsonb
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chronos', '0015_rename_abbrev_to_short_name'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='GlobalPermissions',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+            ],
+            options={
+                'permissions': (('view_all_timetables', 'Can view all timetables'), ('view_timetable_overview', 'Can view timetable overview'), ('view_lessons_day', 'Can view all lessons per day')),
+                'managed': False,
+            },
+        ),
+    ]
diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py
index dc84b5cf90d3c0a424d8415200a35f82a348c3d9..bf5d2fe72241c692aaf9aff935e603b7b5b1d2c1 100644
--- a/aleksis/apps/chronos/models.py
+++ b/aleksis/apps/chronos/models.py
@@ -1111,3 +1111,13 @@ class ExtraLesson(ExtensibleModel, GroupPropertiesMixin):
     class Meta:
         verbose_name = _("Extra lesson")
         verbose_name_plural = _("Extra lessons")
+
+
+class GlobalPermissions(ExtensibleModel):
+    class Meta:
+        managed = False
+        permissions = (
+            ("view_all_timetables", _("Can view all timetables")),
+            ("view_timetable_overview", _("Can view timetable overview")),
+            ("view_lessons_day", _("Can view all lessons per day")),
+        )
diff --git a/aleksis/apps/chronos/rules.py b/aleksis/apps/chronos/rules.py
new file mode 100644
index 0000000000000000000000000000000000000000..ddc398cde95665f12e47e4dfe52f5f7c88d4177a
--- /dev/null
+++ b/aleksis/apps/chronos/rules.py
@@ -0,0 +1,47 @@
+from rules import add_perm, always_allow
+
+from aleksis.core.util.predicates import (
+    has_person,
+    has_global_perm,
+    has_any_object,
+    has_object_perm,
+)
+from .models import LessonSubstitution
+from .util.predicates import (
+    has_timetable_perm
+)
+
+# View timetable overview
+view_timetable_overview_predicate = has_person & has_global_perm("chronos.view_timetable_overview")
+add_perm("chronos.view_timetable_overview", view_timetable_overview_predicate)
+
+# View my timetable
+add_perm("chronos.view_my_timetable", has_person)
+
+# View timetable
+view_timetable_predicate = has_person & (
+    has_global_perm("chronos.view_all_timetables") | has_timetable_perm
+)
+add_perm("chronos.view_timetable", view_timetable_predicate)
+
+# View all lessons per day
+view_lessons_day_predicate = has_person & has_global_perm("chronos.view_lessons_day")
+add_perm("chronos.view_lessons_day", view_lessons_day_predicate)
+
+# Edit substition
+edit_substitution_predicate = has_person & (
+    has_global_perm("chronos.change_lessonsubstitution") | has_object_perm("chronos.change_lessonsubstitution")
+)
+add_perm("chronos.edit_substitution", edit_substitution_predicate)
+
+# Delete substitution
+delete_substitution_predicate = has_person & (
+    has_global_perm("chronos.delete_lessonsubstitution") | has_object_perm("chronos.delete_lessonsubstitution")
+)
+add_perm("chronos.delete_substitution", delete_substitution_predicate)
+
+# View substitutions
+view_substitutions_predicate = has_person & (
+    has_global_perm("chronos.view_lessonsubstitution") | has_any_object("chronos.view_lessonsubstitution", LessonSubstitution)
+)
+add_perm("chronos.view_substitutions", view_substitutions_predicate)
diff --git a/aleksis/apps/chronos/util/predicates.py b/aleksis/apps/chronos/util/predicates.py
new file mode 100644
index 0000000000000000000000000000000000000000..11b5d7411208a84361baf9bab63d97353541392d
--- /dev/null
+++ b/aleksis/apps/chronos/util/predicates.py
@@ -0,0 +1,21 @@
+from django.contrib.auth.models import User
+from django.db.models import Model
+
+from rules import predicate
+
+from aleksis.core.models import Group, Person
+from aleksis.apps.chronos.models import Room
+
+
+@predicate
+def has_timetable_perm(user: User, obj: Model) -> bool:
+    """ Predicate which checks whether the user is allowed to access the requested timetable """
+
+    if obj.model is Group:
+        return obj in user.person.member_of
+    elif obj.model is Person:
+        return user.person == obj
+    elif obj.model is Room:
+        return True
+    else:
+        return False
diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py
index 9ebe7de9b927262a372ec68c581c9eef45eb693b..ef128e754e4ebb32e7ed7ef23bf42cd1b4b924fd 100644
--- a/aleksis/apps/chronos/views.py
+++ b/aleksis/apps/chronos/views.py
@@ -11,8 +11,8 @@ from django.urls import reverse
 from django.utils import timezone
 from django.utils.translation import ugettext as _
 from django_tables2 import RequestConfig
+from rules.contrib.views import permission_required
 
-from aleksis.core.decorators import admin_required
 from aleksis.core.models import Person, Group, Announcement
 from aleksis.core.util import messages
 from .forms import LessonSubstitutionForm
@@ -24,7 +24,7 @@ from .util.date import CalendarWeek, get_weeks_for_year
 from aleksis.core.util.core_helpers import has_person
 
 
-@login_required
+@permission_required("chronos.view_timetable_overview")
 def all_timetables(request: HttpRequest) -> HttpResponse:
     context = {}
 
@@ -49,7 +49,7 @@ def all_timetables(request: HttpRequest) -> HttpResponse:
     return render(request, "chronos/all.html", context)
 
 
-@login_required
+@permission_required("chronos.view_my_timetable")
 def my_timetable(
     request: HttpRequest,
     year: Optional[int] = None,
@@ -95,7 +95,25 @@ def my_timetable(
         return redirect("all_timetables")
 
 
-@login_required
+def get_el_by_pk(
+    request: HttpRequest,
+    type_: str,
+    pk: int,
+    year: Optional[int] = None,
+    week: Optional[int] = None,
+    regular: Optional[str] = None,
+):
+    if type_ == TimetableType.GROUP.value:
+        return get_object_or_404(Group, pk=pk)
+    elif type_ == TimetableType.TEACHER.value:
+        return get_object_or_404(Person, pk=pk)
+    elif type_ == TimetableType.ROOM.value:
+        return get_object_or_404(Room, pk=pk)
+    else:
+        return HttpResponseNotFound()
+
+
+@permission_required("chronos.view_timetable", fn=get_el_by_pk)
 def timetable(
     request: HttpRequest,
     type_: str,
@@ -108,14 +126,7 @@ def timetable(
 
     is_smart = regular != "regular"
 
-    if type_ == TimetableType.GROUP.value:
-        el = get_object_or_404(Group, pk=pk)
-    elif type_ == TimetableType.TEACHER.value:
-        el = get_object_or_404(Person, pk=pk)
-    elif type_ == TimetableType.ROOM.value:
-        el = get_object_or_404(Room, pk=pk)
-    else:
-        return HttpResponseNotFound()
+    el = get_el_by_pk(request, type_, pk)
 
     type_ = TimetableType.from_string(type_)
 
@@ -165,7 +176,7 @@ def timetable(
     return render(request, "chronos/timetable.html", context)
 
 
-@login_required
+@permission_required("chronos.view_lessons_day")
 def lessons_day(
     request: HttpRequest,
     year: Optional[int] = None,
@@ -203,16 +214,24 @@ def lessons_day(
     return render(request, "chronos/lessons_day.html", context)
 
 
-@admin_required
+def get_substitution_by_id(request: HttpRequest, id_: int, week: int):
+    lesson_period = get_object_or_404(LessonPeriod, pk=id_)
+    wanted_week = lesson_period.lesson.get_calendar_week(week)
+
+    return LessonSubstitution.objects.filter(
+        week=wanted_week.week, lesson_period=lesson_period
+    ).first()
+
+
+@permission_required("chronos.edit_substitution", fn=get_substitution_by_id)
 def edit_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse:
     context = {}
 
     lesson_period = get_object_or_404(LessonPeriod, pk=id_)
     wanted_week = lesson_period.lesson.get_calendar_week(week)
 
-    lesson_substitution = LessonSubstitution.objects.filter(
-        week=wanted_week.week, lesson_period=lesson_period
-    ).first()
+    lesson_substitution = get_substitution_by_id(request, id_, week)
+
     if lesson_substitution:
         edit_substitution_form = LessonSubstitutionForm(
             request.POST or None, instance=lesson_substitution
@@ -242,14 +261,12 @@ def edit_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse
     return render(request, "chronos/edit_substitution.html", context)
 
 
-@admin_required
+@permission_required("chronos.delete_substitution", fn=get_substitution_by_id)
 def delete_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse:
     lesson_period = get_object_or_404(LessonPeriod, pk=id_)
     wanted_week = lesson_period.lesson.get_calendar_week(week)
 
-    LessonSubstitution.objects.filter(
-        week=wanted_week.week, lesson_period=lesson_period
-    ).delete()
+    get_substitution_by_id(request, id_, week).delete()
 
     messages.success(request, _("The substitution has been deleted."))
 
@@ -260,7 +277,7 @@ def delete_substitution(request: HttpRequest, id_: int, week: int) -> HttpRespon
     )
 
 
-@login_required
+@permission_required("chronos.view_substitutions")
 def substitutions(
     request: HttpRequest,
     year: Optional[int] = None,