aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/resource/core.py
diff options
context:
space:
mode:
authorRuan HE <ruan.he@orange.com>2016-06-09 08:12:34 +0000
committerGerrit Code Review <gerrit@172.30.200.206>2016-06-09 08:12:34 +0000
commit4bc079a2664f9a407e332291f34d174625a9d5ea (patch)
tree7481cd5d0a9b3ce37c44c797a1e0d39881221cbe /keystone-moon/keystone/resource/core.py
parent2f179c5790fbbf6144205d3c6e5089e6eb5f048a (diff)
parent2e7b4f2027a1147ca28301e4f88adf8274b39a1f (diff)
Merge "Update Keystone core to Mitaka."
Diffstat (limited to 'keystone-moon/keystone/resource/core.py')
-rw-r--r--keystone-moon/keystone/resource/core.py1321
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