Browse Source

Merge "Add X509 Certificate Panel"

tags/0.1.0
Zuul 1 year ago
parent
commit
2cbad27df7

+ 1
- 1
README.rst View File

@@ -54,7 +54,7 @@ Install Castellan UI with all dependencies in your virtual environment::
54 54
 And enable it in Horizon::
55 55
 
56 56
     ln -s ../castellan-ui/castellan_ui/enabled/_90_project_key_manager_panelgroup.py openstack_dashboard/local/enabled
57
-    TODO(kfarr): add the panels here
57
+    ln -s ../castellan-ui/castellan_ui/enabled/_91_project_key_manager_x509_certificates_panel.py openstack_dashboard/local/enabled
58 58
 
59 59
 To run horizon with the newly enabled Castellan UI plugin run::
60 60
 

+ 18
- 0
castellan_ui/content/filters.py View File

@@ -0,0 +1,18 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+from datetime import datetime
13
+from horizon.utils import filters as horizon_filters
14
+
15
+
16
+def timestamp_to_iso(timestamp):
17
+    date = datetime.utcfromtimestamp(timestamp)
18
+    return horizon_filters.parse_isotime(date.isoformat())

+ 0
- 0
castellan_ui/content/x509_certificates/__init__.py View File


+ 114
- 0
castellan_ui/content/x509_certificates/forms.py View File

@@ -0,0 +1,114 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+
14
+import base64
15
+from django.utils.translation import ugettext_lazy as _
16
+import re
17
+
18
+from castellan.common.objects import x_509
19
+from cryptography.hazmat.backends import default_backend
20
+from cryptography.hazmat.primitives.serialization import Encoding
21
+from cryptography.x509 import load_pem_x509_certificate
22
+from horizon import exceptions
23
+from horizon import forms
24
+from horizon import messages
25
+
26
+from castellan_ui.api import client
27
+
28
+NAME_REGEX = re.compile(r"^\w+(?:[- ]\w+)*$", re.UNICODE)
29
+ERROR_MESSAGES = {
30
+    'invalid': _('Name may only contain letters, '
31
+                 'numbers, underscores, spaces, and hyphens '
32
+                 'and may not be white space.')}
33
+
34
+
35
+class ImportX509Certificate(forms.SelfHandlingForm):
36
+    name = forms.RegexField(required=False,
37
+                            max_length=255,
38
+                            label=_("Certificate Name"),
39
+                            regex=NAME_REGEX,
40
+                            error_messages=ERROR_MESSAGES)
41
+    source_type = forms.ChoiceField(
42
+        label=_('Source'),
43
+        required=False,
44
+        choices=[('file', _('Import File')),
45
+                 ('raw', _('Direct Input'))],
46
+        widget=forms.ThemableSelectWidget(
47
+            attrs={'class': 'switchable', 'data-slug': 'source'}))
48
+    cert_file = forms.FileField(
49
+        label=_("Choose file"),
50
+        widget=forms.FileInput(
51
+            attrs={'class': 'switched', 'data-switch-on': 'source',
52
+                   'data-source-file': _('PEM Certificate File')}),
53
+        required=False)
54
+    direct_input = forms.CharField(
55
+        label=_('PEM Certificate'),
56
+        widget=forms.widgets.Textarea(
57
+            attrs={'class': 'switched', 'data-switch-on': 'source',
58
+                   'data-source-raw': _('PEM Certificate')}),
59
+        required=False)
60
+
61
+    def clean(self):
62
+        data = super(ImportX509Certificate, self).clean()
63
+
64
+        # The cert can be missing based on particular upload
65
+        # conditions. Code defensively for it here...
66
+        cert_file = data.get('cert_file', None)
67
+        cert_raw = data.get('direct_input', None)
68
+
69
+        if cert_raw and cert_file:
70
+            raise forms.ValidationError(
71
+                _("Cannot specify both file and direct input."))
72
+        if not cert_raw and not cert_file:
73
+            raise forms.ValidationError(
74
+                _("No input was provided for the certificate value."))
75
+        try:
76
+            if cert_file:
77
+                cert_pem = self.files['cert_file'].read()
78
+            else:
79
+                cert_pem = str(data['direct_input'])
80
+            cert_obj = load_pem_x509_certificate(
81
+                cert_pem.encode('utf-8'), default_backend())
82
+            cert_der = cert_obj.public_bytes(Encoding.DER)
83
+        except Exception as e:
84
+            msg = _('There was a problem loading the certificate: %s. '
85
+                    'Is the certificate valid and in PEM format?') % e
86
+            raise forms.ValidationError(msg)
87
+
88
+        data['cert_data'] = base64.b64encode(cert_der).decode('utf-8')
89
+
90
+        return data
91
+
92
+    def handle(self, request, data):
93
+        try:
94
+            cert_pem = data.get('cert_data')
95
+            cert_uuid = client.import_object(
96
+                request,
97
+                data=cert_pem,
98
+                name=data['name'],
99
+                object_type=x_509.X509)
100
+
101
+            if data['name']:
102
+                identifier = data['name']
103
+            else:
104
+                identifier = cert_uuid
105
+            messages.success(request,
106
+                             _('Successfully imported certificate: %s')
107
+                             % identifier)
108
+            return cert_uuid
109
+        except Exception as e:
110
+            msg = _('Unable to import certificate: %s')
111
+            messages.error(request, msg % e)
112
+            exceptions.handle(request, ignore=True)
113
+            self.api_error(_('Unable to import certificate.'))
114
+            return False

+ 23
- 0
castellan_ui/content/x509_certificates/panel.py View File

@@ -0,0 +1,23 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from django.utils.translation import ugettext_lazy as _
14
+import horizon
15
+
16
+# This panel will be loaded from horizon, because specified in enabled file.
17
+# To register REST api, import below here.
18
+from castellan_ui.api import client  # noqa: F401
19
+
20
+
21
+class X509Certificates(horizon.Panel):
22
+    name = _("X.509 Certificates")
23
+    slug = "x509_certificates"

+ 84
- 0
castellan_ui/content/x509_certificates/tables.py View File

@@ -0,0 +1,84 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+
14
+from castellan_ui.content import filters
15
+from django.core.urlresolvers import reverse
16
+from django.utils.translation import ugettext_lazy as _
17
+from django.utils.translation import ungettext_lazy
18
+
19
+from castellan_ui.api import client
20
+from horizon import tables
21
+
22
+
23
+class ImportX509Certificate(tables.LinkAction):
24
+    name = "import_x509_certificate"
25
+    verbose_name = _("Import Certificate")
26
+    url = "horizon:project:x509_certificates:import"
27
+    classes = ("ajax-modal",)
28
+    icon = "upload"
29
+    policy_rules = ()
30
+
31
+
32
+class DownloadX509Certificate(tables.LinkAction):
33
+    name = "download"
34
+    verbose_name = _("Download Certificate")
35
+    url = "horizon:project:x509_certificates:download"
36
+    classes = ("btn-download",)
37
+    policy_rules = ()
38
+
39
+    def get_link_url(self, datum):
40
+        return reverse(self.url,
41
+                       kwargs={'object_id': datum.id})
42
+
43
+
44
+class DeleteX509Certificate(tables.DeleteAction):
45
+    policy_rules = ()
46
+    help_text = _("You should not delete a certificate unless you are "
47
+                  "certain it is not being used anywhere.")
48
+
49
+    @staticmethod
50
+    def action_present(count):
51
+        return ungettext_lazy(
52
+            u"Delete X.509 Certificate",
53
+            u"Delete X.509 Certificates",
54
+            count
55
+        )
56
+
57
+    @staticmethod
58
+    def action_past(count):
59
+        return ungettext_lazy(
60
+            u"Deleted X.509 Certificate",
61
+            u"Deleted X.509 Certificates",
62
+            count
63
+        )
64
+
65
+    def delete(self, request, obj_id):
66
+        client.delete(request, obj_id)
67
+
68
+
69
+class X509CertificateTable(tables.DataTable):
70
+    detail_link = "horizon:project:x509_certificates:detail"
71
+    uuid = tables.Column("id", verbose_name=_("ID"), link=detail_link)
72
+    name = tables.Column("name", verbose_name=_("Name"))
73
+    created_date = tables.Column("created",
74
+                                 verbose_name=_("Created Date"),
75
+                                 filters=(filters.timestamp_to_iso,))
76
+
77
+    def get_object_display(self, datum):
78
+        return datum.name if datum.name else datum.id
79
+
80
+    class Meta(object):
81
+        name = "x509_certificate"
82
+        table_actions = (ImportX509Certificate,
83
+                         DeleteX509Certificate,)
84
+        row_actions = (DownloadX509Certificate, DeleteX509Certificate)

+ 26
- 0
castellan_ui/content/x509_certificates/urls.py View File

@@ -0,0 +1,26 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from castellan_ui.content.x509_certificates import views
14
+from django.conf.urls import url
15
+
16
+urlpatterns = [
17
+    url(r'^$', views.IndexView.as_view(), name='index'),
18
+    url(r'^import/$', views.ImportView.as_view(), name='import'),
19
+    url(r'^(?P<object_id>[^/]+)/$',
20
+        views.DetailView.as_view(),
21
+        name='detail'),
22
+    url(r'^download/$', views.download_cert, name='download'),
23
+    url(r'^(?P<object_id>[^/]+)/download$',
24
+        views.download_cert,
25
+        name='download'),
26
+]

+ 186
- 0
castellan_ui/content/x509_certificates/views.py View File

@@ -0,0 +1,186 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from django.core.urlresolvers import reverse
14
+from django.core.urlresolvers import reverse_lazy
15
+from django.http import HttpResponse
16
+from django.utils.translation import ugettext_lazy as _
17
+
18
+import binascii
19
+from castellan.common.objects import x_509
20
+from castellan_ui.api import client
21
+from castellan_ui.content.x509_certificates import forms as x509_forms
22
+from castellan_ui.content.x509_certificates import tables
23
+from cryptography.hazmat.backends import default_backend
24
+from cryptography.hazmat.primitives import hashes
25
+from cryptography.hazmat.primitives.serialization import Encoding
26
+from cryptography.x509 import load_der_x509_certificate
27
+
28
+from datetime import datetime
29
+from horizon import exceptions
30
+from horizon import forms
31
+from horizon.tables import views as tables_views
32
+from horizon.utils import memoized
33
+from horizon import views
34
+
35
+
36
+def download_cert(request, object_id):
37
+    try:
38
+        obj = client.get(request, object_id)
39
+        der_data = obj.get_encoded()
40
+        cert_obj = load_der_x509_certificate(der_data, default_backend())
41
+        data = cert_obj.public_bytes(Encoding.PEM)
42
+        response = HttpResponse()
43
+        response.write(data)
44
+        response['Content-Disposition'] = ('attachment; '
45
+                                           'filename="%s.pem"' % object_id)
46
+        response['Content-Length'] = str(len(response.content))
47
+        return response
48
+
49
+    except Exception:
50
+        redirect = reverse('horizon:project:x509_certificates:index')
51
+        msg = _('Unable to download x509_certificate "%s".')\
52
+            % (object_id)
53
+        exceptions.handle(request, msg, redirect=redirect)
54
+
55
+
56
+class IndexView(tables_views.MultiTableView):
57
+    table_classes = [
58
+        tables.X509CertificateTable
59
+    ]
60
+    template_name = 'x509_certificates.html'
61
+
62
+    def get_x509_certificate_data(self):
63
+        try:
64
+            return client.list(self.request, object_type=x_509.X509)
65
+        except Exception as e:
66
+            msg = _('Unable to list certificates: "%s".') % (e.message)
67
+            exceptions.handle(self.request, msg)
68
+            return []
69
+
70
+
71
+class ImportView(forms.ModalFormView):
72
+    form_class = x509_forms.ImportX509Certificate
73
+    template_name = 'x509_certificate_import.html'
74
+    submit_url = reverse_lazy(
75
+        "horizon:project:x509_certificates:import")
76
+    success_url = reverse_lazy('horizon:project:x509_certificates:index')
77
+    submit_label = page_title = _("Import X.509 Certificate")
78
+
79
+    def get_form(self, form_class=None):
80
+        if form_class is None:
81
+            form_class = self.get_form_class()
82
+        return form_class(self.request, **self.get_form_kwargs())
83
+
84
+    def get_object_id(self, key_uuid):
85
+        return key_uuid
86
+
87
+
88
+class DetailView(views.HorizonTemplateView):
89
+    template_name = 'x509_certificate_detail.html'
90
+    page_title = _("X.509 Certificate Details")
91
+
92
+    @memoized.memoized_method
93
+    def _get_data(self):
94
+        try:
95
+            obj = client.get(self.request, self.kwargs['object_id'])
96
+        except Exception:
97
+            redirect = reverse('horizon:project:x509_certificates:index')
98
+            msg = _('Unable to retrieve details for x509_certificate "%s".')\
99
+                % (self.kwargs['object_id'])
100
+            exceptions.handle(self.request, msg,
101
+                              redirect=redirect)
102
+        return obj
103
+
104
+    @memoized.memoized_method
105
+    def _get_data_created_date(self, obj):
106
+        try:
107
+            created_date = datetime.utcfromtimestamp(obj.created).isoformat()
108
+        except Exception:
109
+            redirect = reverse('horizon:project:x509_certificates:index')
110
+            msg = _('Unable to retrieve details for x509_certificate "%s".')\
111
+                % (self.kwargs['object_id'])
112
+            exceptions.handle(self.request, msg,
113
+                              redirect=redirect)
114
+        return created_date
115
+
116
+    @memoized.memoized_method
117
+    def _get_crypto_obj(self, obj):
118
+        der_data = obj.get_encoded()
119
+        return load_der_x509_certificate(der_data, default_backend())
120
+
121
+    @memoized.memoized_method
122
+    def _get_certificate_version(self, obj):
123
+        return self._get_crypto_obj(obj).version
124
+
125
+    @memoized.memoized_method
126
+    def _get_certificate_fingerprint(self, obj):
127
+        return binascii.hexlify(
128
+            self._get_crypto_obj(obj).fingerprint(hashes.SHA256()))
129
+
130
+    @memoized.memoized_method
131
+    def _get_serial_number(self, obj):
132
+        return self._get_crypto_obj(obj).serial_number
133
+
134
+    @memoized.memoized_method
135
+    def _get_validity_start(self, obj):
136
+        return self._get_crypto_obj(obj).not_valid_before
137
+
138
+    @memoized.memoized_method
139
+    def _get_validity_end(self, obj):
140
+        return self._get_crypto_obj(obj).not_valid_after
141
+
142
+    @memoized.memoized_method
143
+    def _get_issuer(self, obj):
144
+        result = ""
145
+        issuer = self._get_crypto_obj(obj).issuer
146
+        for attribute in issuer:
147
+            result = (result + str(attribute.oid._name) + "=" +
148
+                      str(attribute.value) + ",")
149
+        return result[:-1]
150
+
151
+    @memoized.memoized_method
152
+    def _get_subject(self, obj):
153
+        result = ""
154
+        issuer = self._get_crypto_obj(obj).subject
155
+        for attribute in issuer:
156
+            result = (result + str(attribute.oid._name) + "=" +
157
+                      str(attribute.value) + ",")
158
+        return result[:-1]
159
+
160
+    @memoized.memoized_method
161
+    def _get_data_bytes(self, obj):
162
+        try:
163
+            data = self._get_crypto_obj(obj).public_bytes(Encoding.PEM)
164
+        except Exception:
165
+            redirect = reverse('horizon:project:x509_certificates:index')
166
+            msg = _('Unable to retrieve details for x509_certificate "%s".')\
167
+                % (self.kwargs['object_id'])
168
+            exceptions.handle(self.request, msg,
169
+                              redirect=redirect)
170
+        return data
171
+
172
+    def get_context_data(self, **kwargs):
173
+        """Gets the context data for key."""
174
+        context = super(DetailView, self).get_context_data(**kwargs)
175
+        obj = self._get_data()
176
+        context['object'] = obj
177
+        context['object_created_date'] = self._get_data_created_date(obj)
178
+        context['object_bytes'] = self._get_data_bytes(obj)
179
+        context['cert_version'] = self._get_certificate_version(obj)
180
+        context['cert_fingerprint'] = self._get_certificate_fingerprint(obj)
181
+        context['cert_serial_number'] = self._get_serial_number(obj)
182
+        context['cert_validity_start'] = self._get_validity_start(obj)
183
+        context['cert_validity_end'] = self._get_validity_end(obj)
184
+        context['cert_issuer'] = self._get_issuer(obj)
185
+        context['cert_subject'] = self._get_subject(obj)
186
+        return context

+ 23
- 0
castellan_ui/enabled/_91_project_key_manager_x509_certificates_panel.py View File

@@ -0,0 +1,23 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+# The slug of the panel to be added to HORIZON_CONFIG. Required.
14
+PANEL = 'x509_certificates'
15
+# The slug of the panel group the PANEL is associated with.
16
+PANEL_GROUP = 'key_manager'
17
+# The slug of the dashboard the PANEL associated with. Required.
18
+PANEL_DASHBOARD = 'project'
19
+
20
+ADD_INSTALLED_APP = ['castellan_ui', ]
21
+
22
+# Python panel class of the PANEL to be added.
23
+ADD_PANEL = 'castellan_ui.content.x509_certificates.panel.X509Certificates'

+ 54
- 0
castellan_ui/templates/_object_import.html View File

@@ -0,0 +1,54 @@
1
+{% extends "horizon/common/_modal.html" %}
2
+{% block content %}
3
+  {% if table %}
4
+    <div class="modal-body">
5
+      {{ table.render }}
6
+    </div>
7
+  {% endif %}
8
+  <form id="{% block form_id %}{{ form_id }}{% endblock %}"
9
+        ng-controller="{% block ng_controller %}DummyController{% endblock %}"
10
+        name="{% block form_name %}{% endblock %}"
11
+        autocomplete="{% block autocomplete %}{% if form.no_autocomplete %}off{% endif %}{% endblock %}"
12
+        class="{% block form_class %}{% endblock %}"
13
+        action="{% block form_action %}{{ submit_url }}{% endblock %}"
14
+        method="{% block form-method %}POST{% endblock %}"
15
+      {% block form_validation %}{% endblock %}
16
+        {% if add_to_field %}data-add-to-field="{{ add_to_field }}"{% endif %} {% block form_attrs %}enctype="multipart/form-data"{% endblock %}>{% csrf_token %}
17
+    <div class="modal-body clearfix">
18
+      {% comment %}
19
+        These fake fields are required to prevent Chrome v34+ from autofilling form.
20
+        {% endcomment %}
21
+      {% if form.no_autocomplete %}
22
+      <div class="fake_credentials" style="display: none">
23
+        <input type="text" name="fake_email" value="" />
24
+        <input type="password" name="fake_password" value="" />
25
+      </div>
26
+      {% endif %}
27
+      {% block modal-body %}
28
+        <div class="row">
29
+          <div class="col-sm-6">
30
+            <fieldset>
31
+            {% include "horizon/common/_form_fields.html" %}
32
+            </fieldset>
33
+          </div>
34
+          <div class="col-sm-6">
35
+            {% block modal-body-right %}{% endblock %}
36
+          </div>
37
+        </div>
38
+      {% endblock %}
39
+    </div>
40
+    <div class="modal-footer">
41
+      {% block modal-footer %}
42
+        {% if cancel_url %}
43
+        <a href="{% block cancel_url %}{{ cancel_url }}{% endblock %}"
44
+           class="btn btn-default cancel">
45
+          {{ cancel_label }}
46
+        </a>
47
+        {% endif %}
48
+        <input class="btn btn-primary" type="submit" value="{{ submit_label }}">
49
+      {% endblock %}
50
+    </div>
51
+  </form>
52
+{% endblock %}
53
+{% load i18n %}
54
+

+ 9
- 0
castellan_ui/templates/_x509_certificate_import.html View File

@@ -0,0 +1,9 @@
1
+{% extends '_object_import.html' %}
2
+{% load i18n %}
3
+
4
+{% block modal-body-right %}
5
+  <p>{% trans "X.509 certificates can be imported if they are in Privacy Enhanced Mail (PEM) format." %}</p>
6
+  <p>{% trans "Your PEM formatted certificate will look like this:" %}</p>
7
+  <p><pre>-----BEGIN CERTIFICATE-----<br>&lt;base64-encoded data&gt;<br>-----END CERTIFICATE-----</pre></p>
8
+{% endblock %}
9
+

+ 37
- 0
castellan_ui/templates/x509_certificate_detail.html View File

@@ -0,0 +1,37 @@
1
+{% extends 'base.html' %}
2
+{% load i18n parse_date %}
3
+
4
+{% block title %}{{ page_title }}{% endblock %}
5
+
6
+{% block page_header %}
7
+  {% include "horizon/common/_detail_header.html" %}
8
+{% endblock %}
9
+
10
+{% block main %}
11
+<div class="detail">
12
+  <dl class="dl-horizontal">
13
+    <dt>{% trans "Name" %}</dt>
14
+    <dd>{{ object.name|default:_("None") }}</dd>
15
+    <dt>{% trans "Created" %}</dt>
16
+    <dd>{{ object_created_date|parse_date}}</dd>
17
+    <dt>{% trans "Certificate Version" %}</dt>
18
+    <dd>{{ cert_version}}</dd>
19
+    <dt>{% trans "SHA-256 Fingerprint" %}</dt>
20
+    <dd>{{ cert_fingerprint}}</dd>
21
+    <dt>{% trans "Serial Number" %}</dt>
22
+    <dd>{{ cert_serial_number}}</dd>
23
+    <dt>{% trans "Valid From" %}</dt>
24
+    <dd>{{ cert_validity_start }}</dd>
25
+    <dt>{% trans "Valid To" %}</dt>
26
+    <dd>{{ cert_validity_end }}</dd>
27
+    <dt>{% trans "Issuer" %}</dt>
28
+    <dd>{{ cert_issuer}}</dd>
29
+    <dt>{% trans "Subject" %}</dt>
30
+    <dd>{{ cert_subject}}</dd>
31
+    <dt>{% trans "Raw Certificate Value" %}</dt>
32
+    <dd>
33
+      <div style="white-space: pre-wrap; font-family: monospace">{{ object_bytes|default:_("None") }}</div>
34
+    </dd>
35
+  </dl>
36
+</div>
37
+{% endblock %}

+ 7
- 0
castellan_ui/templates/x509_certificate_import.html View File

@@ -0,0 +1,7 @@
1
+{% extends 'base.html' %}
2
+{% load i18n %}
3
+{% block title %}{{ page_title }}{% endblock %}
4
+
5
+{% block main %}
6
+  {% include '_x509_certificate_import.html' %}
7
+{% endblock %}

+ 23
- 0
castellan_ui/templates/x509_certificates.html View File

@@ -0,0 +1,23 @@
1
+{% extends 'base.html' %}
2
+{% load i18n %}
3
+{% block title %}{% trans "X.509 Certificates" %}{% endblock %}
4
+
5
+{% block breadcrumb_nav %}
6
+  <ol class = "breadcrumb">
7
+    <li>{% trans "Project" %}</li>
8
+    <li>{% trans "Key Manager" %}</li>
9
+    <li class="active">{% trans "X.509 Certificates" %}</li>
10
+  </ol>
11
+{% endblock %}
12
+
13
+{% block page_header %}
14
+  <hz-page-header header="{% trans "X.509 Certificates" %}"></hz-page-header>
15
+{% endblock page_header %}
16
+
17
+{% block main %}
18
+<div class="row">
19
+  <div class="col-sm-12">
20
+  {{ x509_certificate_table.render }}
21
+  </div>
22
+</div>
23
+{% endblock %}

+ 0
- 0
castellan_ui/test/content/__init__.py View File


+ 0
- 0
castellan_ui/test/content/x509_certificates/__init__.py View File


+ 108
- 0
castellan_ui/test/content/x509_certificates/tests.py View File

@@ -0,0 +1,108 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+import base64
14
+from django.core.handlers import wsgi
15
+from django.core.urlresolvers import reverse
16
+from horizon import messages as horizon_messages
17
+import mock
18
+
19
+from castellan.common.objects import x_509
20
+from castellan_ui.api import client as api_castellan
21
+from castellan_ui.test import helpers as tests
22
+from castellan_ui.test import test_data
23
+
24
+INDEX_URL = reverse('horizon:project:x509_certificates:index')
25
+
26
+
27
+class X509CertificatesViewTest(tests.APITestCase):
28
+
29
+    def setUp(self):
30
+        super(X509CertificatesViewTest, self).setUp()
31
+        self.cert = test_data.x509_cert
32
+        self.cert_b64_bytes = base64.b64encode(self.cert.get_encoded())
33
+        self.mock_object(
34
+            api_castellan, "get", mock.Mock(return_value=self.cert))
35
+        self.mock_object(api_castellan, "list", mock.Mock(return_value=[]))
36
+        self.mock_object(horizon_messages, "success")
37
+        FAKE_ENVIRON = {'REQUEST_METHOD': 'GET', 'wsgi.input': 'fake_input'}
38
+        self.request = wsgi.WSGIRequest(FAKE_ENVIRON)
39
+
40
+    def test_index(self):
41
+        cert_list = [test_data.x509_cert, test_data.nameless_x509_cert]
42
+
43
+        self.mock_object(
44
+            api_castellan, "list", mock.Mock(return_value=cert_list))
45
+
46
+        res = self.client.get(INDEX_URL)
47
+        self.assertEqual(res.status_code, 200)
48
+        self.assertTemplateUsed(res, 'x509_certificates.html')
49
+        api_castellan.list.assert_called_with(mock.ANY, object_type=x_509.X509)
50
+
51
+    def test_detail_view(self):
52
+        url = reverse('horizon:project:x509_certificates:detail',
53
+                      args=[self.cert.id])
54
+        self.mock_object(
55
+            api_castellan, "list", mock.Mock(return_value=[self.cert]))
56
+        self.mock_object(
57
+            api_castellan, "get", mock.Mock(return_value=self.cert))
58
+
59
+        res = self.client.get(url)
60
+        self.assertContains(
61
+            res, "<dt>Name</dt>\n    <dd>%s</dd>" % self.cert.name, 1, 200)
62
+        api_castellan.get.assert_called_once_with(mock.ANY, self.cert.id)
63
+
64
+    def test_import_cert(self):
65
+        self.mock_object(
66
+            api_castellan, "list", mock.Mock(return_value=[self.cert]))
67
+        url = reverse('horizon:project:x509_certificates:import')
68
+        self.mock_object(
69
+            api_castellan, "import_object", mock.Mock(return_value=self.cert))
70
+
71
+        cert_input = (
72
+            u"-----BEGIN CERTIFICATE-----\n" +
73
+            self.cert_b64_bytes.decode("utf-8") +
74
+            u"\n-----END CERTIFICATE-----"
75
+        )
76
+
77
+        cert_form_data = {
78
+            'source_type': 'raw',
79
+            'name': self.cert.name,
80
+            'direct_input': cert_input
81
+        }
82
+
83
+        self.client.post(url, cert_form_data)
84
+
85
+        api_castellan.import_object.assert_called_once_with(
86
+            mock.ANY,
87
+            object_type=x_509.X509,
88
+            data=self.cert_b64_bytes.decode('utf-8'),
89
+            name=self.cert.name
90
+        )
91
+
92
+    def test_delete_cert(self):
93
+        self.mock_object(
94
+            api_castellan, "list", mock.Mock(return_value=[self.cert]))
95
+        self.mock_object(api_castellan, "delete")
96
+
97
+        cert_form_data = {
98
+            'action': 'x509_certificate__delete__%s' % self.cert.id
99
+        }
100
+
101
+        res = self.client.post(INDEX_URL, cert_form_data)
102
+
103
+        api_castellan.list.assert_called_with(mock.ANY, object_type=x_509.X509)
104
+        api_castellan.delete.assert_called_once_with(
105
+            mock.ANY,
106
+            self.cert.id,
107
+        )
108
+        self.assertRedirectsNoFollow(res, INDEX_URL)

+ 26
- 0
castellan_ui/test/test_data.py View File

@@ -0,0 +1,26 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from castellan.common import objects
14
+from castellan.tests import utils as castellan_utils
15
+
16
+x509_cert = objects.x_509.X509(
17
+    data=castellan_utils.get_certificate_der(),
18
+    name='test cert',
19
+    created=1448088699,
20
+    id=u'00000000-0000-0000-0000-000000000000')
21
+
22
+nameless_x509_cert = objects.x_509.X509(
23
+    data=castellan_utils.get_certificate_der(),
24
+    name=None,
25
+    created=1448088699,
26
+    id=u'11111111-1111-1111-1111-111111111111')

+ 1
- 1
tox.ini View File

@@ -65,4 +65,4 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen
65 65
 
66 66
 [flake8]
67 67
 exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,node_modules,.tmp
68
-max-complexity = 20
68
+max-complexity = 20

Loading…
Cancel
Save