Fix #17 - Implemented stateless, oauth2 like authentication.

remotes/origin/enhancement/email-actions
Andrey Antukh 2013-10-23 12:01:24 +02:00
parent 274c8fe647
commit de95fb2a91
9 changed files with 159 additions and 57 deletions

View File

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
import base64
import re
from django.core import signing
from django.db.models import get_model
from rest_framework.authentication import BaseAuthentication
import greenmine.base.exceptions as exc
class Session(BaseAuthentication):
"""
Same as rest_framework.authentication.SessionAuthentication
but without csrf.
"""
def authenticate(self, request):
"""
Returns a `User` if the request session currently has a logged in user.
Otherwise returns `None`.
"""
http_request = request._request
user = getattr(http_request, 'user', None)
if not user or not user.is_active:
return None
return (user, None)
def get_token_for_user(user):
data = {"user_id": user.id}
return signing.dumps(data)
def get_user_for_token(token):
data = signing.loads(token)
model_cls = get_model("users", "User")
try:
user = model_cls.objects.get(pk=data["user_id"])
except model_cls.DoesNotExist:
raise exc.BadRequest("Invalid token")
else:
return user
class Token(BaseAuthentication):
"""
Stateless authentication system partially based on oauth.
"""
auth_rx = re.compile(r"^Bearer (.+)$")
def authenticate(self, request):
if "HTTP_AUTHORIZATION" not in request.META:
return None
token_rx_match = self.auth_rx.search(request.META["HTTP_AUTHORIZATION"])
if not token_rx_match:
return None
token = token_rx_match.group(1)
user = get_user_for_token(token)
return (user, token)
def authenticate_header(self, request):
return 'Bearer realm="api"'

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from django.db.models.loading import get_model
from django.contrib.auth import logout, login, authenticate
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework import status, viewsets
from greenmine.base import exceptions as exc
from greenmine.base import auth
from greenmine.base.users.models import User, Role
from greenmine.base.users.serializers import UserSerializer
class AuthViewSet(viewsets.ViewSet):
permission_classes = (AllowAny,)
def create(self, request, **kwargs):
username = request.DATA.get('username', None)
password = request.DATA.get('password', None)
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise exc.BadRequest("Invalid username or password")
if not user.check_password(password):
raise exc.BadRequest("Invalid username or password")
serializer = UserSerializer(user)
response_data = serializer.data
response_data["auth_token"] = auth.get_token_for_user(user)
return Response(response_data, status=status.HTTP_200_OK)

View File

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from django.core.urlresolvers import reverse
from django.conf.urls import patterns, include, url
from django import test
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from greenmine import urls
from greenmine.base import auth
from greenmine.base.users.tests import create_user
class TestAuthView(APIView):
authentication_classes = (auth.Token,)
permission_classes = (IsAuthenticated,)
def get(self, request, *args, **kwargs):
return Response("ok")
urls.urlpatterns += patterns("",
url(r'^test-api/v1/auth/', TestAuthView.as_view(), name="test-token-auth"),
)
class SimpleTokenAuthTests(test.TestCase):
def setUp(self):
self.user1 = create_user(1)
def test_token_auth_01(self):
response = self.client.get(reverse("test-token-auth"))
self.assertEqual(response.status_code, 401)
def test_token_auth_02(self):
token = auth.get_token_for_user(self.user1)
response = self.client.get(reverse("test-token-auth"),
HTTP_AUTHORIZATION="Bearer {}".format(token))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b'"ok"')

View File

@ -24,7 +24,7 @@ COORS_ALLOWED_METHODS = getattr(settings, 'COORS_ALLOWED_METHODS',
['POST', 'GET', 'OPTIONS', 'PUT', 'DELETE', 'PATCH']) ['POST', 'GET', 'OPTIONS', 'PUT', 'DELETE', 'PATCH'])
COORS_ALLOWED_HEADERS = getattr(settings, 'COORS_ALLOWED_HEADERS', COORS_ALLOWED_HEADERS = getattr(settings, 'COORS_ALLOWED_HEADERS',
['Content-Type', 'X-Requested-With', ['Content-Type', 'X-Requested-With',
'X-Session-Token', 'Accept-Encoding', 'Authorization', 'Accept-Encoding',
'X-Disable-Pagination']) 'X-Disable-Pagination'])
COORS_ALLOWED_CREDENTIALS = getattr(settings, 'COORS_ALLOWED_CREDENTIALS', True) COORS_ALLOWED_CREDENTIALS = getattr(settings, 'COORS_ALLOWED_CREDENTIALS', True)
@ -43,7 +43,6 @@ class CoorsMiddleware(object):
response = http.HttpResponse() response = http.HttpResponse()
self._populate_response(response) self._populate_response(response)
return response return response
return None return None
def process_response(self, request, response): def process_response(self, request, response):

View File

@ -128,31 +128,3 @@ class UsersViewSet(ModelCrudViewSet):
request.user.set_password(password) request.user.set_password(password)
request.user.save(update_fields=["password"]) request.user.save(update_fields=["password"])
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class AuthViewSet(viewsets.ViewSet):
permission_classes = (AllowAny,)
def create(self, request, **kwargs):
username = request.DATA.get('username', None)
password = request.DATA.get('password', None)
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise exc.BadRequest("Invalid username or password")
if not user.check_password(password):
raise exc.BadRequest("Invalid username or password")
user = authenticate(username=username, password=password)
login(request, user)
serializer = UserSerializer(user)
response_data = serializer.data
response_data["auth_token"] = request.session.session_key
return Response(response_data)
def destroy(self, request, pk=None):
logout(request)
return Response({})

View File

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
from rest_framework.authentication import BaseAuthentication
class SessionAuthentication(BaseAuthentication):
"""
Same as rest_framework.authentication.SessionAuthentication
but without csrf.
"""
def authenticate(self, request):
"""
Returns a `User` if the request session currently has a logged in user.
Otherwise returns `None`.
"""
http_request = request._request
user = getattr(http_request, 'user', None)
if not user or not user.is_active:
return None
return (user, None)

View File

@ -1,7 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from greenmine.base import routers from greenmine.base import routers
from greenmine.base.users.api import AuthViewSet, RolesViewSet, UsersViewSet from greenmine.base.auth.api import AuthViewSet
from greenmine.base.users.api import RolesViewSet, UsersViewSet
from greenmine.base.searches.api import SearchViewSet from greenmine.base.searches.api import SearchViewSet
from greenmine.projects.api import ProjectViewSet, MembershipViewSet from greenmine.projects.api import ProjectViewSet, MembershipViewSet
from greenmine.projects.milestones.api import MilestoneViewSet from greenmine.projects.milestones.api import MilestoneViewSet

View File

@ -139,12 +139,17 @@ TEMPLATE_LOADERS = [
] ]
MIDDLEWARE_CLASSES = [ MIDDLEWARE_CLASSES = [
# Common middlewares
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
'greenmine.base.middleware.GreenmineSessionMiddleware',
'greenmine.base.middleware.CoorsMiddleware', 'greenmine.base.middleware.CoorsMiddleware',
# Only needed by django admin
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
# 'greenmine.base.middleware.GreenmineSessionMiddleware',
'reversion.middleware.RevisionMiddleware', 'reversion.middleware.RevisionMiddleware',
] ]
@ -270,7 +275,8 @@ MAX_SEARCH_RESULTS = 100
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (
'greenmine.base.users.auth.SessionAuthentication', 'greenmine.base.auth.Token',
'greenmine.base.auth.Session',
), ),
'FILTER_BACKEND': 'greenmine.base.filters.FilterBackend', 'FILTER_BACKEND': 'greenmine.base.filters.FilterBackend',
'PAGINATE_BY': 50, 'PAGINATE_BY': 50,