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') + }