Add support for signing documents from the UI. Mayan EDMS is now in the major leagues :)

This commit is contained in:
Roberto Rosario
2016-03-30 03:47:58 -04:00
parent bc59613945
commit 09b71144b6
11 changed files with 190 additions and 23 deletions

View File

@@ -28,6 +28,7 @@
- Handle unicode filenames in staging folders. - Handle unicode filenames in staging folders.
- Add staging file deletion permission. - Add staging file deletion permission.
- New document_signature_view permission. - New document_signature_view permission.
- Add support for signing documents.
2.0.2 (2016-02-09) 2.0.2 (2016-02-09)
================== ==================

View File

@@ -117,7 +117,7 @@ class Key(models.Model):
super(Key, self).save(*args, **kwargs) super(Key, self).save(*args, **kwargs)
def __str__(self): 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): def sign_file(self, file_object, passphrase=None, clearsign=True, detached=False, binary=False, output=None):
temporary_directory = tempfile.mkdtemp() temporary_directory = tempfile.mkdtemp()

View File

@@ -33,13 +33,7 @@ class KeyDeleteView(SingleObjectDeleteView):
return reverse_lazy('django_gpg:key_private_list') return reverse_lazy('django_gpg:key_private_list')
def get_extra_context(self): def get_extra_context(self):
return { return {'title': _('Delete key: %s') % self.get_object()}
'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(),
}
class KeyDetailView(SingleObjectDetailView): class KeyDetailView(SingleObjectDetailView):

View File

@@ -24,12 +24,14 @@ from .links import (
link_all_document_version_signature_verify, link_all_document_version_signature_verify,
link_document_signature_list, link_document_signature_list,
link_document_version_signature_delete, link_document_version_signature_delete,
link_document_version_signature_detached_create,
link_document_version_signature_details, link_document_version_signature_details,
link_document_version_signature_download, link_document_version_signature_download,
link_document_version_signature_list, link_document_version_signature_list,
link_document_version_signature_upload, link_document_version_signature_upload,
) )
from .permissions import ( from .permissions import (
permission_document_version_sign_detached,
permission_document_version_signature_delete, permission_document_version_signature_delete,
permission_document_version_signature_download, permission_document_version_signature_download,
permission_document_version_signature_upload, permission_document_version_signature_upload,
@@ -74,6 +76,7 @@ class DocumentSignaturesApp(MayanAppConfig):
ModelPermission.register( ModelPermission.register(
model=Document, permissions=( model=Document, permissions=(
permission_document_version_sign_detached,
permission_document_version_signature_delete, permission_document_version_signature_delete,
permission_document_version_signature_download, permission_document_version_signature_download,
permission_document_version_signature_view, permission_document_version_signature_view,
@@ -93,7 +96,7 @@ class DocumentSignaturesApp(MayanAppConfig):
func=lambda context: context['object'].signature_id or _('None') func=lambda context: context['object'].signature_id or _('None')
) )
SourceColumn( SourceColumn(
source=SignatureBaseModel, label=_('Is embedded?'), source=SignatureBaseModel, label=_('Type'),
func=lambda context: SignatureBaseModel.objects.get_subclass( func=lambda context: SignatureBaseModel.objects.get_subclass(
pk=context['object'].pk pk=context['object'].pk
).get_signature_type_display() ).get_signature_type_display()
@@ -126,8 +129,10 @@ class DocumentSignaturesApp(MayanAppConfig):
links=(link_document_signature_list,), sources=(Document,) links=(link_document_signature_list,), sources=(Document,)
) )
menu_object.bind_links( menu_object.bind_links(
links=(link_document_version_signature_list,), links=(
sources=(DocumentVersion,) link_document_version_signature_list,
link_document_version_signature_detached_create,
), sources=(DocumentVersion,)
) )
menu_object.bind_links( menu_object.bind_links(
links=( links=(

View File

@@ -1,13 +1,51 @@
from __future__ import unicode_literals from __future__ import absolute_import, unicode_literals
import logging
from django import forms from django import forms
from django.core.exceptions import PermissionDenied
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from acls.models import AccessControlList
from permissions import Permission
from common.forms import DetailForm from common.forms import DetailForm
from django_gpg.models import Key from django_gpg.models import Key
from django_gpg.permissions import permission_key_sign
from .models import SignatureBaseModel 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): class DocumentVersionSignatureDetailForm(DetailForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@@ -64,6 +64,12 @@ link_document_version_signature_download = Link(
link_document_version_signature_upload = Link( link_document_version_signature_upload = Link(
args='resolved_object.pk', args='resolved_object.pk',
permissions=(permission_document_version_signature_upload,), permissions=(permission_document_version_signature_upload,),
text=_('Upload signature'), permissions_related='document', text=_('Upload signature'),
view='signatures:document_version_signature_upload', 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',
)

View File

@@ -130,7 +130,8 @@ class DetachedSignature(SignatureBaseModel):
return '{}-{}'.format(self.document_version, _('signature')) return '{}-{}'.format(self.document_version, _('signature'))
def delete(self, *args, **kwargs): 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) super(DetachedSignature, self).delete(*args, **kwargs)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View File

@@ -8,6 +8,10 @@ namespace = PermissionNamespace(
'document_signatures', _('Document signatures') '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( permission_document_version_signature_delete = namespace.add_permission(
name='document_version_signature_delete', name='document_version_signature_delete',
label=_('Delete detached signatures') label=_('Delete detached signatures')

View File

@@ -14,7 +14,6 @@ from user_management.tests import (
from ..models import DetachedSignature, EmbeddedSignature from ..models import DetachedSignature, EmbeddedSignature
from ..permissions import ( from ..permissions import (
permission_document_version_signature_view,
permission_document_version_signature_delete, permission_document_version_signature_delete,
permission_document_version_signature_download, permission_document_version_signature_download,
permission_document_version_signature_upload, permission_document_version_signature_upload,
@@ -288,8 +287,6 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase):
for document in self.document_type.documents.all(): for document in self.document_type.documents.all():
document.delete(to_trash=False) document.delete(to_trash=False)
from documents.models import DocumentType
old_hooks = DocumentVersion._post_save_hooks old_hooks = DocumentVersion._post_save_hooks
DocumentVersion._post_save_hooks = {} DocumentVersion._post_save_hooks = {}
for count in range(TEST_UNSIGNED_DOCUMENT_COUNT): for count in range(TEST_UNSIGNED_DOCUMENT_COUNT):

View File

@@ -3,9 +3,10 @@ from __future__ import unicode_literals
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from .views import ( from .views import (
AllDocumentSignatureVerifyView, DocumentVersionSignatureDeleteView, AllDocumentSignatureVerifyView, DocumentVersionDetachedSignatureCreateView,
DocumentVersionSignatureDetailView, DocumentVersionSignatureDownloadView, DocumentVersionSignatureDeleteView, DocumentVersionSignatureDetailView,
DocumentVersionSignatureListView, DocumentVersionSignatureUploadView DocumentVersionSignatureDownloadView, DocumentVersionSignatureListView,
DocumentVersionSignatureUploadView
) )
urlpatterns = patterns( urlpatterns = patterns(
@@ -26,10 +27,15 @@ urlpatterns = patterns(
name='document_version_signature_list' name='document_version_signature_list'
), ),
url( url(
r'^documents/version/(?P<pk>\d+)/signature/upload/$', r'^documents/version/(?P<pk>\d+)/signature/detached/upload/$',
DocumentVersionSignatureUploadView.as_view(), DocumentVersionSignatureUploadView.as_view(),
name='document_version_signature_upload' name='document_version_signature_upload'
), ),
url(
r'^documents/version/(?P<pk>\d+)/signature/detached/create/$',
DocumentVersionDetachedSignatureCreateView.as_view(),
name='document_version_signature_detached_create'
),
url( url(
r'^signature/(?P<pk>\d+)/delete/$', r'^signature/(?P<pk>\d+)/delete/$',
DocumentVersionSignatureDeleteView.as_view(), DocumentVersionSignatureDeleteView.as_view(),

View File

@@ -1,24 +1,33 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import tempfile
import logging import logging
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.files import File
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from acls.models import AccessControlList from acls.models import AccessControlList
from common.generics import ( from common.generics import (
ConfirmView, SingleObjectCreateView, SingleObjectDeleteView, ConfirmView, FormView, SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectDetailView, SingleObjectDownloadView, SingleObjectListView SingleObjectDetailView, SingleObjectDownloadView, SingleObjectListView
) )
from django_gpg.exceptions import NeedPassphrase, PassphraseError
from django_gpg.permissions import permission_key_sign
from documents.models import DocumentVersion from documents.models import DocumentVersion
from permissions import Permission from permissions import Permission
from .forms import DocumentVersionSignatureDetailForm from .forms import (
DocumentVersionDetachedSignatureCreateForm,
DocumentVersionSignatureDetailForm
)
from .models import DetachedSignature, SignatureBaseModel from .models import DetachedSignature, SignatureBaseModel
from .permissions import ( from .permissions import (
permission_document_version_sign_detached,
permission_document_version_signature_delete, permission_document_version_signature_delete,
permission_document_version_signature_download, permission_document_version_signature_download,
permission_document_version_signature_upload, permission_document_version_signature_upload,
@@ -30,6 +39,112 @@ from .tasks import task_verify_missing_embedded_signature
logger = logging.getLogger(__name__) 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): class DocumentVersionSignatureDeleteView(SingleObjectDeleteView):
model = DetachedSignature model = DetachedSignature
object_permission = permission_document_version_signature_delete object_permission = permission_document_version_signature_delete