From 45c116a4e72bbb20da84b0c5ab6b60dce980817c Mon Sep 17 00:00:00 2001 From: Josue Kouka Date: Fri, 19 Jan 2018 17:20:59 +0100 Subject: [PATCH 1/2] use drf for oauth2 APIs (#16842) --- fargo/oauth2/rest_authentication.py | 91 +++++++++++++++++++ fargo/oauth2/utils.py | 44 +++++++++ fargo/oauth2/views.py | 174 +++++++++++++----------------------- tests/test_oauth2.py | 2 +- 4 files changed, 196 insertions(+), 115 deletions(-) create mode 100644 fargo/oauth2/rest_authentication.py create mode 100644 fargo/oauth2/utils.py diff --git a/fargo/oauth2/rest_authentication.py b/fargo/oauth2/rest_authentication.py new file mode 100644 index 0000000..e732eb4 --- /dev/null +++ b/fargo/oauth2/rest_authentication.py @@ -0,0 +1,91 @@ +# fargo - document box +# Copyright (C) 2016-2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import base64 + +from django.utils.translation import ugettext_lazy as _ + +from rest_framework.authentication import BaseAuthentication +from rest_framework.exceptions import AuthenticationFailed + +from .models import OAuth2Client + + +class OAuth2User(object): + """ Fake user class to return in case OAuth2 Client authentication + """ + + def __init__(self, oauth2_client): + self.oauth2_client = oauth2_client + self.authenticated = False + + def has_perm(self, *args, **kwargs): + return True + + def has_perm_any(self, *args, **kwargs): + return True + + def has_ou_perm(self, *args, **kwargs): + return True + + def filter_by_perm(self, perms, queryset): + return queryset + + def is_authenticated(self): + return self.authenticated + + def is_staff(self): + return False + + +class FargoOAUTH2Authentication(BaseAuthentication): + + def authenticate_header(self, request): + return 'basic HTTP authentication failed' + + def authenticate(self, request): + '''Authenticate client on the token endpoint''' + + if 'HTTP_AUTHORIZATION' in request.META: + authorization = request.META['HTTP_AUTHORIZATION'].split() + if authorization[0] != 'Basic' or len(authorization) != 2: + return False + try: + decoded = base64.b64decode(authorization[1]) + except TypeError: + return False + parts = decoded.split(':') + if len(parts) != 2: + return False + client_id, client_secret = parts + elif 'client_id' in request.POST: + client_id = request.POST['client_id'] + client_secret = request.POST.get('client_secret', '') + else: + raise AuthenticationFailed(self.authenticate_header(request)) + + return self.authenticate_credentials(client_id, client_secret) + + def authenticate_credentials(self, client_id, client_secret): + try: + client = OAuth2Client.objects.get( + client_id=client_id, client_secret=client_secret) + except OAuth2Client.DoesNotExist: + raise AuthenticationFailed(_('Invalid client_id/client_secret.')) + + user = OAuth2User(client) + user.authenticated = True + return user, True diff --git a/fargo/oauth2/utils.py b/fargo/oauth2/utils.py new file mode 100644 index 0000000..19da3f7 --- /dev/null +++ b/fargo/oauth2/utils.py @@ -0,0 +1,44 @@ +import cgi +from urllib import unquote + +from .models import OAuth2Authorize + + +def authenticate_bearer(request): + authorization = request.META.get('HTTP_AUTHORIZATION') + if not authorization: + return False + splitted = authorization.split() + if len(splitted) < 2: + return False + if splitted[0] != 'Bearer': + return False + token = splitted[1] + try: + return OAuth2Authorize.objects.get(access_token=token) + except OAuth2Authorize.DoesNotExist: + return False + + +def get_content_disposition_value(request): + if 'HTTP_CONTENT_DISPOSITION' not in request.META: + return None, 'missing content-disposition header' + content_header = request.META['HTTP_CONTENT_DISPOSITION'] + disposition_type, filename = cgi.parse_header(content_header) + if disposition_type != 'attachement': + return None, 'wrong disposition type: attachement excpected' + if 'filename*' in filename: + encode, country, name = filename['filename*'].split("'") + + # check accepted charset from rfc 5987 + if encode == 'UTF-8': + return unquote(name.decode('utf8')), None + elif encode == 'ISO-8859-1': + return unquote(name.decode('iso-8859-1')), None + else: + return None, 'unknown encoding: UTF-8 or ISO-8859-1 allowed' + elif 'filename' in filename: + return filename['filename'], None + else: + # no filename in header + return None, 'missing filename(*) parameter in header' diff --git a/fargo/oauth2/views.py b/fargo/oauth2/views.py index 0bb7d24..0f76f6d 100644 --- a/fargo/oauth2/views.py +++ b/fargo/oauth2/views.py @@ -14,28 +14,48 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import cgi -import base64 import urllib -from urllib import quote, unquote +from urllib import quote from django.core.files.base import ContentFile from django.core.urlresolvers import reverse from django.http import (HttpResponse, HttpResponseBadRequest, - HttpResponseRedirect, JsonResponse) + HttpResponseRedirect) from django.views.decorators.csrf import csrf_exempt from django.views.generic import FormView, TemplateView +from rest_framework import serializers +from rest_framework.response import Response +from rest_framework.views import APIView + from .forms import OAuth2AuthorizeForm from .models import OAuth2Authorize, OAuth2Client, OAuth2TempFile +from .utils import authenticate_bearer, get_content_disposition_value from fargo.fargo.models import UserDocument, Document +from fargo.oauth2.rest_authentication import FargoOAUTH2Authentication class OAuth2Exception(Exception): pass +class OAuth2ClientSerializer(serializers.ModelSerializer): + + class Meta: + model = OAuth2Client + fields = ('client_id', 'client_secret') + + +class OAUTH2ClientApiViewMixin(APIView): + http_method_names = ['post'] + authentication_classes = (FargoOAUTH2Authentication,) + + @csrf_exempt + def dispatch(self, request, *args, **kwargs): + return super(OAUTH2ClientApiViewMixin, self).dispatch(request, *args, **kwargs) + + class OAuth2AuthorizeView(FormView): template_name = 'oauth2/authorize.html' form_class = OAuth2AuthorizeForm @@ -99,29 +119,25 @@ class OAuth2AuthorizeView(FormView): return super(OAuth2AuthorizeView, self).form_valid(form) -@csrf_exempt -def get_document_token(request): - grant_type = request.POST.get('grant_type') - redirect_uri = request.POST.get('redirect_uri') - code = request.POST.get('code') - client = authenticate_client(request) +class GetDocumentTokenView(OAUTH2ClientApiViewMixin): + + def post(self, request, *args, **kwargs): + client = request.user.oauth2_client + data = request.data + if not client.check_redirect_uri(data['redirect_uri']): + return Response({'error': 'invalid_request'}, status=400) + if data['grant_type'] != 'authorization_code': + return Response({'error': 'unsopported_grant_type'}, status=400) + + try: + token = OAuth2Authorize.objects.get(code=data['code']).access_token + except OAuth2Authorize.DoesNotExist: + return Response({'error': 'invalid_request'}, status=400) - try: - if not client: - raise OAuth2Exception('invalid client') - if redirect_uri not in client.get_redirect_uris(): - raise OAuth2Exception('invalid_request') - if grant_type != 'authorization_code': - raise OAuth2Exception('unsupported_grant_type') - except OAuth2Exception as e: - return JsonResponse({'error': e.message}, status=400) + return Response({'access_token': token, 'expires': '3600'}) - try: - doc_token = OAuth2Authorize.objects.get(code=code).access_token - except OAuth2Authorize.DoesNotExist: - return JsonResponse({'error': 'invalid_request'}, status=400) - return JsonResponse({'access_token': doc_token, 'expires': '3600'}) +get_document_token = GetDocumentTokenView.as_view() def get_document(request): @@ -140,96 +156,26 @@ def get_document(request): return response -def authenticate_bearer(request): - authorization = request.META.get('HTTP_AUTHORIZATION') - if not authorization: - return False - splitted = authorization.split() - if len(splitted) < 2: - return False - if splitted[0] != 'Bearer': - return False - token = splitted[1] - try: - return OAuth2Authorize.objects.get(access_token=token) - except OAuth2Authorize.DoesNotExist: - return False - - -def authenticate_client(request, client=False): - '''Authenticate client on the token endpoint''' - - if 'HTTP_AUTHORIZATION' in request.META: - authorization = request.META['HTTP_AUTHORIZATION'].split() - if authorization[0] != 'Basic' or len(authorization) != 2: - return False - try: - decoded = base64.b64decode(authorization[1]) - except TypeError: - return False - parts = decoded.split(':') - if len(parts) != 2: - return False - client_id, client_secret = parts - elif 'client_id' in request.POST: - client_id = request.POST['client_id'] - client_secret = request.POST.get('client_secret', '') - else: - return False - if not client: - try: - client = OAuth2Client.objects.get(client_id=client_id) - except OAuth2Client.DoesNotExist: - return False - if client.client_secret != client_secret: - return False - return client - - -def get_content_disposition_value(request): - if 'HTTP_CONTENT_DISPOSITION' not in request.META: - return None, 'missing content-disposition header' - content_header = request.META['HTTP_CONTENT_DISPOSITION'] - disposition_type, filename = cgi.parse_header(content_header) - if disposition_type != 'attachement': - return None, 'wrong disposition type: attachement excpected' - if 'filename*' in filename: - encode, country, name = filename['filename*'].split("'") - - # check accepted charset from rfc 5987 - if encode == 'UTF-8': - return unquote(name.decode('utf8')), None - elif encode == 'ISO-8859-1': - return unquote(name.decode('iso-8859-1')), None - else: - return None, 'unknown encoding: UTF-8 or ISO-8859-1 allowed' - elif 'filename' in filename: - return filename['filename'], None - else: - # no filename in header - return None, 'missing filename(*) parameter in header' - - -@csrf_exempt -def put_document(request): - client = authenticate_client(request) - if not client: - return HttpResponseBadRequest('basic HTTP authentication failed') - - filename, error = get_content_disposition_value(request) - if error: - return HttpResponseBadRequest(error) - - f = ContentFile(request.body, name=filename) - document = Document.objects.get_by_file(f) - oauth2_document = OAuth2TempFile.objects.create(document=document, - filename=filename) - uri = reverse('oauth2-put-document-authorize', - args=[oauth2_document.pk]) + '/' - - response = HttpResponse() - response['Location'] = uri - return response +class PutDocumentAPIView(OAUTH2ClientApiViewMixin): + + def post(self, request, *args, **kwargs): + filename, error = get_content_disposition_value(request) + if error: + return HttpResponseBadRequest(error) + + f = ContentFile(request.body, name=filename) + document = Document.objects.get_by_file(f) + oauth2_document = OAuth2TempFile.objects.create(document=document, + filename=filename) + uri = reverse('oauth2-put-document-authorize', + args=[oauth2_document.pk]) + '/' + + response = Response() + response['Location'] = uri + return response + + +put_document = PutDocumentAPIView.as_view() class OAuth2AuthorizePutView(TemplateView): diff --git a/tests/test_oauth2.py b/tests/test_oauth2.py index 0705218..3925a90 100644 --- a/tests/test_oauth2.py +++ b/tests/test_oauth2.py @@ -109,7 +109,7 @@ def test_put_document(app, john_doe, oauth2_client): data = f.read() url = reverse('oauth2-put-document') - resp = app.post(url, params=data, status=400) + resp = app.post(url, params=data, status=401) assert 'basic HTTP authentication failed' in resp.content app.authorization = ('Basic', (str(oauth2_client.client_id), str(oauth2_client.client_secret))) -- 2.11.0