diff --git a/openstack_dashboard/api/keystone.py b/openstack_dashboard/api/keystone.py index 46ffc9305e..05cfeb677b 100644 --- a/openstack_dashboard/api/keystone.py +++ b/openstack_dashboard/api/keystone.py @@ -394,9 +394,27 @@ def group_delete(request, group_id): return manager.delete(group_id) -def group_list(request): +def group_list(request, domain=None, project=None, user=None): manager = keystoneclient(request, admin=True).groups - return manager.list() + groups = manager.list(user=user) + # TODO(dklyle): once keystoneclient supports filtering by + # domain change this to use that cleaner implementation + if domain: + domain_groups = [] + for group in groups: + if group.domain_id == domain: + domain_groups.append(group) + groups = domain_groups + + if project: + project_groups = [] + for group in groups: + roles = roles_for_group(request, group=group.id, project=project) + if roles and len(roles) > 0: + project_groups.append(group) + groups = project_groups + + return groups def group_update(request, group_id, name=None, description=None): @@ -480,6 +498,36 @@ def remove_tenant_user(request, project=None, user=None, domain=None): project=project, domain=domain) +def roles_for_group(request, group, domain=None, project=None): + manager = keystoneclient(request, admin=True).roles + return manager.list(group=group, domain=domain, project=project) + + +def add_group_role(request, role, group, domain=None, project=None): + """ Adds a role for a group on a domain or project .""" + manager = keystoneclient(request, admin=True).roles + return manager.grant(role=role, group=group, domain=domain, + project=project) + + +def remove_group_role(request, role, group, domain=None, project=None): + """ Removes a given single role for a group from a domain or project. """ + manager = keystoneclient(request, admin=True).roles + return manager.revoke(role=role, group=group, project=project, + domain=domain) + + +def remove_group_roles(request, group, domain=None, project=None): + """ Removes all roles from a group on a domain or project, + removing them from it. + """ + client = keystoneclient(request, admin=True) + roles = client.roles.list(group=group, domain=domain, project=project) + for role in roles: + remove_group_role(request, role=role.id, group=group, + domain=domain, project=project) + + def get_default_role(request): """ Gets the default role object from Keystone and saves it as a global diff --git a/openstack_dashboard/dashboards/admin/groups/tests.py b/openstack_dashboard/dashboards/admin/groups/tests.py index 2bb59105bd..a0b36e5b71 100644 --- a/openstack_dashboard/dashboards/admin/groups/tests.py +++ b/openstack_dashboard/dashboards/admin/groups/tests.py @@ -63,7 +63,8 @@ class GroupsViewTests(test.BaseAdminViewTests): domain_id = self._get_domain_id() groups = self._get_groups(domain_id) - api.keystone.group_list(IgnoreArg()).AndReturn(groups) + api.keystone.group_list(IgnoreArg(), domain=domain_id) \ + .AndReturn(groups) self.mox.ReplayAll() @@ -91,7 +92,8 @@ class GroupsViewTests(test.BaseAdminViewTests): domain_id = self._get_domain_id() groups = self._get_groups(domain_id) - api.keystone.group_list(IgnoreArg()).AndReturn(groups) + api.keystone.group_list(IgnoreArg(), domain=domain_id) \ + .AndReturn(groups) api.keystone.keystone_can_edit_group() \ .MultipleTimes().AndReturn(False) @@ -158,9 +160,11 @@ class GroupsViewTests(test.BaseAdminViewTests): @test.create_stubs({api.keystone: ('group_list', 'group_delete')}) def test_delete_group(self): + domain_id = self._get_domain_id() group = self.groups.get(id="2") - api.keystone.group_list(IgnoreArg()).AndReturn(self.groups.list()) + api.keystone.group_list(IgnoreArg(), domain=domain_id) \ + .AndReturn(self.groups.list()) api.keystone.group_delete(IgnoreArg(), group.id) self.mox.ReplayAll() diff --git a/openstack_dashboard/dashboards/admin/groups/views.py b/openstack_dashboard/dashboards/admin/groups/views.py index 4c78223abb..d6b5a029f1 100644 --- a/openstack_dashboard/dashboards/admin/groups/views.py +++ b/openstack_dashboard/dashboards/admin/groups/views.py @@ -55,15 +55,8 @@ class IndexView(tables.DataTableView): groups = [] domain_context = self.request.session.get('domain_context', None) try: - # TODO(dklyle): once keystoneclient supports filtering by - # domain change this to use that cleaner method - groups = api.keystone.group_list(self.request) - if domain_context: - domain_groups = [] - for group in groups: - if group.domain_id == domain_context: - domain_groups.append(group) - groups = domain_groups + groups = api.keystone.group_list(self.request, + domain=domain_context) except Exception: exceptions.handle(self.request, _('Unable to retrieve group list.')) diff --git a/openstack_dashboard/dashboards/admin/projects/tables.py b/openstack_dashboard/dashboards/admin/projects/tables.py index 2924db5cfa..296d8001c0 100644 --- a/openstack_dashboard/dashboards/admin/projects/tables.py +++ b/openstack_dashboard/dashboards/admin/projects/tables.py @@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _ from horizon import tables from openstack_dashboard import api +from openstack_dashboard.api.keystone import VERSIONS as IDENTITY_VERSIONS LOG = logging.getLogger(__name__) @@ -25,6 +26,22 @@ class ViewMembersLink(tables.LinkAction): return "?".join([base_url, param]) +class ViewGroupsLink(tables.LinkAction): + name = "groups" + verbose_name = _("Modify Groups") + url = "horizon:admin:projects:update" + classes = ("ajax-modal", "btn-edit") + + def allowed(self, request, project): + return IDENTITY_VERSIONS.active >= 3 + + def get_link_url(self, project): + step = 'update_group_members' + base_url = reverse(self.url, args=[project.id]) + param = urlencode({"step": step}) + return "?".join([base_url, param]) + + class UsageLink(tables.LinkAction): name = "usage" verbose_name = _("View Usage") @@ -100,8 +117,8 @@ class TenantsTable(tables.DataTable): class Meta: name = "tenants" verbose_name = _("Projects") - row_actions = (ViewMembersLink, UpdateProject, UsageLink, - ModifyQuotas, DeleteTenantsAction) + row_actions = (ViewMembersLink, ViewGroupsLink, UpdateProject, + UsageLink, ModifyQuotas, DeleteTenantsAction) table_actions = (TenantFilterAction, CreateProject, DeleteTenantsAction) pagination_param = "tenant_marker" diff --git a/openstack_dashboard/dashboards/admin/projects/tests.py b/openstack_dashboard/dashboards/admin/projects/tests.py index b2c630c6a5..dc60a6cab2 100644 --- a/openstack_dashboard/dashboards/admin/projects/tests.py +++ b/openstack_dashboard/dashboards/admin/projects/tests.py @@ -30,6 +30,8 @@ from openstack_dashboard.usage import quotas from openstack_dashboard.dashboards.admin.projects.workflows \ import CreateProject +from openstack_dashboard.dashboards.admin.projects.workflows \ + import PROJECT_GROUP_MEMBER_SLUG from openstack_dashboard.dashboards.admin.projects.workflows \ import PROJECT_USER_MEMBER_SLUG from openstack_dashboard.dashboards.admin.projects.workflows \ @@ -37,7 +39,8 @@ from openstack_dashboard.dashboards.admin.projects.workflows \ INDEX_URL = reverse('horizon:admin:projects:index') -USER_ROLE_PREFIX = PROJECT_USER_MEMBER_SLUG + "_role_" +USER_ROLE_PREFIX = PROJECT_GROUP_MEMBER_SLUG + "_role_" +GROUP_ROLE_PREFIX = PROJECT_USER_MEMBER_SLUG + "_role_" @test.create_stubs({api.keystone: ('tenant_list',)}) @@ -112,8 +115,17 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): if user.domain_id == domain_id] return users + def _get_all_groups(self, domain_id): + if not domain_id: + groups = self.groups.list() + else: + groups = [group for group in self.groups.list() + if group.domain_id == domain_id] + return groups + @test.create_stubs({api.keystone: ('get_default_role', 'user_list', + 'group_list', 'role_list'), quotas: ('get_default_quota_data',)}) def test_add_project_get(self): @@ -121,16 +133,20 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): default_role = self.roles.first() domain_id = self._get_domain_id() users = self._get_all_users(domain_id) + groups = self._get_all_groups(domain_id) roles = self.roles.list() quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) # init api.keystone.get_default_role(IsA(http.HttpRequest)) \ - .AndReturn(default_role) + .MultipleTimes().AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \ .AndReturn(users) api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles) + api.keystone.group_list(IsA(http.HttpRequest), domain=domain_id) \ + .AndReturn(groups) + api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles) self.mox.ReplayAll() @@ -149,6 +165,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): self.assertQuerysetEqual(workflow.steps, ['', '', + '', '']) def test_add_project_get_domain(self): @@ -161,6 +178,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): 'add_tenant_user_role', 'tenant_create', 'user_list', + 'group_list', 'role_list'), quotas: ('get_default_quota_data',), api.cinder: ('tenant_quota_update',), @@ -171,17 +189,20 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): default_role = self.roles.first() domain_id = self._get_domain_id() users = self._get_all_users(domain_id) + groups = self._get_all_groups(domain_id) roles = self.roles.list() # init quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.keystone.get_default_role(IsA(http.HttpRequest)) \ - .AndReturn(default_role) + .MultipleTimes().AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \ .AndReturn(users) api.keystone.role_list(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(roles) + api.keystone.group_list(IsA(http.HttpRequest), domain=domain_id) \ + .AndReturn(groups) # handle project_details = self._get_project_info(project) @@ -199,6 +220,14 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): project=self.tenant.id, user=user_id, role=role.id) + for role in roles: + if GROUP_ROLE_PREFIX + role.id in workflow_data: + ulist = workflow_data[GROUP_ROLE_PREFIX + role.id] + for group_id in ulist: + api.keystone.add_group_role(IsA(http.HttpRequest), + role=role.id, + group=group_id, + project=self.tenant.id) nova_updated_quota = dict([(key, quota_data[key]) for key in quotas.NOVA_QUOTA_FIELDS]) @@ -229,12 +258,14 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): @test.create_stubs({api.keystone: ('user_list', 'role_list', + 'group_list', 'get_default_role'), quotas: ('get_default_quota_data',)}) def test_add_project_quota_defaults_error(self): default_role = self.roles.first() domain_id = self._get_domain_id() users = self._get_all_users(domain_id) + groups = self._get_all_groups(domain_id) roles = self.roles.list() # init @@ -242,11 +273,13 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): .AndRaise(self.exceptions.nova) api.keystone.get_default_role(IsA(http.HttpRequest)) \ - .AndReturn(default_role) + .MultipleTimes().AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \ .AndReturn(users) api.keystone.role_list(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(roles) + api.keystone.group_list(IsA(http.HttpRequest), domain=domain_id) \ + .AndReturn(groups) self.mox.ReplayAll() @@ -265,6 +298,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): @test.create_stubs({api.keystone: ('tenant_create', 'user_list', 'role_list', + 'group_list', 'get_default_role'), quotas: ('get_default_quota_data',)}) def test_add_project_tenant_create_error(self): @@ -273,17 +307,20 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): default_role = self.roles.first() domain_id = self._get_domain_id() users = self._get_all_users(domain_id) + groups = self._get_all_groups(domain_id) roles = self.roles.list() # init quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.keystone.get_default_role(IsA(http.HttpRequest)) \ - .AndReturn(default_role) + .MultipleTimes().AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \ .AndReturn(users) api.keystone.role_list(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(roles) + api.keystone.group_list(IsA(http.HttpRequest), domain=domain_id) \ + .AndReturn(groups) # handle project_details = self._get_project_info(project) @@ -310,6 +347,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): @test.create_stubs({api.keystone: ('tenant_create', 'user_list', 'role_list', + 'group_list', 'get_default_role', 'add_tenant_user_role'), quotas: ('get_default_quota_data',), @@ -320,17 +358,20 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): default_role = self.roles.first() domain_id = self._get_domain_id() users = self._get_all_users(domain_id) + groups = self._get_all_groups(domain_id) roles = self.roles.list() # init quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.keystone.get_default_role(IsA(http.HttpRequest)) \ - .AndReturn(default_role) + .MultipleTimes().AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \ .AndReturn(users) api.keystone.role_list(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(roles) + api.keystone.group_list(IsA(http.HttpRequest), domain=domain_id) \ + .AndReturn(groups) # handle project_details = self._get_project_info(project) @@ -348,6 +389,14 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): project=self.tenant.id, user=user_id, role=role.id) + for role in roles: + if GROUP_ROLE_PREFIX + role.id in workflow_data: + ulist = workflow_data[GROUP_ROLE_PREFIX + role.id] + for group_id in ulist: + api.keystone.add_group_role(IsA(http.HttpRequest), + role=role.id, + group=group_id, + project=self.tenant.id) nova_updated_quota = dict([(key, quota_data[key]) for key in quotas.NOVA_QUOTA_FIELDS]) @@ -375,6 +424,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): @test.create_stubs({api.keystone: ('tenant_create', 'user_list', 'role_list', + 'group_list', 'get_default_role', 'add_tenant_user_role'), quotas: ('get_default_quota_data',), @@ -386,17 +436,20 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): default_role = self.roles.first() domain_id = self._get_domain_id() users = self._get_all_users(domain_id) + groups = self._get_all_groups(domain_id) roles = self.roles.list() # init quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.keystone.get_default_role(IsA(http.HttpRequest)) \ - .AndReturn(default_role) + .MultipleTimes().AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \ .AndReturn(users) api.keystone.role_list(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(roles) + api.keystone.group_list(IsA(http.HttpRequest), domain=domain_id) \ + .AndReturn(groups) # handle project_details = self._get_project_info(project) @@ -448,6 +501,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): @test.create_stubs({api.keystone: ('user_list', 'role_list', + 'group_list', 'get_default_role'), quotas: ('get_default_quota_data',)}) def test_add_project_missing_field_error(self): @@ -456,17 +510,20 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): default_role = self.roles.first() domain_id = self._get_domain_id() users = self._get_all_users(domain_id) + groups = self._get_all_groups(domain_id) roles = self.roles.list() # init quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.keystone.get_default_role(IsA(http.HttpRequest)) \ - .AndReturn(default_role) + .MultipleTimes().AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \ .AndReturn(users) api.keystone.role_list(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(roles) + api.keystone.group_list(IsA(http.HttpRequest), domain=domain_id) \ + .AndReturn(groups) self.mox.ReplayAll() @@ -506,14 +563,28 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): if user.domain_id == domain_id] return users + def _get_all_groups(self, domain_id): + if not domain_id: + groups = self.groups.list() + else: + groups = [group for group in self.groups.list() + if group.domain_id == domain_id] + return groups + def _get_proj_users(self, project_id): return [user for user in self.users.list() if user.project_id == project_id] + def _get_proj_groups(self, project_id): + return [group for group in self.groups.list() + if group.project_id == project_id] + @test.create_stubs({api.keystone: ('get_default_role', 'roles_for_user', 'tenant_get', 'user_list', + 'roles_for_group', + 'group_list', 'role_list'), quotas: ('get_tenant_quota_data',)}) def test_update_project_get(self): @@ -522,6 +593,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): default_role = self.roles.first() domain_id = self._get_domain_id() users = self._get_all_users(domain_id) + groups = self._get_all_groups(domain_id) roles = self.roles.list() api.keystone.tenant_get(IsA(http.HttpRequest), @@ -532,17 +604,25 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): .AndReturn(quota) api.keystone.get_default_role(IsA(http.HttpRequest)) \ - .AndReturn(default_role) + .MultipleTimes().AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \ .AndReturn(users) api.keystone.role_list(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(roles) + api.keystone.group_list(IsA(http.HttpRequest), domain=domain_id) \ + .AndReturn(groups) for user in users: api.keystone.roles_for_user(IsA(http.HttpRequest), user.id, self.tenant.id).AndReturn(roles) + for group in groups: + api.keystone.roles_for_group(IsA(http.HttpRequest), + group=group.id, + project=self.tenant.id) \ + .AndReturn(roles) + self.mox.ReplayAll() url = reverse('horizon:admin:projects:update', @@ -564,6 +644,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): self.assertQuerysetEqual(workflow.steps, ['', '', + '', '']) def test_update_project_get_domain(self): @@ -579,6 +660,10 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): 'remove_tenant_user_role', 'add_tenant_user_role', 'user_list', + 'roles_for_group', + 'remove_group_role', + 'add_group_role', + 'group_list', 'role_list'), api.nova: ('tenant_quota_update',), api.cinder: ('tenant_quota_update',), @@ -590,6 +675,8 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): domain_id = self._get_domain_id() users = self._get_all_users(domain_id) proj_users = self._get_proj_users(project.id) + groups = self._get_all_groups(domain_id) + proj_groups = self._get_proj_groups(project.id) roles = self.roles.list() # get/init @@ -601,20 +688,30 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): .AndReturn(quota) api.keystone.get_default_role(IsA(http.HttpRequest)) \ - .AndReturn(default_role) + .MultipleTimes().AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \ .AndReturn(users) api.keystone.role_list(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(roles) + api.keystone.group_list(IsA(http.HttpRequest), domain=domain_id) \ + .AndReturn(groups) workflow_data = {} for user in users: api.keystone.roles_for_user(IsA(http.HttpRequest), user.id, self.tenant.id).AndReturn(roles) + for group in groups: + api.keystone.roles_for_group(IsA(http.HttpRequest), + group=group.id, + project=self.tenant.id) \ + .AndReturn(roles) workflow_data[USER_ROLE_PREFIX + "1"] = ['3'] # admin role workflow_data[USER_ROLE_PREFIX + "2"] = ['2'] # member role + # Group assignment form data + workflow_data[GROUP_ROLE_PREFIX + "1"] = ['3'] # admin role + workflow_data[GROUP_ROLE_PREFIX + "2"] = ['2'] # member role # update some fields project._info["name"] = "updated name" @@ -671,6 +768,54 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): user='3', role='1') + # Group assignments + api.keystone.group_list(IsA(http.HttpRequest), + domain=domain_id, + project=self.tenant.id).AndReturn(proj_groups) + + # admin group - try to remove all roles on current project + api.keystone.roles_for_group(IsA(http.HttpRequest), + group='1', + project=self.tenant.id) \ + .AndReturn(roles) + for role in roles: + api.keystone.remove_group_role(IsA(http.HttpRequest), + role=role.id, + group='1', + project=self.tenant.id) + + # member group 1 - has role 1, will remove it + api.keystone.roles_for_group(IsA(http.HttpRequest), + group='2', + project=self.tenant.id) \ + .AndReturn((roles[0],)) + # remove role 1 + api.keystone.remove_group_role(IsA(http.HttpRequest), + role='1', + group='2', + project=self.tenant.id) + # add role 2 + api.keystone.add_group_role(IsA(http.HttpRequest), + role='2', + group='2', + project=self.tenant.id) + + # member group 3 - has role 2 + api.keystone.roles_for_group(IsA(http.HttpRequest), + group='3', + project=self.tenant.id) \ + .AndReturn((roles[1],)) + # remove role 2 + api.keystone.remove_group_role(IsA(http.HttpRequest), + role='2', + group='3', + project=self.tenant.id) + # add role 1 + api.keystone.add_group_role(IsA(http.HttpRequest), + role='1', + group='3', + project=self.tenant.id) + nova_updated_quota = dict([(key, updated_quota[key]) for key in quotas.NOVA_QUOTA_FIELDS]) api.nova.tenant_quota_update(IsA(http.HttpRequest), @@ -727,6 +872,10 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): 'remove_tenant_user', 'add_tenant_user_role', 'user_list', + 'roles_for_group', + 'remove_group_role', + 'add_group_role', + 'group_list', 'role_list'), quotas: ('get_tenant_quota_data',), api.nova: ('tenant_quota_update',)}) @@ -736,6 +885,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): default_role = self.roles.first() domain_id = self._get_domain_id() users = self._get_all_users(domain_id) + groups = self._get_all_groups(domain_id) roles = self.roles.list() # get/init @@ -747,11 +897,13 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): .AndReturn(quota) api.keystone.get_default_role(IsA(http.HttpRequest)) \ - .AndReturn(default_role) + .MultipleTimes().AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \ .AndReturn(users) api.keystone.role_list(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(roles) + api.keystone.group_list(IsA(http.HttpRequest), domain=domain_id) \ + .AndReturn(groups) workflow_data = {} for user in users: @@ -763,6 +915,16 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): workflow_data.setdefault(USER_ROLE_PREFIX + role_ids[0], []) \ .append(user.id) + for group in groups: + api.keystone.roles_for_group(IsA(http.HttpRequest), + group=group.id, + project=self.tenant.id) \ + .AndReturn(roles) + role_ids = [role.id for role in roles] + if role_ids: + workflow_data.setdefault(GROUP_ROLE_PREFIX + role_ids[0], []) \ + .append(group.id) + # update some fields project._info["name"] = "updated name" project._info["description"] = "updated description" @@ -809,6 +971,10 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): 'remove_tenant_user_role', 'add_tenant_user_role', 'user_list', + 'roles_for_group', + 'remove_group_role', + 'add_group_role', + 'group_list', 'role_list'), quotas: ('get_tenant_quota_data',), api.nova: ('tenant_quota_update',)}) @@ -819,6 +985,8 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): domain_id = self._get_domain_id() users = self._get_all_users(domain_id) proj_users = self._get_proj_users(project.id) + groups = self._get_all_groups(domain_id) + proj_groups = self._get_proj_groups(project.id) roles = self.roles.list() # get/init @@ -830,11 +998,13 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): .AndReturn(quota) api.keystone.get_default_role(IsA(http.HttpRequest)) \ - .AndReturn(default_role) + .MultipleTimes().AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \ .AndReturn(users) api.keystone.role_list(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(roles) + api.keystone.group_list(IsA(http.HttpRequest), domain=domain_id) \ + .AndReturn(groups) workflow_data = {} @@ -843,8 +1013,17 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): user.id, self.tenant.id).AndReturn(roles) + for group in groups: + api.keystone.roles_for_group(IsA(http.HttpRequest), + group=group.id, + project=self.tenant.id) \ + .AndReturn(roles) + workflow_data[USER_ROLE_PREFIX + "1"] = ['1', '3'] # admin role workflow_data[USER_ROLE_PREFIX + "2"] = ['1', '2', '3'] # member role + # Group role assignment data + workflow_data[GROUP_ROLE_PREFIX + "1"] = ['1', '3'] # admin role + workflow_data[GROUP_ROLE_PREFIX + "2"] = ['1', '2', '3'] # member role # update some fields project._info["name"] = "updated name" @@ -886,6 +1065,35 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): user='3', role='2') + # Group assignment + api.keystone.group_list(IsA(http.HttpRequest), + domain=domain_id, + project=self.tenant.id).AndReturn(proj_groups) + + # admin group 1- try to remove all roles on current project + api.keystone.roles_for_group(IsA(http.HttpRequest), + group='1', + project=self.tenant.id) \ + .AndReturn(roles) + + # member group 1 - has no change + api.keystone.roles_for_group(IsA(http.HttpRequest), + group='2', + project=self.tenant.id) \ + .AndReturn((roles[1],)) + + # member group 3 - has role 1 + api.keystone.roles_for_group(IsA(http.HttpRequest), + group='3', + project=self.tenant.id) \ + .AndReturn((roles[0],)) + + # add role 2 + api.keystone.add_group_role(IsA(http.HttpRequest), + role='2', + group='3', + project=self.tenant.id) + nova_updated_quota = dict([(key, updated_quota[key]) for key in quotas.NOVA_QUOTA_FIELDS]) api.nova.tenant_quota_update(IsA(http.HttpRequest), @@ -923,6 +1131,10 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): 'remove_tenant_user_role', 'add_tenant_user_role', 'user_list', + 'roles_for_group', + 'remove_group_role', + 'add_group_role', + 'group_list', 'role_list'), quotas: ('get_tenant_quota_data',)}) def test_update_project_member_update_error(self): @@ -932,6 +1144,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): domain_id = self._get_domain_id() users = self._get_all_users(domain_id) proj_users = self._get_proj_users(project.id) + groups = self._get_all_groups(domain_id) roles = self.roles.list() # get/init @@ -948,16 +1161,25 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): .AndReturn(users) api.keystone.role_list(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(roles) + api.keystone.group_list(IsA(http.HttpRequest), domain=domain_id) \ + .AndReturn(groups) workflow_data = {} for user in users: api.keystone.roles_for_user(IsA(http.HttpRequest), user.id, self.tenant.id).AndReturn(roles) + for group in groups: + api.keystone.roles_for_group(IsA(http.HttpRequest), + group=group.id, + project=self.tenant.id) \ + .AndReturn(roles) workflow_data[USER_ROLE_PREFIX + "1"] = ['1', '3'] # admin role workflow_data[USER_ROLE_PREFIX + "2"] = ['1', '2', '3'] # member role + workflow_data[GROUP_ROLE_PREFIX + "1"] = ['1', '3'] # admin role + workflow_data[GROUP_ROLE_PREFIX + "2"] = ['1', '2', '3'] # member role # update some fields project._info["name"] = "updated name" project._info["description"] = "updated description" diff --git a/openstack_dashboard/dashboards/admin/projects/workflows.py b/openstack_dashboard/dashboards/admin/projects/workflows.py index 5ee4fc1dbf..4dbede1fda 100644 --- a/openstack_dashboard/dashboards/admin/projects/workflows.py +++ b/openstack_dashboard/dashboards/admin/projects/workflows.py @@ -31,6 +31,7 @@ from horizon import workflows from openstack_dashboard import api from openstack_dashboard.api.base import is_service_enabled from openstack_dashboard.api import cinder +from openstack_dashboard.api.keystone import VERSIONS as IDENTITY_VERSIONS from openstack_dashboard.api import nova from openstack_dashboard.usage.quotas import CINDER_QUOTA_FIELDS from openstack_dashboard.usage.quotas import get_disabled_quotas @@ -39,7 +40,9 @@ from openstack_dashboard.usage.quotas import QUOTA_FIELDS INDEX_URL = "horizon:admin:projects:index" ADD_USER_URL = "horizon:admin:projects:create_user" +PROJECT_GROUP_ENABLED = IDENTITY_VERSIONS.active >= 3 PROJECT_USER_MEMBER_SLUG = "update_members" +PROJECT_GROUP_MEMBER_SLUG = "update_group_members" class UpdateProjectQuotaAction(workflows.Action): @@ -205,6 +208,102 @@ class UpdateProjectMembers(workflows.UpdateMembersStep): return context +class UpdateProjectGroupsAction(workflows.MembershipAction): + def __init__(self, request, *args, **kwargs): + super(UpdateProjectGroupsAction, self).__init__(request, + *args, + **kwargs) + err_msg = _('Unable to retrieve group list. Please try again later.') + project_id = '' + if 'project_id' in args[0]: + project_id = args[0]['project_id'] + + # Get the default role + try: + default_role = api.keystone.get_default_role(self.request) + # Default role is necessary to add members to a project + if default_role is None: + default = getattr(settings, + "OPENSTACK_KEYSTONE_DEFAULT_ROLE", None) + msg = _('Could not find default role "%s" in Keystone') % \ + default + raise exceptions.NotFound(msg) + except Exception: + exceptions.handle(self.request, + err_msg, + redirect=reverse(INDEX_URL)) + default_role_name = self.get_default_role_field_name() + self.fields[default_role_name] = forms.CharField(required=False) + self.fields[default_role_name].initial = default_role.id + + # Get list of available groups + all_groups = [] + domain_context = request.session.get('domain_context', None) + try: + all_groups = api.keystone.group_list(request, + domain=domain_context) + except Exception: + exceptions.handle(request, err_msg) + groups_list = [(group.id, group.name) for group in all_groups] + + # Get list of roles + role_list = [] + try: + role_list = api.keystone.role_list(request) + except Exception: + exceptions.handle(request, + err_msg, + redirect=reverse(INDEX_URL)) + for role in role_list: + field_name = self.get_member_field_name(role.id) + label = _(role.name) + self.fields[field_name] = forms.MultipleChoiceField(required=False, + label=label) + self.fields[field_name].choices = groups_list + self.fields[field_name].initial = [] + + # Figure out groups & roles + if project_id: + for group in all_groups: + try: + roles = api.keystone.roles_for_group(self.request, + group=group.id, + project=project_id) + except Exception: + exceptions.handle(request, + err_msg, + redirect=reverse(INDEX_URL)) + for role in roles: + field_name = self.get_member_field_name(role.id) + self.fields[field_name].initial.append(group.id) + + class Meta: + name = _("Project Groups") + slug = PROJECT_GROUP_MEMBER_SLUG + + +class UpdateProjectGroups(workflows.UpdateMembersStep): + action_class = UpdateProjectGroupsAction + available_list_title = _("All Groups") + members_list_title = _("Project Groups") + no_available_text = _("No groups found.") + no_members_text = _("No groups.") + + def contribute(self, data, context): + if data: + try: + roles = api.keystone.role_list(self.workflow.request) + except Exception: + exceptions.handle(self.workflow.request, + _('Unable to retrieve role list.')) + + post = self.workflow.request.POST + for role in roles: + field = self.get_member_field_name(role.id) + context[field] = post.getlist(field) + return context + + class CreateProject(workflows.Workflow): slug = "create_project" name = _("Create Project") @@ -218,6 +317,11 @@ class CreateProject(workflows.Workflow): def __init__(self, request=None, context_seed=None, entry_point=None, *args, **kwargs): + if PROJECT_GROUP_ENABLED: + self.default_steps = (CreateProjectInfo, + UpdateProjectMembers, + UpdateProjectGroups, + UpdateProjectQuota) super(CreateProject, self).__init__(request=request, context_seed=context_seed, entry_point=entry_point, @@ -266,9 +370,44 @@ class CreateProject(workflows.Workflow): users_added += 1 users_to_add -= users_added except Exception: - exceptions.handle(request, _('Failed to add %s project members ' - 'and set project quotas.') - % users_to_add) + if PROJECT_GROUP_ENABLED: + group_msg = _(", add project groups") + else: + group_msg = "" + exceptions.handle(request, _('Failed to add %(users_to_add)s ' + 'project members%(group_msg)s and ' + 'set project quotas.') + % {'users_to_add': users_to_add, + 'group_msg': group_msg}) + + if PROJECT_GROUP_ENABLED: + # update project groups + groups_to_add = 0 + try: + available_roles = api.keystone.role_list(request) + member_step = self.get_step(PROJECT_GROUP_MEMBER_SLUG) + + # count how many groups are to be added + for role in available_roles: + field_name = member_step.get_member_field_name(role.id) + role_list = data[field_name] + groups_to_add += len(role_list) + # add new groups to project + for role in available_roles: + field_name = member_step.get_member_field_name(role.id) + role_list = data[field_name] + groups_added = 0 + for group in role_list: + api.keystone.add_group_role(request, + role=role.id, + group=group, + project=project_id) + groups_added += 1 + groups_to_add -= groups_added + except Exception: + exceptions.handle(request, _('Failed to add %s project groups ' + 'and update project quotas.' + % groups_to_add)) # Update the project quota. nova_data = dict([(key, data[key]) for key in NOVA_QUOTA_FIELDS]) @@ -316,6 +455,12 @@ class UpdateProject(workflows.Workflow): def __init__(self, request=None, context_seed=None, entry_point=None, *args, **kwargs): + if PROJECT_GROUP_ENABLED: + self.default_steps = (UpdateProjectInfo, + UpdateProjectMembers, + UpdateProjectGroups, + UpdateProjectQuota) + super(UpdateProject, self).__init__(request=request, context_seed=context_seed, entry_point=entry_point, @@ -330,6 +475,7 @@ class UpdateProject(workflows.Workflow): # sets and do this all in a single "roles to add" and "roles to remove" # pass instead of the multi-pass thing happening now. + domain_context = request.session.get('domain_context', None) project_id = data['project_id'] # update project info try: @@ -342,14 +488,13 @@ class UpdateProject(workflows.Workflow): exceptions.handle(request, ignore=True) return False - # Get our role options - available_roles = api.keystone.role_list(request) - # update project members users_to_modify = 0 # Project-user member step member_step = self.get_step(PROJECT_USER_MEMBER_SLUG) try: + # Get our role options + available_roles = api.keystone.role_list(request) # Get the users currently associated with this project so we # can diff against it. project_members = api.keystone.user_list(request, @@ -428,11 +573,88 @@ class UpdateProject(workflows.Workflow): users_added += 1 users_to_modify -= users_added except Exception: - exceptions.handle(request, _('Failed to modify %s project members ' - 'and update project quotas.') - % users_to_modify) + if PROJECT_GROUP_ENABLED: + group_msg = _(", update project groups") + else: + group_msg = "" + exceptions.handle(request, _('Failed to modify %(users_to_modify)s' + ' project members%(group_msg)s and ' + 'update project quotas.') + % {'users_to_modify': users_to_modify, + 'group_msg': group_msg}) return True + if PROJECT_GROUP_ENABLED: + # update project groups + groups_to_modify = 0 + member_step = self.get_step(PROJECT_GROUP_MEMBER_SLUG) + try: + # Get the groups currently associated with this project so we + # can diff against it. + project_groups = api.keystone.group_list(request, + domain=domain_context, + project=project_id) + groups_to_modify = len(project_groups) + for group in project_groups: + # Check if there have been any changes in the roles of + # Existing project members. + current_roles = api.keystone.roles_for_group( + self.request, + group=group.id, + project=project_id) + current_role_ids = [role.id for role in current_roles] + for role in available_roles: + # Check if the group is in the list of groups with + # this role. + field_name = member_step.get_member_field_name(role.id) + if group.id in data[field_name]: + # Add it if necessary + if role.id not in current_role_ids: + # group role has changed + api.keystone.add_group_role( + request, + role=role.id, + group=group.id, + project=project_id) + else: + # Group role is unchanged, so remove it from + # the remaining roles list to avoid removing it + # later. + index = current_role_ids.index(role.id) + current_role_ids.pop(index) + + # Revoke any removed roles. + for id_to_delete in current_role_ids: + api.keystone.remove_group_role(request, + role=id_to_delete, + group=group.id, + project=project_id) + groups_to_modify -= 1 + + # Grant new roles on the project. + for role in available_roles: + field_name = member_step.get_member_field_name(role.id) + # Count how many groups may be added for error handling. + groups_to_modify += len(data[field_name]) + for role in available_roles: + groups_added = 0 + field_name = member_step.get_member_field_name(role.id) + for group_id in data[field_name]: + if not filter(lambda x: group_id == x.id, + project_groups): + api.keystone.add_group_role(request, + role=role.id, + group=group_id, + project=project_id) + groups_added += 1 + groups_to_modify -= groups_added + except Exception: + exceptions.handle(request, _('Failed to modify %s project ' + 'members, update project groups ' + 'and update project quotas.' + % groups_to_modify)) + return True + # update the project quota nova_data = dict([(key, data[key]) for key in NOVA_QUOTA_FIELDS]) try: diff --git a/openstack_dashboard/test/test_data/keystone_data.py b/openstack_dashboard/test/test_data/keystone_data.py index e92131b143..b891aca415 100644 --- a/openstack_dashboard/test/test_data/keystone_data.py +++ b/openstack_dashboard/test/test_data/keystone_data.py @@ -185,19 +185,28 @@ def data(TEST): group_dict = {'id': "1", 'name': 'group_one', 'description': 'group one description', + 'project_id': '1', 'domain_id': '1'} group = groups.Group(groups.GroupManager(None), group_dict) group_dict = {'id': "2", 'name': 'group_two', 'description': 'group two description', + 'project_id': '1', 'domain_id': '1'} group2 = groups.Group(groups.GroupManager(None), group_dict) group_dict = {'id': "3", 'name': 'group_three', 'description': 'group three description', - 'domain_id': '2'} + 'project_id': '1', + 'domain_id': '1'} group3 = groups.Group(groups.GroupManager(None), group_dict) - TEST.groups.add(group, group2, group3) + group_dict = {'id': "4", + 'name': 'group_four', + 'description': 'group four description', + 'project_id': '2', + 'domain_id': '2'} + group4 = groups.Group(groups.GroupManager(None), group_dict) + TEST.groups.add(group, group2, group3, group4) tenant_dict = {'id': "1", 'name': 'test_tenant',