Merge "Improve two factor authentication config in Horizon"
This commit is contained in:
commit
fa1e28805c
openstack_dashboard
api
dashboards
identity
settings
credentials
dashboard.pyenabled
test/test_data
@ -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
|
||||
|
115
openstack_dashboard/dashboards/identity/credentials/forms.py
Normal file
115
openstack_dashboard/dashboards/identity/credentials/forms.py
Normal 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.'))
|
34
openstack_dashboard/dashboards/identity/credentials/panel.py
Normal file
34
openstack_dashboard/dashboards/identity/credentials/panel.py
Normal 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)
|
103
openstack_dashboard/dashboards/identity/credentials/tables.py
Normal file
103
openstack_dashboard/dashboards/identity/credentials/tables.py
Normal 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)
|
13
openstack_dashboard/dashboards/identity/credentials/templates/credentials/_create.html
Normal file
13
openstack_dashboard/dashboards/identity/credentials/templates/credentials/_create.html
Normal 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": <access>, "secret": <secret>}</tt>.
|
||||
{% endblocktrans %}</p>
|
||||
{% endblock %}
|
13
openstack_dashboard/dashboards/identity/credentials/templates/credentials/_update.html
Normal file
13
openstack_dashboard/dashboards/identity/credentials/templates/credentials/_update.html
Normal 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": <access>, "secret": <secret>}</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 %}
|
39
openstack_dashboard/dashboards/identity/credentials/tests.py
Normal file
39
openstack_dashboard/dashboards/identity/credentials/tests.py
Normal 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)
|
23
openstack_dashboard/dashboards/identity/credentials/urls.py
Normal file
23
openstack_dashboard/dashboards/identity/credentials/urls.py
Normal 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'),
|
||||
]
|
107
openstack_dashboard/dashboards/identity/credentials/views.py
Normal file
107
openstack_dashboard/dashboards/identity/credentials/views.py
Normal 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")
|
@ -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,)
|
||||
|
33
openstack_dashboard/dashboards/settings/credentials/forms.py
Normal file
33
openstack_dashboard/dashboards/settings/credentials/forms.py
Normal 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'
|
25
openstack_dashboard/dashboards/settings/credentials/panel.py
Normal file
25
openstack_dashboard/dashboards/settings/credentials/panel.py
Normal 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)
|
@ -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)
|
13
openstack_dashboard/dashboards/settings/credentials/templates/credentials/_create.html
Normal file
13
openstack_dashboard/dashboards/settings/credentials/templates/credentials/_create.html
Normal 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": <access>, "secret": <secret>}</tt>.
|
||||
{% endblocktrans %}</p>
|
||||
{% endblock %}
|
13
openstack_dashboard/dashboards/settings/credentials/templates/credentials/_update.html
Normal file
13
openstack_dashboard/dashboards/settings/credentials/templates/credentials/_update.html
Normal 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": <access>, "secret": <secret>}</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 %}
|
39
openstack_dashboard/dashboards/settings/credentials/tests.py
Normal file
39
openstack_dashboard/dashboards/settings/credentials/tests.py
Normal 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)
|
23
openstack_dashboard/dashboards/settings/credentials/urls.py
Normal file
23
openstack_dashboard/dashboards/settings/credentials/urls.py
Normal 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'),
|
||||
]
|
47
openstack_dashboard/dashboards/settings/credentials/views.py
Normal file
47
openstack_dashboard/dashboards/settings/credentials/views.py
Normal 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')
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user