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 0000000000..3cf460e112 Binary files /dev/null and b/apps/django_gpg/static/images/icons/key.png differ diff --git a/apps/django_gpg/static/images/icons/key_add.png b/apps/django_gpg/static/images/icons/key_add.png new file mode 100644 index 0000000000..da22eb0570 Binary files /dev/null and b/apps/django_gpg/static/images/icons/key_add.png differ diff --git a/apps/django_gpg/static/images/icons/key_delete.png b/apps/django_gpg/static/images/icons/key_delete.png new file mode 100644 index 0000000000..9462a1272d Binary files /dev/null and b/apps/django_gpg/static/images/icons/key_delete.png differ diff --git a/apps/django_gpg/urls.py b/apps/django_gpg/urls.py index d274237795..20c4c69360 100644 --- a/apps/django_gpg/urls.py +++ b/apps/django_gpg/urls.py @@ -5,4 +5,7 @@ urlpatterns = patterns('django_gpg.views', url(r'^list/private/$', 'key_list', {'secret': True}, 'key_private_list'), url(r'^list/public/$', 'key_list', {'secret': False}, 'key_public_list'), url(r'^verify/(?P\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)