From dc5d25fd0095c4db1845a50b449e27bc9f352caa Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 22 Mar 2016 05:10:54 -0400 Subject: [PATCH 01/57] Initial code for database stored GPG keys. --- mayan/apps/django_gpg/admin.py | 27 ++++ .../django_gpg/migrations/0001_initial.py | 32 +++++ mayan/apps/django_gpg/migrations/__init__.py | 0 mayan/apps/django_gpg/models.py | 130 ++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 mayan/apps/django_gpg/admin.py create mode 100644 mayan/apps/django_gpg/migrations/0001_initial.py create mode 100644 mayan/apps/django_gpg/migrations/__init__.py create mode 100644 mayan/apps/django_gpg/models.py diff --git a/mayan/apps/django_gpg/admin.py b/mayan/apps/django_gpg/admin.py new file mode 100644 index 0000000000..4de5c004be --- /dev/null +++ b/mayan/apps/django_gpg/admin.py @@ -0,0 +1,27 @@ +from __future__ import unicode_literals + +from django.contrib import admin + +from .models import Key + + +@admin.register(Key) +class KeyAdmin(admin.ModelAdmin): + #date_hierarchy = 'datetime' + list_display = ('key_id', 'user_id', 'key_type') + #readonly_fields = list_display + + """ + key_id = models.CharField( + max_length=16, unique=True, verbose_name=_('Key ID') + ) + creation_date = models.DateField(verbose_name=_('Creation date')) + expiration_date = models.DateField(verbose_name=_('Expiration date')) + fingerprint = models.CharField( + max_length=40, verbose_name=_('Fingerprint') + ) + length = models.PositiveIntegerField(verbose_name=_('Length')) + algorithm = models.PositiveIntegerField(verbose_name=_('Algorithm')) + user_id = models.TextField(verbose_name=_('User ID')) + key_type = models.CharField(max_length=3, verbose_name=_('Type')) + """ diff --git a/mayan/apps/django_gpg/migrations/0001_initial.py b/mayan/apps/django_gpg/migrations/0001_initial.py new file mode 100644 index 0000000000..df1505e689 --- /dev/null +++ b/mayan/apps/django_gpg/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Key', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('data', models.TextField(verbose_name='Data')), + ('key_id', models.CharField(unique=True, max_length=16, verbose_name='Key ID')), + ('creation_date', models.DateField(verbose_name='Creation date')), + ('expiration_date', models.DateField(null=True, verbose_name='Expiration date', blank=True)), + ('fingerprint', models.CharField(unique=True, max_length=40, verbose_name='Fingerprint')), + ('length', models.PositiveIntegerField(verbose_name='Length')), + ('algorithm', models.PositiveIntegerField(verbose_name='Algorithm')), + ('user_id', models.TextField(verbose_name='User ID')), + ('key_type', models.CharField(max_length=3, verbose_name='Type')), + ], + options={ + 'verbose_name': 'Key', + 'verbose_name_plural': 'Keys', + }, + ), + ] diff --git a/mayan/apps/django_gpg/migrations/__init__.py b/mayan/apps/django_gpg/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py new file mode 100644 index 0000000000..49f7763b51 --- /dev/null +++ b/mayan/apps/django_gpg/models.py @@ -0,0 +1,130 @@ +from __future__ import absolute_import, unicode_literals + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +from datetime import date +import logging +import os +import shutil +import tempfile + +import gnupg + +from django.conf import settings +from django.core.exceptions import PermissionDenied +from django.core.files import File +from django.core.urlresolvers import reverse +from django.db import models, transaction +from django.utils.encoding import python_2_unicode_compatible +from django.utils.timezone import now +from django.utils.translation import ugettext, ugettext_lazy as _ + +from .settings import setting_gpg_path, setting_keyservers + +logger = logging.getLogger(__name__) + + +class KeyManager(models.Manager): + def receive_key(self, key_id): + temporary_directory = tempfile.mkdtemp() + + os.chmod(temporary_directory, 0x1C0) + + gpg = gnupg.GPG( + gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + ) + + import_results = gpg.recv_keys(setting_keyservers.value[0], key_id) + + key_data = gpg.export_keys(import_results.fingerprints[0]) + + shutil.rmtree(temporary_directory) + + return self.create(data=key_data) + + def search(self, query): + temporary_directory = tempfile.mkdtemp() + + gpg = gnupg.GPG( + gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + ) + + result = gpg.search_keys(query=query, keyserver=setting_keyservers.value[0]) + shutil.rmtree(temporary_directory) + + return result + + +@python_2_unicode_compatible +class Key(models.Model): + data = models.TextField(verbose_name=_('Data')) + key_id = models.CharField( + max_length=16, unique=True, verbose_name=_('Key ID') + ) + creation_date = models.DateField(verbose_name=_('Creation date')) + expiration_date = models.DateField( + blank=True, null=True, verbose_name=_('Expiration date') + ) + fingerprint = models.CharField( + max_length=40, unique=True, verbose_name=_('Fingerprint') + ) + length = models.PositiveIntegerField(verbose_name=_('Length')) + algorithm = models.PositiveIntegerField(verbose_name=_('Algorithm')) + user_id = models.TextField(verbose_name=_('User ID')) + key_type = models.CharField(max_length=3, verbose_name=_('Type')) + + objects = KeyManager() + + class Meta: + verbose_name = _('Key') + verbose_name_plural = _('Keys') + + def save(self, *args, **kwargs): + temporary_directory = tempfile.mkdtemp() + + logger.debug('temporary_directory: %s', temporary_directory) + + gpg = gnupg.GPG( + gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + ) + + import_results = gpg.import_keys(key_data=self.data) + + logger.debug('import_results.results: %s', import_results.results) + logger.debug('import_results.fingerprints: %s', import_results.fingerprints) + + key_data = gpg.list_keys(keys=import_results.fingerprints[0])[0] + + logger.debug('key_data: %s', key_data) + + shutil.rmtree(temporary_directory) + + self.key_id = key_data['keyid'] + self.algorithm = key_data['algo'] + self.creation_date = date.fromtimestamp(int(key_data['date'])) + if key_data['expires']: + self.expiration_date = date.fromtimestamp(int(key_data['expires'])) + self.fingerprint = key_data['fingerprint'] + self.length = int(key_data['length']) + self.user_id = key_data['uids'][0] + self.key_type = key_data['type'] + + super(Key, self).save(*args, **kwargs) + + def __str__(self): + return self.key_id + + def sign_file(self, file_object, passphrase=None, clearsign=True, detach=False, binary=False): + output = StringIO() + + temporary_directory = tempfile.mkdtemp() + + gpg = gnupg.GPG( + gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + ) + + import_results = gpg.import_keys(key_data=self.data) + From 189cda437fec933f036d41fa011e6dd8c7f3f956 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 22 Mar 2016 18:02:40 -0400 Subject: [PATCH 02/57] Add model test. Finish file signing method. Add key signing exceptions. --- mayan/apps/django_gpg/admin.py | 22 +-- mayan/apps/django_gpg/exceptions.py | 12 ++ mayan/apps/django_gpg/literals.py | 8 + .../migrations/0002_auto_20160322_1756.py | 24 +++ .../migrations/0003_auto_20160322_1810.py | 59 ++++++++ .../migrations/0004_auto_20160322_2202.py | 24 +++ mayan/apps/django_gpg/models.py | 141 ++++++++++++------ mayan/apps/django_gpg/settings.py | 12 +- mayan/apps/django_gpg/tests/literals.py | 63 ++++++++ mayan/apps/django_gpg/tests/test_models.py | 16 ++ 10 files changed, 316 insertions(+), 65 deletions(-) create mode 100644 mayan/apps/django_gpg/migrations/0002_auto_20160322_1756.py create mode 100644 mayan/apps/django_gpg/migrations/0003_auto_20160322_1810.py create mode 100644 mayan/apps/django_gpg/migrations/0004_auto_20160322_2202.py create mode 100644 mayan/apps/django_gpg/tests/test_models.py diff --git a/mayan/apps/django_gpg/admin.py b/mayan/apps/django_gpg/admin.py index 4de5c004be..8aa6c2872d 100644 --- a/mayan/apps/django_gpg/admin.py +++ b/mayan/apps/django_gpg/admin.py @@ -7,21 +7,9 @@ from .models import Key @admin.register(Key) class KeyAdmin(admin.ModelAdmin): - #date_hierarchy = 'datetime' - list_display = ('key_id', 'user_id', 'key_type') - #readonly_fields = list_display - - """ - key_id = models.CharField( - max_length=16, unique=True, verbose_name=_('Key ID') + list_display = ( + 'key_id', 'user_id', 'creation_date', 'expiration_date', 'key_type' ) - creation_date = models.DateField(verbose_name=_('Creation date')) - expiration_date = models.DateField(verbose_name=_('Expiration date')) - fingerprint = models.CharField( - max_length=40, verbose_name=_('Fingerprint') - ) - length = models.PositiveIntegerField(verbose_name=_('Length')) - algorithm = models.PositiveIntegerField(verbose_name=_('Algorithm')) - user_id = models.TextField(verbose_name=_('User ID')) - key_type = models.CharField(max_length=3, verbose_name=_('Type')) - """ + list_filter = ('key_type',) + readonly_fields = list_display + ('fingerprint', 'length', 'algorithm') + search_fields = ('key_id', 'user_id',) diff --git a/mayan/apps/django_gpg/exceptions.py b/mayan/apps/django_gpg/exceptions.py index e9473a61bf..31691d4a42 100644 --- a/mayan/apps/django_gpg/exceptions.py +++ b/mayan/apps/django_gpg/exceptions.py @@ -39,3 +39,15 @@ class KeyDoesNotExist(GPGException): class KeyImportError(GPGException): pass + + +class NeedPassphrase(GPGException): + """ + Passphrase is needed but none was provided + """ + + +class PassphraseError(GPGException): + """ + Passphrase provided is incorrect + """ diff --git a/mayan/apps/django_gpg/literals.py b/mayan/apps/django_gpg/literals.py index 85b75e061f..2a8b14997a 100644 --- a/mayan/apps/django_gpg/literals.py +++ b/mayan/apps/django_gpg/literals.py @@ -7,6 +7,14 @@ KEY_TYPES = { 'sec': _('Secret'), } +KEY_TYPE_PUBLIC = 'pub' +KEY_TYPE_SECRET = 'sec' + +KEY_TYPE_CHOICES = ( + (KEY_TYPE_PUBLIC, _('Public')), + (KEY_TYPE_SECRET, _('Secret')), +) + KEY_CLASS_RSA = 'RSA' KEY_CLASS_DSA = 'DSA' KEY_CLASS_ELG = 'ELG-E' diff --git a/mayan/apps/django_gpg/migrations/0002_auto_20160322_1756.py b/mayan/apps/django_gpg/migrations/0002_auto_20160322_1756.py new file mode 100644 index 0000000000..fd085a20d6 --- /dev/null +++ b/mayan/apps/django_gpg/migrations/0002_auto_20160322_1756.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_gpg', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='key', + name='data', + ), + migrations.AddField( + model_name='key', + name='key_data', + field=models.TextField(default='', verbose_name='Key data'), + preserve_default=False, + ), + ] diff --git a/mayan/apps/django_gpg/migrations/0003_auto_20160322_1810.py b/mayan/apps/django_gpg/migrations/0003_auto_20160322_1810.py new file mode 100644 index 0000000000..40689f948e --- /dev/null +++ b/mayan/apps/django_gpg/migrations/0003_auto_20160322_1810.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_gpg', '0002_auto_20160322_1756'), + ] + + operations = [ + migrations.AlterField( + model_name='key', + name='algorithm', + field=models.PositiveIntegerField(verbose_name='Algorithm', editable=False), + ), + migrations.AlterField( + model_name='key', + name='creation_date', + field=models.DateField(verbose_name='Creation date', editable=False), + ), + migrations.AlterField( + model_name='key', + name='expiration_date', + field=models.DateField(verbose_name='Expiration date', null=True, editable=False, blank=True), + ), + migrations.AlterField( + model_name='key', + name='fingerprint', + field=models.CharField(verbose_name='Fingerprint', unique=True, max_length=40, editable=False), + ), + migrations.AlterField( + model_name='key', + name='key_data', + field=models.TextField(verbose_name='Key data', editable=False), + ), + migrations.AlterField( + model_name='key', + name='key_id', + field=models.CharField(verbose_name='Key ID', unique=True, max_length=16, editable=False), + ), + migrations.AlterField( + model_name='key', + name='key_type', + field=models.CharField(verbose_name='Type', max_length=3, editable=False), + ), + migrations.AlterField( + model_name='key', + name='length', + field=models.PositiveIntegerField(verbose_name='Length', editable=False), + ), + migrations.AlterField( + model_name='key', + name='user_id', + field=models.TextField(verbose_name='User ID', editable=False), + ), + ] diff --git a/mayan/apps/django_gpg/migrations/0004_auto_20160322_2202.py b/mayan/apps/django_gpg/migrations/0004_auto_20160322_2202.py new file mode 100644 index 0000000000..a2bea9e955 --- /dev/null +++ b/mayan/apps/django_gpg/migrations/0004_auto_20160322_2202.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_gpg', '0003_auto_20160322_1810'), + ] + + operations = [ + migrations.AlterField( + model_name='key', + name='key_data', + field=models.TextField(verbose_name='Key data'), + ), + migrations.AlterField( + model_name='key', + name='key_type', + field=models.CharField(verbose_name='Type', max_length=3, editable=False, choices=[('pub', 'Public'), ('sec', 'Secret')]), + ), + ] diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py index 49f7763b51..14bf97b1e9 100644 --- a/mayan/apps/django_gpg/models.py +++ b/mayan/apps/django_gpg/models.py @@ -1,10 +1,5 @@ from __future__ import absolute_import, unicode_literals -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - from datetime import date import logging import os @@ -13,20 +8,37 @@ import tempfile import gnupg -from django.conf import settings -from django.core.exceptions import PermissionDenied -from django.core.files import File -from django.core.urlresolvers import reverse -from django.db import models, transaction +from django.core.exceptions import ValidationError +from django.db import models from django.utils.encoding import python_2_unicode_compatible -from django.utils.timezone import now -from django.utils.translation import ugettext, ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _ -from .settings import setting_gpg_path, setting_keyservers +from .literals import KEY_TYPE_CHOICES, KEY_TYPE_SECRET +from .exceptions import NeedPassphrase, PassphraseError +from .settings import setting_gpg_path, setting_keyserver +ERROR_MSG_NEED_PASSPHRASE = 'NEED_PASSPHRASE' +ERROR_MSG_BAD_PASSPHRASE = 'BAD_PASSPHRASE' +ERROR_MSG_GOOD_PASSPHRASE = 'GOOD_PASSPHRASE' +OUTPUT_MESSAGE_CONTAINS_PRIVATE_KEY = 'Contains private key' logger = logging.getLogger(__name__) +def gpg_command(function): + temporary_directory = tempfile.mkdtemp() + os.chmod(temporary_directory, 0x1C0) + + gpg = gnupg.GPG( + gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + ) + + result = function(gpg=gpg) + + shutil.rmtree(temporary_directory) + + return result + + class KeyManager(models.Manager): def receive_key(self, key_id): temporary_directory = tempfile.mkdtemp() @@ -37,7 +49,7 @@ class KeyManager(models.Manager): gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value ) - import_results = gpg.recv_keys(setting_keyservers.value[0], key_id) + import_results = gpg.recv_keys(setting_keyserver.value, key_id) key_data = gpg.export_keys(import_results.fingerprints[0]) @@ -52,29 +64,48 @@ class KeyManager(models.Manager): gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value ) - result = gpg.search_keys(query=query, keyserver=setting_keyservers.value[0]) + result = gpg.search_keys( + query=query, keyserver=setting_keyserver.value + ) shutil.rmtree(temporary_directory) return result + def public_keys(self): + return self.filter(key_type='pub') + + def private_keys(self): + return self.filter(key_type='') + @python_2_unicode_compatible class Key(models.Model): - data = models.TextField(verbose_name=_('Data')) + key_data = models.TextField(verbose_name=_('Key data')) key_id = models.CharField( - max_length=16, unique=True, verbose_name=_('Key ID') + editable=False, max_length=16, unique=True, verbose_name=_('Key ID') + ) + creation_date = models.DateField( + editable=False, verbose_name=_('Creation date') ) - creation_date = models.DateField(verbose_name=_('Creation date')) expiration_date = models.DateField( - blank=True, null=True, verbose_name=_('Expiration date') + blank=True, editable=False, null=True, + verbose_name=_('Expiration date') ) fingerprint = models.CharField( - max_length=40, unique=True, verbose_name=_('Fingerprint') + editable=False, max_length=40, unique=True, + verbose_name=_('Fingerprint') + ) + length = models.PositiveIntegerField( + editable=False, verbose_name=_('Length') + ) + algorithm = models.PositiveIntegerField( + editable=False, verbose_name=_('Algorithm') + ) + user_id = models.TextField(editable=False, verbose_name=_('User ID')) + key_type = models.CharField( + choices=KEY_TYPE_CHOICES, editable=False, max_length=3, + verbose_name=_('Type') ) - length = models.PositiveIntegerField(verbose_name=_('Length')) - algorithm = models.PositiveIntegerField(verbose_name=_('Algorithm')) - user_id = models.TextField(verbose_name=_('User ID')) - key_type = models.CharField(max_length=3, verbose_name=_('Type')) objects = KeyManager() @@ -82,44 +113,49 @@ class Key(models.Model): verbose_name = _('Key') verbose_name_plural = _('Keys') + def clean(self): + def import_key(gpg): + return gpg.import_keys(key_data=self.key_data) + + import_results = gpg_command(function=import_key) + + if not import_results.count: + raise ValidationError('Invalid key data') + def save(self, *args, **kwargs): temporary_directory = tempfile.mkdtemp() - logger.debug('temporary_directory: %s', temporary_directory) - gpg = gnupg.GPG( gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value ) - import_results = gpg.import_keys(key_data=self.data) + import_results = gpg.import_keys(key_data=self.key_data) - logger.debug('import_results.results: %s', import_results.results) - logger.debug('import_results.fingerprints: %s', import_results.fingerprints) + key_info = gpg.list_keys(keys=import_results.fingerprints[0])[0] - key_data = gpg.list_keys(keys=import_results.fingerprints[0])[0] - - logger.debug('key_data: %s', key_data) + logger.debug('key_info: %s', key_info) shutil.rmtree(temporary_directory) - self.key_id = key_data['keyid'] - self.algorithm = key_data['algo'] - self.creation_date = date.fromtimestamp(int(key_data['date'])) - if key_data['expires']: - self.expiration_date = date.fromtimestamp(int(key_data['expires'])) - self.fingerprint = key_data['fingerprint'] - self.length = int(key_data['length']) - self.user_id = key_data['uids'][0] - self.key_type = key_data['type'] + self.key_id = key_info['keyid'] + self.algorithm = key_info['algo'] + self.creation_date = date.fromtimestamp(int(key_info['date'])) + if key_info['expires']: + self.expiration_date = date.fromtimestamp(int(key_info['expires'])) + self.fingerprint = key_info['fingerprint'] + self.length = int(key_info['length']) + self.user_id = key_info['uids'][0] + if OUTPUT_MESSAGE_CONTAINS_PRIVATE_KEY in import_results.results[0]['text']: + self.key_type = KEY_TYPE_SECRET + else: + self.key_type = key_info['type'] super(Key, self).save(*args, **kwargs) def __str__(self): return self.key_id - def sign_file(self, file_object, passphrase=None, clearsign=True, detach=False, binary=False): - output = StringIO() - + def sign_file(self, file_object, passphrase=None, clearsign=True, detached=False, binary=False, output=None): temporary_directory = tempfile.mkdtemp() gpg = gnupg.GPG( @@ -128,3 +164,20 @@ class Key(models.Model): import_results = gpg.import_keys(key_data=self.data) + file_sign_results = gpg.sign_file( + file=file_object, keyid=import_results.fingerprints[0], + passphrase=passphrase, clearsign=clearsign, detach=detached, + binary=binary, output=output + ) + + shutil.rmtree(temporary_directory) + + logger.debug('file_sign_results.stderr: %s', file_sign_results.stderr) + + if ERROR_MSG_NEED_PASSPHRASE in file_sign_results.stderr: + if ERROR_MSG_BAD_PASSPHRASE in file_sign_results.stderr: + raise PassphraseError + elif ERROR_MSG_GOOD_PASSPHRASE not in file_sign_results.stderr: + raise NeedPassphrase + + return file_sign_results diff --git a/mayan/apps/django_gpg/settings.py b/mayan/apps/django_gpg/settings.py index 10d7464fb1..c7d3da34f1 100644 --- a/mayan/apps/django_gpg/settings.py +++ b/mayan/apps/django_gpg/settings.py @@ -8,10 +8,6 @@ from django.utils.translation import ugettext_lazy as _ from smart_settings import Namespace namespace = Namespace(name='django_gpg', label=_('Signatures')) -setting_keyservers = namespace.add_setting( - global_name='SIGNATURES_KEYSERVERS', default=['pool.sks-keyservers.net'], - help_text=_('List of keyservers to be queried for unknown keys.') -) setting_gpg_home = namespace.add_setting( global_name='SIGNATURES_GPG_HOME', default=os.path.join(settings.MEDIA_ROOT, 'gpg_home'), @@ -24,3 +20,11 @@ setting_gpg_path = namespace.add_setting( global_name='SIGNATURES_GPG_PATH', default='/usr/bin/gpg', help_text=_('Path to the GPG binary.'), is_path=True ) +setting_keyserver = namespace.add_setting( + global_name='SIGNATURES_KEYSERVER', default='pool.sks-keyservers.net', + help_text=_('Keyserver used to query for keys.') +) +setting_keyservers = namespace.add_setting( + global_name='SIGNATURES_KEYSERVERS', default=['pool.sks-keyservers.net'], + help_text=_('List of keyservers to be queried for unknown keys.') +) diff --git a/mayan/apps/django_gpg/tests/literals.py b/mayan/apps/django_gpg/tests/literals.py index 6d18beb52d..822bdcd593 100644 --- a/mayan/apps/django_gpg/tests/literals.py +++ b/mayan/apps/django_gpg/tests/literals.py @@ -4,3 +4,66 @@ TEST_GPG_HOME = '/tmp/test_gpg_home' TEST_KEY_ID = '607138F1AECC5A5CA31CB7715F3F7F75D210724D' TEST_KEYSERVERS = ['pool.sks-keyservers.net'] TEST_UIDS = 'Roberto Rosario' + +TEST_KEY_DATA = '''-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQO+BFbxfC8BCACnUZoD96W4+CSIaU9G8I08kXu2zJLzy2XgUtwLx8VQ8dOHr0E/ +UembHkjMeH6Gjxu2Yrrbl9/Anzd+lkP0L9BV7WqjXpHmPxuaRlsrNXuMyX0YWtjo +zvCo/mVBKEt1aEejuE6YbeZdBQraym32ew8hTXQhPwqbKPC9LTUa2tDjkJHs0DLU +5Hvg2/16IYd94ZHAH+wOa4WrR/6wU1VBfFCGBl+xbSvburLYDwhNZC9+sIu61BO8 +fZh48IIQ89Hin7cS/ovHTBF2Sr3n5yRzatV2eXXmT5AQdpTEpD3HPF82HXNRrSUK +I+BIoIGXnPg3wotOyahFGrC8RluY7QhU/KBdABEBAAH+AwMCyBnD0YX+KwtgKrBg +Nxz+lWc6bWQ4CvdxW4rlLTujXBbTYQ0YUpZ44qLXhq9Yso7760LF/ZZK4I12AZ+J +PCxubmYCBKg7HIHG1/tT6ACJyoWhCaO2rNXx7zh3SnYFNjvEoCUXoEoupoZ/Hk6J +NGCdJPUZe4mTY9lVHTSnwPusyGeSu9i51J4kREb0E1sN9UgMHNoJawu5BJw0Yl97 +wD0U1cP93BB9FA+3KHUZDcj0v5exSkvWO1HQKzkZAaWOPfHoGCVRRBe4fYhjgumv +cbu7p1ve4ysooOO28DD/bIgbLA9swQjJT9CgwTnudmrn+3PEY9ghPFm4pLjUMWBK +nkBsSGQ1y7rCeGNGg5lAAKQfzL7gseiS0f+lmfSXsl1VTFWI89cCwnP7rTYHjsyS +Fs1V5/HhwCUL3SVJL+p6VMtZ4VWVlZ+Hm27hD0VYnmvd/cO8h14NRF3R/If7Ut+8 +nqDwwtxTUPcDLzs2gbjGt9XhpVXCvoUExxZuf/q91wTUJGQ96wjKOopyH67i22m/ +Orr29VGdzaE9iLe+cicf4ZwwKLzLczTVSjk2KUpSFx5KaFMcekHaBo+h1ABYfYQd +DE+3zKnuVMgF3Z2VXdKj4meibByc0BvrILLhcZ08eqWAd+Duyo2eSZyWV+1FKbKw +qtzudRxKMtEh5h4y1vn4eRd1zEQPBG9m9CTLUeO0l60Q1/gy/VwmAsiJZkcI8KSS +9HVw672+Q3gAcblLyYJrIvKT2EyLD2rSijxgx61//s9UR9k0a9iFXB11FtQ6N3Ct ++msBMO3wFGviZ2iqWiMYiGDoIXMil6G1KtJLkDc5uDXFMc5see12vlsFmEDFScvj +Nnslh9ajbC+mfgRPZFtprtoaGFUd4VRDM7/rr7kuuCZFQ1QebEVjJjmQnfgpowa7 +C7QhRXhhbXBsZSBhZG1pbiA8YWRtaW5AZXhhbXBsZS5jb20+iQE4BBMBAgAiAhsD +BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAUCVvGFyAAKCRBBJenFcfN4rIwSB/4l +PbS0F8tGtetzPIgPYerI2OwZDbVyrTVGbrY0ZJJfWXR0vyTJ38s3dZNC22ct3+g1 +t1RVxFGssSZYW0StlPyb2u+5VUI4LDWmbaDL3QbN5KTkyXtGLaWUwJ/TC4EkAKEa +8HKqRpZOfeUj4gTIm2uwpYwihyVY2M6EW6we5DUOScX1kIO/VTB+QWChFcLEjZb3 +LdSSOEKI56QHV/Sxn38jp0tD7E/yBBVw+HhFamhqwrYTnxy2/W/xHBvWQk34ZnTf +o/mZyBlWz5h6JaKhHyw0akRQbBSfo3huW6+RKI3QHj82f85zv01Uzvjxvaz/N5SP +/MuHTPgG8g69Bg4Ik6jjnQO+BFbxfC8BCADQYEUpx79976Ut5ZtMj3CNpndUWHB1 +l2wa/vd+Gb6Yzm+/hu3t5GG8uxzFk9TC33G7/Ugyob2V0eVXS8rqIbiqbRW7Nmb6 +RF4xeEZkUlLTmzXu9vJLRCW0f0ui+YJz6Rdgn4BCRJ+/OkLIoB9axDxDL+961ftw +LqBTK3IpQc+VwjBLPTofApJGjM/pExJDskAi4IJpd8sz5Djc7MkF/tANSWVdvNOA +lTIWZkfSiY2cThmC1WgL1KfSSYcFH0Z6/8M2qzF/9+D//j7WSq2GPahqVueVIE4r +CIi3ffayXdPsiEzgkZqJxeZyt8ht74qTgZhAhmIxnobrLg1nbwOVGx0tABEBAAH+ +AwMCyBnD0YX+Kwtgqas29fXB07iu+YJbSEXDsg56zrdDBToOFODrpRsqQtVofRyO +1GVDt1qE8jJF+zxnxSWawFLwR3mUs8/RKmdOm9cLnsadjCSWXWXPgb0w5mzcaVBa +tn9CtnF2G30D77LtBrkhnKtmjpW2Etudd7wkBYtSL4mqADX+8SgbFlR5jYtlFcUl +6HziXFzFSDEJ3YOE4LMm39pk+p7Kn/1GvxLleXu46uQZU3yEUxmnrHFSmolehWJk +1OR6CZ4SDmsKyFF9aNJPo+0ytU/VyOOuruaEQwp6r+zuM9sanrZJVGwlN5PRhfmr ++TrUwStsh2sdKrQ10xDxBBp7xThR3wz3+REO2c6uIEIkXhSAOARK1EQGXpAeK35x +uAUief4yMMiBKweKADT9ic36xxmc52Ov7Nrkwgj8PXma3gWiktTPhGWLZQ/YdXTW +fV+IwDShJEmTPOAAtxqPljj9isC1qPS2ylJXrHyws3jz0xIMYe8GbgK1UmURC7DI +CAXC4K6x5/3Uuz+kirbQRXVt1c8O8azy/Zc9a97qodWd7NBHTAr8xk2JlcesjHmk +rGSKsm53sGV0PTweoi4n1YiEE6yBpCEoobcAABWfojCYIe5W54PTf7nkc+Ayzd9t +7ipTELF8RKHHBU42penurBAX+U3aSe6rUfhlTuVs8KykzT/4pQeUzndNYQos6KLH +C50CHXQbeLchdvDAzO0j80j8YGciRv0U+juaZMct+NCi/SNU46RD7qs85M9rB77/ +GzOyrpsfVA0lfS5Z/g25+TqxEBTypiGMSh5Exza1Nwc2tIRExoYThW22SAM2PWqg +zw+aeNyC4uJWc9Qzf9sVMC1vaUUkf7cRMl8Lh7fNkX/sBUB4X8E3IG2UpeHKiWxp +UjRRioHbL6k8qEviaSyJLIkBJQQYAQIADwUCVvF8LwIbDAUJAA0vAAAKCRBBJenF +cfN4rAkxB/9Xyvsny6iBY1aFrIr2roOyXg1rX+NjEfo+HZqUIjpESQcviIatQcGB +1MVnvABVKCQWzQyoIkOyAmTUHKb0aLDynDblIctMVOy80wEtWRHcMQo4PzGUPJn3 +hZOukiotQTeawLvyeoBY1M4FJaCvPYvUNl+PEUVLi2h2VFkANrtzJMjZpmI5iR62 +h4oCbUV5JHhOyB+89Y1w8haFU9LrgOER2kXff1xU6wMfLdcO5ApV/sRJcNdYL7Cg +7nJLpOu33rvGW97adFMStZxXz4k+VXLErvtkT72XZX9TjS8hmIRxHKZgpb12ZkUe +8aeg3z/W+YctdRt81bi5isgM+oML9LAQ +=JZ5G +-----END PGP PRIVATE KEY BLOCK-----''' + +TEST_KEY_ID = '4125E9C571F378AC' +TEST_KEY_FINGERPRINT = '6A24574E0A35004CDDFD22704125E9C571F378AC' diff --git a/mayan/apps/django_gpg/tests/test_models.py b/mayan/apps/django_gpg/tests/test_models.py new file mode 100644 index 0000000000..69d74f3b04 --- /dev/null +++ b/mayan/apps/django_gpg/tests/test_models.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals + +from django.test import TestCase + +from ..models import Key + +from .literals import TEST_KEY_DATA, TEST_KEY_FINGERPRINT, TEST_KEY_ID + + +class KeyTestCase(TestCase): + def test_key_instance_creation(self): + # Creating a Key instance is analogous to importing a key + key = Key.objects.create(key_data=TEST_KEY_DATA) + + self.assertEqual(key.key_id, TEST_KEY_ID) + self.assertEqual(key.fingerprint, TEST_KEY_FINGERPRINT) From 2748d5959fa17c90542c60acb740cb6002ec1486 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 23 Mar 2016 00:35:32 -0400 Subject: [PATCH 03/57] Place KeyManage on a seprate module. Convert views to use the new Key model. Add KeyStub class and use it to return query results. Add Key detail link and view. Remove the setting for multiple keyservers. --- mayan/apps/django_gpg/apps.py | 57 +++++++--- mayan/apps/django_gpg/classes.py | 11 ++ mayan/apps/django_gpg/forms.py | 38 +++++++ mayan/apps/django_gpg/links.py | 15 ++- mayan/apps/django_gpg/literals.py | 5 + mayan/apps/django_gpg/managers.py | 59 ++++++++++ mayan/apps/django_gpg/models.py | 56 ++-------- mayan/apps/django_gpg/permissions.py | 12 +- mayan/apps/django_gpg/runtime.py | 7 -- mayan/apps/django_gpg/settings.py | 4 - mayan/apps/django_gpg/urls.py | 13 ++- mayan/apps/django_gpg/views.py | 158 ++++++++++++--------------- 12 files changed, 259 insertions(+), 176 deletions(-) create mode 100644 mayan/apps/django_gpg/classes.py create mode 100644 mayan/apps/django_gpg/managers.py delete mode 100644 mayan/apps/django_gpg/runtime.py diff --git a/mayan/apps/django_gpg/apps.py b/mayan/apps/django_gpg/apps.py index b972569438..1062fbf3a4 100644 --- a/mayan/apps/django_gpg/apps.py +++ b/mayan/apps/django_gpg/apps.py @@ -1,18 +1,24 @@ -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from datetime import datetime from django.utils.translation import ugettext_lazy as _ -from common import MayanAppConfig, menu_object, menu_setup, menu_sidebar +from acls import ModelPermission +from acls.links import link_acl_list +from acls.permissions import permission_acl_edit, permission_acl_view +from common import ( + MayanAppConfig, menu_facet, menu_object, menu_setup, menu_sidebar +) from common.classes import Package from navigation import SourceColumn -from .api import Key, KeyStub +from .classes import KeyStub from .links import ( - link_key_delete, link_key_query, link_key_receive, link_key_setup, - link_public_keys + link_key_delete, link_key_detail, link_key_query, link_key_receive, + link_key_setup, link_private_keys, link_public_keys ) +from .permissions import permission_key_delete, permission_key_view class DjangoGPGApp(MayanAppConfig): @@ -24,6 +30,15 @@ class DjangoGPGApp(MayanAppConfig): def ready(self): super(DjangoGPGApp, self).ready() + Key = self.get_model('Key') + + ModelPermission.register( + model=Key, permissions=( + permission_acl_edit, permission_acl_view, + permission_key_delete, permission_key_view + ) + ) + Package(label='python-gnupg', license_text=''' Copyright (c) 2008-2014 by Vinay Sajip. All rights reserved. @@ -52,11 +67,8 @@ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ''') - SourceColumn(source=Key, label=_('ID'), attribute='key_id') - SourceColumn( - source=Key, label=_('Owner'), - func=lambda context: ', '.join(context['object'].uids) - ) + SourceColumn(source=Key, label=_('Key ID'), attribute='key_id') + SourceColumn(source=Key, label=_('User ID'), attribute='user_id') SourceColumn( source=KeyStub, label=_('ID'), @@ -75,17 +87,30 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ) SourceColumn(source=KeyStub, label=_('Length'), attribute='length') SourceColumn( - source=KeyStub, label=_('Identities'), + source=KeyStub, label=_('User ID'), func=lambda context: ', '.join(context['object'].uids) ) - menu_object.bind_links(links=(link_key_delete,), sources=(Key,)) + menu_object.bind_links(links=(link_key_detail,), sources=(Key,)) menu_object.bind_links(links=(link_key_receive,), sources=(KeyStub,)) + + menu_object.bind_links( + links=(link_acl_list, link_key_delete,), sources=(Key,) + ) menu_setup.bind_links(links=(link_key_setup,)) - menu_sidebar.bind_links( - links=(link_public_keys, link_key_query), + menu_facet.bind_links( + links=(link_private_keys, link_public_keys), sources=( - 'django_gpg:key_delete', 'django_gpg:key_public_list', - 'django_gpg:key_query', 'django_gpg:key_query_results', + 'django_gpg:key_public_list', 'django_gpg:key_private_list', + 'django_gpg:key_query', 'django_gpg:key_query_results', Key, + KeyStub + ) + ) + menu_sidebar.bind_links( + links=(link_key_query,), + sources=( + 'django_gpg:key_public_list', 'django_gpg:key_private_list', + 'django_gpg:key_query', 'django_gpg:key_query_results', Key, + KeyStub ) ) diff --git a/mayan/apps/django_gpg/classes.py b/mayan/apps/django_gpg/classes.py new file mode 100644 index 0000000000..6d72870e38 --- /dev/null +++ b/mayan/apps/django_gpg/classes.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import, unicode_literals + + +class KeyStub(object): + def __init__(self, raw): + self.key_id = raw['keyid'] + self.key_type = raw['type'] + self.date = raw['date'] + self.expires = raw['expires'] + self.length = raw['length'] + self.uids = raw['uids'] diff --git a/mayan/apps/django_gpg/forms.py b/mayan/apps/django_gpg/forms.py index c58b0eb02f..ecc934db39 100644 --- a/mayan/apps/django_gpg/forms.py +++ b/mayan/apps/django_gpg/forms.py @@ -1,8 +1,46 @@ from __future__ import unicode_literals from django import forms +from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ +from common.forms import DetailForm + +from .models import Key + + +class KeyDetailForm(DetailForm): + def __init__(self, *args, **kwargs): + instance = kwargs['instance'] + + extra_fields = ( + {'label': _('Key ID'), 'field': 'key_id'}, + { + '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': _('key_type'), 'field': 'key_type'}, + ) + + kwargs['extra_fields'] = extra_fields + super(KeyDetailForm, self).__init__(*args, **kwargs) + + class Meta: + fields = () + model = Key + class KeySearchForm(forms.Form): term = forms.CharField( diff --git a/mayan/apps/django_gpg/links.py b/mayan/apps/django_gpg/links.py index 2f71491674..3a6cda3ee0 100644 --- a/mayan/apps/django_gpg/links.py +++ b/mayan/apps/django_gpg/links.py @@ -10,18 +10,21 @@ from .permissions import ( ) link_private_keys = Link( - icon='fa fa-key', permissions=(permission_key_view,), - text=_('Private keys'), view='django_gpg:key_private_list' + permissions=(permission_key_view,), text=_('Private keys'), + view='django_gpg:key_private_list' ) link_public_keys = Link( - icon='fa fa-key', permissions=(permission_key_view,), - text=_('Public keys'), view='django_gpg:key_public_list' + permissions=(permission_key_view,), text=_('Public keys'), + view='django_gpg:key_public_list' ) link_key_delete = Link( permissions=(permission_key_delete,), tags='dangerous', text=_('Delete'), - view='django_gpg:key_delete', args=('object.fingerprint', 'object.type',) + view='django_gpg:key_delete', args=('resolved_object.pk',) +) +link_key_detail = Link( + permissions=(permission_key_view,), text=_('Details'), + view='django_gpg:key_detail', args=('resolved_object.pk',) ) - link_key_query = Link( permissions=(permission_keyserver_query,), text=_('Query keyservers'), view='django_gpg:key_query' diff --git a/mayan/apps/django_gpg/literals.py b/mayan/apps/django_gpg/literals.py index 2a8b14997a..6af8911ce9 100644 --- a/mayan/apps/django_gpg/literals.py +++ b/mayan/apps/django_gpg/literals.py @@ -61,3 +61,8 @@ SIGNATURE_STATES = { 'text': _('Document is signed with a valid signature.'), }, } + +ERROR_MSG_NEED_PASSPHRASE = 'NEED_PASSPHRASE' +ERROR_MSG_BAD_PASSPHRASE = 'BAD_PASSPHRASE' +ERROR_MSG_GOOD_PASSPHRASE = 'GOOD_PASSPHRASE' +OUTPUT_MESSAGE_CONTAINS_PRIVATE_KEY = 'Contains private key' diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py new file mode 100644 index 0000000000..20b9074ab7 --- /dev/null +++ b/mayan/apps/django_gpg/managers.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import, unicode_literals + +import logging +import os +import shutil +import tempfile + +import gnupg + +from django.db import models + +from .classes import KeyStub +from .literals import KEY_TYPE_PUBLIC, KEY_TYPE_SECRET +from .settings import setting_gpg_path, setting_keyserver + +logger = logging.getLogger(__name__) + + +class KeyManager(models.Manager): + def receive_key(self, key_id): + temporary_directory = tempfile.mkdtemp() + + os.chmod(temporary_directory, 0x1C0) + + gpg = gnupg.GPG( + gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + ) + + import_results = gpg.recv_keys(setting_keyserver.value, key_id) + + key_data = gpg.export_keys(import_results.fingerprints[0]) + + shutil.rmtree(temporary_directory) + + return self.create(key_data=key_data) + + def search(self, query): + temporary_directory = tempfile.mkdtemp() + + gpg = gnupg.GPG( + gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + ) + + key_data_list = gpg.search_keys( + query=query, keyserver=setting_keyserver.value + ) + shutil.rmtree(temporary_directory) + + result = [] + for key_data in key_data_list: + result.append(KeyStub(raw=key_data)) + + return result + + def public_keys(self): + return self.filter(key_type=KEY_TYPE_PUBLIC) + + def private_keys(self): + return self.filter(key_type=KEY_TYPE_SECRET) diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py index 14bf97b1e9..a5b61afc32 100644 --- a/mayan/apps/django_gpg/models.py +++ b/mayan/apps/django_gpg/models.py @@ -9,18 +9,20 @@ import tempfile import gnupg from django.core.exceptions import ValidationError +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 .literals import KEY_TYPE_CHOICES, KEY_TYPE_SECRET from .exceptions import NeedPassphrase, PassphraseError -from .settings import setting_gpg_path, setting_keyserver +from .literals import ( + ERROR_MSG_NEED_PASSPHRASE, ERROR_MSG_BAD_PASSPHRASE, + ERROR_MSG_GOOD_PASSPHRASE, KEY_TYPE_CHOICES, KEY_TYPE_SECRET, + OUTPUT_MESSAGE_CONTAINS_PRIVATE_KEY +) +from .managers import KeyManager +from .settings import setting_gpg_path -ERROR_MSG_NEED_PASSPHRASE = 'NEED_PASSPHRASE' -ERROR_MSG_BAD_PASSPHRASE = 'BAD_PASSPHRASE' -ERROR_MSG_GOOD_PASSPHRASE = 'GOOD_PASSPHRASE' -OUTPUT_MESSAGE_CONTAINS_PRIVATE_KEY = 'Contains private key' logger = logging.getLogger(__name__) @@ -39,45 +41,6 @@ def gpg_command(function): return result -class KeyManager(models.Manager): - def receive_key(self, key_id): - temporary_directory = tempfile.mkdtemp() - - os.chmod(temporary_directory, 0x1C0) - - gpg = gnupg.GPG( - gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value - ) - - import_results = gpg.recv_keys(setting_keyserver.value, key_id) - - key_data = gpg.export_keys(import_results.fingerprints[0]) - - shutil.rmtree(temporary_directory) - - return self.create(data=key_data) - - def search(self, query): - temporary_directory = tempfile.mkdtemp() - - gpg = gnupg.GPG( - gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value - ) - - result = gpg.search_keys( - query=query, keyserver=setting_keyserver.value - ) - shutil.rmtree(temporary_directory) - - return result - - def public_keys(self): - return self.filter(key_type='pub') - - def private_keys(self): - return self.filter(key_type='') - - @python_2_unicode_compatible class Key(models.Model): key_data = models.TextField(verbose_name=_('Key data')) @@ -122,6 +85,9 @@ class Key(models.Model): if not import_results.count: raise ValidationError('Invalid key data') + def get_absolute_url(self): + return reverse('django_gpg:key_detail', args=(self.pk,)) + def save(self, *args, **kwargs): temporary_directory = tempfile.mkdtemp() diff --git a/mayan/apps/django_gpg/permissions.py b/mayan/apps/django_gpg/permissions.py index dc5536b2ca..fc984d3567 100644 --- a/mayan/apps/django_gpg/permissions.py +++ b/mayan/apps/django_gpg/permissions.py @@ -6,15 +6,15 @@ from permissions import PermissionNamespace namespace = PermissionNamespace('django_gpg', _('Key management')) -permission_key_view = namespace.add_permission( - name='key_view', label=_('View keys') -) permission_key_delete = namespace.add_permission( name='key_delete', label=_('Delete keys') ) -permission_keyserver_query = namespace.add_permission( - name='keyserver_query', label=_('Query keyservers') -) permission_key_receive = namespace.add_permission( name='key_receive', label=_('Import keys from keyservers') ) +permission_key_view = namespace.add_permission( + name='key_view', label=_('View keys') +) +permission_keyserver_query = namespace.add_permission( + name='keyserver_query', label=_('Query keyservers') +) diff --git a/mayan/apps/django_gpg/runtime.py b/mayan/apps/django_gpg/runtime.py deleted file mode 100644 index af8bfe4c42..0000000000 --- a/mayan/apps/django_gpg/runtime.py +++ /dev/null @@ -1,7 +0,0 @@ -from .api import GPG -from .settings import setting_gpg_home, setting_gpg_path, setting_keyservers - -gpg = GPG( - binary_path=setting_gpg_path.value, home=setting_gpg_home.value, - keyservers=setting_keyservers.value -) diff --git a/mayan/apps/django_gpg/settings.py b/mayan/apps/django_gpg/settings.py index c7d3da34f1..e8cb60c1d7 100644 --- a/mayan/apps/django_gpg/settings.py +++ b/mayan/apps/django_gpg/settings.py @@ -24,7 +24,3 @@ setting_keyserver = namespace.add_setting( global_name='SIGNATURES_KEYSERVER', default='pool.sks-keyservers.net', help_text=_('Keyserver used to query for keys.') ) -setting_keyservers = namespace.add_setting( - global_name='SIGNATURES_KEYSERVERS', default=['pool.sks-keyservers.net'], - help_text=_('List of keyservers to be queried for unknown keys.') -) diff --git a/mayan/apps/django_gpg/urls.py b/mayan/apps/django_gpg/urls.py index aae4d12c9e..47126ee413 100644 --- a/mayan/apps/django_gpg/urls.py +++ b/mayan/apps/django_gpg/urls.py @@ -3,14 +3,17 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url from .views import ( - KeyQueryView, KeyQueryResultView, PrivateKeyListView, PublicKeyListView + KeyDeleteView, KeyDetailView, KeyQueryView, KeyQueryResultView, KeyReceive, + PrivateKeyListView, PublicKeyListView ) urlpatterns = patterns( 'django_gpg.views', url( - r'^delete/(?P.+)/(?P\w+)/$', 'key_delete', - name='key_delete' + r'^(?P\d+)/$', KeyDetailView.as_view(), name='key_detail' + ), + url( + r'^delete/(?P\d+)/$', KeyDeleteView.as_view(), name='key_delete' ), url( r'^list/private/$', PrivateKeyListView.as_view(), @@ -24,5 +27,7 @@ urlpatterns = patterns( r'^query/results/$', KeyQueryResultView.as_view(), name='key_query_results' ), - url(r'^receive/(?P.+)/$', 'key_receive', name='key_receive'), + url( + r'^receive/(?P.+)/$', KeyReceive.as_view(), name='key_receive' + ), ) diff --git a/mayan/apps/django_gpg/views.py b/mayan/apps/django_gpg/views.py index a075aad618..54e575fde6 100644 --- a/mayan/apps/django_gpg/views.py +++ b/mayan/apps/django_gpg/views.py @@ -2,117 +2,77 @@ from __future__ import absolute_import, unicode_literals import logging -from django.conf import settings from django.contrib import messages -from django.core.urlresolvers import reverse -from django.http import HttpResponseRedirect -from django.shortcuts import redirect, render_to_response -from django.template import RequestContext +from django.core.urlresolvers import reverse, reverse_lazy from django.utils.translation import ugettext_lazy as _ -from common.generics import SimpleView, SingleObjectListView -from permissions import Permission +from common.generics import ( + ConfirmView, SimpleView, SingleObjectDeleteView, SingleObjectDetailView, + SingleObjectListView +) -from .api import Key -from .forms import KeySearchForm +from .forms import KeyDetailForm, KeySearchForm +from .models import Key from .permissions import ( permission_key_delete, permission_key_receive, permission_key_view, permission_keyserver_query ) -from .runtime import gpg logger = logging.getLogger(__name__) -def key_receive(request, key_id): - Permission.check_permissions(request.user, (permission_key_receive,)) - - previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))) - - if request.method == 'POST': - try: - gpg.receive_key(key_id=key_id) - except Exception as exception: - messages.error( - request, - _('Unable to import key: %(key_id)s; %(error)s') % - { - 'key_id': key_id, - 'error': exception, - } - ) - return HttpResponseRedirect(previous) - else: - messages.success( - request, - _('Successfully received key: %(key_id)s') % - { - 'key_id': key_id, - } - ) - - return redirect('django_gpg:key_public_list') - - return render_to_response('appearance/generic_confirm.html', { - 'message': _('Import key ID: %s?') % key_id, - 'previous': previous, - 'title': _('Import key'), - }, context_instance=RequestContext(request)) - - -class PublicKeyListView(SingleObjectListView): - view_permission = permission_key_view +class KeyDeleteView(SingleObjectDeleteView): + model = Key + object_permission = permission_key_delete def get_extra_context(self): return { - 'hide_object': True, - 'title': self.get_title() + '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(), } - def get_queryset(self): - return Key.get_all(gpg) - def get_title(self): - return _('Public keys') +class KeyDetailView(SingleObjectDetailView): + form_class = KeyDetailForm + model = Key + object_permission = permission_key_view + + def get_extra_context(self): + return { + 'title': _('Details for key: %s') % self.get_object(), + } -class PrivateKeyListView(PublicKeyListView): - def get_title(self): - return _('Private keys') +class KeyReceive(ConfirmView): + post_action_redirect = reverse_lazy('django_gpg:key_public_list') + view_permission = permission_key_receive - def get_queryset(self): - return Key.get_all(gpg, secret=True) + def get_extra_context(self): + return { + 'message': _('Import key ID: %s?') % self.kwargs['key_id'], + 'title': _('Import key'), + } - -def key_delete(request, fingerprint, key_type): - Permission.check_permissions(request.user, (permission_key_delete,)) - - secret = key_type == 'sec' - key = Key.get(gpg, fingerprint, secret=secret) - - post_action_redirect = redirect('django_gpg:key_public_list') - 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': + def view_action(self): try: - gpg.delete_key(key) - messages.success(request, _('Key: %s, deleted successfully.') % fingerprint) - return HttpResponseRedirect(next) + Key.objects.receive_key(key_id=self.kwargs['key_id']) except Exception as exception: - messages.error(request, exception) - return HttpResponseRedirect(previous) - - return render_to_response('appearance/generic_confirm.html', { - 'title': _('Delete key'), - 'delete_view': True, - '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.' - ) % key, - 'next': next, - 'previous': previous, - }, context_instance=RequestContext(request)) + messages.error( + self.request, + _('Unable to import key: %(key_id)s; %(error)s') % { + 'key_id': self.kwargs['key_id'], + 'error': exception, + } + ) + else: + messages.success( + self.request, _('Successfully received key: %(key_id)s') % { + 'key_id': self.kwargs['key_id'], + } + ) class KeyQueryView(SimpleView): @@ -149,6 +109,28 @@ class KeyQueryResultView(SingleObjectListView): def get_queryset(self): term = self.request.GET.get('term') if term: - return gpg.query(term) + return Key.objects.search(query=term) else: return () + + +class PublicKeyListView(SingleObjectListView): + object_permission = permission_key_view + queryset = Key.objects.public_keys() + + def get_extra_context(self): + return { + 'hide_object': True, + 'title': _('Public keys') + } + + +class PrivateKeyListView(SingleObjectListView): + object_permission = permission_key_view + queryset = Key.objects.private_keys() + + def get_extra_context(self): + return { + 'hide_object': True, + 'title': _('Private keys') + } From f82b2000c3c82863c9f9a22e3900649d52a617d4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 23 Mar 2016 00:37:26 -0400 Subject: [PATCH 04/57] Properly render date widgets on form instances. --- .../appearance/templates/appearance/generic_form_instance.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mayan/apps/appearance/templates/appearance/generic_form_instance.html b/mayan/apps/appearance/templates/appearance/generic_form_instance.html index 08a7e68fb1..ac6cf98582 100644 --- a/mayan/apps/appearance/templates/appearance/generic_form_instance.html +++ b/mayan/apps/appearance/templates/appearance/generic_form_instance.html @@ -69,7 +69,7 @@ {{ option.render }} {% endfor %} - {% elif field|widget_type == 'datetimeinput' %} + {% elif field|widget_type == 'datetimeinput' or field|widget_type == 'dateinput' %} {% if read_only %} {{ field.value }} {% else %} From cd077cb07688d514b28503b11ee54e0f39ba665a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 23 Mar 2016 11:58:08 -0400 Subject: [PATCH 05/57] Add more tests for the Key model. Remove the key_id field and made it a property derived from the fingerprint. --- mayan/apps/django_gpg/apps.py | 5 +-- mayan/apps/django_gpg/classes.py | 6 ++- mayan/apps/django_gpg/exceptions.py | 4 +- mayan/apps/django_gpg/forms.py | 6 +-- mayan/apps/django_gpg/managers.py | 11 +++-- .../migrations/0005_remove_key_key_id.py | 18 ++++++++ mayan/apps/django_gpg/models.py | 8 ++-- mayan/apps/django_gpg/tests/literals.py | 6 ++- mayan/apps/django_gpg/tests/test_classes.py | 45 ------------------- mayan/apps/django_gpg/tests/test_models.py | 21 ++++++++- 10 files changed, 65 insertions(+), 65 deletions(-) create mode 100644 mayan/apps/django_gpg/migrations/0005_remove_key_key_id.py delete mode 100644 mayan/apps/django_gpg/tests/test_classes.py diff --git a/mayan/apps/django_gpg/apps.py b/mayan/apps/django_gpg/apps.py index 1062fbf3a4..938fc2896b 100644 --- a/mayan/apps/django_gpg/apps.py +++ b/mayan/apps/django_gpg/apps.py @@ -70,10 +70,7 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SourceColumn(source=Key, label=_('Key ID'), attribute='key_id') SourceColumn(source=Key, label=_('User ID'), attribute='user_id') - SourceColumn( - source=KeyStub, label=_('ID'), - func=lambda context: '...{0}'.format(context['object'].key_id[-16:]) - ) + SourceColumn(source=KeyStub, label=_('Key ID'), attribute='key_id') SourceColumn(source=KeyStub, label=_('Type'), attribute='key_type') SourceColumn( source=KeyStub, label=_('Creation date'), diff --git a/mayan/apps/django_gpg/classes.py b/mayan/apps/django_gpg/classes.py index 6d72870e38..6f16e4076c 100644 --- a/mayan/apps/django_gpg/classes.py +++ b/mayan/apps/django_gpg/classes.py @@ -3,9 +3,13 @@ from __future__ import absolute_import, unicode_literals class KeyStub(object): def __init__(self, raw): - self.key_id = raw['keyid'] + self.fingerprint = raw['keyid'] self.key_type = raw['type'] self.date = raw['date'] self.expires = raw['expires'] self.length = raw['length'] self.uids = raw['uids'] + + @property + def key_id(self): + return self.fingerprint[-8:] diff --git a/mayan/apps/django_gpg/exceptions.py b/mayan/apps/django_gpg/exceptions.py index 31691d4a42..14dbe5ffa8 100644 --- a/mayan/apps/django_gpg/exceptions.py +++ b/mayan/apps/django_gpg/exceptions.py @@ -30,7 +30,9 @@ class KeyGenerationError(GPGException): class KeyFetchingError(GPGException): - pass + """ + Unable to receive key or key not found + """ class KeyDoesNotExist(GPGException): diff --git a/mayan/apps/django_gpg/forms.py b/mayan/apps/django_gpg/forms.py index ecc934db39..a2d1235c9e 100644 --- a/mayan/apps/django_gpg/forms.py +++ b/mayan/apps/django_gpg/forms.py @@ -29,9 +29,9 @@ class KeyDetailForm(DetailForm): 'widget': forms.widgets.DateInput }, {'label': _('Fingerprint'), 'field': 'fingerprint'}, - {'label': _('length'), 'field': 'length'}, - {'label': _('algorithm'), 'field': 'algorithm'}, - {'label': _('key_type'), 'field': 'key_type'}, + {'label': _('Length'), 'field': 'length'}, + {'label': _('Algorithm'), 'field': 'algorithm'}, + {'label': _('Type'), 'field': lambda x: instance.get_key_type_display()}, ) kwargs['extra_fields'] = extra_fields diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py index 20b9074ab7..89c5100308 100644 --- a/mayan/apps/django_gpg/managers.py +++ b/mayan/apps/django_gpg/managers.py @@ -10,6 +10,7 @@ import gnupg from django.db import models from .classes import KeyStub +from .exceptions import KeyFetchingError from .literals import KEY_TYPE_PUBLIC, KEY_TYPE_SECRET from .settings import setting_gpg_path, setting_keyserver @@ -28,11 +29,15 @@ class KeyManager(models.Manager): import_results = gpg.recv_keys(setting_keyserver.value, key_id) - key_data = gpg.export_keys(import_results.fingerprints[0]) + if not import_results.count: + shutil.rmtree(temporary_directory) + raise KeyFetchingError('No key found') + else: + key_data = gpg.export_keys(import_results.fingerprints[0]) - shutil.rmtree(temporary_directory) + shutil.rmtree(temporary_directory) - return self.create(key_data=key_data) + return self.create(key_data=key_data) def search(self, query): temporary_directory = tempfile.mkdtemp() diff --git a/mayan/apps/django_gpg/migrations/0005_remove_key_key_id.py b/mayan/apps/django_gpg/migrations/0005_remove_key_key_id.py new file mode 100644 index 0000000000..6acb9c702e --- /dev/null +++ b/mayan/apps/django_gpg/migrations/0005_remove_key_key_id.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_gpg', '0004_auto_20160322_2202'), + ] + + operations = [ + migrations.RemoveField( + model_name='key', + name='key_id', + ), + ] diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py index a5b61afc32..9235adb65c 100644 --- a/mayan/apps/django_gpg/models.py +++ b/mayan/apps/django_gpg/models.py @@ -44,9 +44,6 @@ def gpg_command(function): @python_2_unicode_compatible class Key(models.Model): key_data = models.TextField(verbose_name=_('Key data')) - key_id = models.CharField( - editable=False, max_length=16, unique=True, verbose_name=_('Key ID') - ) creation_date = models.DateField( editable=False, verbose_name=_('Creation date') ) @@ -103,7 +100,6 @@ class Key(models.Model): shutil.rmtree(temporary_directory) - self.key_id = key_info['keyid'] self.algorithm = key_info['algo'] self.creation_date = date.fromtimestamp(int(key_info['date'])) if key_info['expires']: @@ -147,3 +143,7 @@ class Key(models.Model): raise NeedPassphrase return file_sign_results + + @property + def key_id(self): + return self.fingerprint[-8:] diff --git a/mayan/apps/django_gpg/tests/literals.py b/mayan/apps/django_gpg/tests/literals.py index 822bdcd593..0936a239d1 100644 --- a/mayan/apps/django_gpg/tests/literals.py +++ b/mayan/apps/django_gpg/tests/literals.py @@ -1,9 +1,11 @@ from __future__ import unicode_literals TEST_GPG_HOME = '/tmp/test_gpg_home' -TEST_KEY_ID = '607138F1AECC5A5CA31CB7715F3F7F75D210724D' +TEST_KEY_FINGERPRINT = '6A24574E0A35004CDDFD22704125E9C571F378AC' TEST_KEYSERVERS = ['pool.sks-keyservers.net'] -TEST_UIDS = 'Roberto Rosario' + +TEST_SEARCH_UID = 'Roberto Rosario' +TEST_SEARCH_FINGERPRINT = '607138F1AECC5A5CA31CB7715F3F7F75D210724D' TEST_KEY_DATA = '''-----BEGIN PGP PRIVATE KEY BLOCK----- Version: GnuPG v1 diff --git a/mayan/apps/django_gpg/tests/test_classes.py b/mayan/apps/django_gpg/tests/test_classes.py deleted file mode 100644 index a31618414c..0000000000 --- a/mayan/apps/django_gpg/tests/test_classes.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import unicode_literals - -import shutil - -from django.test import TestCase - -from ..api import GPG, Key -from ..settings import setting_gpg_path - -from .literals import TEST_GPG_HOME, TEST_KEY_ID, TEST_KEYSERVERS, TEST_UIDS - - -class DjangoGPGTestCase(TestCase): - def setUp(self): - try: - shutil.rmtree(TEST_GPG_HOME) - except OSError: - pass - - self.gpg = GPG( - binary_path=setting_gpg_path.value, home=TEST_GPG_HOME, - keyservers=TEST_KEYSERVERS - ) - - def test_main(self): - # No private or public keys in the keyring - self.assertEqual(Key.get_all(self.gpg, secret=True), []) - self.assertEqual(Key.get_all(self.gpg), []) - - # Test querying the keyservers - self.assertTrue( - TEST_KEY_ID in [ - key_stub.key_id for key_stub in self.gpg.query(TEST_UIDS) - ] - ) - - # Receive a public key from the keyserver - self.gpg.receive_key(key_id=TEST_KEY_ID[-8:]) - - # Check that the received key is indeed in the keyring - self.assertTrue( - TEST_KEY_ID[-16:] in [ - key_stub.key_id for key_stub in Key.get_all(self.gpg) - ] - ) diff --git a/mayan/apps/django_gpg/tests/test_models.py b/mayan/apps/django_gpg/tests/test_models.py index 69d74f3b04..bcb97cda34 100644 --- a/mayan/apps/django_gpg/tests/test_models.py +++ b/mayan/apps/django_gpg/tests/test_models.py @@ -4,7 +4,10 @@ from django.test import TestCase from ..models import Key -from .literals import TEST_KEY_DATA, TEST_KEY_FINGERPRINT, TEST_KEY_ID +from .literals import ( + TEST_KEY_DATA, TEST_KEY_FINGERPRINT, TEST_SEARCH_FINGERPRINT, + TEST_SEARCH_UID +) class KeyTestCase(TestCase): @@ -12,5 +15,19 @@ class KeyTestCase(TestCase): # Creating a Key instance is analogous to importing a key key = Key.objects.create(key_data=TEST_KEY_DATA) - self.assertEqual(key.key_id, TEST_KEY_ID) self.assertEqual(key.fingerprint, TEST_KEY_FINGERPRINT) + + def test_key_search(self): + search_results = Key.objects.search(query=TEST_SEARCH_UID) + + self.assertTrue( + TEST_SEARCH_FINGERPRINT in [ + key_stub.fingerprint for key_stub in search_results + ] + ) + + def test_key_receive(self): + Key.objects.receive_key(key_id=TEST_SEARCH_FINGERPRINT) + + self.assertEqual(Key.objects.all().count(), 1) + self.assertEqual(Key.objects.first().fingerprint, TEST_SEARCH_FINGERPRINT) From 2f7c6ed0d970692e9559df936da250127d38bd4e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 23 Mar 2016 13:03:48 -0400 Subject: [PATCH 06/57] Add embedded file signature verification. --- mayan/apps/django_gpg/managers.py | 26 +++++++++++++++++- .../migrations/0005_remove_key_key_id.py | 2 +- .../contrib/test_files/test_file.txt.gpg | Bin 0 -> 337 bytes mayan/apps/django_gpg/tests/literals.py | 18 ++++++++---- mayan/apps/django_gpg/tests/test_models.py | 17 +++++++++++- 5 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 mayan/apps/django_gpg/tests/contrib/test_files/test_file.txt.gpg diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py index 89c5100308..a84a3d45b2 100644 --- a/mayan/apps/django_gpg/managers.py +++ b/mayan/apps/django_gpg/managers.py @@ -10,7 +10,7 @@ import gnupg from django.db import models from .classes import KeyStub -from .exceptions import KeyFetchingError +from .exceptions import KeyDoesNotExist, KeyFetchingError from .literals import KEY_TYPE_PUBLIC, KEY_TYPE_SECRET from .settings import setting_gpg_path, setting_keyserver @@ -62,3 +62,27 @@ class KeyManager(models.Manager): def private_keys(self): return self.filter(key_type=KEY_TYPE_SECRET) + + def verify_file(self, file_object, signature_file=None): + temporary_directory = tempfile.mkdtemp() + + gpg = gnupg.GPG( + gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + ) + + verify_result = gpg.verify_file(file=file_object) + + if 'no public key' in verify_result.status: + # File is signed but we need the key for full verification + try: + key = self.get(fingerprint__endswith=verify_result.key_id) + except self.model.DoesNotExist: + raise KeyDoesNotExist('Signature key is not found in keyring') + else: + gpg.import_keys(key_data=key.key_data) + file_object.seek(0) + verify_result = gpg.verify_file(file=file_object) + + shutil.rmtree(temporary_directory) + + return verify_result diff --git a/mayan/apps/django_gpg/migrations/0005_remove_key_key_id.py b/mayan/apps/django_gpg/migrations/0005_remove_key_key_id.py index 6acb9c702e..7f6b0718aa 100644 --- a/mayan/apps/django_gpg/migrations/0005_remove_key_key_id.py +++ b/mayan/apps/django_gpg/migrations/0005_remove_key_key_id.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/django_gpg/tests/contrib/test_files/test_file.txt.gpg b/mayan/apps/django_gpg/tests/contrib/test_files/test_file.txt.gpg new file mode 100644 index 0000000000000000000000000000000000000000..97c77a242b4eb7e5ac9f22657aaa73ac26474961 GIT binary patch literal 337 zcmV-X0j~a|0h_?f%)rFxsQU6~;pd7qjB6B=cuP`?OXAZqb5iw6DoVmW9gRm3;Ob(^=;Cz1}zi75Gq=3cwQFX;SgL*qg}Uhj%qN073Jflb^r+e?@Bf zDX}xJZ7WYTd1gIb*C};c(!0QAFUzX7pqW#9A8Q?2b^8|6r6b1{PnqZVK5v@5`PIcO zF7~fkZhr4wC3kY2;uoHNrBkvR;>E2pDSG?7l#3F?znxg^ryLV%%UZrBBfI&lc);Xe jE=u?2{pt(opY<%~r+MMsy$KN; Date: Wed, 23 Mar 2016 18:56:29 -0400 Subject: [PATCH 07/57] Add SignatureVerification class to return verification results. Add support for specifing against which key to verify a signature. Add support to preload all keys before verifing a signature. All test for specific key verificatio and all key preloading. --- mayan/apps/django_gpg/apps.py | 9 +++---- mayan/apps/django_gpg/classes.py | 29 +++++++++++++++++++--- mayan/apps/django_gpg/managers.py | 24 +++++++++++++++--- mayan/apps/django_gpg/tests/test_models.py | 16 ++++++++++++ 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/mayan/apps/django_gpg/apps.py b/mayan/apps/django_gpg/apps.py index 938fc2896b..4efb36ac89 100644 --- a/mayan/apps/django_gpg/apps.py +++ b/mayan/apps/django_gpg/apps.py @@ -73,19 +73,16 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SourceColumn(source=KeyStub, label=_('Key ID'), attribute='key_id') SourceColumn(source=KeyStub, label=_('Type'), attribute='key_type') SourceColumn( - source=KeyStub, label=_('Creation date'), - func=lambda context: datetime.fromtimestamp( - int(context['object'].date) - ) + source=KeyStub, label=_('Creation date'), attribute='date' ) SourceColumn( source=KeyStub, label=_('Expiration date'), - func=lambda context: datetime.fromtimestamp(int(context['object'].expires)) if context['object'].expires else _('No expiration') + func=lambda context: context['object'].expires or _('No expiration') ) SourceColumn(source=KeyStub, label=_('Length'), attribute='length') SourceColumn( source=KeyStub, label=_('User ID'), - func=lambda context: ', '.join(context['object'].uids) + func=lambda context: ', '.join(context['object'].user_id) ) menu_object.bind_links(links=(link_key_detail,), sources=(Key,)) diff --git a/mayan/apps/django_gpg/classes.py b/mayan/apps/django_gpg/classes.py index 6f16e4076c..42fa79c76b 100644 --- a/mayan/apps/django_gpg/classes.py +++ b/mayan/apps/django_gpg/classes.py @@ -1,15 +1,38 @@ from __future__ import absolute_import, unicode_literals +from datetime import date + class KeyStub(object): def __init__(self, raw): self.fingerprint = raw['keyid'] self.key_type = raw['type'] - self.date = raw['date'] - self.expires = raw['expires'] + self.date = date.fromtimestamp(int(raw['date'])) + if raw['expires']: + self.expires = date.fromtimestamp(int(raw['expires'])) + else: + self.expires = None self.length = raw['length'] - self.uids = raw['uids'] + self.user_id = raw['uids'] @property def key_id(self): return self.fingerprint[-8:] + + +class SignatureVerification(object): + def __init__(self, raw): + self.user_id = raw['username'] + self.status = raw['status'] + self.pubkey_fingerprint = raw['pubkey_fingerprint'] + self.date = date.fromtimestamp(int(raw['sig_timestamp'])) + if raw['expire_timestamp']: + self.expires = date.fromtimestamp(int(raw['expire_timestamp'])) + else: + self.expires = None + self.trust_text = raw['trust_text'] + self.valid = raw['valid'] + self.stderr = raw['stderr'] + self.fingerprint = raw['fingerprint'] + self.signature_id = raw['signature_id'] + self.trust_level = raw['trust_level'] diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py index a84a3d45b2..f99e703d01 100644 --- a/mayan/apps/django_gpg/managers.py +++ b/mayan/apps/django_gpg/managers.py @@ -9,7 +9,7 @@ import gnupg from django.db import models -from .classes import KeyStub +from .classes import KeyStub, SignatureVerification from .exceptions import KeyDoesNotExist, KeyFetchingError from .literals import KEY_TYPE_PUBLIC, KEY_TYPE_SECRET from .settings import setting_gpg_path, setting_keyserver @@ -63,20 +63,36 @@ class KeyManager(models.Manager): def private_keys(self): return self.filter(key_type=KEY_TYPE_SECRET) - def verify_file(self, file_object, signature_file=None): + def verify_file(self, file_object, signature_file=None, key_fingerprint=None, all_keys=False): temporary_directory = tempfile.mkdtemp() gpg = gnupg.GPG( gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value ) + # Preload keys + if all_keys: + for key in Key.objects.all(): + gpg.import_keys(key_data=key.key_data) + elif key_fingerprint: + try: + key = self.get(fingerprint=key_fingerprint) + except self.model.DoesNotExist: + shutil.rmtree(temporary_directory) + raise KeyDoesNotExist('Specified key for verification not found in keyring') + else: + gpg.import_keys(key_data=key.key_data) + verify_result = gpg.verify_file(file=file_object) - if 'no public key' in verify_result.status: + logger.debug('verify_result.status: %s', verify_result.status) + + if 'no public key' in verify_result.status and not key_fingerprint and not all_keys: # File is signed but we need the key for full verification try: key = self.get(fingerprint__endswith=verify_result.key_id) except self.model.DoesNotExist: + shutil.rmtree(temporary_directory) raise KeyDoesNotExist('Signature key is not found in keyring') else: gpg.import_keys(key_data=key.key_data) @@ -85,4 +101,4 @@ class KeyManager(models.Manager): shutil.rmtree(temporary_directory) - return verify_result + return SignatureVerification(verify_result.__dict__) diff --git a/mayan/apps/django_gpg/tests/test_models.py b/mayan/apps/django_gpg/tests/test_models.py index a4fd8426db..484e8b64d9 100644 --- a/mayan/apps/django_gpg/tests/test_models.py +++ b/mayan/apps/django_gpg/tests/test_models.py @@ -46,3 +46,19 @@ class KeyTestCase(TestCase): self.assertTrue(result) self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) + + def test_embedded_verification_with_correct_fingerprint(self): + Key.objects.create(key_data=TEST_KEY_DATA) + + with open(TEST_SIGNED_FILE) as signed_file: + result = Key.objects.verify_file(signed_file, key_fingerprint=TEST_KEY_FINGERPRINT) + + self.assertTrue(result) + self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) + + def test_embedded_verification_with_incorrect_fingerprint(self): + Key.objects.create(key_data=TEST_KEY_DATA) + + with open(TEST_SIGNED_FILE) as signed_file: + with self.assertRaises(KeyDoesNotExist): + Key.objects.verify_file(signed_file, key_fingerprint='999') From 048ba4b5cdb55873a2a0296eeb805e3e6e37aea0 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 23 Mar 2016 19:47:41 -0400 Subject: [PATCH 08/57] Add file decryption support. --- mayan/apps/django_gpg/exceptions.py | 4 ++-- mayan/apps/django_gpg/managers.py | 22 +++++++++++++++++++- mayan/apps/django_gpg/tests/literals.py | 1 + mayan/apps/django_gpg/tests/test_models.py | 24 ++++++++++++++++++++-- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/mayan/apps/django_gpg/exceptions.py b/mayan/apps/django_gpg/exceptions.py index 14dbe5ffa8..0afb08945a 100644 --- a/mayan/apps/django_gpg/exceptions.py +++ b/mayan/apps/django_gpg/exceptions.py @@ -9,7 +9,7 @@ class GPGException(Exception): pass -class GPGVerificationError(GPGException): +class VerificationError(GPGException): pass @@ -17,7 +17,7 @@ class GPGSigningError(GPGException): pass -class GPGDecryptionError(GPGException): +class DecryptionError(GPGException): pass diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py index f99e703d01..e1435ca0dc 100644 --- a/mayan/apps/django_gpg/managers.py +++ b/mayan/apps/django_gpg/managers.py @@ -10,7 +10,7 @@ import gnupg from django.db import models from .classes import KeyStub, SignatureVerification -from .exceptions import KeyDoesNotExist, KeyFetchingError +from .exceptions import DecryptionError, KeyDoesNotExist, KeyFetchingError from .literals import KEY_TYPE_PUBLIC, KEY_TYPE_SECRET from .settings import setting_gpg_path, setting_keyserver @@ -18,6 +18,26 @@ logger = logging.getLogger(__name__) class KeyManager(models.Manager): + def decrypt_file(self, file_object): + temporary_directory = tempfile.mkdtemp() + + os.chmod(temporary_directory, 0x1C0) + + gpg = gnupg.GPG( + gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + ) + + decrypt_result = gpg.decrypt_file(file=file_object) + + shutil.rmtree(temporary_directory) + + logger.debug('decrypt_result.__dict__: %s', decrypt_result.__dict__) + + if not decrypt_result.status or decrypt_result.status == 'no data was provided': + raise DecryptionError('Unable to decrypt file') + + return str(decrypt_result) + def receive_key(self, key_id): temporary_directory = tempfile.mkdtemp() diff --git a/mayan/apps/django_gpg/tests/literals.py b/mayan/apps/django_gpg/tests/literals.py index d37e28ba1f..908181254a 100644 --- a/mayan/apps/django_gpg/tests/literals.py +++ b/mayan/apps/django_gpg/tests/literals.py @@ -77,3 +77,4 @@ TEST_SIGNED_FILE = os.path.join( settings.BASE_DIR, 'mayan', 'apps', 'django_gpg', 'tests', 'contrib', 'test_files', 'test_file.txt.gpg' ) +TEST_SIGNED_FILE_CONTENT = 'test_file.txt\n' diff --git a/mayan/apps/django_gpg/tests/test_models.py b/mayan/apps/django_gpg/tests/test_models.py index 484e8b64d9..fc098846bf 100644 --- a/mayan/apps/django_gpg/tests/test_models.py +++ b/mayan/apps/django_gpg/tests/test_models.py @@ -1,13 +1,15 @@ from __future__ import unicode_literals +import tempfile + from django.test import TestCase -from ..exceptions import KeyDoesNotExist +from ..exceptions import DecryptionError, KeyDoesNotExist from ..models import Key from .literals import ( TEST_KEY_DATA, TEST_KEY_FINGERPRINT, TEST_SEARCH_FINGERPRINT, - TEST_SEARCH_UID, TEST_SIGNED_FILE + TEST_SEARCH_UID, TEST_SIGNED_FILE, TEST_SIGNED_FILE_CONTENT ) @@ -62,3 +64,21 @@ class KeyTestCase(TestCase): with open(TEST_SIGNED_FILE) as signed_file: with self.assertRaises(KeyDoesNotExist): Key.objects.verify_file(signed_file, key_fingerprint='999') + + def test_signed_file_decryption(self): + Key.objects.create(key_data=TEST_KEY_DATA) + + with open(TEST_SIGNED_FILE) as signed_file: + result = Key.objects.decrypt_file(file_object=signed_file) + + self.assertEqual(result, TEST_SIGNED_FILE_CONTENT) + + def test_cleartext_file_decryption(self): + cleartext_file = tempfile.TemporaryFile() + cleartext_file.write('test') + cleartext_file.seek(0) + + with self.assertRaises(DecryptionError): + Key.objects.decrypt_file(file_object=cleartext_file) + + cleartext_file.close() From e96f74843991698a23cb165763892d86208c4f8c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 23 Mar 2016 19:57:16 -0400 Subject: [PATCH 09/57] Add key_id property to SignatureVerification class --- mayan/apps/django_gpg/classes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mayan/apps/django_gpg/classes.py b/mayan/apps/django_gpg/classes.py index 42fa79c76b..5e49317cc8 100644 --- a/mayan/apps/django_gpg/classes.py +++ b/mayan/apps/django_gpg/classes.py @@ -36,3 +36,7 @@ class SignatureVerification(object): self.fingerprint = raw['fingerprint'] self.signature_id = raw['signature_id'] self.trust_level = raw['trust_level'] + + @property + def key_id(self): + return self.fingerprint[-8:] From c8f7c4ef86c8c25469e2df591eb60242944a50b7 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 23 Mar 2016 19:57:45 -0400 Subject: [PATCH 10/57] Update document signature app to work with new django_gpg changes. --- mayan/apps/document_signatures/hooks.py | 24 +++++++++++++--------- mayan/apps/document_signatures/managers.py | 8 ++++---- mayan/apps/document_signatures/models.py | 13 ++++++++---- mayan/apps/document_signatures/views.py | 10 ++++----- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/mayan/apps/document_signatures/hooks.py b/mayan/apps/document_signatures/hooks.py index 0065d2c425..8443e66adc 100644 --- a/mayan/apps/document_signatures/hooks.py +++ b/mayan/apps/document_signatures/hooks.py @@ -4,33 +4,37 @@ import io import logging from django.apps import apps -from django_gpg.exceptions import GPGDecryptionError -from django_gpg.runtime import gpg +from django_gpg.exceptions import DecryptionError logger = logging.getLogger(__name__) -def document_pre_open_hook(descriptor, instance): +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 = gpg.decrypt_file(descriptor, close_descriptor=False) + result = Key.objects.decrypt_file(file_object=file_object) # gpg return a string, turn it into a file like object - except GPGDecryptionError: + except DecryptionError: # At least return the original raw content - descriptor.seek(0) - return descriptor + file_object.seek(0) + return file_object else: - descriptor.close() - return io.BytesIO(result.data) + file_object.close() + return io.BytesIO(result) + #return result else: - return descriptor + return file_object def document_version_post_save_hook(instance): diff --git a/mayan/apps/document_signatures/managers.py b/mayan/apps/document_signatures/managers.py index c03e2b5699..4b9e64b673 100644 --- a/mayan/apps/document_signatures/managers.py +++ b/mayan/apps/document_signatures/managers.py @@ -4,8 +4,8 @@ import logging from django.db import models -from django_gpg.exceptions import GPGVerificationError -from django_gpg.runtime import gpg +from django_gpg.exceptions import VerificationError +from django_gpg.models import Key logger = logging.getLogger(__name__) @@ -84,8 +84,8 @@ class DocumentVersionSignatureManager(models.Manager): args = (document_version_descriptor,) try: - return gpg.verify_file(*args, fetch_key=False) - except GPGVerificationError: + return Key.objects.verify_file(*args) + except VerificationError: return None finally: document_version_descriptor.close() diff --git a/mayan/apps/document_signatures/models.py b/mayan/apps/document_signatures/models.py index 931bdde8eb..cf1ca82e3b 100644 --- a/mayan/apps/document_signatures/models.py +++ b/mayan/apps/document_signatures/models.py @@ -6,7 +6,8 @@ import uuid from django.db import models from django.utils.translation import ugettext_lazy as _ -from django_gpg.runtime import gpg +from django_gpg.exceptions import DecryptionError +from django_gpg.models import Key from documents.models import DocumentVersion from .managers import DocumentVersionSignatureManager @@ -40,9 +41,13 @@ class DocumentVersionSignature(models.Model): logger.debug('checking for embedded signature') with self.document_version.open(raw=True) as file_object: - self.has_embedded_signature = gpg.has_embedded_signature( - 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): diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index 00c691b9b1..0b443402bd 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -10,7 +10,7 @@ from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render_to_response from django.template import RequestContext -from django.template.defaultfilters import force_escape +from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ from acls.models import AccessControlList @@ -70,11 +70,9 @@ def document_verify(request, document_pk): [ _('Signature ID: %s') % signature.signature_id, _('Signature type: %s') % signature_type, - _('Key ID: %s') % signature.key_id, - _('Timestamp: %s') % datetime.fromtimestamp( - int(signature.sig_timestamp) - ), - _('Signee: %s') % force_escape(getattr(signature, 'username', '')), + _('Key fingerprint: %s') % signature.fingerprint, + _('Timestamp: %s') % signature.date, + _('Signee: %s') % escape(signature.user_id), ] ) From a5f3d46373b59e272b9543236dd971b77e33305a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 24 Mar 2016 00:26:53 -0400 Subject: [PATCH 11/57] Add a cleartext test file with a detached signature. --- .../django_gpg/tests/contrib/test_files/test_file.txt | 1 + .../tests/contrib/test_files/test_file.txt.asc | 11 +++++++++++ mayan/apps/django_gpg/tests/literals.py | 10 ++++++++++ 3 files changed, 22 insertions(+) create mode 100644 mayan/apps/django_gpg/tests/contrib/test_files/test_file.txt create mode 100644 mayan/apps/django_gpg/tests/contrib/test_files/test_file.txt.asc diff --git a/mayan/apps/django_gpg/tests/contrib/test_files/test_file.txt b/mayan/apps/django_gpg/tests/contrib/test_files/test_file.txt new file mode 100644 index 0000000000..85cdf2f0a0 --- /dev/null +++ b/mayan/apps/django_gpg/tests/contrib/test_files/test_file.txt @@ -0,0 +1 @@ +test_file content diff --git a/mayan/apps/django_gpg/tests/contrib/test_files/test_file.txt.asc b/mayan/apps/django_gpg/tests/contrib/test_files/test_file.txt.asc new file mode 100644 index 0000000000..7a6ff24ddd --- /dev/null +++ b/mayan/apps/django_gpg/tests/contrib/test_files/test_file.txt.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1 + +iQEcBAABAgAGBQJW8z8CAAoJEEEl6cVx83isoK0IAKa7TGyBBhebx/EM8/tzKwgd +mKmXFtgpSoJhqoLZiBdFVP4gJtxfaDnqSFt7uQA69V0TMqI6cIuX0x7h0lRrNLea ++FQZGsa8HwnJzsTQXIRzszY/wvvFyHfk8jYjBQi7BRwg5kZdW5fUgprYsE8j08WA +cn/VAP5xigxKJfM0ny3pL3mbJj7rdoz+bEu4z7yMg5EsRLbF4MZDU9mUo5QjAvQg +oMMIjv6fFpuvBP9xY/D03IstyqoEEfwl/+36ZQwyGNsA3ZHWXgSb5fwcpXWPdqaD +PuX5l137B2VHz9YNPK9sJdnr5ZRyY0vebzkAvCFPfysAaGWH36iTE58V3uhLNMk= +=yL7D +-----END PGP SIGNATURE----- diff --git a/mayan/apps/django_gpg/tests/literals.py b/mayan/apps/django_gpg/tests/literals.py index 908181254a..a35d0ae523 100644 --- a/mayan/apps/django_gpg/tests/literals.py +++ b/mayan/apps/django_gpg/tests/literals.py @@ -4,6 +4,16 @@ import os from django.conf import settings +TEST_DETACHED_SIGNATURE = os.path.join( + settings.BASE_DIR, 'mayan', 'apps', 'django_gpg', 'tests', 'contrib', + 'test_files', 'test_file.txt.asc' +) + +TEST_FILE = os.path.join( + settings.BASE_DIR, 'mayan', 'apps', 'django_gpg', 'tests', 'contrib', + 'test_files', 'test_file.txt' +) + TEST_KEY_DATA = '''-----BEGIN PGP PRIVATE KEY BLOCK----- Version: GnuPG v1 From d41dac5587b7fa2b22e4e3fdbcf44aae174d5184 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 24 Mar 2016 00:27:49 -0400 Subject: [PATCH 12/57] Add support for verification of detached signatures. --- mayan/apps/django_gpg/managers.py | 66 ++++++++++++++----- mayan/apps/django_gpg/tests/test_models.py | 23 ++++++- mayan/apps/document_signatures/hooks.py | 1 - .../document_signatures/tests/test_models.py | 7 +- 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py index e1435ca0dc..940f207e08 100644 --- a/mayan/apps/django_gpg/managers.py +++ b/mayan/apps/django_gpg/managers.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, unicode_literals +import io import logging import os import shutil @@ -10,7 +11,9 @@ import gnupg from django.db import models from .classes import KeyStub, SignatureVerification -from .exceptions import DecryptionError, KeyDoesNotExist, KeyFetchingError +from .exceptions import ( + DecryptionError, KeyDoesNotExist, KeyFetchingError, VerificationError +) from .literals import KEY_TYPE_PUBLIC, KEY_TYPE_SECRET from .settings import setting_gpg_path, setting_keyserver @@ -31,7 +34,7 @@ class KeyManager(models.Manager): shutil.rmtree(temporary_directory) - logger.debug('decrypt_result.__dict__: %s', decrypt_result.__dict__) + logger.debug('decrypt_result.status: %s', decrypt_result.status) if not decrypt_result.status or decrypt_result.status == 'no data was provided': raise DecryptionError('Unable to decrypt file') @@ -83,7 +86,7 @@ class KeyManager(models.Manager): def private_keys(self): return self.filter(key_type=KEY_TYPE_SECRET) - def verify_file(self, file_object, signature_file=None, key_fingerprint=None, all_keys=False): + def verify_file(self, file_object, signature_file=None, key_id=None, key_fingerprint=None, all_keys=False): temporary_directory = tempfile.mkdtemp() gpg = gnupg.GPG( @@ -102,23 +105,50 @@ class KeyManager(models.Manager): raise KeyDoesNotExist('Specified key for verification not found in keyring') else: gpg.import_keys(key_data=key.key_data) + elif key_id: + try: + key = self.get(fingerprint__endswith=key_id) + except self.model.DoesNotExist: + shutil.rmtree(temporary_directory) + raise KeyDoesNotExist('Specified key for verification not found in keyring') + else: + gpg.import_keys(key_data=key.key_data) - verify_result = gpg.verify_file(file=file_object) + if signature_file: + # Save the original data and invert the argument order + # Signature first, file second + temporary_file_object, temporary_filename = tempfile.mkstemp() + os.write(temporary_file_object, file_object.read()) + os.close(temporary_file_object) + + signature_file_buffer = io.BytesIO() + signature_file_buffer.write(signature_file.read()) + signature_file_buffer.seek(0) + verify_result = gpg.verify_file( + file=signature_file_buffer, data_filename=temporary_filename + ) + signature_file_buffer.close() + # TODO: delete file + else: + verify_result = gpg.verify_file(file=file_object) logger.debug('verify_result.status: %s', verify_result.status) - if 'no public key' in verify_result.status and not key_fingerprint and not all_keys: + if verify_result: + shutil.rmtree(temporary_directory) + SignatureVerification(verify_result.__dict__) + elif verify_result.status == 'no public key' and not (key_fingerprint or all_keys): # File is signed but we need the key for full verification - try: - key = self.get(fingerprint__endswith=verify_result.key_id) - except self.model.DoesNotExist: - shutil.rmtree(temporary_directory) - raise KeyDoesNotExist('Signature key is not found in keyring') - else: - gpg.import_keys(key_data=key.key_data) - file_object.seek(0) - verify_result = gpg.verify_file(file=file_object) - - shutil.rmtree(temporary_directory) - - return SignatureVerification(verify_result.__dict__) + #try: + # key = self.get(fingerprint__endswith=verify_result.key_id) + #except self.model.DoesNotExist: + # shutil.rmtree(temporary_directory) + # raise KeyDoesNotExist('Signature key is not found in keyring') + #else: + # gpg.import_keys(key_data=key.key_data) + file_object.seek(0) + return self.verify_file(file_object=file_object, signature_file=signature_file, key_id=verify_result.key_id, key_fingerprint=key_fingerprint, all_keys=all_keys) + # verify_result = gpg.verify_file(file=file_object) + else: + shutil.rmtree(temporary_directory) + raise VerificationError('File not signed') diff --git a/mayan/apps/django_gpg/tests/test_models.py b/mayan/apps/django_gpg/tests/test_models.py index fc098846bf..13a6e9d5bf 100644 --- a/mayan/apps/django_gpg/tests/test_models.py +++ b/mayan/apps/django_gpg/tests/test_models.py @@ -4,12 +4,13 @@ import tempfile from django.test import TestCase -from ..exceptions import DecryptionError, KeyDoesNotExist +from ..exceptions import DecryptionError, KeyDoesNotExist, VerificationError from ..models import Key from .literals import ( - TEST_KEY_DATA, TEST_KEY_FINGERPRINT, TEST_SEARCH_FINGERPRINT, - TEST_SEARCH_UID, TEST_SIGNED_FILE, TEST_SIGNED_FILE_CONTENT + TEST_DETACHED_SIGNATURE, TEST_FILE, TEST_KEY_DATA, TEST_KEY_FINGERPRINT, + TEST_SEARCH_FINGERPRINT, TEST_SEARCH_UID, TEST_SIGNED_FILE, + TEST_SIGNED_FILE_CONTENT ) @@ -82,3 +83,19 @@ class KeyTestCase(TestCase): Key.objects.decrypt_file(file_object=cleartext_file) cleartext_file.close() + + def test_detached_verification_no_key(self): + with open(TEST_DETACHED_SIGNATURE) as signature_file: + with open(TEST_FILE) as test_file: + with self.assertRaises(VerificationError): + Key.objects.verify_file(file_object=test_file, signature_file=signature_file) + + def test_detached_verification_with_key(self): + Key.objects.create(key_data=TEST_KEY_DATA) + + with open(TEST_DETACHED_SIGNATURE) as signature_file: + with open(TEST_FILE) as test_file: + result = Key.objects.verify_file(file_object=test_file, signature_file=signature_file) + + self.assertTrue(result) + self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) diff --git a/mayan/apps/document_signatures/hooks.py b/mayan/apps/document_signatures/hooks.py index 8443e66adc..323edeb386 100644 --- a/mayan/apps/document_signatures/hooks.py +++ b/mayan/apps/document_signatures/hooks.py @@ -32,7 +32,6 @@ def document_pre_open_hook(file_object, instance): else: file_object.close() return io.BytesIO(result) - #return result else: return file_object diff --git a/mayan/apps/document_signatures/tests/test_models.py b/mayan/apps/document_signatures/tests/test_models.py index 7f1733db77..5516e029e5 100644 --- a/mayan/apps/document_signatures/tests/test_models.py +++ b/mayan/apps/document_signatures/tests/test_models.py @@ -7,10 +7,9 @@ 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 django_gpg.literals import SIGNATURE_STATE_VALID -from django_gpg.runtime import gpg from ..models import DocumentVersionSignature @@ -39,7 +38,7 @@ class DocumentTestCase(TestCase): ) with open(TEST_KEY_FILE) as file_object: - gpg.import_key(file_object.read()) + Key.objects.create(key_data=file_object.read()) def tearDown(self): self.document_type.delete() @@ -60,7 +59,7 @@ class DocumentTestCase(TestCase): # Artifical delay since MySQL doesn't store microsecond data in # timestamps. Version timestamp is used to determine which version # is the latest. - time.sleep(1) + time.sleep(2) self.assertEqual( DocumentVersionSignature.objects.has_detached_signature( From ab6e2d8c230c4249677bb783f5c079cb439790b5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 24 Mar 2016 02:47:53 -0400 Subject: [PATCH 13/57] Fix verification using detached signature. --- mayan/apps/django_gpg/managers.py | 20 ++++++-------------- mayan/apps/django_gpg/tests/test_models.py | 6 +++--- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py index 940f207e08..79a29b5af5 100644 --- a/mayan/apps/django_gpg/managers.py +++ b/mayan/apps/django_gpg/managers.py @@ -112,7 +112,7 @@ class KeyManager(models.Manager): shutil.rmtree(temporary_directory) raise KeyDoesNotExist('Specified key for verification not found in keyring') else: - gpg.import_keys(key_data=key.key_data) + result = gpg.import_keys(key_data=key.key_data) if signature_file: # Save the original data and invert the argument order @@ -124,11 +124,12 @@ class KeyManager(models.Manager): signature_file_buffer = io.BytesIO() signature_file_buffer.write(signature_file.read()) signature_file_buffer.seek(0) + signature_file.seek(0) verify_result = gpg.verify_file( file=signature_file_buffer, data_filename=temporary_filename ) signature_file_buffer.close() - # TODO: delete file + os.unlink(temporary_filename) else: verify_result = gpg.verify_file(file=file_object) @@ -136,19 +137,10 @@ class KeyManager(models.Manager): if verify_result: shutil.rmtree(temporary_directory) - SignatureVerification(verify_result.__dict__) - elif verify_result.status == 'no public key' and not (key_fingerprint or all_keys): - # File is signed but we need the key for full verification - #try: - # key = self.get(fingerprint__endswith=verify_result.key_id) - #except self.model.DoesNotExist: - # shutil.rmtree(temporary_directory) - # raise KeyDoesNotExist('Signature key is not found in keyring') - #else: - # gpg.import_keys(key_data=key.key_data) + return SignatureVerification(verify_result.__dict__) + elif verify_result.status == 'no public key' and not (key_fingerprint or all_keys or key_id): file_object.seek(0) - return self.verify_file(file_object=file_object, signature_file=signature_file, key_id=verify_result.key_id, key_fingerprint=key_fingerprint, all_keys=all_keys) - # verify_result = gpg.verify_file(file=file_object) + return self.verify_file(file_object=file_object, signature_file=signature_file, key_id=verify_result.key_id) else: shutil.rmtree(temporary_directory) raise VerificationError('File not signed') diff --git a/mayan/apps/django_gpg/tests/test_models.py b/mayan/apps/django_gpg/tests/test_models.py index 13a6e9d5bf..5b11afd959 100644 --- a/mayan/apps/django_gpg/tests/test_models.py +++ b/mayan/apps/django_gpg/tests/test_models.py @@ -47,7 +47,7 @@ class KeyTestCase(TestCase): with open(TEST_SIGNED_FILE) as signed_file: result = Key.objects.verify_file(signed_file) - self.assertTrue(result) + self.assertTrue(result.valid) self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) def test_embedded_verification_with_correct_fingerprint(self): @@ -56,7 +56,7 @@ class KeyTestCase(TestCase): with open(TEST_SIGNED_FILE) as signed_file: result = Key.objects.verify_file(signed_file, key_fingerprint=TEST_KEY_FINGERPRINT) - self.assertTrue(result) + self.assertTrue(result.valid) self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) def test_embedded_verification_with_incorrect_fingerprint(self): @@ -87,7 +87,7 @@ class KeyTestCase(TestCase): def test_detached_verification_no_key(self): with open(TEST_DETACHED_SIGNATURE) as signature_file: with open(TEST_FILE) as test_file: - with self.assertRaises(VerificationError): + with self.assertRaises(KeyDoesNotExist): Key.objects.verify_file(file_object=test_file, signature_file=signature_file) def test_detached_verification_with_key(self): From 355190e919444510402917d657eb669c0de836e4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Mar 2016 04:07:57 -0400 Subject: [PATCH 14/57] Start of document_signatures app refactor. --- HISTORY.rst | 1 + mayan/apps/document_signatures/admin.py | 5 +- mayan/apps/document_signatures/apps.py | 96 ++++++++--- mayan/apps/document_signatures/forms.py | 41 +++++ mayan/apps/document_signatures/hooks.py | 54 ------ mayan/apps/document_signatures/links.py | 79 +++++---- mayan/apps/document_signatures/managers.py | 119 ++++++------- .../migrations/0003_auto_20160325_0052.py | 78 +++++++++ .../migrations/0004_auto_20160325_0418.py | 39 +++++ .../migrations/0005_auto_20160325_0748.py | 21 +++ mayan/apps/document_signatures/models.py | 95 +++++++---- mayan/apps/document_signatures/permissions.py | 24 ++- .../document_signatures/tests/test_models.py | 133 +++++++++------ mayan/apps/document_signatures/urls.py | 34 +++- mayan/apps/document_signatures/views.py | 156 +++++++++++------- .../migrations/0033_auto_20160325_0052.py | 18 ++ 16 files changed, 650 insertions(+), 343 deletions(-) delete mode 100644 mayan/apps/document_signatures/hooks.py create mode 100644 mayan/apps/document_signatures/migrations/0003_auto_20160325_0052.py create mode 100644 mayan/apps/document_signatures/migrations/0004_auto_20160325_0418.py create mode 100644 mayan/apps/document_signatures/migrations/0005_auto_20160325_0748.py create mode 100644 mayan/apps/documents/migrations/0033_auto_20160325_0052.py 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'}, + ), + ] From 09b92858d9a7d02a1c2b3095d6acef245de3917a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Mar 2016 04:08:25 -0400 Subject: [PATCH 15/57] Raise MayanAppConfig initialization exception if they are not bening about missing URLs. --- mayan/apps/common/apps.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mayan/apps/common/apps.py b/mayan/apps/common/apps.py index a514036aff..457ecc2073 100644 --- a/mayan/apps/common/apps.py +++ b/mayan/apps/common/apps.py @@ -61,6 +61,8 @@ class MayanAppConfig(apps.AppConfig): 'App %s doesn\'t have URLs defined. Exception: %s', self.name, exception ) + if 'No module named urls' not in unicode(exception): + raise exception class CommonApp(MayanAppConfig): From 7e801ef02e8ad1501040ef75911109855e0caef1 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Mar 2016 04:09:30 -0400 Subject: [PATCH 16/57] Fix verifyresult timestamp field. Remove Key ID property and verify return a separate small key id without a fingerprint when the signing key is not available. --- mayan/apps/django_gpg/classes.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mayan/apps/django_gpg/classes.py b/mayan/apps/django_gpg/classes.py index 5e49317cc8..510f14ada0 100644 --- a/mayan/apps/django_gpg/classes.py +++ b/mayan/apps/django_gpg/classes.py @@ -24,8 +24,9 @@ class SignatureVerification(object): def __init__(self, raw): self.user_id = raw['username'] self.status = raw['status'] + self.key_id = raw['key_id'] self.pubkey_fingerprint = raw['pubkey_fingerprint'] - self.date = date.fromtimestamp(int(raw['sig_timestamp'])) + self.date = date.fromtimestamp(int(raw['timestamp'])) if raw['expire_timestamp']: self.expires = date.fromtimestamp(int(raw['expire_timestamp'])) else: @@ -36,7 +37,3 @@ class SignatureVerification(object): self.fingerprint = raw['fingerprint'] self.signature_id = raw['signature_id'] self.trust_level = raw['trust_level'] - - @property - def key_id(self): - return self.fingerprint[-8:] From ffb29e0f54239ee922cf338702954f344593923b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Mar 2016 04:10:20 -0400 Subject: [PATCH 17/57] Remove obsolete Document property. Use resolved_object in documents links to avoid context variable clashes with signatures. --- mayan/apps/django_gpg/managers.py | 18 ++++++++--- mayan/apps/django_gpg/tests/test_models.py | 37 ++++++++++++++++------ mayan/apps/documents/links.py | 22 ++++++------- mayan/apps/documents/models.py | 4 --- 4 files changed, 52 insertions(+), 29 deletions(-) diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py index 79a29b5af5..3ef0545fa2 100644 --- a/mayan/apps/django_gpg/managers.py +++ b/mayan/apps/django_gpg/managers.py @@ -39,7 +39,9 @@ class KeyManager(models.Manager): if not decrypt_result.status or decrypt_result.status == 'no data was provided': raise DecryptionError('Unable to decrypt file') - return str(decrypt_result) + file_object.close() + + return io.BytesIO(str(decrypt_result)) def receive_key(self, key_id): temporary_directory = tempfile.mkdtemp() @@ -109,8 +111,9 @@ class KeyManager(models.Manager): try: key = self.get(fingerprint__endswith=key_id) except self.model.DoesNotExist: - shutil.rmtree(temporary_directory) - raise KeyDoesNotExist('Specified key for verification not found in keyring') + pass + #shutil.rmtree(temporary_directory) + #raise KeyDoesNotExist('Specified key for verification not found in keyring') else: result = gpg.import_keys(key_data=key.key_data) @@ -135,12 +138,17 @@ class KeyManager(models.Manager): logger.debug('verify_result.status: %s', verify_result.status) + shutil.rmtree(temporary_directory) + if verify_result: - shutil.rmtree(temporary_directory) + # Signed and key present return SignatureVerification(verify_result.__dict__) elif verify_result.status == 'no public key' and not (key_fingerprint or all_keys or key_id): + # Signed but key not present, retry with key fetch file_object.seek(0) return self.verify_file(file_object=file_object, signature_file=signature_file, key_id=verify_result.key_id) + elif verify_result.key_id: + # Signed, retried and key still not found + return SignatureVerification(verify_result.__dict__) else: - shutil.rmtree(temporary_directory) raise VerificationError('File not signed') diff --git a/mayan/apps/django_gpg/tests/test_models.py b/mayan/apps/django_gpg/tests/test_models.py index 5b11afd959..e871ecf1b3 100644 --- a/mayan/apps/django_gpg/tests/test_models.py +++ b/mayan/apps/django_gpg/tests/test_models.py @@ -34,12 +34,25 @@ class KeyTestCase(TestCase): Key.objects.receive_key(key_id=TEST_SEARCH_FINGERPRINT) self.assertEqual(Key.objects.all().count(), 1) - self.assertEqual(Key.objects.first().fingerprint, TEST_SEARCH_FINGERPRINT) + self.assertEqual( + Key.objects.first().fingerprint, TEST_SEARCH_FINGERPRINT + ) + + def test_cleartext_file_verification(self): + cleartext_file = tempfile.TemporaryFile() + cleartext_file.write('test') + cleartext_file.seek(0) + + with self.assertRaises(VerificationError): + Key.objects.verify_file(file_object=cleartext_file) + + cleartext_file.close() def test_embedded_verification_no_key(self): with open(TEST_SIGNED_FILE) as signed_file: - with self.assertRaises(KeyDoesNotExist): - Key.objects.verify_file(signed_file) + result = Key.objects.verify_file(signed_file) + + self.assertTrue(result.key_id in TEST_KEY_FINGERPRINT) def test_embedded_verification_with_key(self): Key.objects.create(key_data=TEST_KEY_DATA) @@ -47,14 +60,15 @@ class KeyTestCase(TestCase): with open(TEST_SIGNED_FILE) as signed_file: result = Key.objects.verify_file(signed_file) - self.assertTrue(result.valid) self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) def test_embedded_verification_with_correct_fingerprint(self): Key.objects.create(key_data=TEST_KEY_DATA) with open(TEST_SIGNED_FILE) as signed_file: - result = Key.objects.verify_file(signed_file, key_fingerprint=TEST_KEY_FINGERPRINT) + result = Key.objects.verify_file( + signed_file, key_fingerprint=TEST_KEY_FINGERPRINT + ) self.assertTrue(result.valid) self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) @@ -72,7 +86,7 @@ class KeyTestCase(TestCase): with open(TEST_SIGNED_FILE) as signed_file: result = Key.objects.decrypt_file(file_object=signed_file) - self.assertEqual(result, TEST_SIGNED_FILE_CONTENT) + self.assertEqual(result.read(), TEST_SIGNED_FILE_CONTENT) def test_cleartext_file_decryption(self): cleartext_file = tempfile.TemporaryFile() @@ -87,15 +101,20 @@ class KeyTestCase(TestCase): def test_detached_verification_no_key(self): with open(TEST_DETACHED_SIGNATURE) as signature_file: with open(TEST_FILE) as test_file: - with self.assertRaises(KeyDoesNotExist): - Key.objects.verify_file(file_object=test_file, signature_file=signature_file) + result = Key.objects.verify_file( + file_object=test_file, signature_file=signature_file + ) + + self.assertTrue(result.key_id in TEST_KEY_FINGERPRINT) def test_detached_verification_with_key(self): Key.objects.create(key_data=TEST_KEY_DATA) with open(TEST_DETACHED_SIGNATURE) as signature_file: with open(TEST_FILE) as test_file: - result = Key.objects.verify_file(file_object=test_file, signature_file=signature_file) + result = Key.objects.verify_file( + file_object=test_file, signature_file=signature_file + ) self.assertTrue(result) self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) diff --git a/mayan/apps/documents/links.py b/mayan/apps/documents/links.py index e48512402b..f1bb286c83 100644 --- a/mayan/apps/documents/links.py +++ b/mayan/apps/documents/links.py @@ -18,7 +18,7 @@ from .settings import setting_zoom_max_level, setting_zoom_min_level def is_not_current_version(context): - return context['object'].document.latest_version.timestamp != context['object'].timestamp + return context['resolved_object'].document.latest_version.timestamp != context['resolved_object'].timestamp def is_first_page(context): @@ -40,12 +40,12 @@ def is_min_zoom(context): # Facet link_document_preview = Link( icon='fa fa-eye', permissions=(permission_document_view,), - text=_('Preview'), view='documents:document_preview', args='object.id' + text=_('Preview'), view='documents:document_preview', args='resolved_object.id' ) link_document_properties = Link( icon='fa fa-info', permissions=(permission_document_view,), text=_('Properties'), view='documents:document_properties', - args='object.id' + args='resolved_object.id' ) link_document_version_list = Link( icon='fa fa-code-fork', permissions=(permission_document_view,), @@ -61,32 +61,32 @@ link_document_pages = Link( link_document_clear_transformations = Link( permissions=(permission_transformation_delete,), text=_('Clear transformations'), - view='documents:document_clear_transformations', args='object.id' + view='documents:document_clear_transformations', args='resolved_object.id' ) link_document_delete = Link( permissions=(permission_document_delete,), tags='dangerous', - text=_('Delete'), view='documents:document_delete', args='object.id' + text=_('Delete'), view='documents:document_delete', args='resolved_object.id' ) link_document_trash = Link( permissions=(permission_document_trash,), tags='dangerous', - text=_('Move to trash'), view='documents:document_trash', args='object.id' + text=_('Move to trash'), view='documents:document_trash', args='resolved_object.id' ) link_document_edit = Link( permissions=(permission_document_properties_edit,), text=_('Edit properties'), view='documents:document_edit', - args='object.id' + args='resolved_object.id' ) link_document_document_type_edit = Link( permissions=(permission_document_properties_edit,), text=_('Change type'), - view='documents:document_document_type_edit', args='object.id' + view='documents:document_document_type_edit', args='resolved_object.id' ) link_document_download = Link( permissions=(permission_document_download,), text=_('Download'), - view='documents:document_download', args='object.id' + view='documents:document_download', args='resolved_object.id' ) link_document_print = Link( permissions=(permission_document_print,), text=_('Print'), - view='documents:document_print', args='object.id' + view='documents:document_print', args='resolved_object.id' ) link_document_update_page_count = Link( permissions=(permission_document_tools,), text=_('Recalculate page count'), @@ -125,7 +125,7 @@ link_document_multiple_restore = Link( ) link_document_version_download = Link( args='object.pk', permissions=(permission_document_download,), - text=_('Download'), view='documents:document_version_download' + text=_('Download version'), view='documents:document_version_download' ) # Views diff --git a/mayan/apps/documents/models.py b/mayan/apps/documents/models.py index 7f32be5832..04f0d49a3d 100644 --- a/mayan/apps/documents/models.py +++ b/mayan/apps/documents/models.py @@ -322,10 +322,6 @@ class Document(models.Model): # Document has no version yet return 0 - @property - def signature_state(self): - return self.latest_version.signature_state - class DeletedDocument(Document): objects = TrashCanManager() From 467ad0dadb0b6684dc517b9b72d608ecd42da164 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 26 Mar 2016 04:04:34 -0400 Subject: [PATCH 18/57] Add more logging. Add preloading of keys to decrypt_file method. Cleanups. --- mayan/apps/django_gpg/managers.py | 54 +++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py index 3ef0545fa2..b6bdbab7db 100644 --- a/mayan/apps/django_gpg/managers.py +++ b/mayan/apps/django_gpg/managers.py @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) class KeyManager(models.Manager): - def decrypt_file(self, file_object): + def decrypt_file(self, file_object, all_keys=False, key_fingerprint=None, key_id=None): temporary_directory = tempfile.mkdtemp() os.chmod(temporary_directory, 0x1C0) @@ -30,6 +30,33 @@ class KeyManager(models.Manager): gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value ) + # Preload keys + if all_keys: + logger.debug('preloading all keys') + for key in Key.objects.all(): + gpg.import_keys(key_data=key.key_data) + elif key_fingerprint: + logger.debug('preloading key fingerprint: %s', key_fingerprint) + try: + key = self.get(fingerprint=key_fingerprint) + except self.model.DoesNotExist: + logger.debug('key fingerprint %s not found', key_fingerprint) + shutil.rmtree(temporary_directory) + raise KeyDoesNotExist( + 'Specified key for verification not found' + ) + else: + gpg.import_keys(key_data=key.key_data) + elif key_id: + logger.debug('preloading key id: %s', key_id) + try: + key = self.get(fingerprint__endswith=key_id) + except self.model.DoesNotExist: + logger.debug('key id %s not found', key_id) + else: + gpg.import_keys(key_data=key.key_data) + logger.debug('key id %s impored', key_id) + decrypt_result = gpg.decrypt_file(file=file_object) shutil.rmtree(temporary_directory) @@ -67,6 +94,8 @@ class KeyManager(models.Manager): def search(self, query): temporary_directory = tempfile.mkdtemp() + os.chmod(temporary_directory, 0x1C0) + gpg = gnupg.GPG( gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value ) @@ -88,34 +117,41 @@ class KeyManager(models.Manager): def private_keys(self): return self.filter(key_type=KEY_TYPE_SECRET) - def verify_file(self, file_object, signature_file=None, key_id=None, key_fingerprint=None, all_keys=False): + def verify_file(self, file_object, signature_file=None, all_keys=False, key_fingerprint=None, key_id=None): temporary_directory = tempfile.mkdtemp() + os.chmod(temporary_directory, 0x1C0) + gpg = gnupg.GPG( gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value ) # Preload keys if all_keys: + logger.debug('preloading all keys') for key in Key.objects.all(): gpg.import_keys(key_data=key.key_data) elif key_fingerprint: + logger.debug('preloading key fingerprint: %s', key_fingerprint) try: key = self.get(fingerprint=key_fingerprint) except self.model.DoesNotExist: + logger.debug('key fingerprint %s not found', key_fingerprint) shutil.rmtree(temporary_directory) - raise KeyDoesNotExist('Specified key for verification not found in keyring') + raise KeyDoesNotExist( + 'Specified key for verification not found' + ) else: gpg.import_keys(key_data=key.key_data) elif key_id: + logger.debug('preloading key id: %s', key_id) try: key = self.get(fingerprint__endswith=key_id) except self.model.DoesNotExist: - pass - #shutil.rmtree(temporary_directory) - #raise KeyDoesNotExist('Specified key for verification not found in keyring') + logger.debug('key id %s not found', key_id) else: - result = gpg.import_keys(key_data=key.key_data) + gpg.import_keys(key_data=key.key_data) + logger.debug('key id %s impored', key_id) if signature_file: # Save the original data and invert the argument order @@ -142,13 +178,17 @@ class KeyManager(models.Manager): if verify_result: # Signed and key present + logger.debug('signed and key present') return SignatureVerification(verify_result.__dict__) elif verify_result.status == 'no public key' and not (key_fingerprint or all_keys or key_id): # Signed but key not present, retry with key fetch + logger.debug('no public key') file_object.seek(0) return self.verify_file(file_object=file_object, signature_file=signature_file, key_id=verify_result.key_id) elif verify_result.key_id: # Signed, retried and key still not found + logger.debug('signed, retried and key still not found') return SignatureVerification(verify_result.__dict__) else: + logger.debug('file not signed') raise VerificationError('File not signed') From 4a4573fb1bc3f0845a3e331eeb2ded35e0d011b8 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 26 Mar 2016 04:05:15 -0400 Subject: [PATCH 19/57] Add missing gpg directory chmod. --- mayan/apps/django_gpg/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py index 9235adb65c..9f4ab45239 100644 --- a/mayan/apps/django_gpg/models.py +++ b/mayan/apps/django_gpg/models.py @@ -88,6 +88,8 @@ class Key(models.Model): def save(self, *args, **kwargs): temporary_directory = tempfile.mkdtemp() + os.chmod(temporary_directory, 0x1C0) + gpg = gnupg.GPG( gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value ) @@ -120,6 +122,8 @@ class Key(models.Model): def sign_file(self, file_object, passphrase=None, clearsign=True, detached=False, binary=False, output=None): temporary_directory = tempfile.mkdtemp() + os.chmod(temporary_directory, 0x1C0) + gpg = gnupg.GPG( gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value ) From 9744cdd35879a4cd6e7d5d8ab986df7b7f6b4440 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 26 Mar 2016 04:05:40 -0400 Subject: [PATCH 20/57] Redirect to public or private key list after deletion of a key. --- mayan/apps/django_gpg/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mayan/apps/django_gpg/views.py b/mayan/apps/django_gpg/views.py index 54e575fde6..4defb8c90b 100644 --- a/mayan/apps/django_gpg/views.py +++ b/mayan/apps/django_gpg/views.py @@ -12,6 +12,7 @@ from common.generics import ( ) from .forms import KeyDetailForm, KeySearchForm +from .literals import KEY_TYPE_PUBLIC from .models import Key from .permissions import ( permission_key_delete, permission_key_receive, permission_key_view, @@ -25,6 +26,12 @@ class KeyDeleteView(SingleObjectDeleteView): model = Key object_permission = permission_key_delete + def get_post_action_redirect(self): + if self.get_object().key_type == KEY_TYPE_PUBLIC: + post_action_redirect = reverse_lazy('django_gpg:key_public_list') + else: + post_action_redirect = reverse_lazy('django_gpg:key_private_list') + def get_extra_context(self): return { 'title': _('Delete key'), From 779a14977d57fa1b7b6a9f5b87a4e4e6e0e5fb2f Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 26 Mar 2016 04:06:08 -0400 Subject: [PATCH 21/57] Add admin interface for embedded and detached signatures. --- mayan/apps/document_signatures/admin.py | 29 +++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/mayan/apps/document_signatures/admin.py b/mayan/apps/document_signatures/admin.py index 4d069195b9..c930da3b3d 100644 --- a/mayan/apps/document_signatures/admin.py +++ b/mayan/apps/document_signatures/admin.py @@ -2,22 +2,23 @@ from __future__ import unicode_literals from django.contrib import admin -#from .models import DocumentVersionSignature +from .models import DetachedSignature, EmbeddedSignature -""" -@admin.register(DocumentVersionSignature) -class DocumentVersionSignatureAdmin(admin.ModelAdmin): - def document(self, instance): - return instance.document_version.document - def has_detached_signature(self, instance): - return True if instance.signature_file else False - - has_detached_signature.boolean = True +@admin.register(DetachedSignature) +class DetachedSignatureAdmin(admin.ModelAdmin): list_display = ( - 'document', 'document_version', 'has_embedded_signature', - 'has_detached_signature' + 'document_version', 'date', 'key_id', 'signature_id', + 'public_key_fingerprint', 'signature_file' ) list_display_links = ('document_version',) - search_fields = ('document_version__document__label',) -""" + + +@admin.register(EmbeddedSignature) +class EmbeddedSignatureAdmin(admin.ModelAdmin): + list_display = ( + 'document_version', 'date', 'key_id', 'signature_id', + 'public_key_fingerprint' + ) + list_display_links = ('document_version',) + From 1f0dedc9aa3b2d75893edea9a5758d4b6cefb22e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 26 Mar 2016 04:06:43 -0400 Subject: [PATCH 22/57] Add more fields to signature detail form. --- mayan/apps/document_signatures/forms.py | 74 +++++++++++++++++-------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/mayan/apps/document_signatures/forms.py b/mayan/apps/document_signatures/forms.py index 050597aadf..b323b1abf6 100644 --- a/mayan/apps/document_signatures/forms.py +++ b/mayan/apps/document_signatures/forms.py @@ -4,6 +4,7 @@ from django import forms from django.utils.translation import ugettext_lazy as _ from common.forms import DetailForm +from django_gpg.models import Key from .models import SignatureBaseModel @@ -17,35 +18,60 @@ class DetachedSignatureForm(forms.Form): 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'}, + {'label': _('Signature is embedded?'), 'field': 'is_embedded'}, + { + 'label': _('Signature date'), 'field': 'date', + 'widget': forms.widgets.DateInput + }, + {'label': _('Signature key ID'), 'field': 'key_id'}, + { + 'label': _('Signature key present?'), + 'field': lambda x: x.public_key_fingerprint is not None + }, ) + if kwargs['instance'].public_key_fingerprint: + key = Key.objects.get( + fingerprint=kwargs['instance'].public_key_fingerprint + ) + + extra_fields += ( + {'label': _('Signature ID'), 'field': 'signature_id'}, + { + 'label': _('Key fingerprint'), + 'field': lambda x: key.fingerprint + }, + { + 'label': _('Key creation date'), + 'field': lambda x: key.creation_date, + 'widget': forms.widgets.DateInput + }, + { + 'label': _('Key expiration date'), + 'field': lambda x: key.expiration_date or _('None'), + 'widget': forms.widgets.DateInput + }, + { + 'label': _('Key length'), + 'field': lambda x: key.length + }, + { + 'label': _('Key algorithm'), + 'field': lambda x: key.algorithm + }, + { + 'label': _('Key user ID'), + 'field': lambda x: key.user_id + }, + { + 'label': _('Key type'), + 'field': lambda x: key.get_key_type_display() + }, + ) + 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()}, -""" From 5de63c44779b9e4a505007184807c042b782a6da Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 26 Mar 2016 04:23:16 -0400 Subject: [PATCH 23/57] Add support for verifying and unverifying signatures after a key is added or deleted. --- mayan/apps/django_gpg/apps.py | 2 - mayan/apps/django_gpg/exceptions.py | 6 +- mayan/apps/django_gpg/managers.py | 4 +- mayan/apps/django_gpg/views.py | 4 +- mayan/apps/document_signatures/admin.py | 1 - mayan/apps/document_signatures/apps.py | 40 ++++- mayan/apps/document_signatures/handlers.py | 15 ++ mayan/apps/document_signatures/managers.py | 61 ------- .../migrations/0006_auto_20160326_0616.py | 19 +++ mayan/apps/document_signatures/models.py | 51 +++++- mayan/apps/document_signatures/tasks.py | 50 ++++++ .../document_signatures/tests/test_models.py | 152 ++++++++++++++++-- mayan/apps/document_signatures/views.py | 60 +------ mayan/apps/documents/models.py | 8 +- 14 files changed, 325 insertions(+), 148 deletions(-) create mode 100644 mayan/apps/document_signatures/handlers.py create mode 100644 mayan/apps/document_signatures/migrations/0006_auto_20160326_0616.py create mode 100644 mayan/apps/document_signatures/tasks.py diff --git a/mayan/apps/django_gpg/apps.py b/mayan/apps/django_gpg/apps.py index 4efb36ac89..f187bf25ba 100644 --- a/mayan/apps/django_gpg/apps.py +++ b/mayan/apps/django_gpg/apps.py @@ -1,7 +1,5 @@ from __future__ import absolute_import, unicode_literals -from datetime import datetime - from django.utils.translation import ugettext_lazy as _ from acls import ModelPermission diff --git a/mayan/apps/django_gpg/exceptions.py b/mayan/apps/django_gpg/exceptions.py index 0afb08945a..a725ee0e8a 100644 --- a/mayan/apps/django_gpg/exceptions.py +++ b/mayan/apps/django_gpg/exceptions.py @@ -1,6 +1,6 @@ __all__ = ( - 'GPGException', 'GPGVerificationError', 'GPGSigningError', - 'GPGDecryptionError', 'KeyDeleteError', 'KeyGenerationError', + 'GPGException', 'VerificationError', 'SigningError', + 'DecryptionError', 'KeyDeleteError', 'KeyGenerationError', 'KeyFetchingError', 'KeyDoesNotExist', 'KeyImportError' ) @@ -13,7 +13,7 @@ class VerificationError(GPGException): pass -class GPGSigningError(GPGException): +class SigningError(GPGException): pass diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py index b6bdbab7db..1a26a4e0cf 100644 --- a/mayan/apps/django_gpg/managers.py +++ b/mayan/apps/django_gpg/managers.py @@ -33,7 +33,7 @@ class KeyManager(models.Manager): # Preload keys if all_keys: logger.debug('preloading all keys') - for key in Key.objects.all(): + for key in self.all(): gpg.import_keys(key_data=key.key_data) elif key_fingerprint: logger.debug('preloading key fingerprint: %s', key_fingerprint) @@ -129,7 +129,7 @@ class KeyManager(models.Manager): # Preload keys if all_keys: logger.debug('preloading all keys') - for key in Key.objects.all(): + for key in self.all(): gpg.import_keys(key_data=key.key_data) elif key_fingerprint: logger.debug('preloading key fingerprint: %s', key_fingerprint) diff --git a/mayan/apps/django_gpg/views.py b/mayan/apps/django_gpg/views.py index 4defb8c90b..d7fa69928a 100644 --- a/mayan/apps/django_gpg/views.py +++ b/mayan/apps/django_gpg/views.py @@ -28,9 +28,9 @@ class KeyDeleteView(SingleObjectDeleteView): def get_post_action_redirect(self): if self.get_object().key_type == KEY_TYPE_PUBLIC: - post_action_redirect = reverse_lazy('django_gpg:key_public_list') + return reverse_lazy('django_gpg:key_public_list') else: - post_action_redirect = reverse_lazy('django_gpg:key_private_list') + return reverse_lazy('django_gpg:key_private_list') def get_extra_context(self): return { diff --git a/mayan/apps/document_signatures/admin.py b/mayan/apps/document_signatures/admin.py index c930da3b3d..0c104fbd25 100644 --- a/mayan/apps/document_signatures/admin.py +++ b/mayan/apps/document_signatures/admin.py @@ -21,4 +21,3 @@ class EmbeddedSignatureAdmin(admin.ModelAdmin): 'public_key_fingerprint' ) list_display_links = ('document_version',) - diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index d541eaf410..73ef1bc865 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -2,7 +2,10 @@ from __future__ import unicode_literals import logging +from kombu import Exchange, Queue + from django.apps import apps +from django.db.models.signals import post_save, post_delete from django.utils.translation import ugettext_lazy as _ from acls import ModelPermission @@ -10,8 +13,10 @@ from common import ( MayanAppConfig, menu_facet, menu_object, menu_secondary, menu_sidebar ) from common.widgets import two_state_template +from mayan.celery import app from navigation import SourceColumn +from .handlers import unverify_signatures, verify_signatures from .links import ( link_document_version_signature_delete, link_document_version_signature_details, @@ -49,6 +54,10 @@ class DocumentSignaturesApp(MayanAppConfig): app_label='documents', model_name='DocumentVersion' ) + Key = apps.get_model( + app_label='django_gpg', model_name='Key' + ) + DetachedSignature = self.get_model('DetachedSignature') EmbeddedSignature = self.get_model('EmbeddedSignature') @@ -56,7 +65,7 @@ class DocumentSignaturesApp(MayanAppConfig): SignatureBaseModel = self.get_model('SignatureBaseModel') DocumentVersion.register_post_save_hook( - order=1, func=EmbeddedSignature.objects.check_signature + order=1, func=EmbeddedSignature.objects.create ) DocumentVersion.register_pre_open_hook( order=1, func=EmbeddedSignature.objects.open_signed @@ -83,7 +92,7 @@ class DocumentSignaturesApp(MayanAppConfig): func=lambda context: context['object'].signature_id or _('None') ) SourceColumn( - source=SignatureBaseModel, label=_('Public key ID'), + source=SignatureBaseModel, label=_('Public key fingerprint'), func=lambda context: context['object'].public_key_fingerprint or _('None') ) SourceColumn( @@ -103,6 +112,23 @@ class DocumentSignaturesApp(MayanAppConfig): ) ) + app.conf.CELERY_QUEUES.append( + Queue( + 'signatures', Exchange('signatures'), routing_key='signatures' + ), + ) + + app.conf.CELERY_ROUTES.update( + { + 'document_signatures.tasks.task_verify_signatures': { + 'queue': 'signatures' + }, + 'document_signatures.tasks.task_unverify_signatures': { + 'queue': 'signatures' + }, + } + ) + menu_object.bind_links( links=(link_document_version_signature_list,), sources=(DocumentVersion,) @@ -120,3 +146,13 @@ class DocumentSignaturesApp(MayanAppConfig): link_document_version_signature_verify, ), sources=(DocumentVersion,) ) + post_delete.connect( + unverify_signatures, + dispatch_uid='unverify_signatures', + sender=Key + ) + post_save.connect( + verify_signatures, + dispatch_uid='verify_signatures', + sender=Key + ) diff --git a/mayan/apps/document_signatures/handlers.py b/mayan/apps/document_signatures/handlers.py new file mode 100644 index 0000000000..38a91cc821 --- /dev/null +++ b/mayan/apps/document_signatures/handlers.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals + +from .tasks import task_unverify_signatures, task_verify_signatures + + +def unverify_signatures(sender, **kwargs): + task_unverify_signatures.apply_async( + kwargs=dict(key_id=kwargs['instance'].key_id) + ) + + +def verify_signatures(sender, **kwargs): + task_verify_signatures.apply_async( + kwargs=dict(key_pk=kwargs['instance'].pk) + ) diff --git a/mayan/apps/document_signatures/managers.py b/mayan/apps/document_signatures/managers.py index 8b21116f80..41297a0cd4 100644 --- a/mayan/apps/document_signatures/managers.py +++ b/mayan/apps/document_signatures/managers.py @@ -10,45 +10,7 @@ from django_gpg.models import Key logger = logging.getLogger(__name__) -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: - 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, - ) - - 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: @@ -62,26 +24,3 @@ class EmbeddedSignatureManager(models.Manager): return file_object else: return file_object - - """ - def verify_signature(self, document_version): - document_version_descriptor = document_version.open(raw=True) - detached_signature = None - if self.has_detached_signature(document_version=document_version): - logger.debug('has detached signature') - detached_signature = self.detached_signature( - document_version=document_version - ) - args = (document_version_descriptor, detached_signature) - else: - args = (document_version_descriptor,) - - try: - return Key.objects.verify_file(*args) - except VerificationError: - return None - finally: - document_version_descriptor.close() - if detached_signature: - detached_signature.close() - """ diff --git a/mayan/apps/document_signatures/migrations/0006_auto_20160326_0616.py b/mayan/apps/document_signatures/migrations/0006_auto_20160326_0616.py new file mode 100644 index 0000000000..e5db17f576 --- /dev/null +++ b/mayan/apps/document_signatures/migrations/0006_auto_20160326_0616.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('document_signatures', '0005_auto_20160325_0748'), + ] + + operations = [ + migrations.AlterField( + model_name='signaturebasemodel', + name='public_key_fingerprint', + field=models.CharField(verbose_name='Public key fingerprint', max_length=40, null=True, editable=False, blank=True), + ), + ] diff --git a/mayan/apps/document_signatures/models.py b/mayan/apps/document_signatures/models.py index 397ac92354..0e78c833fa 100644 --- a/mayan/apps/document_signatures/models.py +++ b/mayan/apps/document_signatures/models.py @@ -15,7 +15,7 @@ from django_gpg.exceptions import DecryptionError, VerificationError from django_gpg.models import Key from documents.models import DocumentVersion -from .managers import EmbeddedSignatureManager, DetachedSignatureManager +from .managers import EmbeddedSignatureManager from .runtime import storage_backend logger = logging.getLogger(__name__) @@ -42,7 +42,7 @@ class SignatureBaseModel(models.Model): verbose_name=_('Signature ID') ) public_key_fingerprint = models.CharField( - blank=True, editable=False, null=True, max_length=40, unique=True, + blank=True, editable=False, null=True, max_length=40, verbose_name=_('Public key fingerprint') ) @@ -77,6 +77,30 @@ class EmbeddedSignature(SignatureBaseModel): verbose_name = _('Document version embedded signature') verbose_name_plural = _('Document version embedded signatures') + def save(self, *args, **kwargs): + logger.debug('checking for embedded signature') + + if self.pk: + raw = True + else: + raw = False + + with self.document_version.open(raw=raw) as file_object: + try: + verify_result = Key.objects.verify_file(file_object=file_object) + except VerificationError as exception: + # Not signed + logger.debug( + 'embedded signature verification error; %s', exception + ) + else: + self.date = verify_result.date + self.key_id = verify_result.key_id + self.signature_id = verify_result.signature_id + self.public_key_fingerprint = verify_result.pubkey_fingerprint + + super(EmbeddedSignature, self).save(*args, **kwargs) + class DetachedSignature(SignatureBaseModel): signature_file = models.FileField( @@ -84,8 +108,6 @@ class DetachedSignature(SignatureBaseModel): verbose_name=_('Signature file') ) - objects = DetachedSignatureManager() - class Meta: verbose_name = _('Document version detached signature') verbose_name_plural = _('Document version detached signatures') @@ -93,3 +115,24 @@ class DetachedSignature(SignatureBaseModel): def delete(self, *args, **kwargs): self.signature_file.storage.delete(self.signature_file.name) super(DetachedSignature, self).delete(*args, **kwargs) + + def save(self, *args, **kwargs): + with self.document_version.open() as file_object: + try: + verify_result = Key.objects.verify_file( + file_object=file_object, signature_file=self.signature_file + ) + except VerificationError: + # Not signed + logger.debug( + 'detached signature verification error; %s', exception + ) + else: + self.signature_file.seek(0) + + self.date = verify_result.date + self.key_id = verify_result.key_id + self.signature_id = verify_result.signature_id + self.public_key_fingerprint = verify_result.pubkey_fingerprint + + return super(DetachedSignature, self).save(*args, **kwargs) diff --git a/mayan/apps/document_signatures/tasks.py b/mayan/apps/document_signatures/tasks.py new file mode 100644 index 0000000000..854b5bb1d4 --- /dev/null +++ b/mayan/apps/document_signatures/tasks.py @@ -0,0 +1,50 @@ +from __future__ import unicode_literals + +import logging + +from django.apps import apps + +from mayan.celery import app + +RETRY_DELAY = 10 +logger = logging.getLogger(__name__) + + +@app.task(bind=True, ignore_result=True) +def task_unverify_signatures(self, key_id): + DetachedSignature = apps.get_model( + app_label='document_signatures', model_name='DetachedSignature' + ) + + EmbeddedSignature = apps.get_model( + app_label='document_signatures', model_name='EmbeddedSignature' + ) + + for signature in DetachedSignature.objects.filter(key_id__endswith=key_id).filter(signature_id__isnull=False): + signature.save() + + for signature in EmbeddedSignature.objects.filter(key_id__endswith=key_id).filter(signature_id__isnull=False): + signature.save() + + +@app.task(bind=True, ignore_result=True) +def task_verify_signatures(self, key_pk): + Key = apps.get_model( + app_label='django_gpg', model_name='Key' + ) + + DetachedSignature = apps.get_model( + app_label='document_signatures', model_name='DetachedSignature' + ) + + EmbeddedSignature = apps.get_model( + app_label='document_signatures', model_name='EmbeddedSignature' + ) + + key = Key.objects.get(pk=key_pk) + + for signature in DetachedSignature.objects.filter(key_id__endswith=key.key_id).filter(signature_id__isnull=True): + signature.save() + + for signature in EmbeddedSignature.objects.filter(key_id__endswith=key.key_id).filter(signature_id__isnull=True): + signature.save() diff --git a/mayan/apps/document_signatures/tests/test_models.py b/mayan/apps/document_signatures/tests/test_models.py index 12338f3d4c..fa032d7621 100644 --- a/mayan/apps/document_signatures/tests/test_models.py +++ b/mayan/apps/document_signatures/tests/test_models.py @@ -4,6 +4,7 @@ import os import time from django.conf import settings +from django.core.files import File from django.test import TestCase, override_settings from django_gpg.models import Key @@ -23,10 +24,11 @@ TEST_KEY_FILE = os.path.join( 'key0x5F3F7F75D210724D.asc' ) TEST_KEY_ID = '5F3F7F75D210724D' +TEST_SIGNATURE_ID = 'XVkoGKw35yU1iq11dZPiv7uAY7k' @override_settings(OCR_AUTO_OCR=False) -class DocumentTestCase(TestCase): +class DocumentSignaturesTestCase(TestCase): def setUp(self): self.document_type = DocumentType.objects.create( label=TEST_DOCUMENT_TYPE @@ -35,7 +37,7 @@ class DocumentTestCase(TestCase): def tearDown(self): self.document_type.delete() - def test_embedded_signature(self): + def test_embedded_signature_no_key(self): with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: signed_document = self.document_type.new_document( file_object=file_object @@ -49,6 +51,55 @@ class DocumentTestCase(TestCase): signature.document_version, signed_document.latest_version ) self.assertEqual(signature.key_id, TEST_KEY_ID) + self.assertEqual(signature.signature_id, None) + + def test_embedded_signature_post_key_verify(self): + with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: + 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) + self.assertEqual(signature.signature_id, None) + + with open(TEST_KEY_FILE) as file_object: + key = Key.objects.create(key_data=file_object.read()) + + signature = EmbeddedSignature.objects.first() + + self.assertEqual(signature.signature_id, TEST_SIGNATURE_ID) + + def test_embedded_signature_post_no_key_verify(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: + 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) + self.assertEqual(signature.signature_id, TEST_SIGNATURE_ID) + + key.delete() + + signature = EmbeddedSignature.objects.first() + + self.assertEqual(signature.signature_id, None) def test_embedded_signature_with_key(self): with open(TEST_KEY_FILE) as file_object: @@ -69,27 +120,106 @@ class DocumentTestCase(TestCase): ) self.assertEqual(signature.key_id, TEST_KEY_ID) self.assertEqual(signature.public_key_fingerprint, key.fingerprint) + self.assertEqual(signature.signature_id, TEST_SIGNATURE_ID) - def test_detached_signature(self): + def test_detached_signature_no_key(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( + DetachedSignature.objects.create( document_version=document.latest_version, - signature_file=file_object + signature_file=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): + signature = DetachedSignature.objects.first() + + self.assertEqual(signature.document_version, document.latest_version) + self.assertEqual(signature.key_id, TEST_KEY_ID) + self.assertEqual(signature.public_key_fingerprint, None) + + def test_detached_signature_with_key(self): + with open(TEST_KEY_FILE) as file_object: + key = Key.objects.create(key_data=file_object.read()) + + 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.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.assertEqual(DetachedSignature.objects.count(), 1) + + signature = DetachedSignature.objects.first() + + self.assertEqual(signature.document_version, document.latest_version) + self.assertEqual(signature.key_id, TEST_KEY_ID) + self.assertEqual(signature.public_key_fingerprint, key.fingerprint) + + def test_detached_signature_post_key_verify(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.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.assertEqual(DetachedSignature.objects.count(), 1) + + signature = DetachedSignature.objects.first() + + self.assertEqual(signature.document_version, document.latest_version) + self.assertEqual(signature.key_id, TEST_KEY_ID) + self.assertEqual(signature.public_key_fingerprint, None) + + with open(TEST_KEY_FILE) as file_object: + key = Key.objects.create(key_data=file_object.read()) + + signature = DetachedSignature.objects.first() + + self.assertEqual(signature.public_key_fingerprint, key.fingerprint) + + def test_detached_signature_post_no_key_verify(self): + with open(TEST_KEY_FILE) as file_object: + key = Key.objects.create(key_data=file_object.read()) + + 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.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.assertEqual(DetachedSignature.objects.count(), 1) + + signature = DetachedSignature.objects.first() + + self.assertEqual(signature.document_version, document.latest_version) + self.assertEqual(signature.key_id, TEST_KEY_ID) + self.assertEqual(signature.public_key_fingerprint, key.fingerprint) + + key.delete() + + signature = DetachedSignature.objects.first() + + self.assertEqual(signature.public_key_fingerprint, None) def test_document_no_signature(self): with open(TEST_DOCUMENT_PATH) as file_object: diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index 99d8be3715..b1c655dc1b 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -40,7 +40,7 @@ class DocumentVersionSignatureDeleteView(SingleObjectDeleteView): def get_extra_context(self): return { - 'document': self.get_object().document_version.document, + '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(), @@ -103,62 +103,6 @@ class DocumentVersionSignatureListView(SingleObjectListView): return queryset -def document_verify(request, document_pk): - document = get_object_or_404(Document, pk=document_pk) - - try: - Permission.check_permissions( - request.user, (permission_document_verify,) - ) - except PermissionDenied: - AccessControlList.objects.check_access( - permission_document_verify, request.user, document - ) - - document.add_as_recent_document_for_user(request.user) - - try: - signature = DocumentVersionSignature.objects.verify_signature( - document.latest_version - ) - except AttributeError: - signature_state = SIGNATURE_STATES.get(SIGNATURE_STATE_NONE) - signature = None - else: - signature_state = SIGNATURE_STATES.get( - getattr(signature, 'status', None) - ) - - paragraphs = [_('Signature status: %s') % signature_state['text']] - - try: - if DocumentVersionSignature.objects.has_embedded_signature(document.latest_version): - signature_type = _('Embedded') - else: - signature_type = _('Detached') - except ValueError: - signature_type = _('None') - - if signature: - paragraphs.extend( - [ - _('Signature ID: %s') % signature.signature_id, - _('Signature type: %s') % signature_type, - _('Key fingerprint: %s') % signature.fingerprint, - _('Timestamp: %s') % signature.date, - _('Signee: %s') % escape(signature.user_id), - ] - ) - - return render_to_response('appearance/generic_template.html', { - 'document': document, - 'object': document, - 'paragraphs': paragraphs, - 'title': _('Signature properties for document: %s') % document, - }, context_instance=RequestContext(request)) - - - def document_version_signature_upload(request, pk): document_version = get_object_or_404(DocumentVersion, pk=pk) @@ -181,7 +125,7 @@ def document_version_signature_upload(request, pk): form = DetachedSignatureForm(request.POST, request.FILES) if form.is_valid(): try: - DetachedSignature.objects.upload_signature( + DetachedSignature.objects.create( document_version=document_version, signature_file=request.FILES['file'] ) diff --git a/mayan/apps/documents/models.py b/mayan/apps/documents/models.py index 04f0d49a3d..e795bf74bf 100644 --- a/mayan/apps/documents/models.py +++ b/mayan/apps/documents/models.py @@ -401,7 +401,9 @@ class DocumentVersion(models.Model): super(DocumentVersion, self).save(*args, **kwargs) for key in sorted(DocumentVersion._post_save_hooks): - DocumentVersion._post_save_hooks[key](self) + DocumentVersion._post_save_hooks[key]( + document_version=self + ) if new_document_version: # Only do this for new documents @@ -499,7 +501,9 @@ class DocumentVersion(models.Model): else: result = self.file.storage.open(self.file.name) for key in sorted(DocumentVersion._pre_open_hooks): - result = DocumentVersion._pre_open_hooks[key](result, self) + result = DocumentVersion._pre_open_hooks[key]( + file_object=result, document_version=self + ) return result From 0ffe20befd17c885e6122c22b57e6c463bd500f5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 26 Mar 2016 16:27:47 -0400 Subject: [PATCH 24/57] Fix link resolution in the document signature list view. This view exposes 3 navigable objects, use 'resolved_object' for correct link resolution. --- mayan/apps/checkouts/links.py | 3 ++- mayan/apps/document_comments/links.py | 3 ++- mayan/apps/document_indexing/links.py | 2 +- mayan/apps/document_signatures/apps.py | 4 ---- mayan/apps/document_signatures/links.py | 11 ++-------- mayan/apps/document_signatures/permissions.py | 4 ---- mayan/apps/document_signatures/urls.py | 4 ---- mayan/apps/document_signatures/views.py | 1 - mayan/apps/document_states/links.py | 3 ++- mayan/apps/documents/links.py | 20 +++++++++++-------- mayan/apps/folders/links.py | 3 ++- mayan/apps/linking/links.py | 2 +- mayan/apps/mailer/links.py | 8 ++++---- mayan/apps/metadata/links.py | 2 +- mayan/apps/ocr/links.py | 4 ++-- mayan/apps/sources/links.py | 3 +-- mayan/apps/tags/links.py | 2 +- 17 files changed, 33 insertions(+), 46 deletions(-) diff --git a/mayan/apps/checkouts/links.py b/mayan/apps/checkouts/links.py index 23dfa6c805..fbf4a8a372 100644 --- a/mayan/apps/checkouts/links.py +++ b/mayan/apps/checkouts/links.py @@ -45,5 +45,6 @@ link_checkout_info = Link( icon='fa fa-shopping-cart', permissions=( permission_document_checkin, permission_document_checkin_override, permission_document_checkout - ), text=_('Check in/out'), view='checkouts:checkout_info', args='object.pk' + ), text=_('Check in/out'), view='checkouts:checkout_info', + args='resolved_object.pk' ) diff --git a/mayan/apps/document_comments/links.py b/mayan/apps/document_comments/links.py index bf53b39a56..5e73468415 100644 --- a/mayan/apps/document_comments/links.py +++ b/mayan/apps/document_comments/links.py @@ -19,5 +19,6 @@ link_comment_delete = Link( ) link_comments_for_document = Link( icon='fa fa-comment', permissions=(permission_comment_view,), - text=_('Comments'), view='comments:comments_for_document', args='object.pk' + text=_('Comments'), view='comments:comments_for_document', + args='resolved_object.pk' ) diff --git a/mayan/apps/document_indexing/links.py b/mayan/apps/document_indexing/links.py index 2bf89f7dbc..34079fe382 100644 --- a/mayan/apps/document_indexing/links.py +++ b/mayan/apps/document_indexing/links.py @@ -16,7 +16,7 @@ def is_not_root_node(context): link_document_index_list = Link( icon='fa fa-list-ul', text=_('Indexes'), - view='indexing:document_index_list', args='object.pk' + view='indexing:document_index_list', args='resolved_object.pk' ) link_index_main_menu = Link( icon='fa fa-list-ul', text=_('Indexes'), view='indexing:index_list' diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index 73ef1bc865..d4d0907706 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -23,13 +23,11 @@ from .links import ( 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_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_upload, - permission_document_version_signature_verify, permission_document_version_signature_view, ) @@ -75,7 +73,6 @@ class DocumentSignaturesApp(MayanAppConfig): model=Document, permissions=( 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, ) @@ -143,7 +140,6 @@ class DocumentSignaturesApp(MayanAppConfig): menu_sidebar.bind_links( links=( link_document_version_signature_upload, - link_document_version_signature_verify, ), sources=(DocumentVersion,) ) post_delete.connect( diff --git a/mayan/apps/document_signatures/links.py b/mayan/apps/document_signatures/links.py index fd5b8a6ef4..c49c85a120 100644 --- a/mayan/apps/document_signatures/links.py +++ b/mayan/apps/document_signatures/links.py @@ -9,8 +9,6 @@ from .permissions import ( 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 ) @@ -51,12 +49,7 @@ link_document_version_signature_download = Link( ) 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', + text=_('Upload signature'), + view='signatures:document_version_signature_upload', args='resolved_object.pk' ) diff --git a/mayan/apps/document_signatures/permissions.py b/mayan/apps/document_signatures/permissions.py index e973aac602..f81ad66806 100644 --- a/mayan/apps/document_signatures/permissions.py +++ b/mayan/apps/document_signatures/permissions.py @@ -12,10 +12,6 @@ permission_document_version_signature_view = namespace.add_permission( name='document_version_signature_view', label=_('View details of document signatures') ) -permission_document_version_signature_verify = namespace.add_permission( - name='document_version_signature_verify', - label=_('Verify document signatures') -) permission_document_version_signature_delete = namespace.add_permission( name='document_version_signature_delete', label=_('Delete detached signatures') diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index 061aa95e53..2b5d34ed13 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -24,10 +24,6 @@ urlpatterns = patterns( DocumentVersionSignatureListView.as_view(), name='document_version_signature_list' ), - url( - 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', diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index b1c655dc1b..e3256661c5 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -26,7 +26,6 @@ from .forms import DetachedSignatureForm, DocumentVersionSignatureDetailForm from .models import DetachedSignature, SignatureBaseModel from .permissions import ( 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 diff --git a/mayan/apps/document_states/links.py b/mayan/apps/document_states/links.py index be928ac56d..f9a0e3bba3 100644 --- a/mayan/apps/document_states/links.py +++ b/mayan/apps/document_states/links.py @@ -13,7 +13,8 @@ from .permissions import ( link_document_workflow_instance_list = Link( icon='fa fa-sitemap', permissions=(permission_workflow_view,), text=_('Workflows'), - view='document_states:document_workflow_instance_list', args='object.pk' + view='document_states:document_workflow_instance_list', + args='resolved_object.pk' ) link_setup_workflow_create = Link( permissions=(permission_workflow_create,), text=_('Create workflow'), diff --git a/mayan/apps/documents/links.py b/mayan/apps/documents/links.py index f1bb286c83..33ab42f630 100644 --- a/mayan/apps/documents/links.py +++ b/mayan/apps/documents/links.py @@ -18,7 +18,7 @@ from .settings import setting_zoom_max_level, setting_zoom_min_level def is_not_current_version(context): - return context['resolved_object'].document.latest_version.timestamp != context['resolved_object'].timestamp + return context['object'].document.latest_version.timestamp != context['object'].timestamp def is_first_page(context): @@ -40,7 +40,8 @@ def is_min_zoom(context): # Facet link_document_preview = Link( icon='fa fa-eye', permissions=(permission_document_view,), - text=_('Preview'), view='documents:document_preview', args='resolved_object.id' + text=_('Preview'), view='documents:document_preview', + args='resolved_object.id' ) link_document_properties = Link( icon='fa fa-info', permissions=(permission_document_view,), @@ -50,7 +51,7 @@ link_document_properties = Link( link_document_version_list = Link( icon='fa fa-code-fork', permissions=(permission_document_view,), text=_('Versions'), view='documents:document_version_list', - args='object.pk' + args='resolved_object.pk' ) link_document_pages = Link( icon='fa fa-files-o', permissions=(permission_document_view,), @@ -65,11 +66,13 @@ link_document_clear_transformations = Link( ) link_document_delete = Link( permissions=(permission_document_delete,), tags='dangerous', - text=_('Delete'), view='documents:document_delete', args='resolved_object.id' + text=_('Delete'), view='documents:document_delete', + args='resolved_object.id' ) link_document_trash = Link( permissions=(permission_document_trash,), tags='dangerous', - text=_('Move to trash'), view='documents:document_trash', args='resolved_object.id' + text=_('Move to trash'), view='documents:document_trash', + args='resolved_object.id' ) link_document_edit = Link( permissions=(permission_document_properties_edit,), @@ -89,8 +92,9 @@ link_document_print = Link( view='documents:document_print', args='resolved_object.id' ) link_document_update_page_count = Link( - permissions=(permission_document_tools,), text=_('Recalculate page count'), - view='documents:document_update_page_count', args='object.pk' + args='resolved_object.pk', permissions=(permission_document_tools,), + text=_('Recalculate page count'), + view='documents:document_update_page_count' ) link_document_restore = Link( permissions=(permission_document_restore,), text=_('Restore'), @@ -124,7 +128,7 @@ link_document_multiple_restore = Link( text=_('Restore'), view='documents:document_multiple_restore' ) link_document_version_download = Link( - args='object.pk', permissions=(permission_document_download,), + args='resolved_object.pk', permissions=(permission_document_download,), text=_('Download version'), view='documents:document_version_download' ) diff --git a/mayan/apps/folders/links.py b/mayan/apps/folders/links.py index ab8a3f95f6..ce0163cde9 100644 --- a/mayan/apps/folders/links.py +++ b/mayan/apps/folders/links.py @@ -13,7 +13,8 @@ from .permissions import ( link_document_folder_list = Link( icon='fa fa-folder', permissions=(permission_document_view,), - text=_('Folders'), view='folders:document_folder_list', args='object.pk' + text=_('Folders'), view='folders:document_folder_list', + args='resolved_object.pk' ) link_folder_add_document = Link( permissions=(permission_folder_add_document,), text=_('Add to a folder'), diff --git a/mayan/apps/linking/links.py b/mayan/apps/linking/links.py index 2071f6b151..baf74586a5 100644 --- a/mayan/apps/linking/links.py +++ b/mayan/apps/linking/links.py @@ -52,7 +52,7 @@ link_smart_link_instance_view = Link( link_smart_link_instances_for_document = Link( icon='fa fa-link', permissions=(permission_document_view,), text=_('Smart links'), view='linking:smart_link_instances_for_document', - args='object.pk' + args='resolved_object.pk' ) link_smart_link_list = Link( permissions=(permission_smart_link_create,), text=_('Smart links'), diff --git a/mayan/apps/mailer/links.py b/mayan/apps/mailer/links.py index ebafcd33f0..2909f83a10 100644 --- a/mayan/apps/mailer/links.py +++ b/mayan/apps/mailer/links.py @@ -10,12 +10,12 @@ from .permissions import ( ) link_send_document = Link( - permissions=(permission_mailing_send_document,), text=_('Email document'), - view='mailer:send_document', args='object.pk' + args='resolved_object.pk', permissions=(permission_mailing_send_document,), + text=_('Email document'), view='mailer:send_document' ) link_send_document_link = Link( - permissions=(permission_mailing_link,), text=_('Email link'), - view='mailer:send_document_link', args='object.pk' + args='resolved_object.pk', permissions=(permission_mailing_link,), + text=_('Email link'), view='mailer:send_document_link' ) link_document_mailing_error_log = Link( icon='fa fa-envelope', permissions=(permission_view_error_log,), diff --git a/mayan/apps/metadata/links.py b/mayan/apps/metadata/links.py index 39412bbfcb..1750dcef35 100644 --- a/mayan/apps/metadata/links.py +++ b/mayan/apps/metadata/links.py @@ -36,7 +36,7 @@ link_metadata_remove = Link( ) link_metadata_view = Link( icon='fa fa-pencil', permissions=(permission_metadata_document_view,), - text=_('Metadata'), view='metadata:metadata_view', args='object.pk' + text=_('Metadata'), view='metadata:metadata_view', args='resolved_object.pk' ) link_setup_document_type_metadata = Link( permissions=(permission_document_type_edit,), text=_('Optional metadata'), diff --git a/mayan/apps/ocr/links.py b/mayan/apps/ocr/links.py index bc4a935d81..70a827294d 100644 --- a/mayan/apps/ocr/links.py +++ b/mayan/apps/ocr/links.py @@ -14,8 +14,8 @@ link_document_content = Link( text=_('OCR'), view='ocr:document_content', args='resolved_object.id' ) link_document_submit = Link( - permissions=(permission_ocr_document,), text=_('Submit for OCR'), - view='ocr:document_submit', args='object.id' + args='resolved_object.id', permissions=(permission_ocr_document,), + text=_('Submit for OCR'), view='ocr:document_submit' ) link_document_submit_all = Link( icon='fa fa-font', permissions=(permission_ocr_document,), diff --git a/mayan/apps/sources/links.py b/mayan/apps/sources/links.py index 0a3846bec5..926fada9ce 100644 --- a/mayan/apps/sources/links.py +++ b/mayan/apps/sources/links.py @@ -75,10 +75,9 @@ link_staging_file_delete = Link( args=('source.pk', 'object.encoded_filename',) ) link_upload_version = Link( - condition=document_new_version_not_blocked, + args='resolved_object.pk', condition=document_new_version_not_blocked, permissions=(permission_document_new_version,), text=_('Upload new version'), view='sources:upload_version', - args='object.pk' ) link_setup_source_logs = Link( text=_('Logs'), view='sources:setup_source_logs', diff --git a/mayan/apps/tags/links.py b/mayan/apps/tags/links.py index 8627cca4d3..7acd009875 100644 --- a/mayan/apps/tags/links.py +++ b/mayan/apps/tags/links.py @@ -38,7 +38,7 @@ link_tag_edit = Link( ) link_tag_document_list = Link( icon='fa fa-tag', permissions=(permission_tag_view,), text=_('Tags'), - view='tags:document_tags', args='object.pk' + view='tags:document_tags', args='resolved_object.pk' ) link_tag_list = Link(icon='fa fa-tag', text=_('Tags'), view='tags:tag_list') link_tag_multiple_delete = Link( From fa1450fe5a0b4b5182c925555b94f2c7622f1109 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 27 Mar 2016 03:27:56 -0400 Subject: [PATCH 25/57] PEP8 cleanups. --- mayan/apps/document_signatures/apps.py | 2 +- mayan/apps/document_signatures/links.py | 2 +- mayan/apps/document_signatures/managers.py | 2 +- .../migrations/0005_auto_20160325_0748.py | 2 +- mayan/apps/document_signatures/models.py | 5 +-- .../document_signatures/tests/test_models.py | 20 +++------- mayan/apps/document_signatures/views.py | 39 ++++++++----------- mayan/apps/documents/links.py | 2 +- mayan/apps/documents/tests/test_views.py | 2 +- 9 files changed, 29 insertions(+), 47 deletions(-) diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index d4d0907706..dacb9278e0 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from acls import ModelPermission from common import ( - MayanAppConfig, menu_facet, menu_object, menu_secondary, menu_sidebar + MayanAppConfig, menu_object, menu_sidebar ) from common.widgets import two_state_template from mayan.celery import app diff --git a/mayan/apps/document_signatures/links.py b/mayan/apps/document_signatures/links.py index c49c85a120..b5bb879275 100644 --- a/mayan/apps/document_signatures/links.py +++ b/mayan/apps/document_signatures/links.py @@ -44,7 +44,7 @@ link_document_version_signature_list = Link( link_document_version_signature_download = Link( condition=is_detached_signature, text=_('Download'), - view='signatures:document_signature_download', args='resolved_object.pk', + view='signatures:document_version_signature_download', args='resolved_object.pk', #permissions=(permission_document_version_signature_download,) ) link_document_version_signature_upload = Link( diff --git a/mayan/apps/document_signatures/managers.py b/mayan/apps/document_signatures/managers.py index 41297a0cd4..0c5cf80ca3 100644 --- a/mayan/apps/document_signatures/managers.py +++ b/mayan/apps/document_signatures/managers.py @@ -4,7 +4,7 @@ import logging from django.db import models -from django_gpg.exceptions import DecryptionError, VerificationError +from django_gpg.exceptions import DecryptionError from django_gpg.models import Key logger = logging.getLogger(__name__) diff --git a/mayan/apps/document_signatures/migrations/0005_auto_20160325_0748.py b/mayan/apps/document_signatures/migrations/0005_auto_20160325_0748.py index cc087b93e2..76bbc3bee3 100644 --- a/mayan/apps/document_signatures/migrations/0005_auto_20160325_0748.py +++ b/mayan/apps/document_signatures/migrations/0005_auto_20160325_0748.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/document_signatures/models.py b/mayan/apps/document_signatures/models.py index 0e78c833fa..942379a483 100644 --- a/mayan/apps/document_signatures/models.py +++ b/mayan/apps/document_signatures/models.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from datetime import date import logging import uuid @@ -11,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from model_utils.managers import InheritanceManager -from django_gpg.exceptions import DecryptionError, VerificationError +from django_gpg.exceptions import VerificationError from django_gpg.models import Key from documents.models import DocumentVersion @@ -122,7 +121,7 @@ class DetachedSignature(SignatureBaseModel): verify_result = Key.objects.verify_file( file_object=file_object, signature_file=self.signature_file ) - except VerificationError: + except VerificationError as exception: # Not signed logger.debug( 'detached signature verification error; %s', exception diff --git a/mayan/apps/document_signatures/tests/test_models.py b/mayan/apps/document_signatures/tests/test_models.py index fa032d7621..5366c3bac3 100644 --- a/mayan/apps/document_signatures/tests/test_models.py +++ b/mayan/apps/document_signatures/tests/test_models.py @@ -1,9 +1,7 @@ from __future__ import unicode_literals -import os import time -from django.conf import settings from django.core.files import File from django.test import TestCase, override_settings @@ -13,18 +11,10 @@ from documents.tests import TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE from ..models import DetachedSignature, EmbeddedSignature -TEST_SIGNED_DOCUMENT_PATH = os.path.join( - settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.gpg' +from .literals import ( + TEST_SIGNED_DOCUMENT_PATH, TEST_SIGNATURE_FILE_PATH, TEST_KEY_FILE, + TEST_KEY_ID, TEST_SIGNATURE_ID ) -TEST_SIGNATURE_FILE_PATH = os.path.join( - settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.sig' -) -TEST_KEY_FILE = os.path.join( - settings.BASE_DIR, 'contrib', 'sample_documents', - 'key0x5F3F7F75D210724D.asc' -) -TEST_KEY_ID = '5F3F7F75D210724D' -TEST_SIGNATURE_ID = 'XVkoGKw35yU1iq11dZPiv7uAY7k' @override_settings(OCR_AUTO_OCR=False) @@ -70,7 +60,7 @@ class DocumentSignaturesTestCase(TestCase): self.assertEqual(signature.signature_id, None) with open(TEST_KEY_FILE) as file_object: - key = Key.objects.create(key_data=file_object.read()) + Key.objects.create(key_data=file_object.read()) signature = EmbeddedSignature.objects.first() @@ -223,7 +213,7 @@ class DocumentSignaturesTestCase(TestCase): def test_document_no_signature(self): with open(TEST_DOCUMENT_PATH) as file_object: - document = self.document_type.new_document( + self.document_type.new_document( file_object=file_object ) diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index e3256661c5..e47da28d8d 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -1,6 +1,5 @@ from __future__ import absolute_import, unicode_literals -from datetime import datetime import logging from django.conf import settings @@ -10,15 +9,13 @@ from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render_to_response from django.template import RequestContext -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, DocumentVersion +from documents.models import DocumentVersion from filetransfers.api import serve_file from permissions import Permission @@ -55,6 +52,8 @@ class DocumentVersionSignatureDeleteView(SingleObjectDeleteView): class DocumentVersionSignatureDetailView(SingleObjectDetailView): form_class = DocumentVersionSignatureDetailForm + object_permission = permission_document_version_signature_view + object_permission_related = 'document_version.document' def get_extra_context(self): return { @@ -73,6 +72,9 @@ class DocumentVersionSignatureDetailView(SingleObjectDetailView): class DocumentVersionSignatureListView(SingleObjectListView): + object_permission = permission_document_version_signature_view + object_permission_related = 'document_version.document' + def get_document_version(self): return get_object_or_404(DocumentVersion, pk=self.kwargs['pk']) @@ -149,8 +151,8 @@ def document_version_signature_upload(request, pk): }, context_instance=RequestContext(request)) -def document_signature_download(request, document_pk): - document = get_object_or_404(Document, pk=document_pk) +def document_signature_download(request, pk): + signature = get_object_or_404(DetachedSignature, pk=pk) try: Permission.check_permissions( @@ -158,22 +160,13 @@ def document_signature_download(request, document_pk): ) except PermissionDenied: AccessControlList.objects.check_access( - permission_document_version_signature_download, request.user, document + permission_document_version_signature_download, request.user, + signature.document_version.signature ) - try: - if DocumentVersionSignature.objects.has_detached_signature(document.latest_version): - signature = DocumentVersionSignature.objects.detached_signature( - document.latest_version - ) - return serve_file( - request, - signature, - save_as='"%s.sig"' % document.filename, - content_type='application/octet-stream' - ) - except Exception as exception: - messages.error(request, exception) - return HttpResponseRedirect(request.META['HTTP_REFERER']) - - return HttpResponseRedirect(request.META['HTTP_REFERER']) + return serve_file( + request, + signature, + save_as='"%s.sig"' % signature.document_version.document, + content_type='application/octet-stream' + ) diff --git a/mayan/apps/documents/links.py b/mayan/apps/documents/links.py index 33ab42f630..6b64e5fb2b 100644 --- a/mayan/apps/documents/links.py +++ b/mayan/apps/documents/links.py @@ -18,7 +18,7 @@ from .settings import setting_zoom_max_level, setting_zoom_min_level def is_not_current_version(context): - return context['object'].document.latest_version.timestamp != context['object'].timestamp + return context['resolved_object'].document.latest_version.timestamp != context['resolved_object'].timestamp def is_first_page(context): diff --git a/mayan/apps/documents/tests/test_views.py b/mayan/apps/documents/tests/test_views.py index 7eaeec594f..d952db9aab 100644 --- a/mayan/apps/documents/tests/test_views.py +++ b/mayan/apps/documents/tests/test_views.py @@ -49,7 +49,7 @@ class GenericDocumentViewTestCase(GenericViewTestCase): with open(TEST_SMALL_DOCUMENT_PATH) as file_object: self.document = self.document_type.new_document( - file_object=file_object, label='mayan_11_1.pdf' + file_object=file_object ) def tearDown(self): From e708e0250e4d8e624447ab441f444b3981f7cb1c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 27 Mar 2016 03:28:14 -0400 Subject: [PATCH 26/57] Support related object permission ACLs for more than just 1 level of relationship. --- mayan/apps/acls/managers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index e4b3acadd1..fb6c4f822c 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -8,6 +8,7 @@ from django.db import models from django.db.models import Q from django.utils.translation import ugettext +from common.utils import return_attrib from permissions.models import StoredPermission from .classes import ModelPermission @@ -57,7 +58,7 @@ class AccessControlListManager(models.Manager): stored_permissions = [permissions.stored_permission] if related: - obj = getattr(obj, related) + obj = return_attrib(obj, related) try: parent_accessor = ModelPermission.get_inheritance(obj._meta.model) From 35df61bca1e27c43c2fca53edf8e714de8c24a4a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 27 Mar 2016 03:29:19 -0400 Subject: [PATCH 27/57] Add view test for the document signatures app. --- .../document_signatures/tests/literals.py | 18 +++ .../document_signatures/tests/test_views.py | 130 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 mayan/apps/document_signatures/tests/literals.py create mode 100644 mayan/apps/document_signatures/tests/test_views.py diff --git a/mayan/apps/document_signatures/tests/literals.py b/mayan/apps/document_signatures/tests/literals.py new file mode 100644 index 0000000000..1d3f3380a4 --- /dev/null +++ b/mayan/apps/document_signatures/tests/literals.py @@ -0,0 +1,18 @@ +from __future__ import unicode_literals + +import os + +from django.conf import settings + +TEST_SIGNED_DOCUMENT_PATH = os.path.join( + settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.gpg' +) +TEST_SIGNATURE_FILE_PATH = os.path.join( + settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.sig' +) +TEST_KEY_FILE = os.path.join( + settings.BASE_DIR, 'contrib', 'sample_documents', + 'key0x5F3F7F75D210724D.asc' +) +TEST_KEY_ID = '5F3F7F75D210724D' +TEST_SIGNATURE_ID = 'XVkoGKw35yU1iq11dZPiv7uAY7k' diff --git a/mayan/apps/document_signatures/tests/test_views.py b/mayan/apps/document_signatures/tests/test_views.py new file mode 100644 index 0000000000..9f37484206 --- /dev/null +++ b/mayan/apps/document_signatures/tests/test_views.py @@ -0,0 +1,130 @@ +from __future__ import absolute_import, unicode_literals + +from django.core.files import File + +from django_gpg.models import Key +from documents.permissions import permission_document_view +from documents.tests.literals import TEST_DOCUMENT_PATH +from documents.tests.test_views import GenericDocumentViewTestCase +from user_management.tests import ( + TEST_USER_USERNAME, TEST_USER_PASSWORD +) + +from ..models import DetachedSignature +from ..permissions import ( + permission_document_version_signature_view, + permission_document_version_signature_delete, + permission_document_version_signature_download, + permission_document_version_signature_upload, +) + +from .literals import ( + TEST_SIGNED_DOCUMENT_PATH, TEST_SIGNATURE_FILE_PATH, TEST_KEY_FILE, + TEST_KEY_ID, TEST_SIGNATURE_ID +) + + +class SignaturesViewTestCase(GenericDocumentViewTestCase): + def test_signature_list_view_no_permission(self): + with open(TEST_KEY_FILE) as file_object: + Key.objects.create(key_data=file_object.read()) + + 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.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + response = self.get( + 'signatures:document_version_signature_list', + args=(document.latest_version.pk,) + ) + + self.assertContains(response, 'Total: 0', status_code=200) + + def test_signature_list_view_with_permission(self): + with open(TEST_KEY_FILE) as file_object: + Key.objects.create(key_data=file_object.read()) + + 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.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add( + permission_document_version_signature_view.stored_permission + ) + + response = self.get( + 'signatures:document_version_signature_list', + args=(document.latest_version.pk,) + ) + + self.assertContains(response, 'Total: 1', status_code=200) + + def test_signature_detail_view_no_permission(self): + with open(TEST_KEY_FILE) as file_object: + Key.objects.create(key_data=file_object.read()) + + 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: + signature = DetachedSignature.objects.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + response = self.get( + 'signatures:document_version_signature_details', + args=(signature.pk,) + ) + + self.assertEqual(response.status_code, 403) + + def test_signature_detail_view_with_permission(self): + with open(TEST_KEY_FILE) as file_object: + Key.objects.create(key_data=file_object.read()) + + 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: + signature = DetachedSignature.objects.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add( + permission_document_version_signature_view.stored_permission + ) + + response = self.get( + 'signatures:document_version_signature_details', + args=(signature.pk,) + ) + + self.assertContains(response, signature.signature_id, status_code=200) From e5c47f16d499bb94c8ebb66c63ce99acae72b730 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 28 Mar 2016 14:56:49 -0400 Subject: [PATCH 28/57] Update document version signature upload view to CBV and add test. --- .../document_signatures/tests/test_views.py | 40 +++++++++ mayan/apps/document_signatures/urls.py | 4 +- mayan/apps/document_signatures/views.py | 84 +++++++++---------- 3 files changed, 82 insertions(+), 46 deletions(-) diff --git a/mayan/apps/document_signatures/tests/test_views.py b/mayan/apps/document_signatures/tests/test_views.py index 9f37484206..85088784ba 100644 --- a/mayan/apps/document_signatures/tests/test_views.py +++ b/mayan/apps/document_signatures/tests/test_views.py @@ -128,3 +128,43 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): ) self.assertContains(response, signature.signature_id, status_code=200) + + def test_signature_upload_view_no_permission(self): + with open(TEST_DOCUMENT_PATH) as file_object: + document = self.document_type.new_document( + file_object=file_object + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + with open(TEST_SIGNATURE_FILE_PATH) as file_object: + response = self.post( + 'signatures:document_version_signature_upload', + args=(document.latest_version.pk,), + data={'signature_file': file_object} + ) + + self.assertEqual(response.status_code, 403) + self.assertEqual(DetachedSignature.objects.count(), 0) + + def test_signature_upload_view_with_permission(self): + with open(TEST_DOCUMENT_PATH) as file_object: + document = self.document_type.new_document( + file_object=file_object + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add( + permission_document_version_signature_upload.stored_permission + ) + + with open(TEST_SIGNATURE_FILE_PATH) as file_object: + response = self.post( + 'signatures:document_version_signature_upload', + args=(document.latest_version.pk,), + data={'signature_file': file_object} + ) + + self.assertEqual(response.status_code, 302) + self.assertEqual(DetachedSignature.objects.count(), 1) diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index 2b5d34ed13..1695d51de3 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -4,7 +4,7 @@ from django.conf.urls import patterns, url from .views import ( DocumentVersionSignatureDeleteView, DocumentVersionSignatureDetailView, - DocumentVersionSignatureListView + DocumentVersionSignatureListView, DocumentVersionSignatureUploadView ) urlpatterns = patterns( @@ -26,7 +26,7 @@ urlpatterns = patterns( ), url( r'^documents/version/(?P\d+)/signature/upload/$', - 'document_version_signature_upload', + DocumentVersionSignatureUploadView.as_view(), name='document_version_signature_upload' ), url( diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index e47da28d8d..e1703ce687 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -13,7 +13,8 @@ from django.utils.translation import ugettext_lazy as _ from acls.models import AccessControlList from common.generics import ( - SingleObjectDeleteView, SingleObjectDetailView, SingleObjectListView + SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView, + SingleObjectListView ) from documents.models import DocumentVersion from filetransfers.api import serve_file @@ -104,51 +105,46 @@ class DocumentVersionSignatureListView(SingleObjectListView): return queryset -def document_version_signature_upload(request, pk): - document_version = get_object_or_404(DocumentVersion, pk=pk) +class DocumentVersionSignatureUploadView(SingleObjectCreateView): + fields = ('signature_file',) + model = DetachedSignature - try: - Permission.check_permissions( - request.user, (permission_document_version_signature_upload,) + def dispatch(self, request, *args, **kwargs): + try: + Permission.check_permissions( + request.user, (permission_document_version_signature_upload,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_document_version_signature_upload, request.user, + self.get_document_version() + ) + + return super( + DocumentVersionSignatureUploadView, 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': _( + 'Upload detached signature for document version: %s' + ) % self.get_document_version(), + } + + def get_instance_extra_data(self): + return {'document_version': self.get_document_version()} + + def get_post_action_redirect(self): + return reverse( + 'signatures:document_version_signature_list', + args=(self.get_document_version().pk,) ) - except PermissionDenied: - AccessControlList.objects.check_access( - permission_document_version_signature_upload, request.user, document_version.document - ) - - 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)))) - 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': - form = DetachedSignatureForm(request.POST, request.FILES) - if form.is_valid(): - try: - DetachedSignature.objects.create( - document_version=document_version, - signature_file=request.FILES['file'] - ) - messages.success( - request, _('Detached signature uploaded successfully.') - ) - return HttpResponseRedirect(next) - except Exception as exception: - messages.error(request, exception) - return HttpResponseRedirect(previous) - else: - form = DetachedSignatureForm() - - return render_to_response('appearance/generic_form.html', { - 'form': form, - 'next': next, - 'document': document_version.document, - 'document_version': document_version, - 'navigation_object_list': ('document', 'document_version'), - 'previous': previous, - 'title': _('Upload detached signature for document version: %s') % document_version, - }, context_instance=RequestContext(request)) def document_signature_download(request, pk): From c1cb9838691f0cf3958d57068a4b844142ca65d6 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 28 Mar 2016 14:57:19 -0400 Subject: [PATCH 29/57] Just display the signature type to conserve UI space. --- mayan/apps/document_signatures/apps.py | 23 +++++------------------ mayan/apps/document_signatures/models.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index dacb9278e0..e8b2468e04 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -82,31 +82,18 @@ class DocumentSignaturesApp(MayanAppConfig): source=SignatureBaseModel, label=_('Date'), attribute='date' ) SourceColumn( - source=SignatureBaseModel, label=_('Key ID'), attribute='key_id' + source=SignatureBaseModel, label=_('Key ID'), + attribute='get_key_id' ) SourceColumn( source=SignatureBaseModel, label=_('Signature ID'), func=lambda context: context['object'].signature_id or _('None') ) - SourceColumn( - source=SignatureBaseModel, label=_('Public key fingerprint'), - 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 - ) + func=lambda context: SignatureBaseModel.objects.get_subclass( + pk=context['object'].pk + ).get_signature_type_display() ) app.conf.CELERY_QUEUES.append( diff --git a/mayan/apps/document_signatures/models.py b/mayan/apps/document_signatures/models.py index 942379a483..197ab83103 100644 --- a/mayan/apps/document_signatures/models.py +++ b/mayan/apps/document_signatures/models.py @@ -60,6 +60,18 @@ class SignatureBaseModel(models.Model): args=(self.pk,) ) + def get_key_id(self): + if self.public_key_fingerprint: + return self.public_key_fingerprint[-16:] + else: + return self.key_id + + def get_signature_type_display(self): + if self.is_detached: + return _('Detached') + else: + return _('Embedded') + @property def is_detached(self): return hasattr(self, 'signature_file') From 3b593e10fd2f1076eabe7e8b2c39032c2ae640ab Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Mar 2016 05:11:08 -0400 Subject: [PATCH 30/57] Add django-downloadview to the used packages. Add a Mayan generic sub class download view based on django-downloadviews' virtual download view class. --- mayan/apps/common/generics.py | 8 ++++++++ requirements/base.txt | 1 + 2 files changed, 9 insertions(+) diff --git a/mayan/apps/common/generics.py b/mayan/apps/common/generics.py index 6fd6e03bf4..639c5cecea 100644 --- a/mayan/apps/common/generics.py +++ b/mayan/apps/common/generics.py @@ -8,11 +8,15 @@ from django.utils.translation import ugettext_lazy as _ from django.views.generic import ( FormView as DjangoFormView, DetailView, TemplateView ) +from django.views.generic.base import ContextMixin +from django.views.generic.detail import SingleObjectMixin from django.views.generic.edit import ( CreateView, DeleteView, ModelFormMixin, UpdateView ) from django.views.generic.list import ListView +from django_downloadview import VirtualDownloadView +from django_downloadview import VirtualFile from pure_pagination.mixins import PaginationMixin from .forms import ChoiceForm @@ -344,6 +348,10 @@ class SingleObjectDetailView(ViewPermissionCheckMixin, ObjectPermissionCheckMixi return context +class SingleObjectDownloadView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, VirtualDownloadView, SingleObjectMixin): + VirtualFile = VirtualFile + + class SingleObjectEditView(ObjectNameMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, UpdateView): template_name = 'appearance/generic_form.html' diff --git a/requirements/base.txt b/requirements/base.txt index cc68b931b2..0e12651d4b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -10,6 +10,7 @@ django-celery==3.1.17 django-colorful==1.1.0 django-compressor==2.0 django-cors-headers==1.1.0 +django-downloadview==1.9 django-filetransfers==0.1.0 django-formtools==1.0 django-pure-pagination==0.3.0 From b9d75e525f1f524e18cc8292965aadbf7a5bfc5d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Mar 2016 05:12:04 -0400 Subject: [PATCH 31/57] Convert the signature download view to CBV and add corresponding test. --- mayan/apps/document_signatures/apps.py | 3 - mayan/apps/document_signatures/forms.py | 10 +-- mayan/apps/document_signatures/models.py | 8 ++- .../document_signatures/tests/test_views.py | 51 ++++++++++++++- mayan/apps/document_signatures/urls.py | 7 ++- mayan/apps/document_signatures/views.py | 62 +++++++++---------- 6 files changed, 93 insertions(+), 48 deletions(-) diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index e8b2468e04..6be04810e7 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -12,7 +12,6 @@ from acls import ModelPermission from common import ( MayanAppConfig, menu_object, menu_sidebar ) -from common.widgets import two_state_template from mayan.celery import app from navigation import SourceColumn @@ -56,8 +55,6 @@ class DocumentSignaturesApp(MayanAppConfig): app_label='django_gpg', model_name='Key' ) - DetachedSignature = self.get_model('DetachedSignature') - EmbeddedSignature = self.get_model('EmbeddedSignature') SignatureBaseModel = self.get_model('SignatureBaseModel') diff --git a/mayan/apps/document_signatures/forms.py b/mayan/apps/document_signatures/forms.py index b323b1abf6..09139f78cd 100644 --- a/mayan/apps/document_signatures/forms.py +++ b/mayan/apps/document_signatures/forms.py @@ -9,12 +9,6 @@ from django_gpg.models import Key from .models import SignatureBaseModel -class DetachedSignatureForm(forms.Form): - file = forms.FileField( - label=_('Signature file'), - ) - - class DocumentVersionSignatureDetailForm(DetailForm): def __init__(self, *args, **kwargs): extra_fields = ( @@ -70,7 +64,9 @@ class DocumentVersionSignatureDetailForm(DetailForm): ) kwargs['extra_fields'] = extra_fields - super(DocumentVersionSignatureDetailForm, self).__init__(*args, **kwargs) + super( + DocumentVersionSignatureDetailForm, self + ).__init__(*args, **kwargs) class Meta: fields = () diff --git a/mayan/apps/document_signatures/models.py b/mayan/apps/document_signatures/models.py index 197ab83103..2a7b3bff95 100644 --- a/mayan/apps/document_signatures/models.py +++ b/mayan/apps/document_signatures/models.py @@ -98,7 +98,9 @@ class EmbeddedSignature(SignatureBaseModel): with self.document_version.open(raw=raw) as file_object: try: - verify_result = Key.objects.verify_file(file_object=file_object) + verify_result = Key.objects.verify_file( + file_object=file_object + ) except VerificationError as exception: # Not signed logger.debug( @@ -113,6 +115,7 @@ class EmbeddedSignature(SignatureBaseModel): super(EmbeddedSignature, self).save(*args, **kwargs) +@python_2_unicode_compatible class DetachedSignature(SignatureBaseModel): signature_file = models.FileField( blank=True, null=True, storage=storage_backend, upload_to=upload_to, @@ -123,6 +126,9 @@ class DetachedSignature(SignatureBaseModel): verbose_name = _('Document version detached signature') verbose_name_plural = _('Document version detached signatures') + def __str__(self): + return '{}-{}'.format(self.document_version, _('signature')) + 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/tests/test_views.py b/mayan/apps/document_signatures/tests/test_views.py index 85088784ba..eaa1f82ed2 100644 --- a/mayan/apps/document_signatures/tests/test_views.py +++ b/mayan/apps/document_signatures/tests/test_views.py @@ -2,8 +2,9 @@ from __future__ import absolute_import, unicode_literals from django.core.files import File +from django_downloadview.test import assert_download_response + from django_gpg.models import Key -from documents.permissions import permission_document_view from documents.tests.literals import TEST_DOCUMENT_PATH from documents.tests.test_views import GenericDocumentViewTestCase from user_management.tests import ( @@ -168,3 +169,51 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): self.assertEqual(response.status_code, 302) self.assertEqual(DetachedSignature.objects.count(), 1) + + def test_signature_download_view_no_permission(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: + signature = DetachedSignature.objects.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + response = self.get( + 'signatures:document_version_signature_download', + args=(signature.pk,), + ) + + self.assertEqual(response.status_code, 403) + + def test_signature_download_view_with_permission(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: + signature = DetachedSignature.objects.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add( + permission_document_version_signature_download.stored_permission + ) + + response = self.get( + 'signatures:document_version_signature_download', + args=(signature.pk,), + ) + + assert_download_response( + self, response=response, content=signature.signature_file.read(), + ) diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index 1695d51de3..971b615877 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -4,11 +4,12 @@ from django.conf.urls import patterns, url from .views import ( DocumentVersionSignatureDeleteView, DocumentVersionSignatureDetailView, - DocumentVersionSignatureListView, DocumentVersionSignatureUploadView + DocumentSignatureDownloadView, DocumentVersionSignatureListView, + DocumentVersionSignatureUploadView ) urlpatterns = patterns( - 'document_signatures.views', + '', url( r'^(?P\d+)/details/$', DocumentVersionSignatureDetailView.as_view(), @@ -16,7 +17,7 @@ urlpatterns = patterns( ), url( r'^signature/(?P\d+)/download/$', - 'document_signature_download', + DocumentSignatureDownloadView.as_view(), name='document_version_signature_download' ), url( diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index e1703ce687..f05b483983 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -2,25 +2,20 @@ from __future__ import absolute_import, unicode_literals import logging -from django.conf import settings -from django.contrib import messages from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse -from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404, render_to_response -from django.template import RequestContext +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 ( SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView, - SingleObjectListView + SingleObjectDownloadView, SingleObjectListView ) from documents.models import DocumentVersion -from filetransfers.api import serve_file from permissions import Permission -from .forms import DetachedSignatureForm, DocumentVersionSignatureDetailForm +from .forms import DocumentVersionSignatureDetailForm from .models import DetachedSignature, SignatureBaseModel from .permissions import ( permission_document_version_signature_view, @@ -34,12 +29,16 @@ logger = logging.getLogger(__name__) class DocumentVersionSignatureDeleteView(SingleObjectDeleteView): model = DetachedSignature + object_permission = permission_document_version_signature_delete + object_permission_related = 'document_version.document' 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'), + 'navigation_object_list': ( + 'document', 'document_version', 'signature' + ), 'signature': self.get_object(), 'title': _('Delete detached signature: %s') % self.get_object() } @@ -61,7 +60,9 @@ class DocumentVersionSignatureDetailView(SingleObjectDetailView): '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'), + 'navigation_object_list': ( + 'document', 'document_version', 'signature' + ), 'hide_object': True, 'title': _( 'Details for signature: %s' @@ -72,6 +73,19 @@ class DocumentVersionSignatureDetailView(SingleObjectDetailView): return SignatureBaseModel.objects.select_subclasses() +class DocumentSignatureDownloadView(SingleObjectDownloadView): + model = DetachedSignature + object_permission = permission_document_version_signature_download + object_permission_related = 'document_version.document' + + def get_file(self): + signature = self.get_object() + + return DocumentSignatureDownloadView.VirtualFile( + signature.signature_file, name=unicode(signature) + ) + + class DocumentVersionSignatureListView(SingleObjectListView): object_permission = permission_document_version_signature_view object_permission_related = 'document_version.document' @@ -95,11 +109,14 @@ class DocumentVersionSignatureListView(SingleObjectListView): try: Permission.check_permissions( - self.request.user, (permission_document_version_signature_view,) + 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 + permission_document_version_signature_view, self.request.user, + queryset ) else: return queryset @@ -145,24 +162,3 @@ class DocumentVersionSignatureUploadView(SingleObjectCreateView): 'signatures:document_version_signature_list', args=(self.get_document_version().pk,) ) - - -def document_signature_download(request, pk): - signature = get_object_or_404(DetachedSignature, pk=pk) - - try: - Permission.check_permissions( - request.user, (permission_document_version_signature_download,) - ) - except PermissionDenied: - AccessControlList.objects.check_access( - permission_document_version_signature_download, request.user, - signature.document_version.signature - ) - - return serve_file( - request, - signature, - save_as='"%s.sig"' % signature.document_version.document, - content_type='application/octet-stream' - ) From 0783806fd1a620f729f73aafc903786abc24c5a6 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Mar 2016 13:52:35 -0400 Subject: [PATCH 32/57] Add signature deletion view tests. --- .../document_signatures/tests/test_views.py | 54 +++++++++++++++++++ mayan/apps/document_signatures/urls.py | 4 +- mayan/apps/document_signatures/views.py | 4 +- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/mayan/apps/document_signatures/tests/test_views.py b/mayan/apps/document_signatures/tests/test_views.py index eaa1f82ed2..3565604c22 100644 --- a/mayan/apps/document_signatures/tests/test_views.py +++ b/mayan/apps/document_signatures/tests/test_views.py @@ -217,3 +217,57 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): assert_download_response( self, response=response, content=signature.signature_file.read(), ) + + def test_signature_delete_view_no_permission(self): + with open(TEST_KEY_FILE) as file_object: + Key.objects.create(key_data=file_object.read()) + + 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: + signature = DetachedSignature.objects.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + response = self.post( + 'signatures:document_version_signature_delete', + args=(signature.pk,) + ) + + self.assertEqual(response.status_code, 403) + self.assertEqual(DetachedSignature.objects.count(), 1) + + def test_signature_delete_view_with_permission(self): + with open(TEST_KEY_FILE) as file_object: + Key.objects.create(key_data=file_object.read()) + + 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: + signature = DetachedSignature.objects.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add( + permission_document_version_signature_delete.stored_permission + ) + + response = self.post( + 'signatures:document_version_signature_delete', + args=(signature.pk,), follow=True + ) + + self.assertContains(response, 'deleted', status_code=200) + self.assertEqual(DetachedSignature.objects.count(), 0) diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index 971b615877..f143168f6e 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -4,7 +4,7 @@ from django.conf.urls import patterns, url from .views import ( DocumentVersionSignatureDeleteView, DocumentVersionSignatureDetailView, - DocumentSignatureDownloadView, DocumentVersionSignatureListView, + DocumentVersionSignatureDownloadView, DocumentVersionSignatureListView, DocumentVersionSignatureUploadView ) @@ -17,7 +17,7 @@ urlpatterns = patterns( ), url( r'^signature/(?P\d+)/download/$', - DocumentSignatureDownloadView.as_view(), + DocumentVersionSignatureDownloadView.as_view(), name='document_version_signature_download' ), url( diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index f05b483983..848f946e84 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -73,7 +73,7 @@ class DocumentVersionSignatureDetailView(SingleObjectDetailView): return SignatureBaseModel.objects.select_subclasses() -class DocumentSignatureDownloadView(SingleObjectDownloadView): +class DocumentVersionSignatureDownloadView(SingleObjectDownloadView): model = DetachedSignature object_permission = permission_document_version_signature_download object_permission_related = 'document_version.document' @@ -81,7 +81,7 @@ class DocumentSignatureDownloadView(SingleObjectDownloadView): def get_file(self): signature = self.get_object() - return DocumentSignatureDownloadView.VirtualFile( + return DocumentVersionSignatureDownloadView.VirtualFile( signature.signature_file, name=unicode(signature) ) From 739b96ed3745ef12c22637f178e9f1e233ff7140 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Mar 2016 16:27:53 -0400 Subject: [PATCH 33/57] Add related object link permission support. --- mayan/apps/acls/managers.py | 1 + mayan/apps/navigation/classes.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index fb6c4f822c..ef66326d31 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -55,6 +55,7 @@ class AccessControlListManager(models.Manager): permission.stored_permission for permission in permissions ] except TypeError: + # Not a list of permissions, just one stored_permissions = [permissions.stored_permission] if related: diff --git a/mayan/apps/navigation/classes.py b/mayan/apps/navigation/classes.py index 6a32c07466..2151585b08 100644 --- a/mayan/apps/navigation/classes.py +++ b/mayan/apps/navigation/classes.py @@ -202,7 +202,7 @@ class Link(object): def __init__(self, text, view, args=None, condition=None, conditional_disable=None, description=None, icon=None, keep_query=False, kwargs=None, permissions=None, - remove_from_query=None, tags=None): + permissions_related=None, remove_from_query=None, tags=None): self.args = args or [] self.condition = condition @@ -245,7 +245,8 @@ class Link(object): if resolved_object: try: AccessControlList.objects.check_access( - self.permissions, request.user, resolved_object + self.permissions, request.user, resolved_object, + related=getattr(self, 'permissions_related', None) ) except PermissionDenied: return None From d83a80c65be0054e840b1bec12a9c9b337770dc3 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Mar 2016 16:28:24 -0400 Subject: [PATCH 34/57] Add document signature app link tests. --- mayan/apps/document_signatures/links.py | 32 ++--- .../migrations/0003_auto_20160325_0052.py | 76 +++++++++-- .../migrations/0004_auto_20160325_0418.py | 25 +++- .../migrations/0006_auto_20160326_0616.py | 5 +- mayan/apps/document_signatures/permissions.py | 8 +- .../document_signatures/tests/test_links.py | 129 ++++++++++++++++++ .../document_signatures/tests/test_views.py | 5 +- mayan/apps/document_signatures/views.py | 32 ++--- 8 files changed, 253 insertions(+), 59 deletions(-) create mode 100644 mayan/apps/document_signatures/tests/test_links.py diff --git a/mayan/apps/document_signatures/links.py b/mayan/apps/document_signatures/links.py index b5bb879275..66a93d8317 100644 --- a/mayan/apps/document_signatures/links.py +++ b/mayan/apps/document_signatures/links.py @@ -9,6 +9,7 @@ from .permissions import ( permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_upload, + permission_document_version_signature_view ) @@ -23,33 +24,32 @@ def is_detached_signature(context): 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' + args='resolved_object.pk', condition=is_detached_signature, + permissions=(permission_document_version_signature_delete,), + permissions_related='document_version.document', tags='dangerous', + text=_('Delete'), view='signatures:document_version_signature_delete', ) link_document_version_signature_details = Link( - #permissions=(permission_document_version_signature_view,), - text=_('Details'), + args='resolved_object.pk', + permissions=(permission_document_version_signature_view,), + permissions_related='document_version.document', text=_('Details'), view='signatures:document_version_signature_details', - args='resolved_object.pk' ) link_document_version_signature_list = Link( - #permissions=(permission_document_version_signature_view,), + args='resolved_object.pk', + permissions=(permission_document_version_signature_view,), text=_('Signature list'), view='signatures:document_version_signature_list', - args='resolved_object.pk' ) link_document_version_signature_download = Link( - condition=is_detached_signature, - text=_('Download'), - view='signatures:document_version_signature_download', args='resolved_object.pk', - #permissions=(permission_document_version_signature_download,) + args='resolved_object.pk', condition=is_detached_signature, + permissions=(permission_document_version_signature_download,), + permissions_related='document_version.document', text=_('Download'), + view='signatures:document_version_signature_download', ) link_document_version_signature_upload = Link( - #permissions=(permission_document_version_signature_upload,), + args='resolved_object.pk', + permissions=(permission_document_version_signature_upload,), text=_('Upload signature'), view='signatures:document_version_signature_upload', - args='resolved_object.pk' ) diff --git a/mayan/apps/document_signatures/migrations/0003_auto_20160325_0052.py b/mayan/apps/document_signatures/migrations/0003_auto_20160325_0052.py index 76fa8f097f..044e6f5a35 100644 --- a/mayan/apps/document_signatures/migrations/0003_auto_20160325_0052.py +++ b/mayan/apps/document_signatures/migrations/0003_auto_20160325_0052.py @@ -17,11 +17,34 @@ class Migration(migrations.Migration): 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)), + ( + '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', @@ -35,23 +58,43 @@ class Migration(migrations.Migration): migrations.AddField( model_name='documentversionsignature', name='date', - field=models.DateField(null=True, verbose_name='Date signed', blank=True), + 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), + 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'), + 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)), + ( + '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', @@ -62,7 +105,13 @@ class Migration(migrations.Migration): migrations.CreateModel( name='EmbeddedSignature', fields=[ - ('signaturebasemodel_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='document_signatures.SignatureBaseModel')), + ( + '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', @@ -73,6 +122,9 @@ class Migration(migrations.Migration): migrations.AddField( model_name='signaturebasemodel', name='document_version', - field=models.ForeignKey(related_name='signaturebasemodel', editable=False, to='documents.DocumentVersion', verbose_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 index 469cf232b9..8cbbd1c419 100644 --- a/mayan/apps/document_signatures/migrations/0004_auto_20160325_0418.py +++ b/mayan/apps/document_signatures/migrations/0004_auto_20160325_0418.py @@ -14,26 +14,41 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='documentversionsignature', name='document_version', - field=models.ForeignKey(editable=False, to='documents.DocumentVersion', verbose_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), + 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'), + 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'), + 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), + field=models.CharField( + verbose_name='Signature ID', max_length=64, null=True, + editable=False, blank=True + ), ), ] diff --git a/mayan/apps/document_signatures/migrations/0006_auto_20160326_0616.py b/mayan/apps/document_signatures/migrations/0006_auto_20160326_0616.py index e5db17f576..adb06aeea3 100644 --- a/mayan/apps/document_signatures/migrations/0006_auto_20160326_0616.py +++ b/mayan/apps/document_signatures/migrations/0006_auto_20160326_0616.py @@ -14,6 +14,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='signaturebasemodel', name='public_key_fingerprint', - field=models.CharField(verbose_name='Public key fingerprint', max_length=40, null=True, editable=False, blank=True), + field=models.CharField( + verbose_name='Public key fingerprint', max_length=40, + null=True, editable=False, blank=True + ), ), ] diff --git a/mayan/apps/document_signatures/permissions.py b/mayan/apps/document_signatures/permissions.py index f81ad66806..8db3806b60 100644 --- a/mayan/apps/document_signatures/permissions.py +++ b/mayan/apps/document_signatures/permissions.py @@ -8,10 +8,6 @@ namespace = PermissionNamespace( 'document_signatures', _('Document signatures') ) -permission_document_version_signature_view = namespace.add_permission( - name='document_version_signature_view', - label=_('View details of document signatures') -) permission_document_version_signature_delete = namespace.add_permission( name='document_version_signature_delete', label=_('Delete detached signatures') @@ -24,3 +20,7 @@ permission_document_version_signature_upload = namespace.add_permission( name='document_version_signature_upload', label=_('Upload detached document signatures') ) +permission_document_version_signature_view = namespace.add_permission( + name='document_version_signature_view', + label=_('View details of document signatures') +) diff --git a/mayan/apps/document_signatures/tests/test_links.py b/mayan/apps/document_signatures/tests/test_links.py new file mode 100644 index 0000000000..169af19344 --- /dev/null +++ b/mayan/apps/document_signatures/tests/test_links.py @@ -0,0 +1,129 @@ +from __future__ import unicode_literals + +from django.core.files import File +from django.core.urlresolvers import reverse + +from documents.tests.literals import TEST_DOCUMENT_PATH +from documents.tests.test_views import GenericDocumentViewTestCase +from user_management.tests.literals import ( + TEST_USER_PASSWORD, TEST_USER_USERNAME +) + +from ..links import ( + link_document_version_signature_delete, + link_document_version_signature_details, +) +from ..models import DetachedSignature +from ..permissions import ( + permission_document_version_signature_delete, + permission_document_version_signature_view +) +from .literals import TEST_SIGNATURE_FILE_PATH, TEST_SIGNED_DOCUMENT_PATH + + +class DocumentSignatureLinksTestCase(GenericDocumentViewTestCase): + def test_document_version_signature_detail_link_no_permission(self): + with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: + document = self.document_type.new_document( + file_object=file_object + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.add_test_view( + test_object=document.latest_version.signatures.first() + ) + context = self.get_test_view() + resolved_link = link_document_version_signature_details.resolve( + context=context + ) + + self.assertEqual(resolved_link, None) + + def test_document_version_signature_detail_link_with_permission(self): + with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: + document = self.document_type.new_document( + file_object=file_object + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add( + permission_document_version_signature_view.stored_permission + ) + + self.add_test_view( + test_object=document.latest_version.signatures.first() + ) + context = self.get_test_view() + resolved_link = link_document_version_signature_details.resolve( + context=context + ) + + self.assertNotEqual(resolved_link, None) + self.assertEqual( + resolved_link.url, + reverse( + 'signatures:document_version_signature_details', + args=(document.latest_version.signatures.first().pk,) + ) + ) + + def test_document_version_signature_delete_link_no_permission(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.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.add_test_view( + test_object=document.latest_version.signatures.first() + ) + context = self.get_test_view() + resolved_link = link_document_version_signature_delete.resolve( + context=context + ) + + self.assertEqual(resolved_link, None) + + def test_document_version_signature_delete_link_with_permission(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.create( + document_version=document.latest_version, + signature_file=File(file_object) + ) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add( + permission_document_version_signature_delete.stored_permission + ) + + self.add_test_view( + test_object=document.latest_version.signatures.first() + ) + context = self.get_test_view() + resolved_link = link_document_version_signature_delete.resolve( + context=context + ) + + self.assertNotEqual(resolved_link, None) + self.assertEqual( + resolved_link.url, + reverse( + 'signatures:document_version_signature_delete', + args=(document.latest_version.signatures.first().pk,) + ) + ) diff --git a/mayan/apps/document_signatures/tests/test_views.py b/mayan/apps/document_signatures/tests/test_views.py index 3565604c22..8bacb24f82 100644 --- a/mayan/apps/document_signatures/tests/test_views.py +++ b/mayan/apps/document_signatures/tests/test_views.py @@ -19,10 +19,7 @@ from ..permissions import ( permission_document_version_signature_upload, ) -from .literals import ( - TEST_SIGNED_DOCUMENT_PATH, TEST_SIGNATURE_FILE_PATH, TEST_KEY_FILE, - TEST_KEY_ID, TEST_SIGNATURE_ID -) +from .literals import TEST_SIGNATURE_FILE_PATH, TEST_KEY_FILE class SignaturesViewTestCase(GenericDocumentViewTestCase): diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index 848f946e84..083ae02c22 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -87,8 +87,20 @@ class DocumentVersionSignatureDownloadView(SingleObjectDownloadView): class DocumentVersionSignatureListView(SingleObjectListView): - object_permission = permission_document_version_signature_view - object_permission_related = 'document_version.document' + def dispatch(self, request, *args, **kwargs): + try: + Permission.check_permissions( + request.user, (permission_document_version_signature_view,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_document_version_signature_view, request.user, + self.get_document_version() + ) + + return super( + DocumentVersionSignatureListView, self + ).dispatch(request, *args, **kwargs) def get_document_version(self): return get_object_or_404(DocumentVersion, pk=self.kwargs['pk']) @@ -105,21 +117,7 @@ class DocumentVersionSignatureListView(SingleObjectListView): } 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 + return self.get_document_version().signatures.all() class DocumentVersionSignatureUploadView(SingleObjectCreateView): From 7da6cf1863e23941a7ba061beb41b637457e15d2 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Mar 2016 20:13:58 -0400 Subject: [PATCH 35/57] Add view, task and post upgrade signal handler to verify all documents for embedded signatures. --- mayan/apps/document_signatures/apps.py | 35 ++++-- mayan/apps/document_signatures/handlers.py | 17 ++- mayan/apps/document_signatures/links.py | 6 + mayan/apps/document_signatures/managers.py | 6 + mayan/apps/document_signatures/permissions.py | 4 + mayan/apps/document_signatures/tasks.py | 30 ++++- .../document_signatures/tests/test_models.py | 69 +++++++++++- .../document_signatures/tests/test_views.py | 105 +++++++++++++++++- mayan/apps/document_signatures/urls.py | 11 +- mayan/apps/document_signatures/views.py | 31 +++++- 10 files changed, 287 insertions(+), 27 deletions(-) diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index 6be04810e7..516730e827 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -10,13 +10,18 @@ from django.utils.translation import ugettext_lazy as _ from acls import ModelPermission from common import ( - MayanAppConfig, menu_object, menu_sidebar + MayanAppConfig, menu_object, menu_sidebar, menu_tools ) +from common.signals import post_upgrade from mayan.celery import app from navigation import SourceColumn -from .handlers import unverify_signatures, verify_signatures +from .handlers import ( + unverify_key_signatures, verify_key_signatures, + verify_missing_embedded_signature +) from .links import ( + link_all_document_version_signature_verify, link_document_version_signature_delete, link_document_version_signature_details, link_document_version_signature_download, @@ -101,12 +106,18 @@ class DocumentSignaturesApp(MayanAppConfig): app.conf.CELERY_ROUTES.update( { - 'document_signatures.tasks.task_verify_signatures': { + 'document_signatures.tasks.task_verify_key_signatures': { 'queue': 'signatures' }, - 'document_signatures.tasks.task_unverify_signatures': { + 'document_signatures.tasks.task_unverify_key_signatures': { 'queue': 'signatures' }, + 'document_signatures.tasks.task_verify_document_version': { + 'queue': 'signatures' + }, + 'document_signatures.tasks.task_verify_missing_embedded_signature': { + 'queue': 'tools' + }, } ) @@ -126,13 +137,21 @@ class DocumentSignaturesApp(MayanAppConfig): link_document_version_signature_upload, ), sources=(DocumentVersion,) ) + menu_tools.bind_links( + links=(link_all_document_version_signature_verify,) + ) + post_delete.connect( - unverify_signatures, - dispatch_uid='unverify_signatures', + unverify_key_signatures, + dispatch_uid='unverify_key_signatures', sender=Key ) + post_upgrade.connect( + verify_missing_embedded_signature, + dispatch_uid='verify_missing_embedded_signature', + ) post_save.connect( - verify_signatures, - dispatch_uid='verify_signatures', + verify_key_signatures, + dispatch_uid='verify_key_signatures', sender=Key ) diff --git a/mayan/apps/document_signatures/handlers.py b/mayan/apps/document_signatures/handlers.py index 38a91cc821..988f4da513 100644 --- a/mayan/apps/document_signatures/handlers.py +++ b/mayan/apps/document_signatures/handlers.py @@ -1,15 +1,22 @@ from __future__ import unicode_literals -from .tasks import task_unverify_signatures, task_verify_signatures +from .tasks import ( + task_unverify_key_signatures, task_verify_missing_embedded_signature, + task_verify_key_signatures +) -def unverify_signatures(sender, **kwargs): - task_unverify_signatures.apply_async( +def unverify_key_signatures(sender, **kwargs): + task_unverify_key_signatures.apply_async( kwargs=dict(key_id=kwargs['instance'].key_id) ) -def verify_signatures(sender, **kwargs): - task_verify_signatures.apply_async( +def verify_key_signatures(sender, **kwargs): + task_verify_key_signatures.apply_async( kwargs=dict(key_pk=kwargs['instance'].pk) ) + + +def verify_missing_embedded_signature(sender, **kwargs): + task_verify_missing_embedded_signature.delay() diff --git a/mayan/apps/document_signatures/links.py b/mayan/apps/document_signatures/links.py index 66a93d8317..b2206cf3dd 100644 --- a/mayan/apps/document_signatures/links.py +++ b/mayan/apps/document_signatures/links.py @@ -9,6 +9,7 @@ from .permissions import ( 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 ) @@ -23,6 +24,11 @@ def is_detached_signature(context): ).is_detached +link_all_document_version_signature_verify = Link( + permissions=(permission_document_version_signature_verify,), + text=_('Verify all documents'), + view='signatures:all_document_version_signature_verify', +) link_document_version_signature_delete = Link( args='resolved_object.pk', condition=is_detached_signature, permissions=(permission_document_version_signature_delete,), diff --git a/mayan/apps/document_signatures/managers.py b/mayan/apps/document_signatures/managers.py index 0c5cf80ca3..9c3b90de19 100644 --- a/mayan/apps/document_signatures/managers.py +++ b/mayan/apps/document_signatures/managers.py @@ -6,6 +6,7 @@ from django.db import models from django_gpg.exceptions import DecryptionError from django_gpg.models import Key +from documents.models import DocumentVersion logger = logging.getLogger(__name__) @@ -24,3 +25,8 @@ class EmbeddedSignatureManager(models.Manager): return file_object else: return file_object + + def unsigned_document_versions(self): + return DocumentVersion.objects.exclude( + pk__in=self.values('document_version') + ) diff --git a/mayan/apps/document_signatures/permissions.py b/mayan/apps/document_signatures/permissions.py index 8db3806b60..307758c374 100644 --- a/mayan/apps/document_signatures/permissions.py +++ b/mayan/apps/document_signatures/permissions.py @@ -20,6 +20,10 @@ permission_document_version_signature_upload = namespace.add_permission( name='document_version_signature_upload', label=_('Upload detached document signatures') ) +permission_document_version_signature_verify = namespace.add_permission( + name='document_version_signature_verify', + label=_('Verify document signatures') +) permission_document_version_signature_view = namespace.add_permission( name='document_version_signature_view', label=_('View details of document signatures') diff --git a/mayan/apps/document_signatures/tasks.py b/mayan/apps/document_signatures/tasks.py index 854b5bb1d4..9657f6417f 100644 --- a/mayan/apps/document_signatures/tasks.py +++ b/mayan/apps/document_signatures/tasks.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) @app.task(bind=True, ignore_result=True) -def task_unverify_signatures(self, key_id): +def task_unverify_key_signatures(self, key_id): DetachedSignature = apps.get_model( app_label='document_signatures', model_name='DetachedSignature' ) @@ -28,7 +28,7 @@ def task_unverify_signatures(self, key_id): @app.task(bind=True, ignore_result=True) -def task_verify_signatures(self, key_pk): +def task_verify_key_signatures(self, key_pk): Key = apps.get_model( app_label='django_gpg', model_name='Key' ) @@ -48,3 +48,29 @@ def task_verify_signatures(self, key_pk): for signature in EmbeddedSignature.objects.filter(key_id__endswith=key.key_id).filter(signature_id__isnull=True): signature.save() + + +@app.task(bind=True, ignore_result=True) +def task_verify_missing_embedded_signature(self): + EmbeddedSignature = apps.get_model( + app_label='document_signatures', model_name='EmbeddedSignature' + ) + + for document_version in EmbeddedSignature.objects.unsigned_document_versions(): + task_verify_document_version.apply_async( + kwargs=dict(document_version_pk=document_version.pk) + ) + + +@app.task(bind=True, ignore_result=True) +def task_verify_document_version(self, document_version_pk): + DocumentVersion = apps.get_model( + app_label='documents', model_name='DocumentVersion' + ) + + EmbeddedSignature = apps.get_model( + app_label='document_signatures', model_name='EmbeddedSignature' + ) + + document_version = DocumentVersion.objects.get(pk=document_version_pk) + EmbeddedSignature.objects.create(document_version=document_version) diff --git a/mayan/apps/document_signatures/tests/test_models.py b/mayan/apps/document_signatures/tests/test_models.py index 5366c3bac3..8e9f61c299 100644 --- a/mayan/apps/document_signatures/tests/test_models.py +++ b/mayan/apps/document_signatures/tests/test_models.py @@ -6,10 +6,11 @@ from django.core.files import File from django.test import TestCase, override_settings from django_gpg.models import Key -from documents.models import DocumentType +from documents.models import DocumentType, DocumentVersion from documents.tests import TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE from ..models import DetachedSignature, EmbeddedSignature +from ..tasks import task_verify_missing_embedded_signature from .literals import ( TEST_SIGNED_DOCUMENT_PATH, TEST_SIGNATURE_FILE_PATH, TEST_KEY_FILE, @@ -241,3 +242,69 @@ class DocumentSignaturesTestCase(TestCase): self.assertEqual(signature.document_version, signed_version) self.assertEqual(signature.key_id, TEST_KEY_ID) + + +@override_settings(OCR_AUTO_OCR=False) +class EmbeddedSignaturesTestCase(TestCase): + def setUp(self): + self.document_type = DocumentType.objects.create( + label=TEST_DOCUMENT_TYPE + ) + + def tearDown(self): + self.document_type.delete() + + def test_unsigned_document_version_method(self): + TEST_UNSIGNED_DOCUMENT_COUNT = 3 + TEST_SIGNED_DOCUMENT_COUNT = 3 + + for count in range(TEST_UNSIGNED_DOCUMENT_COUNT): + with open(TEST_DOCUMENT_PATH) as file_object: + self.document_type.new_document( + file_object=file_object + ) + + for count in range(TEST_SIGNED_DOCUMENT_COUNT): + with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: + self.document_type.new_document( + file_object=file_object + ) + + self.assertEqual( + EmbeddedSignature.objects.unsigned_document_versions().count(), + TEST_UNSIGNED_DOCUMENT_COUNT + ) + + def test_task_verify_missing_embedded_signature(self): + old_hooks = DocumentVersion._post_save_hooks + + DocumentVersion._post_save_hooks = {} + + TEST_UNSIGNED_DOCUMENT_COUNT = 4 + TEST_SIGNED_DOCUMENT_COUNT = 2 + + for count in range(TEST_UNSIGNED_DOCUMENT_COUNT): + with open(TEST_DOCUMENT_PATH) as file_object: + self.document_type.new_document( + file_object=file_object + ) + + for count in range(TEST_SIGNED_DOCUMENT_COUNT): + with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: + self.document_type.new_document( + file_object=file_object + ) + + self.assertEqual( + EmbeddedSignature.objects.unsigned_document_versions().count(), + TEST_UNSIGNED_DOCUMENT_COUNT + TEST_SIGNED_DOCUMENT_COUNT + ) + + DocumentVersion._post_save_hooks = old_hooks + + task_verify_missing_embedded_signature.delay() + + self.assertEqual( + EmbeddedSignature.objects.unsigned_document_versions().count(), + TEST_UNSIGNED_DOCUMENT_COUNT + ) diff --git a/mayan/apps/document_signatures/tests/test_views.py b/mayan/apps/document_signatures/tests/test_views.py index 8bacb24f82..fdcaa38649 100644 --- a/mayan/apps/document_signatures/tests/test_views.py +++ b/mayan/apps/document_signatures/tests/test_views.py @@ -5,21 +5,29 @@ from django.core.files import File from django_downloadview.test import assert_download_response from django_gpg.models import Key +from documents.models import Document, DocumentVersion from documents.tests.literals import TEST_DOCUMENT_PATH from documents.tests.test_views import GenericDocumentViewTestCase from user_management.tests import ( TEST_USER_USERNAME, TEST_USER_PASSWORD ) -from ..models import DetachedSignature +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, + permission_document_version_signature_verify, + permission_document_version_signature_view ) -from .literals import TEST_SIGNATURE_FILE_PATH, TEST_KEY_FILE +from .literals import ( + TEST_SIGNATURE_FILE_PATH, TEST_SIGNED_DOCUMENT_PATH, TEST_KEY_FILE +) + +TEST_UNSIGNED_DOCUMENT_COUNT = 4 +TEST_SIGNED_DOCUMENT_COUNT = 2 class SignaturesViewTestCase(GenericDocumentViewTestCase): @@ -45,7 +53,7 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): args=(document.latest_version.pk,) ) - self.assertContains(response, 'Total: 0', status_code=200) + self.assertEqual(response.status_code, 403) def test_signature_list_view_with_permission(self): with open(TEST_KEY_FILE) as file_object: @@ -232,6 +240,10 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + self.role.permissions.add( + permission_document_version_signature_view.stored_permission + ) + response = self.post( 'signatures:document_version_signature_delete', args=(signature.pk,) @@ -260,6 +272,9 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): self.role.permissions.add( permission_document_version_signature_delete.stored_permission ) + self.role.permissions.add( + permission_document_version_signature_view.stored_permission + ) response = self.post( 'signatures:document_version_signature_delete', @@ -268,3 +283,87 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): self.assertContains(response, 'deleted', status_code=200) self.assertEqual(DetachedSignature.objects.count(), 0) + + def test_missing_signature_verify_view_no_permission(self): + 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): + with open(TEST_DOCUMENT_PATH) as file_object: + self.document_type.new_document( + file_object=file_object + ) + + for count in range(TEST_SIGNED_DOCUMENT_COUNT): + with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: + self.document_type.new_document( + file_object=file_object + ) + + self.assertEqual( + EmbeddedSignature.objects.unsigned_document_versions().count(), + TEST_UNSIGNED_DOCUMENT_COUNT + TEST_SIGNED_DOCUMENT_COUNT + ) + + DocumentVersion._post_save_hooks = old_hooks + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + response = self.post( + 'signatures:all_document_version_signature_verify', follow=True + ) + + self.assertEqual(response.status_code, 403) + + self.assertEqual( + EmbeddedSignature.objects.unsigned_document_versions().count(), + TEST_UNSIGNED_DOCUMENT_COUNT + TEST_SIGNED_DOCUMENT_COUNT + ) + + def test_missing_signature_verify_view_with_permission(self): + 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): + with open(TEST_DOCUMENT_PATH) as file_object: + self.document_type.new_document( + file_object=file_object + ) + + for count in range(TEST_SIGNED_DOCUMENT_COUNT): + with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: + self.document_type.new_document( + file_object=file_object + ) + + self.assertEqual( + EmbeddedSignature.objects.unsigned_document_versions().count(), + TEST_UNSIGNED_DOCUMENT_COUNT + TEST_SIGNED_DOCUMENT_COUNT + ) + + DocumentVersion._post_save_hooks = old_hooks + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add( + permission_document_version_signature_verify.stored_permission + ) + + response = self.post( + 'signatures:all_document_version_signature_verify', follow=True + ) + + self.assertContains(response, 'queued', status_code=200) + + self.assertEqual( + EmbeddedSignature.objects.unsigned_document_versions().count(), + TEST_UNSIGNED_DOCUMENT_COUNT + ) diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index f143168f6e..988b5a9226 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -3,9 +3,9 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url from .views import ( - DocumentVersionSignatureDeleteView, DocumentVersionSignatureDetailView, - DocumentVersionSignatureDownloadView, DocumentVersionSignatureListView, - DocumentVersionSignatureUploadView + AllDocumentSignatureVerifyView, DocumentVersionSignatureDeleteView, + DocumentVersionSignatureDetailView, DocumentVersionSignatureDownloadView, + DocumentVersionSignatureListView, DocumentVersionSignatureUploadView ) urlpatterns = patterns( @@ -35,4 +35,9 @@ urlpatterns = patterns( DocumentVersionSignatureDeleteView.as_view(), name='document_version_signature_delete' ), + url( + r'^tools/all/document/version/signature/verify/$', + AllDocumentSignatureVerifyView.as_view(), + name='all_document_version_signature_verify' + ), ) diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index 083ae02c22..0dfd71463d 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals import logging +from django.contrib import messages from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse from django.shortcuts import get_object_or_404 @@ -9,8 +10,8 @@ from django.utils.translation import ugettext_lazy as _ from acls.models import AccessControlList from common.generics import ( - SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView, - SingleObjectDownloadView, SingleObjectListView + ConfirmView, SingleObjectCreateView, SingleObjectDeleteView, + SingleObjectDetailView, SingleObjectDownloadView, SingleObjectListView ) from documents.models import DocumentVersion from permissions import Permission @@ -18,11 +19,13 @@ from permissions import Permission from .forms import DocumentVersionSignatureDetailForm from .models import DetachedSignature, SignatureBaseModel from .permissions import ( - permission_document_version_signature_view, - permission_document_version_signature_upload, + permission_document_version_signature_delete, permission_document_version_signature_download, - permission_document_version_signature_delete + permission_document_version_signature_upload, + permission_document_version_signature_verify, + permission_document_version_signature_view, ) +from .tasks import task_verify_missing_embedded_signature logger = logging.getLogger(__name__) @@ -160,3 +163,21 @@ class DocumentVersionSignatureUploadView(SingleObjectCreateView): 'signatures:document_version_signature_list', args=(self.get_document_version().pk,) ) + + +class AllDocumentSignatureVerifyView(ConfirmView): + extra_context = { + 'message': _( + 'On large databases this operation may take some time to execute.' + ), 'title': _('Verify all document for signatures?'), + } + view_permission = permission_document_version_signature_verify + + def get_post_action_redirect(self): + return reverse('common:tools_list') + + def view_action(self): + task_verify_missing_embedded_signature.delay() + messages.success( + self.request, _('Signature verification queued successfully.') + ) From 8b28bdd4432ed8db4aa33098cb38a6706cbe35ba Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Mar 2016 21:03:16 -0400 Subject: [PATCH 36/57] Add missing icon to the all document signature verification tool link. --- mayan/apps/document_signatures/links.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mayan/apps/document_signatures/links.py b/mayan/apps/document_signatures/links.py index b2206cf3dd..fc2e060ec6 100644 --- a/mayan/apps/document_signatures/links.py +++ b/mayan/apps/document_signatures/links.py @@ -25,6 +25,7 @@ def is_detached_signature(context): link_all_document_version_signature_verify = Link( + icon='fa fa-certificate', permissions=(permission_document_version_signature_verify,), text=_('Verify all documents'), view='signatures:all_document_version_signature_verify', From c6890c487ab50ee324ff79eb02965cbf1dddec73 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Mar 2016 01:32:53 -0400 Subject: [PATCH 37/57] Extract a file like object's file name as per docs (using the .name property) and not an unicode representation of the file object instance. Reference: https://docs.python.org/2.7/library/stdtypes.html#file-objects --- mayan/apps/documents/models.py | 4 ++-- mayan/apps/sources/models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mayan/apps/documents/models.py b/mayan/apps/documents/models.py index e795bf74bf..ed605e11e4 100644 --- a/mayan/apps/documents/models.py +++ b/mayan/apps/documents/models.py @@ -127,7 +127,7 @@ class DocumentType(models.Model): with transaction.atomic(): document = self.documents.create( description=description or '', - label=label or unicode(file_object), + label=label or file_object.name, language=language or setting_language.value ) document.save(_user=_user) @@ -138,7 +138,7 @@ class DocumentType(models.Model): logger.critical( 'Unexpected exception while trying to create new document ' '"%s" from document type "%s"; %s', - label or unicode(file_object), self, exception + label or file_object.name, self, exception ) raise diff --git a/mayan/apps/sources/models.py b/mayan/apps/sources/models.py index 1e4baf9ebd..ed8cc90c6b 100644 --- a/mayan/apps/sources/models.py +++ b/mayan/apps/sources/models.py @@ -65,7 +65,7 @@ class Source(models.Model): with transaction.atomic(): document = Document.objects.create( description=description or '', document_type=document_type, - label=label or unicode(file_object), + label=label or file_object.name, language=language or setting_language.value ) document.save(_user=user) @@ -100,7 +100,7 @@ class Source(models.Model): logger.critical( 'Unexpected exception while trying to create new document ' '"%s" from source "%s"; %s', - label or unicode(file_object), self, exception + label or file_object.name, self, exception ) raise From 1e7e17bdce66a49a8b69d59419df9703608f5f2f Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Mar 2016 01:42:17 -0400 Subject: [PATCH 38/57] Insert the resolved_object context variable in the test views. --- mayan/apps/common/tests/test_views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mayan/apps/common/tests/test_views.py b/mayan/apps/common/tests/test_views.py index 4420a69794..b7851896e9 100644 --- a/mayan/apps/common/tests/test_views.py +++ b/mayan/apps/common/tests/test_views.py @@ -50,7 +50,9 @@ class GenericViewTestCase(TestCase): def test_view(request): template = Template('{{ object }}') - context = Context({'object': test_object}) + context = Context( + {'object': test_object, 'resolved_object': test_object} + ) return HttpResponse(template.render(context=context)) urlpatterns.insert(0, url(TEST_VIEW_URL, test_view, name=TEST_VIEW_NAME)) From 8baca70ef5c373aa4c5a526aad4bfccd1135ba66 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Mar 2016 02:00:17 -0400 Subject: [PATCH 39/57] Add link to document facet menu to display its latest version signatures. --- mayan/apps/document_signatures/apps.py | 6 +++++- mayan/apps/document_signatures/links.py | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index 516730e827..54f5e9509e 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from acls import ModelPermission from common import ( - MayanAppConfig, menu_object, menu_sidebar, menu_tools + MayanAppConfig, menu_facet, menu_object, menu_sidebar, menu_tools ) from common.signals import post_upgrade from mayan.celery import app @@ -22,6 +22,7 @@ from .handlers import ( ) from .links import ( link_all_document_version_signature_verify, + link_document_signature_list, link_document_version_signature_delete, link_document_version_signature_details, link_document_version_signature_download, @@ -121,6 +122,9 @@ class DocumentSignaturesApp(MayanAppConfig): } ) + menu_facet.bind_links( + links=(link_document_signature_list,), sources=(Document,) + ) menu_object.bind_links( links=(link_document_version_signature_list,), sources=(DocumentVersion,) diff --git a/mayan/apps/document_signatures/links.py b/mayan/apps/document_signatures/links.py index fc2e060ec6..6494764291 100644 --- a/mayan/apps/document_signatures/links.py +++ b/mayan/apps/document_signatures/links.py @@ -30,6 +30,13 @@ link_all_document_version_signature_verify = Link( text=_('Verify all documents'), view='signatures:all_document_version_signature_verify', ) +link_document_signature_list = Link( + args='resolved_object.latest_version.pk', + icon='fa fa-certificate', + permissions=(permission_document_version_signature_view,), + text=_('Signatures'), + view='signatures:document_version_signature_list', +) link_document_version_signature_delete = Link( args='resolved_object.pk', condition=is_detached_signature, permissions=(permission_document_version_signature_delete,), @@ -45,7 +52,7 @@ link_document_version_signature_details = Link( link_document_version_signature_list = Link( args='resolved_object.pk', permissions=(permission_document_version_signature_view,), - text=_('Signature list'), + permissions_related='document', text=_('Signature list'), view='signatures:document_version_signature_list', ) link_document_version_signature_download = Link( From 24d82870727b2b0886957ff302d5539ef60feb9c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Mar 2016 02:30:50 -0400 Subject: [PATCH 40/57] Add Key usage for signing permission. --- mayan/apps/django_gpg/apps.py | 6 ++++-- mayan/apps/django_gpg/permissions.py | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mayan/apps/django_gpg/apps.py b/mayan/apps/django_gpg/apps.py index f187bf25ba..7490aa6d21 100644 --- a/mayan/apps/django_gpg/apps.py +++ b/mayan/apps/django_gpg/apps.py @@ -16,7 +16,9 @@ from .links import ( link_key_delete, link_key_detail, link_key_query, link_key_receive, link_key_setup, link_private_keys, link_public_keys ) -from .permissions import permission_key_delete, permission_key_view +from .permissions import ( + permission_key_delete, permission_key_sign, permission_key_view +) class DjangoGPGApp(MayanAppConfig): @@ -33,7 +35,7 @@ class DjangoGPGApp(MayanAppConfig): ModelPermission.register( model=Key, permissions=( permission_acl_edit, permission_acl_view, - permission_key_delete, permission_key_view + permission_key_delete, permission_key_sign, permission_key_view ) ) diff --git a/mayan/apps/django_gpg/permissions.py b/mayan/apps/django_gpg/permissions.py index fc984d3567..5603010405 100644 --- a/mayan/apps/django_gpg/permissions.py +++ b/mayan/apps/django_gpg/permissions.py @@ -12,6 +12,9 @@ permission_key_delete = namespace.add_permission( permission_key_receive = namespace.add_permission( name='key_receive', label=_('Import keys from keyservers') ) +permission_key_sign = namespace.add_permission( + name='key_sign', label=_('Use keys to sign content') +) permission_key_view = namespace.add_permission( name='key_view', label=_('View keys') ) From bc59613945c749bf07258323481335f7deb8c8a5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Mar 2016 02:32:08 -0400 Subject: [PATCH 41/57] Add key content sign tests. --- mayan/apps/django_gpg/models.py | 2 +- mayan/apps/django_gpg/tests/test_models.py | 51 ++++++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py index 9f4ab45239..68c3e7028b 100644 --- a/mayan/apps/django_gpg/models.py +++ b/mayan/apps/django_gpg/models.py @@ -128,7 +128,7 @@ class Key(models.Model): gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value ) - import_results = gpg.import_keys(key_data=self.data) + import_results = gpg.import_keys(key_data=self.key_data) file_sign_results = gpg.sign_file( file=file_object, keyid=import_results.fingerprints[0], diff --git a/mayan/apps/django_gpg/tests/test_models.py b/mayan/apps/django_gpg/tests/test_models.py index e871ecf1b3..d95a4d78e4 100644 --- a/mayan/apps/django_gpg/tests/test_models.py +++ b/mayan/apps/django_gpg/tests/test_models.py @@ -1,16 +1,20 @@ from __future__ import unicode_literals +import StringIO import tempfile from django.test import TestCase -from ..exceptions import DecryptionError, KeyDoesNotExist, VerificationError +from ..exceptions import ( + DecryptionError, KeyDoesNotExist, NeedPassphrase, PassphraseError, + VerificationError +) from ..models import Key from .literals import ( TEST_DETACHED_SIGNATURE, TEST_FILE, TEST_KEY_DATA, TEST_KEY_FINGERPRINT, - TEST_SEARCH_FINGERPRINT, TEST_SEARCH_UID, TEST_SIGNED_FILE, - TEST_SIGNED_FILE_CONTENT + TEST_KEY_PASSPHRASE, TEST_SEARCH_FINGERPRINT, TEST_SEARCH_UID, + TEST_SIGNED_FILE, TEST_SIGNED_FILE_CONTENT ) @@ -118,3 +122,44 @@ class KeyTestCase(TestCase): self.assertTrue(result) self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) + + def test_detached_signing_no_passphrase(self): + key = Key.objects.create(key_data=TEST_KEY_DATA) + + with self.assertRaises(NeedPassphrase): + with open(TEST_FILE) as test_file: + detached_signature = key.sign_file( + file_object=test_file, detached=True, + ) + + def test_detached_signing_bad_passphrase(self): + key = Key.objects.create(key_data=TEST_KEY_DATA) + + with self.assertRaises(PassphraseError): + with open(TEST_FILE) as test_file: + detached_signature = key.sign_file( + file_object=test_file, detached=True, + passphrase='bad passphrase' + ) + + def test_detached_signing_with_passphrase(self): + key = Key.objects.create(key_data=TEST_KEY_DATA) + + with open(TEST_FILE) as test_file: + detached_signature = key.sign_file( + file_object=test_file, detached=True, + passphrase=TEST_KEY_PASSPHRASE + ) + + signature_file = StringIO.StringIO() + signature_file.write(detached_signature) + signature_file.seek(0) + + with open(TEST_FILE) as test_file: + result = Key.objects.verify_file( + file_object=test_file, signature_file=signature_file + ) + + signature_file.close() + self.assertTrue(result) + self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) From 09b71144b6f47d2df39249ea29b18113f02f1b87 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Mar 2016 03:47:58 -0400 Subject: [PATCH 42/57] Add support for signing documents from the UI. Mayan EDMS is now in the major leagues :) --- HISTORY.rst | 1 + mayan/apps/django_gpg/models.py | 2 +- mayan/apps/django_gpg/views.py | 8 +- mayan/apps/document_signatures/apps.py | 11 +- mayan/apps/document_signatures/forms.py | 40 +++++- mayan/apps/document_signatures/links.py | 8 +- mayan/apps/document_signatures/models.py | 3 +- mayan/apps/document_signatures/permissions.py | 4 + .../document_signatures/tests/test_views.py | 3 - mayan/apps/document_signatures/urls.py | 14 ++- mayan/apps/document_signatures/views.py | 119 +++++++++++++++++- 11 files changed, 190 insertions(+), 23 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index b5a0e194f6..c03c33009a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -28,6 +28,7 @@ - Handle unicode filenames in staging folders. - Add staging file deletion permission. - New document_signature_view permission. +- Add support for signing documents. 2.0.2 (2016-02-09) ================== diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py index 68c3e7028b..7bb4d100de 100644 --- a/mayan/apps/django_gpg/models.py +++ b/mayan/apps/django_gpg/models.py @@ -117,7 +117,7 @@ class Key(models.Model): super(Key, self).save(*args, **kwargs) def __str__(self): - return self.key_id + return '{} - {}'.format(self.key_id, self.user_id) def sign_file(self, file_object, passphrase=None, clearsign=True, detached=False, binary=False, output=None): temporary_directory = tempfile.mkdtemp() diff --git a/mayan/apps/django_gpg/views.py b/mayan/apps/django_gpg/views.py index d7fa69928a..bc0c602265 100644 --- a/mayan/apps/django_gpg/views.py +++ b/mayan/apps/django_gpg/views.py @@ -33,13 +33,7 @@ class KeyDeleteView(SingleObjectDeleteView): return reverse_lazy('django_gpg:key_private_list') def get_extra_context(self): - return { - 'title': _('Delete key'), - 'message': _( - 'Delete key %s? If you delete a public key that is part of a ' - 'public/private pair the private key will be deleted as well.' - ) % self.get_object(), - } + return {'title': _('Delete key: %s') % self.get_object()} class KeyDetailView(SingleObjectDetailView): diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index 54f5e9509e..516838cc33 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -24,12 +24,14 @@ from .links import ( link_all_document_version_signature_verify, link_document_signature_list, link_document_version_signature_delete, + link_document_version_signature_detached_create, link_document_version_signature_details, link_document_version_signature_download, link_document_version_signature_list, link_document_version_signature_upload, ) from .permissions import ( + permission_document_version_sign_detached, permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_upload, @@ -74,6 +76,7 @@ class DocumentSignaturesApp(MayanAppConfig): ModelPermission.register( model=Document, permissions=( + permission_document_version_sign_detached, permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_view, @@ -93,7 +96,7 @@ class DocumentSignaturesApp(MayanAppConfig): func=lambda context: context['object'].signature_id or _('None') ) SourceColumn( - source=SignatureBaseModel, label=_('Is embedded?'), + source=SignatureBaseModel, label=_('Type'), func=lambda context: SignatureBaseModel.objects.get_subclass( pk=context['object'].pk ).get_signature_type_display() @@ -126,8 +129,10 @@ class DocumentSignaturesApp(MayanAppConfig): links=(link_document_signature_list,), sources=(Document,) ) menu_object.bind_links( - links=(link_document_version_signature_list,), - sources=(DocumentVersion,) + links=( + link_document_version_signature_list, + link_document_version_signature_detached_create, + ), sources=(DocumentVersion,) ) menu_object.bind_links( links=( diff --git a/mayan/apps/document_signatures/forms.py b/mayan/apps/document_signatures/forms.py index 09139f78cd..0ee13c12c2 100644 --- a/mayan/apps/document_signatures/forms.py +++ b/mayan/apps/document_signatures/forms.py @@ -1,13 +1,51 @@ -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals + +import logging from django import forms +from django.core.exceptions import PermissionDenied from django.utils.translation import ugettext_lazy as _ +from acls.models import AccessControlList +from permissions import Permission + from common.forms import DetailForm from django_gpg.models import Key +from django_gpg.permissions import permission_key_sign from .models import SignatureBaseModel +logger = logging.getLogger(__name__) + + +class DocumentVersionDetachedSignatureCreateForm(forms.Form): + key = forms.ModelChoiceField( + label=_('Key'), queryset=Key.objects.none() + ) + + passphrase = forms.CharField( + label=_('Passphrase'), required=False, + widget=forms.widgets.PasswordInput + ) + + def __init__(self, *args, **kwargs): + user = kwargs.pop('user', None) + logger.debug('user: %s', user) + super( + DocumentVersionDetachedSignatureCreateForm, self + ).__init__(*args, **kwargs) + + queryset = Key.objects.private_keys() + + try: + Permission.check_permissions(user, (permission_key_sign,)) + except PermissionDenied: + queryset = AccessControlList.objects.filter_by_access( + permission_key_sign, user, queryset + ) + + self.fields['key'].queryset = queryset + class DocumentVersionSignatureDetailForm(DetailForm): def __init__(self, *args, **kwargs): diff --git a/mayan/apps/document_signatures/links.py b/mayan/apps/document_signatures/links.py index 6494764291..5c5d3acc28 100644 --- a/mayan/apps/document_signatures/links.py +++ b/mayan/apps/document_signatures/links.py @@ -64,6 +64,12 @@ link_document_version_signature_download = Link( link_document_version_signature_upload = Link( args='resolved_object.pk', permissions=(permission_document_version_signature_upload,), - text=_('Upload signature'), + permissions_related='document', text=_('Upload signature'), view='signatures:document_version_signature_upload', ) +link_document_version_signature_detached_create = Link( + args='resolved_object.pk', + permissions=(permission_document_version_signature_upload,), + permissions_related='document', text=_('Sign detached'), + view='signatures:document_version_signature_detached_create', +) diff --git a/mayan/apps/document_signatures/models.py b/mayan/apps/document_signatures/models.py index 2a7b3bff95..53dcff0de7 100644 --- a/mayan/apps/document_signatures/models.py +++ b/mayan/apps/document_signatures/models.py @@ -130,7 +130,8 @@ class DetachedSignature(SignatureBaseModel): return '{}-{}'.format(self.document_version, _('signature')) def delete(self, *args, **kwargs): - self.signature_file.storage.delete(self.signature_file.name) + if self.signature_file.name: + self.signature_file.storage.delete(name=self.signature_file.name) super(DetachedSignature, self).delete(*args, **kwargs) def save(self, *args, **kwargs): diff --git a/mayan/apps/document_signatures/permissions.py b/mayan/apps/document_signatures/permissions.py index 307758c374..15f5cacc7c 100644 --- a/mayan/apps/document_signatures/permissions.py +++ b/mayan/apps/document_signatures/permissions.py @@ -8,6 +8,10 @@ namespace = PermissionNamespace( 'document_signatures', _('Document signatures') ) +permission_document_version_sign_detached = namespace.add_permission( + name='document_version_sign_detached', + label=_('Sign documents with detached signatures') +) permission_document_version_signature_delete = namespace.add_permission( name='document_version_signature_delete', label=_('Delete detached signatures') diff --git a/mayan/apps/document_signatures/tests/test_views.py b/mayan/apps/document_signatures/tests/test_views.py index fdcaa38649..c903969b75 100644 --- a/mayan/apps/document_signatures/tests/test_views.py +++ b/mayan/apps/document_signatures/tests/test_views.py @@ -14,7 +14,6 @@ from user_management.tests import ( from ..models import DetachedSignature, EmbeddedSignature from ..permissions import ( - permission_document_version_signature_view, permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_upload, @@ -288,8 +287,6 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): for document in self.document_type.documents.all(): document.delete(to_trash=False) - from documents.models import DocumentType - old_hooks = DocumentVersion._post_save_hooks DocumentVersion._post_save_hooks = {} for count in range(TEST_UNSIGNED_DOCUMENT_COUNT): diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index 988b5a9226..3d6a584e82 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -3,9 +3,10 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url from .views import ( - AllDocumentSignatureVerifyView, DocumentVersionSignatureDeleteView, - DocumentVersionSignatureDetailView, DocumentVersionSignatureDownloadView, - DocumentVersionSignatureListView, DocumentVersionSignatureUploadView + AllDocumentSignatureVerifyView, DocumentVersionDetachedSignatureCreateView, + DocumentVersionSignatureDeleteView, DocumentVersionSignatureDetailView, + DocumentVersionSignatureDownloadView, DocumentVersionSignatureListView, + DocumentVersionSignatureUploadView ) urlpatterns = patterns( @@ -26,10 +27,15 @@ urlpatterns = patterns( name='document_version_signature_list' ), url( - r'^documents/version/(?P\d+)/signature/upload/$', + r'^documents/version/(?P\d+)/signature/detached/upload/$', DocumentVersionSignatureUploadView.as_view(), name='document_version_signature_upload' ), + url( + r'^documents/version/(?P\d+)/signature/detached/create/$', + DocumentVersionDetachedSignatureCreateView.as_view(), + name='document_version_signature_detached_create' + ), url( r'^signature/(?P\d+)/delete/$', DocumentVersionSignatureDeleteView.as_view(), diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index 0dfd71463d..919fa5dbd1 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -1,24 +1,33 @@ from __future__ import absolute_import, unicode_literals +import tempfile import logging from django.contrib import messages from django.core.exceptions import PermissionDenied +from django.core.files import File from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from acls.models import AccessControlList from common.generics import ( - ConfirmView, SingleObjectCreateView, SingleObjectDeleteView, + ConfirmView, FormView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView, SingleObjectDownloadView, SingleObjectListView ) +from django_gpg.exceptions import NeedPassphrase, PassphraseError +from django_gpg.permissions import permission_key_sign from documents.models import DocumentVersion from permissions import Permission -from .forms import DocumentVersionSignatureDetailForm +from .forms import ( + DocumentVersionDetachedSignatureCreateForm, + DocumentVersionSignatureDetailForm +) from .models import DetachedSignature, SignatureBaseModel from .permissions import ( + permission_document_version_sign_detached, permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_upload, @@ -30,6 +39,112 @@ from .tasks import task_verify_missing_embedded_signature logger = logging.getLogger(__name__) +class DocumentVersionDetachedSignatureCreateView(FormView): + form_class = DocumentVersionDetachedSignatureCreateForm + + def form_valid(self, form): + key = form.cleaned_data['key'] + passphrase = form.cleaned_data['passphrase'] or None + + try: + Permission.check_permissions( + self.request.user, (permission_key_sign,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_key_sign, self.request.user, key + ) + + try: + with self.get_document_version().open() as file_object: + detached_signature = key.sign_file( + file_object=file_object, detached=True, + passphrase=passphrase + ) + except NeedPassphrase: + messages.error( + self.request, _('Passphrase is needed to unlock this key.') + ) + return HttpResponseRedirect( + reverse( + 'signatures:document_version_signature_detached_create', + args=(self.get_document_version().pk,) + ) + ) + except PassphraseError: + messages.error( + self.request, _('Passphrase is incorrect.') + ) + return HttpResponseRedirect( + reverse( + 'signatures:document_version_signature_detached_create', + args=(self.get_document_version().pk,) + ) + ) + else: + temporary_file_object = tempfile.TemporaryFile() + temporary_file_object.write(detached_signature.data) + temporary_file_object.seek(0) + + DetachedSignature.objects.create( + document_version=self.get_document_version(), + signature_file=File(temporary_file_object) + ) + + temporary_file_object.close() + + messages.success( + self.request, _('Document version signed successfully.') + ) + + return super( + DocumentVersionDetachedSignatureCreateView, self + ).form_valid(form) + + def dispatch(self, request, *args, **kwargs): + try: + Permission.check_permissions( + request.user, (permission_document_version_sign_detached,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_document_version_sign_detached, request.user, + self.get_document_version().document + ) + + return super( + DocumentVersionDetachedSignatureCreateView, self + ).dispatch(request, *args, **kwargs) + + def get_document_version(self): + return get_object_or_404(DocumentVersion, pk=self.kwargs['pk']) + + def get_extra_context(self): + return { + 'document': self.get_document_version().document, + 'document_version': self.get_document_version(), + 'navigation_object_list': ('document', 'document_version'), + 'title': _( + 'Sign document version "%s" with a detached signature?' + ) % self.get_document_version(), + } + + def get_form_kwargs(self): + result = super( + DocumentVersionDetachedSignatureCreateView, self + ).get_form_kwargs() + + result.update({'user': self.request.user}) + + return result + + def get_post_action_redirect(self): + return reverse( + 'signatures:document_version_signature_list', + args=(self.get_document_version().pk,) + ) + + class DocumentVersionSignatureDeleteView(SingleObjectDeleteView): model = DetachedSignature object_permission = permission_document_version_signature_delete From 94b00c7ce54c819e2a8971d5a05f11c78f385d21 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Mar 2016 17:19:27 -0400 Subject: [PATCH 43/57] Replace document type selection widget with an opened select list HTML control. --- HISTORY.rst | 1 + mayan/apps/documents/forms.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c03c33009a..e71d547645 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -29,6 +29,7 @@ - Add staging file deletion permission. - New document_signature_view permission. - Add support for signing documents. +- Replace document type selection widget with an opened selection list. 2.0.2 (2016-02-09) ================== diff --git a/mayan/apps/documents/forms.py b/mayan/apps/documents/forms.py index d1c6126f94..90636b4425 100644 --- a/mayan/apps/documents/forms.py +++ b/mayan/apps/documents/forms.py @@ -169,8 +169,8 @@ class DocumentTypeSelectForm(forms.Form): ) self.fields['document_type'] = forms.ModelChoiceField( - queryset=queryset, - label=_('Document type') + empty_label=None, label=_('Document type'), queryset=queryset, + required=True, widget=forms.widgets.Select(attrs={'size': 10}) ) From e8c0951b0d0fb2dcc85a885a44f0a3a943cb12a0 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Mar 2016 18:29:39 -0400 Subject: [PATCH 44/57] Add key download view, permission, link and test. --- mayan/apps/django_gpg/apps.py | 13 +++--- mayan/apps/django_gpg/links.py | 8 +++- mayan/apps/django_gpg/permissions.py | 3 ++ mayan/apps/django_gpg/tests/test_views.py | 50 +++++++++++++++++++++++ mayan/apps/django_gpg/urls.py | 10 +++-- mayan/apps/django_gpg/views.py | 17 ++++++-- 6 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 mayan/apps/django_gpg/tests/test_views.py diff --git a/mayan/apps/django_gpg/apps.py b/mayan/apps/django_gpg/apps.py index 7490aa6d21..791df1c8be 100644 --- a/mayan/apps/django_gpg/apps.py +++ b/mayan/apps/django_gpg/apps.py @@ -13,11 +13,12 @@ from navigation import SourceColumn from .classes import KeyStub from .links import ( - link_key_delete, link_key_detail, link_key_query, link_key_receive, - link_key_setup, link_private_keys, link_public_keys + link_key_delete, link_key_detail, link_key_download, link_key_query, + link_key_receive, link_key_setup, link_private_keys, link_public_keys ) from .permissions import ( - permission_key_delete, permission_key_sign, permission_key_view + permission_key_delete, permission_key_download, permission_key_sign, + permission_key_view ) @@ -35,7 +36,8 @@ class DjangoGPGApp(MayanAppConfig): ModelPermission.register( model=Key, permissions=( permission_acl_edit, permission_acl_view, - permission_key_delete, permission_key_sign, permission_key_view + permission_key_delete, permission_key_download, + permission_key_sign, permission_key_view ) ) @@ -89,7 +91,8 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. menu_object.bind_links(links=(link_key_receive,), sources=(KeyStub,)) menu_object.bind_links( - links=(link_acl_list, link_key_delete,), sources=(Key,) + links=(link_acl_list, link_key_delete, link_key_download,), + sources=(Key,) ) menu_setup.bind_links(links=(link_key_setup,)) menu_facet.bind_links( diff --git a/mayan/apps/django_gpg/links.py b/mayan/apps/django_gpg/links.py index 3a6cda3ee0..22771fe351 100644 --- a/mayan/apps/django_gpg/links.py +++ b/mayan/apps/django_gpg/links.py @@ -5,8 +5,8 @@ from django.utils.translation import ugettext_lazy as _ from navigation import Link from .permissions import ( - permission_key_delete, permission_key_receive, permission_key_view, - permission_keyserver_query + permission_key_delete, permission_key_download, permission_key_receive, + permission_key_view, permission_keyserver_query ) link_private_keys = Link( @@ -25,6 +25,10 @@ link_key_detail = Link( permissions=(permission_key_view,), text=_('Details'), view='django_gpg:key_detail', args=('resolved_object.pk',) ) +link_key_download = Link( + permissions=(permission_key_download,), text=_('Download'), + view='django_gpg:key_download', args=('resolved_object.pk',) +) link_key_query = Link( permissions=(permission_keyserver_query,), text=_('Query keyservers'), view='django_gpg:key_query' diff --git a/mayan/apps/django_gpg/permissions.py b/mayan/apps/django_gpg/permissions.py index 5603010405..2b224a5348 100644 --- a/mayan/apps/django_gpg/permissions.py +++ b/mayan/apps/django_gpg/permissions.py @@ -9,6 +9,9 @@ namespace = PermissionNamespace('django_gpg', _('Key management')) permission_key_delete = namespace.add_permission( name='key_delete', label=_('Delete keys') ) +permission_key_download = namespace.add_permission( + name='key_download', label=_('Download keys') +) permission_key_receive = namespace.add_permission( name='key_receive', label=_('Import keys from keyservers') ) diff --git a/mayan/apps/django_gpg/tests/test_views.py b/mayan/apps/django_gpg/tests/test_views.py new file mode 100644 index 0000000000..ac281526ca --- /dev/null +++ b/mayan/apps/django_gpg/tests/test_views.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import, unicode_literals + +from django.core.files import File + +from django_downloadview.test import assert_download_response + +from documents.models import Document, DocumentVersion +from documents.tests.literals import TEST_DOCUMENT_PATH +from documents.tests.test_views import GenericDocumentViewTestCase +from user_management.tests import ( + TEST_USER_USERNAME, TEST_USER_PASSWORD +) + +from ..models import Key +from ..permissions import permission_key_download + +from .literals import ( + TEST_DETACHED_SIGNATURE, TEST_FILE, TEST_KEY_DATA, TEST_KEY_FINGERPRINT, + TEST_KEY_PASSPHRASE, TEST_SEARCH_FINGERPRINT, TEST_SEARCH_UID, + TEST_SIGNED_FILE, TEST_SIGNED_FILE_CONTENT +) + + +class KeyViewTestCase(GenericDocumentViewTestCase): + def test_key_download_view_no_permission(self): + key = Key.objects.create(key_data=TEST_KEY_DATA) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + response = self.get( + viewname='django_gpg:key_download', args=(key.pk,) + ) + + self.assertEqual(response.status_code, 403) + + def test_key_download_view_with_permission(self): + key = Key.objects.create(key_data=TEST_KEY_DATA) + + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add(permission_key_download.stored_permission) + + response = self.get( + viewname='django_gpg:key_download', args=(key.pk,) + ) + + assert_download_response( + self, response=response, content=key.key_data, + basename=key.key_id, + ) diff --git a/mayan/apps/django_gpg/urls.py b/mayan/apps/django_gpg/urls.py index 47126ee413..e9530136fb 100644 --- a/mayan/apps/django_gpg/urls.py +++ b/mayan/apps/django_gpg/urls.py @@ -3,8 +3,8 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url from .views import ( - KeyDeleteView, KeyDetailView, KeyQueryView, KeyQueryResultView, KeyReceive, - PrivateKeyListView, PublicKeyListView + KeyDeleteView, KeyDetailView, KeyDownloadView, KeyQueryView, + KeyQueryResultView, KeyReceive, PrivateKeyListView, PublicKeyListView ) urlpatterns = patterns( @@ -13,7 +13,11 @@ urlpatterns = patterns( r'^(?P\d+)/$', KeyDetailView.as_view(), name='key_detail' ), url( - r'^delete/(?P\d+)/$', KeyDeleteView.as_view(), name='key_delete' + r'^(?P\d+)/delete/$', KeyDeleteView.as_view(), name='key_delete' + ), + url( + r'^(?P\d+)/download/$', KeyDownloadView.as_view(), + name='key_download' ), url( r'^list/private/$', PrivateKeyListView.as_view(), diff --git a/mayan/apps/django_gpg/views.py b/mayan/apps/django_gpg/views.py index bc0c602265..8a40f822d0 100644 --- a/mayan/apps/django_gpg/views.py +++ b/mayan/apps/django_gpg/views.py @@ -3,20 +3,21 @@ from __future__ import absolute_import, unicode_literals import logging from django.contrib import messages +from django.core.files.base import ContentFile from django.core.urlresolvers import reverse, reverse_lazy from django.utils.translation import ugettext_lazy as _ from common.generics import ( ConfirmView, SimpleView, SingleObjectDeleteView, SingleObjectDetailView, - SingleObjectListView + SingleObjectDownloadView, SingleObjectListView ) from .forms import KeyDetailForm, KeySearchForm from .literals import KEY_TYPE_PUBLIC from .models import Key from .permissions import ( - permission_key_delete, permission_key_receive, permission_key_view, - permission_keyserver_query + permission_key_delete, permission_key_download, permission_key_receive, + permission_key_view, permission_keyserver_query ) logger = logging.getLogger(__name__) @@ -47,6 +48,16 @@ class KeyDetailView(SingleObjectDetailView): } +class KeyDownloadView(SingleObjectDownloadView): + model = Key + object_permission = permission_key_download + + def get_file(self): + key = self.get_object() + + return ContentFile(key.key_data, name=key.key_id) + + class KeyReceive(ConfirmView): post_action_redirect = reverse_lazy('django_gpg:key_public_list') view_permission = permission_key_receive From 14988bab344e738a80952a942dc80705ce486ca2 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Mar 2016 19:05:53 -0400 Subject: [PATCH 45/57] Add key upload view, permission, link and test. --- mayan/apps/django_gpg/apps.py | 5 ++- mayan/apps/django_gpg/links.py | 22 ++++++---- .../django_gpg/migrations/0001_initial.py | 43 ++++++++++++++++--- .../migrations/0003_auto_20160322_1810.py | 31 ++++++++++--- .../migrations/0004_auto_20160322_2202.py | 5 ++- mayan/apps/django_gpg/models.py | 10 ++++- mayan/apps/django_gpg/permissions.py | 3 ++ mayan/apps/django_gpg/tests/test_models.py | 4 +- mayan/apps/django_gpg/tests/test_views.py | 41 ++++++++++++------ mayan/apps/django_gpg/urls.py | 6 ++- mayan/apps/django_gpg/views.py | 19 ++++++-- 11 files changed, 143 insertions(+), 46 deletions(-) diff --git a/mayan/apps/django_gpg/apps.py b/mayan/apps/django_gpg/apps.py index 791df1c8be..b3abb60d18 100644 --- a/mayan/apps/django_gpg/apps.py +++ b/mayan/apps/django_gpg/apps.py @@ -14,7 +14,8 @@ from navigation import SourceColumn from .classes import KeyStub from .links import ( link_key_delete, link_key_detail, link_key_download, link_key_query, - link_key_receive, link_key_setup, link_private_keys, link_public_keys + link_key_receive, link_key_setup, link_key_upload, link_private_keys, + link_public_keys ) from .permissions import ( permission_key_delete, permission_key_download, permission_key_sign, @@ -104,7 +105,7 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ) ) menu_sidebar.bind_links( - links=(link_key_query,), + links=(link_key_query, link_key_upload), sources=( 'django_gpg:key_public_list', 'django_gpg:key_private_list', 'django_gpg:key_query', 'django_gpg:key_query_results', Key, diff --git a/mayan/apps/django_gpg/links.py b/mayan/apps/django_gpg/links.py index 22771fe351..70ea4ec69b 100644 --- a/mayan/apps/django_gpg/links.py +++ b/mayan/apps/django_gpg/links.py @@ -6,17 +6,9 @@ from navigation import Link from .permissions import ( permission_key_delete, permission_key_download, permission_key_receive, - permission_key_view, permission_keyserver_query + permission_key_view, permission_key_upload, permission_keyserver_query ) -link_private_keys = Link( - permissions=(permission_key_view,), text=_('Private keys'), - view='django_gpg:key_private_list' -) -link_public_keys = Link( - permissions=(permission_key_view,), text=_('Public keys'), - view='django_gpg:key_public_list' -) link_key_delete = Link( permissions=(permission_key_delete,), tags='dangerous', text=_('Delete'), view='django_gpg:key_delete', args=('resolved_object.pk',) @@ -41,3 +33,15 @@ link_key_setup = Link( icon='fa fa-key', permissions=(permission_key_view,), text=_('Key management'), view='django_gpg:key_public_list' ) +link_key_upload = Link( + permissions=(permission_key_upload,), text=_('Upload key'), + view='django_gpg:key_upload' +) +link_private_keys = Link( + permissions=(permission_key_view,), text=_('Private keys'), + view='django_gpg:key_private_list' +) +link_public_keys = Link( + permissions=(permission_key_view,), text=_('Public keys'), + view='django_gpg:key_public_list' +) diff --git a/mayan/apps/django_gpg/migrations/0001_initial.py b/mayan/apps/django_gpg/migrations/0001_initial.py index df1505e689..68e0aa0e31 100644 --- a/mayan/apps/django_gpg/migrations/0001_initial.py +++ b/mayan/apps/django_gpg/migrations/0001_initial.py @@ -13,16 +13,45 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Key', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', models.AutoField( + verbose_name='ID', serialize=False, auto_created=True, + primary_key=True + ) + ), ('data', models.TextField(verbose_name='Data')), - ('key_id', models.CharField(unique=True, max_length=16, verbose_name='Key ID')), - ('creation_date', models.DateField(verbose_name='Creation date')), - ('expiration_date', models.DateField(null=True, verbose_name='Expiration date', blank=True)), - ('fingerprint', models.CharField(unique=True, max_length=40, verbose_name='Fingerprint')), + ( + 'key_id', models.CharField( + unique=True, max_length=16, verbose_name='Key ID' + ) + ), + ( + 'creation_date', models.DateField( + verbose_name='Creation date' + ) + ), + ( + 'expiration_date', models.DateField( + null=True, verbose_name='Expiration date', blank=True + ) + ), + ( + 'fingerprint', models.CharField( + unique=True, max_length=40, verbose_name='Fingerprint' + ) + ), ('length', models.PositiveIntegerField(verbose_name='Length')), - ('algorithm', models.PositiveIntegerField(verbose_name='Algorithm')), + ( + 'algorithm', models.PositiveIntegerField( + verbose_name='Algorithm' + ) + ), ('user_id', models.TextField(verbose_name='User ID')), - ('key_type', models.CharField(max_length=3, verbose_name='Type')), + ( + 'key_type', models.CharField( + max_length=3, verbose_name='Type' + ) + ), ], options={ 'verbose_name': 'Key', diff --git a/mayan/apps/django_gpg/migrations/0003_auto_20160322_1810.py b/mayan/apps/django_gpg/migrations/0003_auto_20160322_1810.py index 40689f948e..c7a62e5450 100644 --- a/mayan/apps/django_gpg/migrations/0003_auto_20160322_1810.py +++ b/mayan/apps/django_gpg/migrations/0003_auto_20160322_1810.py @@ -14,22 +14,32 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='key', name='algorithm', - field=models.PositiveIntegerField(verbose_name='Algorithm', editable=False), + field=models.PositiveIntegerField( + verbose_name='Algorithm', editable=False + ), ), migrations.AlterField( model_name='key', name='creation_date', - field=models.DateField(verbose_name='Creation date', editable=False), + field=models.DateField( + verbose_name='Creation date', editable=False + ), ), migrations.AlterField( model_name='key', name='expiration_date', - field=models.DateField(verbose_name='Expiration date', null=True, editable=False, blank=True), + field=models.DateField( + verbose_name='Expiration date', null=True, editable=False, + blank=True + ), ), migrations.AlterField( model_name='key', name='fingerprint', - field=models.CharField(verbose_name='Fingerprint', unique=True, max_length=40, editable=False), + field=models.CharField( + verbose_name='Fingerprint', unique=True, max_length=40, + editable=False + ), ), migrations.AlterField( model_name='key', @@ -39,17 +49,24 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='key', name='key_id', - field=models.CharField(verbose_name='Key ID', unique=True, max_length=16, editable=False), + field=models.CharField( + verbose_name='Key ID', unique=True, max_length=16, + editable=False + ), ), migrations.AlterField( model_name='key', name='key_type', - field=models.CharField(verbose_name='Type', max_length=3, editable=False), + field=models.CharField( + verbose_name='Type', max_length=3, editable=False + ), ), migrations.AlterField( model_name='key', name='length', - field=models.PositiveIntegerField(verbose_name='Length', editable=False), + field=models.PositiveIntegerField( + verbose_name='Length', editable=False + ), ), migrations.AlterField( model_name='key', diff --git a/mayan/apps/django_gpg/migrations/0004_auto_20160322_2202.py b/mayan/apps/django_gpg/migrations/0004_auto_20160322_2202.py index a2bea9e955..d4d24ecc20 100644 --- a/mayan/apps/django_gpg/migrations/0004_auto_20160322_2202.py +++ b/mayan/apps/django_gpg/migrations/0004_auto_20160322_2202.py @@ -19,6 +19,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='key', name='key_type', - field=models.CharField(verbose_name='Type', max_length=3, editable=False, choices=[('pub', 'Public'), ('sec', 'Secret')]), + field=models.CharField( + verbose_name='Type', max_length=3, editable=False, + choices=[('pub', 'Public'), ('sec', 'Secret')] + ), ), ] diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py index 7bb4d100de..de4f3e9528 100644 --- a/mayan/apps/django_gpg/models.py +++ b/mayan/apps/django_gpg/models.py @@ -43,7 +43,10 @@ def gpg_command(function): @python_2_unicode_compatible class Key(models.Model): - key_data = models.TextField(verbose_name=_('Key data')) + key_data = models.TextField( + help_text=_('ASCII armored version of the key.'), + verbose_name=_('Key data') + ) creation_date = models.DateField( editable=False, verbose_name=_('Creation date') ) @@ -80,7 +83,10 @@ class Key(models.Model): import_results = gpg_command(function=import_key) if not import_results.count: - raise ValidationError('Invalid key data') + raise ValidationError(_('Invalid key data')) + + if Key.objects.filter(fingerprint=import_results.fingerprints[0]).exists(): + raise ValidationError(_('Key already exists.')) def get_absolute_url(self): return reverse('django_gpg:key_detail', args=(self.pk,)) diff --git a/mayan/apps/django_gpg/permissions.py b/mayan/apps/django_gpg/permissions.py index 2b224a5348..5e36afadb9 100644 --- a/mayan/apps/django_gpg/permissions.py +++ b/mayan/apps/django_gpg/permissions.py @@ -18,6 +18,9 @@ permission_key_receive = namespace.add_permission( permission_key_sign = namespace.add_permission( name='key_sign', label=_('Use keys to sign content') ) +permission_key_upload = namespace.add_permission( + name='key_upload', label=_('Upload keys') +) permission_key_view = namespace.add_permission( name='key_view', label=_('View keys') ) diff --git a/mayan/apps/django_gpg/tests/test_models.py b/mayan/apps/django_gpg/tests/test_models.py index d95a4d78e4..afc59b692a 100644 --- a/mayan/apps/django_gpg/tests/test_models.py +++ b/mayan/apps/django_gpg/tests/test_models.py @@ -128,7 +128,7 @@ class KeyTestCase(TestCase): with self.assertRaises(NeedPassphrase): with open(TEST_FILE) as test_file: - detached_signature = key.sign_file( + key.sign_file( file_object=test_file, detached=True, ) @@ -137,7 +137,7 @@ class KeyTestCase(TestCase): with self.assertRaises(PassphraseError): with open(TEST_FILE) as test_file: - detached_signature = key.sign_file( + key.sign_file( file_object=test_file, detached=True, passphrase='bad passphrase' ) diff --git a/mayan/apps/django_gpg/tests/test_views.py b/mayan/apps/django_gpg/tests/test_views.py index ac281526ca..7f65c1329e 100644 --- a/mayan/apps/django_gpg/tests/test_views.py +++ b/mayan/apps/django_gpg/tests/test_views.py @@ -1,27 +1,19 @@ from __future__ import absolute_import, unicode_literals -from django.core.files import File - from django_downloadview.test import assert_download_response -from documents.models import Document, DocumentVersion -from documents.tests.literals import TEST_DOCUMENT_PATH -from documents.tests.test_views import GenericDocumentViewTestCase +from common.tests.test_views import GenericViewTestCase from user_management.tests import ( TEST_USER_USERNAME, TEST_USER_PASSWORD ) from ..models import Key -from ..permissions import permission_key_download +from ..permissions import permission_key_download, permission_key_upload -from .literals import ( - TEST_DETACHED_SIGNATURE, TEST_FILE, TEST_KEY_DATA, TEST_KEY_FINGERPRINT, - TEST_KEY_PASSPHRASE, TEST_SEARCH_FINGERPRINT, TEST_SEARCH_UID, - TEST_SIGNED_FILE, TEST_SIGNED_FILE_CONTENT -) +from .literals import TEST_KEY_DATA, TEST_KEY_FINGERPRINT -class KeyViewTestCase(GenericDocumentViewTestCase): +class KeyViewTestCase(GenericViewTestCase): def test_key_download_view_no_permission(self): key = Key.objects.create(key_data=TEST_KEY_DATA) @@ -48,3 +40,28 @@ class KeyViewTestCase(GenericDocumentViewTestCase): self, response=response, content=key.key_data, basename=key.key_id, ) + + def test_key_upload_view_no_permission(self): + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + response = self.post( + viewname='django_gpg:key_upload', data={'key_data': TEST_KEY_DATA} + ) + + self.assertEqual(response.status_code, 403) + self.assertEqual(Key.objects.count(), 0) + + def test_key_upload_view_with_permission(self): + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add(permission_key_upload.stored_permission) + + response = self.post( + viewname='django_gpg:key_upload', data={'key_data': TEST_KEY_DATA}, + follow=True + ) + + self.assertContains(response, 'created', status_code=200) + + self.assertEqual(Key.objects.count(), 1) + self.assertEqual(Key.objects.first().fingerprint, TEST_KEY_FINGERPRINT) diff --git a/mayan/apps/django_gpg/urls.py b/mayan/apps/django_gpg/urls.py index e9530136fb..929e613e71 100644 --- a/mayan/apps/django_gpg/urls.py +++ b/mayan/apps/django_gpg/urls.py @@ -4,7 +4,8 @@ from django.conf.urls import patterns, url from .views import ( KeyDeleteView, KeyDetailView, KeyDownloadView, KeyQueryView, - KeyQueryResultView, KeyReceive, PrivateKeyListView, PublicKeyListView + KeyQueryResultView, KeyReceive, KeyUploadView, PrivateKeyListView, + PublicKeyListView ) urlpatterns = patterns( @@ -26,6 +27,9 @@ urlpatterns = patterns( url( r'^list/public/$', PublicKeyListView.as_view(), name='key_public_list' ), + url( + r'^upload/$', KeyUploadView.as_view(), name='key_upload' + ), url(r'^query/$', KeyQueryView.as_view(), name='key_query'), url( r'^query/results/$', KeyQueryResultView.as_view(), diff --git a/mayan/apps/django_gpg/views.py b/mayan/apps/django_gpg/views.py index 8a40f822d0..4e6591640a 100644 --- a/mayan/apps/django_gpg/views.py +++ b/mayan/apps/django_gpg/views.py @@ -8,8 +8,9 @@ from django.core.urlresolvers import reverse, reverse_lazy from django.utils.translation import ugettext_lazy as _ from common.generics import ( - ConfirmView, SimpleView, SingleObjectDeleteView, SingleObjectDetailView, - SingleObjectDownloadView, SingleObjectListView + ConfirmView, SingleObjectCreateView, SingleObjectDeleteView, + SingleObjectDetailView, SingleObjectDownloadView, SingleObjectListView, + SimpleView ) from .forms import KeyDetailForm, KeySearchForm @@ -17,7 +18,7 @@ from .literals import KEY_TYPE_PUBLIC from .models import Key from .permissions import ( permission_key_delete, permission_key_download, permission_key_receive, - permission_key_view, permission_keyserver_query + permission_key_upload, permission_key_view, permission_keyserver_query ) logger = logging.getLogger(__name__) @@ -126,6 +127,18 @@ class KeyQueryResultView(SingleObjectListView): return () +class KeyUploadView(SingleObjectCreateView): + fields = ('key_data',) + model = Key + post_action_redirect = reverse_lazy('django_gpg:key_public_list') + view_permission = permission_key_upload + + def get_extra_context(self): + return { + 'title': _('Upload new key'), + } + + class PublicKeyListView(SingleObjectListView): object_permission = permission_key_view queryset = Key.objects.public_keys() From 3d74bdb5902cfa57e1897a99301dadb7bfcf2daf Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Mar 2016 19:06:31 -0400 Subject: [PATCH 46/57] Add document embedded signature signing support. --- mayan/apps/document_signatures/apps.py | 4 + mayan/apps/document_signatures/forms.py | 4 +- mayan/apps/document_signatures/links.py | 10 +- mayan/apps/document_signatures/managers.py | 23 ++++ mayan/apps/document_signatures/permissions.py | 4 + mayan/apps/document_signatures/urls.py | 6 + mayan/apps/document_signatures/views.py | 112 +++++++++++++++++- 7 files changed, 157 insertions(+), 6 deletions(-) diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index 516838cc33..eecfad9102 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -25,6 +25,7 @@ from .links import ( link_document_signature_list, link_document_version_signature_delete, link_document_version_signature_detached_create, + link_document_version_signature_embedded_create, link_document_version_signature_details, link_document_version_signature_download, link_document_version_signature_list, @@ -32,6 +33,7 @@ from .links import ( ) from .permissions import ( permission_document_version_sign_detached, + permission_document_version_sign_embedded, permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_upload, @@ -77,6 +79,7 @@ class DocumentSignaturesApp(MayanAppConfig): ModelPermission.register( model=Document, permissions=( permission_document_version_sign_detached, + permission_document_version_sign_embedded, permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_view, @@ -132,6 +135,7 @@ class DocumentSignaturesApp(MayanAppConfig): links=( link_document_version_signature_list, link_document_version_signature_detached_create, + link_document_version_signature_embedded_create ), sources=(DocumentVersion,) ) menu_object.bind_links( diff --git a/mayan/apps/document_signatures/forms.py b/mayan/apps/document_signatures/forms.py index 0ee13c12c2..15c8f46663 100644 --- a/mayan/apps/document_signatures/forms.py +++ b/mayan/apps/document_signatures/forms.py @@ -18,7 +18,7 @@ from .models import SignatureBaseModel logger = logging.getLogger(__name__) -class DocumentVersionDetachedSignatureCreateForm(forms.Form): +class DocumentVersionSignatureCreateForm(forms.Form): key = forms.ModelChoiceField( label=_('Key'), queryset=Key.objects.none() ) @@ -32,7 +32,7 @@ class DocumentVersionDetachedSignatureCreateForm(forms.Form): user = kwargs.pop('user', None) logger.debug('user: %s', user) super( - DocumentVersionDetachedSignatureCreateForm, self + DocumentVersionSignatureCreateForm, self ).__init__(*args, **kwargs) queryset = Key.objects.private_keys() diff --git a/mayan/apps/document_signatures/links.py b/mayan/apps/document_signatures/links.py index 5c5d3acc28..72ee5d4f5f 100644 --- a/mayan/apps/document_signatures/links.py +++ b/mayan/apps/document_signatures/links.py @@ -6,6 +6,8 @@ from django.utils.translation import ugettext_lazy as _ from navigation import Link from .permissions import ( + permission_document_version_sign_detached, + permission_document_version_sign_embedded, permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_upload, @@ -69,7 +71,13 @@ link_document_version_signature_upload = Link( ) link_document_version_signature_detached_create = Link( args='resolved_object.pk', - permissions=(permission_document_version_signature_upload,), + permissions=(permission_document_version_sign_detached,), permissions_related='document', text=_('Sign detached'), view='signatures:document_version_signature_detached_create', ) +link_document_version_signature_embedded_create = Link( + args='resolved_object.pk', + permissions=(permission_document_version_sign_embedded,), + permissions_related='document', text=_('Sign embedded'), + view='signatures:document_version_signature_embedded_create', +) diff --git a/mayan/apps/document_signatures/managers.py b/mayan/apps/document_signatures/managers.py index 9c3b90de19..7252d9211c 100644 --- a/mayan/apps/document_signatures/managers.py +++ b/mayan/apps/document_signatures/managers.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals import logging +import os +import tempfile from django.db import models @@ -30,3 +32,24 @@ class EmbeddedSignatureManager(models.Manager): return DocumentVersion.objects.exclude( pk__in=self.values('document_version') ) + + def sign_document_version(self, document_version, key, passphrase=None, user=None): + temporary_file_object, temporary_filename = tempfile.mkstemp() + + try: + with document_version.open() as file_object: + signature_result = key.sign_file( + binary=True, file_object=file_object, + output=temporary_filename, passphrase=passphrase + ) + except Exception: + raise + else: + with open(temporary_filename) as file_object: + new_version = document_version.document.new_version( + file_object=file_object, _user=user + ) + finally: + os.unlink(temporary_filename) + + return new_version diff --git a/mayan/apps/document_signatures/permissions.py b/mayan/apps/document_signatures/permissions.py index 15f5cacc7c..c9bdbe684c 100644 --- a/mayan/apps/document_signatures/permissions.py +++ b/mayan/apps/document_signatures/permissions.py @@ -12,6 +12,10 @@ permission_document_version_sign_detached = namespace.add_permission( name='document_version_sign_detached', label=_('Sign documents with detached signatures') ) +permission_document_version_sign_embedded = namespace.add_permission( + name='document_version_sign_embedded', + label=_('Sign documents with embedded signatures') +) permission_document_version_signature_delete = namespace.add_permission( name='document_version_signature_delete', label=_('Delete detached signatures') diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index 3d6a584e82..1c269c3d65 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -4,6 +4,7 @@ from django.conf.urls import patterns, url from .views import ( AllDocumentSignatureVerifyView, DocumentVersionDetachedSignatureCreateView, + DocumentVersionEmbeddedSignatureCreateView, DocumentVersionSignatureDeleteView, DocumentVersionSignatureDetailView, DocumentVersionSignatureDownloadView, DocumentVersionSignatureListView, DocumentVersionSignatureUploadView @@ -36,6 +37,11 @@ urlpatterns = patterns( DocumentVersionDetachedSignatureCreateView.as_view(), name='document_version_signature_detached_create' ), + url( + r'^documents/version/(?P\d+)/signature/embedded/create/$', + DocumentVersionEmbeddedSignatureCreateView.as_view(), + name='document_version_signature_embedded_create' + ), url( r'^signature/(?P\d+)/delete/$', DocumentVersionSignatureDeleteView.as_view(), diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index 919fa5dbd1..8e483062f6 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -22,12 +22,13 @@ from documents.models import DocumentVersion from permissions import Permission from .forms import ( - DocumentVersionDetachedSignatureCreateForm, + DocumentVersionSignatureCreateForm, DocumentVersionSignatureDetailForm ) from .models import DetachedSignature, SignatureBaseModel from .permissions import ( permission_document_version_sign_detached, + permission_document_version_sign_embedded, permission_document_version_signature_delete, permission_document_version_signature_download, permission_document_version_signature_upload, @@ -40,7 +41,7 @@ logger = logging.getLogger(__name__) class DocumentVersionDetachedSignatureCreateView(FormView): - form_class = DocumentVersionDetachedSignatureCreateForm + form_class = DocumentVersionSignatureCreateForm def form_valid(self, form): key = form.cleaned_data['key'] @@ -125,7 +126,7 @@ class DocumentVersionDetachedSignatureCreateView(FormView): 'document_version': self.get_document_version(), 'navigation_object_list': ('document', 'document_version'), 'title': _( - 'Sign document version "%s" with a detached signature?' + 'Sign document version "%s" with a detached signature' ) % self.get_document_version(), } @@ -145,6 +146,111 @@ class DocumentVersionDetachedSignatureCreateView(FormView): ) +class DocumentVersionEmbeddedSignatureCreateView(FormView): + form_class = DocumentVersionSignatureCreateForm + + 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: + signature_result = key.sign_file( + binary=True, file_object=file_object, passphrase=passphrase + ) + except NeedPassphrase: + messages.error( + self.request, _('Passphrase is needed to unlock this key.') + ) + return HttpResponseRedirect( + reverse( + 'signatures:document_version_signature_embedded_create', + args=(self.get_document_version().pk,) + ) + ) + except PassphraseError: + messages.error( + self.request, _('Passphrase is incorrect.') + ) + return HttpResponseRedirect( + reverse( + 'signatures:document_version_signature_embedded_create', + args=(self.get_document_version().pk,) + ) + ) + else: + temporary_file_object = tempfile.TemporaryFile() + temporary_file_object.write(signature_result.data) + temporary_file_object.seek(0) + + new_version = self.get_document_version().document.new_version( + file_object=temporary_file_object, _user=self.request.user + ) + + temporary_file_object.close() + + messages.success( + self.request, _('Document version signed successfully.') + ) + + return HttpResponseRedirect( + reverse( + 'signatures:document_version_signature_list', + args=(new_version.pk,) + ) + ) + + return super( + DocumentVersionEmbeddedSignatureCreateView, self + ).form_valid(form) + + def dispatch(self, request, *args, **kwargs): + try: + Permission.check_permissions( + request.user, (permission_document_version_sign_embedded,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_document_version_sign_embedded, request.user, + self.get_document_version().document + ) + + return super( + DocumentVersionEmbeddedSignatureCreateView, 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 embedded signature' + ) % self.get_document_version(), + } + + def get_form_kwargs(self): + result = super( + DocumentVersionEmbeddedSignatureCreateView, self + ).get_form_kwargs() + + result.update({'user': self.request.user}) + + return result + + class DocumentVersionSignatureDeleteView(SingleObjectDeleteView): model = DetachedSignature object_permission = permission_document_version_signature_delete From 07cd6d078ff8d097f63762e92600c45fb48316db Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 1 Apr 2016 12:33:11 -0400 Subject: [PATCH 47/57] No need to typecast the result --- mayan/apps/django_gpg/managers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py index 1a26a4e0cf..1542dfa940 100644 --- a/mayan/apps/django_gpg/managers.py +++ b/mayan/apps/django_gpg/managers.py @@ -68,7 +68,7 @@ class KeyManager(models.Manager): file_object.close() - return io.BytesIO(str(decrypt_result)) + return io.BytesIO(decrypt_result.data) def receive_key(self, key_id): temporary_directory = tempfile.mkdtemp() From 9a0dd8c192cc9bc3f545ca969dedd5dd34032ada Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 1 Apr 2016 12:43:06 -0400 Subject: [PATCH 48/57] Add embedded signing test. --- .../document_signatures/tests/test_models.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/mayan/apps/document_signatures/tests/test_models.py b/mayan/apps/document_signatures/tests/test_models.py index 8e9f61c299..30b57f8b02 100644 --- a/mayan/apps/document_signatures/tests/test_models.py +++ b/mayan/apps/document_signatures/tests/test_models.py @@ -1,11 +1,13 @@ from __future__ import unicode_literals +import hashlib import time from django.core.files import File from django.test import TestCase, override_settings from django_gpg.models import Key +from django_gpg.tests.literals import TEST_KEY_DATA, TEST_KEY_PASSPHRASE from documents.models import DocumentType, DocumentVersion from documents.tests import TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE @@ -308,3 +310,36 @@ class EmbeddedSignaturesTestCase(TestCase): EmbeddedSignature.objects.unsigned_document_versions().count(), TEST_UNSIGNED_DOCUMENT_COUNT ) + + def test_signing(self): + key = Key.objects.create(key_data=TEST_KEY_DATA) + + with open(TEST_DOCUMENT_PATH) as file_object: + document = self.document_type.new_document( + file_object=file_object + ) + + with document.latest_version.open() as file_object: + file_object.seek(0, 2) + original_size = file_object.tell() + file_object.seek(0) + original_hash = hashlib.sha256(file_object.read()).hexdigest() + + new_version = EmbeddedSignature.objects.sign_document_version( + document_version=document.latest_version, key=key, + passphrase=TEST_KEY_PASSPHRASE + ) + + self.assertEqual(EmbeddedSignature.objects.count(), 1) + + with new_version.open() as file_object: + document_content_hash = hashlib.sha256(file_object.read()).hexdigest() + + with new_version.open() as file_object: + file_object.seek(0, 2) + new_size = file_object.tell() + file_object.seek(0) + new_hash = hashlib.sha256(file_object.read()).hexdigest() + + self.assertEqual(original_size, new_size) + self.assertEqual(origianl_hash, new_hash) From c6fb008562ed94b2ba89d3d98a0fb27899f97fd4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 1 Apr 2016 23:28:34 -0400 Subject: [PATCH 49/57] Workaround problem with gpg embedded signatures corrupting the source file by using clearsign=False by default. --- mayan/apps/django_gpg/models.py | 9 ++++++++- mayan/apps/document_signatures/tests/test_models.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py index de4f3e9528..a4f1b814cb 100644 --- a/mayan/apps/django_gpg/models.py +++ b/mayan/apps/django_gpg/models.py @@ -125,7 +125,14 @@ class Key(models.Model): def __str__(self): return '{} - {}'.format(self.key_id, self.user_id) - def sign_file(self, file_object, passphrase=None, clearsign=True, detached=False, binary=False, output=None): + def sign_file(self, file_object, passphrase=None, clearsign=False, detached=False, binary=False, output=None): + # WARNING: using clearsign=True and subsequent decryption corrupts the + # file. Appears to be a problem in python-gnupg or gpg itself. + # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=55647 + # "The problems differ from run to run and file to + # file, and appear to be due to random data being inserted in the + # output data stream." + temporary_directory = tempfile.mkdtemp() os.chmod(temporary_directory, 0x1C0) diff --git a/mayan/apps/document_signatures/tests/test_models.py b/mayan/apps/document_signatures/tests/test_models.py index 30b57f8b02..2d57063591 100644 --- a/mayan/apps/document_signatures/tests/test_models.py +++ b/mayan/apps/document_signatures/tests/test_models.py @@ -342,4 +342,4 @@ class EmbeddedSignaturesTestCase(TestCase): new_hash = hashlib.sha256(file_object.read()).hexdigest() self.assertEqual(original_size, new_size) - self.assertEqual(origianl_hash, new_hash) + self.assertEqual(original_hash, new_hash) From 33aefdaef7276fed60702dfac675ce1ac15a896f Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 4 Apr 2016 13:49:14 -0400 Subject: [PATCH 50/57] Add document download and document preview event logging. Add corresponding tests. Closes GitLab issue #261. --- mayan/apps/documents/events.py | 8 ++ mayan/apps/documents/tests/test_events.py | 114 ++++++++++++++++++++++ mayan/apps/documents/views.py | 12 +++ 3 files changed, 134 insertions(+) create mode 100644 mayan/apps/documents/tests/test_events.py diff --git a/mayan/apps/documents/events.py b/mayan/apps/documents/events.py index c7856f6d65..21bd2f834b 100644 --- a/mayan/apps/documents/events.py +++ b/mayan/apps/documents/events.py @@ -7,6 +7,10 @@ from events.classes import Event event_document_create = Event( name='documents_document_create', label=_('Document created') ) +event_document_download = Event( + name='documents_document_download', + label=_('Document download') +) event_document_properties_edit = Event( name='documents_document_edit', label=_('Document properties edited') ) @@ -20,3 +24,7 @@ event_document_version_revert = Event( name='documents_document_version_revert', label=_('Document version reverted') ) +event_document_view = Event( + name='documents_document_view', + label=_('Document viewed') +) diff --git a/mayan/apps/documents/tests/test_events.py b/mayan/apps/documents/tests/test_events.py new file mode 100644 index 0000000000..80eb33f436 --- /dev/null +++ b/mayan/apps/documents/tests/test_events.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.contrib.contenttypes.models import ContentType +from django.test import override_settings +from django.utils.six import BytesIO + +from actstream.models import Action + +from common.tests.test_views import GenericViewTestCase +from converter.models import Transformation +from converter.permissions import permission_transformation_delete +from user_management.tests.literals import ( + TEST_USER_PASSWORD, TEST_USER_USERNAME +) + +from ..events import event_document_download, event_document_view +from ..literals import DEFAULT_DELETE_PERIOD, DEFAULT_DELETE_TIME_UNIT +from ..models import ( + DeletedDocument, Document, DocumentType, HASH_FUNCTION +) +from ..permissions import ( + permission_document_create, permission_document_delete, + permission_document_download, permission_document_properties_edit, + permission_document_restore, permission_document_tools, + permission_document_trash, permission_document_type_create, + permission_document_type_delete, permission_document_type_edit, + permission_document_type_view, permission_document_version_revert, + permission_document_view, permission_empty_trash +) + +from .literals import ( + TEST_DOCUMENT_TYPE, TEST_DOCUMENT_TYPE_QUICK_LABEL, + TEST_SMALL_DOCUMENT_CHECKSUM, TEST_SMALL_DOCUMENT_PATH +) +from .test_views import GenericDocumentViewTestCase + + +TEST_DOCUMENT_TYPE_EDITED_LABEL = 'test document type edited label' +TEST_DOCUMENT_TYPE_2_LABEL = 'test document type 2 label' +TEST_TRANSFORMATION_NAME = 'rotate' +TEST_TRANSFORMATION_ARGUMENT = 'degrees: 180' + + + +class DocumentEventsTestCase(GenericDocumentViewTestCase): + def test_document_download_event_no_permissions(self): + self.login( + username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD + ) + + Action.objects.all().delete() + + response = self.post( + 'documents:document_download', args=(self.document.pk,) + ) + + self.assertEqual(response.status_code, 302) + self.assertEqual(list(Action.objects.any(obj=self.document)), []) + + def test_document_download_event_with_permissions(self): + self.login( + username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD + ) + + Action.objects.all().delete() + + self.role.permissions.add( + permission_document_download.stored_permission + ) + response = self.post( + 'documents:document_download', args=(self.document.pk,), + ) + + event = Action.objects.any(obj=self.document).first() + + self.assertEqual(event.verb, event_document_download.name) + self.assertEqual(event.target, self.document) + self.assertEqual(event.actor, self.user) + + def test_document_view_event_no_permissions(self): + self.login( + username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD + ) + + Action.objects.all().delete() + + response = self.get( + 'documents:document_preview', args=(self.document.pk,) + ) + + self.assertEqual(response.status_code, 403) + self.assertEqual(list(Action.objects.any(obj=self.document)), []) + + def test_document_view_event_with_permissions(self): + self.login( + username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD + ) + + Action.objects.all().delete() + + self.role.permissions.add( + permission_document_view.stored_permission + ) + response = self.get( + 'documents:document_preview', args=(self.document.pk,), + ) + + event = Action.objects.any(obj=self.document).first() + + self.assertEqual(event.verb, event_document_view.name) + self.assertEqual(event.target, self.document) + self.assertEqual(event.actor, self.user) diff --git a/mayan/apps/documents/views.py b/mayan/apps/documents/views.py index b657c164ad..9cdcabf951 100644 --- a/mayan/apps/documents/views.py +++ b/mayan/apps/documents/views.py @@ -30,6 +30,7 @@ from converter.permissions import permission_transformation_delete from filetransfers.api import serve_file from permissions import Permission +from .events import event_document_download, event_document_view from .forms import ( DocumentDownloadForm, DocumentForm, DocumentPageForm, DocumentPreviewForm, DocumentPropertiesForm, DocumentTypeSelectForm, @@ -316,6 +317,10 @@ class DocumentPreviewView(SingleObjectDetailView): DocumentPreviewView, self ).dispatch(request, *args, **kwargs) self.get_object().add_as_recent_document_for_user(request.user) + event_document_view.commit( + actor=request.user, target=self.get_object() + ) + return result def get_extra_context(self): @@ -841,6 +846,10 @@ def document_download(request, document_id=None, document_id_list=None, document arcname=document_version.document.label ) descriptor.close() + event_document_download.commit( + actor=request.user, + target=document_version.document + ) compressed_file.close() @@ -865,6 +874,9 @@ def document_download(request, document_id=None, document_id_list=None, document # Test permissions and trigger exception fd = queryset.first().open() fd.close() + event_document_download.commit( + actor=request.user, target=queryset.first().document + ) return serve_file( request, queryset.first().file, From f042533576fd2496809e7df65898bf2776ed033d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 4 Apr 2016 13:51:23 -0400 Subject: [PATCH 51/57] Put event action description in past tense. --- mayan/apps/documents/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mayan/apps/documents/events.py b/mayan/apps/documents/events.py index 21bd2f834b..c474b6fbbd 100644 --- a/mayan/apps/documents/events.py +++ b/mayan/apps/documents/events.py @@ -9,7 +9,7 @@ event_document_create = Event( ) event_document_download = Event( name='documents_document_download', - label=_('Document download') + label=_('Document downloaded') ) event_document_properties_edit = Event( name='documents_document_edit', label=_('Document properties edited') From be392823bbba16bdbd79a9f93ed333dc1b442b32 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 4 Apr 2016 14:55:37 -0400 Subject: [PATCH 52/57] Remove all GPG interface module. --- mayan/apps/django_gpg/api.py | 306 ----------------------------------- 1 file changed, 306 deletions(-) delete mode 100644 mayan/apps/django_gpg/api.py diff --git a/mayan/apps/django_gpg/api.py b/mayan/apps/django_gpg/api.py deleted file mode 100644 index aadc50bc37..0000000000 --- a/mayan/apps/django_gpg/api.py +++ /dev/null @@ -1,306 +0,0 @@ -from __future__ import unicode_literals - -import logging -import os -import tempfile - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -import gnupg - -from django.utils.translation import ugettext_lazy as _ - -from .exceptions import * # NOQA -from .literals import KEY_TYPES - -logger = logging.getLogger(__name__) - - -class KeyStub(object): - def __init__(self, raw): - self.key_id = raw['keyid'] - self.key_type = raw['type'] - self.date = raw['date'] - self.expires = raw['expires'] - self.length = raw['length'] - self.uids = raw['uids'] - - -class Key(object): - @staticmethod - def get_key_id(fingerprint): - return fingerprint[-16:] - - @classmethod - def get_all(cls, gpg, secret=False, exclude=None): - result = [] - keys = gpg.gpg.list_keys(secret=secret) - if exclude: - excluded_id = exclude.key_id - else: - excluded_id = '' - for key in keys: - if not key['keyid'] in excluded_id: - key_instance = Key( - fingerprint=key['fingerprint'], - uids=key['uids'], - type=key['type'], - data=gpg.gpg.export_keys([key['keyid']], secret=secret) - ) - result.append(key_instance) - - return result - - @classmethod - def get(cls, gpg, key_id, secret=False, search_keyservers=False): - if len(key_id) > 16: - # key_id is a fingerprint - key_id = Key.get_key_id(key_id) - - keys = gpg.gpg.list_keys(secret=secret) - key = next((key for key in keys if key['keyid'] == key_id), None) - if not key: - if search_keyservers and secret is False: - try: - gpg.receive_key(key_id) - return Key(gpg, key_id) - except KeyFetchingError: - raise KeyDoesNotExist - else: - raise KeyDoesNotExist - - key_instance = Key( - fingerprint=key['fingerprint'], - uids=key['uids'], - type=key['type'], - data=gpg.gpg.export_keys([key['keyid']], secret=secret) - ) - - return key_instance - - def __init__(self, fingerprint, uids, type, data): - self.fingerprint = fingerprint - self.uids = uids - self.type = type - self.data = data - - @property - def key_id(self): - return Key.get_key_id(self.fingerprint) - - @property - def user_ids(self): - return ', '.join(self.uids) - - def __str__(self): - return '%s "%s" (%s)' % ( - self.key_id, self.user_ids, KEY_TYPES.get(self.type, _('Unknown')) - ) - - def __unicode__(self): - return unicode(self.__str__()) - - def __repr__(self): - return self.__unicode__() - - -class GPG(object): - @staticmethod - def get_descriptor(file_input): - try: - # Is it a file like object? - file_input.seek(0) - except AttributeError: - # If not, try open it. - return open(file_input, 'rb') - else: - return file_input - - def __init__(self, binary_path=None, home=None, keyring=None, keyservers=None): - kwargs = {} - if binary_path: - kwargs['gpgbinary'] = binary_path - - if home: - kwargs['gnupghome'] = home - - if keyring: - kwargs['keyring'] = keyring - - self.keyservers = keyservers - - try: - self.gpg = gnupg.GPG(**kwargs) - except OSError as exception: - raise GPGException( - 'ERROR: GPG initialization error; Make sure the GPG binary is properly installed; %s' % exception - ) - except Exception as exception: - raise GPGException( - 'ERROR: GPG initialization error; %s' % exception - ) - - def verify_file(self, file_input, detached_signature=None, fetch_key=False): - """ - Verify the signature of a file. - """ - - input_descriptor = GPG.get_descriptor(file_input) - - if detached_signature: - # Save the original data and invert the argument order - # Signature first, file second - file_descriptor, filename = tempfile.mkstemp(prefix='django_gpg') - os.write(file_descriptor, input_descriptor.read()) - os.close(file_descriptor) - - detached_signature = GPG.get_descriptor(detached_signature) - signature_file = StringIO() - signature_file.write(detached_signature.read()) - signature_file.seek(0) - verify = self.gpg.verify_file( - signature_file, data_filename=filename - ) - signature_file.close() - else: - verify = self.gpg.verify_file(input_descriptor) - - logger.debug('verify.status: %s', getattr(verify, 'status', None)) - if verify: - logger.debug('verify ok') - return verify - elif getattr(verify, 'status', None) == 'no public key': - # Exception to the rule, to be able to query the keyservers - if fetch_key: - try: - self.receive_key(verify.key_id) - return self.verify_file( - input_descriptor, detached_signature, fetch_key=False - ) - except KeyFetchingError: - return verify - else: - return verify - else: - logger.debug('No verify') - raise GPGVerificationError() - - def verify(self, data): - # TODO: try to merge with verify_file - verify = self.gpg.verify(data) - - if verify: - return verify - else: - raise GPGVerificationError(verify.status) - - def sign_file(self, file_input, key=None, destination=None, key_id=None, passphrase=None, clearsign=False): - """ - Signs a filename, storing the signature and the original file - in the destination filename provided (the destination file is - overrided if it already exists), if no destination file name is - provided the signature is returned. - """ - - kwargs = {} - kwargs['clearsign'] = clearsign - - if key_id: - kwargs['keyid'] = key_id - - if key: - kwargs['keyid'] = key.key_id - - if passphrase: - kwargs['passphrase'] = passphrase - - input_descriptor = GPG.get_descriptor(file_input) - - if destination: - output_descriptor = open(destination, 'wb') - - signed_data = self.gpg.sign_file(input_descriptor, **kwargs) - if not signed_data.fingerprint: - raise GPGSigningError('Unable to sign file') - - if destination: - output_descriptor.write(signed_data.data) - - input_descriptor.close() - - if destination: - output_descriptor.close() - - if not destination: - return signed_data - - def has_embedded_signature(self, *args, **kwargs): - try: - self.decrypt_file(*args, **kwargs) - except GPGDecryptionError: - return False - else: - return True - - def decrypt_file(self, file_input, close_descriptor=True): - input_descriptor = GPG.get_descriptor(file_input) - - result = self.gpg.decrypt_file(input_descriptor) - if close_descriptor: - input_descriptor.close() - - if not result.status or result.status == 'no data was provided': - raise GPGDecryptionError('Unable to decrypt file') - - return result - - def create_key(self, *args, **kwargs): - if kwargs.get('passphrase') == '': - kwargs.pop('passphrase') - - input_data = self.gpg.gen_key_input(**kwargs) - key = self.gpg.gen_key(input_data) - if not key: - raise KeyGenerationError('Unable to generate key') - - return Key.get(self, key.fingerprint) - - def delete_key(self, key): - status = self.gpg.delete_keys( - key.fingerprint, key.type == 'sec' - ).status - if status == 'Must delete secret key first': - self.delete_key(Key.get(self, key.fingerprint, secret=True)) - self.delete_key(key) - elif status != 'ok': - raise KeyDeleteError('Unable to delete key') - - def receive_key(self, key_id): - for keyserver in self.keyservers: - import_result = self.gpg.recv_keys(keyserver, key_id) - if import_result: - return Key.get( - self, import_result.fingerprints[0], secret=False - ) - - raise KeyFetchingError - - def query(self, term): - results = {} - for keyserver in self.keyservers: - for key_data in self.gpg.search_keys(query=term, keyserver=keyserver): - results[key_data['keyid']] = KeyStub(raw=key_data) - - return results.values() - - def import_key(self, key_data): - import_result = self.gpg.import_keys(key_data) - logger.debug('import_result: %s', import_result) - - if import_result: - return Key.get(self, import_result.fingerprints[0], secret=False) - - raise KeyImportError(import_result.results) From e5743d8964665fa838bf4b06a335dde5def23929 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 4 Apr 2016 16:25:52 -0400 Subject: [PATCH 53/57] Remove Django 1.7 and add Django 1.9 to the tox test matrix. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 79581d3850..a64387cd66 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = coverage-clean - py{27,33,34,35}-django{1.7,1.8} + py{27,33,34,35}-django{1.8,1.9} coverage-report [testenv] @@ -16,8 +16,8 @@ commands= deps = -rrequirements/testing-no-django.txt - django1.7: django>=1.7,<1.8 django1.8: django>=1.8,<1.9 + django1.9: django>=1.9,<1.10 setenv= COVERAGE_FILE=.coverage.tox.{envname} From b04421438e49920a687f866a977097d3a09c95f8 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 4 Apr 2016 23:04:20 -0400 Subject: [PATCH 54/57] Update changelog. --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index e71d547645..1fd539552f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -29,6 +29,7 @@ - Add staging file deletion permission. - New document_signature_view permission. - Add support for signing documents. +- Instead of multiple keyservers only one keyserver is now supported. - Replace document type selection widget with an opened selection list. 2.0.2 (2016-02-09) From e3200511acc55b787e2155ac7da28217534cb264 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 4 Apr 2016 23:04:35 -0400 Subject: [PATCH 55/57] Small query optimization. --- mayan/apps/acls/managers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index ef66326d31..e9bbb88b5b 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -81,8 +81,7 @@ class AccessControlListManager(models.Manager): user_roles.append(role) - # TODO: possible .exists() optimization - if not self.filter(content_type=ContentType.objects.get_for_model(obj), object_id=obj.pk, permissions__in=stored_permissions, role__in=user_roles): + if not self.filter(content_type=ContentType.objects.get_for_model(obj), object_id=obj.pk, permissions__in=stored_permissions, role__in=user_roles).exists(): raise PermissionDenied(ugettext('Insufficient access.')) def filter_by_access(self, permission, user, queryset): From 756108af8d3ffd1de2f3d119a506656f95e34949 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 4 Apr 2016 23:05:06 -0400 Subject: [PATCH 56/57] Disable GitLab CI MySQL testing until GitLab mysql container isssue are fixed. --- .gitlab-ci.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c7ed5587b6..fa3e214c59 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,16 +10,16 @@ variables: POSTGRES_PASSWORD: "postgres" MYSQL_ALLOW_EMPTY_PASSWORD: "yes" MYSQL_DATABASE: "mayan_edms" -test:mysql: - script: - - pip install -r requirements/testing.txt - - pip install -q mysql-python - - apt-get install -qq mysql-client - - mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -p"$MYSQL_ENV_MYSQL_ROOT_PASSWORD" -e "ALTER DATABASE $MYSQL_DATABASE CHARACTER SET utf8 COLLATE utf8_unicode_ci;" - - coverage run manage.py runtests --settings=mayan.settings.testing.gitlab-ci.db_mysql --nomigrations - - bash <(curl https://raw.githubusercontent.com/codecov/codecov-bash/master/codecov) -t $CODECOV_TOKEN - tags: - - mysql +#test:mysql: +# script: +# - pip install -r requirements/testing.txt +# - pip install -q mysql-python +# - apt-get install -qq mysql-client +# - mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -p"$MYSQL_ENV_MYSQL_ROOT_PASSWORD" -e "ALTER DATABASE $MYSQL_DATABASE CHARACTER SET utf8 COLLATE utf8_unicode_ci;" +# - coverage run manage.py runtests --settings=mayan.settings.testing.gitlab-ci.db_mysql --nomigrations +# - bash <(curl https://raw.githubusercontent.com/codecov/codecov-bash/master/codecov) -t $CODECOV_TOKEN +# tags: +# - mysql test:postgres: script: - pip install -r requirements/testing.txt From f020a7a1c6ce9d8f7a7da10cfc0c43e0b9f107d6 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 21 Apr 2016 17:01:45 -0400 Subject: [PATCH 57/57] Expand v2.1 release notes. --- docs/releases/2.1.rst | 130 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 116 insertions(+), 14 deletions(-) diff --git a/docs/releases/2.1.rst b/docs/releases/2.1.rst index a0f7d1bf12..cbf2a47ec9 100644 --- a/docs/releases/2.1.rst +++ b/docs/releases/2.1.rst @@ -7,22 +7,125 @@ Released: April, 2016 What's new ========== -- Upgrade to use Django 1.8.11. -- Remove remaining references to Django's User model. -- Remove included login required middleware using django-stronghold instead (http://mikegrouchy.com/django-stronghold/). -- Improve generation of success and error messages for class based views. -- Remove ownership concept from folders. -- Replace strip_spaces middleware with the spaceless template tag. -- Deselect the update checkbox for optional metadata by default. -- Implement per document type document creation permission. -- Make document type delete time period optional. -- Fixed date locale handling in document properties, checkout and user detail views. -- Add HTML5 upload widget. -- Add Message of the Day app. +Upgrade to use Django 1.8.11 +---------------------------- +With the end of life support for Django 1.7, moving to the next Mayan EDMS +minor version was a target for this release. The Django minor release chosen was +1.8 as it is very compatible with 1.7 and required minimal changes. Django 1.8 +is an LTS release (Long Term Support) meaning that is no new big feature of a +new Django version is required, the project can stay in Django 1.8 for a good +amount of time with no downsides. + +Remove remaining references to Django's User model +-------------------------------------------------- +The few remaining hard code references to Django's User model that were missed +in a previous release have been removed. Using a custom User model with Mayan +should present very little if any obstacles. + +Remove included login required middleware +----------------------------------------- +The custom middleware include with Mayan EDMS that forces user to be +authenticated before being able to access any view has been removed in favor of +a dedicated 3rd party Django app for that purpose. The app chosen was +django-stronghold (http://mikegrouchy.com/django-stronghold/). + +Improve generation of success and error messages for class based views +---------------------------------------------------------------------- +In the past success messages for actions would show a generic mention to the +object being manipulated (document, folder, tag). Now the errors and success +messages with be more explcit in describing what the view has or was trying +to manipulate. + +Remove ownership concept from folders +------------------------------------- +Currently Folders in Mayan EDMS have a field that stores a reference to the +user that has created that folders. One of the design decissions of Mayan EDMS +is that there should never be any explicit ownership of any object. Ownership +is relative and is defined by the Access Control List of an object. The +removal of the user field from the Folders model brings this app in line with +the defined behavior. + +Replacement of strip_spaces middleware with the spaceless template tag +---------------------------------------------------------------------- +As a size optimization technique HTML content was dynamically stripped of spaces +as it was being served. The techique used involved detecting the MIME type of +the content being served and if found to be of text/HTML type spaces between +tags were stripped. An edge case was found where this did not worked always. +The approached has been changed to use Django's official tag to strip spaces. +In addition to using an official approach, the removal of spaces only happens +when the template is compiled and not at each HTTP response. The optimization +is minimal but since it happened at every response a small increase in speed +is expected for all deployment scenarios. + +Deselect the update checkbox for optional metadata by default +------------------------------------------------------------- +During the last releases the behavior of the of metadata edit checkbox has seen +several tune ups. Thanks to community feedback one small change has been +introduced. The edit checkbox will be deselected by default for all optional +document type metadata entries. + +Implement per document type document creation permission +-------------------------------------------------------- +If is now possible to grant the document creation permission to a role for a +document type. Previously document creation was a "blanket" permission. Having +the permission meant that user could create any type of document. With this +change it is now possible to restrict which types of document users of a +specific role can create. + +Make document type delete time period optional +---------------------------------------------- +The entries that defined after how long a document in the trash would be +permanently deleted have been made optional. This means that if a document +type has this option blank, the corresponding document of this type would never +be deleted from the trash can. + +Fixed date locale handling in document properties, checkout and user detail views +--------------------------------------------------------------------------------- +A few releases back the ability to for users to set their timezone was added. +This change also included a smart date rendering update to adjust the dates +and times fields to the user's timezone. Some users reported a few views where +this timezone adjustment was not happeding, this has been fully fixed. + +HTML5 upload widget +------------------- +A common request is the ability to just drap and drop documents from other +windows into Mayan EDMS's document upload wizard. This release includes that +capability and will also show a completion bar for the upload. Document +uploading is sped up dramatically with this change. + +Message of the Day app +---------------------- +Administrators wanting to display announcements has no other way to do so +than to customize the login template. To avoid this a new app has been added +that allows for the creation of messages to be shown at the user login +screen. These messages can have an activation and an experiation date and +time. These messages are useful for display company access policies, +maintenance announcement, etc. + +Document signing +---------------- +The biggest change for this release if the addition of document signing from +within the UI. Enterprise users request this feature very often as in those +environments cryptographic signatures are a basic requirement. Previously +Mayan EDMS had the ability to automatically check if a document was signed and +if signed, verify the validity of the signature. However, to sign documents +user had to download the document, sign the document offline, and either +re-upload the signed document as a new version or upload a detached +signature for the existing document version. Aside from being now able to sign +documents from the web user iterface, the way keys are handled has been +rewritten from scratch to support distributed key storage. This means that +a key uploaded in one computer by one user can be used transparently by +other users in other computers to sign documents. The relevant access control +updates were added to the new document signing system. Users wanting to sign a +document need the singing permission for the document (or document type), +for the private key they intend to use, and the passphrase (if the key has one). +Finally documents are now checked just once for signatures and not every time +they are accessed, this provides a very sizable speed improvement in document +access and availability. Other changes ============= -- Upgrade requirements. +- Upgrade Python requirements to recent versions. - Rename 'Content' search box to 'OCR'. - Silence all Django 1.8 model import warnings. - Add icons to the document face menu links. @@ -94,5 +197,4 @@ Bugs fixed or issues closed * `GitLab issue #246 `_ Upgrade to Django version 1.8 as Django 1.7 is end-of-life. * `GitLab issue #255 `_ UnicodeDecodeError in apps/common/middleware/strip_spaces_widdleware.py. - .. _PyPI: https://pypi.python.org/pypi/mayan-edms/