Просмотр исходного кода

WIP django auth rewamp for Ember.js app

Rafał Pitoń 10 лет назад
Родитель
Сommit
2a179c789e

+ 1 - 1
misago/conf/defaults.py

@@ -250,7 +250,7 @@ MISAGO_STOP_FORUM_SPAM_MIN_CONFIDENCE = 80
 MISAGO_MAILER_BATCH_SIZE = 20
 
 # Auth paths
-MISAGO_AUTH_API_URL = 'misago:api:authenticate'
+MISAGO_AUTH_API_URL = 'misago:api:login'
 
 LOGIN_REDIRECT_URL = 'misago:index'
 LOGIN_URL = 'misago:login'

+ 14 - 2
misago/users/api/auth.py

@@ -1,4 +1,5 @@
 from django.conf import settings
+from django.contrib import auth
 from django.utils.translation import ugettext as _
 from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
@@ -11,6 +12,7 @@ from rest_framework.response import Response
 from misago.users.decorators import (deny_authenticated, deny_guests,
                                      deny_banned_ips)
 from misago.users.forms.auth import AuthenticationForm
+from misago.users.serializers import AuthenticatedUserSerializer
 
 
 @sensitive_post_parameters()
@@ -19,13 +21,23 @@ from misago.users.forms.auth import AuthenticationForm
 @deny_authenticated
 @csrf_protect
 @deny_banned_ips
-def authenticate(request):
+def login(request):
     form = AuthenticationForm(request, data=request.data)
     if form.is_valid():
-        return Response()
+        auth.login(request, form.user_cache)
+        return Response(AuthenticatedUserSerializer(form.user_cache).data)
     else:
         error = form.errors.as_data()['__all__'][0]
         return Response({
             'detail': error.messages[0],
             'code': error.code
         }, status=status.HTTP_400_BAD_REQUEST)
+
+
+@api_view(['GET', 'POST'])
+def user(request):
+    if request.user.is_authenticated():
+        return Response(AuthenticatedUserSerializer(request.user).data)
+    else:
+        return Response({'id': None})
+

+ 1 - 0
misago/users/serializers/__init__.py

@@ -0,0 +1 @@
+from misago.users.serializers.user import *

+ 9 - 0
misago/users/serializers/user.py

@@ -0,0 +1,9 @@
+from django.contrib.auth import get_user_model
+
+from rest_framework import serializers
+
+
+class AuthenticatedUserSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = get_user_model()
+        fields = ('id', 'username', 'slug')

+ 51 - 0
misago/users/tests/test_auth_api.py

@@ -0,0 +1,51 @@
+import json
+
+from django.contrib.auth import get_user_model
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+
+
+class AuthenticateAPITests(TestCase):
+    def test_api_invalid_credentials(self):
+        """login api returns 400 on invalid POST"""
+        response = self.client.post(
+            reverse('misago:api:login'),
+            data={'username': 'nope', 'password': 'nope'})
+
+        self.assertEqual(response.status_code, 400)
+        self.assertIn("Login or password is incorrect.", response.content)
+
+        response = self.client.get(reverse('misago:api:auth_user'))
+        self.assertEqual(response.status_code, 200)
+
+        user_json = json.loads(response.content)
+        self.assertIsNone(user_json['id'])
+
+    def test_api_signin(self):
+        """api signs user in"""
+        User = get_user_model()
+        user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
+
+        response = self.client.post(
+            reverse('misago:api:login'),
+            data={'username': 'Bob', 'password': 'Pass.123'})
+
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.get(reverse('misago:index'))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.get(reverse('misago:api:auth_user'))
+        self.assertEqual(response.status_code, 200)
+
+        user_json = json.loads(response.content)
+        self.assertEqual(user_json['id'], user.id)
+        self.assertEqual(user_json['username'], user.username)
+
+    def test_api_signin_banned(self):
+        """login api fails to sign banned user in"""
+        raise NotImplemented("TODO: test_api_signin_banned")
+
+    def test_api_signin_inactive(self):
+        """login api fails to sign inactive user in"""
+        raise NotImplemented("TODO: test_api_signin_inactive")

+ 64 - 49
misago/users/tests/test_auth_views.py

@@ -1,72 +1,87 @@
+import json
+
 from django.contrib.auth import get_user_model
 from django.core.urlresolvers import reverse
 from django.test import TestCase
 
-from misago.users.models import Ban, BAN_USERNAME
-
 
-class LoginViewTests(TestCase):
-    def test_view_get_returns_200(self):
-        """login view returns 200 on GET"""
+class AuthViewsTests(TestCase):
+    def test_auth_views_return_302(self):
+        """auth views should always return redirect"""
         response = self.client.get(reverse('misago:login'))
-        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.status_code, 302)
 
-    def test_view_invalid_credentials(self):
-        """login view returns 200 on invalid POST"""
-        response = self.client.post(
-            reverse('misago:login'),
-            data={'username': 'nope', 'password': 'nope'})
+        response = self.client.post(reverse('misago:login'))
+        self.assertEqual(response.status_code, 302)
 
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Your login or password is incorrect.", response.content)
+        response = self.client.get(reverse('misago:logout'))
+        self.assertEqual(response.status_code, 302)
 
-    def test_view_signin(self):
-        """login view signs user in"""
-        User = get_user_model()
-        User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
+        response = self.client.post(reverse('misago:logout'))
+        self.assertEqual(response.status_code, 302)
 
-        response = self.client.post(
-            reverse('misago:login'),
-            data={'username': 'Bob', 'password': 'Pass.123'})
+    def test_login_view_redirect_to(self):
+        """login view respects redirect_to POST"""
+        # valid redirect
+        response = self.client.post(reverse('misago:login'), data={
+            'redirect_to': '/redirect/'
+        })
 
         self.assertEqual(response.status_code, 302)
+        self.assertEqual(response['location'], 'http://testserver/redirect/')
 
-        response = self.client.get(reverse('misago:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Bob', response.content)
-
-    def test_view_signin_banned(self):
-        """login view fails to sign banned user in"""
-        User = get_user_model()
-        User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
-        Ban.objects.create(check_type=BAN_USERNAME,
-                           banned_value='bob',
-                           user_message='Nope!')
+        # invalid redirect (redirects to other site)
+        response = self.client.post(reverse('misago:login'), data={
+            'redirect_to': 'http://somewhereelse.com/page.html'
+        })
 
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(response['location'], 'http://testserver/')
+
+        # in-dev ember-cli redirect
+        conf_overrides = {
+            'DEBUG': True,
+            'MISAGO_EMBER_CLI_ORIGIN': 'http://localhost:4200'
+        }
+
+        with self.settings(**conf_overrides):
+            # valid request, has Origin header
+            response = self.client.post(reverse('misago:login'), data={
+                'redirect_to': 'http://localhost:4200/page.html'
+            }, HTTP_ORIGIN='http://localhost:4200')
+
+            self.assertEqual(response.status_code, 302)
+            self.assertEqual(response['location'],
+                             'http://localhost:4200/page.html')
+
+            # invalid request, different Origin header
+            response = self.client.post(reverse('misago:login'), data={
+                'redirect_to': 'http://localhost:4200/page.html'
+            }, HTTP_ORIGIN='http://somewhere.com/')
+
+            self.assertEqual(response.status_code, 302)
+            self.assertEqual(response['location'], 'http://testserver/')
+
+    def test_logout_view(self):
+        """logout view logs user out on post"""
         response = self.client.post(
-            reverse('misago:login'),
-            data={'username': 'Bob', 'password': 'Pass.123'})
+            reverse('misago:api:login'),
+            data={'username': 'nope', 'password': 'nope'})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Nope!', response.content)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn("Login or password is incorrect.", response.content)
 
-        response = self.client.get(reverse('misago:index'))
+        response = self.client.get(reverse('misago:api:auth_user'))
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn('Bob', response.content)
 
-    def test_view_signin_inactive(self):
-        """login view fails to sign inactive user in"""
-        User = get_user_model()
-        User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
-                                 requires_activation=1)
+        user_json = json.loads(response.content)
+        self.assertIsNone(user_json['id'])
 
-        response = self.client.post(
-            reverse('misago:login'),
-            data={'username': 'Bob', 'password': 'Pass.123'})
+        response = self.client.post(reverse('misago:logout'))
+        self.assertEqual(response.status_code, 302)
 
+        response = self.client.get(reverse('misago:api:auth_user'))
         self.assertEqual(response.status_code, 200)
-        self.assertIn('activate', response.content)
 
-        response = self.client.get(reverse('misago:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn('Bob', response.content)
+        user_json = json.loads(response.content)
+        self.assertIsNone(user_json['id'])

+ 2 - 1
misago/users/urls/api.py

@@ -2,5 +2,6 @@ from django.conf.urls import patterns, url
 
 
 urlpatterns = patterns('misago.users.api.auth',
-    url(r'^auth/$', 'authenticate', name='authenticate'),
+    url(r'^auth/login/$', 'login', name='login'),
+    url(r'^auth/$', 'user', name='auth_user'),
 )

+ 23 - 21
misago/users/views/auth.py

@@ -1,39 +1,41 @@
 from django.conf import settings
-from django.contrib import auth, messages
-from django.shortcuts import render, redirect
-from django.utils.translation import ugettext as _
+from django.contrib import auth
+from django.shortcuts import redirect
+from django.utils.http import is_safe_url
+from django.utils.six.moves.urllib.parse import urlparse
+
 from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
 from django.views.decorators.debug import sensitive_post_parameters
 
-from misago.core.decorators import require_POST
-
-from misago.users.decorators import (deny_authenticated, deny_guests,
-                                     deny_banned_ips)
-from misago.users.forms.auth import AuthenticationForm
+from misago.core.embercli import is_ember_cli_request
 
 
 @sensitive_post_parameters()
 @never_cache
-@deny_authenticated
 @csrf_protect
-@deny_banned_ips
 def login(request):
-    form = AuthenticationForm(request)
-
     if request.method == 'POST':
-        form = AuthenticationForm(request, data=request.POST)
-        if form.is_valid():
-            auth.login(request, form.user_cache)
-            return redirect(settings.LOGIN_REDIRECT_URL)
-
-    return render(request, 'misago/login.html', {'form': form})
+        redirect_to = request.POST.get('redirect_to')
+        if redirect_to:
+            is_redirect_safe = is_safe_url(
+                url=redirect_to, host=request.get_host())
+
+            if not is_redirect_safe and is_ember_cli_request(request):
+                parsed_url = urlparse(settings.MISAGO_EMBER_CLI_ORIGIN)
+                trusted_host = '%s:%s' % (parsed_url.hostname, parsed_url.port)
+                is_redirect_safe = is_safe_url(
+                    url=redirect_to, host=trusted_host)
+
+            if is_redirect_safe:
+                redirect_to_path = urlparse(redirect_to).path
+                return redirect(redirect_to_path)
+    return redirect(settings.LOGIN_REDIRECT_URL)
 
 
 @never_cache
-@deny_guests
-@require_POST
 @csrf_protect
 def logout(request):
-    auth.logout(request)
+    if request.method == 'POST' and request.user.is_authenticated():
+        auth.logout(request)
     return redirect(settings.LOGIN_REDIRECT_URL)