diff --git a/aleksis/apps/chronos/admin.py b/aleksis/apps/chronos/admin.py
index 5145472fdbf9eff3349c81d0cbf10a59c24442f2..34b8df69b1effd3169e6b1557f0501674cd6734d 100644
--- a/aleksis/apps/chronos/admin.py
+++ b/aleksis/apps/chronos/admin.py
@@ -3,6 +3,8 @@
 from django.contrib import admin
 from django.utils.html import format_html
 
+from guardian.admin import GuardedModelAdmin
+
 from .models import (
     Absence,
     AbsenceReason,
@@ -144,7 +146,7 @@ class LessonAdmin(admin.ModelAdmin):
 admin.site.register(Lesson, LessonAdmin)
 
 
-class RoomAdmin(admin.ModelAdmin):
+class RoomAdmin(GuardedModelAdmin):
     list_display = ("short_name", "name")
     list_display_links = ("short_name", "name")
 
diff --git a/aleksis/apps/chronos/migrations/0005_add_permissions.py b/aleksis/apps/chronos/migrations/0005_add_permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..9fcc2fba01d69514c27568e632e6ee1dba227672
--- /dev/null
+++ b/aleksis/apps/chronos/migrations/0005_add_permissions.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.3 on 2020-12-22 12:22
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chronos', '0004_substitution_extra_lesson_year'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='room',
+            options={'ordering': ['name', 'short_name'],
+                     'permissions': (('view_room_timetable', 'Can view room timetable'),),
+                     'verbose_name': 'Room', 'verbose_name_plural': 'Rooms'},
+        ),
+        migrations.AlterModelOptions(
+            name='chronosglobalpermissions',
+            options={'managed': False, 'permissions': (('view_all_room_timetables', 'Can view all room timetables'), ('view_all_group_timetables', 'Can view all group timetables'), ('view_all_person_timetables', 'Can view all person timetables'), ('view_timetable_overview', 'Can view timetable overview'), ('view_lessons_day', 'Can view all lessons per day'))},
+        ),
+    ]
diff --git a/aleksis/apps/chronos/migrations/0005_indexes.py b/aleksis/apps/chronos/migrations/0006_indexes.py
similarity index 97%
rename from aleksis/apps/chronos/migrations/0005_indexes.py
rename to aleksis/apps/chronos/migrations/0006_indexes.py
index a00fefca15a12802d608a6494f6b5fded6e8ee66..9b86b6b7906ab3568f89190199eba02f88562d22 100644
--- a/aleksis/apps/chronos/migrations/0005_indexes.py
+++ b/aleksis/apps/chronos/migrations/0006_indexes.py
@@ -8,7 +8,7 @@ class Migration(migrations.Migration):
 
     dependencies = [
         ('core', '0010_external_link_widget'),
-        ('chronos', '0004_substitution_extra_lesson_year'),
+        ('chronos', '0005_add_permissions'),
     ]
 
     operations = [
diff --git a/aleksis/apps/chronos/model_extensions.py b/aleksis/apps/chronos/model_extensions.py
index 9bdf9fa8d9f183898873e5b22dc61722dcfd53d8..6cb98dad7d5c7ca732dfe800345abbf34f6320de 100644
--- a/aleksis/apps/chronos/model_extensions.py
+++ b/aleksis/apps/chronos/model_extensions.py
@@ -134,3 +134,12 @@ Announcement.field(
 )
 
 Group.foreign_key("subject", Subject, related_name="groups")
+
+# Dynamically add extra permissions to Group and Person models in core
+# Note: requires migrate afterwards
+Group.add_permission(
+    "view_group_timetable", _("Can view group timetable"),
+)
+Person.add_permission(
+    "view_person_timetable", _("Can view person timetable"),
+)
diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py
index 9aaf8227c0b4639b2b3042af45fdc505bb7d5c4c..9842363ca301fb7bf3ddcc9a3f7bd96a51f7f375 100644
--- a/aleksis/apps/chronos/models.py
+++ b/aleksis/apps/chronos/models.py
@@ -331,6 +331,7 @@ class Room(ExtensibleModel):
         return reverse("timetable", args=["room", self.id])
 
     class Meta:
+        permissions = (("view_room_timetable", _("Can view room timetable")),)
         ordering = ["name", "short_name"]
         verbose_name = _("Room")
         verbose_name_plural = _("Rooms")
@@ -1069,7 +1070,9 @@ class ChronosGlobalPermissions(GlobalPermissionModel):
     class Meta:
         managed = False
         permissions = (
-            ("view_all_timetables", _("Can view all timetables")),
+            ("view_all_room_timetables", _("Can view all room timetables")),
+            ("view_all_group_timetables", _("Can view all group timetables")),
+            ("view_all_person_timetables", _("Can view all person 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
index 7ee9d1e0dcfb7b937b24ef82bb1adf36fa10dc46..aec25bf3b7e773871d4e9428c7c5c8da46e551ba 100644
--- a/aleksis/apps/chronos/rules.py
+++ b/aleksis/apps/chronos/rules.py
@@ -8,19 +8,19 @@ from aleksis.core.util.predicates import (
 )
 
 from .models import LessonSubstitution
-from .util.predicates import has_timetable_perm
+from .util.predicates import has_any_timetable_object, has_timetable_perm
 
 # View timetable overview
-view_timetable_overview_predicate = has_person & has_global_perm("chronos.view_timetable_overview")
+view_timetable_overview_predicate = has_person & (
+    has_any_timetable_object | 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
-)
+view_timetable_predicate = has_person & has_timetable_perm
 add_perm("chronos.view_timetable", view_timetable_predicate)
 
 # View all lessons per day
diff --git a/aleksis/apps/chronos/templates/chronos/timetable.html b/aleksis/apps/chronos/templates/chronos/timetable.html
index 9201367e5373557fbd468e69403d77ccedf27edb..9f015f8f65e86a02d1736c4c8f7c7786ec1c04a5 100644
--- a/aleksis/apps/chronos/templates/chronos/timetable.html
+++ b/aleksis/apps/chronos/templates/chronos/timetable.html
@@ -36,7 +36,7 @@
       {% endif %}
     </div>
     <div class="col s4 m6 l4 xl3 right align-right no-print">
-      <a class="waves-effect waves-teal btn-flat btn-flat-medium right hide-on-small-and-down" href="{% url "timetable_print" type.value pk %}" id="print">
+      <a class="waves-effect waves-teal btn-flat btn-flat-medium right hide-on-small-and-down" href="{% url "timetable_print" type.value pk %}">
         <i class="material-icons center">print</i>
       </a>
     </div>
diff --git a/aleksis/apps/chronos/util/chronos_helpers.py b/aleksis/apps/chronos/util/chronos_helpers.py
index 797eefcd95a18d9a3a930f24e8a7c50af9c92642..f4bb7d2d1d0c0bf771d68076f429b3ca8dd348ee 100644
--- a/aleksis/apps/chronos/util/chronos_helpers.py
+++ b/aleksis/apps/chronos/util/chronos_helpers.py
@@ -1,13 +1,22 @@
-from typing import Optional
+from typing import TYPE_CHECKING, Optional
 
+from django.db.models import Count, Q
 from django.http import HttpRequest, HttpResponseNotFound
 from django.shortcuts import get_object_or_404
 
+from guardian.core import ObjectPermissionChecker
+
 from aleksis.core.models import Group, Person
+from aleksis.core.util.predicates import check_global_permission
 
 from ..managers import TimetableType
 from ..models import LessonPeriod, LessonSubstitution, Room
 
+if TYPE_CHECKING:
+    from django.contrib.auth import get_user_model
+
+    User = get_user_model()  # noqa
+
 
 def get_el_by_pk(
     request: HttpRequest,
@@ -39,3 +48,85 @@ def get_substitution_by_id(request: HttpRequest, id_: int, week: int):
     return LessonSubstitution.objects.filter(
         week=wanted_week.week, year=wanted_week.year, lesson_period=lesson_period
     ).first()
+
+
+def get_teachers(user: "User"):
+    """Get the teachers whose timetables are allowed to be seen by current user."""
+    checker = ObjectPermissionChecker(user)
+
+    teachers = (
+        Person.objects.annotate(lessons_count=Count("lessons_as_teacher"))
+        .filter(lessons_count__gt=0)
+        .order_by("short_name", "last_name")
+    )
+
+    if not check_global_permission(user, "chronos.view_all_person_timetables"):
+        checker.prefetch_perms(teachers)
+
+        wanted_teachers = set()
+
+        for teacher in teachers:
+            if checker.has_perm("core.view_person_timetable", teacher):
+                wanted_teachers.add(teacher.pk)
+
+        teachers = teachers.filter(Q(pk=user.person.pk) | Q(pk__in=wanted_teachers))
+
+    return teachers
+
+
+def get_classes(user: "User"):
+    """Get the classes whose timetables are allowed to be seen by current user."""
+    checker = ObjectPermissionChecker(user)
+
+    classes = (
+        Group.objects.for_current_school_term_or_all()
+        .annotate(
+            lessons_count=Count("lessons"), child_lessons_count=Count("child_groups__lessons"),
+        )
+        .filter(
+            Q(lessons_count__gt=0, parent_groups=None)
+            | Q(child_lessons_count__gt=0, parent_groups=None)
+        )
+        .order_by("short_name", "name")
+    )
+
+    if not check_global_permission(user, "chronos.view_all_group_timetables"):
+        checker.prefetch_perms(classes)
+
+        wanted_classes = set()
+
+        for _class in classes:
+            if checker.has_perm("core.view_group_timetable", _class):
+                wanted_classes.add(_class.pk)
+
+        classes = classes.filter(
+            Q(pk__in=wanted_classes) | Q(members=user.person) | Q(owners=user.person)
+        )
+        if user.person.primary_group:
+            classes = classes.filter(Q(pk=user.person.primary_group.pk))
+
+    return classes
+
+
+def get_rooms(user: "User"):
+    """Get the rooms whose timetables are allowed to be seen by current user."""
+    checker = ObjectPermissionChecker(user)
+
+    rooms = (
+        Room.objects.annotate(lessons_count=Count("lesson_periods"))
+        .filter(lessons_count__gt=0)
+        .order_by("short_name", "name")
+    )
+
+    if not check_global_permission(user, "chronos.view_all_room_timetables"):
+        checker.prefetch_perms(rooms)
+
+        wanted_rooms = set()
+
+        for room in rooms:
+            if checker.has_perm("chronos.view_room_timetable", room):
+                wanted_rooms.add(room.pk)
+
+        rooms = rooms.filter(Q(pk__in=wanted_rooms))
+
+    return rooms
diff --git a/aleksis/apps/chronos/util/predicates.py b/aleksis/apps/chronos/util/predicates.py
index cdbe5e2c268579558b0f29a184ed82709eda764a..fb70edee23dbae9f2014634526bea4b7d867adf7 100644
--- a/aleksis/apps/chronos/util/predicates.py
+++ b/aleksis/apps/chronos/util/predicates.py
@@ -3,18 +3,77 @@ from django.db.models import Model
 
 from rules import predicate
 
-from aleksis.apps.chronos.models import Room
 from aleksis.core.models import Group, Person
+from aleksis.core.util.predicates import has_global_perm, has_object_perm
+
+from ..models import Room
+from .chronos_helpers import get_classes, get_rooms, get_teachers
 
 
 @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
+    """
+    Check if can access timetable.
+
+    Predicate which checks whether the user is allowed
+    to access the requested timetable.
+    """
+    if isinstance(obj, Group):
+        return has_group_timetable_perm(user, obj)
+    elif isinstance(obj, Person):
+        return has_person_timetable_perm(user, obj)
+    elif isinstance(obj, Room):
+        return has_room_timetable_perm(user, obj)
     else:
         return False
+
+
+@predicate
+def has_group_timetable_perm(user: User, obj: Group) -> bool:
+    """
+    Check if can access group timetable.
+
+    Predicate which checks whether the user is allowed
+    to access the requested group timetable.
+    """
+    return (
+        obj in user.person.member_of.all()
+        or user.person.primary_group == obj
+        or obj in user.person.owner_of.all()
+        or has_global_perm("chronos.view_all_group_timetables")(user)
+        or has_object_perm("core.view_group_timetable")(user, obj)
+    )
+
+
+@predicate
+def has_person_timetable_perm(user: User, obj: Person) -> bool:
+    """
+    Check if can access person timetable.
+
+    Predicate which checks whether the user is allowed
+    to access the requested person timetable.
+    """
+    return (
+        user.person == obj
+        or has_global_perm("chronos.view_all_person_timetables")(user)
+        or has_object_perm("core.view_person_timetable")(user, obj)
+    )
+
+
+@predicate
+def has_room_timetable_perm(user: User, obj: Room) -> bool:
+    """
+    Check if can access room timetable.
+
+    Predicate which checks whether the user is allowed
+    to access the requested room timetable.
+    """
+    return has_global_perm("chronos.view_all_room_timetables")(user) or has_object_perm(
+        "chronos.view_room_timetable"
+    )(user, obj)
+
+
+@predicate
+def has_any_timetable_object(user: User) -> bool:
+    """Predicate which checks whether there are any timetables the user is allowed to access."""
+    return get_classes(user).exists() or get_rooms(user).exists() or get_teachers(user).exists()
diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py
index a13d65356ebb2ec4981eb375170f6b0c35aac5b8..4ddc0c73b7d8f24b5b6adc8d0a6a9aaeaf3e0d9f 100644
--- a/aleksis/apps/chronos/views.py
+++ b/aleksis/apps/chronos/views.py
@@ -1,7 +1,7 @@
 from datetime import datetime, timedelta
 from typing import Optional
 
-from django.db.models import Count, Q
+from django.db.models import Q
 from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
@@ -12,17 +12,23 @@ from django.views.decorators.cache import never_cache
 from django_tables2 import RequestConfig
 from rules.contrib.views import permission_required
 
-from aleksis.core.models import Announcement, Group, Person
+from aleksis.core.models import Announcement, Group
 from aleksis.core.util import messages
 from aleksis.core.util.core_helpers import get_site_preferences, has_person
 from aleksis.core.util.pdf import render_pdf
 
 from .forms import LessonSubstitutionForm
 from .managers import TimetableType
-from .models import Absence, Holiday, LessonPeriod, LessonSubstitution, Room, TimePeriod
+from .models import Absence, Holiday, LessonPeriod, LessonSubstitution, TimePeriod
 from .tables import LessonsTable
 from .util.build import build_substitutions_list, build_timetable, build_weekdays
-from .util.chronos_helpers import get_el_by_pk, get_substitution_by_id
+from .util.chronos_helpers import (
+    get_classes,
+    get_el_by_pk,
+    get_rooms,
+    get_substitution_by_id,
+    get_teachers,
+)
 from .util.date import CalendarWeek, get_weeks_for_year
 from .util.js import date_unix
 
@@ -32,22 +38,8 @@ def all_timetables(request: HttpRequest) -> HttpResponse:
     """View all timetables for persons, groups and rooms."""
     context = {}
 
-    teachers = (
-        Person.objects.alias(lessons_count=Count("lessons_as_teacher"))
-        .filter(lessons_count__gt=0)
-        .order_by("short_name", "last_name")
-    )
-    groups = Group.objects.for_current_school_term_or_all().alias(
-        lessons_count=Count("lessons"), child_lessons_count=Count("child_groups__lessons"),
-    )
-    classes = groups.filter(lessons_count__gt=0, parent_groups=None) | groups.filter(
-        child_lessons_count__gt=0, parent_groups=None
-    ).order_by("short_name", "name")
-    rooms = (
-        Room.objects.alias(lessons_count=Count("lesson_periods"))
-        .filter(lessons_count__gt=0)
-        .order_by("short_name", "name")
-    )
+    user = request.user
+    teachers, classes, rooms = get_teachers(user), get_classes(user), get_rooms(user)
 
     context["teachers"] = teachers
     context["classes"] = classes