Improve two factor authentication config in Horizon

User Credentials panel is added to Identity dashboard.
Credentials table has Create, Update and Delete credential actions.

Credentials tab is added to the user details for
Identity -> Users table.

Credentials panel is added to user settings.

Change-Id: Icaabed327604d39b0bf6ac3e3cacf9c62f9e5d5d
This commit is contained in:
Tatiana Ovchinnikova 2024-12-11 14:42:46 -06:00
parent 378addadbe
commit edad0dbfc4
29 changed files with 837 additions and 2 deletions

View File

@ -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

View File

@ -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.'))

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,13 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Create a new user credential." %}</p>
<p>{% blocktrans trimmed %}
Project limits the scope of the credential. It is is mandatory if the credential type is EC2.
{% endblocktrans %}</p>
<p>{% blocktrans trimmed %}
If the credential type is EC2, credential data has to be <tt>{"access": &lt;access&gt;, "secret": &lt;secret&gt;}</tt>.
{% endblocktrans %}</p>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Edit the credential's details." %}</p>
<p>{% blocktrans trimmed %}
Project limits the scope of the credential. It is is mandatory if the credential type is EC2.
{% endblocktrans %}</p>
<p>{% blocktrans trimmed %}
If the credential type is EC2, credential data has to be <tt>{"access": &lt;access&gt;, "secret": &lt;secret&gt;}</tt>.
{% endblocktrans %}</p>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create User Credential" %}{% endblock %}
{% block main %}
{% include 'identity/credentials/_create.html' %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update User Credential" %}{% endblock %}
{% block main %}
{% include 'identity/credentials/_update.html' %}
{% endblock %}

View File

@ -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)

View File

@ -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<credential_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'),
re_path(r'^create/$', views.CreateView.as_view(), name='create'),
]

View File

@ -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")

View File

@ -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")

View File

@ -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,)

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,13 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Create a new credential." %}</p>
<p>{% blocktrans trimmed %}
Project limits the scope of the credential. It is is mandatory if the credential type is EC2.
{% endblocktrans %}</p>
<p>{% blocktrans trimmed %}
If the credential type is EC2, credential data has to be <tt>{"access": &lt;access&gt;, "secret": &lt;secret&gt;}</tt>.
{% endblocktrans %}</p>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Edit the credential's details." %}</p>
<p>{% blocktrans trimmed %}
Project limits the scope of the credential. It is is mandatory if the credential type is EC2.
{% endblocktrans %}</p>
<p>{% blocktrans trimmed %}
If the credential type is EC2, credential data has to be <tt>{"access": &lt;access&gt;, "secret": &lt;secret&gt;}</tt>.
{% endblocktrans %}</p>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Credential" %}{% endblock %}
{% block main %}
{% include 'settings/credentials/_create.html' %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update Credential" %}{% endblock %}
{% block main %}
{% include 'settings/credentials/_update.html' %}
{% endblock %}

View File

@ -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)

View File

@ -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<credential_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'),
re_path(r'^create/$', views.CreateView.as_view(), name='create'),
]

View File

@ -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')

View File

@ -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):

View File

@ -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')

View File

@ -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)