diff --git a/README.rst b/README.rst index 1257e1d..f4bbb0e 100644 --- a/README.rst +++ b/README.rst @@ -54,7 +54,7 @@ Install Castellan UI with all dependencies in your virtual environment:: And enable it in Horizon:: ln -s ../castellan-ui/castellan_ui/enabled/_90_project_key_manager_panelgroup.py openstack_dashboard/local/enabled - TODO(kfarr): add the panels here + ln -s ../castellan-ui/castellan_ui/enabled/_91_project_key_manager_x509_certificates_panel.py openstack_dashboard/local/enabled To run horizon with the newly enabled Castellan UI plugin run:: diff --git a/castellan_ui/content/filters.py b/castellan_ui/content/filters.py new file mode 100644 index 0000000..7d60bc9 --- /dev/null +++ b/castellan_ui/content/filters.py @@ -0,0 +1,18 @@ +# 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 datetime import datetime +from horizon.utils import filters as horizon_filters + + +def timestamp_to_iso(timestamp): + date = datetime.utcfromtimestamp(timestamp) + return horizon_filters.parse_isotime(date.isoformat()) diff --git a/castellan_ui/content/x509_certificates/__init__.py b/castellan_ui/content/x509_certificates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/castellan_ui/content/x509_certificates/forms.py b/castellan_ui/content/x509_certificates/forms.py new file mode 100644 index 0000000..b0314df --- /dev/null +++ b/castellan_ui/content/x509_certificates/forms.py @@ -0,0 +1,114 @@ +# 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 +from django.utils.translation import ugettext_lazy as _ +import re + +from castellan.common.objects import x_509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.x509 import load_pem_x509_certificate +from horizon import exceptions +from horizon import forms +from horizon import messages + +from castellan_ui.api import client + +NAME_REGEX = re.compile(r"^\w+(?:[- ]\w+)*$", re.UNICODE) +ERROR_MESSAGES = { + 'invalid': _('Name may only contain letters, ' + 'numbers, underscores, spaces, and hyphens ' + 'and may not be white space.')} + + +class ImportX509Certificate(forms.SelfHandlingForm): + name = forms.RegexField(required=False, + max_length=255, + label=_("Certificate Name"), + regex=NAME_REGEX, + error_messages=ERROR_MESSAGES) + source_type = forms.ChoiceField( + label=_('Source'), + required=False, + choices=[('file', _('Import File')), + ('raw', _('Direct Input'))], + widget=forms.ThemableSelectWidget( + attrs={'class': 'switchable', 'data-slug': 'source'})) + cert_file = forms.FileField( + label=_("Choose file"), + widget=forms.FileInput( + attrs={'class': 'switched', 'data-switch-on': 'source', + 'data-source-file': _('PEM Certificate File')}), + required=False) + direct_input = forms.CharField( + label=_('PEM Certificate'), + widget=forms.widgets.Textarea( + attrs={'class': 'switched', 'data-switch-on': 'source', + 'data-source-raw': _('PEM Certificate')}), + required=False) + + def clean(self): + data = super(ImportX509Certificate, self).clean() + + # The cert can be missing based on particular upload + # conditions. Code defensively for it here... + cert_file = data.get('cert_file', None) + cert_raw = data.get('direct_input', None) + + if cert_raw and cert_file: + raise forms.ValidationError( + _("Cannot specify both file and direct input.")) + if not cert_raw and not cert_file: + raise forms.ValidationError( + _("No input was provided for the certificate value.")) + try: + if cert_file: + cert_pem = self.files['cert_file'].read() + else: + cert_pem = str(data['direct_input']) + cert_obj = load_pem_x509_certificate( + cert_pem.encode('utf-8'), default_backend()) + cert_der = cert_obj.public_bytes(Encoding.DER) + except Exception as e: + msg = _('There was a problem loading the certificate: %s. ' + 'Is the certificate valid and in PEM format?') % e + raise forms.ValidationError(msg) + + data['cert_data'] = base64.b64encode(cert_der).decode('utf-8') + + return data + + def handle(self, request, data): + try: + cert_pem = data.get('cert_data') + cert_uuid = client.import_object( + request, + data=cert_pem, + name=data['name'], + object_type=x_509.X509) + + if data['name']: + identifier = data['name'] + else: + identifier = cert_uuid + messages.success(request, + _('Successfully imported certificate: %s') + % identifier) + return cert_uuid + except Exception as e: + msg = _('Unable to import certificate: %s') + messages.error(request, msg % e) + exceptions.handle(request, ignore=True) + self.api_error(_('Unable to import certificate.')) + return False diff --git a/castellan_ui/content/x509_certificates/panel.py b/castellan_ui/content/x509_certificates/panel.py new file mode 100644 index 0000000..d843fa5 --- /dev/null +++ b/castellan_ui/content/x509_certificates/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 X509Certificates(horizon.Panel): + name = _("X.509 Certificates") + slug = "x509_certificates" diff --git a/castellan_ui/content/x509_certificates/tables.py b/castellan_ui/content/x509_certificates/tables.py new file mode 100644 index 0000000..73457e4 --- /dev/null +++ b/castellan_ui/content/x509_certificates/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 ImportX509Certificate(tables.LinkAction): + name = "import_x509_certificate" + verbose_name = _("Import Certificate") + url = "horizon:project:x509_certificates:import" + classes = ("ajax-modal",) + icon = "upload" + policy_rules = () + + +class DownloadX509Certificate(tables.LinkAction): + name = "download" + verbose_name = _("Download Certificate") + url = "horizon:project:x509_certificates:download" + classes = ("btn-download",) + policy_rules = () + + def get_link_url(self, datum): + return reverse(self.url, + kwargs={'object_id': datum.id}) + + +class DeleteX509Certificate(tables.DeleteAction): + policy_rules = () + help_text = _("You should not delete a certificate unless you are " + "certain it is not being used anywhere.") + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete X.509 Certificate", + u"Delete X.509 Certificates", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted X.509 Certificate", + u"Deleted X.509 Certificates", + count + ) + + def delete(self, request, obj_id): + client.delete(request, obj_id) + + +class X509CertificateTable(tables.DataTable): + detail_link = "horizon:project:x509_certificates:detail" + uuid = tables.Column("id", verbose_name=_("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 = "x509_certificate" + table_actions = (ImportX509Certificate, + DeleteX509Certificate,) + row_actions = (DownloadX509Certificate, DeleteX509Certificate) diff --git a/castellan_ui/content/x509_certificates/urls.py b/castellan_ui/content/x509_certificates/urls.py new file mode 100644 index 0000000..011396c --- /dev/null +++ b/castellan_ui/content/x509_certificates/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.x509_certificates 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_cert, name='download'), + url(r'^(?P[^/]+)/download$', + views.download_cert, + name='download'), +] diff --git a/castellan_ui/content/x509_certificates/views.py b/castellan_ui/content/x509_certificates/views.py new file mode 100644 index 0000000..da37caa --- /dev/null +++ b/castellan_ui/content/x509_certificates/views.py @@ -0,0 +1,186 @@ +# 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 x_509 +from castellan_ui.api import client +from castellan_ui.content.x509_certificates import forms as x509_forms +from castellan_ui.content.x509_certificates import tables +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.x509 import load_der_x509_certificate + +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_cert(request, object_id): + try: + obj = client.get(request, object_id) + der_data = obj.get_encoded() + cert_obj = load_der_x509_certificate(der_data, default_backend()) + data = cert_obj.public_bytes(Encoding.PEM) + response = HttpResponse() + response.write(data) + response['Content-Disposition'] = ('attachment; ' + 'filename="%s.pem"' % object_id) + response['Content-Length'] = str(len(response.content)) + return response + + except Exception: + redirect = reverse('horizon:project:x509_certificates:index') + msg = _('Unable to download x509_certificate "%s".')\ + % (object_id) + exceptions.handle(request, msg, redirect=redirect) + + +class IndexView(tables_views.MultiTableView): + table_classes = [ + tables.X509CertificateTable + ] + template_name = 'x509_certificates.html' + + def get_x509_certificate_data(self): + try: + return client.list(self.request, object_type=x_509.X509) + except Exception as e: + msg = _('Unable to list certificates: "%s".') % (e.message) + exceptions.handle(self.request, msg) + return [] + + +class ImportView(forms.ModalFormView): + form_class = x509_forms.ImportX509Certificate + template_name = 'x509_certificate_import.html' + submit_url = reverse_lazy( + "horizon:project:x509_certificates:import") + success_url = reverse_lazy('horizon:project:x509_certificates:index') + submit_label = page_title = _("Import X.509 Certificate") + + def get_form(self, form_class=None): + if form_class is None: + form_class = self.get_form_class() + return form_class(self.request, **self.get_form_kwargs()) + + def get_object_id(self, key_uuid): + return key_uuid + + +class DetailView(views.HorizonTemplateView): + template_name = 'x509_certificate_detail.html' + page_title = _("X.509 Certificate Details") + + @memoized.memoized_method + def _get_data(self): + try: + obj = client.get(self.request, self.kwargs['object_id']) + except Exception: + redirect = reverse('horizon:project:x509_certificates:index') + msg = _('Unable to retrieve details for x509_certificate "%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:x509_certificates:index') + msg = _('Unable to retrieve details for x509_certificate "%s".')\ + % (self.kwargs['object_id']) + exceptions.handle(self.request, msg, + redirect=redirect) + return created_date + + @memoized.memoized_method + def _get_crypto_obj(self, obj): + der_data = obj.get_encoded() + return load_der_x509_certificate(der_data, default_backend()) + + @memoized.memoized_method + def _get_certificate_version(self, obj): + return self._get_crypto_obj(obj).version + + @memoized.memoized_method + def _get_certificate_fingerprint(self, obj): + return binascii.hexlify( + self._get_crypto_obj(obj).fingerprint(hashes.SHA256())) + + @memoized.memoized_method + def _get_serial_number(self, obj): + return self._get_crypto_obj(obj).serial_number + + @memoized.memoized_method + def _get_validity_start(self, obj): + return self._get_crypto_obj(obj).not_valid_before + + @memoized.memoized_method + def _get_validity_end(self, obj): + return self._get_crypto_obj(obj).not_valid_after + + @memoized.memoized_method + def _get_issuer(self, obj): + result = "" + issuer = self._get_crypto_obj(obj).issuer + for attribute in issuer: + result = (result + str(attribute.oid._name) + "=" + + str(attribute.value) + ",") + return result[:-1] + + @memoized.memoized_method + def _get_subject(self, obj): + result = "" + issuer = self._get_crypto_obj(obj).subject + for attribute in issuer: + result = (result + str(attribute.oid._name) + "=" + + str(attribute.value) + ",") + return result[:-1] + + @memoized.memoized_method + def _get_data_bytes(self, obj): + try: + data = self._get_crypto_obj(obj).public_bytes(Encoding.PEM) + except Exception: + redirect = reverse('horizon:project:x509_certificates:index') + msg = _('Unable to retrieve details for x509_certificate "%s".')\ + % (self.kwargs['object_id']) + exceptions.handle(self.request, msg, + redirect=redirect) + return data + + 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) + context['cert_version'] = self._get_certificate_version(obj) + context['cert_fingerprint'] = self._get_certificate_fingerprint(obj) + context['cert_serial_number'] = self._get_serial_number(obj) + context['cert_validity_start'] = self._get_validity_start(obj) + context['cert_validity_end'] = self._get_validity_end(obj) + context['cert_issuer'] = self._get_issuer(obj) + context['cert_subject'] = self._get_subject(obj) + return context diff --git a/castellan_ui/enabled/_91_project_key_manager_x509_certificates_panel.py b/castellan_ui/enabled/_91_project_key_manager_x509_certificates_panel.py new file mode 100644 index 0000000..c04e152 --- /dev/null +++ b/castellan_ui/enabled/_91_project_key_manager_x509_certificates_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 = 'x509_certificates' +# 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.x509_certificates.panel.X509Certificates' diff --git a/castellan_ui/templates/_object_import.html b/castellan_ui/templates/_object_import.html new file mode 100644 index 0000000..6e3b432 --- /dev/null +++ b/castellan_ui/templates/_object_import.html @@ -0,0 +1,54 @@ +{% extends "horizon/common/_modal.html" %} +{% block content %} + {% if table %} + + {% endif %} +
{% csrf_token %} + + +
+{% endblock %} +{% load i18n %} + diff --git a/castellan_ui/templates/_x509_certificate_import.html b/castellan_ui/templates/_x509_certificate_import.html new file mode 100644 index 0000000..3830057 --- /dev/null +++ b/castellan_ui/templates/_x509_certificate_import.html @@ -0,0 +1,9 @@ +{% extends '_object_import.html' %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "X.509 certificates can be imported if they are in Privacy Enhanced Mail (PEM) format." %}

+

{% trans "Your PEM formatted certificate will look like this:" %}

+

-----BEGIN CERTIFICATE-----
<base64-encoded data>
-----END CERTIFICATE-----

+{% endblock %} + diff --git a/castellan_ui/templates/x509_certificate_detail.html b/castellan_ui/templates/x509_certificate_detail.html new file mode 100644 index 0000000..eb3eb8c --- /dev/null +++ b/castellan_ui/templates/x509_certificate_detail.html @@ -0,0 +1,37 @@ +{% 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 "Certificate Version" %}
+
{{ cert_version}}
+
{% trans "SHA-256 Fingerprint" %}
+
{{ cert_fingerprint}}
+
{% trans "Serial Number" %}
+
{{ cert_serial_number}}
+
{% trans "Valid From" %}
+
{{ cert_validity_start }}
+
{% trans "Valid To" %}
+
{{ cert_validity_end }}
+
{% trans "Issuer" %}
+
{{ cert_issuer}}
+
{% trans "Subject" %}
+
{{ cert_subject}}
+
{% trans "Raw Certificate Value" %}
+
+
{{ object_bytes|default:_("None") }}
+
+
+
+{% endblock %} diff --git a/castellan_ui/templates/x509_certificate_import.html b/castellan_ui/templates/x509_certificate_import.html new file mode 100644 index 0000000..1ac1bba --- /dev/null +++ b/castellan_ui/templates/x509_certificate_import.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{{ page_title }}{% endblock %} + +{% block main %} + {% include '_x509_certificate_import.html' %} +{% endblock %} diff --git a/castellan_ui/templates/x509_certificates.html b/castellan_ui/templates/x509_certificates.html new file mode 100644 index 0000000..4b66c8c --- /dev/null +++ b/castellan_ui/templates/x509_certificates.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "X.509 Certificates" %}{% endblock %} + +{% block breadcrumb_nav %} + +{% endblock %} + +{% block page_header %} + +{% endblock page_header %} + +{% block main %} +
+
+ {{ x509_certificate_table.render }} +
+
+{% endblock %} diff --git a/castellan_ui/test/content/__init__.py b/castellan_ui/test/content/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/castellan_ui/test/content/x509_certificates/__init__.py b/castellan_ui/test/content/x509_certificates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/castellan_ui/test/content/x509_certificates/tests.py b/castellan_ui/test/content/x509_certificates/tests.py new file mode 100644 index 0000000..c6abf26 --- /dev/null +++ b/castellan_ui/test/content/x509_certificates/tests.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 +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 x_509 +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:x509_certificates:index') + + +class X509CertificatesViewTest(tests.APITestCase): + + def setUp(self): + super(X509CertificatesViewTest, self).setUp() + self.cert = test_data.x509_cert + self.cert_b64_bytes = base64.b64encode(self.cert.get_encoded()) + self.mock_object( + api_castellan, "get", mock.Mock(return_value=self.cert)) + 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): + cert_list = [test_data.x509_cert, test_data.nameless_x509_cert] + + self.mock_object( + api_castellan, "list", mock.Mock(return_value=cert_list)) + + res = self.client.get(INDEX_URL) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'x509_certificates.html') + api_castellan.list.assert_called_with(mock.ANY, object_type=x_509.X509) + + def test_detail_view(self): + url = reverse('horizon:project:x509_certificates:detail', + args=[self.cert.id]) + self.mock_object( + api_castellan, "list", mock.Mock(return_value=[self.cert])) + self.mock_object( + api_castellan, "get", mock.Mock(return_value=self.cert)) + + res = self.client.get(url) + self.assertContains( + res, "
Name
\n
%s
" % self.cert.name, 1, 200) + api_castellan.get.assert_called_once_with(mock.ANY, self.cert.id) + + def test_import_cert(self): + self.mock_object( + api_castellan, "list", mock.Mock(return_value=[self.cert])) + url = reverse('horizon:project:x509_certificates:import') + self.mock_object( + api_castellan, "import_object", mock.Mock(return_value=self.cert)) + + cert_input = ( + u"-----BEGIN CERTIFICATE-----\n" + + self.cert_b64_bytes.decode("utf-8") + + u"\n-----END CERTIFICATE-----" + ) + + cert_form_data = { + 'source_type': 'raw', + 'name': self.cert.name, + 'direct_input': cert_input + } + + self.client.post(url, cert_form_data) + + api_castellan.import_object.assert_called_once_with( + mock.ANY, + object_type=x_509.X509, + data=self.cert_b64_bytes.decode('utf-8'), + name=self.cert.name + ) + + def test_delete_cert(self): + self.mock_object( + api_castellan, "list", mock.Mock(return_value=[self.cert])) + self.mock_object(api_castellan, "delete") + + cert_form_data = { + 'action': 'x509_certificate__delete__%s' % self.cert.id + } + + res = self.client.post(INDEX_URL, cert_form_data) + + api_castellan.list.assert_called_with(mock.ANY, object_type=x_509.X509) + api_castellan.delete.assert_called_once_with( + mock.ANY, + self.cert.id, + ) + self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/castellan_ui/test/test_data.py b/castellan_ui/test/test_data.py new file mode 100644 index 0000000..33c1b8a --- /dev/null +++ b/castellan_ui/test/test_data.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.common import objects +from castellan.tests import utils as castellan_utils + +x509_cert = objects.x_509.X509( + data=castellan_utils.get_certificate_der(), + name='test cert', + created=1448088699, + id=u'00000000-0000-0000-0000-000000000000') + +nameless_x509_cert = objects.x_509.X509( + data=castellan_utils.get_certificate_der(), + name=None, + created=1448088699, + id=u'11111111-1111-1111-1111-111111111111') diff --git a/tox.ini b/tox.ini index 4a6497f..a01f5c3 100644 --- a/tox.ini +++ b/tox.ini @@ -65,4 +65,4 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen [flake8] exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,node_modules,.tmp -max-complexity = 20 \ No newline at end of file +max-complexity = 20