# 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 . 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())