Merge "Improve two factor authentication config in Horizon"

This commit is contained in:
Zuul 2025-03-17 17:35:48 +00:00 committed by Gerrit Code Review
commit fa1e28805c
29 changed files with 837 additions and 2 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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