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 %}
+
+ - {% trans "Project" %}
+ - {% trans "Key Manager" %}
+ - {% trans "Symmetric Keys" %}
+
+{% 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