Repository: e-valuation/EvaP

Issue:
Navbar CSRF caching broken for entering staff mode
Steps to reproduce (on master, after reloading test data):

1. Log in as evap
2. Enter and leave staff mode
3. Log out and log in again
4. Try to enter staff mode

This will throw a 403 CSRF something error and people in Discord concluded that it is because the navbar is cached across logins. Clearing this cache on logout was proposed. This is reset when reloading test data again,


Discussion:
We're currently considering dropping the navbar cache alltogether, since it caused quite some bugs over the time and we need a lot of cache invalidation all around the codebase.  Thus, it would be nice to know how much performance we actually currently gain from the cache (originally added in https://github.com/e-valuation/EvaP/pull/598). We suspect that one of the main impacts might be that for n permission checks, we currently do n queries (check out the properties `is_manager`, `is_reviewer` and `is_grade_publisher` for examples). We should be able to prevent this by prefetching the groups of a user and using the prefetched values. Or, we could overwrite the cached_properties (although I consider this more likely to cause bugs in the future, as it is hidden behaviour).
prefetching wouldn't work directly as filter queries are used, but using the newly added is_prefetched would work (so prefetching all the user's groups, and then iterating through the list if it's prefetched instead of doing the filter query)
Seems like we had the session ID in the cache key at some point but removed it: https://github.com/e-valuation/EvaP/issues/1208 
I’ll also add that a Csrf error is thrown when I’m logged in a different browser:

1. Log in as evap (in browser A)
2. Enter staff mode
3. Log in as evap (in browser B)
4. Try to leave staff mode

Due to the similarity with the issue, I guess that this error-case is also covered by the defect

Resolving patch:
diff --git a/evap/evaluation/auth.py b/evap/evaluation/auth.py
--- a/evap/evaluation/auth.py
+++ b/evap/evaluation/auth.py
@@ -7,6 +7,7 @@
 from evap.evaluation.models import UserProfile
 from evap.evaluation.tools import clean_email
 from evap.rewards.tools import can_reward_points_be_used_by
+from evap.staff.tools import delete_navbar_cache_for_users
 
 
 class RequestAuthUserBackend(ModelBackend):
@@ -24,7 +25,9 @@ def authenticate(self, request, key):  # pylint: disable=arguments-differ
             return None
 
         try:
-            return UserProfile.objects.get(login_key=key)
+            user = UserProfile.objects.get(login_key=key)
+            after_login_function(request, user, None)
+            return user
         except UserProfile.DoesNotExist:
             return None
 
@@ -38,10 +41,15 @@ def authenticate(self, request, email=None, password=None):  # pylint: disable=a
             return None
         else:
             if user.check_password(password):
+                after_login_function(request, user, None)
                 return user
         return None
 
 
+def after_login_function(request, user, _client):
+    delete_navbar_cache_for_users([user])
+
+
 def user_passes_test(test_func):
     """
     Decorator for views that checks whether a user passes a given test
diff --git a/evap/settings.py b/evap/settings.py
--- a/evap/settings.py
+++ b/evap/settings.py
@@ -367,6 +367,7 @@
 ### OpenID Login
 # replace 'example.com', OIDC_RP_CLIENT_ID and OIDC_RP_CLIENT_SECRET with real values in localsettings when activating
 ACTIVATE_OPEN_ID_LOGIN = False
+OIDC_AFTER_USERLOGIN_HOOK = 'evap.evaluation.auth.after_login_function'
 OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS = 60 * 60 * 24 * 7  # one week
 OIDC_RP_SIGN_ALGO = 'RS256'
 OIDC_USERNAME_ALGO = ''


Test patch:
diff --git a/evap/evaluation/tests/test_auth.py b/evap/evaluation/tests/test_auth.py
--- a/evap/evaluation/tests/test_auth.py
+++ b/evap/evaluation/tests/test_auth.py
@@ -1,10 +1,11 @@
 from unittest.mock import patch
 import urllib
 
-from django.urls import reverse
-from django.core import mail
 from django.conf import settings
+from django.contrib.auth.models import Group
+from django.core import mail
 from django.test import override_settings
+from django.urls import reverse
 
 from model_bakery import baker
 
@@ -135,3 +136,49 @@ def test_oidc_login(self):
 
         # user should see the Logout button then.
         self.assertIn('Logout', page.body.decode())
+
+
+class LoginTestsWithCSRF(WebTest):
+    @classmethod
+    def setUpTestData(cls):
+        cls.staff_user = baker.make(
+            UserProfile,
+            email='staff@institution.example.com',
+            groups=[Group.objects.get(name='Manager')]
+        )
+        cls.staff_user_password = 'staff'
+        cls.staff_user.set_password(cls.staff_user_password)
+        cls.staff_user.save()
+
+    def test_entering_staff_mode_after_logout_and_login(self):
+        """
+            Asserts that managers can enter the staff mode after logging out and logging in again.
+            Regression test for #1530.
+        """
+        page = self.app.get(reverse('evaluation:index'))
+        form = page.forms['email-login-form']
+        form['email'] = self.staff_user.email
+        form['password'] = self.staff_user_password
+        form.submit()
+
+        # staff user should now be logged in and see the logout button
+        page = self.app.get(reverse('results:index'))
+        self.assertContains(page, 'Logout')
+
+        # log out user
+        page = self.app.get(reverse('django-auth-logout')).follow()
+        self.assertNotContains(page, 'Logout')
+
+        # log user in again
+        page = self.app.get(reverse('evaluation:index'))
+        form = page.forms['email-login-form']
+        form['email'] = self.staff_user.email
+        form['password'] = self.staff_user_password
+        form.submit()
+
+        # enter staff mode
+        page = self.app.get(reverse('results:index'))
+        page.forms['enter-staff-mode-form'].submit()
+        page = self.app.get(reverse('results:index'))
+        self.assertTrue('staff_mode_start_time' in self.app.session)
+        self.assertContains(page, 'Exit Staff Mode')