0001-add-oauth2-access-to-get-and-put-a-document-14147.patch
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 |
- |