From c19eeda853ac8f2ac39f13442b23d226edf2e9c2 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Jaillet Date: Mon, 22 May 2017 18:12:45 +0200 Subject: [PATCH] add oauth2 access to get and put a document (#14147) --- fargo/oauth2/__init__.py | 0 fargo/oauth2/admin.py | 28 ++++ fargo/oauth2/forms.py | 34 ++++ fargo/oauth2/migrations/0001_initial.py | 43 +++++ fargo/oauth2/migrations/__init__.py | 0 fargo/oauth2/models.py | 80 +++++++++ fargo/oauth2/urls.py | 30 ++++ fargo/oauth2/views.py | 278 ++++++++++++++++++++++++++++++++ fargo/settings.py | 1 + fargo/templates/oauth2/authorize.html | 12 ++ fargo/templates/oauth2/confirm.html | 21 +++ fargo/urls.py | 2 +- tests/test_oauth2.py | 162 +++++++++++++++++++ tests/test_oauth2.txt | 36 +++++ 14 files changed, 726 insertions(+), 1 deletion(-) create mode 100644 fargo/oauth2/__init__.py create mode 100644 fargo/oauth2/admin.py create mode 100644 fargo/oauth2/forms.py create mode 100644 fargo/oauth2/migrations/0001_initial.py create mode 100644 fargo/oauth2/migrations/__init__.py create mode 100644 fargo/oauth2/models.py create mode 100644 fargo/oauth2/urls.py create mode 100644 fargo/oauth2/views.py create mode 100644 fargo/templates/oauth2/authorize.html create mode 100644 fargo/templates/oauth2/confirm.html create mode 100644 tests/test_oauth2.py create mode 100644 tests/test_oauth2.txt diff --git a/fargo/oauth2/__init__.py b/fargo/oauth2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fargo/oauth2/admin.py b/fargo/oauth2/admin.py new file mode 100644 index 0000000..91cf12a --- /dev/null +++ b/fargo/oauth2/admin.py @@ -0,0 +1,28 @@ +# 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 . + +from django.contrib import admin + +from .models import OAuth2Client + + +class OAuth2ClientAdmin(admin.ModelAdmin): + fields = ('client_name', 'client_id', 'client_secret', 'redirect_uris') + list_display = ['client_name', 'client_id', 'client_secret', 'redirect_uris'] + readonly_fields = ['client_id', 'client_secret'] + + +admin.site.register(OAuth2Client, OAuth2ClientAdmin) diff --git a/fargo/oauth2/forms.py b/fargo/oauth2/forms.py new file mode 100644 index 0000000..97c5c09 --- /dev/null +++ b/fargo/oauth2/forms.py @@ -0,0 +1,34 @@ +# 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 . + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from fargo.fargo.models import UserDocument + + +class UserDocModelChoiceField(forms.ModelChoiceField): + def label_from_instance(self, obj): + return obj.filename + + +class OAuth2AuthorizeForm(forms.Form): + document = UserDocModelChoiceField(queryset='') + + def __init__(self, user, *args, **kwargs): + super(OAuth2AuthorizeForm, self).__init__(*args, **kwargs) + self.fields['document'].queryset = UserDocument.objects.filter(user=user) + self.fields['document'].label = _('Document') diff --git a/fargo/oauth2/migrations/0001_initial.py b/fargo/oauth2/migrations/0001_initial.py new file mode 100644 index 0000000..d6e5f92 --- /dev/null +++ b/fargo/oauth2/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import fargo.oauth2.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fargo', '0013_document_mime_type'), + ] + + operations = [ + migrations.CreateModel( + name='OAuth2Authorize', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('access_token', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)), + ('code', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)), + ('creation_date', models.DateTimeField(auto_now=True)), + ('user_document', models.ForeignKey(to='fargo.UserDocument')), + ], + ), + migrations.CreateModel( + name='OAuth2Client', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('client_secret', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)), + ('client_id', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)), + ('client_name', models.CharField(max_length=255)), + ('redirect_uris', models.TextField(verbose_name='redirect URIs', validators=[fargo.oauth2.models.validate_https_url])), + ], + ), + migrations.CreateModel( + name='OAuth2TempFile', + fields=[ + ('hash_key', models.CharField(max_length=128, serialize=False, primary_key=True)), + ('filename', models.CharField(max_length=512)), + ('document', models.ForeignKey(to='fargo.Document')), + ], + ), + ] diff --git a/fargo/oauth2/migrations/__init__.py b/fargo/oauth2/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fargo/oauth2/models.py b/fargo/oauth2/models.py new file mode 100644 index 0000000..1ba2869 --- /dev/null +++ b/fargo/oauth2/models.py @@ -0,0 +1,80 @@ +# 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 uuid + +from django.core.exceptions import ValidationError +from django.core.validators import URLValidator +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from fargo.fargo.models import Document, UserDocument + + +def generate_uuid(): + return uuid.uuid4().hex + + +def validate_https_url(data): + errors = [] + data = data.strip() + if not data: + return + for url in data.split(): + try: + URLValidator(schemes=['http', 'https'])(url) + except ValidationError as e: + errors.append(e) + if errors: + raise ValidationError(errors) + + +class OAuth2Authorize(models.Model): + user_document = models.ForeignKey(UserDocument) + access_token = models.CharField(max_length=255, default=generate_uuid) + code = models.CharField(max_length=255, default=generate_uuid) + creation_date = models.DateTimeField(auto_now=True) + + def __repr__(self): + return 'OAuth2Authorize for document %r' % self.user_document + + +class OAuth2Client(models.Model): + client_secret = models.CharField(max_length=255, default=generate_uuid) + client_id = models.CharField(max_length=255, default=generate_uuid) + client_name = models.CharField(max_length=255) + redirect_uris = models.TextField( + verbose_name=_('redirect URIs'), + validators=[validate_https_url]) + + def __repr__(self): + return 'OAuth2Client name: %s with id: %s' % (self.client_name, self.client_id) + + def get_redirect_uris(self): + return self.redirect_uris.split() + + def check_redirect_uri(self, redirect_uri): + return redirect_uri in self.redirect_uris.strip().split() + + +class OAuth2TempFile(models.Model): + hash_key = models.CharField(max_length=128, primary_key=True) + document = models.ForeignKey(Document) + filename = models.CharField(max_length=512) + + def save(self, *args, **kwargs): + self.hash_key = self.document.content_hash + return super(OAuth2TempFile, self).save(*args, **kwargs) diff --git a/fargo/oauth2/urls.py b/fargo/oauth2/urls.py new file mode 100644 index 0000000..093b956 --- /dev/null +++ b/fargo/oauth2/urls.py @@ -0,0 +1,30 @@ +# 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 . + +from django.conf.urls import url +from django.contrib.auth.decorators import login_required + +from .views import (OAuth2AuthorizeView, get_document_token, get_document, + OAuth2AuthorizePutView, put_document) + +urlpatterns = [ + url(r'get-document/authorize', login_required(OAuth2AuthorizeView.as_view()), name='oauth2-authorize'), + url(r'get-document/token', get_document_token, name='oauth2-get-token'), + url(r'get-document/', get_document, name='oauth2-get-document'), + url(r'put-document/$', put_document, name='oauth2-put-document'), + url(r'put-document/(?P\w+)/authorize', login_required(OAuth2AuthorizePutView.as_view()), + name='oauth2-put-document-authorize') +] diff --git a/fargo/oauth2/views.py b/fargo/oauth2/views.py new file mode 100644 index 0000000..0bb7d24 --- /dev/null +++ b/fargo/oauth2/views.py @@ -0,0 +1,278 @@ +# 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 cgi +import base64 +import urllib +from urllib import quote, unquote + +from django.core.files.base import ContentFile +from django.core.urlresolvers import reverse +from django.http import (HttpResponse, HttpResponseBadRequest, + HttpResponseRedirect, JsonResponse) +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import FormView, TemplateView + +from .forms import OAuth2AuthorizeForm +from .models import OAuth2Authorize, OAuth2Client, OAuth2TempFile + +from fargo.fargo.models import UserDocument, Document + + +class OAuth2Exception(Exception): + pass + + +class OAuth2AuthorizeView(FormView): + template_name = 'oauth2/authorize.html' + form_class = OAuth2AuthorizeForm + success_url = '/' + + def get(self, request, *args, **kwargs): + redirect_uri = request.GET.get('redirect_uri') + if not redirect_uri: + return HttpResponseBadRequest('missing parameter redirect_uri') + client_id = request.GET.get('client_id') + response_type = request.GET.get('response_type') + if not client_id or not response_type: + uri = self.error_redirect(redirect_uri, 'invalid_request') + return HttpResponseRedirect(uri) + if response_type != 'code': + uri = self.error_redirect(redirect_uri, 'unsupported_response_type') + return HttpResponseRedirect(uri) + try: + client = OAuth2Client.objects.get(client_id=client_id) + if not client.check_redirect_uri(redirect_uri): + uri = self.error_redirect(redirect_uri, 'invalid_redirect_uri') + return HttpResponseRedirect(uri) + except OAuth2Client.DoesNotExist: + uri = self.error_redirect(redirect_uri, 'unauthorized_client') + return HttpResponseRedirect(uri) + + request.session['redirect_uri'] = redirect_uri + return super(OAuth2AuthorizeView, self).get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + if 'cancel' in request.POST: + uri = self.error_redirect(request.session['redirect_uri'], 'access_denied') + return HttpResponseRedirect(uri) + else: + return super(OAuth2AuthorizeView, self).post(request, *args, **kwargs) + + def get_form_kwargs(self): + kwargs = super(OAuth2AuthorizeView, self).get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + def error_redirect(self, redirect_uri, error_code): + if not redirect_uri[-1] == '/': + redirect_uri += '/' + redirect_uri += '?' + return redirect_uri + urllib.urlencode({'error': error_code}) + + def form_valid(self, form): + doc = form.cleaned_data['document'] + authorization = OAuth2Authorize.objects.create(user_document=doc) + getvars = {'code': authorization.code} + + if 'state' in self.request.GET: + getvars['state'] = self.request.GET['state'] + redirect_uri = self.request.session['redirect_uri'] + if not redirect_uri[-1] == '/': + redirect_uri += '/' + + self.success_url = redirect_uri + '?' + urllib.urlencode(getvars) + + 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) + + 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) + + 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'}) + + +def get_document(request): + oauth_authorize = authenticate_bearer(request) + if not oauth_authorize: + return HttpResponseBadRequest('http bearer authentication failed: invalid authorization header') + + doc = oauth_authorize.user_document + response = HttpResponse(content=doc.document.content, status=200, + content_type='application/octet-stream') + + ascii_filename = doc.filename.encode('ascii', 'replace') + percent_encoded_filename = quote(doc.filename.encode('utf8'), safe='') + response['Content-Disposition'] = 'attachement; filename="%s"; filename*=UTF-8\'\'%s' % (ascii_filename, + percent_encoded_filename) + 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 OAuth2AuthorizePutView(TemplateView): + template_name = 'oauth2/confirm.html' + + def get_context_data(self, **kwargs): + context = super(OAuth2AuthorizePutView, self).get_context_data(**kwargs) + try: + oauth2_document = OAuth2TempFile.objects.get(pk=kwargs['pk']) + + except OAuth2TempFile.DoesNotExist: + context['error_message'] = 'The document has not been uploaded' + context['redirect_uri'] = self.request.GET['redirect_uri'] + return context + + user_document = UserDocument.objects.filter(user=self.request.user, + document=oauth2_document.document) + if user_document: + context['error_message'] = 'This document is already in your portfolio' + context['redirect_uri'] = self.request.GET['redirect_uri'] + return context + else: + context['filename'] = oauth2_document.filename + context['error_message'] = '' + return context + + def get(self, request, *args, **kwargs): + redirect_uri = request.GET.get('redirect_uri', '') + if not redirect_uri: + return HttpResponseBadRequest('missing redirect_uri parameter') + + request.session['redirect_uri'] = redirect_uri + return super(OAuth2AuthorizePutView, self).get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + oauth2_document = OAuth2TempFile.objects.get(pk=kwargs['pk']) + if 'cancel' in request.POST: + error = urllib.urlencode({'error': 'access_denied'}) + oauth2_document.delete() + uri = request.session['redirect_uri'] + '?' + error + return HttpResponseRedirect(uri) + + UserDocument.objects.create( + user=request.user, document=oauth2_document.document, + filename=oauth2_document.filename) + return HttpResponseRedirect(request.session['redirect_uri']) diff --git a/fargo/settings.py b/fargo/settings.py index dc26dce..fb89733 100644 --- a/fargo/settings.py +++ b/fargo/settings.py @@ -42,6 +42,7 @@ INSTALLED_APPS = ( 'gadjo', 'fargo.fargo', 'rest_framework', + 'fargo.oauth2', ) MIDDLEWARE_CLASSES = ( diff --git a/fargo/templates/oauth2/authorize.html b/fargo/templates/oauth2/authorize.html new file mode 100644 index 0000000..fb30772 --- /dev/null +++ b/fargo/templates/oauth2/authorize.html @@ -0,0 +1,12 @@ +{% extends "fargo/base.html" %} +{% load render_table from django_tables2 %} +{% load i18n %} + +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} + + +
+{% endblock %} diff --git a/fargo/templates/oauth2/confirm.html b/fargo/templates/oauth2/confirm.html new file mode 100644 index 0000000..e56d7c7 --- /dev/null +++ b/fargo/templates/oauth2/confirm.html @@ -0,0 +1,21 @@ +{% extends "fargo/base.html" %} +{% load render_table from django_tables2 %} +{% load i18n %} + +{% block content %} +
+ {% if error_message %} +

{% trans error_message %}

+ {% trans "Continue to your client url" %} + {% else %} +

{% blocktrans %} + Do you accept to add {{ filename }} to your portfolio ? + {% endblocktrans %}

+
+ {% csrf_token %} + + +
+ {% endif %} +
+{% endblock %} diff --git a/fargo/urls.py b/fargo/urls.py index 502c7fa..4d315d7 100644 --- a/fargo/urls.py +++ b/fargo/urls.py @@ -30,8 +30,8 @@ urlpatterns = [ url(r'^api/documents/push/$', push_document, name='fargo-api-push-document'), url(r'^api/documents/recently-added/$', recent_documents), url(r'^api/', include(router.urls)), + url(r'^api/', include('fargo.oauth2.urls')), ] - if 'mellon' in settings.INSTALLED_APPS: urlpatterns.append(url(r'^accounts/mellon/', include('mellon.urls'))) diff --git a/tests/test_oauth2.py b/tests/test_oauth2.py new file mode 100644 index 0000000..0705218 --- /dev/null +++ b/tests/test_oauth2.py @@ -0,0 +1,162 @@ +import pytest +from urllib import quote +import urlparse + +from django.core.files.base import ContentFile +from django.core.urlresolvers import reverse +from django.utils.http import urlencode + +from fargo.oauth2.models import OAuth2Client, OAuth2Authorize, OAuth2TempFile +from fargo.fargo.models import Document, UserDocument + +from test_manager import login + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def oauth2_client(): + return OAuth2Client.objects.create( + client_name='test_oauth2', client_id='client-id', client_secret='client-secret', + redirect_uris='https://example.net/document https://doc.example.net/ https://example.com') + + +@pytest.fixture +def document(): + with open('tests/test_oauth2.txt', 'rb') as f: + content = ContentFile(f.read(), 'test_oauth2.txt') + + return Document.objects.get_by_file(content) + + +@pytest.fixture +def user_doc(document, john_doe): + return UserDocument.objects.create(user=john_doe, document=document, filename='Baudelaire.txt') + + +def assert_error_redirect(url, error): + assert urlparse.urlparse(url).query == 'error=%s' % error + + +def test_get_document_oauth2(app, john_doe, oauth2_client, user_doc): + login(app, user=john_doe) + url = reverse('oauth2-authorize') + params = { + 'client_secret': oauth2_client.client_secret, + 'response_type': 'code', + 'state': 'achipeachope' + } + # test missing redirect_uri + resp = app.get(url, params={}, status=400) + assert resp.content == 'missing parameter redirect_uri' + # test missing client id + params['redirect_uri'] = 'https://toto.example.com' + resp = app.get(url, params=params, status=302) + assert_error_redirect(resp.url, 'invalid_request') + # test invalid response type + params['client_id'] = oauth2_client.client_id + params['response_type'] = 'token' + resp = app.get(url, params=params, status=302) + assert_error_redirect(resp.url, 'unsupported_response_type') + # test invalid redirect uri + params['response_type'] = 'code' + resp = app.get(url, params=params, status=302) + assert_error_redirect(resp.url, 'invalid_redirect_uri') + + params['redirect_uri'] = 'https://example.com' + resp = app.get(url, params=params) + + assert resp.status_code == 200 + assert len(resp.forms[0]['document'].options) == 2 + assert 'Baudelaire.txt' in resp.forms[0]['document'].options[1] + + resp.forms[0]['document'].select('1') + resp = resp.forms[0].submit() + assert len(OAuth2Authorize.objects.filter(user_document__user=john_doe)) == 1 + auth = OAuth2Authorize.objects.filter(user_document__user=john_doe)[0] + assert resp.status_code == 302 + assert 'code' in resp.location + assert auth.code in resp.location + assert 'state' in resp.location + assert 'achipeachope' in resp.location + + params.pop('response_type') + params.pop('state') + params['grant_type'] = 'authorization_code' + params['code'] = auth.code + + url = reverse('oauth2-get-token') + resp = app.post(url, params=params, status=200) + assert 'access_token' in resp.json + assert 'expires' in resp.json + assert resp.json['access_token'] == auth.access_token + + url = reverse('oauth2-get-document') + app.authorization = ('Bearer', str(auth.access_token)) + resp = app.get(url, status=200) + + assert resp.content_type == 'application/octet-stream' + assert 'Content-disposition' in resp.headers + content_disposition = resp.content_disposition.replace(' ', '').split(';') + assert content_disposition[0] == 'attachement' + assert content_disposition[1] == 'filename="Baudelaire.txt"' + assert content_disposition[2] == 'filename*=UTF-8\'\'Baudelaire.txt' + + +def test_put_document(app, john_doe, oauth2_client): + login(app, user=john_doe) + with open('tests/test_oauth2.txt', 'rb') as f: + data = f.read() + + url = reverse('oauth2-put-document') + resp = app.post(url, params=data, status=400) + assert 'basic HTTP authentication failed' in resp.content + + app.authorization = ('Basic', (str(oauth2_client.client_id), str(oauth2_client.client_secret))) + resp = app.post(url, params=data, status=400) + assert 'missing content-disposition header' in resp.content + + filename = 'Baudelaire.txt'.encode('ascii', 'replace') + percent_encode_filename = quote(filename.encode('utf8'), safe='') + headers = { + 'Content-disposition': 'attachement; filename="%s"; filename*=UTF-8\'\'%s' % (filename, percent_encode_filename) + } + + assert len(OAuth2TempFile.objects.all()) == 0 + resp = app.post(url, params=data, headers=headers, status=200) + + assert len(OAuth2TempFile.objects.all()) == 1 + doc = OAuth2TempFile.objects.all()[0] + location = reverse('oauth2-put-document-authorize', kwargs={'pk': doc.pk}) + assert location in resp.location + + app.authorization = None + url = location + '?%s' % urlencode({'redirect_uri': 'https://example.com'}) + resp = app.get(url, status=200) + + assert len(UserDocument.objects.all()) == 0 + resp = resp.forms[0].submit() + + assert resp.status_code == 302 + assert resp.location == 'https://example.com' + try: + user_document = UserDocument.objects.get(user=john_doe, document=doc.document) + except UserDocument.DoesNotExist: + assert False + + assert user_document.filename == 'Baudelaire.txt' + + +def test_confirm_put_document_file_exception(app, john_doe, user_doc): + login(app, user=john_doe) + oauth_tmp_file = OAuth2TempFile.objects.create(document=user_doc.document, filename=user_doc.filename) + + url = reverse('oauth2-put-document-authorize', kwargs={'pk': 'fakemofo'}) + url += '?%s' % urlencode({'redirect_uri': 'https://example.com'}) + resp = app.get(url) + assert 'The document has not been uploaded' in resp.content + + url = reverse('oauth2-put-document-authorize', kwargs={'pk': oauth_tmp_file.pk}) + url += '?%s' % urlencode({'redirect_uri': 'https://example.com'}) + resp = app.get(url) + assert 'This document is already in your portfolio' in resp.content diff --git a/tests/test_oauth2.txt b/tests/test_oauth2.txt new file mode 100644 index 0000000..d0d9d8e --- /dev/null +++ b/tests/test_oauth2.txt @@ -0,0 +1,36 @@ +Poème hymne à la beauté du recueil les fleurs du mal de Charles Baudelaire + +Viens-tu du ciel profond ou sors-tu de l'abîme, +Ô Beauté ! ton regard, infernal et divin, +Verse confusément le bienfait et le crime, +Et l'on peut pour cela te comparer au vin. + +Tu contiens dans ton oeil le couchant et l'aurore ; +Tu répands des parfums comme un soir orageux ; +Tes baisers sont un philtre et ta bouche une amphore +Qui font le héros lâche et l'enfant courageux. + +Sors-tu du gouffre noir ou descends-tu des astres ? +Le Destin charmé suit tes jupons comme un chien ; +Tu sèmes au hasard la joie et les désastres, +Et tu gouvernes tout et ne réponds de rien. + +Tu marches sur des morts, Beauté, dont tu te moques ; +De tes bijoux l'Horreur n'est pas le moins charmant, +Et le Meurtre, parmi tes plus chères breloques, +Sur ton ventre orgueilleux danse amoureusement. + +L'éphémère ébloui vole vers toi, chandelle, +Crépite, flambe et dit : Bénissons ce flambeau ! +L'amoureux pantelant incliné sur sa belle +A l'air d'un moribond caressant son tombeau. + +Que tu viennes du ciel ou de l'enfer, qu'importe, +Ô Beauté ! monstre énorme, effrayant, ingénu ! +Si ton oeil, ton souris, ton pied, m'ouvrent la porte +D'un Infini que j'aime et n'ai jamais connu ? + +De Satan ou de Dieu, qu'importe ? Ange ou Sirène, +Qu'importe, si tu rends, - fée aux yeux de velours, +Rythme, parfum, lueur, ô mon unique reine ! - +L'univers moins hideux et les instants moins lourds ? -- 2.11.0