From 5cb96dd6d1ca52dcb006fbeecb0a71e5f95e183f Mon Sep 17 00:00:00 2001 From: David Gutman Date: Fri, 3 Aug 2018 17:01:45 +0200 Subject: [PATCH] Add user tab in project details view. Add an extra tab "users" in the project details view which displays the users which have a role on the project (the members of the project). The users are displayed in a table which is an extension (inheritance) of the user table used in the Users panel. An extra column is added to this table, displaying the roles of each user on project Only users which have directly a role on the project are displayed, the users which have a role through a group are not displayed. Change-Id: I88b40fcda300ee4640155347d479a972abb2df02 Partial-Bug: #1785263 --- .../dashboards/identity/projects/tabs.py | 85 ++++++++++++++++- .../dashboards/identity/projects/tests.py | 95 +++++++++++++++++++ .../identity/projects/users/__init__.py | 0 .../identity/projects/users/tables.py | 33 +++++++ 4 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 openstack_dashboard/dashboards/identity/projects/users/__init__.py create mode 100644 openstack_dashboard/dashboards/identity/projects/users/tables.py diff --git a/openstack_dashboard/dashboards/identity/projects/tabs.py b/openstack_dashboard/dashboards/identity/projects/tabs.py index 739ee5fa58..ac95f1ec32 100644 --- a/openstack_dashboard/dashboards/identity/projects/tabs.py +++ b/openstack_dashboard/dashboards/identity/projects/tabs.py @@ -13,10 +13,14 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ +from horizon import exceptions from horizon import tabs from openstack_dashboard import api +from openstack_dashboard.dashboards.identity.projects.users \ + import tables as users_tables + class OverviewTab(tabs.Tab): """Overview of the project. """ @@ -37,6 +41,85 @@ class OverviewTab(tabs.Tab): return context +class UsersTab(tabs.TableTab): + """Display users member of the project. (directly or through a group).""" + table_classes = (users_tables.UsersTable,) + name = _("Users") + slug = "users" + template_name = "horizon/common/_detail_table.html" + preload = False + + def _update_user_roles_names_from_roles_id(self, user, users_roles, + roles_list): + """Add roles names to user.roles, based on users_roles. + + :param user: user to update + :param users_roles: list of roles ID + :param roles_list: list of roles obtained with keystone + """ + user_roles_names = [role.name for role in roles_list + if role.id in users_roles] + current_user_roles_names = set(getattr(user, "roles", [])) + user.roles = list(current_user_roles_names.union(user_roles_names)) + + def _get_users_from_project(self, project_id, roles, project_users): + """Update with users which have role on project NOT through a group. + + :param project_id: ID of the project + :param roles: list of roles from keystone + :param project_users: list to be updated with the users found + """ + + # For keystone.user_list project_id is not passed as argument because + # it is ignored when using admin credentials + # Get all users (to be able to find user name) + users = api.keystone.user_list(self.request) + users = {user.id: user for user in users} + + # Get project_users_roles ({user_id: [role_id_1, role_id_2]}) + project_users_roles = api.keystone.get_project_users_roles( + self.request, + project=project_id) + + for user_id in project_users_roles: + + if user_id not in project_users: + # Add user to the project_users + project_users[user_id] = users[user_id] + project_users[user_id].roles = [] + project_users[user_id].roles_from_groups = [] + + # Update the project_user role in order to get: + # project_users[user_id].roles = [role_name1, role_name2] + self._update_user_roles_names_from_roles_id( + user=project_users[user_id], + users_roles=project_users_roles[user_id], + roles_list=roles + ) + + def get_userstable_data(self): + """Get users with roles on the project.""" + project_users = {} + project = self.tab_group.kwargs['project'] + + try: + # Get all global roles once to avoid multiple requests. + roles = api.keystone.role_list(self.request) + + # Update project_users with users which have role directly on + # the project, (NOT through a group) + self._get_users_from_project(project_id=project.id, + roles=roles, + project_users=project_users) + + except Exception: + exceptions.handle(self.request, + _("Unable to display the users of this project.") + ) + + return project_users.values() + + class ProjectDetailTabs(tabs.DetailTabsGroup): slug = "project_details" - tabs = (OverviewTab,) + tabs = (OverviewTab, UsersTab,) diff --git a/openstack_dashboard/dashboards/identity/projects/tests.py b/openstack_dashboard/dashboards/identity/projects/tests.py index 61ac4afc78..c68835a84f 100644 --- a/openstack_dashboard/dashboards/identity/projects/tests.py +++ b/openstack_dashboard/dashboards/identity/projects/tests.py @@ -1350,6 +1350,101 @@ class DetailProjectViewTests(test.BaseAdminViewTests): self.tenant.id) self.mock_enabled_quotas.assert_called_once_with(test.IsHttpRequest()) + def _project_user_roles(self, role_assignments): + roles = {} + for role_assignment in role_assignments: + if hasattr(role_assignment, 'user'): + roles[role_assignment.user['id']] = [ + role_assignment.role["id"]] + return roles + + @test.create_mocks({api.keystone: ('tenant_get', + 'user_list', + 'get_project_users_roles', + 'role_list',), + quotas: ('enabled_quotas',)}) + def test_detail_view_users_tab(self): + project = self.tenants.first() + users = self.users.filter(domain_id=project.domain_id) + role_assignments = self.role_assignments.filter( + scope={'project': {'id': project.id}}) + project_users_roles = self._project_user_roles(role_assignments) + + # Prepare mocks + self.mock_tenant_get.return_value = project + self.mock_enabled_quotas.return_value = ('instances',) + self.mock_role_list.return_value = self.roles.list() + + self.mock_user_list.return_value = users + self.mock_get_project_users_roles.return_value = project_users_roles + + # Get project details view on user tab + url = PROJECT_DETAIL_URL % [project.id] + detail_view = tabs.ProjectDetailTabs(self.request, group=project) + users_tab_link = "?%s=%s" % ( + detail_view.param_name, + detail_view.get_tab("users").get_id() + ) + url += users_tab_link + res = self.client.get(url) + + self.assertTemplateUsed(res, "horizon/common/_detail_table.html") + + # Check the content of the table + users_expected = { + '1': {'roles': ['admin'], }, + '2': {'roles': ['_member_'], }, + '3': {'roles': ['_member_'], }, + } + + users_id_observed = [user.id for user in + res.context["userstable_table"].data] + self.assertItemsEqual(users_expected.keys(), users_id_observed) + + # Check the users roles + for user in res.context["userstable_table"].data: + self.assertItemsEqual(users_expected[user.id]["roles"], + user.roles) + + self.mock_tenant_get.assert_called_once_with(test.IsHttpRequest(), + self.tenant.id) + self.mock_enabled_quotas.assert_called_once_with(test.IsHttpRequest()) + self.mock_role_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_get_project_users_roles.assert_called_once_with( + test.IsHttpRequest(), project=project.id) + self.mock_user_list.assert_called_once_with(test.IsHttpRequest()) + + @test.create_mocks({api.keystone: ("tenant_get", + "role_list",), + quotas: ('enabled_quotas',)}) + def test_detail_view_users_tab_exception(self): + project = self.tenants.first() + + # Prepare mocks + self.mock_tenant_get.return_value = project + self.mock_enabled_quotas.return_value = ('instances',) + self.mock_role_list.side_effect = self.exceptions.keystone + + # Get project details view on user tab + url = reverse('horizon:identity:projects:detail', args=[project.id]) + detail_view = tabs.ProjectDetailTabs(self.request, group=project) + users_tab_link = "?%s=%s" % ( + detail_view.param_name, + detail_view.get_tab("users").get_id() + ) + url += users_tab_link + res = self.client.get(url) + + # Check the projects table is empty + self.assertFalse(res.context["userstable_table"].data) + # Check one error message is displayed + self.assertMessageCount(res, error=1) + + self.mock_tenant_get.assert_called_once_with(test.IsHttpRequest(), + self.tenant.id) + self.mock_enabled_quotas.assert_called_once_with(test.IsHttpRequest()) + self.mock_role_list.assert_called_once_with(test.IsHttpRequest()) + @tag('selenium') class SeleniumTests(test.SeleniumAdminTestCase, test.TestCase): diff --git a/openstack_dashboard/dashboards/identity/projects/users/__init__.py b/openstack_dashboard/dashboards/identity/projects/users/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/identity/projects/users/tables.py b/openstack_dashboard/dashboards/identity/projects/users/tables.py new file mode 100644 index 0000000000..aeafc7a0d2 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/projects/users/tables.py @@ -0,0 +1,33 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext_lazy as _ + +from horizon import forms +from horizon import tables + +from openstack_dashboard.dashboards.identity.users \ + import tables as users_tables + + +class UsersTable(users_tables.UsersTable): + """Display Users of the project with roles.""" + roles = tables.Column( + lambda obj: ", ".join(getattr(obj, 'roles', [])), + verbose_name=_('Roles'), + form_field=forms.CharField( + widget=forms.Textarea(attrs={'rows': 4}), + required=False)) + + class Meta(object): + name = "userstable" + verbose_name = _("Users")