diff --git a/apps/document_signatures/__init__.py b/apps/document_signatures/__init__.py new file mode 100644 index 0000000000..11edeb1d07 --- /dev/null +++ b/apps/document_signatures/__init__.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import + +from django.utils.translation import ugettext_lazy as _ + +from documents.models import Document +from navigation.api import register_links, register_top_menu, \ + register_model_list_columns, register_multi_item_links, \ + register_sidebar_template +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 django_gpg.api import Key + +PERMISSION_DOCUMENT_VERIFY = {'namespace': 'document_signatures', 'name': 'document_verify', 'label': _(u'Verify document signatures')} +PERMISSION_SIGNATURE_UPLOAD = {'namespace': 'document_signatures', 'name': 'signature_upload', 'label': _(u'Upload detached signatures')} +PERMISSION_SIGNATURE_DOWNLOAD = {'namespace': 'document_signatures', 'name': 'key_receive', 'label': _(u'Download detached signatures')} + +# Permission setup +set_namespace_title('document_signatures', _(u'Document signatures')) +register_permission(PERMISSION_DOCUMENT_VERIFY) +register_permission(PERMISSION_SIGNATURE_UPLOAD) +register_permission(PERMISSION_SIGNATURE_DOWNLOAD) + +def has_embedded_signature(context): + return context['object'].signature_state + +def doesnt_have_detached_signature(context): + return context['object'].has_detached_signature() == False + +document_signature_upload = {'text': _(u'upload signature'), 'view': 'document_signature_upload', 'args': 'object.pk', 'famfam': 'pencil_add', 'permissions': [PERMISSION_SIGNATURE_UPLOAD], 'conditional_disable': has_embedded_signature} +document_signature_download = {'text': _(u'download signature'), 'view': 'document_signature_download', 'args': 'object.pk', 'famfam': 'disk', 'permissions': [PERMISSION_SIGNATURE_DOWNLOAD], 'conditional_disable': doesnt_have_detached_signature} +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(['document_verify', 'document_signature_upload', 'document_signature_download'], [document_signature_upload, document_signature_download], menu_name='sidebar') diff --git a/apps/document_signatures/forms.py b/apps/document_signatures/forms.py new file mode 100644 index 0000000000..7e3f568b1b --- /dev/null +++ b/apps/document_signatures/forms.py @@ -0,0 +1,12 @@ +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 DetachedSignatureForm(forms.Form): + file = forms.FileField( + label=_(u'Signature file'), + ) diff --git a/apps/document_signatures/models.py b/apps/document_signatures/models.py new file mode 100644 index 0000000000..9086d3d7ce --- /dev/null +++ b/apps/document_signatures/models.py @@ -0,0 +1,58 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from documents.models import DocumentVersion, get_filename_from_uuid +from documents.conf.settings import STORAGE_BACKEND + + +class DocumentVersionSignature(models.Model): + ''' + Model that describes a document version signature properties + ''' + document_version = models.ForeignKey(DocumentVersion, verbose_name=_(u'document version'), editable=False) + signature_state = models.CharField(blank=True, null=True, max_length=16, verbose_name=_(u'signature state'), editable=False) + signature_file = models.FileField(blank=True, null=True, upload_to=get_filename_from_uuid, storage=STORAGE_BACKEND(), verbose_name=_(u'signature file'), editable=False) + + def update_signed_state(self, save=True): + if self.exists(): + try: + self.signature_state = gpg.verify_file(self.open()).status + # TODO: give use choice for auto public key fetch? + # OR maybe new config option + except GPGVerificationError: + self.signature_state = None + + if save: + self.save() + + def add_detached_signature(self, detached_signature): + if not self.signature_state: + self.signature_file = detached_signature + self.save() + else: + raise Exception('document already has an embedded signature') + + def has_detached_signature(self): + if self.signature_file: + return self.signature_file.storage.exists(self.signature_file.path) + else: + return False + + def detached_signature(self): + return self.signature_file.storage.open(self.signature_file.path) + + def verify_signature(self): + try: + if self.has_detached_signature(): + logger.debug('has detached signature') + signature = gpg.verify_w_retry(self.open(), self.detached_signature()) + else: + signature = gpg.verify_w_retry(self.open(raw=True)) + except GPGVerificationError: + signature = None + + return signature + + class Meta: + verbose_name = _(u'document version signature') + verbose_name_plural = _(u'document version signatures') diff --git a/apps/document_signatures/tests.py b/apps/document_signatures/tests.py new file mode 100644 index 0000000000..501deb776c --- /dev/null +++ b/apps/document_signatures/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/apps/document_signatures/urls.py b/apps/document_signatures/urls.py new file mode 100644 index 0000000000..37d857aa40 --- /dev/null +++ b/apps/document_signatures/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import patterns, url + +urlpatterns = patterns('document_signatures.views', + url(r'^verify/(?P\d+)/$', 'document_verify', (), 'document_verify'), + url(r'^upload/signature/(?P\d+)/$', 'document_signature_upload', (), 'document_signature_upload'), + url(r'^download/signature/(?P\d+)/$', 'document_signature_download', (), 'document_signature_download'), +) diff --git a/apps/document_signatures/views.py b/apps/document_signatures/views.py new file mode 100644 index 0000000000..02b33d8847 --- /dev/null +++ b/apps/document_signatures/views.py @@ -0,0 +1,127 @@ +from __future__ import absolute_import + +from datetime import datetime +import logging + +from django.utils.translation import ugettext_lazy as _ +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response, get_object_or_404 +from django.template import RequestContext +from django.contrib import messages +from django.core.urlresolvers import reverse +from django.utils.safestring import mark_safe +from django.conf import settings +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 filetransfers.api import serve_file + +from django_gpg.api import Key, SIGNATURE_STATES +from django_gpg.runtime import gpg +from django_gpg.exceptions import (GPGVerificationError, KeyFetchingError, + KeyImportError) + +from . import (PERMISSION_DOCUMENT_VERIFY, PERMISSION_SIGNATURE_UPLOAD, + PERMISSION_SIGNATURE_DOWNLOAD) +from .forms import DetachedSignatureForm +from .models import DocumentVersionSignature + +logger = logging.getLogger(__name__) + + +def document_verify(request, document_pk): + check_permissions(request.user, [PERMISSION_DOCUMENT_VERIFY]) + document = get_object_or_404(Document, pk=document_pk) + + RecentDocument.objects.add_document_for_user(request.user, document) + + signature = document.verify_signature() + + signature_state = SIGNATURE_STATES.get(getattr(signature, 'status', None)) + + widget = (u'' % (settings.STATIC_URL, signature_state['icon'])) + paragraphs = [ + _(u'Signature status: %(widget)s %(text)s') % { + 'widget': mark_safe(widget), + 'text': signature_state['text'] + }, + ] + + if document.signature_state: + signature_type = _(u'embedded') + else: + signature_type = _(u'detached') + + if signature: + paragraphs.extend( + [ + _(u'Signature ID: %s') % signature.signature_id, + _(u'Signature type: %s') % signature_type, + _(u'Key ID: %s') % signature.key_id, + _(u'Timestamp: %s') % datetime.fromtimestamp(int(signature.sig_timestamp)), + _(u'Signee: %s') % force_escape(getattr(signature, 'username', u'')), + ] + ) + + return render_to_response('generic_template.html', { + 'title': _(u'signature properties for: %s') % document, + 'object': document, + 'document': document, + 'paragraphs': paragraphs, + }, context_instance=RequestContext(request)) + + +def document_signature_upload(request, document_pk): + check_permissions(request.user, [PERMISSION_SIGNATURE_UPLOAD]) + document = get_object_or_404(Document, pk=document_pk) + + RecentDocument.objects.add_document_for_user(request.user, document) + + 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': + form = DetachedSignatureForm(request.POST, request.FILES) + if form.is_valid(): + try: + document.add_detached_signature(request.FILES['file']) + messages.success(request, _(u'Detached signature uploaded successfully.')) + return HttpResponseRedirect(next) + except Exception, msg: + messages.error(request, msg) + return HttpResponseRedirect(previous) + else: + form = DetachedSignatureForm() + + return render_to_response('generic_form.html', { + 'title': _(u'Upload detached signature for: %s') % document, + 'form_icon': 'key_delete.png', + 'next': next, + 'form': form, + 'previous': previous, + 'object': document, + }, context_instance=RequestContext(request)) + + +def document_signature_download(request, document_pk): + check_permissions(request.user, [PERMISSION_SIGNATURE_DOWNLOAD]) + document = get_object_or_404(Document, pk=document_pk) + + try: + if document.has_detached_signature(): + signature = document.detached_signature() + return serve_file( + request, + signature, + save_as=u'"%s.sig"' % document.filename, + content_type=u'application/octet-stream' + ) + except Exception, e: + messages.error(request, e) + return HttpResponseRedirect(request.META['HTTP_REFERER']) + + return HttpResponseRedirect(request.META['HTTP_REFERER'])