add oauth2 access to get and put a document (#14147)
This commit is contained in:
parent
bab24b48c0
commit
fe873ff083
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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')
|
|
@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<pk>\w+)/authorize', login_required(OAuth2AuthorizePutView.as_view()),
|
||||
name='oauth2-put-document-authorize')
|
||||
]
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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'])
|
|
@ -42,6 +42,7 @@ INSTALLED_APPS = (
|
|||
'gadjo',
|
||||
'fargo.fargo',
|
||||
'rest_framework',
|
||||
'fargo.oauth2',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{% extends "fargo/base.html" %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" name="submit" value="{% trans "Choose" %}">
|
||||
<input type="submit" name="cancel" value="{% trans "Cancel" %}">
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,21 @@
|
|||
{% extends "fargo/base.html" %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div id="user-files">
|
||||
{% if error_message %}
|
||||
<p>{% trans error_message %}</p>
|
||||
<a href="{{ redirect_uri }}">{% trans "Continue to your client url" %}</a>
|
||||
{% else %}
|
||||
<p>{% blocktrans %}
|
||||
Do you accept to add<b> {{ filename }} </b>to your portfolio ?
|
||||
{% endblocktrans %}</p>
|
||||
<form id="send-file" method="post" enctype="multipart/form-data" action="{% url 'oauth2-put-document-authorize' pk %}">
|
||||
{% csrf_token %}
|
||||
<input type="submit" name="submit" value="{% trans "Allow" %}"/>
|
||||
<input type="submit" name="cancel" value="{% trans "Cancel" %}"/>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -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')))
|
||||
|
|
|
@ -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
|
|
@ -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 ?
|
Loading…
Reference in New Issue