280 lines
10 KiB
Python
280 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.conf import settings
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.core.files.base import ContentFile
|
|
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
|
|
from django.shortcuts import get_object_or_404
|
|
from django.urls import reverse
|
|
from django.utils.http import quote
|
|
from django.utils.timezone import now
|
|
from django.utils.translation import ugettext as _
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.views.generic import FormView, TemplateView, View
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
from fargo.fargo.models import Document, UserDocument
|
|
from fargo.utils import make_url
|
|
|
|
from .authentication import FargoOAUTH2Authentication
|
|
from .forms import OAuth2AuthorizeForm
|
|
from .models import OAuth2Authorize, OAuth2Client, OAuth2TempFile
|
|
from .utils import authenticate_bearer, get_content_disposition_value
|
|
|
|
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().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().dispatch(request)
|
|
|
|
def post(self, request):
|
|
if 'cancel' in request.POST:
|
|
return self.redirect(error='access_denied')
|
|
return super().post(request)
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().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(
|
|
'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().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(
|
|
'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(
|
|
'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(
|
|
'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().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().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(
|
|
'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())
|