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.
- Add staging file deletion permission.
- New document_signature_view permission.
- Add support for signing documents.
2.0.2 (2016-02-09)
==================

View File

@@ -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()

View File

@@ -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):

View File

@@ -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=(

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.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):

View File

@@ -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',
)

View File

@@ -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):

View File

@@ -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')

View File

@@ -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):

View File

@@ -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<pk>\d+)/signature/upload/$',
r'^documents/version/(?P<pk>\d+)/signature/detached/upload/$',
DocumentVersionSignatureUploadView.as_view(),
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(
r'^signature/(?P<pk>\d+)/delete/$',
DocumentVersionSignatureDeleteView.as_view(),

View File

@@ -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