diff options
author | Ruan HE <ruan.he@orange.com> | 2016-06-09 08:12:34 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@172.30.200.206> | 2016-06-09 08:12:34 +0000 |
commit | 4bc079a2664f9a407e332291f34d174625a9d5ea (patch) | |
tree | 7481cd5d0a9b3ce37c44c797a1e0d39881221cbe /keystone-moon/keystone/resource/core.py | |
parent | 2f179c5790fbbf6144205d3c6e5089e6eb5f048a (diff) | |
parent | 2e7b4f2027a1147ca28301e4f88adf8274b39a1f (diff) |
Merge "Update Keystone core to Mitaka."
Diffstat (limited to 'keystone-moon/keystone/resource/core.py')
-rw-r--r-- | keystone-moon/keystone/resource/core.py | 1321 |
1 files changed, 1032 insertions, 289 deletions
diff --git a/keystone-moon/keystone/resource/core.py b/keystone-moon/keystone/resource/core.py index 6891c572..f8d72e91 100644 --- a/keystone-moon/keystone/resource/core.py +++ b/keystone-moon/keystone/resource/core.py @@ -13,16 +13,20 @@ """Main entry point into the Resource service.""" import abc +import copy from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils import six +from keystone import assignment from keystone.common import cache from keystone.common import clean from keystone.common import dependency from keystone.common import driver_hints from keystone.common import manager +from keystone.common import utils from keystone import exception from keystone.i18n import _, _LE, _LW from keystone import notifications @@ -30,18 +34,27 @@ from keystone import notifications CONF = cfg.CONF LOG = log.getLogger(__name__) -MEMOIZE = cache.get_memoization_decorator(section='resource') +MEMOIZE = cache.get_memoization_decorator(group='resource') def calc_default_domain(): return {'description': - (u'Owns users and tenants (i.e. projects)' - ' available on Identity API v2.'), + (u'The default domain'), 'enabled': True, 'id': CONF.identity.default_domain_id, 'name': u'Default'} +def _get_project_from_domain(domain_ref): + """Creates a project ref from the provided domain ref.""" + project_ref = domain_ref.copy() + project_ref['is_domain'] = True + project_ref['domain_id'] = None + project_ref['parent_id'] = None + + return project_ref + + @dependency.provider('resource_api') @dependency.requires('assignment_api', 'credential_api', 'domain_config_api', 'identity_api', 'revoke_api') @@ -69,48 +82,171 @@ class Manager(manager.Manager): super(Manager, self).__init__(resource_driver) + # Make sure it is a driver version we support, and if it is a legacy + # driver, then wrap it. + if isinstance(self.driver, ResourceDriverV8): + self.driver = V9ResourceWrapperForV8Driver(self.driver) + elif not isinstance(self.driver, ResourceDriverV9): + raise exception.UnsupportedDriverVersion(driver=resource_driver) + def _get_hierarchy_depth(self, parents_list): return len(parents_list) + 1 def _assert_max_hierarchy_depth(self, project_id, parents_list=None): if parents_list is None: parents_list = self.list_project_parents(project_id) - max_depth = CONF.max_project_tree_depth + # NOTE(henry-nash): In upgrading to a scenario where domains are + # represented as projects acting as domains, we will effectively + # increase the depth of any existing project hierarchy by one. To avoid + # pushing any existing hierarchies over the limit, we add one to the + # maximum depth allowed, as specified in the configuration file. + max_depth = CONF.max_project_tree_depth + 1 if self._get_hierarchy_depth(parents_list) > max_depth: - raise exception.ForbiddenAction( - action=_('max hierarchy depth reached for ' - '%s branch.') % project_id) - - def create_project(self, tenant_id, tenant, initiator=None): - tenant = tenant.copy() - tenant.setdefault('enabled', True) - tenant['enabled'] = clean.project_enabled(tenant['enabled']) - tenant.setdefault('description', '') - tenant.setdefault('parent_id', None) - tenant.setdefault('is_domain', False) - - self.get_domain(tenant.get('domain_id')) - if tenant.get('parent_id') is not None: - parent_ref = self.get_project(tenant.get('parent_id')) - parents_list = self.list_project_parents(parent_ref['id']) + raise exception.ForbiddenNotSecurity( + _('Max hierarchy depth reached for %s branch.') % project_id) + + def _assert_is_domain_project_constraints(self, project_ref): + """Enforces specific constraints of projects that act as domains + + Called when is_domain is true, this method ensures that: + + * multiple domains are enabled + * the project name is not the reserved name for a federated domain + * the project is a root project + + :raises keystone.exception.ValidationError: If one of the constraints + was not satisfied. + """ + if (not self.identity_api.multiple_domains_supported and + project_ref['id'] != CONF.identity.default_domain_id): + raise exception.ValidationError( + message=_('Multiple domains are not supported')) + + self.assert_domain_not_federated(project_ref['id'], project_ref) + + if project_ref['parent_id']: + raise exception.ValidationError( + message=_('only root projects are allowed to act as ' + 'domains.')) + + def _assert_regular_project_constraints(self, project_ref): + """Enforces regular project hierarchy constraints + + Called when is_domain is false. The project must contain a valid + domain_id and parent_id. The goal of this method is to check + that the domain_id specified is consistent with the domain of its + parent. + + :raises keystone.exception.ValidationError: If one of the constraints + was not satisfied. + :raises keystone.exception.DomainNotFound: In case the domain is not + found. + """ + # Ensure domain_id is valid, and by inference will not be None. + domain = self.get_domain(project_ref['domain_id']) + parent_ref = self.get_project(project_ref['parent_id']) + + if parent_ref['is_domain']: + if parent_ref['id'] != domain['id']: + raise exception.ValidationError( + message=_('Cannot create project, since its parent ' + '(%(domain_id)s) is acting as a domain, ' + 'but project\'s specified parent_id ' + '(%(parent_id)s) does not match ' + 'this domain_id.') + % {'domain_id': domain['id'], + 'parent_id': parent_ref['id']}) + else: + parent_domain_id = parent_ref.get('domain_id') + if parent_domain_id != domain['id']: + raise exception.ValidationError( + message=_('Cannot create project, since it specifies ' + 'its owner as domain %(domain_id)s, but ' + 'specifies a parent in a different domain ' + '(%(parent_domain_id)s).') + % {'domain_id': domain['id'], + 'parent_domain_id': parent_domain_id}) + + def _enforce_project_constraints(self, project_ref): + if project_ref.get('is_domain'): + self._assert_is_domain_project_constraints(project_ref) + else: + self._assert_regular_project_constraints(project_ref) + # The whole hierarchy (upwards) must be enabled + parent_id = project_ref['parent_id'] + parents_list = self.list_project_parents(parent_id) + parent_ref = self.get_project(parent_id) parents_list.append(parent_ref) for ref in parents_list: - if ref.get('domain_id') != tenant.get('domain_id'): - raise exception.ValidationError( - message=_('cannot create a project within a different ' - 'domain than its parents.')) if not ref.get('enabled', True): raise exception.ValidationError( message=_('cannot create a project in a ' 'branch containing a disabled ' 'project: %s') % ref['id']) - self._assert_max_hierarchy_depth(tenant.get('parent_id'), + + self._assert_max_hierarchy_depth(project_ref.get('parent_id'), parents_list) - ret = self.driver.create_project(tenant_id, tenant) - notifications.Audit.created(self._PROJECT, tenant_id, initiator) + def _raise_reserved_character_exception(self, entity_type, name): + msg = _('%(entity)s name cannot contain the following reserved ' + 'characters: %(chars)s') + raise exception.ValidationError( + message=msg % { + 'entity': entity_type, + 'chars': utils.list_url_unsafe_chars(name) + }) + + def _generate_project_name_conflict_msg(self, project): + if project['is_domain']: + return _('it is not permitted to have two projects ' + 'acting as domains with the same name: %s' + ) % project['name'] + else: + return _('it is not permitted to have two projects ' + 'within a domain with the same name : %s' + ) % project['name'] + + def create_project(self, project_id, project, initiator=None): + project = project.copy() + + if (CONF.resource.project_name_url_safe != 'off' and + utils.is_not_url_safe(project['name'])): + self._raise_reserved_character_exception('Project', + project['name']) + + project.setdefault('enabled', True) + project['enabled'] = clean.project_enabled(project['enabled']) + project.setdefault('description', '') + + # For regular projects, the controller will ensure we have a valid + # domain_id. For projects acting as a domain, the project_id + # is, effectively, the domain_id - and for such projects we don't + # bother to store a copy of it in the domain_id attribute. + project.setdefault('domain_id', None) + project.setdefault('parent_id', None) + if not project['parent_id']: + project['parent_id'] = project['domain_id'] + project.setdefault('is_domain', False) + + self._enforce_project_constraints(project) + + # We leave enforcing name uniqueness to the underlying driver (instead + # of doing it in code in the project_constraints above), so as to allow + # this check to be done at the storage level, avoiding race conditions + # in multi-process keystone configurations. + try: + ret = self.driver.create_project(project_id, project) + except exception.Conflict: + raise exception.Conflict( + type='project', + details=self._generate_project_name_conflict_msg(project)) + + if project.get('is_domain'): + notifications.Audit.created(self._DOMAIN, project_id, initiator) + else: + notifications.Audit.created(self._PROJECT, project_id, initiator) if MEMOIZE.should_cache(ret): - self.get_project.set(ret, self, tenant_id) + self.get_project.set(ret, self, project_id) self.get_project_by_name.set(ret, self, ret['name'], ret['domain_id']) return ret @@ -153,95 +289,257 @@ class Manager(manager.Manager): """ if project is None: project = self.get_project(project_id) - self.assert_domain_enabled(domain_id=project['domain_id']) + # If it's a regular project (i.e. it has a domain_id), we need to make + # sure the domain itself is not disabled + if project['domain_id']: + self.assert_domain_enabled(domain_id=project['domain_id']) if not project.get('enabled', True): raise AssertionError(_('Project is disabled: %s') % project_id) - @notifications.disabled(_PROJECT, public=False) - def _disable_project(self, project_id): - """Emit a notification to the callback system project is been disabled. - - This method, and associated callback listeners, removes the need for - making direct calls to other managers to take action (e.g. revoking - project scoped tokens) when a project is disabled. - - :param project_id: project identifier - :type project_id: string - """ - pass - def _assert_all_parents_are_enabled(self, project_id): parents_list = self.list_project_parents(project_id) for project in parents_list: if not project.get('enabled', True): - raise exception.ForbiddenAction( - action=_('cannot enable project %s since it has ' - 'disabled parents') % project_id) - - def _assert_whole_subtree_is_disabled(self, project_id): - subtree_list = self.list_projects_in_subtree(project_id) - for ref in subtree_list: - if ref.get('enabled', True): - raise exception.ForbiddenAction( - action=_('cannot disable project %s since ' - 'its subtree contains enabled ' - 'projects') % project_id) - - def update_project(self, tenant_id, tenant, initiator=None): - original_tenant = self.driver.get_project(tenant_id) - tenant = tenant.copy() - - parent_id = original_tenant.get('parent_id') - if 'parent_id' in tenant and tenant.get('parent_id') != parent_id: - raise exception.ForbiddenAction( - action=_('Update of `parent_id` is not allowed.')) - - if ('is_domain' in tenant and - tenant['is_domain'] != original_tenant['is_domain']): + raise exception.ForbiddenNotSecurity( + _('Cannot enable project %s since it has disabled ' + 'parents') % project_id) + + def _check_whole_subtree_is_disabled(self, project_id, subtree_list=None): + if not subtree_list: + subtree_list = self.list_projects_in_subtree(project_id) + subtree_enabled = [ref.get('enabled', True) for ref in subtree_list] + return (not any(subtree_enabled)) + + def _update_project(self, project_id, project, initiator=None, + cascade=False): + # Use the driver directly to prevent using old cached value. + original_project = self.driver.get_project(project_id) + project = project.copy() + + if original_project['is_domain']: + domain = self._get_domain_from_project(original_project) + self.assert_domain_not_federated(project_id, domain) + if 'enabled' in domain: + domain['enabled'] = clean.domain_enabled(domain['enabled']) + url_safe_option = CONF.resource.domain_name_url_safe + exception_entity = 'Domain' + else: + url_safe_option = CONF.resource.project_name_url_safe + exception_entity = 'Project' + + if (url_safe_option != 'off' and + 'name' in project and + project['name'] != original_project['name'] and + utils.is_not_url_safe(project['name'])): + self._raise_reserved_character_exception(exception_entity, + project['name']) + + parent_id = original_project.get('parent_id') + if 'parent_id' in project and project.get('parent_id') != parent_id: + raise exception.ForbiddenNotSecurity( + _('Update of `parent_id` is not allowed.')) + + if ('is_domain' in project and + project['is_domain'] != original_project['is_domain']): raise exception.ValidationError( message=_('Update of `is_domain` is not allowed.')) - if 'enabled' in tenant: - tenant['enabled'] = clean.project_enabled(tenant['enabled']) - - # NOTE(rodrigods): for the current implementation we only allow to - # disable a project if all projects below it in the hierarchy are - # already disabled. This also means that we can not enable a - # project that has disabled parents. - original_tenant_enabled = original_tenant.get('enabled', True) - tenant_enabled = tenant.get('enabled', True) - if not original_tenant_enabled and tenant_enabled: - self._assert_all_parents_are_enabled(tenant_id) - if original_tenant_enabled and not tenant_enabled: - self._assert_whole_subtree_is_disabled(tenant_id) - self._disable_project(tenant_id) - - ret = self.driver.update_project(tenant_id, tenant) - notifications.Audit.updated(self._PROJECT, tenant_id, initiator) - self.get_project.invalidate(self, tenant_id) - self.get_project_by_name.invalidate(self, original_tenant['name'], - original_tenant['domain_id']) + update_domain = ('domain_id' in project and + project['domain_id'] != original_project['domain_id']) + + # NOTE(htruta): Even if we are allowing domain_ids to be + # modified (i.e. 'domain_id_immutable' is set False), + # a project.domain_id can only be updated for root projects + # that have no children. The update of domain_id of a project in + # the middle of the hierarchy creates an inconsistent project + # hierarchy. + if update_domain: + if original_project['is_domain']: + raise exception.ValidationError( + message=_('Update of domain_id of projects acting as ' + 'domains is not allowed.')) + parent_project = ( + self.driver.get_project(original_project['parent_id'])) + is_root_project = parent_project['is_domain'] + if not is_root_project: + raise exception.ValidationError( + message=_('Update of domain_id is only allowed for ' + 'root projects.')) + subtree_list = self.list_projects_in_subtree(project_id) + if subtree_list: + raise exception.ValidationError( + message=_('Cannot update domain_id of a project that ' + 'has children.')) + versionutils.report_deprecated_feature( + LOG, + _('update of domain_id is deprecated as of Mitaka ' + 'and will be removed in O.') + ) + + if 'enabled' in project: + project['enabled'] = clean.project_enabled(project['enabled']) + + original_project_enabled = original_project.get('enabled', True) + project_enabled = project.get('enabled', True) + if not original_project_enabled and project_enabled: + self._assert_all_parents_are_enabled(project_id) + if original_project_enabled and not project_enabled: + # NOTE(htruta): In order to disable a regular project, all its + # children must already be disabled. However, to keep + # compatibility with the existing domain behaviour, we allow a + # project acting as a domain to be disabled irrespective of the + # state of its children. Disabling a project acting as domain + # effectively disables its children. + if (not original_project.get('is_domain') and not cascade and not + self._check_whole_subtree_is_disabled(project_id)): + raise exception.ForbiddenNotSecurity( + _('Cannot disable project %(project_id)s since its ' + 'subtree contains enabled projects.') + % {'project_id': project_id}) + + notifications.Audit.disabled(self._PROJECT, project_id, + public=False) + if cascade: + self._only_allow_enabled_to_update_cascade(project, + original_project) + self._update_project_enabled_cascade(project_id, project_enabled) + + try: + project['is_domain'] = (project.get('is_domain') or + original_project['is_domain']) + ret = self.driver.update_project(project_id, project) + except exception.Conflict: + raise exception.Conflict( + type='project', + details=self._generate_project_name_conflict_msg(project)) + + notifications.Audit.updated(self._PROJECT, project_id, initiator) + if original_project['is_domain']: + notifications.Audit.updated(self._DOMAIN, project_id, initiator) + # If the domain is being disabled, issue the disable notification + # as well + if original_project_enabled and not project_enabled: + notifications.Audit.disabled(self._DOMAIN, project_id, + public=False) + + self.get_project.invalidate(self, project_id) + self.get_project_by_name.invalidate(self, original_project['name'], + original_project['domain_id']) + + if ('domain_id' in project and + project['domain_id'] != original_project['domain_id']): + # If the project's domain_id has been updated, invalidate user + # role assignments cache region, as it may be caching inherited + # assignments from the old domain to the specified project + assignment.COMPUTED_ASSIGNMENTS_REGION.invalidate() + return ret - def delete_project(self, tenant_id, initiator=None): - if not self.driver.is_leaf_project(tenant_id): - raise exception.ForbiddenAction( - action=_('cannot delete the project %s since it is not ' - 'a leaf in the hierarchy.') % tenant_id) + def _only_allow_enabled_to_update_cascade(self, project, original_project): + for attr in project: + if attr != 'enabled': + if project.get(attr) != original_project.get(attr): + raise exception.ValidationError( + message=_('Cascade update is only allowed for ' + 'enabled attribute.')) + + def _update_project_enabled_cascade(self, project_id, enabled): + subtree = self.list_projects_in_subtree(project_id) + # Update enabled only if different from original value + subtree_to_update = [child for child in subtree + if child['enabled'] != enabled] + for child in subtree_to_update: + child['enabled'] = enabled + + if not enabled: + # Does not in fact disable the project, only emits a + # notification that it was disabled. The actual disablement + # is done in the next line. + notifications.Audit.disabled(self._PROJECT, child['id'], + public=False) + + self.driver.update_project(child['id'], child) + + def update_project(self, project_id, project, initiator=None, + cascade=False): + ret = self._update_project(project_id, project, initiator, cascade) + if ret['is_domain']: + self.get_domain.invalidate(self, project_id) + self.get_domain_by_name.invalidate(self, ret['name']) - project = self.driver.get_project(tenant_id) + return ret + + def _pre_delete_cleanup_project(self, project_id, project, initiator=None): project_user_ids = ( - self.assignment_api.list_user_ids_for_project(tenant_id)) + self.assignment_api.list_user_ids_for_project(project_id)) for user_id in project_user_ids: - payload = {'user_id': user_id, 'project_id': tenant_id} - self._emit_invalidate_user_project_tokens_notification(payload) - ret = self.driver.delete_project(tenant_id) - self.assignment_api.delete_project_assignments(tenant_id) - self.get_project.invalidate(self, tenant_id) + payload = {'user_id': user_id, 'project_id': project_id} + notifications.Audit.internal( + notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE, + payload + ) + + def _post_delete_cleanup_project(self, project_id, project, + initiator=None): + self.assignment_api.delete_project_assignments(project_id) + self.get_project.invalidate(self, project_id) self.get_project_by_name.invalidate(self, project['name'], project['domain_id']) - self.credential_api.delete_credentials_for_project(tenant_id) - notifications.Audit.deleted(self._PROJECT, tenant_id, initiator) + self.credential_api.delete_credentials_for_project(project_id) + notifications.Audit.deleted(self._PROJECT, project_id, initiator) + # Invalidate user role assignments cache region, as it may + # be caching role assignments where the target is + # the specified project + assignment.COMPUTED_ASSIGNMENTS_REGION.invalidate() + + def delete_project(self, project_id, initiator=None, cascade=False): + project = self.driver.get_project(project_id) + if project.get('is_domain'): + self.delete_domain(project_id, initiator) + else: + self._delete_project(project_id, initiator, cascade) + + def _delete_project(self, project_id, initiator=None, cascade=False): + # Use the driver directly to prevent using old cached value. + project = self.driver.get_project(project_id) + if project['is_domain'] and project['enabled']: + raise exception.ValidationError( + message=_('cannot delete an enabled project acting as a ' + 'domain. Please disable the project %s first.') + % project.get('id')) + + if not self.is_leaf_project(project_id) and not cascade: + raise exception.ForbiddenNotSecurity( + _('Cannot delete the project %s since it is not a leaf in the ' + 'hierarchy. Use the cascade option if you want to delete a ' + 'whole subtree.') + % project_id) + + if cascade: + # Getting reversed project's subtrees list, i.e. from the leaves + # to the root, so we do not break parent_id FK. + subtree_list = self.list_projects_in_subtree(project_id) + subtree_list.reverse() + if not self._check_whole_subtree_is_disabled( + project_id, subtree_list=subtree_list): + raise exception.ForbiddenNotSecurity( + _('Cannot delete project %(project_id)s since its subtree ' + 'contains enabled projects.') + % {'project_id': project_id}) + + project_list = subtree_list + [project] + projects_ids = [x['id'] for x in project_list] + + for prj in project_list: + self._pre_delete_cleanup_project(prj['id'], prj, initiator) + ret = self.driver.delete_projects_from_ids(projects_ids) + for prj in project_list: + self._post_delete_cleanup_project(prj['id'], prj, initiator) + else: + self._pre_delete_cleanup_project(project_id, project, initiator) + ret = self.driver.delete_project(project_id) + self._post_delete_cleanup_project(project_id, project, initiator) + return ret def _filter_projects_list(self, projects_list, user_id): @@ -378,85 +676,127 @@ class Manager(manager.Manager): project_id, _projects_indexed_by_parent(subtree_list)) return subtree_as_ids + def list_domains_from_ids(self, domain_ids): + """List domains for the provided list of ids. + + :param domain_ids: list of ids + + :returns: a list of domain_refs. + + This method is used internally by the assignment manager to bulk read + a set of domains given their ids. + + """ + # Retrieve the projects acting as domains get their correspondent + # domains + projects = self.list_projects_from_ids(domain_ids) + domains = [self._get_domain_from_project(project) + for project in projects] + + return domains + @MEMOIZE def get_domain(self, domain_id): - return self.driver.get_domain(domain_id) + try: + # Retrieve the corresponding project that acts as a domain + project = self.driver.get_project(domain_id) + except exception.ProjectNotFound: + raise exception.DomainNotFound(domain_id=domain_id) + + # Return its correspondent domain + return self._get_domain_from_project(project) @MEMOIZE def get_domain_by_name(self, domain_name): - return self.driver.get_domain_by_name(domain_name) - - def create_domain(self, domain_id, domain, initiator=None): - if (not self.identity_api.multiple_domains_supported and - domain_id != CONF.identity.default_domain_id): - raise exception.Forbidden(_('Multiple domains are not supported')) - self.assert_domain_not_federated(domain_id, domain) - domain.setdefault('enabled', True) - domain['enabled'] = clean.domain_enabled(domain['enabled']) - ret = self.driver.create_domain(domain_id, domain) + try: + # Retrieve the corresponding project that acts as a domain + project = self.driver.get_project_by_name(domain_name, + domain_id=None) + except exception.ProjectNotFound: + raise exception.DomainNotFound(domain_id=domain_name) - notifications.Audit.created(self._DOMAIN, domain_id, initiator) + # Return its correspondent domain + return self._get_domain_from_project(project) - if MEMOIZE.should_cache(ret): - self.get_domain.set(ret, self, domain_id) - self.get_domain_by_name.set(ret, self, ret['name']) - return ret + def _get_domain_from_project(self, project_ref): + """Creates a domain ref from a project ref. - @manager.response_truncated - def list_domains(self, hints=None): - return self.driver.list_domains(hints or driver_hints.Hints()) + Based on the provided project ref, create a domain ref, so that the + result can be returned in response to a domain API call. + """ + if not project_ref['is_domain']: + LOG.error(_LE('Asked to convert a non-domain project into a ' + 'domain - Domain: %(domain_id)s, Project ID: ' + '%(id)s, Project Name: %(project_name)s'), + {'domain_id': project_ref['domain_id'], + 'id': project_ref['id'], + 'project_name': project_ref['name']}) + raise exception.DomainNotFound(domain_id=project_ref['id']) + + domain_ref = project_ref.copy() + # As well as the project specific attributes that we need to remove, + # there is an old compatibility issue in that update project (as well + # as extracting an extra attributes), also includes a copy of the + # actual extra dict as well - something that update domain does not do. + for k in ['parent_id', 'domain_id', 'is_domain', 'extra']: + domain_ref.pop(k, None) + + return domain_ref - @notifications.disabled(_DOMAIN, public=False) - def _disable_domain(self, domain_id): - """Emit a notification to the callback system domain is been disabled. + def create_domain(self, domain_id, domain, initiator=None): + if (CONF.resource.domain_name_url_safe != 'off' and + utils.is_not_url_safe(domain['name'])): + self._raise_reserved_character_exception('Domain', domain['name']) + project_from_domain = _get_project_from_domain(domain) + is_domain_project = self.create_project( + domain_id, project_from_domain, initiator) - This method, and associated callback listeners, removes the need for - making direct calls to other managers to take action (e.g. revoking - domain scoped tokens) when a domain is disabled. + return self._get_domain_from_project(is_domain_project) - :param domain_id: domain identifier - :type domain_id: string - """ - pass + @manager.response_truncated + def list_domains(self, hints=None): + projects = self.list_projects_acting_as_domain(hints) + domains = [self._get_domain_from_project(project) + for project in projects] + return domains def update_domain(self, domain_id, domain, initiator=None): + # TODO(henry-nash): We shouldn't have to check for the federated domain + # here as well as _update_project, but currently our tests assume the + # checks are done in a specific order. The tests should be refactored. self.assert_domain_not_federated(domain_id, domain) - original_domain = self.driver.get_domain(domain_id) - if 'enabled' in domain: - domain['enabled'] = clean.domain_enabled(domain['enabled']) - ret = self.driver.update_domain(domain_id, domain) - notifications.Audit.updated(self._DOMAIN, domain_id, initiator) - # disable owned users & projects when the API user specifically set - # enabled=False - if (original_domain.get('enabled', True) and - not domain.get('enabled', True)): - notifications.Audit.disabled(self._DOMAIN, domain_id, initiator, - public=False) + project = _get_project_from_domain(domain) + try: + original_domain = self.driver.get_project(domain_id) + project = self._update_project(domain_id, project, initiator) + except exception.ProjectNotFound: + raise exception.DomainNotFound(domain_id=domain_id) + domain_from_project = self._get_domain_from_project(project) self.get_domain.invalidate(self, domain_id) self.get_domain_by_name.invalidate(self, original_domain['name']) - return ret - def delete_domain(self, domain_id, initiator=None): - # explicitly forbid deleting the default domain (this should be a - # carefully orchestrated manual process involving configuration - # changes, etc) - if domain_id == CONF.identity.default_domain_id: - raise exception.ForbiddenAction(action=_('delete the default ' - 'domain')) + return domain_from_project - domain = self.driver.get_domain(domain_id) + def delete_domain(self, domain_id, initiator=None): + # Use the driver directly to get the project that acts as a domain and + # prevent using old cached value. + try: + domain = self.driver.get_project(domain_id) + except exception.ProjectNotFound: + raise exception.DomainNotFound(domain_id=domain_id) # To help avoid inadvertent deletes, we insist that the domain # has been previously disabled. This also prevents a user deleting # their own domain since, once it is disabled, they won't be able # to get a valid token to issue this delete. if domain['enabled']: - raise exception.ForbiddenAction( - action=_('cannot delete a domain that is enabled, ' - 'please disable it first.')) + raise exception.ForbiddenNotSecurity( + _('Cannot delete a domain that is enabled, please disable it ' + 'first.')) self._delete_domain_contents(domain_id) + self._delete_project(domain_id, initiator) # Delete any database stored domain config self.domain_config_api.delete_config_options(domain_id) self.domain_config_api.delete_config_options(domain_id, sensitive=True) @@ -468,11 +808,14 @@ class Manager(manager.Manager): # other domains - so we should delete these here by making a call # to the backend to delete all assignments for this domain. # (see Bug #1277847) - self.driver.delete_domain(domain_id) notifications.Audit.deleted(self._DOMAIN, domain_id, initiator) self.get_domain.invalidate(self, domain_id) self.get_domain_by_name.invalidate(self, domain['name']) + # Invalidate user role assignments cache region, as it may be caching + # role assignments where the target is the specified domain + assignment.COMPUTED_ASSIGNMENTS_REGION.invalidate() + def _delete_domain_contents(self, domain_id): """Delete the contents of a domain. @@ -483,7 +826,6 @@ class Manager(manager.Manager): associated with them as well as revoking any relevant tokens. """ - def _delete_projects(project, projects, examined): if project['id'] in examined: msg = _LE('Circular reference or a repeated entry found ' @@ -498,7 +840,7 @@ class Manager(manager.Manager): _delete_projects(proj, projects, examined) try: - self.delete_project(project['id']) + self.delete_project(project['id'], initiator=None) except exception.ProjectNotFound: LOG.debug(('Project %(projectid)s not found when ' 'deleting domain contents for %(domainid)s, ' @@ -509,7 +851,7 @@ class Manager(manager.Manager): proj_refs = self.list_projects_in_domain(domain_id) # Deleting projects recursively - roots = [x for x in proj_refs if x.get('parent_id') is None] + roots = [x for x in proj_refs if x.get('parent_id') == domain_id] examined = set() for project in roots: _delete_projects(project, proj_refs, examined) @@ -524,29 +866,258 @@ class Manager(manager.Manager): def list_projects_in_domain(self, domain_id): return self.driver.list_projects_in_domain(domain_id) + def list_projects_acting_as_domain(self, hints=None): + return self.driver.list_projects_acting_as_domain( + hints or driver_hints.Hints()) + @MEMOIZE def get_project(self, project_id): return self.driver.get_project(project_id) @MEMOIZE - def get_project_by_name(self, tenant_name, domain_id): - return self.driver.get_project_by_name(tenant_name, domain_id) + def get_project_by_name(self, project_name, domain_id): + return self.driver.get_project_by_name(project_name, domain_id) - @notifications.internal( - notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE) - def _emit_invalidate_user_project_tokens_notification(self, payload): - # This notification's payload is a dict of user_id and - # project_id so the token provider can invalidate the tokens - # from persistence if persistence is enabled. - pass + def ensure_default_domain_exists(self): + """Creates the default domain if it doesn't exist. + + This is only used for the v2 API and can go away when V2 does. + + """ + try: + default_domain_attrs = { + 'name': 'Default', + 'id': CONF.identity.default_domain_id, + 'description': 'Domain created automatically to support V2.0 ' + 'operations.', + } + self.create_domain(CONF.identity.default_domain_id, + default_domain_attrs) + LOG.warning(_LW( + 'The default domain was created automatically to contain V2 ' + 'resources. This is deprecated in the M release and will not ' + 'be supported in the O release. Create the default domain ' + 'manually or use the keystone-manage bootstrap command.')) + except exception.Conflict: + LOG.debug('The default domain already exists.') + except Exception: + LOG.error(_LE('Failed to create the default domain.')) + raise + + +# The ResourceDriverBase class is the set of driver methods from earlier +# drivers that we still support, that have not been removed or modified. This +# class is then used to created the augmented V8 and V9 version abstract driver +# classes, without having to duplicate a lot of abstract method signatures. +# If you remove a method from V9, then move the abstract methods from this Base +# class to the V8 class. Do not modify any of the method signatures in the Base +# class - changes should only be made in the V8 and subsequent classes. + +# Starting with V9, some drivers use a special value to represent a domain_id +# of None. See comment in Project class of resource/backends/sql.py for more +# details. +NULL_DOMAIN_ID = '<<keystone.domain.root>>' @six.add_metaclass(abc.ABCMeta) -class ResourceDriverV8(object): +class ResourceDriverBase(object): def _get_list_limit(self): return CONF.resource.list_limit or CONF.list_limit + # project crud + @abc.abstractmethod + def list_projects(self, hints): + """List projects in the system. + + :param hints: filter hints which the driver should + implement if at all possible. + + :returns: a list of project_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_projects_from_ids(self, project_ids): + """List projects for the provided list of ids. + + :param project_ids: list of ids + + :returns: a list of project_refs. + + This method is used internally by the assignment manager to bulk read + a set of projects given their ids. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_project_ids_from_domain_ids(self, domain_ids): + """List project ids for the provided list of domain ids. + + :param domain_ids: list of domain ids + + :returns: a list of project ids owned by the specified domain ids. + + This method is used internally by the assignment manager to bulk read + a set of project ids given a list of domain ids. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_projects_in_domain(self, domain_id): + """List projects in the domain. + + :param domain_id: the driver MUST only return projects + within this domain. + + :returns: a list of project_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_project(self, project_id): + """Get a project by ID. + + :returns: project_ref + :raises keystone.exception.ProjectNotFound: if project_id does not + exist + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_project(self, project_id, project): + """Updates an existing project. + + :raises keystone.exception.ProjectNotFound: if project_id does not + exist + :raises keystone.exception.Conflict: if project name already exists + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_project(self, project_id): + """Deletes an existing project. + + :raises keystone.exception.ProjectNotFound: if project_id does not + exist + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_project_parents(self, project_id): + """List all parents from a project by its ID. + + :param project_id: the driver will list the parents of this + project. + + :returns: a list of project_refs or an empty list. + :raises keystone.exception.ProjectNotFound: if project_id does not + exist + + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def list_projects_in_subtree(self, project_id): + """List all projects in the subtree of a given project. + + :param project_id: the driver will get the subtree under + this project. + + :returns: a list of project_refs or an empty list + :raises keystone.exception.ProjectNotFound: if project_id does not + exist + + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def is_leaf_project(self, project_id): + """Checks if a project is a leaf in the hierarchy. + + :param project_id: the driver will check if this project + is a leaf in the hierarchy. + + :raises keystone.exception.ProjectNotFound: if project_id does not + exist + + """ + raise exception.NotImplemented() + + def _validate_default_domain(self, ref): + """Validate that either the default domain or nothing is specified. + + Also removes the domain from the ref so that LDAP doesn't have to + persist the attribute. + + """ + ref = ref.copy() + domain_id = ref.pop('domain_id', CONF.identity.default_domain_id) + self._validate_default_domain_id(domain_id) + return ref + + def _validate_default_domain_id(self, domain_id): + """Validate that the domain ID belongs to the default domain.""" + if domain_id != CONF.identity.default_domain_id: + raise exception.DomainNotFound(domain_id=domain_id) + + +class ResourceDriverV8(ResourceDriverBase): + """Removed or redefined methods from V8. + + Move the abstract methods of any methods removed or modified in later + versions of the driver from ResourceDriverBase to here. We maintain this + so that legacy drivers, which will be a subclass of ResourceDriverV8, can + still reference them. + + """ + + @abc.abstractmethod + def create_project(self, tenant_id, tenant): + """Creates a new project. + + :param tenant_id: This parameter can be ignored. + :param dict tenant: The new project + + Project schema:: + + type: object + properties: + id: + type: string + name: + type: string + domain_id: + type: string + description: + type: string + enabled: + type: boolean + parent_id: + type: string + is_domain: + type: boolean + required: [id, name, domain_id] + additionalProperties: true + + If project doesn't match the schema the behavior is undefined. + + The driver can impose requirements such as the maximum length of a + field. If these requirements are not met the behavior is undefined. + + :raises keystone.exception.Conflict: if the project id already exists + or the name already exists for the domain_id. + + """ + raise exception.NotImplemented() # pragma: no cover + @abc.abstractmethod def get_project_by_name(self, tenant_name, domain_id): """Get a tenant by name. @@ -558,6 +1129,21 @@ class ResourceDriverV8(object): """ raise exception.NotImplemented() # pragma: no cover + # Domain management functions for backends that only allow a single + # domain. Although we no longer use this, a custom legacy driver might + # have made use of it, so keep it here in case. + def _set_default_domain(self, ref): + """If the domain ID has not been set, set it to the default.""" + if isinstance(ref, dict): + if 'domain_id' not in ref: + ref = ref.copy() + ref['domain_id'] = CONF.identity.default_domain_id + return ref + elif isinstance(ref, list): + return [self._set_default_domain(x) for x in ref] + else: + raise ValueError(_('Expected dict or list: %s') % type(ref)) + # domain crud @abc.abstractmethod def create_domain(self, domain_id, domain): @@ -635,182 +1221,288 @@ class ResourceDriverV8(object): """ raise exception.NotImplemented() # pragma: no cover - # project crud - @abc.abstractmethod - def create_project(self, project_id, project): - """Creates a new project. - :raises keystone.exception.Conflict: if project_id or project name - already exists +class ResourceDriverV9(ResourceDriverBase): + """New or redefined methods from V8. - """ - raise exception.NotImplemented() # pragma: no cover + Add any new V9 abstract methods (or those with modified signatures) to + this class. - @abc.abstractmethod - def list_projects(self, hints): - """List projects in the system. + """ - :param hints: filter hints which the driver should - implement if at all possible. + @abc.abstractmethod + def create_project(self, project_id, project): + """Creates a new project. - :returns: a list of project_refs or an empty list. + :param project_id: This parameter can be ignored. + :param dict project: The new project + + Project schema:: + + type: object + properties: + id: + type: string + name: + type: string + domain_id: + type: [string, null] + description: + type: string + enabled: + type: boolean + parent_id: + type: string + is_domain: + type: boolean + required: [id, name, domain_id] + additionalProperties: true + + If the project doesn't match the schema the behavior is undefined. + + The driver can impose requirements such as the maximum length of a + field. If these requirements are not met the behavior is undefined. + + :raises keystone.exception.Conflict: if the project id already exists + or the name already exists for the domain_id. """ raise exception.NotImplemented() # pragma: no cover @abc.abstractmethod - def list_projects_from_ids(self, project_ids): - """List projects for the provided list of ids. - - :param project_ids: list of ids - - :returns: a list of project_refs. + def get_project_by_name(self, project_name, domain_id): + """Get a project by name. - This method is used internally by the assignment manager to bulk read - a set of projects given their ids. + :returns: project_ref + :raises keystone.exception.ProjectNotFound: if a project with the + project_name does not exist within the domain """ raise exception.NotImplemented() # pragma: no cover @abc.abstractmethod - def list_project_ids_from_domain_ids(self, domain_ids): - """List project ids for the provided list of domain ids. - - :param domain_ids: list of domain ids - - :returns: a list of project ids owned by the specified domain ids. - - This method is used internally by the assignment manager to bulk read - a set of project ids given a list of domain ids. - + def delete_projects_from_ids(self, project_ids): + """Deletes a given list of projects. + + Deletes a list of projects. Ensures no project on the list exists + after it is successfully called. If an empty list is provided, + the it is silently ignored. In addition, if a project ID in the list + of project_ids is not found in the backend, no exception is raised, + but a message is logged. """ raise exception.NotImplemented() # pragma: no cover @abc.abstractmethod - def list_projects_in_domain(self, domain_id): - """List projects in the domain. + def list_projects_acting_as_domain(self, hints): + """List all projects acting as domains. - :param domain_id: the driver MUST only return projects - within this domain. + :param hints: filter hints which the driver should + implement if at all possible. :returns: a list of project_refs or an empty list. """ raise exception.NotImplemented() # pragma: no cover - @abc.abstractmethod - def get_project(self, project_id): - """Get a project by ID. - :returns: project_ref - :raises keystone.exception.ProjectNotFound: if project_id does not - exist +class V9ResourceWrapperForV8Driver(ResourceDriverV9): + """Wrapper class to supported a V8 legacy driver. - """ - raise exception.NotImplemented() # pragma: no cover + In order to support legacy drivers without having to make the manager code + driver-version aware, we wrap legacy drivers so that they look like the + latest version. For the various changes made in a new driver, here are the + actions needed in this wrapper: - @abc.abstractmethod - def update_project(self, project_id, project): - """Updates an existing project. + Method removed from new driver - remove the call-through method from this + class, since the manager will no longer be + calling it. + Method signature (or meaning) changed - wrap the old method in a new + signature here, and munge the input + and output parameters accordingly. + New method added to new driver - add a method to implement the new + functionality here if possible. If that is + not possible, then return NotImplemented, + since we do not guarantee to support new + functionality with legacy drivers. - :raises keystone.exception.ProjectNotFound: if project_id does not - exist - :raises keystone.exception.Conflict: if project name already exists + This wrapper contains the following support for newer manager code: - """ - raise exception.NotImplemented() # pragma: no cover + - The current manager code expects domains to be represented as projects + acting as domains, something that may not be possible in a legacy driver. + Hence the wrapper will map any calls for projects acting as a domain back + onto the driver domain methods. The caveat for this, is that this assumes + that there can not be a clash between a project_id and a domain_id, in + which case it may not be able to locate the correct entry. - @abc.abstractmethod - def delete_project(self, project_id): - """Deletes an existing project. + """ - :raises keystone.exception.ProjectNotFound: if project_id does not - exist + @versionutils.deprecated( + as_of=versionutils.deprecated.MITAKA, + what='keystone.resource.ResourceDriverV8', + in_favor_of='keystone.resource.ResourceDriverV9', + remove_in=+2) + def __init__(self, wrapped_driver): + self.driver = wrapped_driver + def _get_domain_from_project(self, project_ref): + """Creates a domain ref from a project ref. + + Based on the provided project ref (or partial ref), creates a + domain ref, so that the result can be passed to the driver + domain methods. """ - raise exception.NotImplemented() # pragma: no cover + domain_ref = project_ref.copy() + for k in ['parent_id', 'domain_id', 'is_domain']: + domain_ref.pop(k, None) + return domain_ref - @abc.abstractmethod - def list_project_parents(self, project_id): - """List all parents from a project by its ID. + def get_project_by_name(self, project_name, domain_id): + if domain_id is None: + try: + domain_ref = self.driver.get_domain_by_name(project_name) + return _get_project_from_domain(domain_ref) + except exception.DomainNotFound: + raise exception.ProjectNotFound(project_id=project_name) + else: + return self.driver.get_project_by_name(project_name, domain_id) - :param project_id: the driver will list the parents of this - project. + def create_project(self, project_id, project): + if project['is_domain']: + new_domain = self._get_domain_from_project(project) + domain_ref = self.driver.create_domain(project_id, new_domain) + return _get_project_from_domain(domain_ref) + else: + return self.driver.create_project(project_id, project) - :returns: a list of project_refs or an empty list. - :raises keystone.exception.ProjectNotFound: if project_id does not - exist + def list_projects(self, hints): + """List projects and/or domains. - """ - raise exception.NotImplemented() + We use the hints filter to determine whether we are listing projects, + domains or both. - @abc.abstractmethod - def list_projects_in_subtree(self, project_id): - """List all projects in the subtree below the hierarchy of the - given project. + If the filter includes domain_id==None, then we should only list + domains (convert to a project acting as a domain) since regular + projcets always have a non-None value for domain_id. - :param project_id: the driver will get the subtree under - this project. + Likewise, if the filter includes domain_id==<non-None value>, then we + should only list projects. - :returns: a list of project_refs or an empty list - :raises keystone.exception.ProjectNotFound: if project_id does not - exist + If there is no domain_id filter, then we need to do a combained listing + of domains and projects, converting domains to projects acting as a + domain. """ - raise exception.NotImplemented() + domain_listing_filter = None + for f in hints.filters: + if (f['name'] == 'domain_id'): + domain_listing_filter = f + + if domain_listing_filter is not None: + if domain_listing_filter['value'] is not None: + proj_list = self.driver.list_projects(hints) + else: + domains = self.driver.list_domains(hints) + proj_list = [_get_project_from_domain(p) for p in domains] + hints.filters.remove(domain_listing_filter) + return proj_list + else: + # No domain_id filter, so combine domains and projects. Although + # we hand any remaining filters into each driver, since each filter + # might need to be carried out more than once, we use copies of the + # filters, allowing the original filters to be passed back up to + # controller level where a final filter will occur. + local_hints = copy.deepcopy(hints) + proj_list = self.driver.list_projects(local_hints) + local_hints = copy.deepcopy(hints) + domains = self.driver.list_domains(local_hints) + for domain in domains: + proj_list.append(_get_project_from_domain(domain)) + return proj_list - @abc.abstractmethod - def is_leaf_project(self, project_id): - """Checks if a project is a leaf in the hierarchy. + def list_projects_from_ids(self, project_ids): + return [self.get_project(id) for id in project_ids] - :param project_id: the driver will check if this project - is a leaf in the hierarchy. + def list_project_ids_from_domain_ids(self, domain_ids): + return self.driver.list_project_ids_from_domain_ids(domain_ids) - :raises keystone.exception.ProjectNotFound: if project_id does not - exist + def list_projects_in_domain(self, domain_id): + return self.driver.list_projects_in_domain(domain_id) - """ - raise exception.NotImplemented() + def get_project(self, project_id): + try: + domain_ref = self.driver.get_domain(project_id) + return _get_project_from_domain(domain_ref) + except exception.DomainNotFound: + return self.driver.get_project(project_id) - # Domain management functions for backends that only allow a single - # domain. Currently, this is only LDAP, but might be used by other - # backends in the future. - def _set_default_domain(self, ref): - """If the domain ID has not been set, set it to the default.""" - if isinstance(ref, dict): - if 'domain_id' not in ref: - ref = ref.copy() - ref['domain_id'] = CONF.identity.default_domain_id - return ref - elif isinstance(ref, list): - return [self._set_default_domain(x) for x in ref] + def _is_domain(self, project_id): + ref = self.get_project(project_id) + return ref.get('is_domain', False) + + def update_project(self, project_id, project): + if self._is_domain(project_id): + update_domain = self._get_domain_from_project(project) + domain_ref = self.driver.update_domain(project_id, update_domain) + return _get_project_from_domain(domain_ref) else: - raise ValueError(_('Expected dict or list: %s') % type(ref)) + return self.driver.update_project(project_id, project) - def _validate_default_domain(self, ref): - """Validate that either the default domain or nothing is specified. + def delete_project(self, project_id): + if self._is_domain(project_id): + try: + self.driver.delete_domain(project_id) + except exception.DomainNotFound: + raise exception.ProjectNotFound(project_id=project_id) + else: + self.driver.delete_project(project_id) - Also removes the domain from the ref so that LDAP doesn't have to - persist the attribute. + def delete_projects_from_ids(self, project_ids): + raise exception.NotImplemented() # pragma: no cover - """ - ref = ref.copy() - domain_id = ref.pop('domain_id', CONF.identity.default_domain_id) - self._validate_default_domain_id(domain_id) - return ref + def list_project_parents(self, project_id): + """List a project's ancestors. - def _validate_default_domain_id(self, domain_id): - """Validate that the domain ID specified belongs to the default domain. + The current manager expects the ancestor tree to end with the project + acting as the domain (since that's now the top of the tree), but a + legacy driver will not have that top project in their projects table, + since it's still in the domain table. Hence we lift the algorithm for + traversing up the tree from the driver to here, so that our version of + get_project() is called, which will fetch the "project" from the right + table. """ - if domain_id != CONF.identity.default_domain_id: - raise exception.DomainNotFound(domain_id=domain_id) + project = self.get_project(project_id) + parents = [] + examined = set() + while project.get('parent_id') is not None: + if project['id'] in examined: + msg = _LE('Circular reference or a repeated ' + 'entry found in projects hierarchy - ' + '%(project_id)s.') + LOG.error(msg, {'project_id': project['id']}) + return + + examined.add(project['id']) + parent_project = self.get_project(project['parent_id']) + parents.append(parent_project) + project = parent_project + return parents + + def list_projects_in_subtree(self, project_id): + return self.driver.list_projects_in_subtree(project_id) + + def is_leaf_project(self, project_id): + return self.driver.is_leaf_project(project_id) + + def list_projects_acting_as_domain(self, hints): + refs = self.driver.list_domains(hints) + return [_get_project_from_domain(p) for p in refs] Driver = manager.create_legacy_driver(ResourceDriverV8) -MEMOIZE_CONFIG = cache.get_memoization_decorator(section='domain_config') +MEMOIZE_CONFIG = cache.get_memoization_decorator(group='domain_config') @dependency.provider('domain_config_api') @@ -829,15 +1521,16 @@ class DomainConfigManager(manager.Manager): driver_namespace = 'keystone.resource.domain_config' whitelisted_options = { - 'identity': ['driver'], + 'identity': ['driver', 'list_limit'], 'ldap': [ 'url', 'user', 'suffix', 'use_dumb_member', 'dumb_member', 'allow_subtree_delete', 'query_scope', 'page_size', 'alias_dereferencing', 'debug_level', 'chase_referrals', 'user_tree_dn', 'user_filter', 'user_objectclass', 'user_id_attribute', 'user_name_attribute', 'user_mail_attribute', - 'user_pass_attribute', 'user_enabled_attribute', - 'user_enabled_invert', 'user_enabled_mask', 'user_enabled_default', + 'user_description_attribute', 'user_pass_attribute', + 'user_enabled_attribute', 'user_enabled_invert', + 'user_enabled_mask', 'user_enabled_default', 'user_attribute_ignore', 'user_default_project_id_attribute', 'user_allow_create', 'user_allow_update', 'user_allow_delete', 'user_enabled_emulation', 'user_enabled_emulation_dn', @@ -928,7 +1621,6 @@ class DomainConfigManager(manager.Manager): def _config_to_list(self, config): """Build whitelisted and sensitive lists for use by backend drivers.""" - whitelisted = [] sensitive = [] for group in config: @@ -1086,7 +1778,6 @@ class DomainConfigManager(manager.Manager): """ def _assert_valid_update(domain_id, config, group=None, option=None): """Ensure the combination of config, group and option is valid.""" - self._assert_valid_config(config) self._assert_valid_group_and_option(group, option) @@ -1145,7 +1836,6 @@ class DomainConfigManager(manager.Manager): def _update_or_create(domain_id, option, sensitive): """Update the option, if it doesn't exist then create it.""" - try: self.create_config_option( domain_id, option['group'], option['option'], @@ -1266,7 +1956,7 @@ class DomainConfigManager(manager.Manager): 'value: %(value)s.') if warning_msg: - LOG.warn(warning_msg % { + LOG.warning(warning_msg % { 'domain': domain_id, 'group': each_whitelisted['group'], 'option': each_whitelisted['option'], @@ -1285,6 +1975,59 @@ class DomainConfigManager(manager.Manager): """ return self._get_config_with_sensitive_info(domain_id) + def get_config_default(self, group=None, option=None): + """Get default config, or partial default config + + :param group: an optional specific group of options + :param option: an optional specific option within the group + + :returns: a dict of group dicts containing the default options, + filtered by group and option if specified + :raises keystone.exception.InvalidDomainConfig: when the config + and group/option parameters specify an option we do not + support (or one that is not whitelisted). + + An example response:: + + { + 'ldap': { + 'url': 'myurl', + 'user_tree_dn': 'OU=myou', + ....}, + 'identity': { + 'driver': 'ldap'} + + } + + """ + def _option_dict(group, option): + group_attr = getattr(CONF, group) + if group_attr is None: + msg = _('Group %s not found in config') % group + raise exception.UnexpectedError(msg) + return {'group': group, 'option': option, + 'value': getattr(group_attr, option)} + + self._assert_valid_group_and_option(group, option) + config_list = [] + if group: + if option: + if option not in self.whitelisted_options[group]: + msg = _('Reading the default for option %(option)s in ' + 'group %(group)s is not supported') % { + 'option': option, 'group': group} + raise exception.InvalidDomainConfig(reason=msg) + config_list.append(_option_dict(group, option)) + else: + for each_option in self.whitelisted_options[group]: + config_list.append(_option_dict(group, each_option)) + else: + for each_group in self.whitelisted_options: + for each_option in self.whitelisted_options[each_group]: + config_list.append(_option_dict(each_group, each_option)) + + return self._list_to_config(config_list, req_option=option) + @six.add_metaclass(abc.ABCMeta) class DomainConfigDriverV8(object): @@ -1394,8 +2137,8 @@ class DomainConfigDriverV8(object): :param type: type of registration :returns: domain_id of who is registered. - :raises: keystone.exception.ConfigRegistrationNotFound: nobody is - registered. + :raises keystone.exception.ConfigRegistrationNotFound: If nobody is + registered. """ raise exception.NotImplemented() # pragma: no cover |