summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaitlin Farr <kaitlin.farr@jhuapl.edu>2017-11-13 15:14:33 -0500
committerBrianna Poulos <Brianna.Poulos@jhuapl.edu>2018-02-27 21:27:32 +0000
commitbbcebe522f1366791aca78475770515c3eed4d39 (patch)
treead662138d99e0039fd381fdf359bd69ac7373437
parent21504848d687e998b68c52b5824c940a87d0cfb7 (diff)
Add Passphrase Panel0.1.0
Notes
Notes (review): Code-Review+2: Brianna Poulos <Brianna.Poulos@jhuapl.edu> Workflow+1: Brianna Poulos <Brianna.Poulos@jhuapl.edu> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Tue, 27 Feb 2018 21:47:52 +0000 Reviewed-on: https://review.openstack.org/519470 Project: openstack/castellan-ui Branch: refs/heads/master
-rw-r--r--README.rst1
-rw-r--r--castellan_ui/content/passphrases/__init__.py0
-rw-r--r--castellan_ui/content/passphrases/forms.py61
-rw-r--r--castellan_ui/content/passphrases/panel.py23
-rw-r--r--castellan_ui/content/passphrases/tables.py72
-rw-r--r--castellan_ui/content/passphrases/urls.py22
-rw-r--r--castellan_ui/content/passphrases/views.py96
-rw-r--r--castellan_ui/enabled/_96_project_key_manager_passphrases_panel.py23
-rw-r--r--castellan_ui/templates/_passphrase_import.html7
-rw-r--r--castellan_ui/templates/passphrase_detail.html49
-rw-r--r--castellan_ui/templates/passphrase_import.html7
-rw-r--r--castellan_ui/templates/passphrases.html23
-rw-r--r--castellan_ui/test/content/passphrases/__init__.py0
-rw-r--r--castellan_ui/test/content/passphrases/tests.py112
-rw-r--r--castellan_ui/test/test_data.py12
15 files changed, 508 insertions, 0 deletions
diff --git a/README.rst b/README.rst
index bf5bc16..8400e1b 100644
--- a/README.rst
+++ b/README.rst
@@ -59,6 +59,7 @@ And enable it in Horizon::
59 ln -s ../castellan-ui/castellan_ui/enabled/_93_project_key_manager_public_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
60 ln -s ../castellan-ui/castellan_ui/enabled/_94_project_key_manager_symmetric_key_panel.py openstack_dashboard/local/enabled 60 ln -s ../castellan-ui/castellan_ui/enabled/_94_project_key_manager_symmetric_key_panel.py openstack_dashboard/local/enabled
61 ln -s ../castellan-ui/castellan_ui/enabled/_95_project_key_manager_opaque_data_panel.py openstack_dashboard/local/enabled 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
63To run horizon with the newly enabled Castellan UI plugin run:: 64To run horizon with the newly enabled Castellan UI plugin run::
64 65
diff --git a/castellan_ui/content/passphrases/__init__.py b/castellan_ui/content/passphrases/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/castellan_ui/content/passphrases/__init__.py
diff --git a/castellan_ui/content/passphrases/forms.py b/castellan_ui/content/passphrases/forms.py
new file mode 100644
index 0000000..ef786b9
--- /dev/null
+++ b/castellan_ui/content/passphrases/forms.py
@@ -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
14from django.utils.translation import ugettext_lazy as _
15
16from castellan.common.objects import passphrase
17from horizon import exceptions
18from horizon import forms
19from horizon import messages
20
21from castellan_ui.api import client
22from castellan_ui.content import shared_forms
23
24
25class 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
diff --git a/castellan_ui/content/passphrases/panel.py b/castellan_ui/content/passphrases/panel.py
new file mode 100644
index 0000000..320d2f2
--- /dev/null
+++ b/castellan_ui/content/passphrases/panel.py
@@ -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
13from django.utils.translation import ugettext_lazy as _
14import horizon
15
16# This panel will be loaded from horizon, because specified in enabled file.
17# To register REST api, import below here.
18from castellan_ui.api import client # noqa: F401
19
20
21class Passphrases(horizon.Panel):
22 name = _("Passphrases")
23 slug = "passphrases"
diff --git a/castellan_ui/content/passphrases/tables.py b/castellan_ui/content/passphrases/tables.py
new file mode 100644
index 0000000..4f77928
--- /dev/null
+++ b/castellan_ui/content/passphrases/tables.py
@@ -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
14from castellan_ui.content import filters
15from django.utils.translation import ugettext_lazy as _
16from django.utils.translation import ungettext_lazy
17
18from castellan_ui.api import client
19from horizon import tables
20
21
22class 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
31class 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
56class 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, )
diff --git a/castellan_ui/content/passphrases/urls.py b/castellan_ui/content/passphrases/urls.py
new file mode 100644
index 0000000..713c986
--- /dev/null
+++ b/castellan_ui/content/passphrases/urls.py
@@ -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
13from castellan_ui.content.passphrases import views
14from django.conf.urls import url
15
16urlpatterns = [
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]
diff --git a/castellan_ui/content/passphrases/views.py b/castellan_ui/content/passphrases/views.py
new file mode 100644
index 0000000..6822871
--- /dev/null
+++ b/castellan_ui/content/passphrases/views.py
@@ -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
13from django.core.urlresolvers import reverse
14from django.core.urlresolvers import reverse_lazy
15from django.utils.translation import ugettext_lazy as _
16
17from castellan.common.objects import passphrase
18from castellan_ui.api import client
19from castellan_ui.content.passphrases import forms as passphrase_forms
20from castellan_ui.content.passphrases import tables
21from datetime import datetime
22from horizon import exceptions
23from horizon import forms
24from horizon.tables import views as tables_views
25from horizon.utils import memoized
26from horizon import views
27
28
29class 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
45class 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
57class 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
diff --git a/castellan_ui/enabled/_96_project_key_manager_passphrases_panel.py b/castellan_ui/enabled/_96_project_key_manager_passphrases_panel.py
new file mode 100644
index 0000000..7f70aaa
--- /dev/null
+++ b/castellan_ui/enabled/_96_project_key_manager_passphrases_panel.py
@@ -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.
14PANEL = 'passphrases'
15# The slug of the panel group the PANEL is associated with.
16PANEL_GROUP = 'key_manager'
17# The slug of the dashboard the PANEL associated with. Required.
18PANEL_DASHBOARD = 'project'
19
20ADD_INSTALLED_APP = ['castellan_ui', ]
21
22# Python panel class of the PANEL to be added.
23ADD_PANEL = 'castellan_ui.content.passphrases.panel.Passphrases'
diff --git a/castellan_ui/templates/_passphrase_import.html b/castellan_ui/templates/_passphrase_import.html
new file mode 100644
index 0000000..a213022
--- /dev/null
+++ b/castellan_ui/templates/_passphrase_import.html
@@ -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
diff --git a/castellan_ui/templates/passphrase_detail.html b/castellan_ui/templates/passphrase_detail.html
new file mode 100644
index 0000000..af55269
--- /dev/null
+++ b/castellan_ui/templates/passphrase_detail.html
@@ -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">
12function unhide(clickedButton, divID) {
13var item = document.getElementById(divID);
14if (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 %}
diff --git a/castellan_ui/templates/passphrase_import.html b/castellan_ui/templates/passphrase_import.html
new file mode 100644
index 0000000..864272c
--- /dev/null
+++ b/castellan_ui/templates/passphrase_import.html
@@ -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 %}
diff --git a/castellan_ui/templates/passphrases.html b/castellan_ui/templates/passphrases.html
new file mode 100644
index 0000000..232a270
--- /dev/null
+++ b/castellan_ui/templates/passphrases.html
@@ -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 %}
diff --git a/castellan_ui/test/content/passphrases/__init__.py b/castellan_ui/test/content/passphrases/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/castellan_ui/test/content/passphrases/__init__.py
diff --git a/castellan_ui/test/content/passphrases/tests.py b/castellan_ui/test/content/passphrases/tests.py
new file mode 100644
index 0000000..09c2e36
--- /dev/null
+++ b/castellan_ui/test/content/passphrases/tests.py
@@ -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
13from django.core.handlers import wsgi
14from django.core.urlresolvers import reverse
15from horizon import messages as horizon_messages
16import mock
17
18from castellan.common.objects import passphrase
19from castellan_ui.api import client as api_castellan
20from castellan_ui.test import helpers as tests
21from castellan_ui.test import test_data
22
23INDEX_URL = reverse('horizon:project:passphrases:index')
24
25
26class 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)
diff --git a/castellan_ui/test/test_data.py b/castellan_ui/test/test_data.py
index 675d5a4..49f46ee 100644
--- a/castellan_ui/test/test_data.py
+++ b/castellan_ui/test/test_data.py
@@ -84,3 +84,15 @@ nameless_opaque_data = objects.opaque_data.OpaqueData(
84 name=None, 84 name=None,
85 created=1448088699, 85 created=1448088699,
86 id=u'11111111-1111-1111-1111-111111111111') 86 id=u'11111111-1111-1111-1111-111111111111')
87
88passphrase = 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
94nameless_passphrase = objects.passphrase.Passphrase(
95 passphrase=u'P@ssw0rd',
96 name=None,
97 created=1448088699,
98 id=u'11111111-1111-1111-1111-111111111111')