diff --git a/HISTORY.rst b/HISTORY.rst index b5a0e194f6..c03c33009a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -28,6 +28,7 @@ - Handle unicode filenames in staging folders. - Add staging file deletion permission. - New document_signature_view permission. +- Add support for signing documents. 2.0.2 (2016-02-09) ================== diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py index 68c3e7028b..7bb4d100de 100644 --- a/mayan/apps/django_gpg/models.py +++ b/mayan/apps/django_gpg/models.py @@ -117,7 +117,7 @@ class Key(models.Model): super(Key, self).save(*args, **kwargs) def __str__(self): - return self.key_id + return '{} - {}'.format(self.key_id, self.user_id) def sign_file(self, file_object, passphrase=None, clearsign=True, detached=False, binary=False, output=None): temporary_directory = tempfile.mkdtemp() diff --git a/mayan/apps/django_gpg/views.py b/mayan/apps/django_gpg/views.py index d7fa69928a..bc0c602265 100644 --- a/mayan/apps/django_gpg/views.py +++ b/mayan/apps/django_gpg/views.py @@ -33,13 +33,7 @@ class KeyDeleteView(SingleObjectDeleteView): return reverse_lazy('django_gpg:key_private_list') def get_extra_context(self): - return { - 'title': _('Delete key'), - 'message': _( - 'Delete key %s? If you delete a public key that is part of a ' - 'public/private pair the private key will be deleted as well.' - ) % self.get_object(), - } + return {'title': _('Delete key: %s') % self.get_object()} class KeyDetailView(SingleObjectDetailView): diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index 54f5e9509e..516838cc33 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -24,12 +24,14 @@ from .links import ( link_all_document_version_signature_verify, link_document_signature_list, link_document_version_signature_delete, + link_document_version_signature_detached_create, link_document_version_signature_details, link_document_version_signature_download, link_document_version_signature_list, link_document_version_signature_upload, ) from .permissions import ( + permission_document_version_sign_detached, permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_upload, @@ -74,6 +76,7 @@ class DocumentSignaturesApp(MayanAppConfig): ModelPermission.register( model=Document, permissions=( + permission_document_version_sign_detached, permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_view, @@ -93,7 +96,7 @@ class DocumentSignaturesApp(MayanAppConfig): func=lambda context: context['object'].signature_id or _('None') ) SourceColumn( - source=SignatureBaseModel, label=_('Is embedded?'), + source=SignatureBaseModel, label=_('Type'), func=lambda context: SignatureBaseModel.objects.get_subclass( pk=context['object'].pk ).get_signature_type_display() @@ -126,8 +129,10 @@ class DocumentSignaturesApp(MayanAppConfig): links=(link_document_signature_list,), sources=(Document,) ) menu_object.bind_links( - links=(link_document_version_signature_list,), - sources=(DocumentVersion,) + links=( + link_document_version_signature_list, + link_document_version_signature_detached_create, + ), sources=(DocumentVersion,) ) menu_object.bind_links( links=( diff --git a/mayan/apps/document_signatures/forms.py b/mayan/apps/document_signatures/forms.py index 09139f78cd..0ee13c12c2 100644 --- a/mayan/apps/document_signatures/forms.py +++ b/mayan/apps/document_signatures/forms.py @@ -1,13 +1,51 @@ -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals + +import logging from django import forms +from django.core.exceptions import PermissionDenied from django.utils.translation import ugettext_lazy as _ +from acls.models import AccessControlList +from permissions import Permission + from common.forms import DetailForm from django_gpg.models import Key +from django_gpg.permissions import permission_key_sign from .models import SignatureBaseModel +logger = logging.getLogger(__name__) + + +class DocumentVersionDetachedSignatureCreateForm(forms.Form): + key = forms.ModelChoiceField( + label=_('Key'), queryset=Key.objects.none() + ) + + passphrase = forms.CharField( + label=_('Passphrase'), required=False, + widget=forms.widgets.PasswordInput + ) + + def __init__(self, *args, **kwargs): + user = kwargs.pop('user', None) + logger.debug('user: %s', user) + super( + DocumentVersionDetachedSignatureCreateForm, self + ).__init__(*args, **kwargs) + + queryset = Key.objects.private_keys() + + try: + Permission.check_permissions(user, (permission_key_sign,)) + except PermissionDenied: + queryset = AccessControlList.objects.filter_by_access( + permission_key_sign, user, queryset + ) + + self.fields['key'].queryset = queryset + class DocumentVersionSignatureDetailForm(DetailForm): def __init__(self, *args, **kwargs): diff --git a/mayan/apps/document_signatures/links.py b/mayan/apps/document_signatures/links.py index 6494764291..5c5d3acc28 100644 --- a/mayan/apps/document_signatures/links.py +++ b/mayan/apps/document_signatures/links.py @@ -64,6 +64,12 @@ link_document_version_signature_download = Link( link_document_version_signature_upload = Link( args='resolved_object.pk', permissions=(permission_document_version_signature_upload,), - text=_('Upload signature'), + permissions_related='document', text=_('Upload signature'), view='signatures:document_version_signature_upload', ) +link_document_version_signature_detached_create = Link( + args='resolved_object.pk', + permissions=(permission_document_version_signature_upload,), + permissions_related='document', text=_('Sign detached'), + view='signatures:document_version_signature_detached_create', +) diff --git a/mayan/apps/document_signatures/models.py b/mayan/apps/document_signatures/models.py index 2a7b3bff95..53dcff0de7 100644 --- a/mayan/apps/document_signatures/models.py +++ b/mayan/apps/document_signatures/models.py @@ -130,7 +130,8 @@ class DetachedSignature(SignatureBaseModel): return '{}-{}'.format(self.document_version, _('signature')) def delete(self, *args, **kwargs): - self.signature_file.storage.delete(self.signature_file.name) + if self.signature_file.name: + self.signature_file.storage.delete(name=self.signature_file.name) super(DetachedSignature, self).delete(*args, **kwargs) def save(self, *args, **kwargs): diff --git a/mayan/apps/document_signatures/permissions.py b/mayan/apps/document_signatures/permissions.py index 307758c374..15f5cacc7c 100644 --- a/mayan/apps/document_signatures/permissions.py +++ b/mayan/apps/document_signatures/permissions.py @@ -8,6 +8,10 @@ namespace = PermissionNamespace( 'document_signatures', _('Document signatures') ) +permission_document_version_sign_detached = namespace.add_permission( + name='document_version_sign_detached', + label=_('Sign documents with detached signatures') +) permission_document_version_signature_delete = namespace.add_permission( name='document_version_signature_delete', label=_('Delete detached signatures') diff --git a/mayan/apps/document_signatures/tests/test_views.py b/mayan/apps/document_signatures/tests/test_views.py index fdcaa38649..c903969b75 100644 --- a/mayan/apps/document_signatures/tests/test_views.py +++ b/mayan/apps/document_signatures/tests/test_views.py @@ -14,7 +14,6 @@ from user_management.tests import ( from ..models import DetachedSignature, EmbeddedSignature from ..permissions import ( - permission_document_version_signature_view, permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_upload, @@ -288,8 +287,6 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): for document in self.document_type.documents.all(): document.delete(to_trash=False) - from documents.models import DocumentType - old_hooks = DocumentVersion._post_save_hooks DocumentVersion._post_save_hooks = {} for count in range(TEST_UNSIGNED_DOCUMENT_COUNT): diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index 988b5a9226..3d6a584e82 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -3,9 +3,10 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url from .views import ( - AllDocumentSignatureVerifyView, DocumentVersionSignatureDeleteView, - DocumentVersionSignatureDetailView, DocumentVersionSignatureDownloadView, - DocumentVersionSignatureListView, DocumentVersionSignatureUploadView + AllDocumentSignatureVerifyView, DocumentVersionDetachedSignatureCreateView, + DocumentVersionSignatureDeleteView, DocumentVersionSignatureDetailView, + DocumentVersionSignatureDownloadView, DocumentVersionSignatureListView, + DocumentVersionSignatureUploadView ) urlpatterns = patterns( @@ -26,10 +27,15 @@ urlpatterns = patterns( name='document_version_signature_list' ), url( - r'^documents/version/(?P\d+)/signature/upload/$', + r'^documents/version/(?P\d+)/signature/detached/upload/$', DocumentVersionSignatureUploadView.as_view(), name='document_version_signature_upload' ), + url( + r'^documents/version/(?P\d+)/signature/detached/create/$', + DocumentVersionDetachedSignatureCreateView.as_view(), + name='document_version_signature_detached_create' + ), url( r'^signature/(?P\d+)/delete/$', DocumentVersionSignatureDeleteView.as_view(), diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index 0dfd71463d..919fa5dbd1 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -1,24 +1,33 @@ from __future__ import absolute_import, unicode_literals +import tempfile import logging from django.contrib import messages from django.core.exceptions import PermissionDenied +from django.core.files import File from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from acls.models import AccessControlList from common.generics import ( - ConfirmView, SingleObjectCreateView, SingleObjectDeleteView, + ConfirmView, FormView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView, SingleObjectDownloadView, SingleObjectListView ) +from django_gpg.exceptions import NeedPassphrase, PassphraseError +from django_gpg.permissions import permission_key_sign from documents.models import DocumentVersion from permissions import Permission -from .forms import DocumentVersionSignatureDetailForm +from .forms import ( + DocumentVersionDetachedSignatureCreateForm, + DocumentVersionSignatureDetailForm +) from .models import DetachedSignature, SignatureBaseModel from .permissions import ( + permission_document_version_sign_detached, permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_upload, @@ -30,6 +39,112 @@ from .tasks import task_verify_missing_embedded_signature logger = logging.getLogger(__name__) +class DocumentVersionDetachedSignatureCreateView(FormView): + form_class = DocumentVersionDetachedSignatureCreateForm + + def form_valid(self, form): + key = form.cleaned_data['key'] + passphrase = form.cleaned_data['passphrase'] or None + + try: + Permission.check_permissions( + self.request.user, (permission_key_sign,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_key_sign, self.request.user, key + ) + + try: + with self.get_document_version().open() as file_object: + detached_signature = key.sign_file( + file_object=file_object, detached=True, + passphrase=passphrase + ) + except NeedPassphrase: + messages.error( + self.request, _('Passphrase is needed to unlock this key.') + ) + return HttpResponseRedirect( + reverse( + 'signatures:document_version_signature_detached_create', + args=(self.get_document_version().pk,) + ) + ) + except PassphraseError: + messages.error( + self.request, _('Passphrase is incorrect.') + ) + return HttpResponseRedirect( + reverse( + 'signatures:document_version_signature_detached_create', + args=(self.get_document_version().pk,) + ) + ) + else: + temporary_file_object = tempfile.TemporaryFile() + temporary_file_object.write(detached_signature.data) + temporary_file_object.seek(0) + + DetachedSignature.objects.create( + document_version=self.get_document_version(), + signature_file=File(temporary_file_object) + ) + + temporary_file_object.close() + + messages.success( + self.request, _('Document version signed successfully.') + ) + + return super( + DocumentVersionDetachedSignatureCreateView, self + ).form_valid(form) + + def dispatch(self, request, *args, **kwargs): + try: + Permission.check_permissions( + request.user, (permission_document_version_sign_detached,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_document_version_sign_detached, request.user, + self.get_document_version().document + ) + + return super( + DocumentVersionDetachedSignatureCreateView, self + ).dispatch(request, *args, **kwargs) + + def get_document_version(self): + return get_object_or_404(DocumentVersion, pk=self.kwargs['pk']) + + def get_extra_context(self): + return { + 'document': self.get_document_version().document, + 'document_version': self.get_document_version(), + 'navigation_object_list': ('document', 'document_version'), + 'title': _( + 'Sign document version "%s" with a detached signature?' + ) % self.get_document_version(), + } + + def get_form_kwargs(self): + result = super( + DocumentVersionDetachedSignatureCreateView, self + ).get_form_kwargs() + + result.update({'user': self.request.user}) + + return result + + def get_post_action_redirect(self): + return reverse( + 'signatures:document_version_signature_list', + args=(self.get_document_version().pk,) + ) + + class DocumentVersionSignatureDeleteView(SingleObjectDeleteView): model = DetachedSignature object_permission = permission_document_version_signature_delete