diff --git a/README.rst b/README.rst
index 664d44feccd825af419d30650fa31a27e97234c2..067f83e32bfedf6251c91b5d5e42162c1db97b9e 100644
--- a/README.rst
+++ b/README.rst
@@ -36,6 +36,7 @@ Licence
 
 ::
 
+  Copyright © 2021 magicfelix <felix@felix-zauberer.de>
   Copyright © 2017, 2018, 2019, 2020 Jonathan Weth <wethjo@katharineum.de>
   Copyright © 2017, 2018, 2019 Frank Poetzsch-Heffter <p-h@katharineum.de>
   Copyright © 2018, 2019, 2020 Julian Leucker <leuckeju@katharineum.de>
diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index 42edc4bceaa61a55de310f59cea74dc270a3cf0b..a3b9b7a9a3957b9af7ad0b9ddeb57d00a630ca73 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -28,6 +28,7 @@ class CoreConfig(AppConfig):
     }
     licence = "EUPL-1.2+"
     copyright_info = (
+        ([2021], "magicfelix", "felix@felix-zauberer.de"),
         ([2017, 2018, 2019, 2020], "Jonathan Weth", "wethjo@katharineum.de"),
         ([2017, 2018, 2019], "Frank Poetzsch-Heffter", "p-h@katharineum.de"),
         ([2018, 2019, 2020], "Julian Leucker", "leuckeju@katharineum.de"),
diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py
index ad90929ef8f4332daf50ef3485b47383f79a9dea..9c81cf4c82fe573ba8787d309b0d4bbfba1751ff 100644
--- a/aleksis/core/preferences.py
+++ b/aleksis/core/preferences.py
@@ -252,3 +252,12 @@ class DataChecksEmailsRecipientGroups(ModelMultipleChoicePreference):
     default = []
     model = Group
     verbose_name = _("Email recipient groups for data checks problem emails")
+
+
+@site_preferences_registry.register
+class AnonymousDashboard(BooleanPreference):
+    section = general
+    name = "anonymous_dashboard"
+    default = False
+    required = False
+    verbose_name = _("Show dashboard to users without login")
diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index 5e39c1af4d38f15985c4ac13e6348a3130702391..cf29b8790a310573d4719f89230a200850ea9bc8 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -9,12 +9,14 @@ from .util.predicates import (
     is_current_person,
     is_group_owner,
     is_notification_recipient,
+    is_site_preference_set,
 )
 
 rules.add_perm("core", rules.always_allow)
 
 # View dashboard
-rules.add_perm("core.view_dashboard", has_person)
+view_dashboard_predicate = is_site_preference_set("general", "anonymous_dashboard") | has_person
+rules.add_perm("core.view_dashboard", view_dashboard_predicate)
 
 # View notifications
 rules.add_perm("core.view_notifications", has_person)
@@ -310,6 +312,9 @@ rules.add_perm("core.edit_dashboardwidget", edit_dashboard_widget_predicate)
 delete_dashboard_widget_predicate = has_person & has_global_perm("core.delete_dashboardwidget")
 rules.add_perm("core.delete_dashboardwidget", delete_dashboard_widget_predicate)
 
+edit_dashboard_predicate = has_person
+rules.add_perm("core.edit_dashboard", edit_dashboard_predicate)
+
 edit_default_dashboard_predicate = has_person & has_global_perm("core.edit_default_dashboard")
 rules.add_perm("core.edit_default_dashboard", edit_default_dashboard_predicate)
 
diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html
index 419bb59f11b5084eee7ac379605c3cefe00d530b..b690155f010e932cdb35e80eac639e18fc38bd27 100644
--- a/aleksis/core/templates/core/index.html
+++ b/aleksis/core/templates/core/index.html
@@ -1,5 +1,5 @@
 {% extends 'core/base.html' %}
-{% load i18n static dashboard %}
+{% load i18n static dashboard rules %}
 
 {% block browser_title %}{% blocktrans %}Home{% endblocktrans %}{% endblock %}
 {% block no_page_title %}{% endblock %}
@@ -9,51 +9,54 @@
 {% endblock %}
 
 {% block content %}
-  <a class="btn-flat waves-effect waves-light right" href="{% url "edit_dashboard" %}">
-    <i class="material-icons left">edit</i>
-    {% trans "Edit dashboard" %}
-  </a>
+  {% has_perm "core.edit_dashboard" user as can_edit_dashboard %}
+  {% if can_edit_dashboard %}
+    <a class="btn-flat waves-effect waves-light right" href="{% url "edit_dashboard" %}">
+      <i class="material-icons left">edit</i>
+      {% trans "Edit dashboard" %}
+    </a>
+  {% endif %}
   <h4>
     {{ request.site.preferences.general__title }}
   </h4>
 
-  {% if user.is_authenticated %}
-    {% for notification in unread_notifications %}
-      <div class="alert primary scale-transition">
-        <div>
-          <i class="material-icons left">info</i>
-
-          <div class="right">
-            <a class="btn-flat waves-effect" href="{% url "notification_mark_read" notification.id %}">
-              <i class="material-icons center">close</i>
-            </a>
-          </div>
+  {% for notification in unread_notifications %}
+    <div class="alert primary scale-transition">
+      <div>
+        <i class="material-icons left">info</i>
 
-          <strong>{{ notification.title }}</strong>
-          <p>{{ notification.description }}</p>
+        <div class="right">
+          <a class="btn-flat waves-effect" href="{% url "notification_mark_read" notification.id %}">
+            <i class="material-icons center">close</i>
+          </a>
         </div>
-      </div>
-    {% endfor %}
-
-    {% include "core/partials/announcements.html" with announcements=announcements %}
 
-    <div class="row" id="live_load">
-      {% for widget in widgets %}
-        <div class="col s{{ widget.size_s }} m{{ widget.size_m }} l{{ widget.size_l }} xl{{ widget.size_xl }}">
-          {% include_widget widget %}
-        </div>
-      {% endfor %}
+        <strong>{{ notification.title }}</strong>
+        <p>{{ notification.description }}</p>
+      </div>
     </div>
+  {% endfor %}
+
+  {% include "core/partials/announcements.html" with announcements=announcements %}
 
-    {% if default_dashboard and widgets %}
-      <div class="grey-text right">
-        {% blocktrans %}
-          You didn't customise your dashboard so that you see the system default. Please click on "Edit dashboard" to
-          customise your personal dashboard.
-        {% endblocktrans %}
+  <div class="row" id="live_load">
+    {% for widget in widgets %}
+      <div class="col s{{ widget.size_s }} m{{ widget.size_m }} l{{ widget.size_l }} xl{{ widget.size_xl }}">
+        {% include_widget widget %}
       </div>
-    {% endif %}
+    {% endfor %}
+  </div>
+
+  {% if default_dashboard and widgets and can_edit_dashboard %}
+    <div class="grey-text right">
+      {% blocktrans %}
+        You didn't customise your dashboard so that you see the system default. Please click on "Edit dashboard" to
+        customise your personal dashboard.
+      {% endblocktrans %}
+    </div>
+  {% endif %}
 
+  {% if activities or notifications %}
     <div class="row">
       <div class="col s12 m6">
         <h5>{% blocktrans %}Last activities{% endblocktrans %}</h5>
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index f1bc04013f92ed2bbde449c47cf44473be70ece0..2ee83523ca49c5b112579a98b49147dbf3afcff8 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -54,6 +54,7 @@ from .models import (
     DashboardWidget,
     DashboardWidgetOrder,
     DataCheckResult,
+    DummyPerson,
     Group,
     GroupType,
     Notification,
@@ -75,7 +76,7 @@ from .tables import (
 )
 from .util import messages
 from .util.apps import AppConfig
-from .util.core_helpers import objectgetter_optional
+from .util.core_helpers import has_person, objectgetter_optional
 from .util.forms import PreferenceLayout
 
 
@@ -84,19 +85,24 @@ def index(request: HttpRequest) -> HttpResponse:
     """View for dashboard."""
     context = {}
 
-    activities = request.user.person.activities.all()[:5]
-    notifications = request.user.person.notifications.all()[:5]
-    unread_notifications = request.user.person.notifications.all().filter(read=False)
+    if has_person(request.user):
+        person = request.user.person
+        widgets = person.dashboard_widgets
+    else:
+        person = DummyPerson()
+        widgets = []
+
+    activities = person.activities.all()[:5]
+    notifications = person.notifications.all()[:5]
+    unread_notifications = person.notifications.all().filter(read=False)
 
     context["activities"] = activities
     context["notifications"] = notifications
     context["unread_notifications"] = unread_notifications
 
-    announcements = Announcement.objects.at_time().for_person(request.user.person)
+    announcements = Announcement.objects.at_time().for_person(person)
     context["announcements"] = announcements
 
-    widgets = request.user.person.dashboard_widgets
-
     if len(widgets) == 0:
         # Use default dashboard if there are no widgets
         widgets = DashboardWidgetOrder.default_dashboard_widgets
@@ -859,9 +865,11 @@ class DashboardWidgetDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
     success_message = _("The dashboard widget has been deleted.")
 
 
-class EditDashboardView(View):
+class EditDashboardView(PermissionRequiredMixin, View):
     """View for editing dashboard widget order."""
 
+    permission_required = "core.edit_dashboard"
+
     def get_context_data(self, request, **kwargs):
         context = {}
         self.default_dashboard = kwargs.get("default", False)