Projet

Général

Profil

0001-add-oauth2-access-to-get-and-put-a-document-14147.patch

Josué Kouka, 30 octobre 2017 14:56

Télécharger (32,1 ko)

Voir les différences:

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
fargo/oauth2/admin.py
1
# fargo - document box
2
# Copyright (C) 2016-2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.contrib import admin
18

  
19
from .models import OAuth2Client
20

  
21

  
22
class OAuth2ClientAdmin(admin.ModelAdmin):
23
    fields = ('client_name', 'client_id', 'client_secret', 'redirect_uris')
24
    list_display = ['client_name', 'client_id', 'client_secret', 'redirect_uris']
25
    readonly_fields = ['client_id', 'client_secret']
26

  
27

  
28
admin.site.register(OAuth2Client, OAuth2ClientAdmin)
fargo/oauth2/forms.py
1
# fargo - document box
2
# Copyright (C) 2016-2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django import forms
18
from django.utils.translation import ugettext_lazy as _
19

  
20
from fargo.fargo.models import UserDocument
21

  
22

  
23
class UserDocModelChoiceField(forms.ModelChoiceField):
24
    def label_from_instance(self, obj):
25
        return obj.filename
26

  
27

  
28
class OAuth2AuthorizeForm(forms.Form):
29
    document = UserDocModelChoiceField(queryset='')
30

  
31
    def __init__(self, user, *args, **kwargs):
32
        super(OAuth2AuthorizeForm, self).__init__(*args, **kwargs)
33
        self.fields['document'].queryset = UserDocument.objects.filter(user=user)
34
        self.fields['document'].label = _('Document')
fargo/oauth2/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import models, migrations
5
import fargo.oauth2.models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('fargo', '0013_document_mime_type'),
12
    ]
13

  
14
    operations = [
15
        migrations.CreateModel(
16
            name='OAuth2Authorize',
17
            fields=[
18
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
19
                ('access_token', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)),
20
                ('code', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)),
21
                ('creation_date', models.DateTimeField(auto_now=True)),
22
                ('user_document', models.ForeignKey(to='fargo.UserDocument')),
23
            ],
24
        ),
25
        migrations.CreateModel(
26
            name='OAuth2Client',
27
            fields=[
28
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
29
                ('client_secret', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)),
30
                ('client_id', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)),
31
                ('client_name', models.CharField(max_length=255)),
32
                ('redirect_uris', models.TextField(verbose_name='redirect URIs', validators=[fargo.oauth2.models.validate_https_url])),
33
            ],
34
        ),
35
        migrations.CreateModel(
36
            name='OAuth2TempFile',
37
            fields=[
38
                ('hash_key', models.CharField(max_length=128, serialize=False, primary_key=True)),
39
                ('filename', models.CharField(max_length=512)),
40
                ('document', models.ForeignKey(to='fargo.Document')),
41
            ],
42
        ),
43
    ]
fargo/oauth2/models.py
1
# fargo - document box
2
# Copyright (C) 2016-2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import uuid
18

  
19
from django.core.exceptions import ValidationError
20
from django.core.validators import URLValidator
21
from django.db import models
22
from django.utils.translation import ugettext_lazy as _
23

  
24
from fargo.fargo.models import Document, UserDocument
25

  
26

  
27
def generate_uuid():
28
    return uuid.uuid4().hex
29

  
30

  
31
def validate_https_url(data):
32
    errors = []
33
    data = data.strip()
34
    if not data:
35
        return
36
    for url in data.split():
37
        try:
38
            URLValidator(schemes=['http', 'https'])(url)
39
        except ValidationError as e:
40
            errors.append(e)
41
    if errors:
42
        raise ValidationError(errors)
43

  
44

  
45
class OAuth2Authorize(models.Model):
46
    user_document = models.ForeignKey(UserDocument)
47
    access_token = models.CharField(max_length=255, default=generate_uuid)
48
    code = models.CharField(max_length=255, default=generate_uuid)
49
    creation_date = models.DateTimeField(auto_now=True)
50

  
51
    def __repr__(self):
52
        return 'OAuth2Authorize for document %r' % self.user_document
53

  
54

  
55
class OAuth2Client(models.Model):
56
    client_secret = models.CharField(max_length=255, default=generate_uuid)
57
    client_id = models.CharField(max_length=255, default=generate_uuid)
58
    client_name = models.CharField(max_length=255)
59
    redirect_uris = models.TextField(
60
        verbose_name=_('redirect URIs'),
61
        validators=[validate_https_url])
62

  
63
    def __repr__(self):
64
        return 'OAuth2Client name: %s with id: %s' % (self.client_name, self.client_id)
65

  
66
    def get_redirect_uris(self):
67
        return self.redirect_uris.split()
68

  
69
    def check_redirect_uri(self, redirect_uri):
70
        return redirect_uri in self.redirect_uris.strip().split()
71

  
72

  
73
class OAuth2TempFile(models.Model):
74
    hash_key = models.CharField(max_length=128, primary_key=True)
75
    document = models.ForeignKey(Document)
76
    filename = models.CharField(max_length=512)
77

  
78
    def save(self, *args, **kwargs):
79
        self.hash_key = self.document.content_hash
80
        return super(OAuth2TempFile, self).save(*args, **kwargs)
fargo/oauth2/urls.py
1
# fargo - document box
2
# Copyright (C) 2016-2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import url
18
from django.contrib.auth.decorators import login_required
19

  
20
from .views import (OAuth2AuthorizeView, get_document_token, get_document,
21
                    OAuth2AuthorizePutView, put_document)
22

  
23
urlpatterns = [
24
    url(r'get-document/authorize', login_required(OAuth2AuthorizeView.as_view()), name='oauth2-authorize'),
25
    url(r'get-document/token', get_document_token, name='oauth2-get-token'),
26
    url(r'get-document/', get_document, name='oauth2-get-document'),
27
    url(r'put-document/$', put_document, name='oauth2-put-document'),
28
    url(r'put-document/(?P<pk>\w+)/authorize', login_required(OAuth2AuthorizePutView.as_view()),
29
        name='oauth2-put-document-authorize')
30
]
fargo/oauth2/views.py
1
# fargo - document box
2
# Copyright (C) 2016-2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import cgi
18
import base64
19
import urllib
20
from urllib import quote, unquote
21

  
22
from django.core.files.base import ContentFile
23
from django.core.urlresolvers import reverse
24
from django.http import (HttpResponse, HttpResponseBadRequest,
25
                         HttpResponseRedirect, JsonResponse)
26
from django.views.decorators.csrf import csrf_exempt
27
from django.views.generic import FormView, TemplateView
28

  
29
from .forms import OAuth2AuthorizeForm
30
from .models import OAuth2Authorize, OAuth2Client, OAuth2TempFile
31

  
32
from fargo.fargo.models import UserDocument, Document
33

  
34

  
35
class OAuth2Exception(Exception):
36
    pass
37

  
38

  
39
class OAuth2AuthorizeView(FormView):
40
    template_name = 'oauth2/authorize.html'
41
    form_class = OAuth2AuthorizeForm
42
    success_url = '/'
43

  
44
    def get(self, request, *args, **kwargs):
45
        redirect_uri = request.GET.get('redirect_uri')
46
        if not redirect_uri:
47
            return HttpResponseBadRequest('missing parameter redirect_uri')
48
        client_id = request.GET.get('client_id')
49
        response_type = request.GET.get('response_type')
50
        if not client_id or not response_type:
51
            uri = self.error_redirect(redirect_uri, 'invalid_request')
52
            return HttpResponseRedirect(uri)
53
        if response_type != 'code':
54
            uri = self.error_redirect(redirect_uri, 'unsupported_response_type')
55
            return HttpResponseRedirect(uri)
56
        try:
57
            client = OAuth2Client.objects.get(client_id=client_id)
58
            if not client.check_redirect_uri(redirect_uri):
59
                uri = self.error_redirect(redirect_uri, 'invalid_redirect_uri')
60
                return HttpResponseRedirect(uri)
61
        except OAuth2Client.DoesNotExist:
62
            uri = self.error_redirect(redirect_uri, 'unauthorized_client')
63
            return HttpResponseRedirect(uri)
64

  
65
        request.session['redirect_uri'] = redirect_uri
66
        return super(OAuth2AuthorizeView, self).get(request, *args, **kwargs)
67

  
68
    def post(self, request, *args, **kwargs):
69
        if 'cancel' in request.POST:
70
            uri = self.error_redirect(request.session['redirect_uri'], 'access_denied')
71
            return HttpResponseRedirect(uri)
72
        else:
73
            return super(OAuth2AuthorizeView, self).post(request, *args, **kwargs)
74

  
75
    def get_form_kwargs(self):
76
        kwargs = super(OAuth2AuthorizeView, self).get_form_kwargs()
77
        kwargs['user'] = self.request.user
78
        return kwargs
79

  
80
    def error_redirect(self, redirect_uri, error_code):
81
        if not redirect_uri[-1] == '/':
82
            redirect_uri += '/'
83
        redirect_uri += '?'
84
        return redirect_uri + urllib.urlencode({'error': error_code})
85

  
86
    def form_valid(self, form):
87
        doc = form.cleaned_data['document']
88
        authorization = OAuth2Authorize.objects.create(user_document=doc)
89
        getvars = {'code': authorization.code}
90

  
91
        if 'state' in self.request.GET:
92
            getvars['state'] = self.request.GET['state']
93
        redirect_uri = self.request.session['redirect_uri']
94
        if not redirect_uri[-1] == '/':
95
            redirect_uri += '/'
96

  
97
        self.success_url = redirect_uri + '?' + urllib.urlencode(getvars)
98

  
99
        return super(OAuth2AuthorizeView, self).form_valid(form)
100

  
101

  
102
@csrf_exempt
103
def get_document_token(request):
104
    grant_type = request.POST.get('grant_type')
105
    redirect_uri = request.POST.get('redirect_uri')
106
    code = request.POST.get('code')
107
    client = authenticate_client(request)
108

  
109
    try:
110
        if not client:
111
            raise OAuth2Exception('invalid client')
112
        if redirect_uri not in client.get_redirect_uris():
113
            raise OAuth2Exception('invalid_request')
114
        if grant_type != 'authorization_code':
115
            raise OAuth2Exception('unsupported_grant_type')
116
    except OAuth2Exception as e:
117
        return JsonResponse({'error': e.message}, status=400)
118

  
119
    try:
120
        doc_token = OAuth2Authorize.objects.get(code=code).access_token
121
    except OAuth2Authorize.DoesNotExist:
122
        return JsonResponse({'error': 'invalid_request'}, status=400)
123

  
124
    return JsonResponse({'access_token': doc_token, 'expires': '3600'})
125

  
126

  
127
def get_document(request):
128
    oauth_authorize = authenticate_bearer(request)
129
    if not oauth_authorize:
130
        return HttpResponseBadRequest('http bearer authentication failed: invalid authorization header')
131

  
132
    doc = oauth_authorize.user_document
133
    response = HttpResponse(content=doc.document.content, status=200,
134
                            content_type='application/octet-stream')
135

  
136
    ascii_filename = doc.filename.encode('ascii', 'replace')
137
    percent_encoded_filename = quote(doc.filename.encode('utf8'), safe='')
138
    response['Content-Disposition'] = 'attachement; filename="%s"; filename*=UTF-8\'\'%s' % (ascii_filename,
139
                                                                                             percent_encoded_filename)
140
    return response
141

  
142

  
143
def authenticate_bearer(request):
144
    authorization = request.META.get('HTTP_AUTHORIZATION')
145
    if not authorization:
146
        return False
147
    splitted = authorization.split()
148
    if len(splitted) < 2:
149
        return False
150
    if splitted[0] != 'Bearer':
151
        return False
152
    token = splitted[1]
153
    try:
154
        return OAuth2Authorize.objects.get(access_token=token)
155
    except OAuth2Authorize.DoesNotExist:
156
        return False
157

  
158

  
159
def authenticate_client(request, client=False):
160
    '''Authenticate client on the token endpoint'''
161

  
162
    if 'HTTP_AUTHORIZATION' in request.META:
163
        authorization = request.META['HTTP_AUTHORIZATION'].split()
164
        if authorization[0] != 'Basic' or len(authorization) != 2:
165
            return False
166
        try:
167
            decoded = base64.b64decode(authorization[1])
168
        except TypeError:
169
            return False
170
        parts = decoded.split(':')
171
        if len(parts) != 2:
172
            return False
173
        client_id, client_secret = parts
174
    elif 'client_id' in request.POST:
175
        client_id = request.POST['client_id']
176
        client_secret = request.POST.get('client_secret', '')
177
    else:
178
        return False
179
    if not client:
180
        try:
181
            client = OAuth2Client.objects.get(client_id=client_id)
182
        except OAuth2Client.DoesNotExist:
183
            return False
184
    if client.client_secret != client_secret:
185
        return False
186
    return client
187

  
188

  
189
def get_content_disposition_value(request):
190
    if 'HTTP_CONTENT_DISPOSITION' not in request.META:
191
        return None, 'missing content-disposition header'
192
    content_header = request.META['HTTP_CONTENT_DISPOSITION']
193
    disposition_type, filename = cgi.parse_header(content_header)
194
    if disposition_type != 'attachement':
195
        return None, 'wrong disposition type: attachement excpected'
196
    if 'filename*' in filename:
197
        encode, country, name = filename['filename*'].split("'")
198

  
199
        # check accepted charset from rfc 5987
200
        if encode == 'UTF-8':
201
            return unquote(name.decode('utf8')), None
202
        elif encode == 'ISO-8859-1':
203
            return unquote(name.decode('iso-8859-1')), None
204
        else:
205
            return None, 'unknown encoding: UTF-8 or ISO-8859-1 allowed'
206
    elif 'filename' in filename:
207
        return filename['filename'], None
208
    else:
209
        # no filename in header
210
        return None, 'missing filename(*) parameter in header'
211

  
212

  
213
@csrf_exempt
214
def put_document(request):
215
    client = authenticate_client(request)
216
    if not client:
217
        return HttpResponseBadRequest('basic HTTP authentication failed')
218

  
219
    filename, error = get_content_disposition_value(request)
220
    if error:
221
        return HttpResponseBadRequest(error)
222

  
223
    f = ContentFile(request.body, name=filename)
224
    document = Document.objects.get_by_file(f)
225
    oauth2_document = OAuth2TempFile.objects.create(document=document,
226
                                                    filename=filename)
227
    uri = reverse('oauth2-put-document-authorize',
228
                  args=[oauth2_document.pk]) + '/'
229

  
230
    response = HttpResponse()
231
    response['Location'] = uri
232
    return response
233

  
234

  
235
class OAuth2AuthorizePutView(TemplateView):
236
    template_name = 'oauth2/confirm.html'
237

  
238
    def get_context_data(self, **kwargs):
239
        context = super(OAuth2AuthorizePutView, self).get_context_data(**kwargs)
240
        try:
241
            oauth2_document = OAuth2TempFile.objects.get(pk=kwargs['pk'])
242

  
243
        except OAuth2TempFile.DoesNotExist:
244
            context['error_message'] = 'The document has not been uploaded'
245
            context['redirect_uri'] = self.request.GET['redirect_uri']
246
            return context
247

  
248
        user_document = UserDocument.objects.filter(user=self.request.user,
249
                                                    document=oauth2_document.document)
250
        if user_document:
251
            context['error_message'] = 'This document is already in your portfolio'
252
            context['redirect_uri'] = self.request.GET['redirect_uri']
253
            return context
254
        else:
255
            context['filename'] = oauth2_document.filename
256
            context['error_message'] = ''
257
            return context
258

  
259
    def get(self, request, *args, **kwargs):
260
        redirect_uri = request.GET.get('redirect_uri', '')
261
        if not redirect_uri:
262
            return HttpResponseBadRequest('missing redirect_uri parameter')
263

  
264
        request.session['redirect_uri'] = redirect_uri
265
        return super(OAuth2AuthorizePutView, self).get(request, *args, **kwargs)
266

  
267
    def post(self, request, *args, **kwargs):
268
        oauth2_document = OAuth2TempFile.objects.get(pk=kwargs['pk'])
269
        if 'cancel' in request.POST:
270
            error = urllib.urlencode({'error': 'access_denied'})
271
            oauth2_document.delete()
272
            uri = request.session['redirect_uri'] + '?' + error
273
            return HttpResponseRedirect(uri)
274

  
275
        UserDocument.objects.create(
276
            user=request.user, document=oauth2_document.document,
277
            filename=oauth2_document.filename)
278
        return HttpResponseRedirect(request.session['redirect_uri'])
fargo/settings.py
42 42
    'gadjo',
43 43
    'fargo.fargo',
44 44
    'rest_framework',
45
    'fargo.oauth2',
45 46
)
46 47

  
47 48
MIDDLEWARE_CLASSES = (
fargo/templates/oauth2/authorize.html
1
{% extends "fargo/base.html" %}
2
{% load render_table from django_tables2 %}
3
{% load i18n %}
4

  
5
{% block content %}
6
  <form method="post" enctype="multipart/form-data">
7
    {% csrf_token %}
8
    {{ form.as_p  }}
9
    <input type="submit" name="submit" value="{% trans "Choose" %}">
10
    <input type="submit" name="cancel" value="{% trans "Cancel" %}">
11
  </form>
12
{% endblock %}
fargo/templates/oauth2/confirm.html
1
{% extends "fargo/base.html" %}
2
{% load render_table from django_tables2 %}
3
{% load i18n %}
4

  
5
{% block content %}
6
  <div id="user-files">
7
  {% if error_message %}
8
    <p>{% trans error_message %}</p>
9
    <a href="{{ redirect_uri }}">{% trans "Continue to your client url" %}</a>
10
  {% else %}
11
      <p>{% blocktrans %}
12
        Do you accept to add<b> {{ filename }} </b>to your portfolio ?
13
      {% endblocktrans %}</p>
14
    <form id="send-file" method="post" enctype="multipart/form-data" action="{% url 'oauth2-put-document-authorize' pk %}">
15
    {% csrf_token %}
16
      <input type="submit" name="submit" value="{% trans "Allow" %}"/>
17
      <input type="submit" name="cancel" value="{% trans "Cancel" %}"/>
18
    </form>
19
  {% endif %}
20
  </div>
21
{% endblock %}
fargo/urls.py
30 30
    url(r'^api/documents/push/$', push_document, name='fargo-api-push-document'),
31 31
    url(r'^api/documents/recently-added/$', recent_documents),
32 32
    url(r'^api/', include(router.urls)),
33
    url(r'^api/', include('fargo.oauth2.urls')),
33 34
]
34 35

  
35

  
36 36
if 'mellon' in settings.INSTALLED_APPS:
37 37
    urlpatterns.append(url(r'^accounts/mellon/', include('mellon.urls')))
tests/test_oauth2.py
1
import pytest
2
from urllib import quote
3
import urlparse
4

  
5
from django.core.files.base import ContentFile
6
from django.core.urlresolvers import reverse
7
from django.utils.http import urlencode
8

  
9
from fargo.oauth2.models import OAuth2Client, OAuth2Authorize, OAuth2TempFile
10
from fargo.fargo.models import Document, UserDocument
11

  
12
from test_manager import login
13

  
14
pytestmark = pytest.mark.django_db
15

  
16

  
17
@pytest.fixture
18
def oauth2_client():
19
    return OAuth2Client.objects.create(
20
        client_name='test_oauth2', client_id='client-id', client_secret='client-secret',
21
        redirect_uris='https://example.net/document https://doc.example.net/ https://example.com')
22

  
23

  
24
@pytest.fixture
25
def document():
26
    with open('tests/test_oauth2.txt', 'rb') as f:
27
        content = ContentFile(f.read(), 'test_oauth2.txt')
28

  
29
    return Document.objects.get_by_file(content)
30

  
31

  
32
@pytest.fixture
33
def user_doc(document, john_doe):
34
    return UserDocument.objects.create(user=john_doe, document=document, filename='Baudelaire.txt')
35

  
36

  
37
def assert_error_redirect(url, error):
38
    assert urlparse.urlparse(url).query == 'error=%s' % error
39

  
40

  
41
def test_get_document_oauth2(app, john_doe, oauth2_client, user_doc):
42
    login(app, user=john_doe)
43
    url = reverse('oauth2-authorize')
44
    params = {
45
        'client_secret': oauth2_client.client_secret,
46
        'response_type': 'code',
47
        'state': 'achipeachope'
48
    }
49
    # test missing redirect_uri
50
    resp = app.get(url, params={}, status=400)
51
    assert resp.content == 'missing parameter redirect_uri'
52
    # test missing client id
53
    params['redirect_uri'] = 'https://toto.example.com'
54
    resp = app.get(url, params=params, status=302)
55
    assert_error_redirect(resp.url, 'invalid_request')
56
    # test invalid response type
57
    params['client_id'] = oauth2_client.client_id
58
    params['response_type'] = 'token'
59
    resp = app.get(url, params=params, status=302)
60
    assert_error_redirect(resp.url, 'unsupported_response_type')
61
    # test invalid redirect uri
62
    params['response_type'] = 'code'
63
    resp = app.get(url, params=params, status=302)
64
    assert_error_redirect(resp.url, 'invalid_redirect_uri')
65

  
66
    params['redirect_uri'] = 'https://example.com'
67
    resp = app.get(url, params=params)
68

  
69
    assert resp.status_code == 200
70
    assert len(resp.forms[0]['document'].options) == 2
71
    assert 'Baudelaire.txt' in resp.forms[0]['document'].options[1]
72

  
73
    resp.forms[0]['document'].select('1')
74
    resp = resp.forms[0].submit()
75
    assert len(OAuth2Authorize.objects.filter(user_document__user=john_doe)) == 1
76
    auth = OAuth2Authorize.objects.filter(user_document__user=john_doe)[0]
77
    assert resp.status_code == 302
78
    assert 'code' in resp.location
79
    assert auth.code in resp.location
80
    assert 'state' in resp.location
81
    assert 'achipeachope' in resp.location
82

  
83
    params.pop('response_type')
84
    params.pop('state')
85
    params['grant_type'] = 'authorization_code'
86
    params['code'] = auth.code
87

  
88
    url = reverse('oauth2-get-token')
89
    resp = app.post(url, params=params, status=200)
90
    assert 'access_token' in resp.json
91
    assert 'expires' in resp.json
92
    assert resp.json['access_token'] == auth.access_token
93

  
94
    url = reverse('oauth2-get-document')
95
    app.authorization = ('Bearer', str(auth.access_token))
96
    resp = app.get(url, status=200)
97

  
98
    assert resp.content_type == 'application/octet-stream'
99
    assert 'Content-disposition' in resp.headers
100
    content_disposition = resp.content_disposition.replace(' ', '').split(';')
101
    assert content_disposition[0] == 'attachement'
102
    assert content_disposition[1] == 'filename="Baudelaire.txt"'
103
    assert content_disposition[2] == 'filename*=UTF-8\'\'Baudelaire.txt'
104

  
105

  
106
def test_put_document(app, john_doe, oauth2_client):
107
    login(app, user=john_doe)
108
    with open('tests/test_oauth2.txt', 'rb') as f:
109
        data = f.read()
110

  
111
    url = reverse('oauth2-put-document')
112
    resp = app.post(url, params=data, status=400)
113
    assert 'basic HTTP authentication failed' in resp.content
114

  
115
    app.authorization = ('Basic', (str(oauth2_client.client_id), str(oauth2_client.client_secret)))
116
    resp = app.post(url, params=data, status=400)
117
    assert 'missing content-disposition header' in resp.content
118

  
119
    filename = 'Baudelaire.txt'.encode('ascii', 'replace')
120
    percent_encode_filename = quote(filename.encode('utf8'), safe='')
121
    headers = {
122
        'Content-disposition': 'attachement; filename="%s"; filename*=UTF-8\'\'%s' % (filename, percent_encode_filename)
123
    }
124

  
125
    assert len(OAuth2TempFile.objects.all()) == 0
126
    resp = app.post(url, params=data, headers=headers, status=200)
127

  
128
    assert len(OAuth2TempFile.objects.all()) == 1
129
    doc = OAuth2TempFile.objects.all()[0]
130
    location = reverse('oauth2-put-document-authorize', kwargs={'pk': doc.pk})
131
    assert location in resp.location
132

  
133
    app.authorization = None
134
    url = location + '?%s' % urlencode({'redirect_uri': 'https://example.com'})
135
    resp = app.get(url, status=200)
136

  
137
    assert len(UserDocument.objects.all()) == 0
138
    resp = resp.forms[0].submit()
139

  
140
    assert resp.status_code == 302
141
    assert resp.location == 'https://example.com'
142
    try:
143
        user_document = UserDocument.objects.get(user=john_doe, document=doc.document)
144
    except UserDocument.DoesNotExist:
145
        assert False
146

  
147
    assert user_document.filename == 'Baudelaire.txt'
148

  
149

  
150
def test_confirm_put_document_file_exception(app, john_doe, user_doc):
151
    login(app, user=john_doe)
152
    oauth_tmp_file = OAuth2TempFile.objects.create(document=user_doc.document, filename=user_doc.filename)
153

  
154
    url = reverse('oauth2-put-document-authorize', kwargs={'pk': 'fakemofo'})
155
    url += '?%s' % urlencode({'redirect_uri': 'https://example.com'})
156
    resp = app.get(url)
157
    assert 'The document has not been uploaded' in resp.content
158

  
159
    url = reverse('oauth2-put-document-authorize', kwargs={'pk': oauth_tmp_file.pk})
160
    url += '?%s' % urlencode({'redirect_uri': 'https://example.com'})
161
    resp = app.get(url)
162
    assert 'This document is already in your portfolio' in resp.content
tests/test_oauth2.txt
1
Poème hymne à la beauté du recueil les fleurs du mal de Charles Baudelaire
2

  
3
Viens-tu du ciel profond ou sors-tu de l'abîme,
4
Ô Beauté ! ton regard, infernal et divin,
5
Verse confusément le bienfait et le crime,
6
Et l'on peut pour cela te comparer au vin.
7

  
8
Tu contiens dans ton oeil le couchant et l'aurore ;
9
Tu répands des parfums comme un soir orageux ;
10
Tes baisers sont un philtre et ta bouche une amphore
11
Qui font le héros lâche et l'enfant courageux.
12

  
13
Sors-tu du gouffre noir ou descends-tu des astres ?
14
Le Destin charmé suit tes jupons comme un chien ;
15
Tu sèmes au hasard la joie et les désastres,
16
Et tu gouvernes tout et ne réponds de rien.
17

  
18
Tu marches sur des morts, Beauté, dont tu te moques ;
19
De tes bijoux l'Horreur n'est pas le moins charmant,
20
Et le Meurtre, parmi tes plus chères breloques,
21
Sur ton ventre orgueilleux danse amoureusement.
22

  
23
L'éphémère ébloui vole vers toi, chandelle,
24
Crépite, flambe et dit : Bénissons ce flambeau !
25
L'amoureux pantelant incliné sur sa belle
26
A l'air d'un moribond caressant son tombeau.
27

  
28
Que tu viennes du ciel ou de l'enfer, qu'importe,
29
Ô Beauté ! monstre énorme, effrayant, ingénu !
30
Si ton oeil, ton souris, ton pied, m'ouvrent la porte
31
D'un Infini que j'aime et n'ai jamais connu ?
32

  
33
De Satan ou de Dieu, qu'importe ? Ange ou Sirène,
34
Qu'importe, si tu rends, - fée aux yeux de velours,
35
Rythme, parfum, lueur, ô mon unique reine ! -
36
L'univers moins hideux et les instants moins lourds ?
0
-