diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fa3e214c59..82d4647ed7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,29 +1,31 @@ -image: python:2.7 +image: ubuntu:16.10 services: - - mysql + - mysql:latest - postgres before_script: - apt-get update -qq - - apt-get install -qq python-dev gcc tesseract-ocr tesseract-ocr-deu unpaper ghostscript libjpeg-dev libpng-dev libtiff-dev poppler-utils libreoffice + - apt-get install -qq python-dev python-pip gcc gnupg1 tesseract-ocr tesseract-ocr-deu ghostscript libjpeg-dev libpng-dev libtiff-dev poppler-utils libreoffice variables: POSTGRES_DB: "mayan_edms" 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: + - apt-get install -qq libmysqlclient-dev + - pip install -r requirements/testing.txt + - pip install 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: + - apt-get install -qq libpq-dev - pip install -r requirements/testing.txt - - pip install -q psycopg2 + - pip install psycopg2 - coverage run manage.py runtests --settings=mayan.settings.testing.gitlab-ci.db_postgres --nomigrations - bash <(curl https://raw.githubusercontent.com/codecov/codecov-bash/master/codecov) -t $CODECOV_TOKEN tags: diff --git a/Makefile b/Makefile index 7645214f2b..f2513c0b07 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,7 @@ help: @echo "release - Package (sdist and wheel) and upload a release." @echo "runserver - Run the development server." + @echo "shell_plus - Run the shell_plus command." # Cleaning @@ -111,3 +112,8 @@ wheel: clean runserver: $(BROWSER) http://127.0.0.1:8000 ./manage.py runserver + +shell_plus: + ./manage.py shell_plus --settings=mayan.settings.development + + diff --git a/docs/conf.py b/docs/conf.py index dfe3065dfb..19e9d860e1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,7 +38,6 @@ extensions = ['djangodocs', 'sphinxcontrib.blockdiag'] blockdiag_antialias = True blockdiag_html_image_format = "SVG" blockdiag_latex_image_format = "PDF" -blockdiag_tex_image_format = "PDF" # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/index.rst b/docs/index.rst index 314da95ccf..20e7fcedcf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ and install it from PyPI with the following commands: .. code-block:: bash - sudo apt-get install libjpeg-dev libmagic1 libpng-dev libreoffice libtiff-dev gcc ghostscript gpgv python-dev python-virtualenv tesseract-ocr poppler-utils -y + sudo apt-get install libjpeg-dev libmagic1 libpng-dev libreoffice libtiff-dev gcc ghostscript gnupg1 python-dev python-virtualenv tesseract-ocr poppler-utils -y virtualenv venv source venv/bin/activate pip install mayan-edms diff --git a/docs/releases/2.1.4.rst b/docs/releases/2.1.4.rst new file mode 100644 index 0000000000..3982ea44a1 --- /dev/null +++ b/docs/releases/2.1.4.rst @@ -0,0 +1,75 @@ +=============================== +Mayan EDMS v2.1.4 release notes +=============================== + +Released: XX, 2016 + +What's new +========== + +This is a bug-fix release and all users are encouraged to upgrade. + +Other changes +------------- +- Fix statistics namespace list display view +- Fix events list display view +- Update to Django 1.8.15 + +Removals +-------- +* None + +Upgrading from a previous version +--------------------------------- + +Using PIP +~~~~~~~~~ + +Type in the console:: + + $ pip install -U mayan-edms + +the requirements will also be updated automatically. + +Using Git +~~~~~~~~~ + +If you installed Mayan EDMS by cloning the Git repository issue the commands:: + + $ git reset --hard HEAD + $ git pull + +otherwise download the compressed archived and uncompress it overriding the +existing installation. + +Next upgrade/add the new requirements:: + + $ pip install --upgrade -r requirements.txt + +Common steps +~~~~~~~~~~~~ + +Migrate existing database schema with:: + + $ mayan-edms.py performupgrade + +Add new static media:: + + $ mayan-edms.py collectstatic --noinput + +The upgrade procedure is now complete. + + +Backward incompatible changes +============================= + +* None + +Bugs fixed or issues closed +=========================== + +* `GitLab issue #311 `_ acl page return ContentType:Document +* `GitLab issue #316 `_ Error when trying to access the statistics +* `GitLab issue #324 `_ Document signature tests fail in Ubuntu 16.10 + +.. _PyPI: https://pypi.python.org/pypi/mayan-edms/ diff --git a/docs/releases/index.rst b/docs/releases/index.rst index 9e1e8e3a91..394b5a7cde 100644 --- a/docs/releases/index.rst +++ b/docs/releases/index.rst @@ -22,6 +22,8 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 +======= + 2.1.4 2.1.3 2.1.2 2.1.1 diff --git a/docs/topics/deploying.rst b/docs/topics/deploying.rst index 9e322148d2..4893a9bd20 100644 --- a/docs/topics/deploying.rst +++ b/docs/topics/deploying.rst @@ -2,15 +2,17 @@ Deploying ========= -OS "bare metal" -=============== +Below are some ways to deploye and use Mayan EDMS. Do use more than one method. + +OS "bare metal" method +====================== Like other Django based projects Mayan EDMS can be deployed in a wide variety of ways. The method provided below is only a bare minimum example. These instructions are independent of the instructions mentioned in the :doc:`installation` chapter but assume you have already made a test install to test the compatibility of your operating system. These instruction are for -Ubuntu 15.04. +Ubuntu 16.10. Switch to superuser:: @@ -20,8 +22,8 @@ Install all system dependencies:: apt-get install nginx supervisor redis-server postgresql \ libpq-dev libjpeg-dev libmagic1 libpng-dev libreoffice \ - libtiff-dev gcc ghostscript gpgv python-dev python-virtualenv \ - tesseract-ocr unpaper poppler-utils -y + libtiff-dev gcc ghostscript gnupg1 python-dev python-virtualenv \ + tesseract-ocr poppler-utils -y Change to the directory where the project will be deployed:: @@ -79,6 +81,7 @@ Append the following to the ``mayan/settings/local.py`` file, paying attention t BROKER_URL = 'redis://127.0.0.1:6379/0' CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0' + SIGNATURES_GPG_PATH = '/usr/bin/gpg1' Migrate the database or initialize the project:: @@ -189,8 +192,8 @@ Restart the services:: /etc/init.d/nginx restart /etc/init.d/supervisor restart -Docker -====== +Docker method +============= Deploy the Docker image stack:: @@ -205,8 +208,8 @@ with:: docker logs mayan-edms -Docker Compose -============== +Docker Compose method +===================== Create a file named ``environment`` with the following content:: @@ -248,8 +251,8 @@ with:: docker logs mayanedms_mayan-edms_1 -Vagrant -======= +Vagrant method +============== Make sure you have Vagrant and a provider properly installed as per https://docs.vagrantup.com/v2/installation/index.html Clone the repository and execute:: diff --git a/docs/topics/ocr_backend.rst b/docs/topics/ocr_backend.rst index ab0de3c835..cdef0c5fff 100644 --- a/docs/topics/ocr_backend.rst +++ b/docs/topics/ocr_backend.rst @@ -2,8 +2,9 @@ OCR backend =========== -Mayan EDMS ships an OCR backend that uses the FLOSS engine Tesseract, but it can -use other engines. To support other engines a wrapper that subclasess the +Mayan EDMS ships an OCR backend that uses the FLOSS engine Tesseract +(https://github.com/tesseract-ocr/tesseract/), but it can +use other engines. To support other engines crate a wrapper that subclasess the ``OCRBackendBase`` class defined in mayan/apps/ocr/classes. This subclass should expose the ``execute`` method. For an example of how the Tesseract backend is implemented take a look at the file ``mayan/apps/ocr/backends/tesseract.py`` @@ -13,3 +14,8 @@ OCR_BACKEND and point it to your new OCR backend class path. The default value of OCR_BACKEND is ``"ocr.backends.tesseract.Tesseract"`` +To add support to OCR more languages when using Tesseract, install the +corresponding language file. If using a Debian based OS, this command will +display the available language files: + + apt-cache search tesseract-ocr diff --git a/docs/topics/signatures.rst b/docs/topics/signatures.rst index c4f3a545c3..f9efcc1e18 100644 --- a/docs/topics/signatures.rst +++ b/docs/topics/signatures.rst @@ -21,4 +21,6 @@ keys no longer needed can also be deleted from this menu. Only `GNU Privacy Guard`_ signatures are support at the moment. +Only version 1 of `GNU Privacy Guard`_ is supported for now. + .. _`GNU Privacy Guard`: www.gnupg.org/ diff --git a/mayan/apps/acls/tests/test_views.py b/mayan/apps/acls/tests/test_views.py index 52d0e74afc..58dfb936b4 100644 --- a/mayan/apps/acls/tests/test_views.py +++ b/mayan/apps/acls/tests/test_views.py @@ -8,7 +8,7 @@ from user_management.tests import ( ) from ..models import AccessControlList -from ..permissions import permission_acl_edit +from ..permissions import permission_acl_edit, permission_acl_view class AccessControlListViewTestCase(GenericDocumentViewTestCase): @@ -109,3 +109,60 @@ class AccessControlListViewTestCase(GenericDocumentViewTestCase): self.assertNotContains(response, text='optgroup', status_code=200) self.assertEqual(AccessControlList.objects.count(), 1) + + def test_acl_list_view_no_permission(self): + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + document = self.document.add_as_recent_document_for_user( + self.user + ).document + + acl = AccessControlList.objects.create( + content_object=document, role=self.role + ) + acl.permissions.add(permission_acl_edit.stored_permission) + + content_type = ContentType.objects.get_for_model(document) + + view_arguments = { + 'app_label': content_type.app_label, + 'model': content_type.model, + 'object_id': document.pk + } + + response = self.get( + viewname='acls:acl_list', kwargs=view_arguments + ) + + self.assertNotContains(response, text=document.label, status_code=403) + self.assertNotContains(response, text='otal: 1', status_code=403) + + def test_acl_list_view_with_permission(self): + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add( + permission_acl_view.stored_permission + ) + + document = self.document.add_as_recent_document_for_user( + self.user + ).document + + acl = AccessControlList.objects.create( + content_object=document, role=self.role + ) + acl.permissions.add(permission_acl_view.stored_permission) + + content_type = ContentType.objects.get_for_model(document) + + view_arguments = { + 'app_label': content_type.app_label, + 'model': content_type.model, + 'object_id': document.pk + } + + response = self.get( + viewname='acls:acl_list', kwargs=view_arguments + ) + self.assertContains(response, text=document.label, status_code=200) + self.assertContains(response, text='otal: 1', status_code=200) diff --git a/mayan/apps/acls/views.py b/mayan/apps/acls/views.py index 1d827fbd81..2ee1228f20 100644 --- a/mayan/apps/acls/views.py +++ b/mayan/apps/acls/views.py @@ -121,13 +121,13 @@ class ACLDeleteView(SingleObjectDeleteView): class ACLListView(SingleObjectListView): def dispatch(self, request, *args, **kwargs): - self.content_type = get_object_or_404( + self.object_content_type = get_object_or_404( ContentType, app_label=self.kwargs['app_label'], model=self.kwargs['model'] ) try: - self.content_object = self.content_type.get_object_for_this_type( + self.content_object = self.object_content_type.get_object_for_this_type( pk=self.kwargs['object_id'] ) except self.content_type.model_class().DoesNotExist: @@ -153,7 +153,7 @@ class ACLListView(SingleObjectListView): def get_queryset(self): return AccessControlList.objects.filter( - content_type=self.content_type, object_id=self.content_object.pk + content_type=self.object_content_type, object_id=self.content_object.pk ) diff --git a/mayan/apps/common/tests/base.py b/mayan/apps/common/tests/base.py index 086179b17b..5142902ce1 100644 --- a/mayan/apps/common/tests/base.py +++ b/mayan/apps/common/tests/base.py @@ -2,10 +2,10 @@ from __future__ import unicode_literals from django.test import TestCase -from .mixins import OpenFileCheckMixin, TempfileCheckMixin +from .mixins import ContentTypeCheckMixin, OpenFileCheckMixin, TempfileCheckMixin -class BaseTestCase(OpenFileCheckMixin, TempfileCheckMixin, TestCase): +class BaseTestCase(ContentTypeCheckMixin, OpenFileCheckMixin, TempfileCheckMixin, TestCase): """ This is the most basic test case class any test in the project should use. """ diff --git a/mayan/apps/common/tests/mixins.py b/mayan/apps/common/tests/mixins.py index 9861b9e278..385ec24de6 100644 --- a/mayan/apps/common/tests/mixins.py +++ b/mayan/apps/common/tests/mixins.py @@ -7,6 +7,30 @@ import psutil from ..settings import setting_temporary_directory +class ContentTypeCheckMixin(object): + expected_content_type = 'text/html; charset=utf-8' + + def _pre_setup(self): + super(ContentTypeCheckMixin, self)._pre_setup() + test_instance = self + + class CustomClient(self.client_class): + def request(self, *args, **kwargs): + response = super(CustomClient, self).request(*args, **kwargs) + + content_type = response._headers['content-type'][1] + test_instance.assertEqual( + content_type, test_instance.expected_content_type, + msg='Unexpected response content type: {}, expected: {}.'.format( + content_type, test_instance.expected_content_type + ) + ) + + return response + + self.client = CustomClient() + + class TempfileCheckMixin(object): def _get_temporary_entries(self): return os.listdir(setting_temporary_directory.value) diff --git a/mayan/apps/django_gpg/classes.py b/mayan/apps/django_gpg/classes.py index 510f14ada0..5dd69945f1 100644 --- a/mayan/apps/django_gpg/classes.py +++ b/mayan/apps/django_gpg/classes.py @@ -1,6 +1,136 @@ from __future__ import absolute_import, unicode_literals from datetime import date +import os +import shutil + +import gnupg + +from common.utils import mkdtemp + + +class GPGBackend(object): + def __init__(self, **kwargs): + self.kwargs = kwargs + + +class PythonGNUPGBackend(GPGBackend): + @staticmethod + def _import_key(gpg, **kwargs): + return gpg.import_keys(**kwargs) + + @staticmethod + def _list_keys(gpg, **kwargs): + return gpg.list_keys(**kwargs) + + @staticmethod + def _import_and_list_keys(gpg, **kwargs): + import_results = gpg.import_keys(**kwargs) + return import_results, gpg.list_keys( + keys=import_results.fingerprints[0] + )[0] + + @staticmethod + def _sign_file(gpg, file_object, key_data, passphrase, clearsign, detached, binary, output): + import_results = gpg.import_keys(key_data=key_data) + + return gpg.sign_file( + file=file_object, keyid=import_results.fingerprints[0], + passphrase=passphrase, clearsign=clearsign, detach=detached, + binary=binary, output=output + ) + + @staticmethod + def _decrypt_file(gpg, file_object, keys): + for key in keys: + gpg.import_keys(key_data=key['key_data']) + + return gpg.decrypt_file(file=file_object) + + @staticmethod + def _verify_file(gpg, file_object, keys, data_filename=None): + for key in keys: + gpg.import_keys(key_data=key['key_data']) + + return gpg.verify_file( + file=file_object, data_filename=data_filename + ) + + @staticmethod + def _recv_keys(gpg, keyserver, key_id): + import_results = gpg.recv_keys(keyserver, key_id) + if import_results.count: + key_data = gpg.export_keys(import_results.fingerprints[0]) + else: + key_data = None + return key_data + + @staticmethod + def _search_keys(gpg, keyserver, query): + return gpg.search_keys( + keyserver=keyserver, query=query + ) + + def gpg_command(self, function, **kwargs): + temporary_directory = mkdtemp() + os.chmod(temporary_directory, 0x1C0) + + gpg = gnupg.GPG( + gnupghome=temporary_directory, gpgbinary=self.kwargs['binary_path'] + ) + + result = function(gpg=gpg, **kwargs) + + shutil.rmtree(temporary_directory) + + return result + + def import_key(self, key_data): + return self.gpg_command( + function=PythonGNUPGBackend._import_key, key_data=key_data + ) + + def list_keys(self, keys): + return self.gpg_command( + function=PythonGNUPGBackend._list_keys, keys=keys + ) + + def import_and_list_keys(self, key_data): + return self.gpg_command( + function=PythonGNUPGBackend._import_and_list_keys, + key_data=key_data + ) + + def sign_file(self, file_object, key_data, passphrase, clearsign, detached, binary, output): + return self.gpg_command( + function=PythonGNUPGBackend._sign_file, file_object=file_object, + key_data=key_data, passphrase=passphrase, clearsign=clearsign, + detached=detached, binary=binary, output=output + ) + + def decrypt_file(self, file_object, keys): + return self.gpg_command( + function=PythonGNUPGBackend._decrypt_file, file_object=file_object, + keys=keys + ) + + def verify_file(self, file_object, keys, data_filename=None): + return self.gpg_command( + function=PythonGNUPGBackend._verify_file, file_object=file_object, + keys=keys, data_filename=data_filename + ) + + def recv_keys(self, keyserver, key_id): + return self.gpg_command( + function=PythonGNUPGBackend._recv_keys, keyserver=keyserver, + key_id=key_id + ) + + def search_keys(self, keyserver, query): + return self.gpg_command( + function=PythonGNUPGBackend._search_keys, keyserver=keyserver, + query=query + ) class KeyStub(object): diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py index 4247529e49..dc5f4ba070 100644 --- a/mayan/apps/django_gpg/managers.py +++ b/mayan/apps/django_gpg/managers.py @@ -3,64 +3,56 @@ from __future__ import absolute_import, unicode_literals import io import logging import os -import shutil - -import gnupg from django.db import models -from common.utils import mkdtemp, mkstemp +from common.utils import mkstemp from .classes import KeyStub, SignatureVerification from .exceptions import ( DecryptionError, KeyDoesNotExist, KeyFetchingError, VerificationError ) from .literals import KEY_TYPE_PUBLIC, KEY_TYPE_SECRET -from .settings import setting_gpg_path, setting_keyserver +from .runtime import gpg_backend +from .settings import setting_keyserver logger = logging.getLogger(__name__) class KeyManager(models.Manager): - def decrypt_file(self, file_object, all_keys=False, key_fingerprint=None, key_id=None): - temporary_directory = mkdtemp() - - os.chmod(temporary_directory, 0x1C0) - - gpg = gnupg.GPG( - gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value - ) - + def _preload_keys(self, all_keys=False, key_fingerprint=None, key_id=None): # Preload keys if all_keys: logger.debug('preloading all keys') - for key in self.all(): - gpg.import_keys(key_data=key.key_data) + keys = self.values() elif key_fingerprint: logger.debug('preloading key fingerprint: %s', key_fingerprint) - try: - key = self.get(fingerprint=key_fingerprint) - except self.model.DoesNotExist: + keys = self.filter(fingerprint=key_fingerprint).values() + if not keys: 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) + keys = self.filter(fingerprint__endswith=key_id).values() + if keys: logger.debug('key id %s impored', key_id) + else: + logger.debug('key id %s not found', key_id) + else: + keys = () - decrypt_result = gpg.decrypt_file(file=file_object) + return keys - shutil.rmtree(temporary_directory) + def decrypt_file(self, file_object, all_keys=False, key_fingerprint=None, key_id=None): + keys = self._preload_keys( + all_keys=all_keys, key_fingerprint=key_fingerprint, key_id=key_id + ) + + decrypt_result = gpg_backend.decrypt_file( + file_object=file_object, keys=keys + ) logger.debug('decrypt_result.status: %s', decrypt_result.status) @@ -72,40 +64,20 @@ class KeyManager(models.Manager): return io.BytesIO(decrypt_result.data) def receive_key(self, key_id): - temporary_directory = mkdtemp() - - os.chmod(temporary_directory, 0x1C0) - - gpg = gnupg.GPG( - gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + key_data = gpg_backend.recv_keys( + keyserver=setting_keyserver.value, key_id=key_id ) - import_results = gpg.recv_keys(setting_keyserver.value, key_id) - - if not import_results.count: - shutil.rmtree(temporary_directory) + if not key_data: raise KeyFetchingError('No key found') else: - 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 = mkdtemp() - - os.chmod(temporary_directory, 0x1C0) - - gpg = gnupg.GPG( - gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + key_data_list = gpg_backend.search_keys( + keyserver=setting_keyserver.value, query=query ) - 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)) @@ -119,41 +91,10 @@ class KeyManager(models.Manager): return self.filter(key_type=KEY_TYPE_SECRET) def verify_file(self, file_object, signature_file=None, all_keys=False, key_fingerprint=None, key_id=None): - temporary_directory = mkdtemp() - - os.chmod(temporary_directory, 0x1C0) - - gpg = gnupg.GPG( - gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + keys = self._preload_keys( + all_keys=all_keys, key_fingerprint=key_fingerprint, key_id=key_id ) - # Preload keys - if all_keys: - logger.debug('preloading all keys') - for key in self.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) - if signature_file: # Save the original data and invert the argument order # Signature first, file second @@ -165,18 +106,19 @@ class KeyManager(models.Manager): 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 + verify_result = gpg_backend.verify_file( + file_object=signature_file_buffer, + data_filename=temporary_filename, keys=keys ) signature_file_buffer.close() os.unlink(temporary_filename) else: - verify_result = gpg.verify_file(file=file_object) + verify_result = gpg_backend.verify_file( + file_object=file_object, keys=keys + ) logger.debug('verify_result.status: %s', verify_result.status) - shutil.rmtree(temporary_directory) - if verify_result: # Signed and key present logger.debug('signed and key present') diff --git a/mayan/apps/django_gpg/migrations/0006_auto_20160510_0025.py b/mayan/apps/django_gpg/migrations/0006_auto_20160510_0025.py index b2a65a9abd..be851a2296 100644 --- a/mayan/apps/django_gpg/migrations/0006_auto_20160510_0025.py +++ b/mayan/apps/django_gpg/migrations/0006_auto_20160510_0025.py @@ -14,6 +14,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='key', name='key_data', - field=models.TextField(help_text='ASCII armored version of the key.', verbose_name='Key data'), + field=models.TextField( + help_text='ASCII armored version of the key.', + verbose_name='Key data' + ), ), ] diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py index 075d11e249..feccaecc75 100644 --- a/mayan/apps/django_gpg/models.py +++ b/mayan/apps/django_gpg/models.py @@ -2,10 +2,6 @@ from __future__ import absolute_import, unicode_literals from datetime import date import logging -import os -import shutil - -import gnupg from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse @@ -13,8 +9,6 @@ from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from common.utils import mkdtemp - from .exceptions import NeedPassphrase, PassphraseError from .literals import ( ERROR_MSG_NEED_PASSPHRASE, ERROR_MSG_BAD_PASSPHRASE, @@ -22,26 +16,11 @@ from .literals import ( OUTPUT_MESSAGE_CONTAINS_PRIVATE_KEY ) from .managers import KeyManager -from .settings import setting_gpg_path +from .runtime import gpg_backend logger = logging.getLogger(__name__) -def gpg_command(function): - temporary_directory = 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 - - @python_2_unicode_compatible class Key(models.Model): key_data = models.TextField( @@ -78,10 +57,7 @@ class Key(models.Model): 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) + import_results = gpg_backend.import_key(key_data=self.key_data) if not import_results.count: raise ValidationError(_('Invalid key data')) @@ -93,22 +69,11 @@ class Key(models.Model): return reverse('django_gpg:key_detail', args=(self.pk,)) def save(self, *args, **kwargs): - temporary_directory = mkdtemp() - - os.chmod(temporary_directory, 0x1C0) - - gpg = gnupg.GPG( - gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value + import_results, key_info = gpg_backend.import_and_list_keys( + key_data=self.key_data ) - - import_results = gpg.import_keys(key_data=self.key_data) - - key_info = gpg.list_keys(keys=import_results.fingerprints[0])[0] - logger.debug('key_info: %s', key_info) - shutil.rmtree(temporary_directory) - self.algorithm = key_info['algo'] self.creation_date = date.fromtimestamp(int(key_info['date'])) if key_info['expires']: @@ -134,24 +99,12 @@ class Key(models.Model): # file, and appear to be due to random data being inserted in the # output data stream." - temporary_directory = mkdtemp() - - os.chmod(temporary_directory, 0x1C0) - - gpg = gnupg.GPG( - gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value - ) - - import_results = gpg.import_keys(key_data=self.key_data) - - file_sign_results = gpg.sign_file( - file=file_object, keyid=import_results.fingerprints[0], - passphrase=passphrase, clearsign=clearsign, detach=detached, + file_sign_results = gpg_backend.sign_file( + file_object=file_object, key_data=self.key_data, + passphrase=passphrase, clearsign=clearsign, detached=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: diff --git a/mayan/apps/django_gpg/runtime.py b/mayan/apps/django_gpg/runtime.py new file mode 100644 index 0000000000..0f4e57c449 --- /dev/null +++ b/mayan/apps/django_gpg/runtime.py @@ -0,0 +1,10 @@ +from django.utils.module_loading import import_string + +from .settings import setting_gpg_path + +# TODO: This will become an setting option in 2.2 +SETTING_GPG_BACKEND = 'django_gpg.classes.PythonGNUPGBackend' + +gpg_backend = import_string(SETTING_GPG_BACKEND)( + binary_path=setting_gpg_path.value +) diff --git a/mayan/apps/django_gpg/tests/test_views.py b/mayan/apps/django_gpg/tests/test_views.py index 7f65c1329e..3a17209faf 100644 --- a/mayan/apps/django_gpg/tests/test_views.py +++ b/mayan/apps/django_gpg/tests/test_views.py @@ -32,6 +32,8 @@ class KeyViewTestCase(GenericViewTestCase): self.role.permissions.add(permission_key_download.stored_permission) + self.expected_content_type = 'application/octet-stream; charset=utf-8' + response = self.get( viewname='django_gpg:key_download', args=(key.pk,) ) diff --git a/mayan/apps/document_signatures/tests/test_views.py b/mayan/apps/document_signatures/tests/test_views.py index 2b0ff6b0f0..a654bc23d9 100644 --- a/mayan/apps/document_signatures/tests/test_views.py +++ b/mayan/apps/document_signatures/tests/test_views.py @@ -213,6 +213,8 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): permission_document_version_signature_download.stored_permission ) + self.expected_content_type = 'application/octet-stream; charset=utf-8' + response = self.get( 'signatures:document_version_signature_download', args=(signature.pk,), diff --git a/mayan/apps/documents/tests/test_events.py b/mayan/apps/documents/tests/test_events.py index 3824de40ee..224b0435ed 100644 --- a/mayan/apps/documents/tests/test_events.py +++ b/mayan/apps/documents/tests/test_events.py @@ -52,6 +52,9 @@ class DocumentEventsTestCase(GenericDocumentViewTestCase): self.role.permissions.add( permission_document_download.stored_permission ) + + self.expected_content_type = 'image/png' + self.post( 'documents:document_download', args=(self.document.pk,), ) diff --git a/mayan/apps/documents/tests/test_views.py b/mayan/apps/documents/tests/test_views.py index 3640c67e76..4645de3501 100644 --- a/mayan/apps/documents/tests/test_views.py +++ b/mayan/apps/documents/tests/test_views.py @@ -247,6 +247,9 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): permission_document_download.stored_permission ) + # Set the expected_content_type for common.tests.mixins.ContentTypeCheckMixin + self.expected_content_type = self.document.file_mimetype + response = self.post( 'documents:document_download', args=(self.document.pk,) ) @@ -284,6 +287,9 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): permission_document_download.stored_permission ) + # Set the expected_content_type for common.tests.mixins.ContentTypeCheckMixin + self.expected_content_type = self.document.file_mimetype + response = self.post( 'documents:document_multiple_download', data={'id_list': self.document.pk} @@ -323,6 +329,9 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): permission_document_download.stored_permission ) + # Set the expected_content_type for common.tests.mixins.ContentTypeCheckMixin + self.expected_content_type = self.document.file_mimetype + response = self.post( 'documents:document_version_download', args=( self.document.latest_version.pk, diff --git a/mayan/apps/events/apps.py b/mayan/apps/events/apps.py index b00954e817..74a6ddc661 100644 --- a/mayan/apps/events/apps.py +++ b/mayan/apps/events/apps.py @@ -15,6 +15,7 @@ from .widgets import event_type_link class EventsApp(MayanAppConfig): name = 'events' + test = True verbose_name = _('Events') def ready(self): diff --git a/mayan/apps/events/tests/__init__.py b/mayan/apps/events/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/events/tests/test_views.py b/mayan/apps/events/tests/test_views.py new file mode 100644 index 0000000000..e136607b75 --- /dev/null +++ b/mayan/apps/events/tests/test_views.py @@ -0,0 +1,72 @@ +from __future__ import absolute_import, unicode_literals + +from django.contrib.contenttypes.models import ContentType + +from acls.models import AccessControlList +from documents.tests.test_views import GenericDocumentViewTestCase +from user_management.tests import ( + TEST_USER_USERNAME, TEST_USER_PASSWORD +) + +from ..permissions import permission_events_view + + +class EventsViewTestCase(GenericDocumentViewTestCase): + def setUp(self): + super(EventsViewTestCase, self).setUp() + + content_type = ContentType.objects.get_for_model(self.document) + + self.view_arguments = { + 'app_label': content_type.app_label, + 'model': content_type.model, + 'object_id': self.document.pk + } + + def test_events_for_object_view_no_permission(self): + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + document = self.document.add_as_recent_document_for_user( + self.user + ).document + + content_type = ContentType.objects.get_for_model(document) + + view_arguments = { + 'app_label': content_type.app_label, + 'model': content_type.model, + 'object_id': document.pk + } + + response = self.get( + viewname='events:events_for_object', kwargs=view_arguments + ) + + self.assertNotContains(response, text=document.label, status_code=403) + self.assertNotContains(response, text='otal:', status_code=403) + + def test_events_for_object_view_with_permission(self): + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add( + permission_events_view.stored_permission + ) + + document = self.document.add_as_recent_document_for_user( + self.user + ).document + + content_type = ContentType.objects.get_for_model(document) + + view_arguments = { + 'app_label': content_type.app_label, + 'model': content_type.model, + 'object_id': document.pk + } + + response = self.get( + viewname='events:events_for_object', kwargs=view_arguments + ) + + self.assertContains(response, text=document.label, status_code=200) + self.assertNotContains(response, text='otal: 0', status_code=200) diff --git a/mayan/apps/events/views.py b/mayan/apps/events/views.py index 3e49200a3d..779bfbc005 100644 --- a/mayan/apps/events/views.py +++ b/mayan/apps/events/views.py @@ -43,16 +43,16 @@ class ObjectEventListView(EventListView): view_permissions = None def dispatch(self, request, *args, **kwargs): - self.content_type = get_object_or_404( + self.object_content_type = get_object_or_404( ContentType, app_label=self.kwargs['app_label'], model=self.kwargs['model'] ) try: - self.content_object = self.content_type.get_object_for_this_type( + self.content_object = self.object_content_type.get_object_for_this_type( pk=self.kwargs['object_id'] ) - except self.content_type.model_class().DoesNotExist: + except self.object_content_type.model_class().DoesNotExist: raise Http404 try: diff --git a/mayan/apps/navigation/classes.py b/mayan/apps/navigation/classes.py index 6edbf818dc..2d45129369 100644 --- a/mayan/apps/navigation/classes.py +++ b/mayan/apps/navigation/classes.py @@ -367,7 +367,7 @@ class SourceColumn(object): # Special case for queryset items produced from # .defer() or .only() optimizations return cls._registry[source._meta.parents.items()[0][0]] - except (KeyError, IndexError): + except (AttributeError, KeyError, IndexError): return () except TypeError: # unhashable type: list diff --git a/mayan/apps/statistics/tests/test_views.py b/mayan/apps/statistics/tests/test_views.py index 133a85d5af..69daca278d 100644 --- a/mayan/apps/statistics/tests/test_views.py +++ b/mayan/apps/statistics/tests/test_views.py @@ -34,3 +34,20 @@ class StatisticsViewTestCase(GenericViewTestCase): ) self.assertEqual(response.status_code, 200) + + + def test_statistic_namespace_list_view_no_permissions(self): + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + response = self.get('statistics:namespace_list') + + self.assertEqual(response.status_code, 403) + + def test_statistic_namespace_list_view_with_permissions(self): + self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) + + self.role.permissions.add(permission_statistics_view.stored_permission) + + response = self.get('statistics:namespace_list') + + self.assertEqual(response.status_code, 200) diff --git a/mayan/settings/testing/base.py b/mayan/settings/testing/base.py index 20e0329014..ab85238029 100644 --- a/mayan/settings/testing/base.py +++ b/mayan/settings/testing/base.py @@ -2,6 +2,8 @@ from __future__ import absolute_import, unicode_literals from ..base import * # NOQA +SIGNATURES_GPG_PATH = '/usr/bin/gpg1' + INSTALLED_APPS += ('test_without_migrations',) TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.Loader', diff --git a/requirements/base.txt b/requirements/base.txt index 862c9451e6..529260ad08 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -30,7 +30,7 @@ pdfminer==20140328 pycountry==1.19 pytesseract==0.1.6 python-dateutil==2.4.2 -python-gnupg==0.3.8 +python-gnupg==0.3.9 python-magic==0.4.10 pytz==2015.4 diff --git a/requirements/common.txt b/requirements/common.txt index d55044d5a9..966a77c01f 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -1,2 +1,2 @@ -r base.txt -Django==1.8.13 +Django==1.8.15