diff --git a/HISTORY.rst b/HISTORY.rst index 519934ee99..f3c6035566 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -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) ================== diff --git a/mayan/apps/document_signatures/api_views.py b/mayan/apps/document_signatures/api_views.py index 255b8c6540..6762a85e6c 100644 --- a/mayan/apps/document_signatures/api_views.py +++ b/mayan/apps/document_signatures/api_views.py @@ -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(), diff --git a/mayan/apps/document_signatures/models.py b/mayan/apps/document_signatures/models.py index 32ddcea334..ab5a77228e 100644 --- a/mayan/apps/document_signatures/models.py +++ b/mayan/apps/document_signatures/models.py @@ -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') ) diff --git a/mayan/apps/document_signatures/serializers.py b/mayan/apps/document_signatures/serializers.py index dfc7b8cd88..103e113b47 100644 --- a/mayan/apps/document_signatures/serializers.py +++ b/mayan/apps/document_signatures/serializers.py @@ -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 + ) diff --git a/mayan/apps/document_signatures/tests/mixins.py b/mayan/apps/document_signatures/tests/mixins.py index c40908f34a..48b152a324 100644 --- a/mayan/apps/document_signatures/tests/mixins.py +++ b/mayan/apps/document_signatures/tests/mixins.py @@ -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): diff --git a/mayan/apps/document_signatures/tests/test_api.py b/mayan/apps/document_signatures/tests/test_api.py index ce7af72057..aa36b08c51 100644 --- a/mayan/apps/document_signatures/tests/test_api.py +++ b/mayan/apps/document_signatures/tests/test_api.py @@ -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(), diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index aec4ed0ccf..547a614200 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -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[0-9]+)/versions/(?P[0-9]+)/signatures/detached/sign/$', + view=APIDocumentSignDetachedView.as_view(), + name='document-version-signature-detached-sign' + ), url( regex=r'^documents/(?P[0-9]+)/versions/(?P[0-9]+)/signatures/detached/(?P[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[0-9]+)/versions/(?P[0-9]+)/signatures/embedded/sign/$', + view=APIDocumentSignEmbeddedView.as_view(), + name='document-version-signature-embedded-sign' + ), url( regex=r'^documents/(?P[0-9]+)/versions/(?P[0-9]+)/signatures/embedded/(?P[0-9]+)/$', view=APIDocumentEmbeddedSignatureView.as_view(),