Repository: e-valuation/EvaP

Issue:
Redirection loop on some 403 errors
Steps to reproduce:
- Login as sample contributor
- Call a function you have no rights to like [http://localhost:8000/contributor/course/1499/edit](http://localhost:8000/contributor/course/1499/edit)
- You will get an redirection loop

Can someone verify it?



Discussion:
contributors/views.py course_edit is only executed if @editor_or_delegate_required succieds. That method proofs if the user is edit or delegator. Because that's not the case he is redirected to the login page with the next param set to the course edit page. The user is already authenticated so he is redirected back again to the course edit page.

fwiw, i found this in an old commit:

```
def user_passes_test_without_redirect(test_func):
    """
    Decorator for views that checks that the user passes the given test.
    The test should be a callable that takes the user object and returns
    True if the user passes.
    """

    def decorator(view_func):
        @wraps(view_func, assigned=available_attrs(view_func))
        def _wrapped_view(request, *args, **kwargs):
            if test_func(request.user):
                return view_func(request, *args, **kwargs)
            raise PermissionDenied
        return _wrapped_view
    return decorator

```

i removed it because it was unused. this is not the complete solution, it does not redirect to login URL if the user is not authenticated.

@WGierke, @lawrencebenson, do you still want to have a look at this? otherwise i'm probably the one who has to do this :)

if you are at it already, feel free to take over


Resolving patch:
diff --git a/evap/evaluation/auth.py b/evap/evaluation/auth.py
--- a/evap/evaluation/auth.py
+++ b/evap/evaluation/auth.py
@@ -1,7 +1,7 @@
-from django.core.exceptions import ImproperlyConfigured
+from django.core.exceptions import ImproperlyConfigured, PermissionDenied
 from django.contrib import auth, messages
 from django.contrib.auth.backends import ModelBackend
-from django.contrib.auth.decorators import user_passes_test
+from django.contrib.auth.views import redirect_to_login
 from django.utils.decorators import available_attrs
 from django.utils.translation import ugettext_lazy as _
 
@@ -70,87 +70,79 @@ def authenticate(self, key):
         except UserProfile.DoesNotExist:
             return None
 
-def login_required(func):
+def user_passes_test(test_func):
     """
-    Decorator for views that checks that the user is logged in
+    Decorator for views that checks whether users are authenticated
+    (redirecting to login if not) and pass a given test (raising 403
+    if not). The test should be a callable
+    that takes the user object and returns True if the user passes.
     """
-    def check_user(user):
-        return user.is_authenticated()
-    return user_passes_test(check_user)(func)
-
+    def decorator(view_func):
+        @wraps(view_func, assigned=available_attrs(view_func))
+        def _wrapped_view(request, *args, **kwargs):
+            if not request.user.is_authenticated():
+                return redirect_to_login(request.get_full_path())
+            if not test_func(request.user):
+                raise PermissionDenied()
+            else:
+                return view_func(request, *args, **kwargs)
+        return _wrapped_view
+    return decorator
 
-def staff_required(func):
+def staff_required(view_func):
     """
     Decorator for views that checks that the user is logged in and a staff member
     """
-
     def check_user(user):
-        if not user.is_authenticated():
-            return False
         return user.is_staff
-    return user_passes_test(check_user)(func)
+    return user_passes_test(check_user)(view_func)
 
 
-def contributor_or_delegate_required(func):
+def contributor_or_delegate_required(view_func):
     """
     Decorator for views that checks that the user is logged in, has edit rights
     for at least one course or is a delegate for such a person or is a
     contributor.
     """
-
     def check_user(user):
-        if not user.is_authenticated():
-            return False
         return user.is_contributor_or_delegate
-    return user_passes_test(check_user)(func)
+    return user_passes_test(check_user)(view_func)
 
 
-def editor_or_delegate_required(func):
+def editor_or_delegate_required(view_func):
     """
     Decorator for views that checks that the user is logged in, has edit rights
     for at least one course or is a delegate for such a person.
     """
-
     def check_user(user):
-        if not user.is_authenticated():
-            return False
         return user.is_editor_or_delegate
-    return user_passes_test(check_user)(func)
+    return user_passes_test(check_user)(view_func)
 
 
-def editor_required(func):
+def editor_required(view_func):
     """
     Decorator for views that checks that the user is logged in and has edit
     right for at least one course.
     """
-
     def check_user(user):
-        if not user.is_authenticated():
-            return False
         return user.is_editor
-    return user_passes_test(check_user)(func)
+    return user_passes_test(check_user)(view_func)
 
-def participant_required(func):
+def participant_required(view_func):
     """
     Decorator for views that checks that the user is logged in and
     participates in at least one course.
     """
-
     def check_user(user):
-        if not user.is_authenticated():
-            return False
         return user.is_participant
-    return user_passes_test(check_user)(func)
+    return user_passes_test(check_user)(view_func)
 
-def reward_user_required(func):
+def reward_user_required(view_func):
     """
     Decorator for views that checks that the user is logged in and can use
     reward points.
     """
-
     def check_user(user):
         from evap.rewards.tools import can_user_use_reward_points
-        if not user.is_authenticated():
-            return False
         return can_user_use_reward_points(user)
-    return user_passes_test(check_user)(func)
+    return user_passes_test(check_user)(view_func)
diff --git a/evap/results/views.py b/evap/results/views.py
--- a/evap/results/views.py
+++ b/evap/results/views.py
@@ -3,8 +3,9 @@
 from django.core.exceptions import PermissionDenied
 from django.shortcuts import get_object_or_404, render
 from django.utils.translation import get_language
+from django.contrib.auth.decorators import login_required
 
-from evap.evaluation.auth import login_required, staff_required
+from evap.evaluation.auth import staff_required
 from evap.evaluation.models import Semester
 from evap.evaluation.tools import calculate_results, calculate_average_and_medium_grades, TextResult
 
diff --git a/evap/rewards/tools.py b/evap/rewards/tools.py
--- a/evap/rewards/tools.py
+++ b/evap/rewards/tools.py
@@ -4,7 +4,7 @@
 from django.utils.translation import ugettext as _
 from django.dispatch import receiver
 
-from evap.evaluation.auth import login_required
+from django.contrib.auth.decorators import login_required
 from evap.evaluation.models import Course
 
 from evap.rewards.models import RewardPointGranting, RewardPointRedemption, RewardPointRedemptionEvent, SemesterActivation


Test patch:
diff --git a/evap/staff/tests.py b/evap/staff/tests.py
--- a/evap/staff/tests.py
+++ b/evap/staff/tests.py
@@ -974,3 +974,33 @@ def test_raise_403(self):
         self.get_assert_403("/staff/semester/4/course/7/edit", "evap")
         self.get_assert_403("/staff/semester/4/course/7/delete", "evap")
         self.get_assert_403("/staff/semester/4/course/7/unpublish", "evap")
+
+
+class RedirectionTest(WebTest):
+    fixtures = ['minimal_test_data']
+    csrf_checks = False
+    extra_environ = {'HTTP_ACCEPT_LANGUAGE': 'en'}
+
+    def get_assert_403(self, url, user):
+        try:
+            self.app.get(url, user=user, status=403)
+        except AppError as e:
+            self.fail('url "{}" failed with user "{}"'.format(url, user))
+
+    def test_not_authenticated(self):
+        url = "/contributor/course/3/edit"
+        response = self.app.get(url)
+        self.assertRedirects(response, "/?next=/contributor/course/3/edit")
+
+    def test_wrong_usergroup(self):
+        url = "/contributor/course/3/edit"
+        self.get_assert_403(url, "student")
+
+    def test_wrong_state(self):
+        url = "/contributor/course/3/edit"
+        self.get_assert_403(url, "responsible")
+
+    def test_ok(self):
+        url = "/contributor/course/2/edit"
+        response = self.app.get(url, user="responsible")
+        self.assertEqual(response.status_code, 200)