Start of document_signatures app refactor.

This commit is contained in:
Roberto Rosario
2016-03-25 04:07:57 -04:00
parent ab6e2d8c23
commit 355190e919
16 changed files with 650 additions and 343 deletions

View File

@@ -27,6 +27,7 @@
- More tests added. - More tests added.
- 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.
2.0.2 (2016-02-09) 2.0.2 (2016-02-09)
================== ==================

View File

@@ -2,9 +2,9 @@ from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from .models import DocumentVersionSignature #from .models import DocumentVersionSignature
"""
@admin.register(DocumentVersionSignature) @admin.register(DocumentVersionSignature)
class DocumentVersionSignatureAdmin(admin.ModelAdmin): class DocumentVersionSignatureAdmin(admin.ModelAdmin):
def document(self, instance): def document(self, instance):
@@ -20,3 +20,4 @@ class DocumentVersionSignatureAdmin(admin.ModelAdmin):
) )
list_display_links = ('document_version',) list_display_links = ('document_version',)
search_fields = ('document_version__document__label',) search_fields = ('document_version__document__label',)
"""

View File

@@ -6,16 +6,26 @@ from django.apps import apps
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from acls import ModelPermission 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 ( from .links import (
link_document_signature_delete, link_document_signature_download, link_document_version_signature_delete,
link_document_signature_upload, link_document_verify 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 ( from .permissions import (
permission_document_verify, permission_signature_delete, permission_document_version_signature_delete,
permission_signature_download, permission_signature_upload permission_document_version_signature_download,
permission_document_version_signature_upload,
permission_document_version_signature_verify,
permission_document_version_signature_view,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -39,30 +49,74 @@ class DocumentSignaturesApp(MayanAppConfig):
app_label='documents', model_name='DocumentVersion' 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( 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( ModelPermission.register(
model=Document, permissions=( model=Document, permissions=(
permission_document_verify, permission_signature_delete, permission_document_version_signature_delete,
permission_signature_download, permission_signature_upload, permission_document_version_signature_download,
permission_document_version_signature_verify,
permission_document_version_signature_view,
permission_document_version_signature_upload,
) )
) )
menu_facet.bind_links( SourceColumn(
links=(link_document_verify,), sources=(Document,) 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( menu_sidebar.bind_links(
links=( links=(
link_document_signature_upload, link_document_version_signature_upload,
link_document_signature_download, link_document_version_signature_verify,
link_document_signature_delete ), sources=(DocumentVersion,)
), sources=(
'signatures:document_verify',
'signatures:document_signature_upload',
'signatures:document_signature_download',
'signatures:document_signature_delete'
)
) )

View File

@@ -3,8 +3,49 @@ from __future__ import unicode_literals
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.forms import DetailForm
from .models import SignatureBaseModel
class DetachedSignatureForm(forms.Form): class DetachedSignatureForm(forms.Form):
file = forms.FileField( file = forms.FileField(
label=_('Signature file'), 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()},
"""

View File

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

View File

@@ -6,50 +6,57 @@ from django.utils.translation import ugettext_lazy as _
from navigation import Link from navigation import Link
from .permissions import ( from .permissions import (
permission_document_verify, permission_signature_delete, permission_document_version_signature_delete,
permission_signature_download, permission_signature_upload, 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): def is_detached_signature(context):
DocumentVersionSignature = apps.get_model( SignatureBaseModel = apps.get_model(
app_label='document_signatures', model_name='DocumentVersionSignature' app_label='document_signatures', model_name='SignatureBaseModel'
) )
return not DocumentVersionSignature.objects.has_detached_signature( return SignatureBaseModel.objects.select_subclasses().get(
context['object'].latest_version pk=context['object'].pk
) and not DocumentVersionSignature.objects.has_embedded_signature( ).is_detached
context['object'].latest_version
)
def can_delete_detached_signature(context): link_document_version_signature_delete = Link(
DocumentVersionSignature = apps.get_model( condition=is_detached_signature,
app_label='document_signatures', model_name='DocumentVersionSignature' #permissions=(permission_document_version_signature_delete,),
) tags='dangerous', text=_('Delete'),
view='signatures:document_version_signature_delete',
return DocumentVersionSignature.objects.has_detached_signature( args='resolved_object.pk'
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_signature_download = Link( link_document_version_signature_details = Link(
condition=can_delete_detached_signature, text=_('Download signature'), #permissions=(permission_document_version_signature_view,),
view='signatures:document_signature_download', args='object.pk', text=_('Details'),
permissions=(permission_signature_download,) view='signatures:document_version_signature_details',
args='resolved_object.pk'
) )
link_document_signature_upload = Link( link_document_version_signature_list = Link(
condition=can_upload_detached_signature, #permissions=(permission_document_version_signature_view,),
permissions=(permission_signature_upload,), text=_('Upload signature'), text=_('Signature list'),
view='signatures:document_signature_upload', args='object.pk' view='signatures:document_version_signature_list',
args='resolved_object.pk'
) )
link_document_verify = Link( link_document_version_signature_download = Link(
icon='fa fa-certificate', permissions=(permission_document_verify,), condition=is_detached_signature,
text=_('Signatures'), view='signatures:document_verify', args='object.pk' 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'
) )

View File

@@ -4,73 +4,66 @@ import logging
from django.db import models from django.db import models
from django_gpg.exceptions import VerificationError from django_gpg.exceptions import DecryptionError, VerificationError
from django_gpg.models import Key from django_gpg.models import Key
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DocumentVersionSignatureManager(models.Manager): class DetachedSignatureManager(models.Manager):
def get_document_signature(self, document_version): def upload_signature(self, document_version, signature_file):
document_signature, created = self.model.objects.get_or_create( with document_version.open() as file_object:
document_version=document_version, try:
) verify_result = Key.objects.verify_file(
file_object=file_object, signature_file=signature_file
return document_signature )
except VerificationError:
def add_detached_signature(self, document_version, detached_signature): # Not signed
document_signature = self.get_document_signature( pass
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
else: 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: class EmbeddedSignatureManager(models.Manager):
document_signature = self.get_document_signature( def check_signature(self, document_version):
document_version=document_version logger.debug('checking for embedded signature')
)
except ValueError: with document_version.open() as file_object:
return False 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: else:
return document_signature.has_embedded_signature return file_object
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
)
"""
def verify_signature(self, document_version): def verify_signature(self, document_version):
document_version_descriptor = document_version.open(raw=True) document_version_descriptor = document_version.open(raw=True)
detached_signature = None detached_signature = None
@@ -91,14 +84,4 @@ class DocumentVersionSignatureManager(models.Manager):
document_version_descriptor.close() document_version_descriptor.close()
if detached_signature: if detached_signature:
detached_signature.close() 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()

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,21 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import date
import logging import logging
import uuid import uuid
from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ 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 django_gpg.models import Key
from documents.models import DocumentVersion from documents.models import DocumentVersion
from .managers import DocumentVersionSignatureManager from .managers import EmbeddedSignatureManager, DetachedSignatureManager
from .runtime import storage_backend from .runtime import storage_backend
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -20,39 +25,71 @@ def upload_to(*args, **kwargs):
return unicode(uuid.uuid4()) return unicode(uuid.uuid4())
class DocumentVersionSignature(models.Model): @python_2_unicode_compatible
""" class SignatureBaseModel(models.Model):
Model that describes a document version signature properties
"""
document_version = models.ForeignKey( 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( # Basic fields
blank=True, null=True, storage=storage_backend, upload_to=upload_to, date = models.DateField(
verbose_name=_('Signature file') blank=True, editable=False, null=True, verbose_name=_('Date signed')
) )
has_embedded_signature = models.BooleanField( key_id = models.CharField(max_length=40, verbose_name=_('Key ID'))
default=False, verbose_name=_('Has embedded signature') # 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() objects = InheritanceManager()
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)
class Meta: class Meta:
verbose_name = _('Document version signature') verbose_name = _('Document version signature')
verbose_name_plural = _('Document version signatures') 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)

View File

@@ -8,15 +8,23 @@ namespace = PermissionNamespace(
'document_signatures', _('Document signatures') 'document_signatures', _('Document signatures')
) )
permission_document_verify = namespace.add_permission( permission_document_version_signature_view = namespace.add_permission(
name='document_verify', label=_('Verify document signatures') name='document_version_signature_view',
label=_('View details of document signatures')
) )
permission_signature_delete = namespace.add_permission( permission_document_version_signature_verify = namespace.add_permission(
name='signature_delete', label=_('Delete detached signatures') name='document_version_signature_verify',
label=_('Verify document signatures')
) )
permission_signature_download = namespace.add_permission( permission_document_version_signature_delete = namespace.add_permission(
name='signature_download', label=_('Download detached signatures') name='document_version_signature_delete',
label=_('Delete detached signatures')
) )
permission_signature_upload = namespace.add_permission( permission_document_version_signature_download = namespace.add_permission(
name='signature_upload', label=_('Upload detached signatures') 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')
) )

View File

@@ -4,14 +4,13 @@ import os
import time import time
from django.conf import settings from django.conf import settings
from django.core.files.base import File
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django_gpg.models import Key from django_gpg.models import Key
from documents.models import DocumentType from documents.models import DocumentType
from documents.tests import TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE 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( TEST_SIGNED_DOCUMENT_PATH = os.path.join(
settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.gpg' 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', settings.BASE_DIR, 'contrib', 'sample_documents',
'key0x5F3F7F75D210724D.asc' 'key0x5F3F7F75D210724D.asc'
) )
TEST_KEY_ID = '5F3F7F75D210724D'
@override_settings(OCR_AUTO_OCR=False) @override_settings(OCR_AUTO_OCR=False)
@@ -32,28 +32,82 @@ class DocumentTestCase(TestCase):
label=TEST_DOCUMENT_TYPE 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): def tearDown(self):
self.document_type.delete() self.document_type.delete()
def test_document_no_signature(self): def test_embedded_signature(self):
self.assertEqual(
DocumentVersionSignature.objects.has_detached_signature(
self.document.latest_version
), False
)
def test_new_document_version_signed(self):
with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: with open(TEST_SIGNED_DOCUMENT_PATH) as file_object:
self.document.new_version( signed_document = self.document_type.new_document(
file_object=File(file_object), comment='test comment 1' 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 # Artifical delay since MySQL doesn't store microsecond data in
@@ -61,40 +115,9 @@ class DocumentTestCase(TestCase):
# is the latest. # is the latest.
time.sleep(2) time.sleep(2)
self.assertEqual( self.assertEqual(EmbeddedSignature.objects.count(), 1)
DocumentVersionSignature.objects.has_detached_signature(
self.document.latest_version
), False
)
self.assertEqual(
DocumentVersionSignature.objects.verify_signature(
self.document.latest_version
).status, SIGNATURE_STATE_VALID
)
def test_detached_signatures(self): signature = EmbeddedSignature.objects.first()
with open(TEST_DOCUMENT_PATH) as file_object:
self.document.new_version(
file_object=File(file_object), comment='test comment 2'
)
# GPGVerificationError self.assertEqual(signature.document_version, signed_version)
self.assertEqual(DocumentVersionSignature.objects.verify_signature( self.assertEqual(signature.key_id, TEST_KEY_ID)
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
)

View File

@@ -2,22 +2,40 @@ from __future__ import unicode_literals
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from .views import (
DocumentVersionSignatureDeleteView, DocumentVersionSignatureDetailView,
DocumentVersionSignatureListView
)
urlpatterns = patterns( urlpatterns = patterns(
'document_signatures.views', 'document_signatures.views',
url( url(
r'^verify/(?P<document_pk>\d+)/$', 'document_verify', r'^(?P<pk>\d+)/details/$',
name='document_verify' DocumentVersionSignatureDetailView.as_view(),
name='document_version_signature_details'
), ),
url( url(
r'^upload/signature/(?P<document_pk>\d+)/$', r'^signature/(?P<pk>\d+)/download/$',
'document_signature_upload', name='document_signature_upload' 'document_signature_download',
name='document_version_signature_download'
), ),
url( url(
r'^download/signature/(?P<document_pk>\d+)/$', r'^document/version/(?P<pk>\d+)/signatures/list/$',
'document_signature_download', name='document_signature_download' DocumentVersionSignatureListView.as_view(),
name='document_version_signature_list'
), ),
url( url(
r'^document/(?P<document_pk>\d+)/signature/delete/$', r'^documents/version/(?P<document_pk>\d+)/signature/verify/$',
'document_signature_delete', name='document_signature_delete' 'document_verify', name='document_version_signature_verify'
),
url(
r'^documents/version/(?P<pk>\d+)/signature/upload/$',
'document_version_signature_upload',
name='document_version_signature_upload'
),
url(
r'^signature/(?P<pk>\d+)/delete/$',
DocumentVersionSignatureDeleteView.as_view(),
name='document_version_signature_delete'
), ),
) )

View File

@@ -14,21 +14,95 @@ from django.utils.html import escape
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 (
SingleObjectDeleteView, SingleObjectDetailView, SingleObjectListView
)
from django_gpg.literals import SIGNATURE_STATE_NONE, SIGNATURE_STATES 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 filetransfers.api import serve_file
from permissions import Permission from permissions import Permission
from .forms import DetachedSignatureForm from .forms import DetachedSignatureForm, DocumentVersionSignatureDetailForm
from .models import DocumentVersionSignature from .models import DetachedSignature, SignatureBaseModel
from .permissions import ( from .permissions import (
permission_document_verify, permission_signature_upload, permission_document_version_signature_view,
permission_signature_download, permission_signature_delete permission_document_version_signature_verify,
permission_document_version_signature_upload,
permission_document_version_signature_download,
permission_document_version_signature_delete
) )
logger = logging.getLogger(__name__) 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): def document_verify(request, document_pk):
document = get_object_or_404(Document, pk=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)) }, 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: try:
Permission.check_permissions( Permission.check_permissions(
request.user, (permission_signature_upload,) request.user, (permission_document_version_signature_upload,)
) )
except PermissionDenied: except PermissionDenied:
AccessControlList.objects.check_access( 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 post_action_redirect = None
previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))) 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) form = DetachedSignatureForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
try: try:
DocumentVersionSignature.objects.add_detached_signature( DetachedSignature.objects.upload_signature(
document.latest_version, request.FILES['file'] document_version=document_version,
signature_file=request.FILES['file']
) )
messages.success( messages.success(
request, _('Detached signature uploaded successfully.') request, _('Detached signature uploaded successfully.')
@@ -122,9 +198,11 @@ def document_signature_upload(request, document_pk):
return render_to_response('appearance/generic_form.html', { return render_to_response('appearance/generic_form.html', {
'form': form, 'form': form,
'next': next, 'next': next,
'object': document, 'document': document_version.document,
'document_version': document_version,
'navigation_object_list': ('document', 'document_version'),
'previous': previous, 'previous': previous,
'title': _('Upload detached signature for document: %s') % document, 'title': _('Upload detached signature for document version: %s') % document_version,
}, context_instance=RequestContext(request)) }, context_instance=RequestContext(request))
@@ -133,11 +211,11 @@ def document_signature_download(request, document_pk):
try: try:
Permission.check_permissions( Permission.check_permissions(
request.user, (permission_signature_download,) request.user, (permission_document_version_signature_download,)
) )
except PermissionDenied: except PermissionDenied:
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permission_signature_download, request.user, document permission_document_version_signature_download, request.user, document
) )
try: try:
@@ -156,49 +234,3 @@ def document_signature_download(request, document_pk):
return HttpResponseRedirect(request.META['HTTP_REFERER']) return HttpResponseRedirect(request.META['HTTP_REFERER'])
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))

View File

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