diff --git a/SAS/TMSS/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/src/tmss/tmssapp/models/specification.py
index b3629f35cfd18d93ccf77af17911b7f9928271cd..11f95ce9340917125e8222053b55de2fbb946a38 100644
--- a/SAS/TMSS/src/tmss/tmssapp/models/specification.py
+++ b/SAS/TMSS/src/tmss/tmssapp/models/specification.py
@@ -21,7 +21,6 @@ from django.urls import reverse as revese_url
 from collections import Counter
 from django.utils.functional import cached_property
 
-
 #
 # Common
 #
@@ -843,6 +842,11 @@ class TaskDraft(NamedCommon):
             pass
         return self.relative_start_time
 
+    @cached_property
+    def project(self) -> Project:
+        '''return the related project of this task
+        '''
+        return self.scheduling_unit_draft.scheduling_set.project
 
     # JK, 28/07/20: After discussion with Sander, we probably only want the
     #  - duration on the scheduling_unit draft (based on relative start/stop times)
diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/CMakeLists.txt b/SAS/TMSS/src/tmss/tmssapp/viewsets/CMakeLists.txt
index fc0325a523508e371b2456d96b3467274dae748d..8f21d3c956f637dd42e50f3676699db3b5bd8139 100644
--- a/SAS/TMSS/src/tmss/tmssapp/viewsets/CMakeLists.txt
+++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/CMakeLists.txt
@@ -6,6 +6,7 @@ set(_py_files
     lofar_viewset.py
     specification.py
     scheduling.py
+    permissions.py
     )
 
 python_install(${_py_files}
diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/permissions.py b/SAS/TMSS/src/tmss/tmssapp/viewsets/permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..43062ba2f172f632076e7b1d3f5b315b786b5352
--- /dev/null
+++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/permissions.py
@@ -0,0 +1,101 @@
+"""
+This file contains permissions and filters that are used in the viewsets
+"""
+
+from rest_framework import permissions, filters
+from lofar.sas.tmss.tmss.tmssapp import models
+#
+# Permissions
+#
+# Note: These prevent accessing the object directly, and return a nice permission error.
+#       However, object permissions are not considered on a listing by default, so make sure to either
+#       A. apply a filter to prevent object without permission to be included in the list with full details, or
+#       B. customize get_queryset on the view to call check_object_permissions.
+
+class IsProjectMember(permissions.DjangoObjectPermissions):
+    """
+    Object-level permission to only allow users of the related project to access it.
+    Note: instance must have a project attribute.
+
+    Define a filter_project_roles attribute on the view to further restrict access to a list of specific project roles.
+    (Some user role background can be found here https://support.astron.nl/confluence/display/TMSS/User+roles).
+    """
+    def has_object_permission(self, request, view, obj):
+
+        # determine which roles are allowed to access this object
+        if hasattr(view, 'filter_project_roles'):
+            filter_project_roles = view.filter_project_roles
+        else:
+            # allow all roles by default, if nothing was specified in the view
+            filter_project_roles = ['PI', 'CO-I', 'Contact Author', 'Shared support user', 'Friend of Project', 'Friend of Project (Primary)']
+
+        # determine the projects that the users has one of the allowed roles for
+        # todo: the following user_project_roles are fake data and will be eventually defined by the OIDC federation, most likely to be read from request.user.???
+        user_project_roles = ({'project': 'high', 'project_role': 'PI'},
+                              {'project': 'low', 'project_role': 'Friend of Project'})
+
+        permitted_projects = [models.Project.objects.get(name=project_role['project']) for project_role in user_project_roles if project_role['project_role'] in filter_project_roles]
+
+        # check whether the related project of this object is one that the user has permitted to see
+        return obj.project in permitted_projects
+
+
+class IsProjectMemberOrReadOnly(IsProjectMember):
+    """
+    Object-level permission to only allow users of the related project to modify it.
+    Note: instance must have a project attribute.
+
+    Define a filter_project_roles attribute on the view to further restrict access to a list of specific project roles.
+    (Some user role background can be found here https://support.astron.nl/confluence/display/TMSS/User+roles).
+    """
+    def has_object_permission(self, request, view, obj):
+
+        # generally allow reading, otherwise check permissions:
+        if request.method in permissions.SAFE_METHODS:
+            return True
+        else:
+            return super().has_object_permission(request, view, obj)
+
+
+#
+# Custom Filters
+#
+
+class IsProjectMemberFilterBackend(filters.BaseFilterBackend):
+    """
+    Filter that only allows users to see objects that are related to their projects.
+
+    This only returns objects that belong to a project for which the requesting user has the required project role.
+    Define a filter_project_roles attribute on the view to further restrict access to a list of specific project roles.
+    (Some user role background can be found here https://support.astron.nl/confluence/display/TMSS/User+roles).
+    """
+    def filter_queryset(self, request, queryset, view):
+
+        # Note that while filtering excludes certain items from the list view as expected, it also means that no
+        # permission errors are raised when we do not restrict filtering to list view, and 'not found' is returned
+        # instead, which might be confusing. So we want to explicitly only filter listings here:
+        if view.action != 'list':
+            return queryset
+
+        # determine which roles are allowed to access this object
+        if hasattr(view, 'filter_project_roles'):
+            filter_project_roles = view.filter_project_roles
+        else:
+            # allow all roles by default, if nothing was specified in the view
+            filter_project_roles = ['PI', 'CO-I', 'Contact Author', 'Shared support user', 'Friend of Project', 'Friend of Project (Primary)']
+
+        # determine the projects that the users has one of the allowed roles for
+        # todo: the following user_project_roles are fake data and will be eventually defined by the OIDC federation, most likely to be read from request.user.???
+        user_project_roles = ({'project': 'high', 'project_role': 'PI'},
+                              {'project': 'low', 'project_role': 'Friend of Project'})
+
+        permitted_projects = [models.Project.objects.get(name=project_role['project']) for project_role in user_project_roles if project_role['project_role'] in filter_project_roles]
+
+        # Unfortunately, we cannot simply filter in SQL for model properties via queryset.filter(project__in=projects).
+        # I'm not sure how we can achieve a generic way to look up the related project in SQL, since it needs to be
+        # resolved differently for different models. For now, fetch all and filter down the full set:
+        permitted_fetched_objects = list(filter(lambda x: x.project in permitted_projects, queryset.all()))
+        # we could return the list of objects, which seems to work if you don't touch the get_queryset.
+        # But are supposed to return a queryset instead, so we make a new one, even though we fetched already.
+        # I don't know, there must be a better way...
+        return queryset.filter(id__in=[o.id for o in permitted_fetched_objects])
diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py
index 64c4e9e588e228a509c12be4f66a687b132ae096..cc3c72586cb58e66f28048b2aa19bdd9b3ff25a2 100644
--- a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py
+++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py
@@ -19,6 +19,7 @@ from drf_yasg.utils import swagger_auto_schema
 from drf_yasg.openapi import Parameter
 
 from lofar.sas.tmss.tmss.tmssapp.viewsets.lofar_viewset import LOFARViewSet, LOFARNestedViewSet, AbstractTemplateViewSet, LOFARCopyViewSet
+from lofar.sas.tmss.tmss.tmssapp.viewsets.permissions import IsProjectMemberFilterBackend, IsProjectMember, IsProjectMemberOrReadOnly
 from lofar.sas.tmss.tmss.tmssapp import models
 from lofar.sas.tmss.tmss.tmssapp import serializers
 from django.http import JsonResponse
@@ -682,6 +683,9 @@ class SchedulingUnitBlueprintNestedViewSet(LOFARNestedViewSet):
 class TaskDraftViewSet(LOFARViewSet):
     queryset = models.TaskDraft.objects.all()
     serializer_class = serializers.TaskDraftSerializer
+    permission_classes = (IsProjectMember,)
+    filter_backends = (IsProjectMemberFilterBackend,)
+    filter_project_roles = ['PI']
 
     # prefetch all reverse related references from other models on their related_name to avoid a ton of duplicate queries
     queryset = queryset.prefetch_related('first_scheduling_relation') \
@@ -699,6 +703,19 @@ class TaskDraftViewSet(LOFARViewSet):
     queryset = queryset.select_related('copies') \
                        .select_related('copy_reason')
 
+    # do not permit listing if the queryset contains objects that the user has not permission for.
+    # Note: this is not required if we apply correct filtering, but it's a nice check that we do filter correctly.
+    # Do not check on other actions, as the queryset might contain items that will be filtered later.
+    # todo: see if there is something like a get_filtered_queryset that always only includes what will be returned
+    #  to the user, so we can always check object permissions on everything.
+    def get_queryset(self):
+        qs = super().get_queryset()
+        if self.action == 'list':
+            qs = self.filter_queryset(qs)
+            for obj in qs:
+                self.check_object_permissions(self.request, obj)
+        return qs
+
     @swagger_auto_schema(responses={201: 'The created task blueprint, see Location in Response header',
                                     403: 'forbidden'},
                          operation_description="Carve this draft task specification in stone, and make an (uneditable) blueprint out of it.")