Browse Source

Add Passphrase Panel

Change-Id: If52f1c29877d91cce50fdfb61319bd195103e6e1
tags/0.1.0^0
Kaitlin Farr 1 year ago
parent
commit
bbcebe522f

+ 1
- 0
README.rst View File

@@ -59,6 +59,7 @@ And enable it in Horizon::
59 59
     ln -s ../castellan-ui/castellan_ui/enabled/_93_project_key_manager_public_key_panel.py openstack_dashboard/local/enabled
60 60
     ln -s ../castellan-ui/castellan_ui/enabled/_94_project_key_manager_symmetric_key_panel.py openstack_dashboard/local/enabled
61 61
     ln -s ../castellan-ui/castellan_ui/enabled/_95_project_key_manager_opaque_data_panel.py openstack_dashboard/local/enabled
62
+    ln -s ../castellan-ui/castellan_ui/enabled/_96_project_key_manager_passphrase_panel.py openstack_dashboard/local/enabled
62 63
 
63 64
 To run horizon with the newly enabled Castellan UI plugin run::
64 65
 

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


+ 61
- 0
castellan_ui/content/passphrases/forms.py View File

@@ -0,0 +1,61 @@
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 django.utils.translation import ugettext_lazy as _
15
+
16
+from castellan.common.objects import passphrase
17
+from horizon import exceptions
18
+from horizon import forms
19
+from horizon import messages
20
+
21
+from castellan_ui.api import client
22
+from castellan_ui.content import shared_forms
23
+
24
+
25
+class ImportPassphrase(forms.SelfHandlingForm):
26
+    name = forms.RegexField(required=False,
27
+                            max_length=255,
28
+                            label=_("Passphrase Name"),
29
+                            regex=shared_forms.NAME_REGEX,
30
+                            error_messages=shared_forms.ERROR_MESSAGES)
31
+    direct_input = forms.CharField(
32
+        label=_('Passphrase'),
33
+        help_text=_('The text of the passphrase in plaintext'),
34
+        widget=forms.widgets.Textarea(),
35
+        required=True)
36
+
37
+    def handle(self, request, data):
38
+        try:
39
+            # Remove any new lines in the passphrase
40
+            direct_input = data.get('direct_input')
41
+            direct_input = shared_forms.NEW_LINES.sub("", direct_input)
42
+            object_uuid = client.import_object(
43
+                request,
44
+                passphrase=direct_input,
45
+                name=data['name'],
46
+                object_type=passphrase.Passphrase)
47
+
48
+            if data['name']:
49
+                object_identifier = data['name']
50
+            else:
51
+                object_identifier = object_uuid
52
+            messages.success(request,
53
+                             _('Successfully imported passphrase: %s')
54
+                             % object_identifier)
55
+            return object_uuid
56
+        except Exception as e:
57
+            msg = _('Unable to import passphrase: %s')
58
+            messages.error(request, msg % e)
59
+            exceptions.handle(request, ignore=True)
60
+            self.api_error(_('Unable to import passphrase.'))
61
+            return False

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

+ 72
- 0
castellan_ui/content/passphrases/tables.py View File

@@ -0,0 +1,72 @@
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.utils.translation import ugettext_lazy as _
16
+from django.utils.translation import ungettext_lazy
17
+
18
+from castellan_ui.api import client
19
+from horizon import tables
20
+
21
+
22
+class ImportPassphrase(tables.LinkAction):
23
+    name = "import_passphrase"
24
+    verbose_name = _("Import Passphrase")
25
+    url = "horizon:project:passphrases:import"
26
+    classes = ("ajax-modal",)
27
+    icon = "upload"
28
+    policy_rules = ()
29
+
30
+
31
+class DeletePassphrase(tables.DeleteAction):
32
+    policy_rules = ()
33
+    help_text = _("You should not delete a passphrase unless you are "
34
+                  "certain it is not being used anywhere.")
35
+
36
+    @staticmethod
37
+    def action_present(count):
38
+        return ungettext_lazy(
39
+            u"Delete Passphrase",
40
+            u"Delete Passphrases",
41
+            count
42
+        )
43
+
44
+    @staticmethod
45
+    def action_past(count):
46
+        return ungettext_lazy(
47
+            u"Deleted Passphrase",
48
+            u"Deleted Passphrases",
49
+            count
50
+        )
51
+
52
+    def delete(self, request, obj_id):
53
+        client.delete(request, obj_id)
54
+
55
+
56
+class PassphraseTable(tables.DataTable):
57
+    detail_link = "horizon:project:passphrases:detail"
58
+    uuid = tables.Column("id", verbose_name=_("Passphrase ID"),
59
+                         link=detail_link)
60
+    name = tables.Column("name", verbose_name=_("Name"))
61
+    created_date = tables.Column("created",
62
+                                 verbose_name=_("Created Date"),
63
+                                 filters=(filters.timestamp_to_iso,))
64
+
65
+    def get_object_display(self, datum):
66
+        return datum.name if datum.name else datum.id
67
+
68
+    class Meta(object):
69
+        name = "passphrase"
70
+        table_actions = (ImportPassphrase,
71
+                         DeletePassphrase,)
72
+        row_actions = (DeletePassphrase, )

+ 22
- 0
castellan_ui/content/passphrases/urls.py View File

@@ -0,0 +1,22 @@
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.passphrases 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
+]

+ 96
- 0
castellan_ui/content/passphrases/views.py View File

@@ -0,0 +1,96 @@
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.utils.translation import ugettext_lazy as _
16
+
17
+from castellan.common.objects import passphrase
18
+from castellan_ui.api import client
19
+from castellan_ui.content.passphrases import forms as passphrase_forms
20
+from castellan_ui.content.passphrases import tables
21
+from datetime import datetime
22
+from horizon import exceptions
23
+from horizon import forms
24
+from horizon.tables import views as tables_views
25
+from horizon.utils import memoized
26
+from horizon import views
27
+
28
+
29
+class IndexView(tables_views.MultiTableView):
30
+    table_classes = [
31
+        tables.PassphraseTable
32
+    ]
33
+    template_name = 'passphrases.html'
34
+
35
+    def get_passphrase_data(self):
36
+        try:
37
+            return client.list(
38
+                self.request, object_type=passphrase.Passphrase)
39
+        except Exception as e:
40
+            msg = _('Unable to list passphrases: "%s".') % (e.message)
41
+            exceptions.handle(self.request, msg)
42
+            return []
43
+
44
+
45
+class ImportView(forms.ModalFormView):
46
+    form_class = passphrase_forms.ImportPassphrase
47
+    template_name = 'passphrase_import.html'
48
+    submit_url = reverse_lazy(
49
+        "horizon:project:passphrases:import")
50
+    success_url = reverse_lazy('horizon:project:passphrases:index')
51
+    submit_label = page_title = _("Import Passphrase")
52
+
53
+    def get_object_id(self, key_uuid):
54
+        return key_uuid
55
+
56
+
57
+class DetailView(views.HorizonTemplateView):
58
+    template_name = 'passphrase_detail.html'
59
+    page_title = _("Passphrase Details")
60
+
61
+    @memoized.memoized_method
62
+    def _get_data(self):
63
+        try:
64
+            obj = client.get(self.request, self.kwargs['object_id'])
65
+        except Exception:
66
+            redirect = reverse('horizon:project:passphrases:index')
67
+            msg = _('Unable to retrieve details for passphrase "%s".')\
68
+                % (self.kwargs['object_id'])
69
+            exceptions.handle(self.request, msg,
70
+                              redirect=redirect)
71
+        return obj
72
+
73
+    @memoized.memoized_method
74
+    def _get_data_created_date(self, obj):
75
+        try:
76
+            created_date = datetime.utcfromtimestamp(obj.created).isoformat()
77
+        except Exception:
78
+            redirect = reverse('horizon:project:passphrases:index')
79
+            msg = _('Unable to retrieve details for passphrase "%s".')\
80
+                % (self.kwargs['object_id'])
81
+            exceptions.handle(self.request, msg,
82
+                              redirect=redirect)
83
+        return created_date
84
+
85
+    @memoized.memoized_method
86
+    def _get_data_bytes(self, obj):
87
+        return obj.get_encoded()
88
+
89
+    def get_context_data(self, **kwargs):
90
+        """Gets the context data for key."""
91
+        context = super(DetailView, self).get_context_data(**kwargs)
92
+        obj = self._get_data()
93
+        context['object'] = obj
94
+        context['object_created_date'] = self._get_data_created_date(obj)
95
+        context['object_bytes'] = self._get_data_bytes(obj)
96
+        return context

+ 23
- 0
castellan_ui/enabled/_96_project_key_manager_passphrases_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 = 'passphrases'
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.passphrases.panel.Passphrases'

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

@@ -0,0 +1,7 @@
1
+{% extends '_object_import.html' %}
2
+{% load i18n %}
3
+
4
+{% block modal-body-right %}
5
+  <p>{% trans "Enter the passphrase as you would type it on on the command line or into a form." %}</p>
6
+{% endblock %}
7
+

+ 49
- 0
castellan_ui/templates/passphrase_detail.html View File

@@ -0,0 +1,49 @@
1
+<style>
2
+.hidden{
3
+    display:none;
4
+}
5
+
6
+.visible{
7
+    display:block;
8
+}
9
+</style>
10
+
11
+<script type="text/javascript">
12
+function unhide(clickedButton, divID) {
13
+var item = document.getElementById(divID);
14
+if (item) {
15
+    if(item.className=='hidden'){
16
+        item.className = 'visible' ;
17
+        clickedButton.textContent = 'hide'
18
+    }else{
19
+        item.className = 'hidden';
20
+        clickedButton.textContent = 'show'
21
+    }
22
+}}
23
+
24
+</script>
25
+
26
+{% extends 'base.html' %}
27
+{% load i18n parse_date %}
28
+
29
+{% block title %}{{ page_title }}{% endblock %}
30
+
31
+{% block page_header %}
32
+  {% include "horizon/common/_detail_header.html" %}
33
+{% endblock %}
34
+
35
+{% block main %}
36
+<div class="detail">
37
+  <dl class="dl-horizontal">
38
+    <dt>{% trans "Name" %}</dt>
39
+    <dd>{{ object.name|default:_("None") }}</dd>
40
+    <dt>{% trans "Created" %}</dt>
41
+    <dd>{{ object_created_date|parse_date}}</dd>
42
+    <dt>{% trans "Passphrase" %}</dt>
43
+    <dd>
44
+        <div id="passphrase" class="hidden">{{ object_bytes }}</div>
45
+        <button class="btn" onclick="unhide(this, 'passphrase') ">show</button>
46
+    </dd>
47
+  </dl>
48
+</div>
49
+{% endblock %}

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

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

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

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


+ 112
- 0
castellan_ui/test/content/passphrases/tests.py View File

@@ -0,0 +1,112 @@
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.handlers import wsgi
14
+from django.core.urlresolvers import reverse
15
+from horizon import messages as horizon_messages
16
+import mock
17
+
18
+from castellan.common.objects import passphrase
19
+from castellan_ui.api import client as api_castellan
20
+from castellan_ui.test import helpers as tests
21
+from castellan_ui.test import test_data
22
+
23
+INDEX_URL = reverse('horizon:project:passphrases:index')
24
+
25
+
26
+class PassphrasesViewTest(tests.APITestCase):
27
+
28
+    class FakeCert(object):
29
+        def __init__(self):
30
+            pass
31
+
32
+    def setUp(self):
33
+        super(PassphrasesViewTest, self).setUp()
34
+        self.passphrase = test_data.passphrase
35
+        self.mock_object(
36
+            api_castellan, "get", mock.Mock(return_value=self.passphrase))
37
+        self.mock_object(api_castellan, "list", mock.Mock(return_value=[]))
38
+        self.mock_object(horizon_messages, "success")
39
+        FAKE_ENVIRON = {'REQUEST_METHOD': 'GET', 'wsgi.input': 'fake_input'}
40
+        self.request = wsgi.WSGIRequest(FAKE_ENVIRON)
41
+
42
+    def test_index(self):
43
+        passphrase_list = [test_data.passphrase, test_data.nameless_passphrase]
44
+
45
+        self.mock_object(
46
+            api_castellan, "list", mock.Mock(return_value=passphrase_list))
47
+
48
+        res = self.client.get(INDEX_URL)
49
+        self.assertEqual(res.status_code, 200)
50
+        self.assertTemplateUsed(res, 'passphrases.html')
51
+        api_castellan.list.assert_called_with(
52
+            mock.ANY, object_type=passphrase.Passphrase)
53
+
54
+    def test_detail_view(self):
55
+        url = reverse('horizon:project:passphrases:detail',
56
+                      args=[self.passphrase.id])
57
+        self.mock_object(
58
+            api_castellan, "list", mock.Mock(return_value=[self.passphrase]))
59
+        self.mock_object(
60
+            api_castellan, "get", mock.Mock(return_value=self.passphrase))
61
+
62
+        res = self.client.get(url)
63
+        self.assertContains(
64
+            res, "<dt>Name</dt>\n    <dd>%s</dd>" % self.passphrase.name,
65
+            1, 200)
66
+        api_castellan.get.assert_called_once_with(mock.ANY, self.passphrase.id)
67
+
68
+    def test_import_cert(self):
69
+        self.mock_object(
70
+            api_castellan, "list", mock.Mock(return_value=[self.passphrase]))
71
+        url = reverse('horizon:project:passphrases:import')
72
+        self.mock_object(
73
+            api_castellan, "import_object", mock.Mock(
74
+                return_value=self.passphrase))
75
+
76
+        passphrase_input = (
77
+            self.passphrase.get_encoded()
78
+        )
79
+
80
+        passphrase_form_data = {
81
+            'source_type': 'raw',
82
+            'name': self.passphrase.name,
83
+            'direct_input': passphrase_input
84
+        }
85
+
86
+        self.client.post(url, passphrase_form_data)
87
+
88
+        api_castellan.import_object.assert_called_once_with(
89
+            mock.ANY,
90
+            object_type=passphrase.Passphrase,
91
+            passphrase=self.passphrase.get_encoded(),
92
+            name=self.passphrase.name
93
+        )
94
+
95
+    def test_delete_cert(self):
96
+        self.mock_object(
97
+            api_castellan, "list", mock.Mock(return_value=[self.passphrase]))
98
+        self.mock_object(api_castellan, "delete")
99
+
100
+        passphrase_form_data = {
101
+            'action': 'passphrase__delete__%s' % self.passphrase.id
102
+        }
103
+
104
+        res = self.client.post(INDEX_URL, passphrase_form_data)
105
+
106
+        api_castellan.list.assert_called_with(
107
+            mock.ANY, object_type=passphrase.Passphrase)
108
+        api_castellan.delete.assert_called_once_with(
109
+            mock.ANY,
110
+            self.passphrase.id,
111
+        )
112
+        self.assertRedirectsNoFollow(res, INDEX_URL)

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

@@ -84,3 +84,15 @@ nameless_opaque_data = objects.opaque_data.OpaqueData(
84 84
     name=None,
85 85
     created=1448088699,
86 86
     id=u'11111111-1111-1111-1111-111111111111')
87
+
88
+passphrase = objects.passphrase.Passphrase(
89
+    passphrase=u'P@ssw0rd',
90
+    name=u'test passphrase',
91
+    created=1448088699,
92
+    id=u'00000000-0000-0000-0000-000000000000')
93
+
94
+nameless_passphrase = objects.passphrase.Passphrase(
95
+    passphrase=u'P@ssw0rd',
96
+    name=None,
97
+    created=1448088699,
98
+    id=u'11111111-1111-1111-1111-111111111111')

Loading…
Cancel
Save