Update signature API to support uploads

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
This commit is contained in:
Roberto Rosario
2019-11-05 01:36:00 -04:00
parent 7d4f11b74b
commit f73dd28c92
7 changed files with 413 additions and 153 deletions

View File

@@ -110,8 +110,7 @@
own module.
- Update label and icon of the document sign form
Label updated from "Save" to "Sign".
- Add list, create, detail and edit API views for
detached and embedded signatures.
- Document signatures API views.
3.2.9 (2019-11-03)
==================

View File

@@ -2,6 +2,9 @@ from __future__ import absolute_import, unicode_literals
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.response import Response
from mayan.apps.acls.models import AccessControlList
from mayan.apps.documents.models import Document
from mayan.apps.rest_api import generics
@@ -11,13 +14,153 @@ from .permissions import (
permission_document_version_sign_detached,
permission_document_version_sign_embedded,
permission_document_version_signature_delete,
permission_document_version_signature_upload,
permission_document_version_signature_view
)
from .serializers import (
DetachedSignatureSerializer, EmbeddedSignatureSerializer
DetachedSignatureSerializer, EmbeddedSignatureSerializer,
SignDetachedSerializer, SignEmbeddedSerializer
)
class APIDocumentSignDetachedView(generics.GenericAPIView):
"""
post: Sign a document version with a detached signature.
"""
serializer_class = SignDetachedSerializer
def get_document(self):
return get_object_or_404(
klass=self.get_document_queryset(), pk=self.kwargs['document_id']
)
def get_document_queryset(self):
if self.request.method == 'POST':
permission = permission_document_version_sign_detached
return AccessControlList.objects.restrict_queryset(
permission=permission, queryset=Document.objects.all(),
user=self.request.user
)
def get_document_version(self):
return get_object_or_404(
klass=self.get_document_version_queryset(),
pk=self.kwargs['document_version_id']
)
def get_document_version_queryset(self):
return self.get_document().versions.all()
def get_queryset(self):
return DetachedSignature.objects.filter(
document_version=self.get_document_version()
)
def get_serializer(self, *args, **kwargs):
if not self.request:
return None
return super(
APIDocumentSignDetachedView, self
).get_serializer(*args, **kwargs)
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
context = super(
APIDocumentSignDetachedView, self
).get_serializer_context()
if self.kwargs:
context.update(
{
'document_version': self.get_document_version(),
}
)
return context
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.sign(
key_id=request.data['key_id'],
passphrase=request.data['passphrase']
)
return Response(status=status.HTTP_200_OK)
class APIDocumentSignEmbeddedView(generics.GenericAPIView):
"""
post: Sign a document version with an embedded signature.
"""
serializer_class = SignEmbeddedSerializer
def get_document(self):
return get_object_or_404(
klass=self.get_document_queryset(), pk=self.kwargs['document_id']
)
def get_document_queryset(self):
if self.request.method == 'POST':
permission = permission_document_version_sign_embedded
return AccessControlList.objects.restrict_queryset(
permission=permission, queryset=Document.objects.all(),
user=self.request.user
)
def get_document_version(self):
return get_object_or_404(
klass=self.get_document_version_queryset(),
pk=self.kwargs['document_version_id']
)
def get_document_version_queryset(self):
return self.get_document().versions.all()
def get_queryset(self):
return DetachedSignature.objects.filter(
document_version=self.get_document_version()
)
def get_serializer(self, *args, **kwargs):
if not self.request:
return None
return super(
APIDocumentSignEmbeddedView, self
).get_serializer(*args, **kwargs)
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
context = super(
APIDocumentSignEmbeddedView, self
).get_serializer_context()
if self.kwargs:
context.update(
{
'document_version': self.get_document_version(),
}
)
return context
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.sign(
key_id=request.data['key_id'],
passphrase=request.data['passphrase']
)
return Response(status=status.HTTP_200_OK)
class APIDocumentDetachedSignatureListView(generics.ListCreateAPIView):
"""
get: Returns a list of all the detached signatures of a document version.
@@ -34,7 +177,7 @@ class APIDocumentDetachedSignatureListView(generics.ListCreateAPIView):
if self.request.method == 'GET':
permission = permission_document_version_signature_view
elif self.request.method == 'POST':
permission = permission_document_version_sign_detached
permission = permission_document_version_signature_upload
return AccessControlList.objects.restrict_queryset(
permission=permission, queryset=Document.objects.all(),
@@ -135,10 +278,9 @@ class APIDocumentDetachedSignatureView(generics.RetrieveDestroyAPIView):
return context
class APIDocumentEmbeddedSignatureListView(generics.ListCreateAPIView):
class APIDocumentEmbeddedSignatureListView(generics.ListAPIView):
"""
get: Returns a list of all the embedded signatures of a document version.
post: Create an embedded signature for a document version.
"""
serializer_class = EmbeddedSignatureSerializer
@@ -150,8 +292,6 @@ class APIDocumentEmbeddedSignatureListView(generics.ListCreateAPIView):
def get_document_queryset(self):
if self.request.method == 'GET':
permission = permission_document_version_signature_view
elif self.request.method == 'POST':
permission = permission_document_version_sign_embedded
return AccessControlList.objects.restrict_queryset(
permission=permission, queryset=Document.objects.all(),

View File

@@ -43,7 +43,10 @@ class SignatureBaseModel(models.Model):
date = models.DateField(
blank=True, editable=False, null=True, verbose_name=_('Date signed')
)
key_id = models.CharField(max_length=40, verbose_name=_('Key ID'))
key_id = models.CharField(
help_text=_('ID of the key that will be used to sign the document.'),
max_length=40, verbose_name=_('Key ID')
)
# With proper key
signature_id = models.CharField(
blank=True, editable=False, null=True, max_length=64,
@@ -128,7 +131,9 @@ class EmbeddedSignature(SignatureBaseModel):
@python_2_unicode_compatible
class DetachedSignature(SignatureBaseModel):
signature_file = models.FileField(
blank=True, null=True, storage=storage_detachedsignature,
blank=True, help_text=_(
'Signature file previously generated.'
), null=True, storage=storage_detachedsignature,
upload_to=upload_to, verbose_name=_('Signature file')
)

View File

@@ -1,5 +1,7 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -11,7 +13,7 @@ from mayan.apps.rest_api.relations import MultiKwargHyperlinkedIdentityField
from .models import DetachedSignature, EmbeddedSignature
class DetachedSignatureSerializer(serializers.HyperlinkedModelSerializer):
class BaseSignatureSerializer(serializers.HyperlinkedModelSerializer):
document_version_url = MultiKwargHyperlinkedIdentityField(
view_kwargs=(
{
@@ -26,6 +28,18 @@ class DetachedSignatureSerializer(serializers.HyperlinkedModelSerializer):
view_name='rest_api:documentversion-detail'
)
class BaseSignSerializer(serializers.HyperlinkedModelSerializer):
passphrase = serializers.CharField(
help_text=_(
'The passphrase to unlock the key and allow it to be used to '
'sign the document version.'
),
required=False, write_only=True
)
class DetachedSignatureSerializer(BaseSignatureSerializer):
url = MultiKwargHyperlinkedIdentityField(
view_kwargs=(
{
@@ -43,38 +57,22 @@ class DetachedSignatureSerializer(serializers.HyperlinkedModelSerializer):
),
view_name='rest_api:detachedsignature-detail'
)
passphrase = serializers.CharField(required=False, write_only=True)
class Meta:
extra_kwargs = {
'signature_file': {'write_only': True},
}
fields = (
'date', 'document_version_url', 'key_id', 'signature_id',
'passphrase', 'public_key_fingerprint', 'url'
'date', 'document_version_url', 'key_id', 'signature_file',
'signature_id', 'public_key_fingerprint', 'url'
)
model = DetachedSignature
read_only_fields = ('key_id',)
def create(self, validated_data):
key_id = validated_data.pop('key_id')
passphrase = validated_data.pop('passphrase', None)
key_queryset = AccessControlList.objects.restrict_queryset(
permission=permission_key_sign, queryset=Key.objects.all(),
user=self.context['request'].user
)
try:
key = key_queryset.get(fingerprint__endswith=key_id)
except Key.DoesNotExist:
raise ValidationError(
{
'key_id': [
'Key "{}" not found.'.format(key_id)
]
}, code='invalid'
)
return DetachedSignature.objects.sign_document_version(
document_version=self.context['document_version'], key=key,
passphrase=passphrase, user=self.context['request'].user
validated_data['document_version'] = self.context['document_version']
return super(DetachedSignatureSerializer, self).create(
validated_data=validated_data
)
@@ -145,3 +143,65 @@ class EmbeddedSignatureSerializer(serializers.HyperlinkedModelSerializer):
)
return signature
class SignDetachedSerializer(BaseSignatureSerializer, BaseSignSerializer):
class Meta:
fields = (
'date', 'document_version_url', 'key_id', 'signature_id',
'passphrase', 'public_key_fingerprint', 'url'
)
model = DetachedSignature
def sign(self, key_id, passphrase):
key_queryset = AccessControlList.objects.restrict_queryset(
permission=permission_key_sign, queryset=Key.objects.all(),
user=self.context['request'].user
)
try:
key = key_queryset.get(fingerprint__endswith=key_id)
except Key.DoesNotExist:
raise ValidationError(
{
'key_id': [
'Key "{}" not found.'.format(key_id)
]
}, code='invalid'
)
return DetachedSignature.objects.sign_document_version(
document_version=self.context['document_version'], key=key,
passphrase=passphrase, user=self.context['request'].user
)
class SignEmbeddedSerializer(SignDetachedSerializer):
class Meta:
fields = (
'date', 'document_version_url', 'key_id', 'signature_id',
'passphrase', 'public_key_fingerprint', 'url'
)
model = EmbeddedSignature
def sign(self, key_id, passphrase):
key_queryset = AccessControlList.objects.restrict_queryset(
permission=permission_key_sign, queryset=Key.objects.all(),
user=self.context['request'].user
)
try:
key = key_queryset.get(fingerprint__endswith=key_id)
except Key.DoesNotExist:
raise ValidationError(
{
'key_id': [
'Key "{}" not found.'.format(key_id)
]
}, code='invalid'
)
return EmbeddedSignature.objects.sign_document_version(
document_version=self.context['document_version'], key=key,
passphrase=passphrase, user=self.context['request'].user
)

View File

@@ -12,16 +12,14 @@ from .literals import TEST_KEY_FILE_PATH, TEST_SIGNATURE_FILE_PATH
class DetachedSignatureAPIViewTestMixin(object):
def _request_test_document_signature_detached_create_view(self):
return self.post(
viewname='rest_api:document-version-signature-detached-list',
kwargs={
'document_id': self.test_document.pk,
'document_version_id': self.test_document_version.pk
}, data={
'key_id': self.test_key_private.key_id,
'passphrase': TEST_KEY_PRIVATE_PASSPHRASE
}
)
with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object:
return self.post(
viewname='rest_api:document-version-signature-detached-list',
kwargs={
'document_id': self.test_document.pk,
'document_version_id': self.test_document_version.pk
}, data={'signature_file': file_object}
)
def _request_test_document_signature_detached_delete_view(self):
return self.delete(
@@ -52,6 +50,18 @@ class DetachedSignatureAPIViewTestMixin(object):
}
)
def _request_test_document_signature_detached_sign_view(self):
return self.post(
viewname='rest_api:document-version-signature-detached-sign',
kwargs={
'document_id': self.test_document.pk,
'document_version_id': self.test_document_version.pk
}, data={
'key_id': self.test_key_private.key_id,
'passphrase': TEST_KEY_PRIVATE_PASSPHRASE
}
)
class DetachedSignatureViewTestMixin(object):
def _request_test_document_version_signature_download_view(self):
@@ -70,18 +80,6 @@ class DetachedSignatureViewTestMixin(object):
class EmbeddedSignatureAPIViewTestMixin(object):
def _request_test_document_signature_embedded_create_view(self):
return self.post(
viewname='rest_api:document-version-signature-embedded-list',
kwargs={
'document_id': self.test_document.pk,
'document_version_id': self.test_document_version.pk
}, data={
'key_id': self.test_key_private.key_id,
'passphrase': TEST_KEY_PRIVATE_PASSPHRASE
}
)
def _request_test_document_signature_embedded_detail_view(self):
return self.get(
viewname='rest_api:embeddedsignature-detail',
@@ -101,6 +99,18 @@ class EmbeddedSignatureAPIViewTestMixin(object):
}
)
def _request_test_document_signature_embedded_sign_view(self):
return self.post(
viewname='rest_api:document-version-signature-embedded-sign',
kwargs={
'document_id': self.test_document.pk,
'document_version_id': self.test_document_version.pk
}, data={
'key_id': self.test_key_private.key_id,
'passphrase': TEST_KEY_PRIVATE_PASSPHRASE
}
)
class SignatureTestMixin(object):
def _create_test_detached_signature(self):

View File

@@ -11,7 +11,8 @@ from ..permissions import (
permission_document_version_sign_detached,
permission_document_version_sign_embedded,
permission_document_version_signature_delete,
permission_document_version_signature_view
permission_document_version_signature_view,
permission_document_version_signature_upload
)
from .literals import TEST_KEY_PUBLIC_ID, TEST_SIGNED_DOCUMENT_PATH
@@ -27,6 +28,39 @@ class DetachedSignatureDocumentAPIViewTestCase(
):
auto_upload_document = False
def test_document_signature_detached_create_view_no_permission(self):
self.upload_document()
self._create_test_key_private()
signatures = self.test_document.latest_version.signatures.count()
response = self._request_test_document_signature_detached_create_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(
self.test_document.latest_version.signatures.count(),
signatures
)
def test_document_signature_detached_create_view_with_access(self):
self.upload_document()
self._create_test_key_private()
signatures = self.test_document.latest_version.signatures.count()
self.grant_access(
obj=self.test_document,
permission=permission_document_version_signature_upload
)
response = self._request_test_document_signature_detached_create_view()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(
self.test_document.latest_version.signatures.count(),
signatures + 1
)
def test_document_signature_detached_delete_no_permission(self):
self.upload_document()
self._create_test_detached_signature()
@@ -83,81 +117,6 @@ class DetachedSignatureDocumentAPIViewTestCase(
response.data['key_id'], TEST_KEY_PUBLIC_ID
)
def test_document_signature_detached_create_view_no_permission(self):
self.upload_document()
self._create_test_key_private()
signatures = self.test_document.latest_version.signatures.count()
response = self._request_test_document_signature_detached_create_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(
self.test_document.latest_version.signatures.count(),
signatures
)
def test_document_signature_detached_create_view_with_document_access(self):
self.upload_document()
self._create_test_key_private()
signatures = self.test_document.latest_version.signatures.count()
self.grant_access(
obj=self.test_document,
permission=permission_document_version_sign_detached
)
response = self._request_test_document_signature_detached_create_view()
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
self.test_document.latest_version.signatures.count(),
signatures
)
def test_document_signature_detached_create_view_with_key_access(self):
self.upload_document()
self._create_test_key_private()
signatures = self.test_document.latest_version.signatures.count()
self.grant_access(
obj=self.test_key_private,
permission=permission_key_sign
)
response = self._request_test_document_signature_detached_create_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(
self.test_document.latest_version.signatures.count(),
signatures
)
def test_document_signature_detached_create_view_with_full_access(self):
self.upload_document()
self._create_test_key_private()
signatures = self.test_document.latest_version.signatures.count()
self.grant_access(
obj=self.test_document,
permission=permission_document_version_sign_detached
)
self.grant_access(
obj=self.test_key_private,
permission=permission_key_sign
)
response = self._request_test_document_signature_detached_create_view()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(
self.test_document.latest_version.signatures.count(),
signatures + 1
)
def test_document_signature_detached_list_view_no_permission(self):
self.upload_document()
self._create_test_detached_signature()
@@ -180,20 +139,14 @@ class DetachedSignatureDocumentAPIViewTestCase(
response.data['results'][0]['key_id'], TEST_KEY_PUBLIC_ID
)
class EmbeddedSignatureDocumentAPIViewTestCase(
DocumentTestMixin, EmbeddedSignatureAPIViewTestMixin,
KeyTestMixin, BaseAPITestCase
):
auto_upload_document = False
def test_document_signature_embedded_create_view_no_permission(self):
##
def test_document_signature_detached_sign_view_with_no_permission(self):
self.upload_document()
self._create_test_key_private()
signatures = self.test_document.latest_version.signatures.count()
response = self._request_test_document_signature_embedded_create_view()
response = self._request_test_document_signature_detached_sign_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(
@@ -201,7 +154,7 @@ class EmbeddedSignatureDocumentAPIViewTestCase(
signatures
)
def test_document_signature_embedded_create_view_with_document_access(self):
def test_document_signature_detached_sign_view_with_document_access(self):
self.upload_document()
self._create_test_key_private()
@@ -209,10 +162,10 @@ class EmbeddedSignatureDocumentAPIViewTestCase(
self.grant_access(
obj=self.test_document,
permission=permission_document_version_sign_embedded
permission=permission_document_version_sign_detached
)
response = self._request_test_document_signature_embedded_create_view()
response = self._request_test_document_signature_detached_sign_view()
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
@@ -220,7 +173,7 @@ class EmbeddedSignatureDocumentAPIViewTestCase(
signatures
)
def test_document_signature_embedded_create_view_with_key_access(self):
def test_document_signature_detached_sign_view_with_key_access(self):
self.upload_document()
self._create_test_key_private()
@@ -231,7 +184,7 @@ class EmbeddedSignatureDocumentAPIViewTestCase(
permission=permission_key_sign
)
response = self._request_test_document_signature_embedded_create_view()
response = self._request_test_document_signature_detached_sign_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(
@@ -239,7 +192,89 @@ class EmbeddedSignatureDocumentAPIViewTestCase(
signatures
)
def test_document_signature_embedded_create_view_with_full_access(self):
def test_document_signature_detached_sign_view_with_full_access(self):
self.upload_document()
self._create_test_key_private()
signatures = self.test_document.latest_version.signatures.count()
self.grant_access(
obj=self.test_document,
permission=permission_document_version_sign_detached
)
self.grant_access(
obj=self.test_key_private,
permission=permission_key_sign
)
response = self._request_test_document_signature_detached_sign_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
self.test_document.latest_version.signatures.count(),
signatures + 1
)
class EmbeddedSignatureDocumentAPIViewTestCase(
DocumentTestMixin, EmbeddedSignatureAPIViewTestMixin,
KeyTestMixin, BaseAPITestCase
):
auto_upload_document = False
def test_document_signature_embedded_sign_view_with_no_permission(self):
self.upload_document()
self._create_test_key_private()
signatures = self.test_document.latest_version.signatures.count()
response = self._request_test_document_signature_embedded_sign_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(
self.test_document.latest_version.signatures.count(),
signatures
)
def test_document_signature_embedded_sign_view_with_document_access(self):
self.upload_document()
self._create_test_key_private()
signatures = self.test_document.latest_version.signatures.count()
self.grant_access(
obj=self.test_document,
permission=permission_document_version_sign_embedded
)
response = self._request_test_document_signature_embedded_sign_view()
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
self.test_document.latest_version.signatures.count(),
signatures
)
def test_document_signature_embedded_sign_view_with_key_access(self):
self.upload_document()
self._create_test_key_private()
signatures = self.test_document.latest_version.signatures.count()
self.grant_access(
obj=self.test_key_private,
permission=permission_key_sign
)
response = self._request_test_document_signature_embedded_sign_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(
self.test_document.latest_version.signatures.count(),
signatures
)
def test_document_signature_embedded_sign_view_with_full_access(self):
self.upload_document()
self._create_test_key_private()
@@ -254,8 +289,8 @@ class EmbeddedSignatureDocumentAPIViewTestCase(
permission=permission_key_sign
)
response = self._request_test_document_signature_embedded_create_view()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self._request_test_document_signature_embedded_sign_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
self.test_document.latest_version.signatures.count(),

View File

@@ -3,8 +3,9 @@ from __future__ import unicode_literals
from django.conf.urls import url
from .api_views import (
APIDocumentDetachedSignatureListView, APIDocumentDetachedSignatureView,
APIDocumentEmbeddedSignatureListView, APIDocumentEmbeddedSignatureView
APIDocumentSignDetachedView, APIDocumentDetachedSignatureListView,
APIDocumentDetachedSignatureView, APIDocumentEmbeddedSignatureListView,
APIDocumentEmbeddedSignatureView, APIDocumentSignEmbeddedView
)
from .views import (
@@ -64,6 +65,11 @@ api_urls = [
view=APIDocumentDetachedSignatureListView.as_view(),
name='document-version-signature-detached-list'
),
url(
regex=r'^documents/(?P<document_id>[0-9]+)/versions/(?P<document_version_id>[0-9]+)/signatures/detached/sign/$',
view=APIDocumentSignDetachedView.as_view(),
name='document-version-signature-detached-sign'
),
url(
regex=r'^documents/(?P<document_id>[0-9]+)/versions/(?P<document_version_id>[0-9]+)/signatures/detached/(?P<detached_signature_id>[0-9]+)/$',
view=APIDocumentDetachedSignatureView.as_view(),
@@ -74,6 +80,11 @@ api_urls = [
view=APIDocumentEmbeddedSignatureListView.as_view(),
name='document-version-signature-embedded-list'
),
url(
regex=r'^documents/(?P<document_id>[0-9]+)/versions/(?P<document_version_id>[0-9]+)/signatures/embedded/sign/$',
view=APIDocumentSignEmbeddedView.as_view(),
name='document-version-signature-embedded-sign'
),
url(
regex=r'^documents/(?P<document_id>[0-9]+)/versions/(?P<document_version_id>[0-9]+)/signatures/embedded/(?P<embedded_signature_id>[0-9]+)/$',
view=APIDocumentEmbeddedSignatureView.as_view(),