diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index 2a0889262e349d0479f4d55f03e8941b5f274bb7..4ebbaee0bc302de092b4f0676eb107254602a547 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -8,6 +8,7 @@ from .util.predicates import (
     is_current_person,
     has_object_perm,
     is_group_owner,
+    is_notification_recipient,
 )
 
 
@@ -104,6 +105,10 @@ add_perm("core.manage_school", manage_school_predicate)
 manage_data_predicate = has_person & has_global_perm("core.manage_data")
 add_perm("core.manage_data", manage_data_predicate)
 
+# Mark notification as read
+mark_notification_as_read_predicate = has_person & is_notification_recipient
+add_perm("core.mark_notification_as_read", mark_notification_as_read_predicate)
+
 # View announcements
 view_announcements_predicate = has_person & (
     has_global_perm("core.view_announcement") | has_any_object("core.view_announcement", Announcement)
diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py
index 3c624f8537aa2b7d1ae2dc089c050f769e55ab08..9c2119a83c71645ced8e67124e8e601dfb31213e 100644
--- a/aleksis/core/util/core_helpers.py
+++ b/aleksis/core/util/core_helpers.py
@@ -4,7 +4,7 @@ from operator import itemgetter
 import os
 import pkgutil
 from importlib import import_module
-from typing import Any, Callable, Sequence, Union, List
+from typing import Any, Callable, Sequence, Union, List, Optional
 from uuid import uuid4
 
 from django.conf import settings
@@ -192,30 +192,13 @@ def now_tomorrow() -> datetime:
     return timezone.now() + timedelta(days=1)
 
 
-def get_person_by_pk(request: HttpRequest, id_: Optional[int] = None):
-    """ Get a person by its ID, defaulting to person in request's user """
+def objectgetter_optional(model: Model, default: Optional[Any] = None, default_eval: bool = False) -> Callable[[HttpRequest, Optional[int]], Model]:
+    """ Get an object by pk, defaulting to None """
 
-    from ..models import Person  # noqa
-
-    if id_:
-        return get_object_or_404(Person, pk=id_)
-    else:
-        return request.user.person
-
-
-def get_group_by_pk(request: HttpRequest, id_: Optional[int] = None) -> Group:
-    """ Get a group by its ID, defaulting to None """
-
-    if id_:
-        return get_object_or_404(Group, id=id_)
-
-    return None
-
-
-def get_announcement_by_pk(request: HttpRequest, id_: Optional[int] = None):
-    """ Get an announcement by its ID; defaulting to None """
-
-    if id_:
-        return get_object_or_404(Announcement, pk=pk)
+    def get_object(request: HttpRequest, id_: Optional[int] = None) -> Model:
+        if id_ is not None:
+            return get_object_or_404(model, pk=id_)
+        else:
+            return eval(default) if default_eval else default
 
-    return None
+    return get_object
diff --git a/aleksis/core/util/predicates.py b/aleksis/core/util/predicates.py
index 396fdbf3a643e96c75394581a67aa98874a13f64..bdfa1c0cdede47436cb3b967be819cc2db0cbd4e 100644
--- a/aleksis/core/util/predicates.py
+++ b/aleksis/core/util/predicates.py
@@ -90,3 +90,9 @@ def is_group_owner(user: User, group: Group) -> bool:
 
     return group.owners.filter(owners=user.person).exists()
 
+
+@predicate
+def is_notification_recipient(user: User, obj: Model) -> bool:
+    """ Predicate which checks whether the recipient of the notification a user wants to mark read is this user """
+
+    return user == obj.recipient.user
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index f005233c4808d58f747461d32288c923aaa342ba..2cdd5284e8bddf2b284f3f9aa516d8c1ee1c6004 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -33,7 +33,7 @@ from .registries import site_preferences_registry, group_preferences_registry, p
 from .tables import GroupsTable, PersonsTable
 from .util import messages
 from .util.apps import AppConfig
-from .util.core_helpers import get_announcement_by_pk, get_group_by_pk, get_person_by_pk
+from .util.core_helpers import objectgetter_optional
 
 
 @permission_required("core.view_dashboard")
@@ -97,13 +97,13 @@ def persons(request: HttpRequest) -> HttpResponse:
     return render(request, "core/persons.html", context)
 
 
-@permission_required("core.view_person", fn=get_person_by_pk)
+@permission_required("core.view_person", fn=objectgetter_optional(Person, "request.user.person", True))
 def person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """ Detail view for one person; defaulting to logged-in person """
 
     context = {}
 
-    person = get_person_by_pk(request, id_)
+    person = objectgetter_optional(Person, "request.user.person", True)(request, id_)
     context["person"] = person
 
     # Get groups where person is member of
@@ -117,13 +117,13 @@ def person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     return render(request, "core/person_full.html", context)
 
 
-@permission_required("core.view_group", fn=get_group_by_pk)
+@permission_required("core.view_group", fn=objectgetter_optional(Group, None, False))
 def group(request: HttpRequest, id_: int) -> HttpResponse:
     """ Detail view for one group """
 
     context = {}
 
-    group = get_group_by_pk(request, id_)
+    group = objectgetter_optional(Group, None, False)(request, id_)
     context["group"] = group
 
     # Get group
@@ -224,13 +224,13 @@ def groups_child_groups(request: HttpRequest) -> HttpResponse:
     return render(request, "core/groups_child_groups.html", context)
 
 
-@permission_required("core.edit_person", fn=get_person_by_pk)
+@permission_required("core.edit_person", fn=objectgetter_optional(Person, "request.user.person", True))
 def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """ Edit view for a single person, defaulting to logged-in person """
 
     context = {}
 
-    person = get_person_by_pk(request, id_)
+    person = objectgetter_optional(Person, "request.user.person", True)(request, id_)
     context["person"] = person
 
     edit_person_form = EditPersonForm(request.POST or None, request.FILES or None, instance=person)
@@ -255,13 +255,13 @@ def get_group_by_id(request: HttpRequest, id_: Optional[int] = None):
         return None
 
 
-@permission_required("core.edit_group", fn=get_group_by_pk)
+@permission_required("core.edit_group", fn=objectgetter_optional(Group, None, False))
 def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """ View to edit or create a group """
 
     context = {}
 
-    group = get_group_by_pk(request, id_)
+    group = objectgetter_optional(Group, None, False)(request, id_)
     context["group"] = group
 
     if id_:
@@ -301,18 +301,16 @@ def system_status(request: HttpRequest) -> HttpResponse:
     return render(request, "core/system_status.html", context)
 
 
+@permission_required("core.mark_notification_as_read", fn=objectgetter_optional(Notification, None, False))
 def notification_mark_read(request: HttpRequest, id_: int) -> HttpResponse:
     """ Mark a notification read """
 
     context = {}
 
-    notification = get_object_or_404(Notification, pk=id_)
+    notification = objectgetter_optional(Notification, None, False)(request, id_)
 
-    if notification.recipient.user == request.user:
-        notification.read = True
-        notification.save()
-    else:
-        raise PermissionDenied(_("You are not allowed to mark notifications from other users as read!"))
+    notification.read = True
+    notification.save()
 
     # Redirect to dashboard as this is only used from there if JavaScript is unavailable
     return redirect("index")
@@ -331,13 +329,13 @@ def announcements(request: HttpRequest) -> HttpResponse:
     return render(request, "core/announcement/list.html", context)
 
 
-@permission_required("core.create_or_edit_announcement", fn=get_announcement_by_pk)
-def announcement_form(request: HttpRequest, pk: Optional[int] = None) -> HttpResponse:
+@permission_required("core.create_or_edit_announcement", fn=objectgetter_optional(Announcement, None, False))
+def announcement_form(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """ View to create or edit an announcement """
 
     context = {}
 
-    announcement = get_announcement_by_pk(request, pk)
+    announcement = objectgetter_optional(Announcement, None, False)(request, id_)
 
     if announcement:
         # Edit form for existing announcement
@@ -363,12 +361,12 @@ def announcement_form(request: HttpRequest, pk: Optional[int] = None) -> HttpRes
     return render(request, "core/announcement/form.html", context)
 
 
-@permission_required("core.delete_announcement", fn=get_announcement_by_pk)
-def delete_announcement(request: HttpRequest, pk: int) -> HttpResponse:
+@permission_required("core.delete_announcement", fn=objectgetter_optional(Announcement, None, False))
+def delete_announcement(request: HttpRequest, id_: int) -> HttpResponse:
     """ View to delete an announcement """
 
     if request.method == "POST":
-        announcement = get_announcement_by_pk(request, pk)
+        announcement = objectgetter_optional(Announcement, None, False)(request, id_)
         announcement.delete()
         messages.success(request, _("The announcement has been deleted."))