Repository: DDMAL/CantusDB

Issue:
if person browsing the site has the url, they can view user detail pages
while investigating #479, I discovered that the detail pages for users who are not indexers are visible to not-logged-in users if you know the URL (e.g. http://206.12.88.113/user/1327). User detail pages should only be accessible to not-logged-in visitor if the user's `is-indexer` field is set to `True` (e.g. http://206.12.88.113/user/613 is (correctly) visible to everyone).

This is not enormously urgent - I don't believe there are any links to the detail pages of non-indexer users displayed anywhere on the site, so you have to know the URL to visit these pages. But there's the potential of people's information (i.e. name and institution) being divulged even if they're not officially affiliated with CantusDB. And these pages should not be visible to anonymous users anyways.


Discussion:

Resolving patch:
diff --git a/django/cantusdb_project/main_app/views/user.py b/django/cantusdb_project/main_app/views/user.py
--- a/django/cantusdb_project/main_app/views/user.py
+++ b/django/cantusdb_project/main_app/views/user.py
@@ -11,6 +11,7 @@
 from django.contrib import messages
 from extra_views import SearchableListMixin
 from django.http import HttpResponseRedirect
+from django.core.exceptions import PermissionDenied
 
 
 class UserDetailView(DetailView):
@@ -24,9 +25,16 @@ class UserDetailView(DetailView):
     template_name = "user_detail.html"
 
     def get_context_data(self, **kwargs):
-        context = super().get_context_data(**kwargs)
         user = self.get_object()
-        display_unpublished = self.request.user.is_authenticated
+        # to begin, if the person viewing the site is not logged in,
+        # they should only be able to view the detail pages of indexers,
+        # and not the detail pages of run-of-the-mill users
+        viewing_user = self.request.user
+        if not (viewing_user.is_authenticated or user.is_indexer):
+            raise PermissionDenied()
+
+        context = super().get_context_data(**kwargs)
+        display_unpublished = viewing_user.is_authenticated
         sort_by_siglum = lambda source: source.siglum
         if display_unpublished:
             context["inventoried_sources"] = sorted(


Test patch:
diff --git a/django/cantusdb_project/users/tests.py b/django/cantusdb_project/users/tests.py
--- a/django/cantusdb_project/users/tests.py
+++ b/django/cantusdb_project/users/tests.py
@@ -142,13 +142,54 @@ def setUpTestData(cls):
         get_user_model().objects.create(email="test@test.com")
 
     def test_url_and_templates(self):
-        user = get_user_model().objects.first()
+        User = get_user_model()
+        user = User.objects.create(
+            email="example@example.com",
+            is_indexer=True,
+        )
         response = self.client.get(reverse("user-detail", args=[user.id]))
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "user_detail.html")
 
     def test_context(self):
-        user = get_user_model().objects.first()
+        User = get_user_model()
+        user = User.objects.create(
+            email="example@example.com",
+            is_indexer=True,
+        )
         response = self.client.get(reverse("user-detail", args=[user.id]))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.context["user"], user)
+
+    def test_permissions(self):
+        User = get_user_model()
+        viewing_user = User.objects.create(email="viewing@example.com")
+        viewing_user.set_password("cantusdb_optimus_est!!1!")
+        viewing_user.save()
+        indexer = User.objects.create(
+            email="indexer@example.com",
+            is_indexer=True,
+        )
+        non_indexer = User.objects.create(
+            email="not_an_indexer@example.com",
+            is_indexer=False,
+        )
+
+        # logged-out
+        response_1 = self.client.get(reverse("user-detail", args=[indexer.id]))
+        self.assertEqual(response_1.status_code, 200)
+
+        response_2 = self.client.get(reverse("user-detail", args=[non_indexer.id]))
+        self.assertEqual(response_2.status_code, 403)  # 403 Forbidden
+
+        # logged-in
+        self.client = Client()
+        self.client.login(
+            email="viewing@example.com",
+            password="cantusdb_optimus_est!!1!",
+        )
+        response_3 = self.client.get(reverse("user-detail", args=[indexer.id]))
+        self.assertEqual(response_3.status_code, 200)
+
+        response_4 = self.client.get(reverse("user-detail", args=[non_indexer.id]))
+        self.assertEqual(response_4.status_code, 200)