From 21504848d687e998b68c52b5824c940a87d0cfb7 Mon Sep 17 00:00:00 2001
From: Kaitlin Farr
Date: Tue, 14 Nov 2017 12:01:29 -0500
Subject: [PATCH] Add Opaque Data Panel
Change-Id: I66e1dd24e831c7e1fb59d24ba46a9d0ff1d85b76
---
README.rst | 3 +-
castellan_ui/content/opaque_data/__init__.py | 0
castellan_ui/content/opaque_data/forms.py | 108 +++++++++++++++
castellan_ui/content/opaque_data/panel.py | 23 ++++
castellan_ui/content/opaque_data/tables.py | 84 ++++++++++++
castellan_ui/content/opaque_data/urls.py | 26 ++++
castellan_ui/content/opaque_data/views.py | 124 ++++++++++++++++++
...5_project_key_manager_opaque_data_panel.py | 23 ++++
.../templates/_opaque_data_import.html | 9 ++
castellan_ui/templates/opaque_data.html | 23 ++++
.../templates/opaque_data_detail.html | 24 ++++
.../templates/opaque_data_import.html | 7 +
.../test/content/opaque_data/__init__.py | 0
.../test/content/opaque_data/tests.py | 109 +++++++++++++++
castellan_ui/test/test_data.py | 12 ++
15 files changed, 574 insertions(+), 1 deletion(-)
create mode 100644 castellan_ui/content/opaque_data/__init__.py
create mode 100644 castellan_ui/content/opaque_data/forms.py
create mode 100644 castellan_ui/content/opaque_data/panel.py
create mode 100644 castellan_ui/content/opaque_data/tables.py
create mode 100644 castellan_ui/content/opaque_data/urls.py
create mode 100644 castellan_ui/content/opaque_data/views.py
create mode 100644 castellan_ui/enabled/_95_project_key_manager_opaque_data_panel.py
create mode 100644 castellan_ui/templates/_opaque_data_import.html
create mode 100644 castellan_ui/templates/opaque_data.html
create mode 100644 castellan_ui/templates/opaque_data_detail.html
create mode 100644 castellan_ui/templates/opaque_data_import.html
create mode 100644 castellan_ui/test/content/opaque_data/__init__.py
create mode 100644 castellan_ui/test/content/opaque_data/tests.py
diff --git a/README.rst b/README.rst
index e747721..bf5bc16 100644
--- a/README.rst
+++ b/README.rst
@@ -57,7 +57,8 @@ 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
+ ln -s ../castellan-ui/castellan_ui/enabled/_94_project_key_manager_symmetric_key_panel.py openstack_dashboard/local/enabled
+ ln -s ../castellan-ui/castellan_ui/enabled/_95_project_key_manager_opaque_data_panel.py openstack_dashboard/local/enabled
To run horizon with the newly enabled Castellan UI plugin run::
diff --git a/castellan_ui/content/opaque_data/__init__.py b/castellan_ui/content/opaque_data/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/castellan_ui/content/opaque_data/forms.py b/castellan_ui/content/opaque_data/forms.py
new file mode 100644
index 0000000..a323c7b
--- /dev/null
+++ b/castellan_ui/content/opaque_data/forms.py
@@ -0,0 +1,108 @@
+# 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 opaque_data
+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
+
+
+class ImportOpaqueData(forms.SelfHandlingForm):
+ name = forms.RegexField(required=False,
+ max_length=255,
+ label=_("Data Name"),
+ regex=shared_forms.NAME_REGEX,
+ error_messages=shared_forms.ERROR_MESSAGES)
+ source_type = forms.ChoiceField(
+ label=_('Source'),
+ required=False,
+ choices=[('file', _('File')),
+ ('raw', _('Direct Input'))],
+ widget=forms.ThemableSelectWidget(
+ attrs={'class': 'switchable', 'data-slug': 'source'}))
+ object_file = forms.FileField(
+ label=_("Choose file"),
+ help_text=_("A local file to upload."),
+ widget=forms.FileInput(
+ attrs={'class': 'switched', 'data-switch-on': 'source',
+ 'data-source-file': _('File')}),
+ required=False)
+ direct_input = forms.CharField(
+ label=_('Object Bytes'),
+ help_text=_('The bytes of the object, represented in hex.'),
+ widget=forms.widgets.Textarea(
+ attrs={'class': 'switched', 'data-switch-on': 'source',
+ 'data-source-raw': _('Bytes')}),
+ required=False)
+
+ def __init__(self, request, *args, **kwargs):
+ super(ImportOpaqueData, self).__init__(request, *args, **kwargs)
+
+ def clean(self):
+ data = super(ImportOpaqueData, self).clean()
+
+ # The data can be missing based on particular upload
+ # conditions. Code defensively for it here...
+ data_file = data.get('object_file', None)
+ data_raw = data.get('direct_input', None)
+
+ if data_raw and data_file:
+ raise forms.ValidationError(
+ _("Cannot specify both file and direct input."))
+ if not data_raw and not data_file:
+ raise forms.ValidationError(
+ _("No input was provided for the object value."))
+ try:
+ if data_file:
+ data_bytes = self.files['object_file'].read()
+ else:
+ data_str = data['direct_input']
+ data_bytes = binascii.unhexlify(data_str)
+ data['object_bytes'] = base64.b64encode(data_bytes)
+ except Exception as e:
+ msg = _('There was a problem loading the object: %s. '
+ 'Is the object valid and in the correct format?') % e
+ raise forms.ValidationError(msg)
+
+ return data
+
+ def handle(self, request, data):
+ try:
+ data_bytes = data.get('object_bytes')
+ data_uuid = client.import_object(
+ request,
+ data=data_bytes,
+ name=data['name'],
+ object_type=opaque_data.OpaqueData)
+
+ if data['name']:
+ data_identifier = data['name']
+ else:
+ data_identifier = data_uuid
+ messages.success(request,
+ _('Successfully imported object: %s')
+ % data_identifier)
+ return data_uuid
+ except Exception as e:
+ msg = _('Unable to import object: %s')
+ messages.error(msg % e)
+ exceptions.handle(request, ignore=True)
+ self.api_error(_('Unable to import object.'))
+ return False
diff --git a/castellan_ui/content/opaque_data/panel.py b/castellan_ui/content/opaque_data/panel.py
new file mode 100644
index 0000000..5725252
--- /dev/null
+++ b/castellan_ui/content/opaque_data/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 OpaqueData(horizon.Panel):
+ name = _("Opaque Data")
+ slug = "opaque_data"
diff --git a/castellan_ui/content/opaque_data/tables.py b/castellan_ui/content/opaque_data/tables.py
new file mode 100644
index 0000000..4129431
--- /dev/null
+++ b/castellan_ui/content/opaque_data/tables.py
@@ -0,0 +1,84 @@
+# 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 ImportOpaqueData(tables.LinkAction):
+ name = "import_opaque_data"
+ verbose_name = _("Import Opaque Data")
+ url = "horizon:project:opaque_data:import"
+ classes = ("ajax-modal",)
+ icon = "upload"
+ policy_rules = ()
+
+
+class DownloadOpaqueData(tables.LinkAction):
+ name = "download"
+ verbose_name = _("Download Opaque Data")
+ url = "horizon:project:opaque_data:download"
+ classes = ("btn-download",)
+ policy_rules = ()
+
+ def get_link_url(self, datum):
+ return reverse(self.url,
+ kwargs={'object_id': datum.id})
+
+
+class DeleteOpaqueData(tables.DeleteAction):
+ policy_rules = ()
+ help_text = _("You should not delete an object unless you are "
+ "certain it is not being used anywhere.")
+
+ @staticmethod
+ def action_present(count):
+ return ungettext_lazy(
+ u"Delete Opaque Data",
+ u"Delete Opaque Data",
+ count
+ )
+
+ @staticmethod
+ def action_past(count):
+ return ungettext_lazy(
+ u"Deleted Opaque Data",
+ u"Deleted Opaque Data",
+ count
+ )
+
+ def delete(self, request, obj_id):
+ client.delete(request, obj_id)
+
+
+class OpaqueDataTable(tables.DataTable):
+ detail_link = "horizon:project:opaque_data:detail"
+ uuid = tables.Column("id", verbose_name=_("Object ID"), link=detail_link)
+ name = tables.Column("name", verbose_name=_("Name"))
+ 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 = "opaque_data"
+ table_actions = (ImportOpaqueData,
+ DeleteOpaqueData,)
+ row_actions = (DownloadOpaqueData, DeleteOpaqueData)
diff --git a/castellan_ui/content/opaque_data/urls.py b/castellan_ui/content/opaque_data/urls.py
new file mode 100644
index 0000000..5e77c40
--- /dev/null
+++ b/castellan_ui/content/opaque_data/urls.py
@@ -0,0 +1,26 @@
+# 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.opaque_data 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'^(?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/opaque_data/views.py b/castellan_ui/content/opaque_data/views.py
new file mode 100644
index 0000000..1e1dae0
--- /dev/null
+++ b/castellan_ui/content/opaque_data/views.py
@@ -0,0 +1,124 @@
+# 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 opaque_data
+from castellan_ui.api import client
+from castellan_ui.content.opaque_data import forms as opaque_data_forms
+from castellan_ui.content.opaque_data 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.opaque"' % object_id)
+ response['Content-Length'] = str(len(response.content))
+ return response
+
+ except Exception:
+ redirect = reverse('horizon:project:opaque_data:index')
+ msg = _('Unable to download opaque_data "%s".')\
+ % (object_id)
+ exceptions.handle(request, msg, redirect=redirect)
+
+
+class IndexView(tables_views.MultiTableView):
+ table_classes = [
+ tables.OpaqueDataTable
+ ]
+ template_name = 'opaque_data.html'
+
+ def get_opaque_data_data(self):
+ try:
+ return client.list(
+ self.request, object_type=opaque_data.OpaqueData)
+ except Exception as e:
+ msg = _('Unable to list objects: "%s".') % (e.message)
+ exceptions.handle(self.request, msg)
+ return []
+
+
+class ImportView(forms.ModalFormView):
+ form_class = opaque_data_forms.ImportOpaqueData
+ template_name = 'opaque_data_import.html'
+ submit_url = reverse_lazy(
+ "horizon:project:opaque_data:import")
+ success_url = reverse_lazy('horizon:project:opaque_data:index')
+ submit_label = page_title = _("Import Opaque Data")
+
+ def get_object_id(self, key_uuid):
+ return key_uuid
+
+
+class DetailView(views.HorizonTemplateView):
+ template_name = 'opaque_data_detail.html'
+ page_title = _("Opaque Data Details")
+
+ @memoized.memoized_method
+ def _get_data(self):
+ try:
+ obj = client.get(self.request, self.kwargs['object_id'])
+ except Exception:
+ redirect = reverse('horizon:project:opaque_data:index')
+ msg = _('Unable to retrieve details for opaque_data "%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:opaque_data:index')
+ msg = _('Unable to retrieve details for opaque_data "%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:opaque_data:index')
+ msg = _('Unable to retrieve details for opaque_data "%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/_95_project_key_manager_opaque_data_panel.py b/castellan_ui/enabled/_95_project_key_manager_opaque_data_panel.py
new file mode 100644
index 0000000..5641277
--- /dev/null
+++ b/castellan_ui/enabled/_95_project_key_manager_opaque_data_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 = 'opaque_data'
+# 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.opaque_data.panel.OpaqueData'
diff --git a/castellan_ui/templates/_opaque_data_import.html b/castellan_ui/templates/_opaque_data_import.html
new file mode 100644
index 0000000..c3d4074
--- /dev/null
+++ b/castellan_ui/templates/_opaque_data_import.html
@@ -0,0 +1,9 @@
+{% extends '_object_import.html' %}
+{% load i18n %}
+
+{% block modal-body-right %}
+ {% trans "When importing your object as a file, the raw bytes of the file will be the raw bytes of the object. If you open the file using a text editor, it may not be human-readable because the bytes may not map to ASCII characters." %}
+ {% trans "To import your object using direct input, use the hex dump of the value of the object. For example, it may look like this:" %}
+ 00112233445566778899aabbccddeeff
+{% endblock %}
+
diff --git a/castellan_ui/templates/opaque_data.html b/castellan_ui/templates/opaque_data.html
new file mode 100644
index 0000000..fa740be
--- /dev/null
+++ b/castellan_ui/templates/opaque_data.html
@@ -0,0 +1,23 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Opaque Data" %}{% endblock %}
+
+{% block breadcrumb_nav %}
+
+ - {% trans "Project" %}
+ - {% trans "Key Manager" %}
+ - {% trans "Opaque Data" %}
+
+{% endblock %}
+
+{% block page_header %}
+
+{% endblock page_header %}
+
+{% block main %}
+
+
+ {{ opaque_data_table.render }}
+
+
+{% endblock %}
diff --git a/castellan_ui/templates/opaque_data_detail.html b/castellan_ui/templates/opaque_data_detail.html
new file mode 100644
index 0000000..6e28ecf
--- /dev/null
+++ b/castellan_ui/templates/opaque_data_detail.html
@@ -0,0 +1,24 @@
+{% 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 "Object Value (in hex)" %}
+ -
+
{{ object_bytes|default:_("None") }}
+
+
+
+
+{% endblock %}
diff --git a/castellan_ui/templates/opaque_data_import.html b/castellan_ui/templates/opaque_data_import.html
new file mode 100644
index 0000000..8c41592
--- /dev/null
+++ b/castellan_ui/templates/opaque_data_import.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{{ page_title }}{% endblock %}
+
+{% block main %}
+ {% include '_opaque_data_import.html' %}
+{% endblock %}
diff --git a/castellan_ui/test/content/opaque_data/__init__.py b/castellan_ui/test/content/opaque_data/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/castellan_ui/test/content/opaque_data/tests.py b/castellan_ui/test/content/opaque_data/tests.py
new file mode 100644
index 0000000..c108964
--- /dev/null
+++ b/castellan_ui/test/content/opaque_data/tests.py
@@ -0,0 +1,109 @@
+# 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 opaque_data
+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:opaque_data:index')
+
+
+class OpaqueDataViewTest(tests.APITestCase):
+
+ def setUp(self):
+ super(OpaqueDataViewTest, self).setUp()
+ self.data = test_data.opaque_data
+ self.data_b64_bytes = base64.b64encode(self.data.get_encoded())
+ self.mock_object(
+ api_castellan, "get", mock.Mock(return_value=self.data))
+ 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):
+ data_list = [test_data.opaque_data, test_data.nameless_opaque_data]
+
+ self.mock_object(
+ api_castellan, "list", mock.Mock(return_value=data_list))
+
+ res = self.client.get(INDEX_URL)
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'opaque_data.html')
+ api_castellan.list.assert_called_with(
+ mock.ANY, object_type=opaque_data.OpaqueData)
+
+ def test_detail_view(self):
+ url = reverse('horizon:project:opaque_data:detail',
+ args=[self.data.id])
+ self.mock_object(
+ api_castellan, "list", mock.Mock(return_value=[self.data]))
+ self.mock_object(
+ api_castellan, "get", mock.Mock(return_value=self.data))
+
+ res = self.client.get(url)
+ self.assertContains(
+ res, "Name\n %s" % self.data.name, 1, 200)
+ api_castellan.get.assert_called_once_with(mock.ANY, self.data.id)
+
+ def test_import_data(self):
+ self.mock_object(
+ api_castellan, "list", mock.Mock(return_value=[self.data]))
+ url = reverse('horizon:project:opaque_data:import')
+ self.mock_object(
+ api_castellan, "import_object", mock.Mock(return_value=self.data))
+
+ data_input = (
+ binascii.hexlify(self.data.get_encoded()).decode('utf-8')
+ )
+
+ data_form_data = {
+ 'source_type': 'raw',
+ 'name': self.data.name,
+ 'direct_input': data_input,
+ }
+
+ self.client.post(url, data_form_data)
+
+ api_castellan.import_object.assert_called_once_with(
+ mock.ANY,
+ object_type=opaque_data.OpaqueData,
+ data=self.data_b64_bytes,
+ name=self.data.name,
+ )
+
+ def test_delete_data(self):
+ self.mock_object(
+ api_castellan, "list", mock.Mock(return_value=[self.data]))
+ self.mock_object(api_castellan, "delete")
+
+ data_form_data = {
+ 'action': 'opaque_data__delete__%s' % self.data.id
+ }
+
+ res = self.client.post(INDEX_URL, data_form_data)
+
+ api_castellan.list.assert_called_with(
+ mock.ANY, object_type=opaque_data.OpaqueData)
+ api_castellan.delete.assert_called_once_with(
+ mock.ANY,
+ self.data.id,
+ )
+ self.assertRedirectsNoFollow(res, INDEX_URL)
diff --git a/castellan_ui/test/test_data.py b/castellan_ui/test/test_data.py
index 3261c55..675d5a4 100644
--- a/castellan_ui/test/test_data.py
+++ b/castellan_ui/test/test_data.py
@@ -72,3 +72,15 @@ nameless_symmetric_key = objects.symmetric_key.SymmetricKey(
name=None,
created=1448088699,
id=u'11111111-1111-1111-1111-111111111111')
+
+opaque_data = objects.opaque_data.OpaqueData(
+ data=b'\xde\xad\xbe\xef',
+ name=u'test opaque data',
+ created=1448088699,
+ id=u'00000000-0000-0000-0000-000000000000')
+
+nameless_opaque_data = objects.opaque_data.OpaqueData(
+ data=b'\xde\xad\xbe\xef',
+ name=None,
+ created=1448088699,
+ id=u'11111111-1111-1111-1111-111111111111')