Update Django GPG app

Add keyword arguments to all calls. Rename URL parameters to be
explicit ("key_id"). Add key delete view test. Update tests
to use a mixin for repeated key creation code. Grant permissions
and access the proper way using self.grant_permission and
self.grant_access.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
This commit is contained in:
Roberto Rosario
2019-01-20 18:08:47 -04:00
parent 14fd5f02a8
commit fc29309f68
16 changed files with 168 additions and 123 deletions

View File

@@ -31,6 +31,8 @@ class APIKeyView(generics.RetrieveDestroyAPIView):
get: Return the details of the selected key.
"""
filter_backends = (MayanObjectPermissionsFilter,)
lookup_field = 'pk'
lookup_url_kwarg = 'key_id'
mayan_object_permissions = {
'DELETE': (permission_key_delete,),
'GET': (permission_key_view,),

View File

@@ -17,6 +17,24 @@ class GPGBackend(object):
self.kwargs = kwargs
class KeyStub(object):
def __init__(self, raw):
self.fingerprint = raw['keyid']
self.key_type = raw['type']
self.date = date.fromtimestamp(int(raw['date']))
if raw['expires']:
self.expires = date.fromtimestamp(int(raw['expires']))
else:
self.expires = None
self.length = raw['length']
self.user_id = raw['uids']
@property
def key_id(self):
return self.fingerprint[-8:]
key_id.fget.short_description = _('Key ID')
class PythonGNUPGBackend(GPGBackend):
@staticmethod
def _import_key(gpg, **kwargs):
@@ -136,24 +154,6 @@ class PythonGNUPGBackend(GPGBackend):
)
class KeyStub(object):
def __init__(self, raw):
self.fingerprint = raw['keyid']
self.key_type = raw['type']
self.date = date.fromtimestamp(int(raw['date']))
if raw['expires']:
self.expires = date.fromtimestamp(int(raw['expires']))
else:
self.expires = None
self.length = raw['length']
self.user_id = raw['uids']
@property
def key_id(self):
return self.fingerprint[-8:]
key_id.fget.short_description = _('Key ID')
class SignatureVerification(object):
def __init__(self, raw):
self.user_id = raw['username']

View File

@@ -44,6 +44,6 @@ class KeyDetailForm(DetailForm):
class KeySearchForm(forms.Form):
term = forms.CharField(
label=_('Term'),
help_text=_('Name, e-mail, key ID or key fingerprint to look for.')
help_text=_('Name, e-mail, key ID or key fingerprint to look for.'),
label=_('Term')
)

View File

@@ -11,16 +11,18 @@ from .permissions import (
)
link_key_delete = Link(
args=('resolved_object.pk',), permissions=(permission_key_delete,),
tags='dangerous', text=_('Delete'), view='django_gpg:key_delete',
kwargs={'key_id': 'resolved_object.pk'},
permissions=(permission_key_delete,), tags='dangerous', text=_('Delete'),
view='django_gpg:key_delete'
)
link_key_detail = Link(
args=('resolved_object.pk',), permissions=(permission_key_view,),
text=_('Details'), view='django_gpg:key_detail',
kwargs={'key_id': 'resolved_object.pk'}, permissions=(permission_key_view,),
text=_('Details'), view='django_gpg:key_detail'
)
link_key_download = Link(
args=('resolved_object.pk',), permissions=(permission_key_download,),
text=_('Download'), view='django_gpg:key_download',
kwargs={'key_id': 'resolved_object.pk'},
permissions=(permission_key_download,), text=_('Download'),
view='django_gpg:key_download'
)
link_key_query = Link(
icon_class=icon_keyserver_search,
@@ -28,7 +30,7 @@ link_key_query = Link(
view='django_gpg:key_query'
)
link_key_receive = Link(
args='object.key_id', keep_query=True,
keep_query=True, kwargs={'key_id': 'object.key_id'},
permissions=(permission_key_receive,), text=_('Import'),
view='django_gpg:key_receive',
)

View File

@@ -9,6 +9,7 @@ if platform.system() == 'OpenBSD':
else:
DEFAULT_GPG_PATH = '/usr/bin/gpg1'
DEFAULT_KEYSERVER = 'pool.sks-keyservers.net'
DEFAULT_SETTING_GPG_BACKEND = 'mayan.apps.django_gpg.classes.PythonGNUPGBackend'
ERROR_MSG_BAD_PASSPHRASE = 'BAD_PASSPHRASE'

View File

@@ -77,7 +77,9 @@ class Key(models.Model):
raise ValidationError(_('Key already exists.'))
def get_absolute_url(self):
return reverse('django_gpg:key_detail', args=(self.pk,))
return reverse(
viewname='django_gpg:key_detail', kwargs={'key_pk': self.pk}
)
@property
def key_id(self):

View File

@@ -7,23 +7,23 @@ from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Key management'), name='django_gpg')
permission_key_delete = namespace.add_permission(
name='key_delete', label=_('Delete keys')
label=_('Delete keys'), name='key_delete'
)
permission_key_download = namespace.add_permission(
name='key_download', label=_('Download keys')
label=_('Download keys'), name='key_download'
)
permission_key_receive = namespace.add_permission(
name='key_receive', label=_('Import keys from keyservers')
label=_('Import keys from keyservers'), name='key_receive'
)
permission_key_sign = namespace.add_permission(
name='key_sign', label=_('Use keys to sign content')
label=_('Use keys to sign content'), name='key_sign'
)
permission_key_upload = namespace.add_permission(
name='key_upload', label=_('Upload keys')
label=_('Upload keys'), name='key_upload'
)
permission_key_view = namespace.add_permission(
name='key_view', label=_('View keys')
label=_('View keys'), name='key_view'
)
permission_keyserver_query = namespace.add_permission(
name='keyserver_query', label=_('Query keyservers')
label=_('Query keyservers'), name='keyserver_query'
)

View File

@@ -8,6 +8,7 @@ from .models import Key
class KeySerializer(serializers.ModelSerializer):
class Meta:
extra_kwargs = {
'lookup_url_kwarg': 'key_id',
'url': {'view_name': 'rest_api:key-detail'},
}
fields = (

View File

@@ -4,7 +4,9 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_GPG_PATH, DEFAULT_SETTING_GPG_BACKEND
from .literals import (
DEFAULT_GPG_PATH, DEFAULT_KEYSERVER, DEFAULT_SETTING_GPG_BACKEND
)
namespace = Namespace(name='django_gpg', label=_('Signatures'))
@@ -23,6 +25,6 @@ setting_gpg_backend_arguments = namespace.add_setting(
)
)
setting_keyserver = namespace.add_setting(
global_name='SIGNATURES_KEYSERVER', default='pool.sks-keyservers.net',
global_name='SIGNATURES_KEYSERVER', default=DEFAULT_KEYSERVER,
help_text=_('Keyserver used to query for keys.')
)

View File

@@ -4,6 +4,18 @@ import os
from django.conf import settings
MOCK_SEARCH_KEYS_RESPONSE = [
{
'algo': u'1',
'date': u'1311475606',
'expires': u'1643601600',
'keyid': u'607138F1AECC5A5CA31CB7715F3F7F75D210724D',
'length': u'2048',
'type': u'pub',
'uids': [u'Roberto Rosario <roberto.rosario.gonzalez@gmail.com>']
}
]
TEST_DETACHED_SIGNATURE = os.path.join(
settings.BASE_DIR, 'apps', 'django_gpg', 'tests', 'contrib',
'test_files', 'test_file.txt.asc'

View File

@@ -0,0 +1,9 @@
from ..models import Key
from .literals import TEST_KEY_DATA
class KeyTestMixin(object):
def _create_test_key(self):
# Creating a Key instance is analogous to importing a key
self.test_key = Key.objects.create(key_data=TEST_KEY_DATA)

View File

@@ -32,7 +32,7 @@ class KeyAPITestCase(BaseAPITestCase):
def test_key_create_view_no_permission(self):
response = self._request_key_create_view()
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(Key.objects.all().count(), 0)
self.assertEqual(Key.objects.count(), 0)
def test_key_create_view_with_permission(self):
self.grant_permission(permission=permission_key_upload)
@@ -50,7 +50,7 @@ class KeyAPITestCase(BaseAPITestCase):
def _request_key_delete_view(self):
return self.delete(
viewname='rest_api:key-detail', args=(self.key.pk,)
viewname='rest_api:key-detail', kwargs={'key_id': self.key.pk}
)
def test_key_delete_view_no_access(self):
@@ -72,7 +72,7 @@ class KeyAPITestCase(BaseAPITestCase):
def _request_key_detail_view(self):
return self.get(
viewname='rest_api:key-detail', args=(self.key.pk,)
viewname='rest_api:key-detail', kwargs={'key_id': self.key.pk}
)
def test_key_detail_view_no_access(self):

View File

@@ -17,22 +17,12 @@ from ..exceptions import (
from ..models import Key
from .literals import (
TEST_DETACHED_SIGNATURE, TEST_FILE, TEST_KEY_DATA, TEST_KEY_FINGERPRINT,
TEST_KEY_PASSPHRASE, TEST_RECEIVE_KEY, TEST_SEARCH_FINGERPRINT,
TEST_SEARCH_UID, TEST_SIGNED_FILE, TEST_SIGNED_FILE_CONTENT
MOCK_SEARCH_KEYS_RESPONSE, TEST_DETACHED_SIGNATURE, TEST_FILE,
TEST_KEY_FINGERPRINT, TEST_KEY_PASSPHRASE, TEST_RECEIVE_KEY,
TEST_SEARCH_FINGERPRINT, TEST_SEARCH_UID, TEST_SIGNED_FILE,
TEST_SIGNED_FILE_CONTENT
)
MOCK_SEARCH_KEYS_RESPONSE = [
{
'algo': u'1',
'date': u'1311475606',
'expires': u'1643601600',
'keyid': u'607138F1AECC5A5CA31CB7715F3F7F75D210724D',
'length': u'2048',
'type': u'pub',
'uids': [u'Roberto Rosario <roberto.rosario.gonzalez@gmail.com>']
}
]
from .mixins import KeyTestMixin
def mock_recv_keys(self, keyserver, *keyids):
@@ -45,12 +35,11 @@ def mock_recv_keys(self, keyserver, *keyids):
return ImportResult()
class KeyTestCase(BaseTestCase):
class KeyTestCase(KeyTestMixin, BaseTestCase):
def test_key_instance_creation(self):
# Creating a Key instance is analogous to importing a key
key = Key.objects.create(key_data=TEST_KEY_DATA)
self._create_test_key()
self.assertEqual(key.fingerprint, TEST_KEY_FINGERPRINT)
self.assertEqual(self.test_key.fingerprint, TEST_KEY_FINGERPRINT)
@mock.patch.object(gnupg.GPG, 'search_keys', autospec=True)
def test_key_search(self, search_keys):
@@ -92,7 +81,7 @@ class KeyTestCase(BaseTestCase):
self.assertTrue(result.key_id in TEST_KEY_FINGERPRINT)
def test_embedded_verification_with_key(self):
Key.objects.create(key_data=TEST_KEY_DATA)
self._create_test_key()
with open(TEST_SIGNED_FILE, mode='rb') as signed_file:
result = Key.objects.verify_file(signed_file)
@@ -100,7 +89,7 @@ class KeyTestCase(BaseTestCase):
self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT)
def test_embedded_verification_with_correct_fingerprint(self):
Key.objects.create(key_data=TEST_KEY_DATA)
self._create_test_key()
with open(TEST_SIGNED_FILE, mode='rb') as signed_file:
result = Key.objects.verify_file(
@@ -111,14 +100,14 @@ class KeyTestCase(BaseTestCase):
self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT)
def test_embedded_verification_with_incorrect_fingerprint(self):
Key.objects.create(key_data=TEST_KEY_DATA)
self._create_test_key()
with open(TEST_SIGNED_FILE, mode='rb') as signed_file:
with self.assertRaises(KeyDoesNotExist):
Key.objects.verify_file(signed_file, key_fingerprint='999')
def test_signed_file_decryption(self):
Key.objects.create(key_data=TEST_KEY_DATA)
self._create_test_key()
with open(TEST_SIGNED_FILE, mode='rb') as signed_file:
result = Key.objects.decrypt_file(file_object=signed_file)
@@ -145,7 +134,7 @@ class KeyTestCase(BaseTestCase):
self.assertTrue(result.key_id in TEST_KEY_FINGERPRINT)
def test_detached_verification_with_key(self):
Key.objects.create(key_data=TEST_KEY_DATA)
self._create_test_key()
with open(TEST_DETACHED_SIGNATURE, mode='rb') as signature_file:
with open(TEST_FILE, mode='rb') as test_file:
@@ -157,29 +146,29 @@ class KeyTestCase(BaseTestCase):
self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT)
def test_detached_signing_no_passphrase(self):
key = Key.objects.create(key_data=TEST_KEY_DATA)
self._create_test_key()
with self.assertRaises(NeedPassphrase):
with open(TEST_FILE, mode='rb') as test_file:
key.sign_file(
self.test_key.sign_file(
file_object=test_file, detached=True,
)
def test_detached_signing_bad_passphrase(self):
key = Key.objects.create(key_data=TEST_KEY_DATA)
self._create_test_key()
with self.assertRaises(PassphraseError):
with open(TEST_FILE, mode='rb') as test_file:
key.sign_file(
self.test_key.sign_file(
file_object=test_file, detached=True,
passphrase='bad passphrase'
)
def test_detached_signing_with_passphrase(self):
key = Key.objects.create(key_data=TEST_KEY_DATA)
self._create_test_key()
with open(TEST_FILE, mode='rb') as test_file:
detached_signature = key.sign_file(
detached_signature = self.test_key.sign_file(
file_object=test_file, detached=True,
passphrase=TEST_KEY_PASSPHRASE
)

View File

@@ -5,62 +5,79 @@ from django_downloadview.test import assert_download_response
from mayan.apps.common.tests import GenericViewTestCase
from ..models import Key
from ..permissions import permission_key_download, permission_key_upload
from ..permissions import (
permission_key_delete, permission_key_download, permission_key_upload
)
from .literals import TEST_KEY_DATA, TEST_KEY_FINGERPRINT
from .mixins import KeyTestMixin
class KeyViewTestCase(GenericViewTestCase):
def test_key_download_view_no_permission(self):
key = Key.objects.create(key_data=TEST_KEY_DATA)
self.login_user()
response = self.get(
viewname='django_gpg:key_download', args=(key.pk,)
class KeyViewTestCase(KeyTestMixin, GenericViewTestCase):
def _request_key_delete_view(self):
return self.post(
viewname='django_gpg:key_delete',
kwargs={'key_id': self.test_key.pk}
)
self.assertEqual(response.status_code, 403)
def test_key_delete_view_no_permission(self):
self._create_test_key()
response = self._request_key_delete_view()
self.assertEqual(response.status_code, 404)
def test_key_delete_view_with_access(self):
self._create_test_key()
self.grant_access(obj=self.test_key, permission=permission_key_delete)
response = self._request_key_delete_view()
self.assertEqual(response.status_code, 302)
self.assertEqual(Key.objects.count(), 0)
def _request_key_download_view(self):
return self.get(
viewname='django_gpg:key_download',
kwargs={'key_id': self.test_key.pk}
)
def test_key_download_view_no_permission(self):
self._create_test_key()
response = self._request_key_download_view()
self.assertEqual(response.status_code, 404)
def test_key_download_view_with_permission(self):
key = Key.objects.create(key_data=TEST_KEY_DATA)
self._create_test_key()
self.login_user()
self.role.permissions.add(permission_key_download.stored_permission)
self.grant_access(obj=self.test_key, permission=permission_key_download)
self.expected_content_type = 'application/octet-stream; charset=utf-8'
response = self.get(
viewname='django_gpg:key_download', args=(key.pk,)
)
response = self._request_key_download_view()
assert_download_response(
self, response=response, content=key.key_data,
basename=key.key_id,
test_case=self, response=response, content=self.test_key.key_data,
basename=self.test_key.key_id,
)
def test_key_upload_view_no_permission(self):
self.login_user()
response = self.post(
def _request_key_upload_view(self):
return self.post(
viewname='django_gpg:key_upload', data={'key_data': TEST_KEY_DATA}
)
def test_key_upload_view_no_permission(self):
response = self._request_key_upload_view()
self.assertEqual(response.status_code, 403)
self.assertEqual(Key.objects.count(), 0)
def test_key_upload_view_with_permission(self):
self.login_user()
self.grant_permission(permission=permission_key_upload)
self.role.permissions.add(permission_key_upload.stored_permission)
response = self.post(
viewname='django_gpg:key_upload', data={'key_data': TEST_KEY_DATA},
follow=True
)
self.assertContains(response, 'created', status_code=200)
response = self._request_key_upload_view()
self.assertEqual(response.status_code, 302)
self.assertEqual(Key.objects.count(), 1)
self.assertEqual(Key.objects.first().fingerprint, TEST_KEY_FINGERPRINT)

View File

@@ -11,39 +11,44 @@ from .views import (
urlpatterns = [
url(
r'^(?P<pk>\d+)/$', KeyDetailView.as_view(), name='key_detail'
regex=r'^keys/(?P<key_id>\d+)/$', name='key_detail',
view=KeyDetailView.as_view()
),
url(
r'^(?P<pk>\d+)/delete/$', KeyDeleteView.as_view(), name='key_delete'
regex=r'^keys/(?P<key_id>\d+)/delete/$', name='key_delete',
view=KeyDeleteView.as_view()
),
url(
r'^(?P<pk>\d+)/download/$', KeyDownloadView.as_view(),
name='key_download'
regex=r'^keys/(?P<key_id>\d+)/download/$', name='key_download',
view=KeyDownloadView.as_view()
),
url(
r'^list/private/$', PrivateKeyListView.as_view(),
name='key_private_list'
regex=r'^keys/private/$', name='key_private_list',
view=PrivateKeyListView.as_view()
),
url(
r'^list/public/$', PublicKeyListView.as_view(), name='key_public_list'
regex=r'^keys/public/$', name='key_public_list',
view=PublicKeyListView.as_view()
),
url(
r'^upload/$', KeyUploadView.as_view(), name='key_upload'
regex=r'^keys/upload/$', name='key_upload',
view=KeyUploadView.as_view()
),
url(r'^query/$', KeyQueryView.as_view(), name='key_query'),
url(regex=r'^keys/query/$', name='key_query', view=KeyQueryView.as_view()),
url(
r'^query/results/$', KeyQueryResultView.as_view(),
name='key_query_results'
regex=r'^keys/query/results/$', name='key_query_results',
view=KeyQueryResultView.as_view()
),
url(
r'^receive/(?P<key_id>.+)/$', KeyReceive.as_view(), name='key_receive'
),
regex=r'^keys/receive/(?P<key_id>.+)/$', name='key_receive',
view=KeyReceive.as_view()
)
]
api_urls = [
url(
r'^keys/(?P<pk>[0-9]+)/$', APIKeyView.as_view(),
name='key-detail'
regex=r'^keys/(?P<key_id>\d+)/$', name='key-detail',
view=APIKeyView.as_view()
),
url(r'^keys/$', APIKeyListView.as_view(), name='key-list'),
url(regex=r'^keys/$', name='key-list', view=APIKeyListView.as_view())
]

View File

@@ -30,12 +30,13 @@ logger = logging.getLogger(__name__)
class KeyDeleteView(SingleObjectDeleteView):
model = Key
object_permission = permission_key_delete
pk_url_kwarg = 'key_id'
def get_post_action_redirect(self):
if self.get_object().key_type == KEY_TYPE_PUBLIC:
return reverse_lazy('django_gpg:key_public_list')
return reverse_lazy(viewname='django_gpg:key_public_list')
else:
return reverse_lazy('django_gpg:key_private_list')
return reverse_lazy(viewname='django_gpg:key_private_list')
def get_extra_context(self):
return {'title': _('Delete key: %s') % self.get_object()}
@@ -45,6 +46,7 @@ class KeyDetailView(SingleObjectDetailView):
form_class = KeyDetailForm
model = Key
object_permission = permission_key_view
pk_url_kwarg = 'key_id'
def get_extra_context(self):
return {
@@ -55,6 +57,7 @@ class KeyDetailView(SingleObjectDetailView):
class KeyDownloadView(SingleObjectDownloadView):
model = Key
object_permission = permission_key_download
pk_url_kwarg = 'key_id'
def get_file(self):
key = self.get_object()
@@ -63,7 +66,7 @@ class KeyDownloadView(SingleObjectDownloadView):
class KeyReceive(ConfirmView):
post_action_redirect = reverse_lazy('django_gpg:key_public_list')
post_action_redirect = reverse_lazy(viewname='django_gpg:key_public_list')
view_permission = permission_key_receive
def get_extra_context(self):
@@ -105,7 +108,7 @@ class KeyQueryView(SimpleView):
def get_extra_context(self):
return {
'form': self.get_form(),
'form_action': reverse('django_gpg:key_query_results'),
'form_action': reverse(viewname='django_gpg:key_query_results'),
'submit_icon_class': icon_keyserver_search,
'submit_label': _('Search'),
'submit_method': 'GET',
@@ -144,7 +147,7 @@ class KeyQueryResultView(SingleObjectListView):
class KeyUploadView(SingleObjectCreateView):
fields = ('key_data',)
model = Key
post_action_redirect = reverse_lazy('django_gpg:key_public_list')
post_action_redirect = reverse_lazy(viewname='django_gpg:key_public_list')
view_permission = permission_key_upload
def get_extra_context(self):