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