From 553d73020dae12db428d0cace7188ddf1487edb6 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 5 Dec 2011 13:37:24 -0400 Subject: [PATCH] Add key management view to the setup menu, add keyserver query view, add keyserver key import, add respective icons --- apps/django_gpg/__init__.py | 29 +++- apps/django_gpg/api.py | 39 ++++- apps/django_gpg/exceptions.py | 3 + apps/django_gpg/forms.py | 13 ++ apps/django_gpg/static/images/icons/key.png | Bin 0 -> 1621 bytes .../static/images/icons/key_add.png | Bin 0 -> 2085 bytes .../static/images/icons/key_delete.png | Bin 0 -> 2072 bytes apps/django_gpg/urls.py | 3 + apps/django_gpg/views.py | 159 ++++++++++++++++-- 9 files changed, 229 insertions(+), 17 deletions(-) create mode 100644 apps/django_gpg/forms.py create mode 100644 apps/django_gpg/static/images/icons/key.png create mode 100644 apps/django_gpg/static/images/icons/key_add.png create mode 100644 apps/django_gpg/static/images/icons/key_delete.png diff --git a/apps/django_gpg/__init__.py b/apps/django_gpg/__init__.py index de9aeeba41..7fe12a221c 100644 --- a/apps/django_gpg/__init__.py +++ b/apps/django_gpg/__init__.py @@ -7,13 +7,40 @@ from navigation.api import register_links, register_top_menu, \ from main.api import register_diagnostic, register_maintenance_links from permissions.api import register_permission, set_namespace_title from project_setup.api import register_setup +from hkp import Key as KeyServerKey + +from django_gpg.api import Key PERMISSION_DOCUMENT_VERIFY = {'namespace': 'django_gpg', 'name': 'document_verify', 'label': _(u'Verify document signatures')} +PERMISSION_KEY_VIEW = {'namespace': 'django_gpg', 'name': 'key_view', 'label': _(u'View keys')} +PERMISSION_KEY_DELETE = {'namespace': 'django_gpg', 'name': 'key_delete', 'label': _(u'Delete keys')} +PERMISSION_KEYSERVER_QUERY = {'namespace': 'django_gpg', 'name': 'keyserver_query', 'label': _(u'Query keyservers')} +PERMISSION_KEY_RECEIVE = {'namespace': 'django_gpg', 'name': 'key_receive', 'label': _(u'Import key from keyservers')} # Permission setup set_namespace_title('django_gpg', _(u'Signatures')) register_permission(PERMISSION_DOCUMENT_VERIFY) +register_permission(PERMISSION_KEY_VIEW) +register_permission(PERMISSION_KEY_DELETE) +register_permission(PERMISSION_KEYSERVER_QUERY) +register_permission(PERMISSION_KEY_RECEIVE) -document_verify = {'text': _(u'Signatures'), 'view': 'document_verify', 'args': 'object.pk', 'famfam': 'text_signature', 'permissions': [PERMISSION_DOCUMENT_VERIFY]} +# Setup views +private_keys = {'text': _(u'private keys'), 'view': 'key_private_list', 'args': 'object.pk', 'famfam': 'key', 'icon': 'key.png', 'permissions': [PERMISSION_KEY_VIEW]} +public_keys = {'text': _(u'public keys'), 'view': 'key_public_list', 'args': 'object.pk', 'famfam': 'key', 'icon': 'key.png', 'permissions': [PERMISSION_KEY_VIEW]} +key_delete = {'text': _(u'delete'), 'view': 'key_delete', 'args': ['object.fingerprint', 'object.type'], 'famfam': 'key_delete', 'permissions': [PERMISSION_KEY_DELETE]} +key_query = {'text': _(u'Query keyservers'), 'view': 'key_query', 'famfam': 'zoom', 'permissions': [PERMISSION_KEYSERVER_QUERY]} +key_receive = {'text': _(u'Import'), 'view': 'key_receive', 'args': 'object.keyid', 'famfam': 'key_add', 'keep_query': True, 'permissions': [PERMISSION_KEY_RECEIVE]} + +# Document views +document_verify = {'text': _(u'signatures'), 'view': 'document_verify', 'args': 'object.pk', 'famfam': 'text_signature', 'permissions': [PERMISSION_DOCUMENT_VERIFY]} register_links(Document, [document_verify], menu_name='form_header') + +register_links(['key_delete', 'key_private_list', 'key_public_list', 'key_query'], [private_keys, public_keys, key_query], menu_name='sidebar') + +register_links(Key, [key_delete]) +register_links(KeyServerKey, [key_receive]) + +register_setup(private_keys) +register_setup(public_keys) diff --git a/apps/django_gpg/api.py b/apps/django_gpg/api.py index e5c6ca5f19..50ca3d564c 100644 --- a/apps/django_gpg/api.py +++ b/apps/django_gpg/api.py @@ -1,16 +1,20 @@ import types from StringIO import StringIO from pickle import dumps - -import gnupg +import logging from django.core.files.base import File from django.utils.translation import ugettext_lazy as _ +from django.utils.http import urlquote_plus -from django_gpg.exceptions import GPGVerificationError, GPGSigningError, \ - GPGDecryptionError, KeyDeleteError, KeyGenerationError, \ - KeyFetchingError, KeyDoesNotExist +from hkp import KeyServer +import gnupg +from django_gpg.exceptions import (GPGVerificationError, GPGSigningError, + GPGDecryptionError, KeyDeleteError, KeyGenerationError, + KeyFetchingError, KeyDoesNotExist, KeyImportError) + +logger = logging.getLogger(__name__) KEY_TYPES = { 'pub': _(u'Public'), @@ -31,6 +35,8 @@ KEY_SECONDARY_CLASSES = ( ((KEY_CLASS_ELG), _(u'Elgamal')), ) +KEYSERVER_DEFAULT_PORT = 11371 + SIGNATURE_STATE_BAD = 'signature bad' SIGNATURE_STATE_NONE = None SIGNATURE_STATE_ERROR = 'signature error' @@ -300,3 +306,26 @@ class GPG(object): return Key.get(self, import_result.fingerprints[0], secret=False) raise KeyFetchingError + + def query(self, term): + results = {} + for keyserver in self.keyservers: + url = u'http://%s' % keyserver + server = KeyServer(url) + try: + key_list = server.search(term) + for key in key_list: + results[key.keyid] = key + except: + pass + + 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 diff --git a/apps/django_gpg/exceptions.py b/apps/django_gpg/exceptions.py index 682ad8f795..52e62315b0 100644 --- a/apps/django_gpg/exceptions.py +++ b/apps/django_gpg/exceptions.py @@ -29,3 +29,6 @@ class KeyFetchingError(GPGException): class KeyDoesNotExist(GPGException): pass + +class KeyImportError(GPGException): + pass diff --git a/apps/django_gpg/forms.py b/apps/django_gpg/forms.py new file mode 100644 index 0000000000..619035fd5d --- /dev/null +++ b/apps/django_gpg/forms.py @@ -0,0 +1,13 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext +from django.core.urlresolvers import reverse +from django.utils.safestring import mark_safe +from django.conf import settings + + +class KeySearchForm(forms.Form): + term = forms.CharField( + label=_(u'Term'), + help_text=_(u'Name, e-mail, key ID or key fingerprint to look for.') + ) diff --git a/apps/django_gpg/static/images/icons/key.png b/apps/django_gpg/static/images/icons/key.png new file mode 100644 index 0000000000000000000000000000000000000000..3cf460e1126ce5c67b2fc0d4166eb4bc6f409e20 GIT binary patch literal 1621 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\d+)/$', 'document_verify', (), 'document_verify'), + url(r'^query/$', 'key_query', (), 'key_query'), + url(r'^receive/(?P.+)/$', 'key_receive', (), 'key_receive'), + ) diff --git a/apps/django_gpg/views.py b/apps/django_gpg/views.py index 4a173cac0a..a4f43bfbcb 100644 --- a/apps/django_gpg/views.py +++ b/apps/django_gpg/views.py @@ -1,4 +1,5 @@ from datetime import datetime +import logging from django.utils.translation import ugettext_lazy as _ from django.http import HttpResponseRedirect @@ -12,45 +13,181 @@ from django.template.defaultfilters import force_escape from documents.models import Document, RecentDocument from permissions.api import check_permissions - +from common.utils import pretty_size, parse_range, urlquote, \ + return_diff, encapsulate + from django_gpg.api import Key, SIGNATURE_STATES from django_gpg.runtime import gpg -from django_gpg.exceptions import GPGVerificationError -from django_gpg import PERMISSION_DOCUMENT_VERIFY +from django_gpg.exceptions import GPGVerificationError, KeyFetchingError +from django_gpg import (PERMISSION_DOCUMENT_VERIFY, PERMISSION_KEY_VIEW, + PERMISSION_KEY_DELETE, PERMISSION_KEYSERVER_QUERY, + PERMISSION_KEY_RECEIVE) +from django_gpg.forms import KeySearchForm +logger = logging.getLogger(__name__) + + +def key_receive(request, key_id): + check_permissions(request.user, [PERMISSION_KEY_RECEIVE]) + + post_action_redirect = None + previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) + next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', '/'))) + + if request.method == 'POST': + try: + term = request.GET.get('term') + results = gpg.query(term) + keys_dict = dict([(key.keyid, key) for key in results]) + key = gpg.import_key(keys_dict[key_id].key) + messages.success(request, _(u'Key: %s, imported successfully.') % key) + return HttpResponseRedirect(next) + except (KeyFetchingError, KeyError, TypeError): + messages.error(request, _(u'Unable to import key id: %s') % key_id) + return HttpResponseRedirect(previous) + + return render_to_response('generic_confirm.html', { + 'title': _(u'Import key'), + 'message': _(u'Are you sure you wish to import key id: %s?') % key_id, + 'form_icon': 'key_add.png', + 'next': next, + 'previous': previous, + 'submit_method': 'GET', + + }, context_instance=RequestContext(request)) + def key_list(request, secret=True): + check_permissions(request.user, [PERMISSION_KEY_VIEW]) + if secret: object_list = Key.get_all(gpg, secret=True) - title = _(u'Private key list') + title = _(u'private keys') else: object_list = Key.get_all(gpg) - title = _(u'Public key list') + title = _(u'public keys') - return render_to_response('key_list.html', { + return render_to_response('generic_list.html', { 'object_list': object_list, 'title': title, + 'hide_object': True, + 'extra_columns': [ + { + 'name': _(u'Key ID'), + 'attribute': 'key_id', + }, + { + 'name': _(u'Owner'), + 'attribute': encapsulate(lambda x: u', '.join(x.uids)), + }, + ] }, context_instance=RequestContext(request)) def key_delete(request, fingerprint, key_type): + check_permissions(request.user, [PERMISSION_KEY_DELETE]) + + secret = key_type == 'sec' + key = Key.get(gpg, fingerprint, secret=secret) + + post_action_redirect = None + previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) + next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', '/'))) + if request.method == 'POST': try: - secret = key_type == 'sec' - key = Key.get(gpg, fingerprint, secret=secret) gpg.delete_key(key) messages.success(request, _(u'Key: %s, deleted successfully.') % fingerprint) - return HttpResponseRedirect(reverse('home_view')) + return HttpResponseRedirect(next) except Exception, msg: messages.error(request, msg) - return HttpResponseRedirect(reverse('home_view')) + return HttpResponseRedirect(previous) return render_to_response('generic_confirm.html', { 'title': _(u'Delete key'), - 'message': _(u'Are you sure you wish to delete key:%s? If you try to delete a public key that is part of a public/private pair the private key will be deleted as well.') % Key.get(gpg, fingerprint) + 'delete_view': True, + 'message': _(u'Are you sure you wish to delete key: %s? If you try to delete a public key that is part of a public/private pair the private key will be deleted as well.') % key, + 'form_icon': 'key_delete.png', + 'next': next, + 'previous': previous, }, context_instance=RequestContext(request)) +def key_query(request): + check_permissions(request.user, [PERMISSION_KEYSERVER_QUERY]) + + subtemplates_list = [] + term = request.GET.get('term') + + form = KeySearchForm(initial={'term': term}) + subtemplates_list.append( + { + 'name': 'generic_form_subtemplate.html', + 'context': { + 'title': _(u'Query key server'), + 'form': form, + 'submit_method': 'GET', + }, + } + ) + + if term: + results = gpg.query(term) + subtemplates_list.append( + { + 'name': 'generic_list_subtemplate.html', + 'context': { + 'title': _(u'results'), + 'object_list': results, + 'hide_object': True, + 'extra_columns': [ + { + 'name': _(u'ID'), + 'attribute': 'keyid', + }, + { + 'name': _(u'type'), + 'attribute': 'algo', + }, + { + 'name': _(u'creation date'), + 'attribute': 'creation_date', + }, + { + 'name': _(u'disabled'), + 'attribute': 'disabled', + }, + { + 'name': _(u'expiration date'), + 'attribute': 'expiration_date', + }, + { + 'name': _(u'expired'), + 'attribute': 'expired', + }, + { + 'name': _(u'length'), + 'attribute': 'key_length', + }, + { + 'name': _(u'revoked'), + 'attribute': 'revoked', + }, + + { + 'name': _(u'Identifies'), + 'attribute': encapsulate(lambda x: u', '.join([identity.uid for identity in x.identities])), + }, + ] + }, + } + ) + + return render_to_response('generic_form.html', { + 'subtemplates_list': subtemplates_list, + }, context_instance=RequestContext(request)) + + def document_verify(request, document_pk): check_permissions(request.user, [PERMISSION_DOCUMENT_VERIFY]) document = get_object_or_404(Document, pk=document_pk)