From 2b846515f388278e2bf8d0198a4f821309e08e69 Mon Sep 17 00:00:00 2001 From: daniel-a-nguyen Date: Sat, 17 Jan 2015 19:19:37 -0800 Subject: [PATCH] Retrieve domain scoped token This patch supports using domain scoped tokens against keystone v3. Use Cases: Cloud Admin - view and manage identity resources across domains Domain Admin - view and manage identity resources in the domain logged in User - view identity project in the domain logged in Regression: Supports keystone v2 through local_settings.py configuration Supports keystone v3 with multidomain = False Supports keystone v3 with mulitdomain = True Relates to https://review.openstack.org/#/c/141153/ Background on how to test is here https://wiki.openstack.org/wiki/Horizon/DomainWorkFlow Co-Authored-By: Brad Pokorny Co-Authored-By: Brian Tully Co-Authored-By: Michael Hagedorn Co-Authored-By: woomatt Partially Implements: blueprint domain-scoped-tokens Closes-Bug: #1413851 Change-Id: Iaa19bfef9b0c70304ff81d083c62b218b2d02479 --- openstack_dashboard/api/keystone.py | 122 +++++++-- .../dashboards/admin/dashboard.py | 2 +- .../dashboards/identity/domains/panel.py | 8 + .../dashboards/identity/domains/tables.py | 9 +- .../dashboards/identity/domains/tests.py | 27 +- .../dashboards/identity/domains/views.py | 11 +- .../dashboards/identity/domains/workflows.py | 10 +- .../dashboards/identity/groups/forms.py | 2 +- .../dashboards/identity/groups/panel.py | 6 + .../dashboards/identity/groups/tables.py | 5 +- .../dashboards/identity/groups/tests.py | 74 ++++-- .../dashboards/identity/groups/views.py | 9 +- .../dashboards/identity/projects/tables.py | 55 ++++- .../dashboards/identity/projects/tests.py | 175 +++++++------ .../dashboards/identity/projects/views.py | 97 +++++--- .../dashboards/identity/projects/workflows.py | 91 ++++++- .../dashboards/identity/roles/panel.py | 6 + .../dashboards/identity/users/forms.py | 33 ++- .../dashboards/identity/users/panel.py | 8 + .../dashboards/identity/users/tables.py | 20 +- .../dashboards/identity/users/tests.py | 232 +++++++++++++++--- .../dashboards/identity/users/views.py | 32 ++- .../dashboards/project/dashboard.py | 5 + .../local/local_settings.py.example | 6 +- openstack_dashboard/policy.py | 5 +- openstack_dashboard/views.py | 5 + .../notes/domains-0581aa42773d5f41.yaml | 22 ++ 27 files changed, 826 insertions(+), 251 deletions(-) create mode 100644 releasenotes/notes/domains-0581aa42773d5f41.yaml diff --git a/openstack_dashboard/api/keystone.py b/openstack_dashboard/api/keystone.py index 5a3e61a753..743dfd752f 100644 --- a/openstack_dashboard/api/keystone.py +++ b/openstack_dashboard/api/keystone.py @@ -40,6 +40,8 @@ from openstack_dashboard import policy LOG = logging.getLogger(__name__) DEFAULT_ROLE = None +DEFAULT_DOMAIN = getattr(settings, 'OPENSTACK_KEYSTONE_DEFAULT_DOMAIN', + 'default') # Set up our data structure for managing Identity API versions, and @@ -144,7 +146,17 @@ def keystoneclient(request, admin=False): The client is cached so that subsequent API calls during the same request/response cycle don't have to be re-authenticated. """ + api_version = VERSIONS.get_active_version() user = request.user + token_id = user.token.id + + if is_multi_domain_enabled: + # Cloud Admin, Domain Admin or Mixed Domain Admin + if is_domain_admin(request): + domain_token = request.session.get('domain_token') + if domain_token: + token_id = getattr(domain_token, 'auth_token', None) + if admin: if not policy.check((("identity", "admin_required"),), request): raise exceptions.NotAuthorized @@ -154,8 +166,6 @@ def keystoneclient(request, admin=False): 'OPENSTACK_ENDPOINT_TYPE', 'internalURL') - api_version = VERSIONS.get_active_version() - # Take care of client connection caching/fetching a new client. # Admin vs. non-admin clients are cached separately for token matching. cache_attr = "_keystoneclient_admin" if admin \ @@ -170,7 +180,7 @@ def keystoneclient(request, admin=False): cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None) LOG.debug("Creating a new keystoneclient connection to %s." % endpoint) remote_addr = request.environ.get('REMOTE_ADDR', '') - conn = api_version['client'].Client(token=user.token.id, + conn = api_version['client'].Client(token=token_id, endpoint=endpoint, original_ip=remote_addr, insecure=insecure, @@ -183,7 +193,7 @@ def keystoneclient(request, admin=False): def domain_create(request, name, description=None, enabled=None): manager = keystoneclient(request, admin=True).domains - return manager.create(name, + return manager.create(name=name, description=description, enabled=enabled) @@ -203,10 +213,29 @@ def domain_list(request): return manager.list() +def domain_lookup(request): + if policy.check((("identity", "identity:list_domains"),), request): + try: + domains = domain_list(request) + return dict((d.id, d.name) for d in domains) + except Exception: + LOG.warn("Pure project admin doesn't have a domain token") + return None + else: + domain = get_default_domain(request) + return {domain.id: domain.name} + + def domain_update(request, domain_id, name=None, description=None, enabled=None): manager = keystoneclient(request, admin=True).domains - return manager.update(domain_id, name, description, enabled) + try: + response = manager.update(domain_id, name=name, + description=description, enabled=enabled) + except Exception as e: + LOG.exception("Unable to update Domain: %s" % domain_id) + raise e + return response def tenant_create(request, name, description=None, enabled=None, @@ -223,28 +252,52 @@ def tenant_create(request, name, description=None, enabled=None, raise exceptions.Conflict() -def get_default_domain(request): +def get_default_domain(request, get_name=True): """Gets the default domain object to use when creating Identity object. Returns the domain context if is set, otherwise return the domain of the logon user. + + :param get_name: Whether to get the domain name from Keystone if the + context isn't set. Setting this to False prevents an unnecessary call + to Keystone if only the domain ID is needed. """ domain_id = request.session.get("domain_context", None) domain_name = request.session.get("domain_context_name", None) # if running in Keystone V3 or later - if VERSIONS.active >= 3 and not domain_id: - # if no domain context set, default to users' domain + if VERSIONS.active >= 3 and domain_id is None: + # if no domain context set, default to user's domain domain_id = request.user.user_domain_id - try: - domain = domain_get(request, domain_id) - domain_name = domain.name - except Exception: - LOG.warning("Unable to retrieve Domain: %s" % domain_id) + domain_name = request.user.user_domain_name + if get_name: + try: + domain = domain_get(request, domain_id) + domain_name = domain.name + except Exception: + LOG.warning("Unable to retrieve Domain: %s" % domain_id) domain = base.APIDictWrapper({"id": domain_id, "name": domain_name}) return domain +def get_effective_domain_id(request): + """Gets the id of the default domain to use when creating Identity objects. + If the requests default domain is the same as DEFAULT_DOMAIN, return None. + """ + domain_id = get_default_domain(request).get('id') + return None if domain_id == DEFAULT_DOMAIN else domain_id + + +def is_cloud_admin(request): + return policy.check((("identity", "cloud_admin"),), request) + + +def is_domain_admin(request): + # TODO(btully): check this to verify that domain id is in scope vs target + return policy.check( + (("identity", "admin_and_matching_domain_id"),), request) + + # TODO(gabriel): Is there ever a valid case for admin to be false here? # A quick search through the codebase reveals that it's always called with # admin=true so I suspect we could eliminate it entirely as with the other @@ -280,15 +333,17 @@ def tenant_list(request, paginate=False, marker=None, domain=None, user=None, if paginate and len(tenants) > page_size: tenants.pop(-1) has_more_data = True + # V3 API else: + domain_id = get_effective_domain_id(request) kwargs = { - "domain": domain, + "domain": domain_id, "user": user } if filters is not None: kwargs.update(filters) tenants = manager.list(**kwargs) - return (tenants, has_more_data) + return tenants, has_more_data def tenant_update(request, project, name=None, description=None, @@ -514,23 +569,34 @@ def get_project_groups_roles(request, project): groups_roles = collections.defaultdict(list) project_role_assignments = role_assignments_list(request, project=project) + for role_assignment in project_role_assignments: if not hasattr(role_assignment, 'group'): continue group_id = role_assignment.group['id'] role_id = role_assignment.role['id'] - groups_roles[group_id].append(role_id) + + # filter by project_id + if ('project' in role_assignment.scope and + role_assignment.scope['project']['id'] == project): + groups_roles[group_id].append(role_id) return groups_roles def role_assignments_list(request, project=None, user=None, role=None, - group=None, domain=None, effective=False): + group=None, domain=None, effective=False, + include_subtree=True): if VERSIONS.active < 3: raise exceptions.NotAvailable + if include_subtree: + domain = None + manager = keystoneclient(request, admin=True).role_assignments + return manager.list(project=project, user=user, role=role, group=group, - domain=domain, effective=effective) + domain=domain, effective=effective, + include_subtree=include_subtree) def role_create(request, name): @@ -570,13 +636,18 @@ def roles_for_user(request, user, project=None, domain=None): def get_domain_users_roles(request, domain): users_roles = collections.defaultdict(list) domain_role_assignments = role_assignments_list(request, - domain=domain) + domain=domain, + include_subtree=False) for role_assignment in domain_role_assignments: if not hasattr(role_assignment, 'user'): continue user_id = role_assignment.user['id'] role_id = role_assignment.role['id'] - users_roles[user_id].append(role_id) + + # filter by domain_id + if ('domain' in role_assignment.scope and + role_assignment.scope['domain']['id'] == domain): + users_roles[user_id].append(role_id) return users_roles @@ -609,7 +680,11 @@ def get_project_users_roles(request, project): continue user_id = role_assignment.user['id'] role_id = role_assignment.role['id'] - users_roles[user_id].append(role_id) + + # filter by project_id + if ('project' in role_assignment.scope and + role_assignment.scope['project']['id'] == project): + users_roles[user_id].append(role_id) return users_roles @@ -755,6 +830,11 @@ def get_version(): return VERSIONS.active +def is_multi_domain_enabled(): + return (VERSIONS.active >= 3 and + getattr(settings, 'OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT', False)) + + def is_federation_management_enabled(): return getattr(settings, 'OPENSTACK_KEYSTONE_FEDERATION_MANAGEMENT', False) diff --git a/openstack_dashboard/dashboards/admin/dashboard.py b/openstack_dashboard/dashboards/admin/dashboard.py index c8cdc63db9..667b364d0c 100644 --- a/openstack_dashboard/dashboards/admin/dashboard.py +++ b/openstack_dashboard/dashboards/admin/dashboard.py @@ -21,6 +21,6 @@ class Admin(horizon.Dashboard): name = _("Admin") slug = "admin" permissions = ('openstack.roles.admin',) - + policy_rules = (("identity", "cloud_admin"),) horizon.register(Admin) diff --git a/openstack_dashboard/dashboards/identity/domains/panel.py b/openstack_dashboard/dashboards/identity/domains/panel.py index ee8705eb3a..7974e36e44 100644 --- a/openstack_dashboard/dashboards/identity/domains/panel.py +++ b/openstack_dashboard/dashboards/identity/domains/panel.py @@ -28,3 +28,11 @@ class Domains(horizon.Panel): @staticmethod def can_register(): return keystone.VERSIONS.active >= 3 + + def can_access(self, context): + if keystone.VERSIONS.active < 3: + return super(Domains, self).can_access(context) + + request = context['request'] + domain_token = request.session.get('domain_token') + return super(Domains, self).can_access(context) and domain_token diff --git a/openstack_dashboard/dashboards/identity/domains/tables.py b/openstack_dashboard/dashboards/identity/domains/tables.py index 0b53234360..914985e6ba 100644 --- a/openstack_dashboard/dashboards/identity/domains/tables.py +++ b/openstack_dashboard/dashboards/identity/domains/tables.py @@ -39,9 +39,7 @@ class UpdateUsersLink(tables.LinkAction): verbose_name = _("Manage Members") url = "horizon:identity:domains:update" classes = ("ajax-modal",) - policy_rules = (("identity", "identity:list_users"), - ("identity", "identity:list_roles"), - ("identity", "identity:list_role_assignments")) + policy_rules = (("identity", "identity:update_domain"),) def get_link_url(self, domain): step = 'update_user_members' @@ -56,6 +54,7 @@ class UpdateGroupsLink(tables.LinkAction): url = "horizon:identity:domains:update" classes = ("ajax-modal",) icon = "pencil" + policy_rules = (("identity", "identity:update_domain"),) def get_link_url(self, domain): step = 'update_group_members' @@ -227,7 +226,7 @@ class SetDomainContext(tables.Action): verbose_name = _("Set Domain Context") url = constants.DOMAINS_INDEX_URL preempt = True - policy_rules = (('identity', 'admin_required'),) + policy_rules = (('identity', 'identity:update_domain'),) def allowed(self, request, datum): multidomain_support = getattr(settings, @@ -262,7 +261,7 @@ class UnsetDomainContext(tables.Action): url = constants.DOMAINS_INDEX_URL preempt = True requires_input = False - policy_rules = (('identity', 'admin_required'),) + policy_rules = (('identity', 'identity:update_domain'),) def allowed(self, request, datum): ctx = request.session.get("domain_context", None) diff --git a/openstack_dashboard/dashboards/identity/domains/tests.py b/openstack_dashboard/dashboards/identity/domains/tests.py index b88713699c..5886c11b0a 100644 --- a/openstack_dashboard/dashboards/identity/domains/tests.py +++ b/openstack_dashboard/dashboards/identity/domains/tests.py @@ -269,7 +269,8 @@ class UpdateDomainWorkflowTests(test.BaseAdminViewTests): api.keystone.user_list(IsA(http.HttpRequest), domain=domain.id) \ .AndReturn(users) api.keystone.role_assignments_list(IsA(http.HttpRequest), - domain=domain.id) \ + domain=domain.id, + include_subtree=False) \ .AndReturn(role_assignments) api.keystone.group_list(IsA(http.HttpRequest), domain=domain.id) \ .AndReturn(groups) @@ -331,7 +332,8 @@ class UpdateDomainWorkflowTests(test.BaseAdminViewTests): api.keystone.user_list(IsA(http.HttpRequest), domain=domain.id) \ .AndReturn(users) api.keystone.role_assignments_list(IsA(http.HttpRequest), - domain=domain.id) \ + domain=domain.id, + include_subtree=False) \ .AndReturn(role_assignments) api.keystone.group_list(IsA(http.HttpRequest), domain=domain.id) \ .AndReturn(groups) @@ -354,15 +356,19 @@ class UpdateDomainWorkflowTests(test.BaseAdminViewTests): # handle api.keystone.domain_update(IsA(http.HttpRequest), + domain.id, + name=domain.name, description=test_description, - domain_id=domain.id, - enabled=domain.enabled, - name=domain.name).AndReturn(None) + enabled=domain.enabled).AndReturn(None) api.keystone.role_assignments_list(IsA(http.HttpRequest), - domain=domain.id) \ + domain=domain.id, + include_subtree=False) \ .AndReturn(role_assignments) + api.keystone.user_list(IsA(http.HttpRequest), + domain=domain.id).AndReturn(users) + # Give user 3 role 1 api.keystone.add_domain_user_role(IsA(http.HttpRequest), domain=domain.id, @@ -468,7 +474,8 @@ class UpdateDomainWorkflowTests(test.BaseAdminViewTests): api.keystone.user_list(IsA(http.HttpRequest), domain=domain.id) \ .AndReturn(users) api.keystone.role_assignments_list(IsA(http.HttpRequest), - domain=domain.id) \ + domain=domain.id, + include_subtree=False) \ .AndReturn(role_assignments) api.keystone.group_list(IsA(http.HttpRequest), domain=domain.id) \ .AndReturn(groups) @@ -492,10 +499,10 @@ class UpdateDomainWorkflowTests(test.BaseAdminViewTests): # handle api.keystone.domain_update(IsA(http.HttpRequest), + domain.id, + name=domain.name, description=test_description, - domain_id=domain.id, - enabled=domain.enabled, - name=domain.name) \ + enabled=domain.enabled) \ .AndRaise(self.exceptions.keystone) self.mox.ReplayAll() diff --git a/openstack_dashboard/dashboards/identity/domains/views.py b/openstack_dashboard/dashboards/identity/domains/views.py index 1c95d6d070..16ddec804c 100644 --- a/openstack_dashboard/dashboards/identity/domains/views.py +++ b/openstack_dashboard/dashboards/identity/domains/views.py @@ -37,13 +37,13 @@ class IndexView(tables.DataTableView): def get_data(self): domains = [] - domain_context = self.request.session.get('domain_context', None) + domain_id = api.keystone.get_effective_domain_id(self.request) + if policy.check((("identity", "identity:list_domains"),), self.request): try: - if domain_context: - domain = api.keystone.domain_get(self.request, - domain_context) + if domain_id: + domain = api.keystone.domain_get(self.request, domain_id) domains.append(domain) else: domains = api.keystone.domain_list(self.request) @@ -53,8 +53,7 @@ class IndexView(tables.DataTableView): elif policy.check((("identity", "identity:get_domain"),), self.request): try: - domain = api.keystone.domain_get(self.request, - self.request.user.domain_id) + domain = api.keystone.domain_get(self.request, domain_id) domains.append(domain) except Exception: exceptions.handle(self.request, diff --git a/openstack_dashboard/dashboards/identity/domains/workflows.py b/openstack_dashboard/dashboards/identity/domains/workflows.py index 666eb13fd6..230ede4243 100644 --- a/openstack_dashboard/dashboards/identity/domains/workflows.py +++ b/openstack_dashboard/dashboards/identity/domains/workflows.py @@ -322,8 +322,16 @@ class UpdateDomain(workflows.Workflow, IdentityMixIn): users_roles = api.keystone.get_domain_users_roles(request, domain=domain_id) users_to_modify = len(users_roles) + all_users = api.keystone.user_list(request, + domain=domain_id) + users_dict = {user.id: user.name for user in all_users} for user_id in users_roles.keys(): + # Don't remove roles if the user isn't in the domain + if user_id not in users_dict: + users_to_modify -= 1 + continue + # Check if there have been any changes in the roles of # Existing domain members. current_role_ids = list(users_roles[user_id]) @@ -484,7 +492,7 @@ class UpdateDomain(workflows.Workflow, IdentityMixIn): try: LOG.info('Updating domain with name "%s"' % data['name']) api.keystone.domain_update(request, - domain_id=domain_id, + domain_id, name=data['name'], description=data['description'], enabled=data['enabled']) diff --git a/openstack_dashboard/dashboards/identity/groups/forms.py b/openstack_dashboard/dashboards/identity/groups/forms.py index 88ad989797..e0041bee5e 100644 --- a/openstack_dashboard/dashboards/identity/groups/forms.py +++ b/openstack_dashboard/dashboards/identity/groups/forms.py @@ -36,7 +36,7 @@ class CreateGroupForm(forms.SelfHandlingForm): def handle(self, request, data): try: LOG.info('Creating group with name "%s"' % data['name']) - domain_context = request.session.get('domain_context', None) + domain_context = api.keystone.get_effective_domain_id(request) api.keystone.group_create( request, domain_id=domain_context, diff --git a/openstack_dashboard/dashboards/identity/groups/panel.py b/openstack_dashboard/dashboards/identity/groups/panel.py index 458dcb1146..bf540f227b 100644 --- a/openstack_dashboard/dashboards/identity/groups/panel.py +++ b/openstack_dashboard/dashboards/identity/groups/panel.py @@ -27,3 +27,9 @@ class Groups(horizon.Panel): @staticmethod def can_register(): return keystone.VERSIONS.active >= 3 + + def can_access(self, context): + if keystone.is_multi_domain_enabled() \ + and not keystone.is_domain_admin(context['request']): + return False + return super(Groups, self).can_access(context) diff --git a/openstack_dashboard/dashboards/identity/groups/tables.py b/openstack_dashboard/dashboards/identity/groups/tables.py index c604c6946e..05de7ec1dd 100644 --- a/openstack_dashboard/dashboards/identity/groups/tables.py +++ b/openstack_dashboard/dashboards/identity/groups/tables.py @@ -24,6 +24,7 @@ from horizon import tables from openstack_dashboard import api from openstack_dashboard.dashboards.identity.groups import constants +from openstack_dashboard import policy LOG = logging.getLogger(__name__) @@ -46,7 +47,7 @@ class CreateGroupLink(tables.LinkAction): return api.keystone.keystone_can_edit_group() -class EditGroupLink(tables.LinkAction): +class EditGroupLink(policy.PolicyTargetMixin, tables.LinkAction): name = "edit" verbose_name = _("Edit Group") url = constants.GROUPS_UPDATE_URL @@ -58,7 +59,7 @@ class EditGroupLink(tables.LinkAction): return api.keystone.keystone_can_edit_group() -class DeleteGroupsAction(tables.DeleteAction): +class DeleteGroupsAction(policy.PolicyTargetMixin, tables.DeleteAction): @staticmethod def action_present(count): return ungettext_lazy( diff --git a/openstack_dashboard/dashboards/identity/groups/tests.py b/openstack_dashboard/dashboards/identity/groups/tests.py index 2651d6bde6..6711b02117 100644 --- a/openstack_dashboard/dashboards/identity/groups/tests.py +++ b/openstack_dashboard/dashboards/identity/groups/tests.py @@ -65,11 +65,33 @@ class GroupsViewTests(test.BaseAdminViewTests): self.assertContains(res, 'Edit') self.assertContains(res, 'Delete Group') + @test.create_stubs({api.keystone: ('group_list', + 'get_effective_domain_id')}) def test_index_with_domain(self): domain = self.domains.get(id="1") + self.setSessionValues(domain_context=domain.id, domain_context_name=domain.name) - self.test_index() + groups = self._get_groups(domain.id) + + api.keystone.get_effective_domain_id(IgnoreArg()).AndReturn(domain.id) + + api.keystone.group_list(IsA(http.HttpRequest), + domain=domain.id).AndReturn(groups) + + self.mox.ReplayAll() + + res = self.client.get(GROUPS_INDEX_URL) + + self.assertTemplateUsed(res, constants.GROUPS_INDEX_VIEW_TEMPLATE) + self.assertItemsEqual(res.context['table'].data, groups) + if domain.id: + for group in res.context['table'].data: + self.assertItemsEqual(group.domain_id, domain.id) + + self.assertContains(res, 'Create Group') + self.assertContains(res, 'Edit') + self.assertContains(res, 'Delete Group') @test.create_stubs({api.keystone: ('group_list', 'keystone_can_edit_group')}) @@ -159,17 +181,27 @@ class GroupsViewTests(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, GROUPS_INDEX_URL) - @test.create_stubs({api.keystone: ('group_get', + @test.create_stubs({api.keystone: ('get_effective_domain_id', + 'group_get', 'user_list',)}) def test_manage(self): group = self.groups.get(id="1") group_members = self.users.list() + domain_id = self._get_domain_id() api.keystone.group_get(IsA(http.HttpRequest), group.id).\ AndReturn(group) - api.keystone.user_list(IgnoreArg(), - group=group.id).\ - AndReturn(group_members) + + if api.keystone.VERSIONS.active >= 3: + api.keystone.get_effective_domain_id( + IgnoreArg()).AndReturn(domain_id) + api.keystone.user_list( + IgnoreArg(), group=group.id, domain=domain_id).AndReturn( + group_members) + + else: + api.keystone.user_list( + IgnoreArg(), group=group.id).AndReturn(group_members) self.mox.ReplayAll() res = self.client.get(GROUP_MANAGE_URL) @@ -177,15 +209,25 @@ class GroupsViewTests(test.BaseAdminViewTests): self.assertTemplateUsed(res, constants.GROUPS_MANAGE_VIEW_TEMPLATE) self.assertItemsEqual(res.context['table'].data, group_members) - @test.create_stubs({api.keystone: ('user_list', + @test.create_stubs({api.keystone: ('get_effective_domain_id', + 'user_list', 'remove_group_user')}) def test_remove_user(self): group = self.groups.get(id="1") user = self.users.get(id="2") + domain_id = self._get_domain_id() + + if api.keystone.VERSIONS.active >= 3: + api.keystone.get_effective_domain_id( + IgnoreArg()).AndReturn(domain_id) + + api.keystone.user_list( + IgnoreArg(), group=group.id, domain=domain_id).AndReturn( + self.users.list()) + else: + api.keystone.user_list( + IgnoreArg(), group=group.id).AndReturn(self.users.list()) - api.keystone.user_list(IgnoreArg(), - group=group.id).\ - AndReturn(self.users.list()) api.keystone.remove_group_user(IgnoreArg(), group_id=group.id, user_id=user.id) @@ -197,20 +239,24 @@ class GroupsViewTests(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, GROUP_MANAGE_URL) self.assertMessageCount(success=1) - @test.create_stubs({api.keystone: ('group_get', + @test.create_stubs({api.keystone: ('get_effective_domain_id', + 'group_get', 'user_list', 'add_group_user')}) def test_add_user(self): group = self.groups.get(id="1") user = self.users.get(id="2") + domain_id = group.domain_id + + api.keystone.get_effective_domain_id(IgnoreArg()).AndReturn(domain_id) api.keystone.group_get(IsA(http.HttpRequest), group.id).\ AndReturn(group) - api.keystone.user_list(IgnoreArg(), - domain=group.domain_id).\ + + api.keystone.user_list(IgnoreArg(), domain=domain_id).\ AndReturn(self.users.list()) - api.keystone.user_list(IgnoreArg(), - group=group.id).\ + + api.keystone.user_list(IgnoreArg(), domain=domain_id, group=group.id).\ AndReturn(self.users.list()[2:]) api.keystone.add_group_user(IgnoreArg(), diff --git a/openstack_dashboard/dashboards/identity/groups/views.py b/openstack_dashboard/dashboards/identity/groups/views.py index b312470be0..bc59751a24 100644 --- a/openstack_dashboard/dashboards/identity/groups/views.py +++ b/openstack_dashboard/dashboards/identity/groups/views.py @@ -39,12 +39,13 @@ class IndexView(tables.DataTableView): def get_data(self): groups = [] - domain_context = self.request.session.get('domain_context', None) + domain_id = api.keystone.get_effective_domain_id(self.request) + if policy.check((("identity", "identity:list_groups"),), self.request): try: groups = api.keystone.group_list(self.request, - domain=domain_context) + domain=domain_id) except Exception: exceptions.handle(self.request, _('Unable to retrieve group list.')) @@ -108,7 +109,9 @@ class GroupManageMixin(object): @memoized.memoized_method def _get_group_members(self): group_id = self.kwargs['group_id'] - return api.keystone.user_list(self.request, group=group_id) + domain_id = api.keystone.get_effective_domain_id(self.request) + return api.keystone.user_list(self.request, domain=domain_id, + group=group_id) @memoized.memoized_method def _get_group_non_members(self): diff --git a/openstack_dashboard/dashboards/identity/projects/tables.py b/openstack_dashboard/dashboards/identity/projects/tables.py index 6ccc249dff..73757400d0 100644 --- a/openstack_dashboard/dashboards/identity/projects/tables.py +++ b/openstack_dashboard/dashboards/identity/projects/tables.py @@ -62,6 +62,14 @@ class UpdateMembersLink(tables.LinkAction): param = urlencode({"step": step}) return "?".join([base_url, param]) + def allowed(self, request, project): + if api.keystone.is_multi_domain_enabled(): + # domain admin or cloud admin = True + # project admin or member = False + return api.keystone.is_domain_admin(request) + else: + return super(UpdateMembersLink, self).allowed(request, project) + class UpdateGroupsLink(tables.LinkAction): name = "groups" @@ -72,7 +80,12 @@ class UpdateGroupsLink(tables.LinkAction): policy_rules = (("identity", "identity:list_groups"),) def allowed(self, request, project): - return api.keystone.VERSIONS.active >= 3 + if api.keystone.is_multi_domain_enabled(): + # domain admin or cloud admin = True + # project admin or member = False + return api.keystone.is_domain_admin(request) + else: + return super(UpdateGroupsLink, self).allowed(request, project) def get_link_url(self, project): step = 'update_group_members' @@ -101,19 +114,30 @@ class CreateProject(tables.LinkAction): policy_rules = (('identity', 'identity:create_project'),) def allowed(self, request, project): - return api.keystone.keystone_can_edit_project() + if api.keystone.is_multi_domain_enabled(): + # domain admin or cloud admin = True + # project admin or member = False + return api.keystone.is_domain_admin(request) + else: + return api.keystone.keystone_can_edit_project() -class UpdateProject(tables.LinkAction): +class UpdateProject(policy.PolicyTargetMixin, tables.LinkAction): name = "update" verbose_name = _("Edit Project") url = "horizon:identity:projects:update" classes = ("ajax-modal",) icon = "pencil" policy_rules = (('identity', 'identity:update_project'),) + policy_target_attrs = (("target.project.domain_id", "domain_id"),) def allowed(self, request, project): - return api.keystone.keystone_can_edit_project() + if api.keystone.is_multi_domain_enabled(): + # domain admin or cloud admin = True + # project admin or member = False + return api.keystone.is_domain_admin(request) + else: + return api.keystone.keystone_can_edit_project() class ModifyQuotas(tables.LinkAction): @@ -124,6 +148,12 @@ class ModifyQuotas(tables.LinkAction): icon = "pencil" policy_rules = (('compute', "compute_extension:quotas:update"),) + def allowed(self, request, datum): + if api.keystone.VERSIONS.active < 3: + return True + else: + return api.keystone.is_cloud_admin(request) + def get_link_url(self, project): step = 'update_quotas' base_url = reverse(self.url, args=[project.id]) @@ -131,7 +161,7 @@ class ModifyQuotas(tables.LinkAction): return "?".join([base_url, param]) -class DeleteTenantsAction(tables.DeleteAction): +class DeleteTenantsAction(policy.PolicyTargetMixin, tables.DeleteAction): @staticmethod def action_present(count): return ungettext_lazy( @@ -149,8 +179,12 @@ class DeleteTenantsAction(tables.DeleteAction): ) policy_rules = (("identity", "identity:delete_project"),) + policy_target_attrs = ("target.project.domain_id", "domain_id"), def allowed(self, request, project): + if api.keystone.is_multi_domain_enabled() \ + and not api.keystone.is_domain_admin(request): + return False return api.keystone.keystone_can_edit_project() def delete(self, request, obj_id): @@ -238,6 +272,17 @@ class TenantsTable(tables.DataTable): required=False), update_action=UpdateCell) + if api.keystone.VERSIONS.active >= 3: + domain_name = tables.Column( + 'domain_name', verbose_name=_('Domain Name')) + enabled = tables.Column('enabled', verbose_name=_('Enabled'), + status=True, + filters=(filters.yesno, filters.capfirst), + form_field=forms.BooleanField( + label=_('Enabled'), + required=False), + update_action=UpdateCell) + def get_project_detail_link(self, project): # this method is an ugly monkey patch, needed because # the column link method does not provide access to the request diff --git a/openstack_dashboard/dashboards/identity/projects/tests.py b/openstack_dashboard/dashboards/identity/projects/tests.py index 238a4cf1a8..22783ada7c 100644 --- a/openstack_dashboard/dashboards/identity/projects/tests.py +++ b/openstack_dashboard/dashboards/identity/projects/tests.py @@ -51,31 +51,43 @@ PROJECT_DETAIL_URL = reverse('horizon:identity:projects:detail', args=[1]) class TenantsViewTests(test.BaseAdminViewTests): - @test.create_stubs({api.keystone: ('tenant_list',)}) + @test.create_stubs({api.keystone: ('tenant_list', 'domain_lookup')}) def test_index(self): + domain = self.domains.get(id="1") api.keystone.tenant_list(IsA(http.HttpRequest), domain=None, paginate=True, marker=None) \ .AndReturn([self.tenants.list(), False]) + api.keystone.domain_lookup(IgnoreArg()).AndReturn({domain.id: + domain.name}) self.mox.ReplayAll() res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'identity/projects/index.html') self.assertItemsEqual(res.context['table'].data, self.tenants.list()) - @test.create_stubs({api.keystone: ('tenant_list', )}) + @test.create_stubs({api.keystone: ('tenant_list', + 'get_effective_domain_id', + 'domain_lookup')}) def test_index_with_domain_context(self): domain = self.domains.get(id="1") + self.setSessionValues(domain_context=domain.id, domain_context_name=domain.name) + domain_tenants = [tenant for tenant in self.tenants.list() if tenant.domain_id == domain.id] + + api.keystone.get_effective_domain_id(IgnoreArg()).AndReturn(domain.id) + api.keystone.tenant_list(IsA(http.HttpRequest), domain=domain.id, paginate=True, marker=None) \ .AndReturn([domain_tenants, False]) + api.keystone.domain_lookup(IgnoreArg()).AndReturn({domain.id: + domain.name}) self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -86,14 +98,18 @@ class TenantsViewTests(test.BaseAdminViewTests): class ProjectsViewNonAdminTests(test.TestCase): @override_settings(POLICY_CHECK_FUNCTION=policy_backend.check) - @test.create_stubs({api.keystone: ('tenant_list',)}) + @test.create_stubs({api.keystone: ('tenant_list', + 'domain_lookup')}) def test_index(self): + domain = self.domains.get(id="1") api.keystone.tenant_list(IsA(http.HttpRequest), user=self.user.id, paginate=True, marker=None, admin=False) \ .AndReturn([self.tenants.list(), False]) + api.keystone.domain_lookup(IgnoreArg()).AndReturn({domain.id: + domain.name}) self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -393,6 +409,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): 'role_list', 'group_list', 'get_default_domain', + 'is_cloud_admin', 'get_default_role'), quotas: ('get_default_quota_data', 'get_disabled_quotas')}) @@ -407,8 +424,13 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): # init api.keystone.get_default_domain(IsA(http.HttpRequest)) \ .AndReturn(default_domain) + + api.keystone.is_cloud_admin(IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) + quotas.get_disabled_quotas(IsA(http.HttpRequest)) \ .AndReturn(self.disabled_quotas.first()) + quotas.get_default_quota_data(IsA(http.HttpRequest)) \ .AndRaise(self.exceptions.nova) @@ -456,7 +478,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): # init api.keystone.get_default_domain(IsA(http.HttpRequest)) \ - .AndReturn(default_domain) + .MultipleTimes().AndReturn(default_domain) quotas.get_disabled_quotas(IsA(http.HttpRequest)) \ .AndReturn(self.disabled_quotas.first()) quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) @@ -515,7 +537,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): # init api.keystone.get_default_domain(IsA(http.HttpRequest)) \ - .AndReturn(default_domain) + .MultipleTimes().AndReturn(default_domain) quotas.get_disabled_quotas(IsA(http.HttpRequest)) \ .AndReturn(self.disabled_quotas.first()) quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) @@ -600,8 +622,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): roles = self.roles.list() # init - api.keystone.get_default_domain(IsA(http.HttpRequest)) \ - .AndReturn(default_domain) + api.keystone.get_default_domain( + IsA(http.HttpRequest)).MultipleTimes().AndReturn(default_domain) quotas.get_disabled_quotas(IsA(http.HttpRequest)) \ .AndReturn(self.disabled_quotas.first()) quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) @@ -772,7 +794,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): workflow_data[GROUP_ROLE_PREFIX + "2"] = ['1', '2', '3'] api.keystone.role_assignments_list(IsA(http.HttpRequest), project=self.tenant.id) \ - .AndReturn(role_assignments) + .MultipleTimes().AndReturn(role_assignments) # Give user 1 role 2 api.keystone.add_tenant_user_role(IsA(http.HttpRequest), project=self.tenant.id, @@ -879,7 +901,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): if keystone_api_version >= 3: api.keystone.role_assignments_list(IsA(http.HttpRequest), project=self.tenant.id) \ - .AndReturn(role_assignments) + .MultipleTimes().AndReturn(role_assignments) else: api.keystone.user_list(IsA(http.HttpRequest), project=self.tenant.id) \ @@ -890,10 +912,6 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): user.id, self.tenant.id).AndReturn(roles) - api.keystone.role_assignments_list(IsA(http.HttpRequest), - project=self.tenant.id) \ - .AndReturn(role_assignments) - self.mox.ReplayAll() url = reverse('horizon:identity:projects:update', @@ -922,6 +940,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): @test.create_stubs({api.keystone: ('tenant_get', 'domain_get', + 'get_effective_domain_id', 'tenant_update', 'get_default_role', 'roles_for_user', @@ -938,7 +957,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): api.cinder: ('tenant_quota_update',), quotas: ('get_tenant_quota_data', 'get_disabled_quotas', - 'tenant_quota_usages')}) + 'tenant_quota_usages',)}) def test_update_project_save(self, neutron=False): keystone_api_version = api.keystone.VERSIONS.active @@ -979,11 +998,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): workflow_data = {} - if keystone_api_version >= 3: - api.keystone.role_assignments_list(IsA(http.HttpRequest), - project=self.tenant.id) \ - .AndReturn(role_assignments) - else: + if keystone_api_version < 3: api.keystone.user_list(IsA(http.HttpRequest), project=self.tenant.id) \ .AndReturn(proj_users) @@ -993,10 +1008,6 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): user.id, self.tenant.id).AndReturn(roles) - api.keystone.role_assignments_list(IsA(http.HttpRequest), - project=self.tenant.id) \ - .AndReturn(role_assignments) - workflow_data[USER_ROLE_PREFIX + "1"] = ['3'] # admin role workflow_data[USER_ROLE_PREFIX + "2"] = ['2'] # member role # Group assignment form data @@ -1010,16 +1021,22 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): quota.metadata_items = 444 quota.volumes = 444 - updated_project = {"name": project._info["name"], - "description": project._info["description"], - "enabled": project.enabled} updated_quota = self._get_quota_info(quota) + # called once for tenant_update + api.keystone.get_effective_domain_id( + IsA(http.HttpRequest)).MultipleTimes().AndReturn(domain_id) + # handle api.keystone.tenant_update(IsA(http.HttpRequest), project.id, - **updated_project) \ - .AndReturn(project) + name=project._info["name"], + description=project._info['description'], + enabled=project.enabled, + domain=domain_id).AndReturn(project) + + api.keystone.user_list(IsA(http.HttpRequest), + domain=domain_id).AndReturn(users) self._check_role_list(keystone_api_version, role_assignments, groups, proj_users, roles, workflow_data) @@ -1092,6 +1109,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): @test.create_stubs({api.keystone: ('tenant_get', 'domain_get', + 'get_effective_domain_id', 'tenant_update', 'get_default_role', 'roles_for_user', @@ -1148,7 +1166,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): if keystone_api_version >= 3: api.keystone.role_assignments_list(IsA(http.HttpRequest), project=self.tenant.id) \ - .AndReturn(role_assignments) + .MultipleTimes().AndReturn(role_assignments) else: api.keystone.user_list(IsA(http.HttpRequest), project=self.tenant.id) \ @@ -1164,10 +1182,6 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): workflow_data.setdefault(USER_ROLE_PREFIX + role_ids[0], []) \ .append(user.id) - api.keystone.role_assignments_list(IsA(http.HttpRequest), - project=self.tenant.id) \ - .AndReturn(role_assignments) - role_ids = [role.id for role in roles] for group in groups: if role_ids: @@ -1181,17 +1195,21 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): quota.metadata_items = 444 quota.volumes = 444 - updated_project = {"name": project._info["name"], - "description": project._info["description"], - "enabled": project.enabled} updated_quota = self._get_quota_info(quota) # handle quotas.tenant_quota_usages(IsA(http.HttpRequest), tenant_id=project.id) \ .AndReturn(quota_usages) + + api.keystone.get_effective_domain_id( + IsA(http.HttpRequest)).MultipleTimes().AndReturn(domain_id) + api.keystone.tenant_update(IsA(http.HttpRequest), project.id, - **updated_project) \ + name=project._info["name"], + domain=domain_id, + description=project._info['description'], + enabled=project.enabled) \ .AndRaise(self.exceptions.keystone) self.mox.ReplayAll() @@ -1213,6 +1231,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): @test.create_stubs({api.keystone: ('tenant_get', 'domain_get', + 'get_effective_domain_id', 'tenant_update', 'get_default_role', 'roles_for_user', @@ -1257,8 +1276,10 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): api.keystone.get_default_role(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(default_role) - api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \ - .AndReturn(users) + + 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) \ @@ -1266,24 +1287,16 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): workflow_data = {} - if keystone_api_version >= 3: - api.keystone.role_assignments_list(IsA(http.HttpRequest), - project=self.tenant.id) \ - .AndReturn(role_assignments) - else: - api.keystone.user_list(IsA(http.HttpRequest), - project=self.tenant.id) \ - .AndReturn(proj_users) + if keystone_api_version < 3: + api.keystone.user_list( + IsA(http.HttpRequest), + project=self.tenant.id).AndReturn(proj_users) for user in proj_users: api.keystone.roles_for_user(IsA(http.HttpRequest), user.id, self.tenant.id).AndReturn(roles) - api.keystone.role_assignments_list(IsA(http.HttpRequest), - project=self.tenant.id) \ - .AndReturn(role_assignments) - 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 @@ -1297,16 +1310,21 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): quota[0].limit = 444 quota[1].limit = -1 - updated_project = {"name": project._info["name"], - "description": project._info["description"], - "enabled": project.enabled} updated_quota = self._get_quota_info(quota) # handle + api.keystone.get_effective_domain_id( + IsA(http.HttpRequest)).MultipleTimes().AndReturn(domain_id) + api.keystone.tenant_update(IsA(http.HttpRequest), project.id, - **updated_project) \ - .AndReturn(project) + name=project._info["name"], + description=project._info['description'], + enabled=project.enabled, + domain=domain_id).AndReturn(project) + + api.keystone.user_list(IsA(http.HttpRequest), + domain=domain_id).AndReturn(users) self._check_role_list(keystone_api_version, role_assignments, groups, proj_users, roles, workflow_data) @@ -1352,7 +1370,8 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): 'add_group_role', 'group_list', 'role_list', - 'role_assignments_list'), + 'role_assignments_list', + 'get_effective_domain_id'), quotas: ('get_tenant_quota_data', 'get_disabled_quotas', 'tenant_quota_usages')}) @@ -1384,8 +1403,10 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): api.keystone.get_default_role(IsA(http.HttpRequest)) \ .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) \ @@ -1393,24 +1414,16 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): workflow_data = {} - if keystone_api_version >= 3: - api.keystone.role_assignments_list(IsA(http.HttpRequest), - project=self.tenant.id) \ - .AndReturn(role_assignments) - else: - api.keystone.user_list(IsA(http.HttpRequest), - project=self.tenant.id) \ - .AndReturn(proj_users) + if keystone_api_version < 3: + api.keystone.user_list( + IsA(http.HttpRequest), + project=self.tenant.id).AndReturn(proj_users) for user in proj_users: api.keystone.roles_for_user(IsA(http.HttpRequest), user.id, self.tenant.id).AndReturn(roles) - api.keystone.role_assignments_list(IsA(http.HttpRequest), - project=self.tenant.id) \ - .AndReturn(role_assignments) - workflow_data[USER_ROLE_PREFIX + "1"] = ['1', '3'] # admin role workflow_data[USER_ROLE_PREFIX + "2"] = ['1', '2', '3'] # member role @@ -1423,21 +1436,28 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): quota.metadata_items = 444 quota.volumes = 444 - updated_project = {"name": project._info["name"], - "description": project._info["description"], - "enabled": project.enabled} updated_quota = self._get_quota_info(quota) # handle quotas.tenant_quota_usages(IsA(http.HttpRequest), tenant_id=project.id) \ .AndReturn(quota_usages) + + api.keystone.get_effective_domain_id( + IsA(http.HttpRequest)).MultipleTimes().AndReturn(domain_id) + api.keystone.tenant_update(IsA(http.HttpRequest), project.id, - **updated_project) \ - .AndReturn(project) + name=project._info["name"], + description=project._info['description'], + enabled=project.enabled, + domain=domain_id).AndReturn(project) + + api.keystone.user_list(IsA(http.HttpRequest), + domain=domain_id).AndReturn(users) self._check_role_list(keystone_api_version, role_assignments, groups, proj_users, roles, workflow_data) + self.mox.ReplayAll() # submit form data @@ -1592,7 +1612,8 @@ class DetailProjectViewTests(test.BaseAdminViewTests): "The WITH_SELENIUM env variable is not set.") class SeleniumTests(test.SeleniumAdminTestCase): @test.create_stubs( - {api.keystone: ('tenant_list', 'tenant_get', 'tenant_update')}) + {api.keystone: ('tenant_list', 'tenant_get', 'tenant_update', + 'domain_lookup')}) def test_inline_editing_update(self): # Tenant List api.keystone.tenant_list(IgnoreArg(), @@ -1600,6 +1621,7 @@ class SeleniumTests(test.SeleniumAdminTestCase): marker=None, paginate=True) \ .AndReturn([self.tenants.list(), False]) + api.keystone.domain_lookup(IgnoreArg()).AndReturn({None: None}) # Edit mod api.keystone.tenant_get(IgnoreArg(), u'1', @@ -1673,7 +1695,7 @@ class SeleniumTests(test.SeleniumAdminTestCase): "'Changed test_tenant'") @test.create_stubs( - {api.keystone: ('tenant_list', 'tenant_get')}) + {api.keystone: ('tenant_list', 'tenant_get', 'domain_lookup')}) def test_inline_editing_cancel(self): # Tenant List api.keystone.tenant_list(IgnoreArg(), @@ -1681,6 +1703,7 @@ class SeleniumTests(test.SeleniumAdminTestCase): marker=None, paginate=True) \ .AndReturn([self.tenants.list(), False]) + api.keystone.domain_lookup(IgnoreArg()).AndReturn({None: None}) # Edit mod api.keystone.tenant_get(IgnoreArg(), u'1', diff --git a/openstack_dashboard/dashboards/identity/projects/views.py b/openstack_dashboard/dashboards/identity/projects/views.py index 388b55d5fc..9b72806689 100644 --- a/openstack_dashboard/dashboards/identity/projects/views.py +++ b/openstack_dashboard/dashboards/identity/projects/views.py @@ -77,10 +77,12 @@ class IndexView(tables.DataTableView): tenants = [] marker = self.request.GET.get( project_tables.TenantsTable._meta.pagination_param, None) - domain_context = self.request.session.get('domain_context', None) + self._more = False + if policy.check((("identity", "identity:list_projects"),), self.request): + domain_context = api.keystone.get_effective_domain_id(self.request) try: tenants, self._more = api.keystone.tenant_list( self.request, @@ -106,6 +108,11 @@ class IndexView(tables.DataTableView): msg = \ _("Insufficient privilege level to view project information.") messages.info(self.request, msg) + + if api.keystone.VERSIONS.active >= 3: + domain_lookup = api.keystone.domain_lookup(self.request) + for t in tenants: + t.domain_name = domain_lookup.get(t.domain_id) return tenants @@ -126,6 +133,11 @@ class CreateProjectView(workflows.WorkflowView): workflow_class = project_workflows.CreateProject def get_initial(self): + + if (api.keystone.is_multi_domain_enabled() and + not api.keystone.is_cloud_admin(self.request)): + self.workflow_class = project_workflows.CreateProjectNoQuota + initial = super(CreateProjectView, self).get_initial() # Set the domain of the project @@ -133,33 +145,36 @@ class CreateProjectView(workflows.WorkflowView): initial["domain_id"] = domain.id initial["domain_name"] = domain.name + # TODO(esp): fix this for Domain Admin or find a work around # get initial quota defaults - try: - quota_defaults = quotas.get_default_quota_data(self.request) - + if api.keystone.is_cloud_admin(self.request): try: - if api.base.is_service_enabled(self.request, 'network') and \ - api.neutron.is_quotas_extension_supported( - self.request): - # TODO(jpichon): There is no API to access the Neutron - # default quotas (LP#1204956). For now, use the values - # from the current project. - project_id = self.request.user.project_id - quota_defaults += api.neutron.tenant_quota_get( - self.request, - tenant_id=project_id) + quota_defaults = quotas.get_default_quota_data(self.request) + + try: + if api.base.is_service_enabled( + self.request, 'network') and \ + api.neutron.is_quotas_extension_supported( + self.request): + # TODO(jpichon): There is no API to access the Neutron + # default quotas (LP#1204956). For now, use the values + # from the current project. + project_id = self.request.user.project_id + quota_defaults += api.neutron.tenant_quota_get( + self.request, + tenant_id=project_id) + except Exception: + error_msg = _('Unable to retrieve default Neutron quota ' + 'values.') + self.add_error_to_step(error_msg, 'create_quotas') + + for field in quotas.QUOTA_FIELDS: + initial[field] = quota_defaults.get(field).limit + except Exception: - error_msg = _('Unable to retrieve default Neutron quota ' - 'values.') + error_msg = _('Unable to retrieve default quota values.') self.add_error_to_step(error_msg, 'create_quotas') - for field in quotas.QUOTA_FIELDS: - initial[field] = quota_defaults.get(field).limit - - except Exception: - error_msg = _('Unable to retrieve default quota values.') - self.add_error_to_step(error_msg, 'create_quotas') - return initial @@ -167,6 +182,11 @@ class UpdateProjectView(workflows.WorkflowView): workflow_class = project_workflows.UpdateProject def get_initial(self): + + if (api.keystone.is_multi_domain_enabled() and + not api.keystone.is_cloud_admin(self.request)): + self.workflow_class = project_workflows.UpdateProjectNoQuota + initial = super(UpdateProjectView, self).get_initial() project_id = self.kwargs['tenant_id'] @@ -182,23 +202,32 @@ class UpdateProjectView(workflows.WorkflowView): # Retrieve the domain name where the project belong if keystone.VERSIONS.active >= 3: try: - domain = api.keystone.domain_get(self.request, - initial["domain_id"]) - initial["domain_name"] = domain.name + if policy.check((("identity", "identity:get_domain"),), + self.request): + domain = api.keystone.domain_get(self.request, + initial["domain_id"]) + initial["domain_name"] = domain.name + + else: + domain = api.keystone.get_default_domain(self.request) + initial["domain_name"] = domain.name + except Exception: exceptions.handle(self.request, _('Unable to retrieve project domain.'), redirect=reverse(INDEX_URL)) # get initial project quota - quota_data = quotas.get_tenant_quota_data(self.request, - tenant_id=project_id) - if api.base.is_service_enabled(self.request, 'network') and \ - api.neutron.is_quotas_extension_supported(self.request): - quota_data += api.neutron.tenant_quota_get( - self.request, tenant_id=project_id) - for field in quotas.QUOTA_FIELDS: - initial[field] = quota_data.get(field).limit + if keystone.is_cloud_admin(self.request): + quota_data = quotas.get_tenant_quota_data(self.request, + tenant_id=project_id) + if api.base.is_service_enabled(self.request, 'network') and \ + api.neutron.is_quotas_extension_supported( + self.request): + quota_data += api.neutron.tenant_quota_get( + self.request, tenant_id=project_id) + for field in quotas.QUOTA_FIELDS: + initial[field] = quota_data.get(field).limit except Exception: exceptions.handle(self.request, _('Unable to retrieve project details.'), diff --git a/openstack_dashboard/dashboards/identity/projects/workflows.py b/openstack_dashboard/dashboards/identity/projects/workflows.py index 10c78a6ad4..edbe833bc5 100644 --- a/openstack_dashboard/dashboards/identity/projects/workflows.py +++ b/openstack_dashboard/dashboards/identity/projects/workflows.py @@ -16,6 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. +import logging from django.conf import settings from django.core.urlresolvers import reverse @@ -34,6 +35,9 @@ from openstack_dashboard.api import nova from openstack_dashboard.usage import quotas from openstack_dashboard.utils.identity import IdentityMixIn +LOG = logging.getLogger(__name__) + + INDEX_URL = "horizon:identity:projects:index" ADD_USER_URL = "horizon:identity:projects:create_user" PROJECT_GROUP_ENABLED = keystone.VERSIONS.active >= 3 @@ -111,6 +115,7 @@ class UpdateProjectQuotaAction(ProjectQuotaAction): name = _("Quota") slug = 'update_quotas' help_text = _("Set maximum quotas for the project.") + permissions = ('openstack.roles.admin', 'openstack.services.compute') class CreateProjectQuotaAction(ProjectQuotaAction): @@ -118,6 +123,7 @@ class CreateProjectQuotaAction(ProjectQuotaAction): name = _("Quota") slug = 'create_quotas' help_text = _("Set maximum quotas for the project.") + permissions = ('openstack.roles.admin', 'openstack.services.compute') class UpdateProjectQuota(workflows.Step): @@ -186,13 +192,14 @@ class UpdateProjectMembersAction(workflows.MembershipAction): err_msg = _('Unable to retrieve user list. Please try again later.') # Use the domain_id from the project domain_id = self.initial.get("domain_id", None) + project_id = '' if 'project_id' in self.initial: project_id = self.initial['project_id'] # Get the default role try: - default_role = api.keystone.get_default_role(self.request) + default_role = keystone.get_default_role(self.request) # Default role is necessary to add members to a project if default_role is None: default = getattr(settings, @@ -528,12 +535,38 @@ class CreateProject(CommonQuotaWorkflow): self._update_project_members(request, data, project_id) if PROJECT_GROUP_ENABLED: self._update_project_groups(request, data, project_id) - self._update_project_quota(request, data, project_id) + if keystone.is_cloud_admin(request): + self._update_project_quota(request, data, project_id) return True +class CreateProjectNoQuota(CreateProject): + slug = "create_project" + name = _("Create Project") + finalize_button_name = _("Create Project") + success_message = _('Created new project "%s".') + failure_message = _('Unable to create project "%s".') + success_url = "horizon:identity:projects:index" + default_steps = (CreateProjectInfo, UpdateProjectMembers) + + def __init__(self, request=None, context_seed=None, entry_point=None, + *args, **kwargs): + if PROJECT_GROUP_ENABLED: + self.default_steps = (CreateProjectInfo, + UpdateProjectMembers, + UpdateProjectGroups,) + super(CreateProject, self).__init__(request=request, + context_seed=context_seed, + entry_point=entry_point, + *args, + **kwargs) + + class UpdateProjectInfoAction(CreateProjectInfoAction): enabled = forms.BooleanField(required=False, label=_("Enabled")) + domain_name = forms.CharField(label=_("Domain Name"), + required=False, + widget=forms.HiddenInput()) def __init__(self, request, initial, *args, **kwargs): super(UpdateProjectInfoAction, self).__init__( @@ -608,7 +641,8 @@ class UpdateProject(CommonQuotaWorkflow, IdentityMixIn): return api.keystone.role_list(request) def _update_project(self, request, data): - # update project info + """Update project info""" + domain_id = api.keystone.get_effective_domain_id(self.request) try: project_id = data['project_id'] return api.keystone.tenant_update( @@ -616,12 +650,14 @@ class UpdateProject(CommonQuotaWorkflow, IdentityMixIn): project_id, name=data['name'], description=data['description'], - enabled=data['enabled']) + enabled=data['enabled'], + domain=domain_id) except exceptions.Conflict: msg = _('Project name "%s" is already used.') % data['name'] self.failure_message = msg return - except Exception: + except Exception as e: + LOG.debug('Project update failed: %s' % e) exceptions.handle(request, ignore=True) return @@ -699,7 +735,22 @@ class UpdateProject(CommonQuotaWorkflow, IdentityMixIn): request, project=project_id) users_to_modify = len(users_roles) + # TODO(bpokorny): The following lines are needed to make sure we + # only modify roles for users who are in the current domain. + # Otherwise, we'll end up removing roles for users who have roles + # on the project but aren't in the domain. For now, Horizon won't + # support managing roles across domains. The Keystone CLI + # supports it, so we may want to add that in the future. + all_users = api.keystone.user_list(request, + domain=data['domain_id']) + users_dict = {user.id: user.name for user in all_users} + for user_id in users_roles.keys(): + # Don't remove roles if the user isn't in the domain + if user_id not in users_dict: + users_to_modify -= 1 + continue + # Check if there have been any changes in the roles of # Existing project members. current_role_ids = list(users_roles[user_id]) @@ -854,8 +905,32 @@ class UpdateProject(CommonQuotaWorkflow, IdentityMixIn): if not ret: return False - ret = self._update_project_quota(request, data, project_id) - if not ret: - return False + if api.keystone.is_cloud_admin(request): + ret = self._update_project_quota(request, data, project_id) + if not ret: + return False return True + + +class UpdateProjectNoQuota(UpdateProject): + slug = "update_project" + name = _("Edit Project") + finalize_button_name = _("Save") + success_message = _('Modified project "%s".') + failure_message = _('Unable to modify project "%s".') + success_url = "horizon:identity:projects:index" + default_steps = (UpdateProjectInfo, UpdateProjectMembers) + + def __init__(self, request=None, context_seed=None, entry_point=None, + *args, **kwargs): + if PROJECT_GROUP_ENABLED: + self.default_steps = (UpdateProjectInfo, + UpdateProjectMembers, + UpdateProjectGroups) + + super(UpdateProject, self).__init__(request=request, + context_seed=context_seed, + entry_point=entry_point, + *args, + **kwargs) diff --git a/openstack_dashboard/dashboards/identity/roles/panel.py b/openstack_dashboard/dashboards/identity/roles/panel.py index 43bca73e85..57f07398ce 100644 --- a/openstack_dashboard/dashboards/identity/roles/panel.py +++ b/openstack_dashboard/dashboards/identity/roles/panel.py @@ -24,6 +24,12 @@ class Roles(horizon.Panel): slug = 'roles' policy_rules = (("identity", "identity:list_roles"),) + def can_access(self, context): + if keystone.is_multi_domain_enabled() \ + and not keystone.is_domain_admin(context['request']): + return False + return super(Roles, self).can_access(context) + @staticmethod def can_register(): return keystone.VERSIONS.active >= 3 diff --git a/openstack_dashboard/dashboards/identity/users/forms.py b/openstack_dashboard/dashboards/identity/users/forms.py index bbaebbe580..79ee882a99 100644 --- a/openstack_dashboard/dashboards/identity/users/forms.py +++ b/openstack_dashboard/dashboards/identity/users/forms.py @@ -68,17 +68,26 @@ class BaseUserForm(forms.SelfHandlingForm): # the user has access to. user_id = kwargs['initial'].get('id', None) domain_id = kwargs['initial'].get('domain_id', None) - projects, has_more = api.keystone.tenant_list(request, - domain=domain_id, - user=user_id) - for project in projects: - if project.enabled: - project_choices.append((project.id, project.name)) - if not project_choices: - project_choices.insert(0, ('', _("No available projects"))) - elif len(project_choices) > 1: - project_choices.insert(0, ('', _("Select a project"))) - self.fields['project'].choices = project_choices + + try: + if api.keystone.VERSIONS.active >= 3: + projects, has_more = api.keystone.tenant_list( + request, domain=domain_id) + else: + projects, has_more = api.keystone.tenant_list( + request, user=user_id) + + for project in projects: + if project.enabled: + project_choices.append((project.id, project.name)) + if not project_choices: + project_choices.insert(0, ('', _("No available projects"))) + elif len(project_choices) > 1: + project_choices.insert(0, ('', _("Select a project"))) + self.fields['project'].choices = project_choices + + except Exception: + LOG.debug("User: %s has no projects" % user_id) ADD_PROJECT_URL = "horizon:identity:projects:create" @@ -135,7 +144,7 @@ class CreateUserForm(PasswordMixin, BaseUserForm): # password and confirm_password strings. @sensitive_variables('data') def handle(self, request, data): - domain = api.keystone.get_default_domain(self.request) + domain = api.keystone.get_default_domain(self.request, False) try: LOG.info('Creating user with name "%s"' % data['name']) desc = data["description"] diff --git a/openstack_dashboard/dashboards/identity/users/panel.py b/openstack_dashboard/dashboards/identity/users/panel.py index 7c5b7b8968..6c20f570b7 100644 --- a/openstack_dashboard/dashboards/identity/users/panel.py +++ b/openstack_dashboard/dashboards/identity/users/panel.py @@ -20,9 +20,17 @@ from django.utils.translation import ugettext_lazy as _ import horizon +from openstack_dashboard.api import keystone + class Users(horizon.Panel): name = _("Users") slug = 'users' policy_rules = (("identity", "identity:get_user"), ("identity", "identity:list_users")) + + def can_access(self, context): + if keystone.is_multi_domain_enabled() \ + and not keystone.is_domain_admin(context['request']): + return False + return super(Users, self).can_access(context) diff --git a/openstack_dashboard/dashboards/identity/users/tables.py b/openstack_dashboard/dashboards/identity/users/tables.py index 82f9b511b6..08575fee8d 100644 --- a/openstack_dashboard/dashboards/identity/users/tables.py +++ b/openstack_dashboard/dashboards/identity/users/tables.py @@ -50,7 +50,8 @@ class EditUserLink(policy.PolicyTargetMixin, tables.LinkAction): icon = "pencil" policy_rules = (("identity", "identity:update_user"), ("identity", "identity:list_projects"),) - policy_target_attrs = (("user_id", "id"),) + policy_target_attrs = (("user_id", "id"), + ("target.user.domain_id", "domain_id"),) def allowed(self, request, user): return api.keystone.keystone_can_edit_user() @@ -103,7 +104,8 @@ class ToggleEnabled(policy.PolicyTargetMixin, tables.BatchAction): ) classes = ("btn-toggle",) policy_rules = (("identity", "identity:update_user"),) - policy_target_attrs = (("user_id", "id"),) + policy_target_attrs = (("user_id", "id"), + ("target.user.domain_id", "domain_id")) def allowed(self, request, user=None): if not api.keystone.keystone_can_edit_user(): @@ -137,7 +139,7 @@ class ToggleEnabled(policy.PolicyTargetMixin, tables.BatchAction): self.current_past_action = ENABLE -class DeleteUsersAction(tables.DeleteAction): +class DeleteUsersAction(policy.PolicyTargetMixin, tables.DeleteAction): @staticmethod def action_present(count): return ungettext_lazy( @@ -256,6 +258,18 @@ class UsersTable(tables.DataTable): defaultfilters.capfirst), empty_value="False") + if api.keystone.VERSIONS.active >= 3: + domain_name = tables.Column( + 'domain_name', + verbose_name=_('Domain Name'), + attrs={'data-type': 'uuid'}) + enabled = tables.Column('enabled', verbose_name=_('Enabled'), + status=True, + status_choices=STATUS_CHOICES, + filters=(defaultfilters.yesno, + defaultfilters.capfirst), + empty_value="False") + class Meta(object): name = "users" verbose_name = _("Users") diff --git a/openstack_dashboard/dashboards/identity/users/tests.py b/openstack_dashboard/dashboards/identity/users/tests.py index 600c0f0a5f..504de15baf 100644 --- a/openstack_dashboard/dashboards/identity/users/tests.py +++ b/openstack_dashboard/dashboards/identity/users/tests.py @@ -53,13 +53,20 @@ class UsersViewTests(test.BaseAdminViewTests): if user.domain_id == domain_id] return users - @test.create_stubs({api.keystone: ('user_list',)}) + @test.create_stubs({api.keystone: ('user_list', + 'get_effective_domain_id', + 'domain_lookup')}) def test_index(self): domain = self._get_default_domain() domain_id = domain.id users = self._get_users(domain_id) + + api.keystone.get_effective_domain_id(IgnoreArg()).AndReturn(domain_id) + api.keystone.user_list(IgnoreArg(), domain=domain_id).AndReturn(users) + api.keystone.domain_lookup(IgnoreArg()).AndReturn({domain.id: + domain.name}) self.mox.ReplayAll() res = self.client.get(USERS_INDEX_URL) @@ -91,11 +98,19 @@ class UsersViewTests(test.BaseAdminViewTests): role = self.roles.first() api.keystone.get_default_domain(IgnoreArg()) \ - .MultipleTimes().AndReturn(domain) - api.keystone.tenant_list(IgnoreArg(), - domain=domain_id, - user=None) \ - .AndReturn([self.tenants.list(), False]) + .AndReturn(domain) + api.keystone.get_default_domain(IgnoreArg(), False) \ + .AndReturn(domain) + + if api.keystone.VERSIONS.active >= 3: + api.keystone.tenant_list( + IgnoreArg(), domain=domain.id).AndReturn( + [self.tenants.list(), False]) + else: + api.keystone.tenant_list( + IgnoreArg(), user=None).AndReturn( + [self.tenants.list(), False]) + api.keystone.user_create(IgnoreArg(), name=user.name, description=user.description, @@ -146,11 +161,19 @@ class UsersViewTests(test.BaseAdminViewTests): domain_id = domain.id role = self.roles.first() api.keystone.get_default_domain(IgnoreArg()) \ - .MultipleTimes().AndReturn(domain) - api.keystone.tenant_list(IgnoreArg(), - domain=domain_id, - user=None) \ - .AndReturn([self.tenants.list(), False]) + .AndReturn(domain) + api.keystone.get_default_domain(IgnoreArg(), False) \ + .AndReturn(domain) + + if api.keystone.VERSIONS.active >= 3: + api.keystone.tenant_list( + IgnoreArg(), domain=domain.id).AndReturn( + [self.tenants.list(), False]) + else: + api.keystone.tenant_list( + IgnoreArg(), user=user.id).AndReturn( + [self.tenants.list(), False]) + api.keystone.user_create(IgnoreArg(), name=user.name, description=user.description, @@ -192,8 +215,16 @@ class UsersViewTests(test.BaseAdminViewTests): api.keystone.get_default_domain(IgnoreArg()) \ .MultipleTimes().AndReturn(domain) - api.keystone.tenant_list(IgnoreArg(), domain=domain_id, user=None) \ - .AndReturn([self.tenants.list(), False]) + + if api.keystone.VERSIONS.active >= 3: + api.keystone.tenant_list( + IgnoreArg(), domain=domain_id).AndReturn( + [self.tenants.list(), False]) + else: + api.keystone.tenant_list( + IgnoreArg(), user=None).AndReturn( + [self.tenants.list(), False]) + api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list()) api.keystone.get_default_role(IgnoreArg()) \ .AndReturn(self.roles.first()) @@ -224,8 +255,16 @@ class UsersViewTests(test.BaseAdminViewTests): api.keystone.get_default_domain(IgnoreArg()) \ .MultipleTimes().AndReturn(domain) - api.keystone.tenant_list(IgnoreArg(), domain=domain_id, user=None) \ - .AndReturn([self.tenants.list(), False]) + + if api.keystone.VERSIONS.active >= 3: + api.keystone.tenant_list( + IgnoreArg(), domain=domain_id).AndReturn( + [self.tenants.list(), False]) + else: + api.keystone.tenant_list( + IgnoreArg(), user=None).AndReturn( + [self.tenants.list(), False]) + api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list()) api.keystone.get_default_role(IgnoreArg()) \ .AndReturn(self.roles.first()) @@ -259,8 +298,16 @@ class UsersViewTests(test.BaseAdminViewTests): api.keystone.get_default_domain(IgnoreArg()) \ .MultipleTimes().AndReturn(domain) - api.keystone.tenant_list(IgnoreArg(), domain=domain_id, user=None) \ - .AndReturn([self.tenants.list(), False]) + + if api.keystone.VERSIONS.active >= 3: + api.keystone.tenant_list( + IgnoreArg(), domain=domain_id).AndReturn( + [self.tenants.list(), False]) + else: + api.keystone.tenant_list( + IgnoreArg(), user=None).AndReturn( + [self.tenants.list(), False]) + api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list()) api.keystone.get_default_role(IgnoreArg()) \ .AndReturn(self.roles.first()) @@ -299,10 +346,16 @@ class UsersViewTests(test.BaseAdminViewTests): admin=True).AndReturn(user) api.keystone.domain_get(IsA(http.HttpRequest), domain_id).AndReturn(domain) - api.keystone.tenant_list(IgnoreArg(), - domain=domain_id, - user=user.id) \ - .AndReturn([self.tenants.list(), False]) + + if api.keystone.VERSIONS.active >= 3: + api.keystone.tenant_list( + IgnoreArg(), domain=domain.id).AndReturn( + [self.tenants.list(), False]) + else: + api.keystone.tenant_list( + IgnoreArg(), user=user.id).AndReturn( + [self.tenants.list(), False]) + api.keystone.user_update(IsA(http.HttpRequest), user.id, email=user.email, @@ -337,10 +390,16 @@ class UsersViewTests(test.BaseAdminViewTests): admin=True).AndReturn(user) api.keystone.domain_get(IsA(http.HttpRequest), domain_id).AndReturn(domain) - api.keystone.tenant_list(IgnoreArg(), - domain=domain_id, - user=user.id) \ - .AndReturn([self.tenants.list(), False]) + + if api.keystone.VERSIONS.active >= 3: + api.keystone.tenant_list( + IgnoreArg(), domain=domain_id).AndReturn( + [self.tenants.list(), False]) + else: + api.keystone.tenant_list( + IgnoreArg(), user=user.id).AndReturn( + [self.tenants.list(), False]) + api.keystone.user_update(IsA(http.HttpRequest), user.id, email=user.email, @@ -376,8 +435,15 @@ class UsersViewTests(test.BaseAdminViewTests): admin=True).AndReturn(user) api.keystone.domain_get(IsA(http.HttpRequest), domain_id) \ .AndReturn(domain) - api.keystone.tenant_list(IgnoreArg(), domain=domain_id, user=user.id) \ - .AndReturn([self.tenants.list(), False]) + if api.keystone.VERSIONS.active >= 3: + api.keystone.tenant_list( + IgnoreArg(), domain=domain_id).AndReturn( + [self.tenants.list(), False]) + else: + api.keystone.tenant_list( + IgnoreArg(), user=user.id).AndReturn( + [self.tenants.list(), False]) + api.keystone.keystone_can_edit_user().AndReturn(False) api.keystone.keystone_can_edit_user().AndReturn(False) @@ -486,7 +552,9 @@ class UsersViewTests(test.BaseAdminViewTests): res, "form", 'password', ['Password must be between 8 and 18 characters.']) - @test.create_stubs({api.keystone: ('user_update_enabled', 'user_list')}) + @test.create_stubs({api.keystone: ('user_update_enabled', + 'user_list', + 'domain_lookup')}) def test_enable_user(self): domain = self._get_default_domain() domain_id = domain.id @@ -498,6 +566,8 @@ class UsersViewTests(test.BaseAdminViewTests): api.keystone.user_update_enabled(IgnoreArg(), user.id, True).AndReturn(user) + api.keystone.domain_lookup(IgnoreArg()).AndReturn({domain.id: + domain.name}) self.mox.ReplayAll() @@ -506,7 +576,9 @@ class UsersViewTests(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, USERS_INDEX_URL) - @test.create_stubs({api.keystone: ('user_update_enabled', 'user_list')}) + @test.create_stubs({api.keystone: ('user_update_enabled', + 'user_list', + 'domain_lookup')}) def test_disable_user(self): domain = self._get_default_domain() domain_id = domain.id @@ -520,6 +592,8 @@ class UsersViewTests(test.BaseAdminViewTests): api.keystone.user_update_enabled(IgnoreArg(), user.id, False).AndReturn(user) + api.keystone.domain_lookup(IgnoreArg()).AndReturn({domain.id: + domain.name}) self.mox.ReplayAll() @@ -528,7 +602,9 @@ class UsersViewTests(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, USERS_INDEX_URL) - @test.create_stubs({api.keystone: ('user_update_enabled', 'user_list')}) + @test.create_stubs({api.keystone: ('user_update_enabled', + 'user_list', + 'domain_lookup')}) def test_enable_disable_user_exception(self): domain = self._get_default_domain() domain_id = domain.id @@ -540,6 +616,8 @@ class UsersViewTests(test.BaseAdminViewTests): .AndReturn(users) api.keystone.user_update_enabled(IgnoreArg(), user.id, True) \ .AndRaise(self.exceptions.keystone) + api.keystone.domain_lookup(IgnoreArg()).AndReturn({domain.id: + domain.name}) self.mox.ReplayAll() formData = {'action': 'users__toggle__%s' % user.id} @@ -547,7 +625,7 @@ class UsersViewTests(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, USERS_INDEX_URL) - @test.create_stubs({api.keystone: ('user_list',)}) + @test.create_stubs({api.keystone: ('user_list', 'domain_lookup')}) def test_disabling_current_user(self): domain = self._get_default_domain() domain_id = domain.id @@ -555,6 +633,33 @@ class UsersViewTests(test.BaseAdminViewTests): for i in range(0, 2): api.keystone.user_list(IgnoreArg(), domain=domain_id) \ .AndReturn(users) + api.keystone.domain_lookup(IgnoreArg()).AndReturn({domain.id: + domain.name}) + + self.mox.ReplayAll() + + formData = {'action': 'users__toggle__%s' % self.request.user.id} + res = self.client.post(USERS_INDEX_URL, formData, follow=True) + + self.assertEqual(list(res.context['messages'])[0].message, + u'You cannot disable the user you are currently ' + u'logged in as.') + + @test.create_stubs({api.keystone: ('user_list', 'domain_lookup')}) + def test_disabling_current_user_domain_name(self): + domain = self._get_default_domain() + domains = self.domains.list() + domain_id = domain.id + users = self._get_users(domain_id) + domain_lookup = dict((d.id, d.name) for d in domains) + + for u in users: + u.domain_name = domain_lookup.get(u.domain_id) + + for i in range(0, 2): + api.keystone.domain_lookup(IgnoreArg()).AndReturn(domain_lookup) + api.keystone.user_list(IgnoreArg(), domain=domain_id) \ + .AndReturn(users) self.mox.ReplayAll() @@ -565,7 +670,7 @@ class UsersViewTests(test.BaseAdminViewTests): u'You cannot disable the user you are currently ' u'logged in as.') - @test.create_stubs({api.keystone: ('user_list',)}) + @test.create_stubs({api.keystone: ('user_list', 'domain_lookup')}) def test_delete_user_with_improper_permissions(self): domain = self._get_default_domain() domain_id = domain.id @@ -573,6 +678,33 @@ class UsersViewTests(test.BaseAdminViewTests): for i in range(0, 2): api.keystone.user_list(IgnoreArg(), domain=domain_id) \ .AndReturn(users) + api.keystone.domain_lookup(IgnoreArg()).AndReturn({domain.id: + domain.name}) + + self.mox.ReplayAll() + + formData = {'action': 'users__delete__%s' % self.request.user.id} + res = self.client.post(USERS_INDEX_URL, formData, follow=True) + + self.assertEqual(list(res.context['messages'])[0].message, + u'You are not allowed to delete user: %s' + % self.request.user.username) + + @test.create_stubs({api.keystone: ('user_list', 'domain_lookup')}) + def test_delete_user_with_improper_permissions_domain_name(self): + domain = self._get_default_domain() + domains = self.domains.list() + domain_id = domain.id + users = self._get_users(domain_id) + domain_lookup = dict((d.id, d.name) for d in domains) + + for u in users: + u.domain_name = domain_lookup.get(u.domain_id) + + for i in range(0, 2): + api.keystone.user_list(IgnoreArg(), domain=domain_id) \ + .AndReturn(users) + api.keystone.domain_lookup(IgnoreArg()).AndReturn(domain_lookup) self.mox.ReplayAll() @@ -662,10 +794,16 @@ class UsersViewTests(test.BaseAdminViewTests): admin=True).AndReturn(user) api.keystone.domain_get(IsA(http.HttpRequest), domain_id).AndReturn(domain) - api.keystone.tenant_list(IgnoreArg(), - domain=domain_id, - user=user.id) \ - .AndReturn([self.tenants.list(), False]) + + if api.keystone.VERSIONS.active >= 3: + api.keystone.tenant_list( + IgnoreArg(), domain=domain.id).AndReturn( + [self.tenants.list(), False]) + else: + api.keystone.tenant_list( + IgnoreArg(), user=user.id).AndReturn( + [self.tenants.list(), False]) + api.keystone.user_update(IsA(http.HttpRequest), user.id, email=user.email, @@ -696,17 +834,27 @@ class SeleniumTests(test.SeleniumAdminTestCase): 'tenant_list', 'get_default_role', 'role_list', - 'user_list')}) + 'user_list', + 'domain_lookup')}) def test_modal_create_user_with_passwords_not_matching(self): domain = self._get_default_domain() api.keystone.get_default_domain(IgnoreArg()) \ - .AndReturn(domain) - api.keystone.tenant_list(IgnoreArg(), domain=None, user=None) \ - .AndReturn([self.tenants.list(), False]) + .MultipleTimes().AndReturn(domain) + + if api.keystone.VERSIONS.active >= 3: + api.keystone.tenant_list( + IgnoreArg(), domain=None).AndReturn( + [self.tenants.list(), False]) + else: + api.keystone.tenant_list( + IgnoreArg(), user=None).AndReturn( + [self.tenants.list(), False]) + api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list()) api.keystone.user_list(IgnoreArg(), domain=None) \ .AndReturn(self.users.list()) + api.keystone.domain_lookup(IgnoreArg()).AndReturn({None: None}) api.keystone.get_default_role(IgnoreArg()) \ .AndReturn(self.roles.first()) self.mox.ReplayAll() @@ -726,6 +874,10 @@ class SeleniumTests(test.SeleniumAdminTestCase): self.selenium.find_element_by_id("id_password").send_keys("test") self.selenium.find_element_by_id("id_confirm_password").send_keys("te") self.selenium.find_element_by_id("id_email").send_keys("a@b.com") + + wait.until(lambda x: self.selenium.find_element_by_id( + "id_confirm_password_error")) + self.assertTrue(self._is_element_present("id_confirm_password_error"), "Couldn't find password error element.") diff --git a/openstack_dashboard/dashboards/identity/users/views.py b/openstack_dashboard/dashboards/identity/users/views.py index 5eae45d651..92c0539373 100644 --- a/openstack_dashboard/dashboards/identity/users/views.py +++ b/openstack_dashboard/dashboards/identity/users/views.py @@ -50,9 +50,10 @@ class IndexView(tables.DataTableView): def get_data(self): users = [] - domain_context = self.request.session.get('domain_context', None) + if policy.check((("identity", "identity:list_users"),), self.request): + domain_context = api.keystone.get_effective_domain_id(self.request) try: users = api.keystone.user_list(self.request, domain=domain_context) @@ -71,6 +72,11 @@ class IndexView(tables.DataTableView): else: msg = _("Insufficient privilege level to view user information.") messages.info(self.request, msg) + + if api.keystone.VERSIONS.active >= 3: + domain_lookup = api.keystone.domain_lookup(self.request) + for u in users: + u.domain_name = domain_lookup.get(u.domain_id) return users @@ -108,12 +114,18 @@ class UpdateView(forms.ModalFormView): user = self.get_object() domain_id = getattr(user, "domain_id", None) domain_name = '' - # Retrieve the domain name where the project belong + # Retrieve the domain name where the project belongs if api.keystone.VERSIONS.active >= 3: try: - domain = api.keystone.domain_get(self.request, - domain_id) - domain_name = domain.name + if policy.check((("identity", "identity:get_domain"),), + self.request): + domain = api.keystone.domain_get(self.request, domain_id) + domain_name = domain.name + + else: + domain = api.keystone.get_default_domain(self.request) + domain_name = domain.get('name') + except Exception: exceptions.handle(self.request, _('Unable to retrieve project domain.')) @@ -176,8 +188,14 @@ class DetailView(views.HorizonTemplateView): domain_name = '' if api.keystone.VERSIONS.active >= 3: try: - domain = api.keystone.domain_get(self.request, domain_id) - domain_name = domain.name + if policy.check((("identity", "identity:get_domain"),), + self.request): + domain = api.keystone.domain_get( + self.request, domain_id) + domain_name = domain.name + else: + domain = api.keystone.get_default_domain(self.request) + domain_name = domain.get('name') except Exception: exceptions.handle(self.request, _('Unable to retrieve project domain.')) diff --git a/openstack_dashboard/dashboards/project/dashboard.py b/openstack_dashboard/dashboards/project/dashboard.py index 942bb72caa..90473e1fec 100644 --- a/openstack_dashboard/dashboards/project/dashboard.py +++ b/openstack_dashboard/dashboards/project/dashboard.py @@ -21,4 +21,9 @@ class Project(horizon.Dashboard): name = _("Project") slug = "project" + def can_access(self, context): + request = context['request'] + has_project = request.user.token.project.get('id') is not None + return super(Project, self).can_access(context) and has_project + horizon.register(Project) diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index c468c1d91c..0569735a17 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -66,7 +66,11 @@ WEBROOT = '/' # Overrides the default domain used when running on single-domain model # with Keystone V3. All entities will be created in the default domain. -#OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'Default' +# NOTE: This value must be the ID of the default domain, NOT the name. +# Also, you will most likely have a value in the keystone policy file like this +# "cloud_admin": "rule:admin_required and domain_id:" +# This value must match the domain id specified there. +#OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'default' # Set this to True to enable panels that provide the ability for users to # manage Identity Providers (IdPs) and establish a set of rules to map diff --git a/openstack_dashboard/policy.py b/openstack_dashboard/policy.py index de5be13e63..6bcb2242c7 100644 --- a/openstack_dashboard/policy.py +++ b/openstack_dashboard/policy.py @@ -40,7 +40,10 @@ class PolicyTargetMixin(object): policy_target_attrs = (("project_id", "tenant_id"), ("user_id", "user_id"), - ("domain_id", "domain_id")) + ("domain_id", "domain_id"), + ("target.project.domain_id", "domain_id"), + ("target.user.domain_id", "domain_id"), + ("target.group.domain_id", "domain_id")) def get_policy_target(self, request, datum=None): policy_target = {} diff --git a/openstack_dashboard/views.py b/openstack_dashboard/views.py index 365e47a4eb..22a024cad3 100644 --- a/openstack_dashboard/views.py +++ b/openstack_dashboard/views.py @@ -36,6 +36,11 @@ def get_user_home(user): if dashboard is None: dashboard = horizon.get_default_dashboard() + # Domain Admin, Project Admin will default to identity + if (user.token.project.get('id') is None or + (user.is_superuser and user.token.project.get('id'))): + dashboard = horizon.get_dashboard('identity') + return dashboard.get_absolute_url() diff --git a/releasenotes/notes/domains-0581aa42773d5f41.yaml b/releasenotes/notes/domains-0581aa42773d5f41.yaml new file mode 100644 index 0000000000..c4c75dc046 --- /dev/null +++ b/releasenotes/notes/domains-0581aa42773d5f41.yaml @@ -0,0 +1,22 @@ +--- +features: + - Added support for managing domains and projects when using Keystone v3. + Horizon now maintains a domain scoped token for users who have a role on a + domain, a project scoped token for users who have a role on a project, or + both a domain scoped token and project scoped token for users who have + roles on both. + - | + Domain management supports the following use cases: + + * Cloud Admin - View and manage identity resources across domains + * Domain Admin - View and manage identity resources in the domain logged in + * User - View identity project in the domain logged in + +other: + - | + Current limitations on managing identity resources with Keystone v3: + + * Does not support role assignments across domains, such as giving a user + in domain1 access to domain2. + * Does not support project admins managing Keystone projects. + * Does not support hierarchical project management.