From 299eb23fa4a7917d1e8818c5516df9e0cb8e21b5 Mon Sep 17 00:00:00 2001 From: Kaitlin Farr Date: Tue, 22 Aug 2017 16:31:54 -0400 Subject: [PATCH] Add Symmetric Keys Panel Change-Id: Id423dbfd30990b8c4240b8008b6c659da38f91fd --- README.rst | 1 + .../content/symmetric_keys/__init__.py | 0 castellan_ui/content/symmetric_keys/forms.py | 96 +++++++++++++ castellan_ui/content/symmetric_keys/panel.py | 23 +++ castellan_ui/content/symmetric_keys/tables.py | 96 +++++++++++++ castellan_ui/content/symmetric_keys/urls.py | 27 ++++ castellan_ui/content/symmetric_keys/views.py | 133 +++++++++++++++++ ...roject_key_manager_symmetric_keys_panel.py | 23 +++ .../templates/_symmetric_key_generate.html | 7 + .../templates/_symmetric_key_import.html | 9 ++ .../templates/import_symmetric_key.html | 7 + .../templates/symmetric_key_detail.html | 27 ++++ .../templates/symmetric_key_generate.html | 7 + .../templates/symmetric_key_import.html | 7 + castellan_ui/templates/symmetric_keys.html | 23 +++ .../test/content/symmetric_keys/__init__.py | 0 .../test/content/symmetric_keys/tests.py | 136 ++++++++++++++++++ castellan_ui/test/test_data.py | 16 +++ tox.ini | 2 +- 19 files changed, 639 insertions(+), 1 deletion(-) create mode 100644 castellan_ui/content/symmetric_keys/__init__.py create mode 100644 castellan_ui/content/symmetric_keys/forms.py create mode 100644 castellan_ui/content/symmetric_keys/panel.py create mode 100644 castellan_ui/content/symmetric_keys/tables.py create mode 100644 castellan_ui/content/symmetric_keys/urls.py create mode 100644 castellan_ui/content/symmetric_keys/views.py create mode 100644 castellan_ui/enabled/_94_project_key_manager_symmetric_keys_panel.py create mode 100644 castellan_ui/templates/_symmetric_key_generate.html create mode 100644 castellan_ui/templates/_symmetric_key_import.html create mode 100644 castellan_ui/templates/import_symmetric_key.html create mode 100644 castellan_ui/templates/symmetric_key_detail.html create mode 100644 castellan_ui/templates/symmetric_key_generate.html create mode 100644 castellan_ui/templates/symmetric_key_import.html create mode 100644 castellan_ui/templates/symmetric_keys.html create mode 100644 castellan_ui/test/content/symmetric_keys/__init__.py create mode 100644 castellan_ui/test/content/symmetric_keys/tests.py diff --git a/README.rst b/README.rst index 060bd19..e747721 100644 --- a/README.rst +++ b/README.rst @@ -57,6 +57,7 @@ And enable it in Horizon:: ln -s ../castellan-ui/castellan_ui/enabled/_91_project_key_manager_x509_certificates_panel.py openstack_dashboard/local/enabled ln -s ../castellan-ui/castellan_ui/enabled/_92_project_key_manager_private_key_panel.py openstack_dashboard/local/enabled ln -s ../castellan-ui/castellan_ui/enabled/_93_project_key_manager_public_key_panel.py openstack_dashboard/local/enabled + ln -s ../castellan-ui/castellan_ui/enabled/_93_project_key_manager_symmetric_key_panel.py openstack_dashboard/local/enabled To run horizon with the newly enabled Castellan UI plugin run:: diff --git a/castellan_ui/content/symmetric_keys/__init__.py b/castellan_ui/content/symmetric_keys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/castellan_ui/content/symmetric_keys/forms.py b/castellan_ui/content/symmetric_keys/forms.py new file mode 100644 index 0000000..ede157f --- /dev/null +++ b/castellan_ui/content/symmetric_keys/forms.py @@ -0,0 +1,96 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import base64 +import binascii +from django.utils.translation import ugettext_lazy as _ + +from castellan.common.objects import symmetric_key +from horizon import exceptions +from horizon import forms +from horizon import messages + +from castellan_ui.api import client +from castellan_ui.content import shared_forms + +ALGORITHMS = ('AES', 'DES', 'DESEDE') + +ALG_HELP_TEXT = _( + "Check which algorithms your key manager supports. " + "Some common algorithms are: %s") % ', '.join(ALGORITHMS) +LENGTH_HELP_TEXT = _( + "Only certain bit lengths are valid for each algorithm. " + "Some common bit lengths are: 128, 256") + + +class ImportSymmetricKey(shared_forms.ImportKey): + + def __init__(self, request, *args, **kwargs): + super(ImportSymmetricKey, self).__init__( + request, *args, algorithms=ALGORITHMS, **kwargs) + self.fields['direct_input'].help_text = _( + "Key bytes represented as hex characters. Acceptable values are " + "0-9, a-f, A-F") + self.fields['key_file'].help_text = _( + "The file should contain the raw bytes of the key.") + + def clean_key_data(self, key_data): + if self.files.get('key_file'): + key_bytes = key_data + else: + key_bytes = binascii.unhexlify(key_data) + b64_key_data = base64.b64encode(key_bytes) + + return b64_key_data + + def handle(self, request, data): + return super(ImportSymmetricKey, self).handle( + request, data, symmetric_key.SymmetricKey) + + +class GenerateSymmetricKey(forms.SelfHandlingForm): + algorithm = forms.CharField(label=_("Algorithm"), + widget=shared_forms.ListTextWidget( + data_list=ALGORITHMS, + name='algorithm-list'), + help_text=ALG_HELP_TEXT) + length = forms.IntegerField(label=_("Bit Length"), min_value=0, + help_text=LENGTH_HELP_TEXT) + name = forms.RegexField(required=False, + max_length=255, + label=_("Key Name"), + regex=shared_forms.NAME_REGEX, + error_messages=shared_forms.ERROR_MESSAGES) + + def handle(self, request, data): + try: + key_uuid = client.generate_symmetric_key( + request, + algorithm=data['algorithm'], + length=data['length'], + name=data['name']) + + if data['name']: + key_identifier = data['name'] + else: + key_identifier = key_uuid + messages.success(request, + _('Successfully generated symmetric key: %s') + % key_identifier) + return key_uuid + except Exception as e: + msg = _('Unable to generate symmetric key: %s') + messages.error(request, msg % e) + exceptions.handle(request, ignore=True) + self.api_error(_('Unable to generate symmetric key.')) + return False diff --git a/castellan_ui/content/symmetric_keys/panel.py b/castellan_ui/content/symmetric_keys/panel.py new file mode 100644 index 0000000..6b879e3 --- /dev/null +++ b/castellan_ui/content/symmetric_keys/panel.py @@ -0,0 +1,23 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext_lazy as _ +import horizon + +# This panel will be loaded from horizon, because specified in enabled file. +# To register REST api, import below here. +from castellan_ui.api import client # noqa: F401 + + +class SymmetricKeys(horizon.Panel): + name = _("Symmetric Keys") + slug = "symmetric_keys" diff --git a/castellan_ui/content/symmetric_keys/tables.py b/castellan_ui/content/symmetric_keys/tables.py new file mode 100644 index 0000000..6e4eda0 --- /dev/null +++ b/castellan_ui/content/symmetric_keys/tables.py @@ -0,0 +1,96 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from castellan_ui.content import filters +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from castellan_ui.api import client +from horizon import tables + + +class GenerateSymmetricKey(tables.LinkAction): + name = "generate_symmetric_key" + verbose_name = _("Generate Symmetric Key") + url = "horizon:project:symmetric_keys:generate" + classes = ("ajax-modal",) + icon = "plus" + policy_rules = () + + +class ImportSymmetricKey(tables.LinkAction): + name = "import_symmetric_key" + verbose_name = _("Import Symmetric Key") + url = "horizon:project:symmetric_keys:import" + classes = ("ajax-modal",) + icon = "upload" + policy_rules = () + + +class DownloadKey(tables.LinkAction): + name = "download" + verbose_name = _("Download Key") + url = "horizon:project:symmetric_keys:download" + classes = ("btn-download",) + policy_rules = () + + def get_link_url(self, datum): + return reverse(self.url, + kwargs={'object_id': datum.id}) + + +class DeleteSymmetricKey(tables.DeleteAction): + policy_rules = () + help_text = _("You should not delete a symmetric key unless you are " + "certain it is not being used anywhere.") + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Symmetric Key", + u"Delete Symmetric Keys", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Symmetric Key", + u"Deleted Symmetric Keys", + count + ) + + def delete(self, request, obj_id): + client.delete(request, obj_id) + + +class SymmetricKeyTable(tables.DataTable): + detail_link = "horizon:project:symmetric_keys:detail" + uuid = tables.Column("id", verbose_name=_("Key ID"), link=detail_link) + name = tables.Column("name", verbose_name=_("Name")) + algorithm = tables.Column("algorithm", verbose_name=_("Algorithm")) + bit_length = tables.Column("bit_length", verbose_name=_("Bit Length")) + created_date = tables.Column("created", + verbose_name=_("Created Date"), + filters=(filters.timestamp_to_iso,)) + + def get_object_display(self, datum): + return datum.name if datum.name else datum.id + + class Meta(object): + name = "symmetric_key" + table_actions = (GenerateSymmetricKey, + ImportSymmetricKey, + DeleteSymmetricKey,) + row_actions = (DownloadKey, DeleteSymmetricKey) diff --git a/castellan_ui/content/symmetric_keys/urls.py b/castellan_ui/content/symmetric_keys/urls.py new file mode 100644 index 0000000..96cd488 --- /dev/null +++ b/castellan_ui/content/symmetric_keys/urls.py @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from castellan_ui.content.symmetric_keys import views +from django.conf.urls import url + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^import/$', views.ImportView.as_view(), name='import'), + url(r'^generate/$', views.GenerateView.as_view(), name='generate'), + url(r'^(?P[^/]+)/$', + views.DetailView.as_view(), + name='detail'), + url(r'^download/$', views.download_key, name='download'), + url(r'^(?P[^/]+)/download$', + views.download_key, + name='download'), +] diff --git a/castellan_ui/content/symmetric_keys/views.py b/castellan_ui/content/symmetric_keys/views.py new file mode 100644 index 0000000..4d95a58 --- /dev/null +++ b/castellan_ui/content/symmetric_keys/views.py @@ -0,0 +1,133 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.http import HttpResponse +from django.utils.translation import ugettext_lazy as _ + +import binascii +from castellan.common.objects import symmetric_key +from castellan_ui.api import client +from castellan_ui.content.symmetric_keys import forms as symmetric_key_forms +from castellan_ui.content.symmetric_keys import tables +from datetime import datetime +from horizon import exceptions +from horizon import forms +from horizon.tables import views as tables_views +from horizon.utils import memoized +from horizon import views + + +def download_key(request, object_id): + try: + obj = client.get(request, object_id) + data = obj.get_encoded() + response = HttpResponse() + response.write(data) + response['Content-Disposition'] = ('attachment; ' + 'filename="%s.key"' % object_id) + response['Content-Length'] = str(len(response.content)) + return response + + except Exception: + redirect = reverse('horizon:project:symmetric_keys:index') + msg = _('Unable to download symmetric_key "%s".')\ + % (object_id) + exceptions.handle(request, msg, redirect=redirect) + + +class IndexView(tables_views.MultiTableView): + table_classes = [ + tables.SymmetricKeyTable + ] + template_name = 'symmetric_keys.html' + + def get_symmetric_key_data(self): + try: + return client.list(self.request, + object_type=symmetric_key.SymmetricKey) + except Exception as e: + msg = _('Unable to list symmetric keys: "%s".') % (e.message) + exceptions.handle(self.request, msg) + return [] + + +class GenerateView(forms.ModalFormView): + form_class = symmetric_key_forms.GenerateSymmetricKey + template_name = 'symmetric_key_generate.html' + submit_url = reverse_lazy( + "horizon:project:symmetric_keys:generate") + success_url = reverse_lazy('horizon:project:symmetric_keys:index') + submit_label = page_title = _("Generate Symmetric Key") + + +class ImportView(forms.ModalFormView): + form_class = symmetric_key_forms.ImportSymmetricKey + template_name = 'symmetric_key_import.html' + submit_url = reverse_lazy( + "horizon:project:symmetric_keys:import") + success_url = reverse_lazy('horizon:project:symmetric_keys:index') + submit_label = page_title = _("Import Symmetric Key") + + def get_object_id(self, key_uuid): + return key_uuid + + +class DetailView(views.HorizonTemplateView): + template_name = 'symmetric_key_detail.html' + page_title = _("Symmetric Key Details") + + @memoized.memoized_method + def _get_data(self): + try: + obj = client.get(self.request, self.kwargs['object_id']) + except Exception: + redirect = reverse('horizon:project:symmetric_keys:index') + msg = _('Unable to retrieve details for symmetric_key "%s".')\ + % (self.kwargs['object_id']) + exceptions.handle(self.request, msg, + redirect=redirect) + return obj + + @memoized.memoized_method + def _get_data_created_date(self, obj): + try: + created_date = datetime.utcfromtimestamp(obj.created).isoformat() + except Exception: + redirect = reverse('horizon:project:symmetric_keys:index') + msg = _('Unable to retrieve details for symmetric_key "%s".')\ + % (self.kwargs['object_id']) + exceptions.handle(self.request, msg, + redirect=redirect) + return created_date + + @memoized.memoized_method + def _get_data_bytes(self, obj): + try: + data_bytes = binascii.hexlify(obj.get_encoded()) + except Exception: + redirect = reverse('horizon:project:symmetric_keys:index') + msg = _('Unable to retrieve details for symmetric_key "%s".')\ + % (self.kwargs['object_id']) + exceptions.handle(self.request, msg, + redirect=redirect) + return data_bytes + + def get_context_data(self, **kwargs): + """Gets the context data for key.""" + context = super(DetailView, self).get_context_data(**kwargs) + obj = self._get_data() + context['object'] = obj + context['object_created_date'] = self._get_data_created_date(obj) + context['object_bytes'] = self._get_data_bytes(obj) + return context diff --git a/castellan_ui/enabled/_94_project_key_manager_symmetric_keys_panel.py b/castellan_ui/enabled/_94_project_key_manager_symmetric_keys_panel.py new file mode 100644 index 0000000..8f75ebd --- /dev/null +++ b/castellan_ui/enabled/_94_project_key_manager_symmetric_keys_panel.py @@ -0,0 +1,23 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'symmetric_keys' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'key_manager' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'project' + +ADD_INSTALLED_APP = ['castellan_ui', ] + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'castellan_ui.content.symmetric_keys.panel.SymmetricKeys' diff --git a/castellan_ui/templates/_symmetric_key_generate.html b/castellan_ui/templates/_symmetric_key_generate.html new file mode 100644 index 0000000..7a318f3 --- /dev/null +++ b/castellan_ui/templates/_symmetric_key_generate.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Check your key manager to see which algorithms and bit lengths are supported." %}

+{% endblock %} + diff --git a/castellan_ui/templates/_symmetric_key_import.html b/castellan_ui/templates/_symmetric_key_import.html new file mode 100644 index 0000000..f417456 --- /dev/null +++ b/castellan_ui/templates/_symmetric_key_import.html @@ -0,0 +1,9 @@ +{% extends '_object_import.html' %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "When importing your key as a file, the raw bytes of the file will be the raw bytes of the key. If you open the key file using a text editor, it will not be human-readable because the bytes may not map to ASCII characters." %}

+

{% trans "To import your key using direct input, use the hex dump of the value of the key. For example, a 128 bit key may look like this:" %}

+

00112233445566778899aabbccddeeff

+{% endblock %} + diff --git a/castellan_ui/templates/import_symmetric_key.html b/castellan_ui/templates/import_symmetric_key.html new file mode 100644 index 0000000..d062316 --- /dev/null +++ b/castellan_ui/templates/import_symmetric_key.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{{ page_title }}{% endblock %} + +{% block main %} + {% include 'project/key_pairs/_import.html' %} +{% endblock %} diff --git a/castellan_ui/templates/symmetric_key_detail.html b/castellan_ui/templates/symmetric_key_detail.html new file mode 100644 index 0000000..02a4da1 --- /dev/null +++ b/castellan_ui/templates/symmetric_key_detail.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} +{% load i18n parse_date %} + +{% block title %}{{ page_title }}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_detail_header.html" %} +{% endblock %} + +{% block main %} +
+
+
{% trans "Name" %}
+
{{ object.name|default:_("None") }}
+
{% trans "Created" %}
+
{{ object_created_date|parse_date}}
+
{% trans "Algorithm" %}
+
{{ object.algorithm|default:_("None") }}
+
{% trans "Bit Length" %}
+
{{ object.bit_length|default:_("None") }}
+
{% trans "Key Value (in hex)" %}
+
+
{{ object_bytes|default:_("None") }}
+
+
+
+{% endblock %} diff --git a/castellan_ui/templates/symmetric_key_generate.html b/castellan_ui/templates/symmetric_key_generate.html new file mode 100644 index 0000000..3ec75e8 --- /dev/null +++ b/castellan_ui/templates/symmetric_key_generate.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{{ page_title }}{% endblock %} + +{% block main %} + {% include '_symmetric_key_generate.html' %} +{% endblock %} diff --git a/castellan_ui/templates/symmetric_key_import.html b/castellan_ui/templates/symmetric_key_import.html new file mode 100644 index 0000000..eaa3e62 --- /dev/null +++ b/castellan_ui/templates/symmetric_key_import.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{{ page_title }}{% endblock %} + +{% block main %} + {% include '_symmetric_key_import.html' %} +{% endblock %} diff --git a/castellan_ui/templates/symmetric_keys.html b/castellan_ui/templates/symmetric_keys.html new file mode 100644 index 0000000..d1d810a --- /dev/null +++ b/castellan_ui/templates/symmetric_keys.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Symmetric Keys" %}{% endblock %} + +{% block breadcrumb_nav %} + +{% endblock %} + +{% block page_header %} + +{% endblock page_header %} + +{% block main %} +
+
+ {{ symmetric_key_table.render }} +
+
+{% endblock %} diff --git a/castellan_ui/test/content/symmetric_keys/__init__.py b/castellan_ui/test/content/symmetric_keys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/castellan_ui/test/content/symmetric_keys/tests.py b/castellan_ui/test/content/symmetric_keys/tests.py new file mode 100644 index 0000000..a593ab7 --- /dev/null +++ b/castellan_ui/test/content/symmetric_keys/tests.py @@ -0,0 +1,136 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import base64 +import binascii +from django.core.handlers import wsgi +from django.core.urlresolvers import reverse +from horizon import messages as horizon_messages +import mock + +from castellan.common.objects import symmetric_key +from castellan_ui.api import client as api_castellan +from castellan_ui.test import helpers as tests +from castellan_ui.test import test_data + +INDEX_URL = reverse('horizon:project:symmetric_keys:index') + + +class SymmetricKeysViewTest(tests.APITestCase): + + def setUp(self): + super(SymmetricKeysViewTest, self).setUp() + self.key = test_data.symmetric_key + self.key_b64_bytes = base64.b64encode(self.key.get_encoded()) + self.mock_object( + api_castellan, "get", mock.Mock(return_value=self.key)) + self.mock_object(api_castellan, "list", mock.Mock(return_value=[])) + self.mock_object(horizon_messages, "success") + FAKE_ENVIRON = {'REQUEST_METHOD': 'GET', 'wsgi.input': 'fake_input'} + self.request = wsgi.WSGIRequest(FAKE_ENVIRON) + + def test_index(self): + key_list = [test_data.symmetric_key, test_data.nameless_symmetric_key] + + self.mock_object( + api_castellan, "list", mock.Mock(return_value=key_list)) + + res = self.client.get(INDEX_URL) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'symmetric_keys.html') + api_castellan.list.assert_called_with( + mock.ANY, object_type=symmetric_key.SymmetricKey) + + def test_detail_view(self): + url = reverse('horizon:project:symmetric_keys:detail', + args=[self.key.id]) + self.mock_object( + api_castellan, "list", mock.Mock(return_value=[self.key])) + self.mock_object( + api_castellan, "get", mock.Mock(return_value=self.key)) + + res = self.client.get(url) + self.assertContains( + res, "
Name
\n
%s
" % self.key.name, 1, 200) + api_castellan.get.assert_called_once_with(mock.ANY, self.key.id) + + def test_import_key(self): + self.mock_object( + api_castellan, "list", mock.Mock(return_value=[self.key])) + url = reverse('horizon:project:symmetric_keys:import') + self.mock_object( + api_castellan, "import_object", mock.Mock(return_value=self.key)) + + key_input = ( + binascii.hexlify(self.key.get_encoded()).decode('utf-8') + ) + + key_form_data = { + 'source_type': 'raw', + 'name': self.key.name, + 'direct_input': key_input, + 'bit_length': 128, + 'algorithm': 'AES' + } + + self.client.post(url, key_form_data) + + api_castellan.import_object.assert_called_once_with( + mock.ANY, + object_type=symmetric_key.SymmetricKey, + key=self.key_b64_bytes, + name=self.key.name, + algorithm=u'AES', + bit_length=128 + ) + + def test_generate_key(self): + self.mock_object( + api_castellan, "list", mock.Mock(return_value=[self.key])) + url = reverse('horizon:project:symmetric_keys:generate') + self.mock_object( + api_castellan, "generate_symmetric_key", + mock.Mock(return_value=self.key)) + + key_form_data = { + 'name': self.key.name, + 'length': 256, + 'algorithm': 'AES' + } + + self.client.post(url, key_form_data) + + api_castellan.generate_symmetric_key.assert_called_once_with( + mock.ANY, + name=self.key.name, + algorithm=u'AES', + length=256 + ) + + def test_delete_key(self): + self.mock_object( + api_castellan, "list", mock.Mock(return_value=[self.key])) + self.mock_object(api_castellan, "delete") + + key_form_data = { + 'action': 'symmetric_key__delete__%s' % self.key.id + } + + res = self.client.post(INDEX_URL, key_form_data) + + api_castellan.list.assert_called_with( + mock.ANY, object_type=symmetric_key.SymmetricKey) + api_castellan.delete.assert_called_once_with( + mock.ANY, + self.key.id, + ) + self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/castellan_ui/test/test_data.py b/castellan_ui/test/test_data.py index cd57734..3261c55 100644 --- a/castellan_ui/test/test_data.py +++ b/castellan_ui/test/test_data.py @@ -56,3 +56,19 @@ nameless_public_key = objects.public_key.PublicKey( name=None, created=1448088699, id=u'11111111-1111-1111-1111-111111111111') + +symmetric_key = objects.symmetric_key.SymmetricKey( + key=castellan_utils.get_symmetric_key(), + algorithm="AES", + bit_length=128, + name=u'test symmetric key', + created=1448088699, + id=u'00000000-0000-0000-0000-000000000000') + +nameless_symmetric_key = objects.symmetric_key.SymmetricKey( + key=castellan_utils.get_symmetric_key(), + algorithm="AES", + bit_length=128, + name=None, + created=1448088699, + id=u'11111111-1111-1111-1111-111111111111') diff --git a/tox.ini b/tox.ini index a01f5c3..04a17d4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py34,py27,py27dj18,pep8 +envlist = py35,py27,py27dj18,pep8 minversion = 2.0 skipsdist = True