Browse Source

Add Public and Private Key Panels

Change-Id: I2c9a1c6562bb3111c558a6a46a2f90b63d048366
tags/0.1.0
Kaitlin Farr 1 year ago
parent
commit
32c35f6f20
33 changed files with 1400 additions and 0 deletions
  1. 2
    0
      README.rst
  2. 0
    0
      castellan_ui/content/private_keys/__init__.py
  3. 47
    0
      castellan_ui/content/private_keys/forms.py
  4. 23
    0
      castellan_ui/content/private_keys/panel.py
  5. 98
    0
      castellan_ui/content/private_keys/tables.py
  6. 27
    0
      castellan_ui/content/private_keys/urls.py
  7. 150
    0
      castellan_ui/content/private_keys/views.py
  8. 0
    0
      castellan_ui/content/public_keys/__init__.py
  9. 46
    0
      castellan_ui/content/public_keys/forms.py
  10. 23
    0
      castellan_ui/content/public_keys/panel.py
  11. 98
    0
      castellan_ui/content/public_keys/tables.py
  12. 27
    0
      castellan_ui/content/public_keys/urls.py
  13. 147
    0
      castellan_ui/content/public_keys/views.py
  14. 195
    0
      castellan_ui/content/shared_forms.py
  15. 23
    0
      castellan_ui/enabled/_92_project_key_manager_private_key_panel.py
  16. 23
    0
      castellan_ui/enabled/_93_project_key_manager_public_key_panel.py
  17. 9
    0
      castellan_ui/templates/_private_key_generate.html
  18. 9
    0
      castellan_ui/templates/_private_key_import.html
  19. 10
    0
      castellan_ui/templates/_public_key_generate.html
  20. 9
    0
      castellan_ui/templates/_public_key_import.html
  21. 27
    0
      castellan_ui/templates/private_key_detail.html
  22. 7
    0
      castellan_ui/templates/private_key_generate.html
  23. 7
    0
      castellan_ui/templates/private_key_import.html
  24. 23
    0
      castellan_ui/templates/private_keys.html
  25. 27
    0
      castellan_ui/templates/public_key_detail.html
  26. 7
    0
      castellan_ui/templates/public_key_generate.html
  27. 7
    0
      castellan_ui/templates/public_key_import.html
  28. 23
    0
      castellan_ui/templates/public_keys.html
  29. 0
    0
      castellan_ui/test/content/private_keys/__init__.py
  30. 137
    0
      castellan_ui/test/content/private_keys/tests.py
  31. 0
    0
      castellan_ui/test/content/public_keys/__init__.py
  32. 137
    0
      castellan_ui/test/content/public_keys/tests.py
  33. 32
    0
      castellan_ui/test/test_data.py

+ 2
- 0
README.rst View File

@@ -55,6 +55,8 @@ 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 57
     ln -s ../castellan-ui/castellan_ui/enabled/_91_project_key_manager_x509_certificates_panel.py openstack_dashboard/local/enabled
58
+    ln -s ../castellan-ui/castellan_ui/enabled/_92_project_key_manager_private_key_panel.py openstack_dashboard/local/enabled
59
+    ln -s ../castellan-ui/castellan_ui/enabled/_93_project_key_manager_public_key_panel.py openstack_dashboard/local/enabled
58 60
 
59 61
 To run horizon with the newly enabled Castellan UI plugin run::
60 62
 

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


+ 47
- 0
castellan_ui/content/private_keys/forms.py View File

@@ -0,0 +1,47 @@
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
+
17
+from castellan.common.objects import private_key
18
+from cryptography.hazmat.backends import default_backend
19
+from cryptography.hazmat.primitives import serialization
20
+from cryptography.hazmat.primitives.serialization import load_pem_private_key
21
+
22
+from castellan_ui.content import shared_forms
23
+
24
+
25
+class ImportPrivateKey(shared_forms.ImportKey):
26
+
27
+    def __init__(self, request, *args, **kwargs):
28
+        super(ImportPrivateKey, self).__init__(
29
+            request, *args, algorithms=shared_forms.KEY_PAIR_ALGORITHMS,
30
+            **kwargs)
31
+        self.fields['direct_input'].help_text = _(
32
+            "PEM formatted private key.")
33
+        self.fields['key_file'].help_text = _(
34
+            "PEM formatted private key file.")
35
+
36
+    def clean_key_data(self, key_data):
37
+        key_obj = load_pem_private_key(
38
+            key_data.encode('utf-8'), password=None, backend=default_backend())
39
+        key_der = key_obj.private_bytes(
40
+            encoding=serialization.Encoding.DER,
41
+            format=serialization.PrivateFormat.PKCS8,
42
+            encryption_algorithm=serialization.NoEncryption())
43
+        return base64.b64encode(key_der)
44
+
45
+    def handle(self, request, data):
46
+        return super(ImportPrivateKey, self).handle(
47
+            request, data, private_key.PrivateKey)

+ 23
- 0
castellan_ui/content/private_keys/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 PrivateKeys(horizon.Panel):
22
+    name = _("Private Keys")
23
+    slug = "private_keys"

+ 98
- 0
castellan_ui/content/private_keys/tables.py View File

@@ -0,0 +1,98 @@
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 GeneratePrivateKey(tables.LinkAction):
24
+    name = "generate_private_key"
25
+    verbose_name = _("Generate Key Pair")
26
+    url = "horizon:project:private_keys:generate"
27
+    classes = ("ajax-modal",)
28
+    icon = "plus"
29
+    policy_rules = ()
30
+
31
+
32
+class ImportPrivateKey(tables.LinkAction):
33
+    name = "import_private_key"
34
+    verbose_name = _("Import Private Key")
35
+    url = "horizon:project:private_keys:import"
36
+    classes = ("ajax-modal",)
37
+    icon = "upload"
38
+    policy_rules = ()
39
+
40
+
41
+class DownloadKey(tables.LinkAction):
42
+    name = "download"
43
+    verbose_name = _("Download Key")
44
+    url = "horizon:project:private_keys:download"
45
+    classes = ("btn-download",)
46
+    policy_rules = ()
47
+
48
+    def get_link_url(self, datum):
49
+        return reverse(self.url,
50
+                       kwargs={'object_id': datum.id})
51
+
52
+
53
+class DeletePrivateKey(tables.DeleteAction):
54
+    policy_rules = ()
55
+    help_text = _("You should not delete a private key unless you are "
56
+                  "certain it is not being used anywhere. If there was a "
57
+                  "public key generated with this private key, it will not "
58
+                  "be deleted.")
59
+
60
+    @staticmethod
61
+    def action_present(count):
62
+        return ungettext_lazy(
63
+            u"Delete Private Key",
64
+            u"Delete Private Keys",
65
+            count
66
+        )
67
+
68
+    @staticmethod
69
+    def action_past(count):
70
+        return ungettext_lazy(
71
+            u"Deleted Private Key",
72
+            u"Deleted Private Keys",
73
+            count
74
+        )
75
+
76
+    def delete(self, request, obj_id):
77
+        client.delete(request, obj_id)
78
+
79
+
80
+class PrivateKeyTable(tables.DataTable):
81
+    detail_link = "horizon:project:private_keys:detail"
82
+    uuid = tables.Column("id", verbose_name=_("Key ID"), link=detail_link)
83
+    name = tables.Column("name", verbose_name=_("Name"))
84
+    algorithm = tables.Column("algorithm", verbose_name=_("Algorithm"))
85
+    bit_length = tables.Column("bit_length", verbose_name=_("Bit Length"))
86
+    created_date = tables.Column("created",
87
+                                 verbose_name=_("Created Date"),
88
+                                 filters=(filters.timestamp_to_iso,))
89
+
90
+    def get_object_display(self, datum):
91
+        return datum.name if datum.name else datum.id
92
+
93
+    class Meta(object):
94
+        name = "private_key"
95
+        table_actions = (GeneratePrivateKey,
96
+                         ImportPrivateKey,
97
+                         DeletePrivateKey,)
98
+        row_actions = (DownloadKey, DeletePrivateKey)

+ 27
- 0
castellan_ui/content/private_keys/urls.py View File

@@ -0,0 +1,27 @@
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.private_keys 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'^generate/$', views.GenerateView.as_view(), name='generate'),
20
+    url(r'^(?P<object_id>[^/]+)/$',
21
+        views.DetailView.as_view(),
22
+        name='detail'),
23
+    url(r'^download/$', views.download_key, name='download'),
24
+    url(r'^(?P<object_id>[^/]+)/download$',
25
+        views.download_key,
26
+        name='download'),
27
+]

+ 150
- 0
castellan_ui/content/private_keys/views.py View File

@@ -0,0 +1,150 @@
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
+from cryptography.hazmat import backends
19
+from cryptography.hazmat.primitives import serialization
20
+from cryptography.hazmat.primitives.serialization import load_der_private_key
21
+
22
+from castellan.common.objects import private_key
23
+from castellan_ui.api import client
24
+from castellan_ui.content.private_keys import forms as private_key_forms
25
+from castellan_ui.content.private_keys import tables
26
+from castellan_ui.content import shared_forms
27
+from datetime import datetime
28
+from horizon import exceptions
29
+from horizon import forms
30
+from horizon.tables import views as tables_views
31
+from horizon.utils import memoized
32
+from horizon import views
33
+
34
+
35
+def download_key(request, object_id):
36
+    try:
37
+        obj = client.get(request, object_id)
38
+        data = obj.get_encoded()
39
+        key_obj = load_der_private_key(
40
+            data, password=None, backend=backends.default_backend())
41
+        key_pem = key_obj.private_bytes(
42
+            encoding=serialization.Encoding.PEM,
43
+            format=serialization.PrivateFormat.PKCS8,
44
+            encryption_algorithm=serialization.NoEncryption())
45
+        response = HttpResponse()
46
+        response.write(key_pem)
47
+        response['Content-Disposition'] = ('attachment; '
48
+                                           'filename="%s.key"' % object_id)
49
+        response['Content-Length'] = str(len(response.content))
50
+        return response
51
+
52
+    except Exception:
53
+        redirect = reverse('horizon:project:private_keys:index')
54
+        msg = _('Unable to download private_key "%s".')\
55
+            % (object_id)
56
+        exceptions.handle(request, msg, redirect=redirect)
57
+
58
+
59
+class IndexView(tables_views.MultiTableView):
60
+    table_classes = [
61
+        tables.PrivateKeyTable
62
+    ]
63
+    template_name = 'private_keys.html'
64
+
65
+    def get_private_key_data(self):
66
+        try:
67
+            return client.list(
68
+                self.request, object_type=private_key.PrivateKey)
69
+        except Exception as e:
70
+            msg = _('Unable to list private keys: "%s".') % (e.message)
71
+            exceptions.handle(self.request, msg)
72
+            return []
73
+
74
+
75
+class GenerateView(forms.ModalFormView):
76
+    form_class = shared_forms.GenerateKeyPair
77
+    template_name = 'private_key_generate.html'
78
+    submit_url = reverse_lazy(
79
+        "horizon:project:private_keys:generate")
80
+    success_url = reverse_lazy('horizon:project:private_keys:index')
81
+    submit_label = page_title = _("Generate Key Pair")
82
+
83
+
84
+class ImportView(forms.ModalFormView):
85
+    form_class = private_key_forms.ImportPrivateKey
86
+    template_name = 'private_key_import.html'
87
+    submit_url = reverse_lazy(
88
+        "horizon:project:private_keys:import")
89
+    success_url = reverse_lazy('horizon:project:private_keys:index')
90
+    submit_label = page_title = _("Import Private Key")
91
+
92
+    def get_object_id(self, key_uuid):
93
+        return key_uuid
94
+
95
+
96
+class DetailView(views.HorizonTemplateView):
97
+    template_name = 'private_key_detail.html'
98
+    page_title = _("Private Key Details")
99
+
100
+    @memoized.memoized_method
101
+    def _get_data(self):
102
+        try:
103
+            obj = client.get(self.request, self.kwargs['object_id'])
104
+        except Exception:
105
+            redirect = reverse('horizon:project:private_keys:index')
106
+            msg = _('Unable to retrieve details for private_key "%s".')\
107
+                % (self.kwargs['object_id'])
108
+            exceptions.handle(self.request, msg,
109
+                              redirect=redirect)
110
+        return obj
111
+
112
+    @memoized.memoized_method
113
+    def _get_data_created_date(self, obj):
114
+        try:
115
+            created_date = datetime.utcfromtimestamp(obj.created).isoformat()
116
+        except Exception:
117
+            redirect = reverse('horizon:project:private_keys:index')
118
+            msg = _('Unable to retrieve details for private_key "%s".')\
119
+                % (self.kwargs['object_id'])
120
+            exceptions.handle(self.request, msg,
121
+                              redirect=redirect)
122
+        return created_date
123
+
124
+    @memoized.memoized_method
125
+    def _get_data_bytes(self, obj):
126
+        try:
127
+            key = serialization.load_der_private_key(
128
+                obj.get_encoded(),
129
+                backend=backends.default_backend(),
130
+                password=None)
131
+            data_bytes = key.private_bytes(
132
+                encoding=serialization.Encoding.PEM,
133
+                format=serialization.PrivateFormat.PKCS8,
134
+                encryption_algorithm=serialization.NoEncryption())
135
+        except Exception:
136
+            redirect = reverse('horizon:project:private_keys:index')
137
+            msg = _('Unable to retrieve details for private_key "%s".')\
138
+                % (self.kwargs['object_id'])
139
+            exceptions.handle(self.request, msg,
140
+                              redirect=redirect)
141
+        return data_bytes
142
+
143
+    def get_context_data(self, **kwargs):
144
+        """Gets the context data for key."""
145
+        context = super(DetailView, self).get_context_data(**kwargs)
146
+        obj = self._get_data()
147
+        context['object'] = obj
148
+        context['object_created_date'] = self._get_data_created_date(obj)
149
+        context['object_bytes'] = self._get_data_bytes(obj)
150
+        return context

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


+ 46
- 0
castellan_ui/content/public_keys/forms.py View File

@@ -0,0 +1,46 @@
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
+
17
+from castellan.common.objects import public_key
18
+from cryptography.hazmat.backends import default_backend
19
+from cryptography.hazmat.primitives import serialization
20
+from cryptography.hazmat.primitives.serialization import load_pem_public_key
21
+
22
+from castellan_ui.content import shared_forms
23
+
24
+
25
+class ImportPublicKey(shared_forms.ImportKey):
26
+
27
+    def __init__(self, request, *args, **kwargs):
28
+        super(ImportPublicKey, self).__init__(
29
+            request, *args, algorithms=shared_forms.KEY_PAIR_ALGORITHMS,
30
+            **kwargs)
31
+        self.fields['direct_input'].help_text = _(
32
+            "PEM formatted public key.")
33
+        self.fields['key_file'].help_text = _(
34
+            "PEM formatted public key file.")
35
+
36
+    def clean_key_data(self, key_data):
37
+        key_obj = load_pem_public_key(
38
+            key_data.encode('utf-8'), backend=default_backend())
39
+        key_der = key_obj.public_bytes(
40
+            encoding=serialization.Encoding.DER,
41
+            format=serialization.PublicFormat.SubjectPublicKeyInfo)
42
+        return base64.b64encode(key_der)
43
+
44
+    def handle(self, request, data):
45
+        return super(ImportPublicKey, self).handle(
46
+            request, data, public_key.PublicKey)

+ 23
- 0
castellan_ui/content/public_keys/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 PublicKeys(horizon.Panel):
22
+    name = _("Public Keys")
23
+    slug = "public_keys"

+ 98
- 0
castellan_ui/content/public_keys/tables.py View File

@@ -0,0 +1,98 @@
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 GeneratePublicKey(tables.LinkAction):
24
+    name = "generate_public_key"
25
+    verbose_name = _("Generate Key Pair")
26
+    url = "horizon:project:public_keys:generate"
27
+    classes = ("ajax-modal",)
28
+    icon = "plus"
29
+    policy_rules = ()
30
+
31
+
32
+class ImportPublicKey(tables.LinkAction):
33
+    name = "import_public_key"
34
+    verbose_name = _("Import Public Key")
35
+    url = "horizon:project:public_keys:import"
36
+    classes = ("ajax-modal",)
37
+    icon = "upload"
38
+    policy_rules = ()
39
+
40
+
41
+class DownloadKey(tables.LinkAction):
42
+    name = "download"
43
+    verbose_name = _("Download Key")
44
+    url = "horizon:project:public_keys:download"
45
+    classes = ("btn-download",)
46
+    policy_rules = ()
47
+
48
+    def get_link_url(self, datum):
49
+        return reverse(self.url,
50
+                       kwargs={'object_id': datum.id})
51
+
52
+
53
+class DeletePublicKey(tables.DeleteAction):
54
+    policy_rules = ()
55
+    help_text = _("You should not delete a public key unless you are "
56
+                  "certain it is not being used anywhere.  If there was a "
57
+                  "private key generated with this public key, it will not "
58
+                  "be deleted.")
59
+
60
+    @staticmethod
61
+    def action_present(count):
62
+        return ungettext_lazy(
63
+            u"Delete Public Key",
64
+            u"Delete Public Keys",
65
+            count
66
+        )
67
+
68
+    @staticmethod
69
+    def action_past(count):
70
+        return ungettext_lazy(
71
+            u"Deleted Public Key",
72
+            u"Deleted Public Keys",
73
+            count
74
+        )
75
+
76
+    def delete(self, request, obj_id):
77
+        client.delete(request, obj_id)
78
+
79
+
80
+class PublicKeyTable(tables.DataTable):
81
+    detail_link = "horizon:project:public_keys:detail"
82
+    uuid = tables.Column("id", verbose_name=_("Key ID"), link=detail_link)
83
+    name = tables.Column("name", verbose_name=_("Name"))
84
+    algorithm = tables.Column("algorithm", verbose_name=_("Algorithm"))
85
+    bit_length = tables.Column("bit_length", verbose_name=_("Bit Length"))
86
+    created_date = tables.Column("created",
87
+                                 verbose_name=_("Created Date"),
88
+                                 filters=(filters.timestamp_to_iso,))
89
+
90
+    def get_object_display(self, datum):
91
+        return datum.name if datum.name else datum.id
92
+
93
+    class Meta(object):
94
+        name = "public_key"
95
+        table_actions = (GeneratePublicKey,
96
+                         ImportPublicKey,
97
+                         DeletePublicKey,)
98
+        row_actions = (DownloadKey, DeletePublicKey)

+ 27
- 0
castellan_ui/content/public_keys/urls.py View File

@@ -0,0 +1,27 @@
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.public_keys 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'^generate/$', views.GenerateView.as_view(), name='generate'),
20
+    url(r'^(?P<object_id>[^/]+)/$',
21
+        views.DetailView.as_view(),
22
+        name='detail'),
23
+    url(r'^download/$', views.download_key, name='download'),
24
+    url(r'^(?P<object_id>[^/]+)/download$',
25
+        views.download_key,
26
+        name='download'),
27
+]

+ 147
- 0
castellan_ui/content/public_keys/views.py View File

@@ -0,0 +1,147 @@
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
+from cryptography.hazmat import backends
19
+from cryptography.hazmat.primitives import serialization
20
+
21
+from castellan.common.objects import public_key
22
+from castellan_ui.api import client
23
+from castellan_ui.content.public_keys import forms as public_key_forms
24
+from castellan_ui.content.public_keys import tables
25
+from castellan_ui.content import shared_forms
26
+from datetime import datetime
27
+from horizon import exceptions
28
+from horizon import forms
29
+from horizon.tables import views as tables_views
30
+from horizon.utils import memoized
31
+from horizon import views
32
+
33
+
34
+def download_key(request, object_id):
35
+    try:
36
+        obj = client.get(request, object_id)
37
+        data = obj.get_encoded()
38
+        key_obj = serialization.load_der_public_key(
39
+            data, backend=backends.default_backend())
40
+        key_pem = key_obj.public_bytes(
41
+            encoding=serialization.Encoding.PEM,
42
+            format=serialization.PublicFormat.SubjectPublicKeyInfo)
43
+
44
+        response = HttpResponse()
45
+        response.write(key_pem)
46
+        response['Content-Disposition'] = ('attachment; '
47
+                                           'filename="%s.key"' % object_id)
48
+        response['Content-Length'] = str(len(response.content))
49
+        return response
50
+
51
+    except Exception:
52
+        redirect = reverse('horizon:project:public_keys:index')
53
+        msg = _('Unable to download public_key "%s".')\
54
+            % (object_id)
55
+        exceptions.handle(request, msg, redirect=redirect)
56
+
57
+
58
+class IndexView(tables_views.MultiTableView):
59
+    table_classes = [
60
+        tables.PublicKeyTable
61
+    ]
62
+    template_name = 'public_keys.html'
63
+
64
+    def get_public_key_data(self):
65
+        try:
66
+            return client.list(
67
+                self.request, object_type=public_key.PublicKey)
68
+        except Exception as e:
69
+            msg = _('Unable to list private keys: "%s".') % (e.message)
70
+            exceptions.handle(self.request, msg)
71
+            return []
72
+
73
+
74
+class GenerateView(forms.ModalFormView):
75
+    form_class = shared_forms.GenerateKeyPair
76
+    template_name = 'public_key_generate.html'
77
+    submit_url = reverse_lazy(
78
+        "horizon:project:public_keys:generate")
79
+    success_url = reverse_lazy('horizon:project:public_keys:index')
80
+    submit_label = page_title = _("Generate Key Pair")
81
+
82
+
83
+class ImportView(forms.ModalFormView):
84
+    form_class = public_key_forms.ImportPublicKey
85
+    template_name = 'public_key_import.html'
86
+    submit_url = reverse_lazy(
87
+        "horizon:project:public_keys:import")
88
+    success_url = reverse_lazy('horizon:project:public_keys:index')
89
+    submit_label = page_title = _("Import Public Key")
90
+
91
+    def get_object_id(self, key_uuid):
92
+        return key_uuid
93
+
94
+
95
+class DetailView(views.HorizonTemplateView):
96
+    template_name = 'public_key_detail.html'
97
+    page_title = _("Public Key Details")
98
+
99
+    @memoized.memoized_method
100
+    def _get_data(self):
101
+        try:
102
+            obj = client.get(self.request, self.kwargs['object_id'])
103
+        except Exception:
104
+            redirect = reverse('horizon:project:public_keys:index')
105
+            msg = _('Unable to retrieve details for public_key "%s".')\
106
+                % (self.kwargs['object_id'])
107
+            exceptions.handle(self.request, msg,
108
+                              redirect=redirect)
109
+        return obj
110
+
111
+    @memoized.memoized_method
112
+    def _get_data_created_date(self, obj):
113
+        try:
114
+            created_date = datetime.utcfromtimestamp(obj.created).isoformat()
115
+        except Exception:
116
+            redirect = reverse('horizon:project:public_keys:index')
117
+            msg = _('Unable to retrieve details for public_key "%s".')\
118
+                % (self.kwargs['object_id'])
119
+            exceptions.handle(self.request, msg,
120
+                              redirect=redirect)
121
+        return created_date
122
+
123
+    @memoized.memoized_method
124
+    def _get_data_bytes(self, obj):
125
+        try:
126
+            key = serialization.load_der_public_key(
127
+                obj.get_encoded(),
128
+                backend=backends.default_backend())
129
+            data_bytes = key.public_bytes(
130
+                encoding=serialization.Encoding.PEM,
131
+                format=serialization.PublicFormat.SubjectPublicKeyInfo)
132
+        except Exception:
133
+            redirect = reverse('horizon:project:public_keys:index')
134
+            msg = _('Unable to retrieve details for public_key "%s".')\
135
+                % (self.kwargs['object_id'])
136
+            exceptions.handle(self.request, msg,
137
+                              redirect=redirect)
138
+        return data_bytes
139
+
140
+    def get_context_data(self, **kwargs):
141
+        """Gets the context data for key."""
142
+        context = super(DetailView, self).get_context_data(**kwargs)
143
+        obj = self._get_data()
144
+        context['object'] = obj
145
+        context['object_created_date'] = self._get_data_created_date(obj)
146
+        context['object_bytes'] = self._get_data_bytes(obj)
147
+        return context

+ 195
- 0
castellan_ui/content/shared_forms.py View File

@@ -0,0 +1,195 @@
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 abc
15
+import re
16
+
17
+from django.utils.translation import ugettext_lazy as _
18
+
19
+from horizon import exceptions
20
+from horizon import forms
21
+from horizon import messages
22
+
23
+from castellan_ui.api import client
24
+
25
+
26
+KEY_PAIR_ALGORITHMS = ('RSA', 'DSA')
27
+
28
+NEW_LINES = re.compile(r"\r|\n")
29
+
30
+NAME_REGEX = re.compile(r"^\w+(?:[- ]\w+)*$", re.UNICODE)
31
+ERROR_MESSAGES = {
32
+    'invalid': _('Key name may only contain letters, '
33
+                 'numbers, underscores, spaces, and hyphens '
34
+                 'and may not be white space.')}
35
+
36
+ALG_HELP_TEXT = _(
37
+    "Check which algorithms your key manager supports. "
38
+    "Some common algorithms are: %s") % ', '.join(KEY_PAIR_ALGORITHMS)
39
+LENGTH_HELP_TEXT = _(
40
+    "Only certain bit lengths are valid for each algorithm. "
41
+    "Some common bit lengths are: 1024, 2048")
42
+
43
+
44
+class ListTextWidget(forms.TextInput):
45
+    def __init__(self, data_list, name, *args, **kwargs):
46
+        super(ListTextWidget, self).__init__(*args, **kwargs)
47
+        self._name = name
48
+        self._list = data_list
49
+        self.attrs.update({'list': 'list__%s' % self._name})
50
+
51
+    def render(self, name, value, attrs=None):
52
+        text_html = super(ListTextWidget, self).render(name,
53
+                                                       value,
54
+                                                       attrs=attrs)
55
+        data_list = '<datalist id="list__%s">' % self._name
56
+        for item in self._list:
57
+            data_list += '<option value="%s">' % item
58
+        data_list += '</datalist>'
59
+
60
+        return (text_html + data_list)
61
+
62
+
63
+class ImportKey(forms.SelfHandlingForm):
64
+    algorithm = forms.CharField(label=_("Algorithm"), help_text=ALG_HELP_TEXT)
65
+    bit_length = forms.IntegerField(
66
+        label=_("Bit Length"), min_value=0, help_text=LENGTH_HELP_TEXT)
67
+    name = forms.RegexField(required=False,
68
+                            max_length=255,
69
+                            label=_("Key Name"),
70
+                            regex=NAME_REGEX,
71
+                            error_messages=ERROR_MESSAGES)
72
+    source_type = forms.ChoiceField(
73
+        label=_('Source'),
74
+        required=False,
75
+        choices=[('file', _('Key File')),
76
+                 ('raw', _('Direct Input'))],
77
+        widget=forms.ThemableSelectWidget(
78
+            attrs={'class': 'switchable', 'data-slug': 'source'}))
79
+    key_file = forms.FileField(
80
+        label=_("Choose file"),
81
+        widget=forms.FileInput(
82
+            attrs={'class': 'switched', 'data-switch-on': 'source',
83
+                   'data-source-file': _('Key File')}),
84
+        required=False)
85
+    direct_input = forms.CharField(
86
+        label=_('Key Value'),
87
+        widget=forms.widgets.Textarea(
88
+            attrs={'class': 'switched', 'data-switch-on': 'source',
89
+                   'data-source-raw': _('Key Value')}),
90
+        required=False)
91
+
92
+    def __init__(self, request, *args, **kwargs):
93
+        algorithms = kwargs.pop('algorithms', None)
94
+        super(ImportKey, self).__init__(request, *args, **kwargs)
95
+        self.fields['algorithm'].widget = ListTextWidget(data_list=algorithms,
96
+                                                         name='algorithms')
97
+
98
+    @abc.abstractmethod
99
+    def clean_key_data(self, key_pem):
100
+        """This should be implemented for the specific key import form"""
101
+        return
102
+
103
+    def clean(self):
104
+        data = super(ImportKey, self).clean()
105
+
106
+        # The key can be missing based on particular upload
107
+        # conditions. Code defensively for it here...
108
+        key_file = data.get('key_file', None)
109
+        key_raw = data.get('direct_input', None)
110
+
111
+        if key_raw and key_file:
112
+            raise forms.ValidationError(
113
+                _("Cannot specify both file and direct input."))
114
+        if not key_raw and not key_file:
115
+            raise forms.ValidationError(
116
+                _("No input was provided for the key value."))
117
+        try:
118
+            if key_file:
119
+                key_pem = self.files['key_file'].read()
120
+            else:
121
+                key_pem = data['direct_input']
122
+
123
+            data['key_data'] = self.clean_key_data(key_pem)
124
+
125
+        except Exception as e:
126
+            msg = _('There was a problem loading the key: %s. '
127
+                    'Is the key valid and in the correct format?') % e
128
+            raise forms.ValidationError(msg)
129
+
130
+        return data
131
+
132
+    def handle(self, request, data, key_type):
133
+        try:
134
+            key_uuid = client.import_object(
135
+                request,
136
+                algorithm=data['algorithm'],
137
+                bit_length=data['bit_length'],
138
+                key=data['key_data'],
139
+                name=data['name'],
140
+                object_type=key_type)
141
+
142
+            if data['name']:
143
+                key_identifier = data['name']
144
+            else:
145
+                key_identifier = key_uuid
146
+            messages.success(request,
147
+                             _('Successfully imported key: %s')
148
+                             % key_identifier)
149
+            return key_uuid
150
+        except Exception as e:
151
+            msg = _('Unable to import key: %s')
152
+            messages.error(request, msg % e)
153
+            exceptions.handle(request, ignore=True)
154
+            self.api_error(_('Unable to import key.'))
155
+            return False
156
+
157
+
158
+class GenerateKeyPair(forms.SelfHandlingForm):
159
+    algorithm = forms.CharField(
160
+        label=_("Algorithm"),
161
+        help_text=ALG_HELP_TEXT,
162
+        widget=ListTextWidget(
163
+            data_list=KEY_PAIR_ALGORITHMS, name='algorithm-list'))
164
+    length = forms.IntegerField(
165
+        label=_("Bit Length"),
166
+        min_value=0,
167
+        help_text=LENGTH_HELP_TEXT)
168
+    name = forms.RegexField(required=False,
169
+                            max_length=255,
170
+                            label=_("Key Name"),
171
+                            regex=NAME_REGEX,
172
+                            error_messages=ERROR_MESSAGES)
173
+
174
+    def handle(self, request, data):
175
+        try:
176
+            key_uuid = client.generate_key_pair(
177
+                request,
178
+                algorithm=data['algorithm'],
179
+                length=data['length'],
180
+                name=data['name'])
181
+
182
+            if data['name']:
183
+                key_identifier = data['name']
184
+            else:
185
+                key_identifier = key_uuid
186
+            messages.success(request,
187
+                             _('Successfully generated key pair %s')
188
+                             % key_identifier)
189
+            return key_uuid
190
+        except Exception as e:
191
+            msg = _('Unable to generate key pair: %s')
192
+            messages.error(request, msg % e)
193
+            exceptions.handle(request, ignore=True)
194
+            self.api_error(_('Unable to generate key pair.'))
195
+            return False

+ 23
- 0
castellan_ui/enabled/_92_project_key_manager_private_key_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 = 'private_keys'
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.private_keys.panel.PrivateKeys'

+ 23
- 0
castellan_ui/enabled/_93_project_key_manager_public_key_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 = 'public_keys'
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.public_keys.panel.PublicKeys'

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

@@ -0,0 +1,9 @@
1
+{% extends "horizon/common/_modal_form.html" %}
2
+{% load i18n %}
3
+
4
+{% block modal-body-right %}
5
+  <p>{% trans "Check your key manager to see which algorithms and bit lengths are supported." %}</p>
6
+  <p>{% trans "A key pair consists of a private key and a public key. When you generate a private key, the public key will also be generated, and vice versa." %}</p>
7
+  <p>{% trans "You can find the corresponding public key on the " %}<a href="{% url 'horizon:project:public_keys:index' %}">Public Keys</a> {% trans "page." %}</p>
8
+{% endblock %}
9
+

+ 9
- 0
castellan_ui/templates/_private_key_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 "Private keys can be imported if they are in Privacy Enhanced Mail (PEM) format." %}</p>
6
+  <p>{% trans "Your PEM formatted key will look something like this:" %}</p>
7
+  <p><pre>-----BEGIN PRIVATE KEY-----<br>&lt;base64-encoded data&gt;<br>-----END PRIVATE KEY-----</pre></p>
8
+{% endblock %}
9
+

+ 10
- 0
castellan_ui/templates/_public_key_generate.html View File

@@ -0,0 +1,10 @@
1
+{% extends "horizon/common/_modal_form.html" %}
2
+{% load i18n %}
3
+
4
+{% block modal-body-right %}
5
+  <p>{% trans "Check your key manager to see which algorithms and bit lengths are supported." %}</p>
6
+  <p>{% trans "A key pair consists of a private key and a public key. When you generate a public key, the private key will also be generated, and vice versa." %}</p>
7
+  <p>{% trans "You can find the corresponding private key on the " %}<a href="{% url 'horizon:project:private_keys:index' %}">Private Keys</a> {% trans "page." %}</p>
8
+
9
+{% endblock %}
10
+

+ 9
- 0
castellan_ui/templates/_public_key_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 "Public keys can be imported if they are in Privacy Enhanced Mail (PEM) format." %}</p>
6
+  <p>{% trans "Your PEM formatted key will look something like this:" %}</p>
7
+  <p><pre>-----BEGIN PUBLIC KEY-----<br>&lt;base64-encoded data&gt;<br>-----END PUBLIC KEY-----</pre></p>
8
+{% endblock %}
9
+

+ 27
- 0
castellan_ui/templates/private_key_detail.html View File

@@ -0,0 +1,27 @@
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 "Algorithm" %}</dt>
18
+    <dd>{{ object.algorithm|default:_("None") }}</dd>
19
+    <dt>{% trans "Bit Length" %}</dt>
20
+    <dd>{{ object.bit_length|default:_("None") }}</dd>
21
+    <dt>{% trans "Key" %}</dt>
22
+    <dd>
23
+      <div style="white-space: pre-wrap; font-family: monospace;">{{ object_bytes|default:_("None") }}</div>
24
+    </dd>
25
+  </dl>
26
+</div>
27
+{% endblock %}

+ 7
- 0
castellan_ui/templates/private_key_generate.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 '_private_key_generate.html' %}
7
+{% endblock %}

+ 7
- 0
castellan_ui/templates/private_key_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 '_private_key_import.html' %}
7
+{% endblock %}

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

@@ -0,0 +1,23 @@
1
+{% extends 'base.html' %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Private Keys" %}{% 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 "Private Keys" %}</li>
10
+  </ol>
11
+{% endblock %}
12
+
13
+{% block page_header %}
14
+  <hz-page-header header="{% trans "Private Keys" %}"></hz-page-header>
15
+{% endblock page_header %}
16
+
17
+{% block main %}
18
+<div class="row">
19
+  <div class="col-sm-12">
20
+  {{ private_key_table.render }}
21
+  </div>
22
+</div>
23
+{% endblock %}

+ 27
- 0
castellan_ui/templates/public_key_detail.html View File

@@ -0,0 +1,27 @@
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 "Algorithm" %}</dt>
18
+    <dd>{{ object.algorithm|default:_("None") }}</dd>
19
+    <dt>{% trans "Bit Length" %}</dt>
20
+    <dd>{{ object.bit_length|default:_("None") }}</dd>
21
+    <dt>{% trans "Key" %}</dt>
22
+    <dd>
23
+      <div style="white-space: pre-wrap; font-family: monospace;">{{ object_bytes|default:_("None") }}</div>
24
+    </dd>
25
+  </dl>
26
+</div>
27
+{% endblock %}

+ 7
- 0
castellan_ui/templates/public_key_generate.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 '_public_key_generate.html' %}
7
+{% endblock %}

+ 7
- 0
castellan_ui/templates/public_key_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 '_public_key_import.html' %}
7
+{% endblock %}

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

@@ -0,0 +1,23 @@
1
+{% extends 'base.html' %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Public Keys" %}{% 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 "Public Keys" %}</li>
10
+  </ol>
11
+{% endblock %}
12
+
13
+{% block page_header %}
14
+  <hz-page-header header="{% trans "Public Keys" %}"></hz-page-header>
15
+{% endblock page_header %}
16
+
17
+{% block main %}
18
+<div class="row">
19
+  <div class="col-sm-12">
20
+  {{ public_key_table.render }}
21
+  </div>
22
+</div>
23
+{% endblock %}

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


+ 137
- 0
castellan_ui/test/content/private_keys/tests.py View File

@@ -0,0 +1,137 @@
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 private_key
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:private_keys:index')
25
+
26
+
27
+class PrivateKeysViewTest(tests.APITestCase):
28
+
29
+    def setUp(self):
30
+        super(PrivateKeysViewTest, self).setUp()
31
+        self.key = test_data.private_key
32
+        self.key_b64_bytes = base64.b64encode(self.key.get_encoded())
33
+        self.mock_object(
34
+            api_castellan, "get", mock.Mock(return_value=self.key))
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
+        key_list = [test_data.private_key, test_data.nameless_private_key]
42
+
43
+        self.mock_object(
44
+            api_castellan, "list", mock.Mock(return_value=key_list))
45
+
46
+        res = self.client.get(INDEX_URL)
47
+        self.assertEqual(res.status_code, 200)
48
+        self.assertTemplateUsed(res, 'private_keys.html')
49
+        api_castellan.list.assert_called_with(
50
+            mock.ANY, object_type=private_key.PrivateKey)
51
+
52
+    def test_detail_view(self):
53
+        url = reverse('horizon:project:private_keys:detail',
54
+                      args=[self.key.id])
55
+        self.mock_object(
56
+            api_castellan, "list", mock.Mock(return_value=[self.key]))
57
+        self.mock_object(
58
+            api_castellan, "get", mock.Mock(return_value=self.key))
59
+
60
+        res = self.client.get(url)
61
+        self.assertContains(
62
+            res, "<dt>Name</dt>\n    <dd>%s</dd>" % self.key.name, 1, 200)
63
+        api_castellan.get.assert_called_once_with(mock.ANY, self.key.id)
64
+
65
+    def test_generate_key_pair(self):
66
+        self.mock_object(
67
+            api_castellan, "list", mock.Mock(return_value=[self.key]))
68
+        url = reverse('horizon:project:private_keys:generate')
69
+        self.mock_object(
70
+            api_castellan, "generate_key_pair",
71
+            mock.Mock(return_value=(self.key, self.key)))
72
+
73
+        key_form_data = {
74
+            'name': self.key.name,
75
+            'length': 2048,
76
+            'algorithm': 'RSA'
77
+        }
78
+
79
+        self.client.post(url, key_form_data)
80
+
81
+        api_castellan.generate_key_pair.assert_called_once_with(
82
+            mock.ANY,
83
+            name=self.key.name,
84
+            algorithm=u'RSA',
85
+            length=2048
86
+        )
87
+
88
+    def test_import_key(self):
89
+        self.mock_object(
90
+            api_castellan, "list", mock.Mock(return_value=[self.key]))
91
+        url = reverse('horizon:project:private_keys:import')
92
+        self.mock_object(
93
+            api_castellan, "import_object", mock.Mock(return_value=self.key))
94
+
95
+        key_input = (
96
+            u"-----BEGIN PRIVATE KEY-----\n" +
97
+            self.key_b64_bytes.decode("utf-8") +
98
+            u"\n-----END PRIVATE KEY-----"
99
+        )
100
+
101
+        key_form_data = {
102
+            'source_type': 'raw',
103
+            'name': self.key.name,
104
+            'direct_input': key_input,
105
+            'bit_length': 2048,
106
+            'algorithm': 'RSA'
107
+        }
108
+
109
+        self.client.post(url, key_form_data)
110
+
111
+        api_castellan.import_object.assert_called_once_with(
112
+            mock.ANY,
113
+            object_type=private_key.PrivateKey,
114
+            key=self.key_b64_bytes,
115
+            name=self.key.name,
116
+            algorithm=u'RSA',
117
+            bit_length=2048
118
+        )
119
+
120
+    def test_delete_key(self):
121
+        self.mock_object(
122
+            api_castellan, "list", mock.Mock(return_value=[self.key]))
123
+        self.mock_object(api_castellan, "delete")
124
+
125
+        key_form_data = {
126
+            'action': 'private_key__delete__%s' % self.key.id
127
+        }
128
+
129
+        res = self.client.post(INDEX_URL, key_form_data)
130
+
131
+        api_castellan.list.assert_called_with(
132
+            mock.ANY, object_type=private_key.PrivateKey)
133
+        api_castellan.delete.assert_called_once_with(
134
+            mock.ANY,
135
+            self.key.id,
136
+        )
137
+        self.assertRedirectsNoFollow(res, INDEX_URL)

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


+ 137
- 0
castellan_ui/test/content/public_keys/tests.py View File

@@ -0,0 +1,137 @@
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 public_key
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:public_keys:index')
25
+
26
+
27
+class PublicKeysViewTest(tests.APITestCase):
28
+
29
+    def setUp(self):
30
+        super(PublicKeysViewTest, self).setUp()
31
+        self.key = test_data.public_key
32
+        self.key_b64_bytes = base64.b64encode(self.key.get_encoded())
33
+        self.mock_object(
34
+            api_castellan, "get", mock.Mock(return_value=self.key))
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
+        key_list = [test_data.public_key, test_data.nameless_public_key]
42
+
43
+        self.mock_object(
44
+            api_castellan, "list", mock.Mock(return_value=key_list))
45
+
46
+        res = self.client.get(INDEX_URL)
47
+        self.assertEqual(res.status_code, 200)
48
+        self.assertTemplateUsed(res, 'public_keys.html')
49
+        api_castellan.list.assert_called_with(
50
+            mock.ANY, object_type=public_key.PublicKey)
51
+
52
+    def test_detail_view(self):
53
+        url = reverse('horizon:project:public_keys:detail',
54
+                      args=[self.key.id])
55
+        self.mock_object(
56
+            api_castellan, "list", mock.Mock(return_value=[self.key]))
57
+        self.mock_object(
58
+            api_castellan, "get", mock.Mock(return_value=self.key))
59
+
60
+        res = self.client.get(url)
61
+        self.assertContains(
62
+            res, "<dt>Name</dt>\n    <dd>%s</dd>" % self.key.name, 1, 200)
63
+        api_castellan.get.assert_called_once_with(mock.ANY, self.key.id)
64
+
65
+    def test_generate_key_pair(self):
66
+        self.mock_object(
67
+            api_castellan, "list", mock.Mock(return_value=[self.key]))
68
+        url = reverse('horizon:project:public_keys:generate')
69
+        self.mock_object(
70
+            api_castellan, "generate_key_pair",
71
+            mock.Mock(return_value=(self.key, self.key)))
72
+
73
+        key_form_data = {
74
+            'name': self.key.name,
75
+            'length': 2048,
76
+            'algorithm': 'RSA'
77
+        }
78
+
79
+        self.client.post(url, key_form_data)
80
+
81
+        api_castellan.generate_key_pair.assert_called_once_with(
82
+            mock.ANY,
83
+            name=self.key.name,
84
+            algorithm=u'RSA',
85
+            length=2048
86
+        )
87
+
88
+    def test_import_key(self):
89
+        self.mock_object(
90
+            api_castellan, "list", mock.Mock(return_value=[self.key]))
91
+        url = reverse('horizon:project:public_keys:import')
92
+        self.mock_object(
93
+            api_castellan, "import_object", mock.Mock(return_value=self.key))
94
+
95
+        key_input = (
96
+            u"-----BEGIN PUBLIC KEY-----\n" +
97
+            self.key_b64_bytes.decode("utf-8") +
98
+            u"\n-----END PUBLIC KEY-----"
99
+        )
100
+
101
+        key_form_data = {
102
+            'source_type': 'raw',
103
+            'name': self.key.name,
104
+            'direct_input': key_input,
105
+            'bit_length': 2048,
106
+            'algorithm': 'RSA'
107
+        }
108
+
109
+        self.client.post(url, key_form_data)
110
+
111
+        api_castellan.import_object.assert_called_once_with(
112
+            mock.ANY,
113
+            object_type=public_key.PublicKey,
114
+            key=self.key_b64_bytes,
115
+            name=self.key.name,
116
+            algorithm=u'RSA',
117
+            bit_length=2048
118
+        )
119
+
120
+    def test_delete_key(self):
121
+        self.mock_object(
122
+            api_castellan, "list", mock.Mock(return_value=[self.key]))
123
+        self.mock_object(api_castellan, "delete")
124
+
125
+        key_form_data = {
126
+            'action': 'public_key__delete__%s' % self.key.id
127
+        }
128
+
129
+        res = self.client.post(INDEX_URL, key_form_data)
130
+
131
+        api_castellan.list.assert_called_with(
132
+            mock.ANY, object_type=public_key.PublicKey)
133
+        api_castellan.delete.assert_called_once_with(
134
+            mock.ANY,
135
+            self.key.id,
136
+        )
137
+        self.assertRedirectsNoFollow(res, INDEX_URL)

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

@@ -24,3 +24,35 @@ nameless_x509_cert = objects.x_509.X509(
24 24
     name=None,
25 25
     created=1448088699,
26 26
     id=u'11111111-1111-1111-1111-111111111111')
27
+
28
+private_key = objects.private_key.PrivateKey(
29
+    key=castellan_utils.get_private_key_der(),
30
+    algorithm="RSA",
31
+    bit_length=2048,
32
+    name=u'test private key',
33
+    created=1448088699,
34
+    id=u'00000000-0000-0000-0000-000000000000')
35
+
36
+nameless_private_key = objects.private_key.PrivateKey(
37
+    key=castellan_utils.get_private_key_der(),
38
+    algorithm="RSA",
39
+    bit_length=2048,
40
+    name=None,
41
+    created=1448088699,
42
+    id=u'11111111-1111-1111-1111-111111111111')
43
+
44
+public_key = objects.public_key.PublicKey(
45
+    key=castellan_utils.get_public_key_der(),
46
+    algorithm="RSA",
47
+    bit_length=2048,
48
+    name=u'test public key',
49
+    created=1448088699,
50
+    id=u'00000000-0000-0000-0000-000000000000')
51
+
52
+nameless_public_key = objects.public_key.PublicKey(
53
+    key=castellan_utils.get_public_key_der(),
54
+    algorithm="RSA",
55
+    bit_length=2048,
56
+    name=None,
57
+    created=1448088699,
58
+    id=u'11111111-1111-1111-1111-111111111111')

Loading…
Cancel
Save