diff --git a/openstack_dashboard/api/keystone.py b/openstack_dashboard/api/keystone.py index dde58a02b3..3eafafa740 100644 --- a/openstack_dashboard/api/keystone.py +++ b/openstack_dashboard/api/keystone.py @@ -499,6 +499,44 @@ def user_update_tenant(request, user, project, admin=True): return manager.update(user, project=project) +@profiler.trace +def credential_create(request, user, type, blob, project=None): + manager = keystoneclient(request).credentials + return manager.create(user=user, type=type, blob=blob, project=project) + + +@profiler.trace +def credential_delete(request, credential_id): + manager = keystoneclient(request, admin=True).credentials + return manager.delete(credential_id) + + +@profiler.trace +def credential_get(request, credential_id, admin=True): + manager = keystoneclient(request, admin=admin).credentials + return manager.get(credential_id) + + +@profiler.trace +def credentials_list(request, user=None): + manager = keystoneclient(request).credentials + return manager.list(user=user) + + +@profiler.trace +def credential_update(request, credential_id, user, + type=None, blob=None, project=None): + manager = keystoneclient(request, admin=True).credentials + try: + return manager.update(credential=credential_id, + user=user, + type=type, + blob=blob, + project=project) + except keystone_exceptions.Conflict: + raise exceptions.Conflict() + + @profiler.trace def group_create(request, domain_id, name, description=None): manager = keystoneclient(request, admin=True).groups diff --git a/openstack_dashboard/dashboards/identity/credentials/__init__.py b/openstack_dashboard/dashboards/identity/credentials/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/identity/credentials/forms.py b/openstack_dashboard/dashboards/identity/credentials/forms.py new file mode 100644 index 0000000000..5947e60fca --- /dev/null +++ b/openstack_dashboard/dashboards/identity/credentials/forms.py @@ -0,0 +1,115 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import gettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from openstack_dashboard.api import keystone + +# Available credential type choices +TYPE_CHOICES = ( + ('totp', _('TOTP')), + ('ec2', _('EC2')), + ('cert', _('cert')), +) + + +class CreateCredentialForm(forms.SelfHandlingForm): + user_name = forms.ThemableChoiceField(label=_('User')) + cred_type = forms.ThemableChoiceField(label=_('Type'), + choices=TYPE_CHOICES) + data = forms.CharField(label=_('Data')) + project = forms.ThemableChoiceField(label=_('Project'), required=False) + failure_url = 'horizon:identity:credentials:index' + + def __init__(self, request, *args, **kwargs): + super().__init__(request, *args, **kwargs) + + users = keystone.user_list(request) + user_choices = [(user.id, user.name) for user in users] + self.fields['user_name'].choices = user_choices + + project_choices = [('', _("Select a project"))] + projects, __ = keystone.tenant_list(request) + for project in projects: + if project.enabled: + project_choices.append((project.id, project.name)) + self.fields['project'].choices = project_choices + + def handle(self, request, data): + try: + params = { + 'user': data['user_name'], + 'type': data["cred_type"], + 'blob': data["data"], + } + if data["project"]: + params['project'] = data['project'] + new_credential = keystone.credential_create(request, **params) + messages.success( + request, _("User credential created successfully.")) + return new_credential + except Exception: + exceptions.handle(request, _('Unable to create user credential.')) + + +class UpdateCredentialForm(forms.SelfHandlingForm): + id = forms.CharField(label=_("ID"), widget=forms.HiddenInput) + user_name = forms.ThemableChoiceField(label=_('User')) + cred_type = forms.ThemableChoiceField(label=_('Type'), + choices=TYPE_CHOICES) + data = forms.CharField(label=_("Data")) + project = forms.ThemableChoiceField(label=_('Project'), required=False) + failure_url = 'horizon:identity:credentials:index' + + def __init__(self, request, *args, **kwargs): + super().__init__(request, *args, **kwargs) + + users = keystone.user_list(request) + user_choices = [(user.id, user.name) for user in users] + self.fields['user_name'].choices = user_choices + + initial = kwargs.get('initial', {}) + cred_type = initial.get('cred_type') + self.fields['cred_type'].initial = cred_type + + # Keystone does not change project to None. If this field is left as + # "Select a project", the project will not be changed. If this field + # is set to another project, the project will be changed. + project_choices = [('', _("Select a project"))] + projects, __ = keystone.tenant_list(request) + for project in projects: + if project.enabled: + project_choices.append((project.id, project.name)) + self.fields['project'].choices = project_choices + + project = initial.get('project_name') + self.fields['project'].initial = project + + def handle(self, request, data): + try: + params = { + 'user': data['user_name'], + 'type': data["cred_type"], + 'blob': data["data"], + } + params['project'] = data['project'] if data['project'] else None + + keystone.credential_update(request, data['id'], **params) + messages.success( + request, _("User credential updated successfully.")) + return True + except Exception: + exceptions.handle(request, _('Unable to update user credential.')) diff --git a/openstack_dashboard/dashboards/identity/credentials/panel.py b/openstack_dashboard/dashboards/identity/credentials/panel.py new file mode 100644 index 0000000000..5cfd8649ab --- /dev/null +++ b/openstack_dashboard/dashboards/identity/credentials/panel.py @@ -0,0 +1,34 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf import settings +from django.utils.translation import gettext_lazy as _ + +import horizon + +from openstack_dashboard.api import keystone +from openstack_dashboard.dashboards.identity import dashboard + + +class CredentialsPanel(horizon.Panel): + name = _("User Credentials") + slug = 'credentials' + policy_rules = (("identity", "identity:list_credentials"),) + + def can_access(self, context): + if (settings.OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT and + not keystone.is_domain_admin(context['request'])): + return False + return super().can_access(context) + + +dashboard.Identity.register(CredentialsPanel) diff --git a/openstack_dashboard/dashboards/identity/credentials/tables.py b/openstack_dashboard/dashboards/identity/credentials/tables.py new file mode 100644 index 0000000000..45a21fcf5c --- /dev/null +++ b/openstack_dashboard/dashboards/identity/credentials/tables.py @@ -0,0 +1,103 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django import urls +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import ngettext_lazy + +from horizon import tables + +from openstack_dashboard.api import keystone +from openstack_dashboard import policy + + +class CreateCredentialAction(tables.LinkAction): + name = "create" + verbose_name = _("Create User Credential") + url = 'horizon:identity:credentials:create' + classes = ("ajax-modal",) + policy_rules = (("identity", "identity:create_credential"),) + icon = "plus" + + +class UpdateCredentialAction(tables.LinkAction): + name = "update" + verbose_name = _("Edit User Credential") + url = 'horizon:identity:credentials:update' + classes = ("ajax-modal",) + policy_rules = (("identity", "identity:update_credential"),) + icon = "pencil" + + +class DeleteCredentialAction(tables.DeleteAction): + help_text = _("Deleted user credentials are not recoverable.") + policy_rules = (("identity", "identity:delete_credential"),) + + @staticmethod + def action_present(count): + return ngettext_lazy( + "Delete User Credential", + "Delete User Credentials", + count + ) + + @staticmethod + def action_past(count): + return ngettext_lazy( + "Deleted User Credential", + "Deleted User Credentials", + count + ) + + def delete(self, request, obj_id): + keystone.credential_delete(request, obj_id) + + +def get_user_link(datum): + if datum.user_id is not None: + return urls.reverse("horizon:identity:users:detail", + args=(datum.user_id,)) + + +def get_project_link(datum, request): + if policy.check((("identity", "identity:get_project"),), + request, target={"project": datum}): + if datum.project_id is not None: + return urls.reverse("horizon:identity:projects:detail", + args=(datum.project_id,)) + + +class CredentialsTable(tables.DataTable): + user_name = tables.WrappingColumn('user_name', + verbose_name=_('User'), + link=get_user_link) + cred_type = tables.WrappingColumn('type', verbose_name=_('Type')) + data = tables.Column('blob', verbose_name=_('Data')) + project_name = tables.WrappingColumn('project_name', + verbose_name=_('Project'), + link=get_project_link) + + def get_object_id(self, datum): + """Identifier of the credential.""" + return datum.id + + def get_object_display(self, datum): + """Display data of the credential.""" + return datum.blob + + class Meta(object): + name = "credentialstable" + verbose_name = _("User Credentials") + table_actions = (CreateCredentialAction, + DeleteCredentialAction) + row_actions = (UpdateCredentialAction, + DeleteCredentialAction) diff --git a/openstack_dashboard/dashboards/identity/credentials/templates/credentials/_create.html b/openstack_dashboard/dashboards/identity/credentials/templates/credentials/_create.html new file mode 100644 index 0000000000..12de21ceaf --- /dev/null +++ b/openstack_dashboard/dashboards/identity/credentials/templates/credentials/_create.html @@ -0,0 +1,13 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Create a new user credential." %}

+

{% blocktrans trimmed %} + Project limits the scope of the credential. It is is mandatory if the credential type is EC2. + {% endblocktrans %}

+

{% blocktrans trimmed %} + If the credential type is EC2, credential data has to be {"access": <access>, "secret": <secret>}. + {% endblocktrans %}

+{% endblock %} diff --git a/openstack_dashboard/dashboards/identity/credentials/templates/credentials/_update.html b/openstack_dashboard/dashboards/identity/credentials/templates/credentials/_update.html new file mode 100644 index 0000000000..1f405874e6 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/credentials/templates/credentials/_update.html @@ -0,0 +1,13 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Edit the credential's details." %}

+

{% blocktrans trimmed %} + Project limits the scope of the credential. It is is mandatory if the credential type is EC2. + {% endblocktrans %}

+

{% blocktrans trimmed %} + If the credential type is EC2, credential data has to be {"access": <access>, "secret": <secret>}. + {% endblocktrans %}

+{% endblock %} diff --git a/openstack_dashboard/dashboards/identity/credentials/templates/credentials/create.html b/openstack_dashboard/dashboards/identity/credentials/templates/credentials/create.html new file mode 100644 index 0000000000..77b01297b0 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/credentials/templates/credentials/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create User Credential" %}{% endblock %} + +{% block main %} + {% include 'identity/credentials/_create.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/identity/credentials/templates/credentials/update.html b/openstack_dashboard/dashboards/identity/credentials/templates/credentials/update.html new file mode 100644 index 0000000000..79256b563d --- /dev/null +++ b/openstack_dashboard/dashboards/identity/credentials/templates/credentials/update.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update User Credential" %}{% endblock %} + +{% block main %} + {% include 'identity/credentials/_update.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/identity/credentials/tests.py b/openstack_dashboard/dashboards/identity/credentials/tests.py new file mode 100644 index 0000000000..916438a811 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/credentials/tests.py @@ -0,0 +1,39 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.urls import reverse + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + +INDEX_URL = reverse('horizon:identity:credentials:index') +INDEX_VIEW_TEMPLATE = 'horizon/common/_data_table_view.html' + + +class UserCredentialsViewTests(test.TestCase): + + def _get_credentials(self, user): + credentials = [cred for cred in self.credentials.list() + if cred.user_id == user.id] + return credentials + + @test.create_mocks({api.keystone: ('credentials_list', + 'user_get', 'tenant_get')}) + def test_index(self): + user = self.users.list()[0] + self.mock_user_get.return_value = user + credentials = self._get_credentials(user) + self.mock_credentials_list.return_value = credentials + + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, INDEX_VIEW_TEMPLATE) + self.assertCountEqual(res.context['table'].data, credentials) diff --git a/openstack_dashboard/dashboards/identity/credentials/urls.py b/openstack_dashboard/dashboards/identity/credentials/urls.py new file mode 100644 index 0000000000..896a42b23e --- /dev/null +++ b/openstack_dashboard/dashboards/identity/credentials/urls.py @@ -0,0 +1,23 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.urls import re_path + +from openstack_dashboard.dashboards.identity.credentials import views + + +urlpatterns = [ + re_path(r'^$', views.CredentialsView.as_view(), name='index'), + re_path(r'^(?P[^/]+)/update/$', + views.UpdateView.as_view(), name='update'), + re_path(r'^create/$', views.CreateView.as_view(), name='create'), +] diff --git a/openstack_dashboard/dashboards/identity/credentials/views.py b/openstack_dashboard/dashboards/identity/credentials/views.py new file mode 100644 index 0000000000..d377838304 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/credentials/views.py @@ -0,0 +1,107 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.urls import reverse +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import tables +from horizon.utils import memoized + +from openstack_dashboard.api import keystone +from openstack_dashboard.dashboards.identity.credentials \ + import forms as credential_forms +from openstack_dashboard.dashboards.identity.credentials \ + import tables as credential_tables + + +@memoized.memoized +def get_project_name(request, project_id): + if project_id is not None: + project = keystone.tenant_get( + request, project_id, admin=False) + return project.name + return None + + +@memoized.memoized +def get_user_name(request, user_id): + if user_id is not None: + user = keystone.user_get(request, user_id, admin=False) + return user.name + return None + + +class CredentialsView(tables.DataTableView): + table_class = credential_tables.CredentialsTable + page_title = _("User Credentials") + policy_rules = (("identity", "identity:list_credentials"),) + + def get_data(self): + try: + credentials = keystone.credentials_list(self.request) + for cred in credentials: + cred.project_name = get_project_name( + self.request, cred.project_id) + cred.user_name = get_user_name(self.request, cred.user_id) + except Exception: + credentials = [] + exceptions.handle(self.request, + _('Unable to retrieve users credentials list.')) + return credentials + + +class UpdateView(forms.ModalFormView): + template_name = 'identity/credentials/update.html' + form_id = "update_credential_form" + form_class = credential_forms.UpdateCredentialForm + submit_label = _("Update User Credential") + submit_url = "horizon:identity:credentials:update" + success_url = reverse_lazy('horizon:identity:credentials:index') + page_title = _("Update User Credential") + + @memoized.memoized_method + def get_object(self): + try: + return keystone.credential_get( + self.request, self.kwargs['credential_id']) + except Exception: + redirect = reverse("horizon:identity:credentials:index") + exceptions.handle(self.request, + _('Unable to update user credential.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + args = (self.get_object().id,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + credential = self.get_object() + return {'id': credential.id, + 'user_name': credential.user_id, + 'data': credential.blob, + 'cred_type': credential.type, + 'project_name': credential.project_id} + + +class CreateView(forms.ModalFormView): + template_name = 'identity/credentials/create.html' + form_id = "create_credential_form" + form_class = credential_forms.CreateCredentialForm + submit_label = _("Create User Credential") + submit_url = reverse_lazy("horizon:identity:credentials:create") + success_url = reverse_lazy('horizon:identity:credentials:index') + page_title = _("Create User Credential") diff --git a/openstack_dashboard/dashboards/identity/users/credentials/__init__.py b/openstack_dashboard/dashboards/identity/users/credentials/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/identity/users/credentials/tables.py b/openstack_dashboard/dashboards/identity/users/credentials/tables.py new file mode 100644 index 0000000000..f32ff02d73 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/users/credentials/tables.py @@ -0,0 +1,26 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import gettext_lazy as _ + +from horizon import tables + +from openstack_dashboard.dashboards.identity.credentials \ + import tables as credentials_tables + + +class CredentialsTable(credentials_tables.CredentialsTable): + user_name = tables.WrappingColumn('user_name', hidden=True) + + class Meta(object): + name = "credentialstable" + verbose_name = _("Credentials") diff --git a/openstack_dashboard/dashboards/identity/users/tabs.py b/openstack_dashboard/dashboards/identity/users/tabs.py index 4bfca87528..94aa2ee5ba 100644 --- a/openstack_dashboard/dashboards/identity/users/tabs.py +++ b/openstack_dashboard/dashboards/identity/users/tabs.py @@ -19,6 +19,10 @@ from horizon import exceptions from horizon import tabs from openstack_dashboard import api +from openstack_dashboard.dashboards.identity.credentials.views \ + import get_project_name +from openstack_dashboard.dashboards.identity.users.credentials \ + import tables as credentials_tables from openstack_dashboard.dashboards.identity.users.groups \ import tables as groups_tables from openstack_dashboard.dashboards.identity.users.role_assignments \ @@ -151,6 +155,33 @@ class GroupsTab(tabs.TableTab): return user_groups +def get_credentials(request, user): + user_credentials = [] + try: + user_credentials = api.keystone.credentials_list(request, user=user) + for cred in user_credentials: + cred.project_name = get_project_name(request, cred.project_id) + except Exception: + exceptions.handle( + request, _("Unable to retrieve the credentials of this user.")) + + return user_credentials + + +class CredentialsTab(tabs.TableTab): + """Credentials of the user.""" + table_classes = (credentials_tables.CredentialsTable,) + name = _("Credentials") + slug = "credentials" + template_name = "horizon/common/_detail_table.html" + preload = False + policy_rules = (("identity", "identity:list_credentials"),) + + def get_credentialstable_data(self): + user = self.tab_group.kwargs['user'] + return get_credentials(self.request, user) + + class UserDetailTabs(tabs.DetailTabsGroup): slug = "user_details" - tabs = (OverviewTab, RoleAssignmentsTab, GroupsTab,) + tabs = (OverviewTab, RoleAssignmentsTab, GroupsTab, CredentialsTab,) diff --git a/openstack_dashboard/dashboards/settings/credentials/__init__.py b/openstack_dashboard/dashboards/settings/credentials/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/settings/credentials/forms.py b/openstack_dashboard/dashboards/settings/credentials/forms.py new file mode 100644 index 0000000000..a10a3817d4 --- /dev/null +++ b/openstack_dashboard/dashboards/settings/credentials/forms.py @@ -0,0 +1,33 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import gettext_lazy as _ + +from horizon import forms + +from openstack_dashboard.dashboards.identity.credentials \ + import forms as credentials_forms + + +class CreateCredentialForm(credentials_forms.CreateCredentialForm): + user_name = forms.CharField(label=_("User"), widget=forms.HiddenInput) + failure_url = 'horizon:settings:credentials:index' + + def __init__(self, request, *args, **kwargs): + super().__init__(request, *args, **kwargs) + + self.fields['user_name'].initial = request.user + + +class UpdateCredentialForm(credentials_forms.UpdateCredentialForm): + user_name = forms.CharField(label=_("User"), widget=forms.HiddenInput) + failure_url = 'horizon:settings:credentials:index' diff --git a/openstack_dashboard/dashboards/settings/credentials/panel.py b/openstack_dashboard/dashboards/settings/credentials/panel.py new file mode 100644 index 0000000000..6060dc1ffe --- /dev/null +++ b/openstack_dashboard/dashboards/settings/credentials/panel.py @@ -0,0 +1,25 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import gettext_lazy as _ + +import horizon + +from openstack_dashboard.dashboards.settings import dashboard + + +class CredentialsPanel(horizon.Panel): + name = _("User Credentials") + slug = 'credentials' + + +dashboard.Settings.register(CredentialsPanel) diff --git a/openstack_dashboard/dashboards/settings/credentials/tables.py b/openstack_dashboard/dashboards/settings/credentials/tables.py new file mode 100644 index 0000000000..45e9eb28a7 --- /dev/null +++ b/openstack_dashboard/dashboards/settings/credentials/tables.py @@ -0,0 +1,42 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import gettext_lazy as _ + +from horizon import tables + +from openstack_dashboard.dashboards.identity.credentials \ + import tables as credentials_tables + + +class CreateCredentialAction(credentials_tables.CreateCredentialAction): + url = 'horizon:settings:credentials:create' + + +class UpdateCredentialAction(credentials_tables.UpdateCredentialAction): + url = 'horizon:settings:credentials:update' + + +class DeleteCredentialAction(credentials_tables.DeleteCredentialAction): + pass + + +class CredentialsTable(credentials_tables.CredentialsTable): + user_name = tables.WrappingColumn('user_name', hidden=True) + + class Meta(object): + name = "credentialstable" + verbose_name = _("User Credentials") + table_actions = (CreateCredentialAction, + DeleteCredentialAction) + row_actions = (UpdateCredentialAction, + DeleteCredentialAction) diff --git a/openstack_dashboard/dashboards/settings/credentials/templates/credentials/_create.html b/openstack_dashboard/dashboards/settings/credentials/templates/credentials/_create.html new file mode 100644 index 0000000000..df1a8330ab --- /dev/null +++ b/openstack_dashboard/dashboards/settings/credentials/templates/credentials/_create.html @@ -0,0 +1,13 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Create a new credential." %}

+

{% blocktrans trimmed %} + Project limits the scope of the credential. It is is mandatory if the credential type is EC2. + {% endblocktrans %}

+

{% blocktrans trimmed %} + If the credential type is EC2, credential data has to be {"access": <access>, "secret": <secret>}. + {% endblocktrans %}

+{% endblock %} diff --git a/openstack_dashboard/dashboards/settings/credentials/templates/credentials/_update.html b/openstack_dashboard/dashboards/settings/credentials/templates/credentials/_update.html new file mode 100644 index 0000000000..1f405874e6 --- /dev/null +++ b/openstack_dashboard/dashboards/settings/credentials/templates/credentials/_update.html @@ -0,0 +1,13 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Edit the credential's details." %}

+

{% blocktrans trimmed %} + Project limits the scope of the credential. It is is mandatory if the credential type is EC2. + {% endblocktrans %}

+

{% blocktrans trimmed %} + If the credential type is EC2, credential data has to be {"access": <access>, "secret": <secret>}. + {% endblocktrans %}

+{% endblock %} diff --git a/openstack_dashboard/dashboards/settings/credentials/templates/credentials/create.html b/openstack_dashboard/dashboards/settings/credentials/templates/credentials/create.html new file mode 100644 index 0000000000..030ce3612e --- /dev/null +++ b/openstack_dashboard/dashboards/settings/credentials/templates/credentials/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Credential" %}{% endblock %} + +{% block main %} + {% include 'settings/credentials/_create.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/settings/credentials/templates/credentials/update.html b/openstack_dashboard/dashboards/settings/credentials/templates/credentials/update.html new file mode 100644 index 0000000000..ccccacfdb4 --- /dev/null +++ b/openstack_dashboard/dashboards/settings/credentials/templates/credentials/update.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Credential" %}{% endblock %} + +{% block main %} + {% include 'settings/credentials/_update.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/settings/credentials/tests.py b/openstack_dashboard/dashboards/settings/credentials/tests.py new file mode 100644 index 0000000000..219273d90a --- /dev/null +++ b/openstack_dashboard/dashboards/settings/credentials/tests.py @@ -0,0 +1,39 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.urls import reverse + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + +INDEX_URL = reverse('horizon:settings:credentials:index') +INDEX_VIEW_TEMPLATE = 'horizon/common/_data_table_view.html' + + +class CredentialsViewTests(test.TestCase): + + def _get_credentials(self, user): + credentials = [cred for cred in self.credentials.list() + if cred.user_id == user.id] + return credentials + + @test.create_mocks({api.keystone: ('credentials_list', + 'user_get', 'tenant_get')}) + def test_index(self): + user = self.users.list()[0] + self.mock_user_get.return_value = user + credentials = self._get_credentials(user) + self.mock_credentials_list.return_value = credentials + + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, INDEX_VIEW_TEMPLATE) + self.assertCountEqual(res.context['table'].data, credentials) diff --git a/openstack_dashboard/dashboards/settings/credentials/urls.py b/openstack_dashboard/dashboards/settings/credentials/urls.py new file mode 100644 index 0000000000..585ac4cb2f --- /dev/null +++ b/openstack_dashboard/dashboards/settings/credentials/urls.py @@ -0,0 +1,23 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.urls import re_path + +from openstack_dashboard.dashboards.settings.credentials import views + + +urlpatterns = [ + re_path(r'^$', views.CredentialsView.as_view(), name='index'), + re_path(r'^(?P[^/]+)/update/$', + views.UpdateView.as_view(), name='update'), + re_path(r'^create/$', views.CreateView.as_view(), name='create'), +] diff --git a/openstack_dashboard/dashboards/settings/credentials/views.py b/openstack_dashboard/dashboards/settings/credentials/views.py new file mode 100644 index 0000000000..7bbded7904 --- /dev/null +++ b/openstack_dashboard/dashboards/settings/credentials/views.py @@ -0,0 +1,47 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ + +from horizon import tables + +from openstack_dashboard.dashboards.identity.credentials \ + import views as credential_views +from openstack_dashboard.dashboards.identity.users.tabs \ + import get_credentials +from openstack_dashboard.dashboards.settings.credentials \ + import forms as credential_forms +from openstack_dashboard.dashboards.settings.credentials \ + import tables as credential_tables + + +class CredentialsView(tables.DataTableView): + table_class = credential_tables.CredentialsTable + page_title = _("Credentials") + policy_rules = (("identity", "identity:list_credentials"),) + + def get_data(self): + user = self.request.user + return get_credentials(self.request, user) + + +class UpdateView(credential_views.UpdateView): + form_class = credential_forms.UpdateCredentialForm + submit_url = "horizon:settings:credentials:update" + success_url = reverse_lazy('horizon:settings:credentials:index') + + +class CreateView(credential_views.CreateView): + form_class = credential_forms.CreateCredentialForm + submit_url = reverse_lazy("horizon:settings:credentials:create") + success_url = reverse_lazy('horizon:settings:credentials:index') diff --git a/openstack_dashboard/dashboards/settings/dashboard.py b/openstack_dashboard/dashboards/settings/dashboard.py index b1f1d006be..ed9f56039e 100644 --- a/openstack_dashboard/dashboards/settings/dashboard.py +++ b/openstack_dashboard/dashboards/settings/dashboard.py @@ -21,7 +21,7 @@ import horizon class Settings(horizon.Dashboard): name = _("Settings") slug = "settings" - panels = ('user', 'password', ) + panels = ('user', 'password', 'credentials', ) default_panel = 'user' def nav(self, context): diff --git a/openstack_dashboard/enabled/_3100_identity_credentials_panel.py b/openstack_dashboard/enabled/_3100_identity_credentials_panel.py new file mode 100644 index 0000000000..86798a2359 --- /dev/null +++ b/openstack_dashboard/enabled/_3100_identity_credentials_panel.py @@ -0,0 +1,10 @@ +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'credentials' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'identity' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'default' + +# Python panel class of the PANEL to be added. +ADD_PANEL = ('openstack_dashboard.dashboards.identity.credentials' + '.panel.CredentialsPanel') diff --git a/openstack_dashboard/test/test_data/keystone_data.py b/openstack_dashboard/test/test_data/keystone_data.py index 8aa9f15017..72407bfd6d 100644 --- a/openstack_dashboard/test/test_data/keystone_data.py +++ b/openstack_dashboard/test/test_data/keystone_data.py @@ -27,6 +27,7 @@ from keystoneclient.v3 import application_credentials from keystoneclient.v3.contrib.federation import identity_providers from keystoneclient.v3.contrib.federation import mappings from keystoneclient.v3.contrib.federation import protocols +from keystoneclient.v3 import credentials from keystoneclient.v3 import domains from keystoneclient.v3 import groups from keystoneclient.v3 import role_assignments @@ -181,6 +182,7 @@ def data(TEST): TEST.idp_protocols = utils.TestDataContainer() TEST.application_credentials = utils.TestDataContainer() + TEST.credentials = utils.TestDataContainer() admin_role_dict = {'id': '1', 'name': 'admin'} @@ -540,3 +542,21 @@ def data(TEST): app_cred_detail = application_credentials.ApplicationCredential( None, app_cred_dict) TEST.application_credentials.add(app_cred_create, app_cred_detail) + + user_cred_dict = { + 'id': 'cred1', + 'user_id': '1', + 'type': 'totp', + 'blob': 'ONSWG4TFOQYTM43FMNZGK5BRGYFA', + 'project_id': 'project1' + } + user_cred_create = credentials.Credential(None, user_cred_dict) + user_cred_dict = { + 'id': 'cred2', + 'user_id': '2', + 'type': 'totp', + 'blob': 'ONSWG4TFOQYTM43FMNZGK5BRGYFA', + 'project_id': 'project2' + } + user_cred_detail = credentials.Credential(None, user_cred_dict) + TEST.credentials.add(user_cred_create, user_cred_detail)