fargo/fargo/oauth2/views.py

267 lines
10 KiB
Python

# fargo - document box
# Copyright (C) 2016-2019 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 logging
from django.shortcuts import get_object_or_404
from django.utils.http import quote
from django.utils.translation import ugettext as _
from django.utils.timezone import now
from django.core.files.base import ContentFile
from django.core.urlresolvers import reverse
from django.http import (HttpResponse, HttpResponseBadRequest,
HttpResponseRedirect)
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import FormView, TemplateView, View
from django.contrib.auth.decorators import login_required
from django.conf import settings
from rest_framework.response import Response
from rest_framework.views import APIView
from .authentication import FargoOAUTH2Authentication
from .forms import OAuth2AuthorizeForm
from .models import OAuth2Authorize, OAuth2Client, OAuth2TempFile
from .utils import authenticate_bearer, get_content_disposition_value
from fargo.fargo.models import UserDocument, Document
from fargo.utils import make_url
logger = logging.getLogger(__name__)
class OAuth2Exception(Exception):
pass
class OAUTH2APIViewMixin(APIView):
http_method_names = ['post']
authentication_classes = (FargoOAUTH2Authentication,)
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
return super(OAUTH2APIViewMixin, self).dispatch(request, *args, **kwargs)
class OAuth2AuthorizeView(FormView):
template_name = 'fargo/oauth2/authorize.html'
form_class = OAuth2AuthorizeForm
success_url = '/'
def redirect(self, **kwargs):
'''Return to requester'''
return HttpResponseRedirect(make_url(self.redirect_uri, **kwargs))
def dispatch(self, request):
self.redirect_uri = request.GET.get('redirect_uri')
if not self.redirect_uri:
return HttpResponseBadRequest('missing redirect_uri parameter')
client_id = request.GET.get('client_id')
response_type = request.GET.get('response_type')
if not client_id or not response_type:
return self.redirect(error='invalid_request')
if response_type != 'code':
return self.redirect(error='unsupported_response_type')
try:
self.client = OAuth2Client.objects.get(client_id=client_id)
if not self.client.check_redirect_uri(self.redirect_uri):
return self.redirect(error='invalid_redirect_uri')
except OAuth2Client.DoesNotExist:
return self.redirect(error='unauthorized_client')
self.state = request.GET.get('state', None)
return super(OAuth2AuthorizeView, self).dispatch(request)
def post(self, request):
if 'cancel' in request.POST:
return self.redirect(error='access_denied')
return super(OAuth2AuthorizeView, self).post(request)
def get_form_kwargs(self):
kwargs = super(OAuth2AuthorizeView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
document = form.cleaned_data['document']
authorization = OAuth2Authorize.objects.create(client=self.client, user_document=document)
logger.info(u'user %s authorized client "%s" to get document "%s" (%s) with code "%s"',
self.request.user,
self.client,
document,
document.pk,
authorization.code)
return self.redirect(code=authorization.code, state=self.state)
def get_context_data(self, **kwargs):
kwargs['oauth2_client'] = self.client
return super(OAuth2AuthorizeView , self).get_context_data(**kwargs)
authorize_get_document = login_required(OAuth2AuthorizeView.as_view())
class GetDocumentTokenView(OAUTH2APIViewMixin):
def error(self, error, description=None):
data = {
'error': error,
}
if description:
data['error_description'] = description
return Response(data, status=400)
def post(self, request):
if request.data['grant_type'] != 'authorization_code':
return self.error('unsupported_grant_type')
try:
authorize = OAuth2Authorize.objects.get(code=request.data['code'])
except OAuth2Authorize.DoesNotExist:
return self.error('invalid_grant', 'code is unknown')
if (now() - authorize.creation_date).total_seconds() > settings.FARGO_CODE_LIFETIME:
return self.error('invalid_grant', 'code is expired')
logger.info(u'client "%s" resolved code "%s" to access token "%s"',
request.user.oauth2_client,
authorize.code,
authorize.access_token)
return Response({
'access_token': authorize.access_token,
'expires': settings.FARGO_ACCESS_TOKEN_LIFETIME
})
get_document_token = GetDocumentTokenView.as_view()
def document_response(user_document):
response = HttpResponse(content=user_document.document.content.chunks(), status=200,
content_type='application/octet-stream')
filename = user_document.filename
ascii_filename = filename.encode('ascii', 'replace').decode()
percent_encoded_filename = quote(filename.encode('utf8'), safe='')
response['Content-Disposition'] = 'attachment; filename="%s"; filename*=UTF-8\'\'%s' % (ascii_filename,
percent_encoded_filename)
return response
def get_document(request):
oauth_authorize = authenticate_bearer(request)
if not oauth_authorize:
return HttpResponseBadRequest('http bearer authentication failed: invalid authorization header')
user_document = oauth_authorize.user_document
logger.info(u'client "%s" retrieved document "%s" (%s) with access token "%s"',
oauth_authorize.client,
user_document,
user_document.pk,
oauth_authorize.access_token)
return document_response(user_document)
class PutDocumentAPIView(OAUTH2APIViewMixin):
def post(self, request, *args, **kwargs):
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(
client=request.user.oauth2_client,
document=document,
filename=filename)
uri = reverse('oauth2-put-document-authorize', args=[oauth2_document.pk])
response = Response()
response['Location'] = uri
logger.info(u'client "%s" uploaded document "%s" (%s)',
request.user.oauth2_client,
filename,
oauth2_document.pk)
return response
put_document = PutDocumentAPIView.as_view()
class OAuth2AuthorizePutView(TemplateView):
template_name = 'fargo/oauth2/confirm.html'
def redirect(self, **kwargs):
'''Return to requester'''
return HttpResponseRedirect(make_url(self.redirect_uri, **kwargs))
def dispatch(self, request, *args, **kwargs):
self.redirect_uri = request.GET.get('redirect_uri', '')
if not self.redirect_uri:
return HttpResponseBadRequest('missing redirect_uri parameter')
self.oauth2_document = OAuth2TempFile.objects.filter(pk=kwargs['pk']).first()
return super(OAuth2AuthorizePutView, self).dispatch(request)
def get_context_data(self, **kwargs):
if self.oauth2_document:
kwargs['oauth2_document'] = self.oauth2_document
kwargs['filename'] = self.oauth2_document.filename
kwargs['thumbnail_image'] = self.oauth2_document.document.thumbnail_image
kwargs['oauth2_client'] = self.oauth2_document.client
kwargs['download_url'] = reverse('oauth2-put-document-download', kwargs={'pk': self.oauth2_document.pk})
# verify if document already exists
if not UserDocument.objects.filter(
user=self.request.user,
document=self.oauth2_document.document).exists():
kwargs['error_message'] = ''
else:
kwargs['error_message'] = _('This document is already in your portfolio')
kwargs['redirect_uri'] = self.request.GET['redirect_uri']
else:
kwargs['error_message'] = _('The document has not been uploaded')
kwargs['redirect_uri'] = self.request.GET['redirect_uri']
return super(OAuth2AuthorizePutView, self).get_context_data(**kwargs)
def post(self, request):
if not self.oauth2_document:
return self.get(request)
try:
if 'cancel' in request.POST:
return self.redirect(error='access_denied')
UserDocument.objects.create(
user=request.user,
document=self.oauth2_document.document,
filename=self.oauth2_document.filename)
logger.info(u'user %s accepted document "%s" (%s) from client "%s"',
request.user,
self.oauth2_document.filename,
self.oauth2_document.pk,
self.oauth2_document.client)
return self.redirect()
finally:
self.oauth2_document.delete()
authorize_put_document = login_required(OAuth2AuthorizePutView.as_view())
class DownloadPutDocument(View):
def get(self, request, *args, **kwargs):
oauth2_document = get_object_or_404(OAuth2TempFile, pk=kwargs['pk'])
return document_response(oauth2_document)
download_put_document = login_required(DownloadPutDocument.as_view())