diff --git a/HISTORY.rst b/HISTORY.rst index afd4fca80e..b5a0e194f6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -27,6 +27,7 @@ - More tests added. - Handle unicode filenames in staging folders. - Add staging file deletion permission. +- New document_signature_view permission. 2.0.2 (2016-02-09) ================== diff --git a/mayan/apps/document_signatures/admin.py b/mayan/apps/document_signatures/admin.py index 67ea829c13..4d069195b9 100644 --- a/mayan/apps/document_signatures/admin.py +++ b/mayan/apps/document_signatures/admin.py @@ -2,9 +2,9 @@ from __future__ import unicode_literals from django.contrib import admin -from .models import DocumentVersionSignature - +#from .models import DocumentVersionSignature +""" @admin.register(DocumentVersionSignature) class DocumentVersionSignatureAdmin(admin.ModelAdmin): def document(self, instance): @@ -20,3 +20,4 @@ class DocumentVersionSignatureAdmin(admin.ModelAdmin): ) list_display_links = ('document_version',) search_fields = ('document_version__document__label',) +""" diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index dcf953a593..d541eaf410 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -6,16 +6,26 @@ from django.apps import apps from django.utils.translation import ugettext_lazy as _ from acls import ModelPermission -from common import MayanAppConfig, menu_facet, menu_sidebar +from common import ( + MayanAppConfig, menu_facet, menu_object, menu_secondary, menu_sidebar +) +from common.widgets import two_state_template +from navigation import SourceColumn -from .hooks import document_pre_open_hook, document_version_post_save_hook from .links import ( - link_document_signature_delete, link_document_signature_download, - link_document_signature_upload, link_document_verify + link_document_version_signature_delete, + link_document_version_signature_details, + link_document_version_signature_download, + link_document_version_signature_list, + link_document_version_signature_upload, + link_document_version_signature_verify ) from .permissions import ( - permission_document_verify, permission_signature_delete, - permission_signature_download, permission_signature_upload + permission_document_version_signature_delete, + permission_document_version_signature_download, + permission_document_version_signature_upload, + permission_document_version_signature_verify, + permission_document_version_signature_view, ) logger = logging.getLogger(__name__) @@ -39,30 +49,74 @@ class DocumentSignaturesApp(MayanAppConfig): app_label='documents', model_name='DocumentVersion' ) + DetachedSignature = self.get_model('DetachedSignature') + + EmbeddedSignature = self.get_model('EmbeddedSignature') + + SignatureBaseModel = self.get_model('SignatureBaseModel') + DocumentVersion.register_post_save_hook( - 1, document_version_post_save_hook + order=1, func=EmbeddedSignature.objects.check_signature + ) + DocumentVersion.register_pre_open_hook( + order=1, func=EmbeddedSignature.objects.open_signed ) - DocumentVersion.register_pre_open_hook(1, document_pre_open_hook) ModelPermission.register( model=Document, permissions=( - permission_document_verify, permission_signature_delete, - permission_signature_download, permission_signature_upload, + permission_document_version_signature_delete, + permission_document_version_signature_download, + permission_document_version_signature_verify, + permission_document_version_signature_view, + permission_document_version_signature_upload, ) ) - menu_facet.bind_links( - links=(link_document_verify,), sources=(Document,) + SourceColumn( + source=SignatureBaseModel, label=_('Date'), attribute='date' + ) + SourceColumn( + source=SignatureBaseModel, label=_('Key ID'), attribute='key_id' + ) + SourceColumn( + source=SignatureBaseModel, label=_('Signature ID'), + func=lambda context: context['object'].signature_id or _('None') + ) + SourceColumn( + source=SignatureBaseModel, label=_('Public key ID'), + func=lambda context: context['object'].public_key_fingerprint or _('None') + ) + SourceColumn( + source=SignatureBaseModel, label=_('Is embedded?'), + func=lambda context: two_state_template( + SignatureBaseModel.objects.get_subclass( + pk=context['object'].pk + ).is_embedded + ) + ) + SourceColumn( + source=SignatureBaseModel, label=_('Is detached?'), + func=lambda context: two_state_template( + SignatureBaseModel.objects.get_subclass( + pk=context['object'].pk + ).is_detached + ) + ) + + menu_object.bind_links( + links=(link_document_version_signature_list,), + sources=(DocumentVersion,) + ) + menu_object.bind_links( + links=( + link_document_version_signature_details, + link_document_version_signature_download, + link_document_version_signature_delete, + ), sources=(SignatureBaseModel,) ) menu_sidebar.bind_links( links=( - link_document_signature_upload, - link_document_signature_download, - link_document_signature_delete - ), sources=( - 'signatures:document_verify', - 'signatures:document_signature_upload', - 'signatures:document_signature_download', - 'signatures:document_signature_delete' - ) + link_document_version_signature_upload, + link_document_version_signature_verify, + ), sources=(DocumentVersion,) ) diff --git a/mayan/apps/document_signatures/forms.py b/mayan/apps/document_signatures/forms.py index a79d2b1718..050597aadf 100644 --- a/mayan/apps/document_signatures/forms.py +++ b/mayan/apps/document_signatures/forms.py @@ -3,8 +3,49 @@ from __future__ import unicode_literals from django import forms from django.utils.translation import ugettext_lazy as _ +from common.forms import DetailForm + +from .models import SignatureBaseModel + class DetachedSignatureForm(forms.Form): file = forms.FileField( label=_('Signature file'), ) + + +class DocumentVersionSignatureDetailForm(DetailForm): + def __init__(self, *args, **kwargs): + extra_fields = ( + {'label': _('Is embedded?'), 'field': 'is_embedded'}, + {'label': _('Date'), 'field': 'date'}, + {'label': _('Key ID'), 'field': 'key_id'}, + ) + + kwargs['extra_fields'] = extra_fields + super(DocumentVersionSignatureDetailForm, self).__init__(*args, **kwargs) + + class Meta: + fields = () + model = SignatureBaseModel + + +""" +{ + 'label': _('User ID'), + 'field': lambda x: escape(instance.user_id), +}, +{ + 'label': _('Creation date'), 'field': 'creation_date', + 'widget': forms.widgets.DateInput +}, +{ + 'label': _('Expiration date'), + 'field': lambda x: instance.expiration_date or _('None'), + 'widget': forms.widgets.DateInput +}, +{'label': _('Fingerprint'), 'field': 'fingerprint'}, +{'label': _('Length'), 'field': 'length'}, +{'label': _('Algorithm'), 'field': 'algorithm'}, +{'label': _('Type'), 'field': lambda x: instance.get_key_type_display()}, +""" diff --git a/mayan/apps/document_signatures/hooks.py b/mayan/apps/document_signatures/hooks.py deleted file mode 100644 index 323edeb386..0000000000 --- a/mayan/apps/document_signatures/hooks.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import unicode_literals - -import io -import logging - -from django.apps import apps -from django_gpg.exceptions import DecryptionError - -logger = logging.getLogger(__name__) - - -def document_pre_open_hook(file_object, instance): - logger.debug('instance: %s', instance) - - DocumentVersionSignature = apps.get_model( - app_label='document_signatures', model_name='DocumentVersionSignature' - ) - - Key = apps.get_model( - app_label='django_gpg', model_name='Key' - ) - - if DocumentVersionSignature.objects.has_embedded_signature(document_version=instance): - # If it has an embedded signature, decrypt - try: - result = Key.objects.decrypt_file(file_object=file_object) - # gpg return a string, turn it into a file like object - except DecryptionError: - # At least return the original raw content - file_object.seek(0) - return file_object - else: - file_object.close() - return io.BytesIO(result) - else: - return file_object - - -def document_version_post_save_hook(instance): - logger.debug('instance: %s', instance) - - DocumentVersionSignature = apps.get_model( - app_label='document_signatures', model_name='DocumentVersionSignature' - ) - - try: - document_signature = DocumentVersionSignature.objects.get( - document_version=instance - ) - except DocumentVersionSignature.DoesNotExist: - document_signature = DocumentVersionSignature.objects.create( - document_version=instance - ) - document_signature.check_for_embedded_signature() diff --git a/mayan/apps/document_signatures/links.py b/mayan/apps/document_signatures/links.py index 955d7eb266..fd5b8a6ef4 100644 --- a/mayan/apps/document_signatures/links.py +++ b/mayan/apps/document_signatures/links.py @@ -6,50 +6,57 @@ from django.utils.translation import ugettext_lazy as _ from navigation import Link from .permissions import ( - permission_document_verify, permission_signature_delete, - permission_signature_download, permission_signature_upload, + permission_document_version_signature_delete, + permission_document_version_signature_download, + permission_document_version_signature_upload, + permission_document_version_signature_verify, + permission_document_version_signature_view ) -def can_upload_detached_signature(context): - DocumentVersionSignature = apps.get_model( - app_label='document_signatures', model_name='DocumentVersionSignature' +def is_detached_signature(context): + SignatureBaseModel = apps.get_model( + app_label='document_signatures', model_name='SignatureBaseModel' ) - return not DocumentVersionSignature.objects.has_detached_signature( - context['object'].latest_version - ) and not DocumentVersionSignature.objects.has_embedded_signature( - context['object'].latest_version - ) + return SignatureBaseModel.objects.select_subclasses().get( + pk=context['object'].pk + ).is_detached -def can_delete_detached_signature(context): - DocumentVersionSignature = apps.get_model( - app_label='document_signatures', model_name='DocumentVersionSignature' - ) - - return DocumentVersionSignature.objects.has_detached_signature( - context['object'].latest_version - ) - - -link_document_signature_delete = Link( - condition=can_delete_detached_signature, - permissions=(permission_signature_delete,), tags='dangerous', - text=_('Delete signature'), view='signatures:document_signature_delete', - args='object.pk' +link_document_version_signature_delete = Link( + condition=is_detached_signature, + #permissions=(permission_document_version_signature_delete,), + tags='dangerous', text=_('Delete'), + view='signatures:document_version_signature_delete', + args='resolved_object.pk' ) -link_document_signature_download = Link( - condition=can_delete_detached_signature, text=_('Download signature'), - view='signatures:document_signature_download', args='object.pk', - permissions=(permission_signature_download,) +link_document_version_signature_details = Link( + #permissions=(permission_document_version_signature_view,), + text=_('Details'), + view='signatures:document_version_signature_details', + args='resolved_object.pk' ) -link_document_signature_upload = Link( - condition=can_upload_detached_signature, - permissions=(permission_signature_upload,), text=_('Upload signature'), - view='signatures:document_signature_upload', args='object.pk' +link_document_version_signature_list = Link( + #permissions=(permission_document_version_signature_view,), + text=_('Signature list'), + view='signatures:document_version_signature_list', + args='resolved_object.pk' ) -link_document_verify = Link( - icon='fa fa-certificate', permissions=(permission_document_verify,), - text=_('Signatures'), view='signatures:document_verify', args='object.pk' +link_document_version_signature_download = Link( + condition=is_detached_signature, + text=_('Download'), + view='signatures:document_signature_download', args='resolved_object.pk', + #permissions=(permission_document_version_signature_download,) +) +link_document_version_signature_upload = Link( + #permissions=(permission_document_version_signature_upload,), + text=_('Upload signature'), view='signatures:document_version_signature_upload', + args='resolved_object.pk' +) +link_document_version_signature_verify = Link( + icon='fa fa-certificate', + #permissions=(permission_document_version_signature_verify,), + text=_('Verify signatures'), view='signatures:document_verify', + args='resolved_object.pk' ) diff --git a/mayan/apps/document_signatures/managers.py b/mayan/apps/document_signatures/managers.py index 4b9e64b673..8b21116f80 100644 --- a/mayan/apps/document_signatures/managers.py +++ b/mayan/apps/document_signatures/managers.py @@ -4,73 +4,66 @@ import logging from django.db import models -from django_gpg.exceptions import VerificationError +from django_gpg.exceptions import DecryptionError, VerificationError from django_gpg.models import Key logger = logging.getLogger(__name__) -class DocumentVersionSignatureManager(models.Manager): - def get_document_signature(self, document_version): - document_signature, created = self.model.objects.get_or_create( - document_version=document_version, - ) - - return document_signature - - def add_detached_signature(self, document_version, detached_signature): - document_signature = self.get_document_signature( - document_version=document_version - ) - - if document_signature.has_embedded_signature: - raise Exception( - 'Document version already has an embedded signature' - ) - else: - if document_signature.signature_file: - logger.debug('Existing detached signature') - document_signature.delete_detached_signature_file() - document_signature.signature_file = None - document_signature.save() - - document_signature.signature_file = detached_signature - document_signature.save() - - def has_detached_signature(self, document_version): - try: - document_signature = self.get_document_signature( - document_version=document_version - ) - except ValueError: - return False - else: - if document_signature.signature_file: - return True +class DetachedSignatureManager(models.Manager): + def upload_signature(self, document_version, signature_file): + with document_version.open() as file_object: + try: + verify_result = Key.objects.verify_file( + file_object=file_object, signature_file=signature_file + ) + except VerificationError: + # Not signed + pass else: - return False + instance = self.create( + document_version=document_version, + date=verify_result.date, + key_id=verify_result.key_id, + signature_id=verify_result.signature_id, + public_key_fingerprint=verify_result.pubkey_fingerprint, + ) - def has_embedded_signature(self, document_version): - logger.debug('document_version: %s', document_version) - try: - document_signature = self.get_document_signature( - document_version=document_version - ) - except ValueError: - return False +class EmbeddedSignatureManager(models.Manager): + def check_signature(self, document_version): + logger.debug('checking for embedded signature') + + with document_version.open() as file_object: + try: + verify_result = Key.objects.verify_file(file_object=file_object) + except VerificationError: + # Not signed + pass + else: + instance = self.create( + document_version=document_version, + date=verify_result.date, + key_id=verify_result.key_id, + signature_id=verify_result.signature_id, + public_key_fingerprint=verify_result.pubkey_fingerprint, + ) + + def open_signed(self, file_object, document_version): + for signature in self.filter(document_version=document_version): + try: + return self.open_signed( + file_object=Key.objects.decrypt_file( + file_object=file_object + ), document_version=document_version + ) + except DecryptionError: + file_object.seek(0) + return file_object else: - return document_signature.has_embedded_signature - - def detached_signature(self, document_version): - document_signature = self.get_document_signature( - document_version=document_version - ) - - return document_signature.signature_file.storage.open( - document_signature.signature_file.name - ) + return file_object + """ def verify_signature(self, document_version): document_version_descriptor = document_version.open(raw=True) detached_signature = None @@ -91,14 +84,4 @@ class DocumentVersionSignatureManager(models.Manager): document_version_descriptor.close() if detached_signature: detached_signature.close() - - def clear_detached_signature(self, document_version): - document_signature = self.get_document_signature( - document_version=document_version - ) - if not document_signature.signature_file: - raise Exception('document doesn\'t have a detached signature') - - document_signature.delete_detached_signature_file() - document_signature.signature_file = None - document_signature.save() + """ diff --git a/mayan/apps/document_signatures/migrations/0003_auto_20160325_0052.py b/mayan/apps/document_signatures/migrations/0003_auto_20160325_0052.py new file mode 100644 index 0000000000..76fa8f097f --- /dev/null +++ b/mayan/apps/document_signatures/migrations/0003_auto_20160325_0052.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import storage.backends.filebasedstorage +import document_signatures.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0033_auto_20160325_0052'), + ('document_signatures', '0002_auto_20150608_1902'), + ] + + operations = [ + migrations.CreateModel( + name='SignatureBaseModel', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('date', models.DateField(null=True, verbose_name='Date signed', blank=True)), + ('key_id', models.CharField(max_length=40, verbose_name='Key ID')), + ('signature_id', models.CharField(max_length=64, null=True, verbose_name='Signature ID', blank=True)), + ('public_key_fingerprint', models.CharField(verbose_name='Public key fingerprint', unique=True, max_length=40, editable=False)), + ], + options={ + 'verbose_name': 'Document version signature', + 'verbose_name_plural': 'Document version signatures', + }, + ), + migrations.RemoveField( + model_name='documentversionsignature', + name='has_embedded_signature', + ), + migrations.AddField( + model_name='documentversionsignature', + name='date', + field=models.DateField(null=True, verbose_name='Date signed', blank=True), + ), + migrations.AddField( + model_name='documentversionsignature', + name='signature_id', + field=models.CharField(max_length=64, null=True, verbose_name='Signature ID', blank=True), + ), + migrations.AlterField( + model_name='documentversionsignature', + name='document_version', + field=models.ForeignKey(related_name='signature', editable=False, to='documents.DocumentVersion', verbose_name='Document version'), + ), + migrations.CreateModel( + name='DetachedSignature', + fields=[ + ('signaturebasemodel_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='document_signatures.SignatureBaseModel')), + ('signature_file', models.FileField(storage=storage.backends.filebasedstorage.FileBasedStorage(), upload_to=document_signatures.models.upload_to, null=True, verbose_name='Signature file', blank=True)), + ], + options={ + 'verbose_name': 'Document version detached signature', + 'verbose_name_plural': 'Document version detached signatures', + }, + bases=('document_signatures.signaturebasemodel',), + ), + migrations.CreateModel( + name='EmbeddedSignature', + fields=[ + ('signaturebasemodel_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='document_signatures.SignatureBaseModel')), + ], + options={ + 'verbose_name': 'Document version embedded signature', + 'verbose_name_plural': 'Document version embedded signatures', + }, + bases=('document_signatures.signaturebasemodel',), + ), + migrations.AddField( + model_name='signaturebasemodel', + name='document_version', + field=models.ForeignKey(related_name='signaturebasemodel', editable=False, to='documents.DocumentVersion', verbose_name='Document version'), + ), + ] diff --git a/mayan/apps/document_signatures/migrations/0004_auto_20160325_0418.py b/mayan/apps/document_signatures/migrations/0004_auto_20160325_0418.py new file mode 100644 index 0000000000..469cf232b9 --- /dev/null +++ b/mayan/apps/document_signatures/migrations/0004_auto_20160325_0418.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('document_signatures', '0003_auto_20160325_0052'), + ] + + operations = [ + migrations.AlterField( + model_name='documentversionsignature', + name='document_version', + field=models.ForeignKey(editable=False, to='documents.DocumentVersion', verbose_name='Document version'), + ), + migrations.AlterField( + model_name='signaturebasemodel', + name='date', + field=models.DateField(verbose_name='Date signed', null=True, editable=False, blank=True), + ), + migrations.AlterField( + model_name='signaturebasemodel', + name='document_version', + field=models.ForeignKey(related_name='signatures', editable=False, to='documents.DocumentVersion', verbose_name='Document version'), + ), + migrations.AlterField( + model_name='signaturebasemodel', + name='public_key_fingerprint', + field=models.CharField(null=True, editable=False, max_length=40, blank=True, unique=True, verbose_name='Public key fingerprint'), + ), + migrations.AlterField( + model_name='signaturebasemodel', + name='signature_id', + field=models.CharField(verbose_name='Signature ID', max_length=64, null=True, editable=False, blank=True), + ), + ] diff --git a/mayan/apps/document_signatures/migrations/0005_auto_20160325_0748.py b/mayan/apps/document_signatures/migrations/0005_auto_20160325_0748.py new file mode 100644 index 0000000000..cc087b93e2 --- /dev/null +++ b/mayan/apps/document_signatures/migrations/0005_auto_20160325_0748.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('document_signatures', '0004_auto_20160325_0418'), + ] + + operations = [ + migrations.RemoveField( + model_name='documentversionsignature', + name='document_version', + ), + migrations.DeleteModel( + name='DocumentVersionSignature', + ), + ] diff --git a/mayan/apps/document_signatures/models.py b/mayan/apps/document_signatures/models.py index cf1ca82e3b..397ac92354 100644 --- a/mayan/apps/document_signatures/models.py +++ b/mayan/apps/document_signatures/models.py @@ -1,16 +1,21 @@ from __future__ import unicode_literals +from datetime import date import logging import uuid +from django.core.urlresolvers import reverse from django.db import models +from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from django_gpg.exceptions import DecryptionError +from model_utils.managers import InheritanceManager + +from django_gpg.exceptions import DecryptionError, VerificationError from django_gpg.models import Key from documents.models import DocumentVersion -from .managers import DocumentVersionSignatureManager +from .managers import EmbeddedSignatureManager, DetachedSignatureManager from .runtime import storage_backend logger = logging.getLogger(__name__) @@ -20,39 +25,71 @@ def upload_to(*args, **kwargs): return unicode(uuid.uuid4()) -class DocumentVersionSignature(models.Model): - """ - Model that describes a document version signature properties - """ +@python_2_unicode_compatible +class SignatureBaseModel(models.Model): document_version = models.ForeignKey( - DocumentVersion, editable=False, verbose_name=_('Document version') + DocumentVersion, editable=False, related_name='signatures', + verbose_name=_('Document version') ) - signature_file = models.FileField( - blank=True, null=True, storage=storage_backend, upload_to=upload_to, - verbose_name=_('Signature file') + # Basic fields + date = models.DateField( + blank=True, editable=False, null=True, verbose_name=_('Date signed') ) - has_embedded_signature = models.BooleanField( - default=False, verbose_name=_('Has embedded signature') + key_id = models.CharField(max_length=40, verbose_name=_('Key ID')) + # With proper key + signature_id = models.CharField( + blank=True, editable=False, null=True, max_length=64, + verbose_name=_('Signature ID') + ) + public_key_fingerprint = models.CharField( + blank=True, editable=False, null=True, max_length=40, unique=True, + verbose_name=_('Public key fingerprint') ) - objects = DocumentVersionSignatureManager() - - def check_for_embedded_signature(self): - logger.debug('checking for embedded signature') - - with self.document_version.open(raw=True) as file_object: - try: - Key.objects.decrypt_file(file_object=file_object) - except DecryptionError: - self.has_embedded_signature = False - else: - self.has_embedded_signature = True - - self.save() - - def delete_detached_signature_file(self): - self.signature_file.storage.delete(self.signature_file.name) + objects = InheritanceManager() class Meta: verbose_name = _('Document version signature') verbose_name_plural = _('Document version signatures') + + def __str__(self): + return self.signature_id or '{} - {}'.format(self.date, self.key_id) + + def get_absolute_url(self): + return reverse( + 'document_signatures:document_version_signature_detail', + args=(self.pk,) + ) + + @property + def is_detached(self): + return hasattr(self, 'signature_file') + + @property + def is_embedded(self): + return not hasattr(self, 'signature_file') + + +class EmbeddedSignature(SignatureBaseModel): + objects = EmbeddedSignatureManager() + + class Meta: + verbose_name = _('Document version embedded signature') + verbose_name_plural = _('Document version embedded signatures') + + +class DetachedSignature(SignatureBaseModel): + signature_file = models.FileField( + blank=True, null=True, storage=storage_backend, upload_to=upload_to, + verbose_name=_('Signature file') + ) + + objects = DetachedSignatureManager() + + class Meta: + verbose_name = _('Document version detached signature') + verbose_name_plural = _('Document version detached signatures') + + def delete(self, *args, **kwargs): + self.signature_file.storage.delete(self.signature_file.name) + super(DetachedSignature, self).delete(*args, **kwargs) diff --git a/mayan/apps/document_signatures/permissions.py b/mayan/apps/document_signatures/permissions.py index 90f8270503..e973aac602 100644 --- a/mayan/apps/document_signatures/permissions.py +++ b/mayan/apps/document_signatures/permissions.py @@ -8,15 +8,23 @@ namespace = PermissionNamespace( 'document_signatures', _('Document signatures') ) -permission_document_verify = namespace.add_permission( - name='document_verify', label=_('Verify document signatures') +permission_document_version_signature_view = namespace.add_permission( + name='document_version_signature_view', + label=_('View details of document signatures') ) -permission_signature_delete = namespace.add_permission( - name='signature_delete', label=_('Delete detached signatures') +permission_document_version_signature_verify = namespace.add_permission( + name='document_version_signature_verify', + label=_('Verify document signatures') ) -permission_signature_download = namespace.add_permission( - name='signature_download', label=_('Download detached signatures') +permission_document_version_signature_delete = namespace.add_permission( + name='document_version_signature_delete', + label=_('Delete detached signatures') ) -permission_signature_upload = namespace.add_permission( - name='signature_upload', label=_('Upload detached signatures') +permission_document_version_signature_download = namespace.add_permission( + name='document_version_signature_download', + label=_('Download detached document signatures') +) +permission_document_version_signature_upload = namespace.add_permission( + name='document_version_signature_upload', + label=_('Upload detached document signatures') ) diff --git a/mayan/apps/document_signatures/tests/test_models.py b/mayan/apps/document_signatures/tests/test_models.py index 5516e029e5..12338f3d4c 100644 --- a/mayan/apps/document_signatures/tests/test_models.py +++ b/mayan/apps/document_signatures/tests/test_models.py @@ -4,14 +4,13 @@ import os import time from django.conf import settings -from django.core.files.base import File from django.test import TestCase, override_settings from django_gpg.models import Key from documents.models import DocumentType from documents.tests import TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE -from ..models import DocumentVersionSignature +from ..models import DetachedSignature, EmbeddedSignature TEST_SIGNED_DOCUMENT_PATH = os.path.join( settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.gpg' @@ -23,6 +22,7 @@ TEST_KEY_FILE = os.path.join( settings.BASE_DIR, 'contrib', 'sample_documents', 'key0x5F3F7F75D210724D.asc' ) +TEST_KEY_ID = '5F3F7F75D210724D' @override_settings(OCR_AUTO_OCR=False) @@ -32,28 +32,82 @@ class DocumentTestCase(TestCase): label=TEST_DOCUMENT_TYPE ) - with open(TEST_DOCUMENT_PATH) as file_object: - self.document = self.document_type.new_document( - file_object=File(file_object), label='mayan_11_1.pdf' - ) - - with open(TEST_KEY_FILE) as file_object: - Key.objects.create(key_data=file_object.read()) - def tearDown(self): self.document_type.delete() - def test_document_no_signature(self): - self.assertEqual( - DocumentVersionSignature.objects.has_detached_signature( - self.document.latest_version - ), False - ) - - def test_new_document_version_signed(self): + def test_embedded_signature(self): with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: - self.document.new_version( - file_object=File(file_object), comment='test comment 1' + signed_document = self.document_type.new_document( + file_object=file_object + ) + + self.assertEqual(EmbeddedSignature.objects.count(), 1) + + signature = EmbeddedSignature.objects.first() + + self.assertEqual( + signature.document_version, signed_document.latest_version + ) + self.assertEqual(signature.key_id, TEST_KEY_ID) + + def test_embedded_signature_with_key(self): + with open(TEST_KEY_FILE) as file_object: + key = Key.objects.create(key_data=file_object.read()) + + with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: + self.signed_document = self.document_type.new_document( + file_object=file_object + ) + + self.assertEqual(EmbeddedSignature.objects.count(), 1) + + signature = EmbeddedSignature.objects.first() + + self.assertEqual( + signature.document_version, + self.signed_document.latest_version + ) + self.assertEqual(signature.key_id, TEST_KEY_ID) + self.assertEqual(signature.public_key_fingerprint, key.fingerprint) + + def test_detached_signature(self): + with open(TEST_DOCUMENT_PATH) as file_object: + document = self.document_type.new_document( + file_object=file_object + ) + + with open(TEST_SIGNATURE_FILE_PATH) as file_object: + DetachedSignature.objects.upload_signature( + document_version=document.latest_version, + signature_file=file_object + ) + + self.assertEqual(DetachedSignature.objects.count(), 1) + self.assertEqual( + DetachedSignature.objects.first().document_version, + document.latest_version + ) + self.assertEqual(DetachedSignature.objects.first().key_id, TEST_KEY_ID) + + # TODO: test_verify_signature_after_new_key(self): + + def test_document_no_signature(self): + with open(TEST_DOCUMENT_PATH) as file_object: + document = self.document_type.new_document( + file_object=file_object + ) + + self.assertEqual(EmbeddedSignature.objects.count(), 0) + + def test_new_signed_version(self): + with open(TEST_DOCUMENT_PATH) as file_object: + document = self.document_type.new_document( + file_object=file_object + ) + + with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: + signed_version = document.new_version( + file_object=file_object, comment='test comment 1' ) # Artifical delay since MySQL doesn't store microsecond data in @@ -61,40 +115,9 @@ class DocumentTestCase(TestCase): # is the latest. time.sleep(2) - self.assertEqual( - DocumentVersionSignature.objects.has_detached_signature( - self.document.latest_version - ), False - ) - self.assertEqual( - DocumentVersionSignature.objects.verify_signature( - self.document.latest_version - ).status, SIGNATURE_STATE_VALID - ) + self.assertEqual(EmbeddedSignature.objects.count(), 1) - def test_detached_signatures(self): - with open(TEST_DOCUMENT_PATH) as file_object: - self.document.new_version( - file_object=File(file_object), comment='test comment 2' - ) + signature = EmbeddedSignature.objects.first() - # GPGVerificationError - self.assertEqual(DocumentVersionSignature.objects.verify_signature( - self.document.latest_version), None - ) - - with open(TEST_SIGNATURE_FILE_PATH, 'rb') as file_object: - DocumentVersionSignature.objects.add_detached_signature( - self.document.latest_version, File(file_object) - ) - - self.assertEqual( - DocumentVersionSignature.objects.has_detached_signature( - self.document.latest_version - ), True - ) - self.assertEqual( - DocumentVersionSignature.objects.verify_signature( - self.document.latest_version - ).status, SIGNATURE_STATE_VALID - ) + self.assertEqual(signature.document_version, signed_version) + self.assertEqual(signature.key_id, TEST_KEY_ID) diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index 408b385fae..061aa95e53 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -2,22 +2,40 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url +from .views import ( + DocumentVersionSignatureDeleteView, DocumentVersionSignatureDetailView, + DocumentVersionSignatureListView +) + urlpatterns = patterns( 'document_signatures.views', url( - r'^verify/(?P\d+)/$', 'document_verify', - name='document_verify' + r'^(?P\d+)/details/$', + DocumentVersionSignatureDetailView.as_view(), + name='document_version_signature_details' ), url( - r'^upload/signature/(?P\d+)/$', - 'document_signature_upload', name='document_signature_upload' + r'^signature/(?P\d+)/download/$', + 'document_signature_download', + name='document_version_signature_download' ), url( - r'^download/signature/(?P\d+)/$', - 'document_signature_download', name='document_signature_download' + r'^document/version/(?P\d+)/signatures/list/$', + DocumentVersionSignatureListView.as_view(), + name='document_version_signature_list' ), url( - r'^document/(?P\d+)/signature/delete/$', - 'document_signature_delete', name='document_signature_delete' + r'^documents/version/(?P\d+)/signature/verify/$', + 'document_verify', name='document_version_signature_verify' + ), + url( + r'^documents/version/(?P\d+)/signature/upload/$', + 'document_version_signature_upload', + name='document_version_signature_upload' + ), + url( + r'^signature/(?P\d+)/delete/$', + DocumentVersionSignatureDeleteView.as_view(), + name='document_version_signature_delete' ), ) diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index 0b443402bd..99d8be3715 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -14,21 +14,95 @@ from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ from acls.models import AccessControlList +from common.generics import ( + SingleObjectDeleteView, SingleObjectDetailView, SingleObjectListView +) from django_gpg.literals import SIGNATURE_STATE_NONE, SIGNATURE_STATES -from documents.models import Document +from documents.models import Document, DocumentVersion from filetransfers.api import serve_file from permissions import Permission -from .forms import DetachedSignatureForm -from .models import DocumentVersionSignature +from .forms import DetachedSignatureForm, DocumentVersionSignatureDetailForm +from .models import DetachedSignature, SignatureBaseModel from .permissions import ( - permission_document_verify, permission_signature_upload, - permission_signature_download, permission_signature_delete + permission_document_version_signature_view, + permission_document_version_signature_verify, + permission_document_version_signature_upload, + permission_document_version_signature_download, + permission_document_version_signature_delete ) logger = logging.getLogger(__name__) +class DocumentVersionSignatureDeleteView(SingleObjectDeleteView): + model = DetachedSignature + + def get_extra_context(self): + return { + 'document': self.get_object().document_version.document, + 'document_version': self.get_object().document_version, + 'navigation_object_list': ('document', 'document_version', 'signature'), + 'signature': self.get_object(), + 'title': _('Delete detached signature: %s') % self.get_object() + } + + def get_post_action_redirect(self): + return reverse( + 'signatures:document_version_signature_list', + args=(self.get_object().document_version.pk,) + ) + + +class DocumentVersionSignatureDetailView(SingleObjectDetailView): + form_class = DocumentVersionSignatureDetailForm + + def get_extra_context(self): + return { + 'document': self.get_object().document_version.document, + 'document_version': self.get_object().document_version, + 'signature': self.get_object(), + 'navigation_object_list': ('document', 'document_version', 'signature'), + 'hide_object': True, + 'title': _( + 'Details for signature: %s' + ) % self.get_object(), + } + + def get_queryset(self): + return SignatureBaseModel.objects.select_subclasses() + + +class DocumentVersionSignatureListView(SingleObjectListView): + 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'), + 'hide_object': True, + 'title': _( + 'Signatures for document version: %s' + ) % self.get_document_version(), + } + + def get_queryset(self): + queryset = self.get_document_version().signatures.all() + + try: + Permission.check_permissions( + self.request.user, (permission_document_version_signature_view,) + ) + except PermissionDenied: + return AccessControlList.objects.filter_by_access( + permission_document_version_signature_view, self.request.user, queryset + ) + else: + return queryset + + def document_verify(request, document_pk): document = get_object_or_404(Document, pk=document_pk) @@ -84,19 +158,20 @@ def document_verify(request, document_pk): }, context_instance=RequestContext(request)) -def document_signature_upload(request, document_pk): - document = get_object_or_404(Document, pk=document_pk) + +def document_version_signature_upload(request, pk): + document_version = get_object_or_404(DocumentVersion, pk=pk) try: Permission.check_permissions( - request.user, (permission_signature_upload,) + request.user, (permission_document_version_signature_upload,) ) except PermissionDenied: AccessControlList.objects.check_access( - permission_signature_upload, request.user, document + permission_document_version_signature_upload, request.user, document_version.document ) - document.add_as_recent_document_for_user(request.user) + document_version.document.add_as_recent_document_for_user(request.user) post_action_redirect = None previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))) @@ -106,8 +181,9 @@ def document_signature_upload(request, document_pk): form = DetachedSignatureForm(request.POST, request.FILES) if form.is_valid(): try: - DocumentVersionSignature.objects.add_detached_signature( - document.latest_version, request.FILES['file'] + DetachedSignature.objects.upload_signature( + document_version=document_version, + signature_file=request.FILES['file'] ) messages.success( request, _('Detached signature uploaded successfully.') @@ -122,9 +198,11 @@ def document_signature_upload(request, document_pk): return render_to_response('appearance/generic_form.html', { 'form': form, 'next': next, - 'object': document, + 'document': document_version.document, + 'document_version': document_version, + 'navigation_object_list': ('document', 'document_version'), 'previous': previous, - 'title': _('Upload detached signature for document: %s') % document, + 'title': _('Upload detached signature for document version: %s') % document_version, }, context_instance=RequestContext(request)) @@ -133,11 +211,11 @@ def document_signature_download(request, document_pk): try: Permission.check_permissions( - request.user, (permission_signature_download,) + request.user, (permission_document_version_signature_download,) ) except PermissionDenied: AccessControlList.objects.check_access( - permission_signature_download, request.user, document + permission_document_version_signature_download, request.user, document ) try: @@ -156,49 +234,3 @@ def document_signature_download(request, document_pk): return HttpResponseRedirect(request.META['HTTP_REFERER']) return HttpResponseRedirect(request.META['HTTP_REFERER']) - - -def document_signature_delete(request, document_pk): - document = get_object_or_404(Document, pk=document_pk) - - try: - Permission.check_permissions( - request.user, (permission_signature_delete,) - ) - except PermissionDenied: - AccessControlList.objects.check_access( - permission_signature_delete, request.user, document - ) - - document.add_as_recent_document_for_user(request.user) - - post_action_redirect = None - previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))) - next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))) - - if request.method == 'POST': - try: - DocumentVersionSignature.objects.clear_detached_signature( - document.latest_version - ) - messages.success( - request, _('Detached signature deleted successfully.') - ) - return HttpResponseRedirect(next) - except Exception as exception: - messages.error( - request, _( - 'Error while deleting the detached signature; %s' - ) % exception - ) - return HttpResponseRedirect(previous) - - return render_to_response('appearance/generic_confirm.html', { - 'delete_view': True, - 'next': next, - 'object': document, - 'previous': previous, - 'title': _( - 'Delete the detached signature from document: %s?' - ) % document, - }, context_instance=RequestContext(request)) diff --git a/mayan/apps/documents/migrations/0033_auto_20160325_0052.py b/mayan/apps/documents/migrations/0033_auto_20160325_0052.py new file mode 100644 index 0000000000..b3fa863e17 --- /dev/null +++ b/mayan/apps/documents/migrations/0033_auto_20160325_0052.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0032_auto_20160315_0537'), + ] + + operations = [ + migrations.AlterModelOptions( + name='documenttypefilename', + options={'ordering': ('filename',), 'verbose_name': 'Quick label', 'verbose_name_plural': 'Quick labels'}, + ), + ]