api: use DRF for OAUTH2 APIs (#16842)

This commit is contained in:
Josue Kouka 2018-01-31 16:57:06 +01:00 committed by Benjamin Dauvergne
parent 94dab06b42
commit 85ebba8394
4 changed files with 114 additions and 73 deletions

View File

@ -0,0 +1,63 @@
# 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 <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext_lazy as _
from rest_framework.authentication import BasicAuthentication
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(BasicAuthentication):
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

View File

@ -1,8 +1,7 @@
import cgi
import base64
from urllib import unquote
from .models import OAuth2Authorize, OAuth2Client
from .models import OAuth2Authorize
def authenticate_bearer(request):
@ -21,36 +20,6 @@ def authenticate_bearer(request):
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'

View File

@ -20,13 +20,17 @@ 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.response import Response
from rest_framework.views import APIView
from .authentication import FargoOAUTH2Authentication
from .forms import OAuth2AuthorizeForm
from .models import OAuth2Authorize, OAuth2Client, OAuth2TempFile
from .utils import authenticate_bearer, authenticate_client, get_content_disposition_value
from .utils import authenticate_bearer, get_content_disposition_value
from fargo.fargo.models import UserDocument, Document
@ -35,6 +39,15 @@ class OAuth2Exception(Exception):
pass
class OAUTH2APIViewMixin(APIView):
http_method_names = ['post']
authentication_classes = (FargoOAUTH2Authentication,)
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
return super(OAUTH2APIViewMixin, self).dispatch(request, *args, **kwargs)
class OAuth2AuthorizeView(FormView):
template_name = 'oauth2/authorize.html'
form_class = OAuth2AuthorizeForm
@ -98,29 +111,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(OAUTH2APIViewMixin):
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)
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:
doc_token = OAuth2Authorize.objects.get(code=code).access_token
except OAuth2Authorize.DoesNotExist:
return JsonResponse({'error': 'invalid_request'}, status=400)
try:
token = OAuth2Authorize.objects.get(code=data['code']).access_token
except OAuth2Authorize.DoesNotExist:
return Response({'error': 'invalid_request'}, status=400)
return JsonResponse({'access_token': doc_token, 'expires': '3600'})
return Response({'access_token': token, 'expires': '3600'})
get_document_token = GetDocumentTokenView.as_view()
def get_document(request):
@ -139,26 +148,26 @@ def get_document(request):
return response
@csrf_exempt
def put_document(request):
client = authenticate_client(request)
if not client:
return HttpResponseBadRequest('basic HTTP authentication failed')
class PutDocumentAPIView(OAUTH2APIViewMixin):
filename, error = get_content_disposition_value(request)
if error:
return HttpResponseBadRequest(error)
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]) + '/'
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
response = Response()
response['Location'] = uri
return response
put_document = PutDocumentAPIView.as_view()
class OAuth2AuthorizePutView(TemplateView):

View File

@ -86,6 +86,7 @@ def test_get_document_oauth2(app, john_doe, oauth2_client, user_doc):
params['code'] = auth.code
url = reverse('oauth2-get-token')
app.authorization = ('Basic', (oauth2_client.client_id, oauth2_client.client_secret))
resp = app.post(url, params=params, status=200)
assert 'access_token' in resp.json
assert 'expires' in resp.json
@ -109,8 +110,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)
assert 'basic HTTP authentication failed' in resp.content
resp = app.post(url, params=data, status=401)
app.authorization = ('Basic', (str(oauth2_client.client_id), str(oauth2_client.client_secret)))
resp = app.post(url, params=data, status=400)