Projet

Général

Profil

0001-use-drf-for-oauth2-APIs-16842.patch

Josué Kouka, 19 janvier 2018 19:19

Télécharger (13,7 ko)

Voir les différences:

Subject: [PATCH 1/2] use drf for oauth2 APIs (#16842)

 fargo/oauth2/rest_authentication.py |  91 +++++++++++++++++++
 fargo/oauth2/utils.py               |  44 +++++++++
 fargo/oauth2/views.py               | 174 +++++++++++++-----------------------
 tests/test_oauth2.py                |   2 +-
 4 files changed, 196 insertions(+), 115 deletions(-)
 create mode 100644 fargo/oauth2/rest_authentication.py
 create mode 100644 fargo/oauth2/utils.py
fargo/oauth2/rest_authentication.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 base64
18

  
19
from django.utils.translation import ugettext_lazy as _
20

  
21
from rest_framework.authentication import BaseAuthentication
22
from rest_framework.exceptions import AuthenticationFailed
23

  
24
from .models import OAuth2Client
25

  
26

  
27
class OAuth2User(object):
28
    """ Fake user class to return in case OAuth2 Client authentication
29
    """
30

  
31
    def __init__(self, oauth2_client):
32
        self.oauth2_client = oauth2_client
33
        self.authenticated = False
34

  
35
    def has_perm(self, *args, **kwargs):
36
        return True
37

  
38
    def has_perm_any(self, *args, **kwargs):
39
        return True
40

  
41
    def has_ou_perm(self, *args, **kwargs):
42
        return True
43

  
44
    def filter_by_perm(self, perms, queryset):
45
        return queryset
46

  
47
    def is_authenticated(self):
48
        return self.authenticated
49

  
50
    def is_staff(self):
51
        return False
52

  
53

  
54
class FargoOAUTH2Authentication(BaseAuthentication):
55

  
56
    def authenticate_header(self, request):
57
        return 'basic HTTP authentication failed'
58

  
59
    def authenticate(self, request):
60
        '''Authenticate client on the token endpoint'''
61

  
62
        if 'HTTP_AUTHORIZATION' in request.META:
63
            authorization = request.META['HTTP_AUTHORIZATION'].split()
64
            if authorization[0] != 'Basic' or len(authorization) != 2:
65
                return False
66
            try:
67
                decoded = base64.b64decode(authorization[1])
68
            except TypeError:
69
                return False
70
            parts = decoded.split(':')
71
            if len(parts) != 2:
72
                return False
73
            client_id, client_secret = parts
74
        elif 'client_id' in request.POST:
75
            client_id = request.POST['client_id']
76
            client_secret = request.POST.get('client_secret', '')
77
        else:
78
            raise AuthenticationFailed(self.authenticate_header(request))
79

  
80
        return self.authenticate_credentials(client_id, client_secret)
81

  
82
    def authenticate_credentials(self, client_id, client_secret):
83
        try:
84
            client = OAuth2Client.objects.get(
85
                client_id=client_id, client_secret=client_secret)
86
        except OAuth2Client.DoesNotExist:
87
            raise AuthenticationFailed(_('Invalid client_id/client_secret.'))
88

  
89
        user = OAuth2User(client)
90
        user.authenticated = True
91
        return user, True
fargo/oauth2/utils.py
1
import cgi
2
from urllib import unquote
3

  
4
from .models import OAuth2Authorize
5

  
6

  
7
def authenticate_bearer(request):
8
    authorization = request.META.get('HTTP_AUTHORIZATION')
9
    if not authorization:
10
        return False
11
    splitted = authorization.split()
12
    if len(splitted) < 2:
13
        return False
14
    if splitted[0] != 'Bearer':
15
        return False
16
    token = splitted[1]
17
    try:
18
        return OAuth2Authorize.objects.get(access_token=token)
19
    except OAuth2Authorize.DoesNotExist:
20
        return False
21

  
22

  
23
def get_content_disposition_value(request):
24
    if 'HTTP_CONTENT_DISPOSITION' not in request.META:
25
        return None, 'missing content-disposition header'
26
    content_header = request.META['HTTP_CONTENT_DISPOSITION']
27
    disposition_type, filename = cgi.parse_header(content_header)
28
    if disposition_type != 'attachement':
29
        return None, 'wrong disposition type: attachement excpected'
30
    if 'filename*' in filename:
31
        encode, country, name = filename['filename*'].split("'")
32

  
33
        # check accepted charset from rfc 5987
34
        if encode == 'UTF-8':
35
            return unquote(name.decode('utf8')), None
36
        elif encode == 'ISO-8859-1':
37
            return unquote(name.decode('iso-8859-1')), None
38
        else:
39
            return None, 'unknown encoding: UTF-8 or ISO-8859-1 allowed'
40
    elif 'filename' in filename:
41
        return filename['filename'], None
42
    else:
43
        # no filename in header
44
        return None, 'missing filename(*) parameter in header'
fargo/oauth2/views.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

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

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

  
27
from rest_framework import serializers
28
from rest_framework.response import Response
29
from rest_framework.views import APIView
30

  
29 31
from .forms import OAuth2AuthorizeForm
30 32
from .models import OAuth2Authorize, OAuth2Client, OAuth2TempFile
33
from .utils import authenticate_bearer, get_content_disposition_value
31 34

  
32 35
from fargo.fargo.models import UserDocument, Document
36
from fargo.oauth2.rest_authentication import FargoOAUTH2Authentication
33 37

  
34 38

  
35 39
class OAuth2Exception(Exception):
36 40
    pass
37 41

  
38 42

  
43
class OAuth2ClientSerializer(serializers.ModelSerializer):
44

  
45
    class Meta:
46
        model = OAuth2Client
47
        fields = ('client_id', 'client_secret')
48

  
49

  
50
class OAUTH2ClientApiViewMixin(APIView):
51
    http_method_names = ['post']
52
    authentication_classes = (FargoOAUTH2Authentication,)
53

  
54
    @csrf_exempt
55
    def dispatch(self, request, *args, **kwargs):
56
        return super(OAUTH2ClientApiViewMixin, self).dispatch(request, *args, **kwargs)
57

  
58

  
39 59
class OAuth2AuthorizeView(FormView):
40 60
    template_name = 'oauth2/authorize.html'
41 61
    form_class = OAuth2AuthorizeForm
......
99 119
        return super(OAuth2AuthorizeView, self).form_valid(form)
100 120

  
101 121

  
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)
122
class GetDocumentTokenView(OAUTH2ClientApiViewMixin):
123

  
124
    def post(self, request, *args, **kwargs):
125
        client = request.user.oauth2_client
126
        data = request.data
127
        if not client.check_redirect_uri(data['redirect_uri']):
128
            return Response({'error': 'invalid_request'}, status=400)
129
        if data['grant_type'] != 'authorization_code':
130
            return Response({'error': 'unsopported_grant_type'}, status=400)
131

  
132
        try:
133
            token = OAuth2Authorize.objects.get(code=data['code']).access_token
134
        except OAuth2Authorize.DoesNotExist:
135
            return Response({'error': 'invalid_request'}, status=400)
108 136

  
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)
137
        return Response({'access_token': token, 'expires': '3600'})
118 138

  
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 139

  
124
    return JsonResponse({'access_token': doc_token, 'expires': '3600'})
140
get_document_token = GetDocumentTokenView.as_view()
125 141

  
126 142

  
127 143
def get_document(request):
......
140 156
    return response
141 157

  
142 158

  
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
159
class PutDocumentAPIView(OAUTH2ClientApiViewMixin):
160

  
161
    def post(self, request, *args, **kwargs):
162
        filename, error = get_content_disposition_value(request)
163
        if error:
164
            return HttpResponseBadRequest(error)
165

  
166
        f = ContentFile(request.body, name=filename)
167
        document = Document.objects.get_by_file(f)
168
        oauth2_document = OAuth2TempFile.objects.create(document=document,
169
                                                        filename=filename)
170
        uri = reverse('oauth2-put-document-authorize',
171
                      args=[oauth2_document.pk]) + '/'
172

  
173
        response = Response()
174
        response['Location'] = uri
175
        return response
176

  
177

  
178
put_document = PutDocumentAPIView.as_view()
233 179

  
234 180

  
235 181
class OAuth2AuthorizePutView(TemplateView):
tests/test_oauth2.py
109 109
        data = f.read()
110 110

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

  
115 115
    app.authorization = ('Basic', (str(oauth2_client.client_id), str(oauth2_client.client_secret)))
116
-