keystone  18.0.0
About: OpenStack Keystone (Core Service: Identity) provides an authentication and authorization service for other OpenStack services. Provides a catalog of endpoints for all OpenStack services.
The "Victoria" series (maintained release).
  Fossies Dox: keystone-18.0.0.tar.gz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

core.py
Go to the documentation of this file.
1 # Licensed under the Apache License, Version 2.0 (the "License"); you may
2 # not use this file except in compliance with the License. You may obtain
3 # a copy of the License at
4 #
5 # http://www.apache.org/licenses/LICENSE-2.0
6 #
7 # Unless required by applicable law or agreed to in writing, software
8 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 # License for the specific language governing permissions and limitations
11 # under the License.
12 
13 """Main entry point into the Resource service."""
14 
15 from oslo_log import log
16 
17 from keystone import assignment
18 from keystone.common import cache
19 from keystone.common import driver_hints
20 from keystone.common import manager
21 from keystone.common import provider_api
22 from keystone.common.resource_options import options as ro_opt
23 from keystone.common import utils
24 import keystone.conf
25 from keystone import exception
26 from keystone.i18n import _
27 from keystone import notifications
28 from keystone.resource.backends import base
29 from keystone.token import provider as token_provider
30 
31 CONF = keystone.conf.CONF
32 LOG = log.getLogger(__name__)
33 MEMOIZE = cache.get_memoization_decorator(group='resource')
34 PROVIDERS = provider_api.ProviderAPIs
35 
36 
37 TAG_SEARCH_FILTERS = ('tags', 'tags-any', 'not-tags', 'not-tags-any')
38 
39 
40 class Manager(manager.Manager):
41  """Default pivot point for the Resource backend.
42 
43  See :mod:`keystone.common.manager.Manager` for more details on how this
44  dynamically calls the backend.
45 
46  """
47 
48  driver_namespace = 'keystone.resource'
49  _provides_api = 'resource_api'
50 
51  _DOMAIN = 'domain'
52  _PROJECT = 'project'
53  _PROJECT_TAG = 'project tag'
54 
55  def __init__(self):
56  resource_driver = CONF.resource.driver
57  super(Manager, self).__init__(resource_driver)
58 
59  def _get_hierarchy_depth(self, parents_list):
60  return len(parents_list) + 1
61 
62  def _assert_max_hierarchy_depth(self, project_id, parents_list=None):
63  if parents_list is None:
64  parents_list = self.list_project_parents(project_id)
65  # NOTE(henry-nash): In upgrading to a scenario where domains are
66  # represented as projects acting as domains, we will effectively
67  # increase the depth of any existing project hierarchy by one. To avoid
68  # pushing any existing hierarchies over the limit, we add one to the
69  # maximum depth allowed, as specified in the configuration file.
70  max_depth = CONF.max_project_tree_depth + 1
71 
72  # NOTE(wxy): If the hierarchical limit enforcement model is used, the
73  # project depth should be not greater than the model's limit as well.
74  #
75  # TODO(wxy): Deprecate and remove CONF.max_project_tree_depth, let the
76  # depth check only based on the limit enforcement model.
77  limit_model = PROVIDERS.unified_limit_api.enforcement_model
78  if limit_model.MAX_PROJECT_TREE_DEPTH is not None:
79  max_depth = min(max_depth, limit_model.MAX_PROJECT_TREE_DEPTH + 1)
80  if self._get_hierarchy_depth(parents_list) > max_depth:
82  _('Max hierarchy depth reached for %s branch.') % project_id)
83 
84  def _assert_is_domain_project_constraints(self, project_ref):
85  """Enforce specific constraints of projects that act as domains.
86 
87  Called when is_domain is true, this method ensures that:
88 
89  * multiple domains are enabled
90  * the project name is not the reserved name for a federated domain
91  * the project is a root project
92 
93  :raises keystone.exception.ValidationError: If one of the constraints
94  was not satisfied.
95  """
96  if (not PROVIDERS.identity_api.multiple_domains_supported and
97  project_ref['id'] != CONF.identity.default_domain_id and
98  project_ref['id'] != base.NULL_DOMAIN_ID):
100  message=_('Multiple domains are not supported'))
101 
102  self.assert_domain_not_federated(project_ref['id'], project_ref)
103 
104  if project_ref['parent_id']:
106  message=_('only root projects are allowed to act as '
107  'domains.'))
108 
109  def _assert_regular_project_constraints(self, project_ref):
110  """Enforce regular project hierarchy constraints.
111 
112  Called when is_domain is false. The project must contain a valid
113  domain_id and parent_id. The goal of this method is to check
114  that the domain_id specified is consistent with the domain of its
115  parent.
116 
117  :raises keystone.exception.ValidationError: If one of the constraints
118  was not satisfied.
119  :raises keystone.exception.DomainNotFound: In case the domain is not
120  found.
121  """
122  # Ensure domain_id is valid, and by inference will not be None.
123  domain = self.get_domain(project_ref['domain_id'])
124  parent_ref = self.get_project(project_ref['parent_id'])
125 
126  if parent_ref['is_domain']:
127  if parent_ref['id'] != domain['id']:
129  message=_('Cannot create project, the parent '
130  '(%(parent_id)s) is acting as a domain, '
131  'but this project\'s domain id (%(domain_id)s) '
132  'does not match the parent\'s id.')
133  % {'parent_id': parent_ref['id'],
134  'domain_id': domain['id']})
135  else:
136  parent_domain_id = parent_ref.get('domain_id')
137  if parent_domain_id != domain['id']:
139  message=_('Cannot create project, since it specifies '
140  'its domain_id %(domain_id)s, but '
141  'specifies a parent in a different domain '
142  '(%(parent_domain_id)s).')
143  % {'domain_id': domain['id'],
144  'parent_domain_id': parent_domain_id})
145 
146  def _enforce_project_constraints(self, project_ref):
147  if project_ref.get('is_domain'):
148  self._assert_is_domain_project_constraints(project_ref)
149  else:
150  self._assert_regular_project_constraints(project_ref)
151  # The whole hierarchy (upwards) must be enabled
152  parent_id = project_ref['parent_id']
153  parents_list = self.list_project_parents(parent_id)
154  parent_ref = self.get_project(parent_id)
155  parents_list.append(parent_ref)
156  for ref in parents_list:
157  if not ref.get('enabled', True):
159  message=_('cannot create a project in a '
160  'branch containing a disabled '
161  'project: %s') % ref['id'])
162 
163  self._assert_max_hierarchy_depth(project_ref.get('parent_id'),
164  parents_list)
165 
166  def _raise_reserved_character_exception(self, entity_type, name):
167  msg = _('%(entity)s name cannot contain the following reserved '
168  'characters: %(chars)s')
170  message=msg % {
171  'entity': entity_type,
172  'chars': utils.list_url_unsafe_chars(name)
173  })
174 
176  if project['is_domain']:
177  return _('it is not permitted to have two projects '
178  'acting as domains with the same name: %s'
179  ) % project['name']
180  else:
181  return _('it is not permitted to have two projects '
182  'with either the same name or same id in '
183  'the same domain: '
184  'name is %(name)s, project id %(id)s'
185  ) % project
186 
187  def create_project(self, project_id, project, initiator=None):
188  project = project.copy()
189 
190  if (CONF.resource.project_name_url_safe != 'off' and
191  utils.is_not_url_safe(project['name'])):
193  project['name'])
194 
195  project.setdefault('enabled', True)
196  project['name'] = project['name'].strip()
197  project.setdefault('description', '')
198 
199  # For regular projects, the controller will ensure we have a valid
200  # domain_id. For projects acting as a domain, the project_id
201  # is, effectively, the domain_id - and for such projects we don't
202  # bother to store a copy of it in the domain_id attribute.
203  project.setdefault('domain_id', None)
204  project.setdefault('parent_id', None)
205  if not project['parent_id']:
206  project['parent_id'] = project['domain_id']
207  project.setdefault('is_domain', False)
208 
209  self._enforce_project_constraints(project)
210 
211  # We leave enforcing name uniqueness to the underlying driver (instead
212  # of doing it in code in the project_constraints above), so as to allow
213  # this check to be done at the storage level, avoiding race conditions
214  # in multi-process keystone configurations.
215  try:
216  ret = self.driver.create_project(project_id, project)
217  except exception.Conflict:
218  raise exception.Conflict(
219  type='project',
220  details=self._generate_project_name_conflict_msg(project))
221 
222  if project.get('is_domain'):
223  notifications.Audit.created(self._DOMAIN, project_id, initiator)
224  else:
225  notifications.Audit.created(self._PROJECT, project_id, initiator)
226  if MEMOIZE.should_cache(ret):
227  self.get_project.set(ret, self, project_id)
228  self.get_project_by_name.set(ret, self, ret['name'],
229  ret['domain_id'])
230 
231  assignment.COMPUTED_ASSIGNMENTS_REGION.invalidate()
232 
233  return ret
234 
235  def assert_domain_enabled(self, domain_id, domain=None):
236  """Assert the Domain is enabled.
237 
238  :raise AssertionError: if domain is disabled.
239  """
240  if domain is None:
241  domain = self.get_domain(domain_id)
242  if not domain.get('enabled', True):
243  raise AssertionError(_('Domain is disabled: %s') % domain_id)
244 
245  def assert_domain_not_federated(self, domain_id, domain):
246  """Assert the Domain's name and id do not match the reserved keyword.
247 
248  Note that the reserved keyword is defined in the configuration file,
249  by default, it is 'Federated', it is also case insensitive.
250  If config's option is empty the default hardcoded value 'Federated'
251  will be used.
252 
253  :raise AssertionError: if domain named match the value in the config.
254 
255  """
256  # NOTE(marek-denis): We cannot create this attribute in the __init__ as
257  # config values are always initialized to default value.
258  federated_domain = CONF.federation.federated_domain_name.lower()
259  if (domain.get('name') and domain['name'].lower() == federated_domain):
260  raise AssertionError(_('Domain cannot be named %s')
261  % domain['name'])
262  if (domain_id.lower() == federated_domain):
263  raise AssertionError(_('Domain cannot have ID %s')
264  % domain_id)
265 
266  def assert_project_enabled(self, project_id, project=None):
267  """Assert the project is enabled and its associated domain is enabled.
268 
269  :raise AssertionError: if the project or domain is disabled.
270  """
271  if project is None:
272  project = self.get_project(project_id)
273  # If it's a regular project (i.e. it has a domain_id), we need to make
274  # sure the domain itself is not disabled
275  if project['domain_id']:
276  self.assert_domain_enabled(domain_id=project['domain_id'])
277  if not project.get('enabled', True):
278  raise AssertionError(_('Project is disabled: %s') % project_id)
279 
280  def _assert_all_parents_are_enabled(self, project_id):
281  parents_list = self.list_project_parents(project_id)
282  for project in parents_list:
283  if not project.get('enabled', True):
285  _('Cannot enable project %s since it has disabled '
286  'parents') % project_id)
287 
288  def _is_immutable(self, project_ref):
289  return project_ref['options'].get(
290  ro_opt.IMMUTABLE_OPT.option_name, False)
291 
292  def _check_whole_subtree_is_disabled(self, project_id, subtree_list=None):
293  if not subtree_list:
294  subtree_list = self.list_projects_in_subtree(project_id)
295  subtree_enabled = [ref.get('enabled', True) for ref in subtree_list]
296  return (not any(subtree_enabled))
297 
298  def _update_project(self, project_id, project, initiator=None,
299  cascade=False):
300  # Use the driver directly to prevent using old cached value.
301  original_project = self.driver.get_project(project_id)
302  project = project.copy()
303  self._require_matching_domain_id(project, original_project)
304 
305  if original_project['is_domain']:
306  # prevent updates to immutable domains
307  ro_opt.check_immutable_update(
308  original_resource_ref=original_project,
309  new_resource_ref=project,
310  type='domain',
311  resource_id=project_id)
312  domain = self._get_domain_from_project(original_project)
313  self.assert_domain_not_federated(project_id, domain)
314  url_safe_option = CONF.resource.domain_name_url_safe
315  exception_entity = 'Domain'
316  else:
317  # prevent updates to immutable projects
318  ro_opt.check_immutable_update(
319  original_resource_ref=original_project,
320  new_resource_ref=project,
321  type='project',
322  resource_id=project_id)
323  url_safe_option = CONF.resource.project_name_url_safe
324  exception_entity = 'Project'
325 
326  project_name_changed = ('name' in project and project['name'] !=
327  original_project['name'])
328  if (url_safe_option != 'off' and project_name_changed and
329  utils.is_not_url_safe(project['name'])):
330  self._raise_reserved_character_exception(exception_entity,
331  project['name'])
332  elif project_name_changed:
333  project['name'] = project['name'].strip()
334  parent_id = original_project.get('parent_id')
335  if 'parent_id' in project and project.get('parent_id') != parent_id:
337  _('Update of `parent_id` is not allowed.'))
338 
339  if ('is_domain' in project and
340  project['is_domain'] != original_project['is_domain']):
342  message=_('Update of `is_domain` is not allowed.'))
343 
344  original_project_enabled = original_project.get('enabled', True)
345  project_enabled = project.get('enabled', True)
346  if not original_project_enabled and project_enabled:
347  self._assert_all_parents_are_enabled(project_id)
348  if original_project_enabled and not project_enabled:
349  # NOTE(htruta): In order to disable a regular project, all its
350  # children must already be disabled. However, to keep
351  # compatibility with the existing domain behaviour, we allow a
352  # project acting as a domain to be disabled irrespective of the
353  # state of its children. Disabling a project acting as domain
354  # effectively disables its children.
355  if (not original_project.get('is_domain') and not cascade and not
356  self._check_whole_subtree_is_disabled(project_id)):
358  _('Cannot disable project %(project_id)s since its '
359  'subtree contains enabled projects.')
360  % {'project_id': project_id})
361 
362  notifications.Audit.disabled(self._PROJECT, project_id,
363  public=False)
364  # Drop the computed assignments if the project is being disabled.
365  # This ensures an accurate list of projects is returned when
366  # listing projects/domains for a user based on role assignments.
367  assignment.COMPUTED_ASSIGNMENTS_REGION.invalidate()
368 
369  if cascade:
371  original_project)
372  self._update_project_enabled_cascade(project_id, project_enabled)
373 
374  try:
375  project['is_domain'] = (project.get('is_domain') or
376  original_project['is_domain'])
377  ret = self.driver.update_project(project_id, project)
378  except exception.Conflict:
379  raise exception.Conflict(
380  type='project',
381  details=self._generate_project_name_conflict_msg(project))
382 
383  try:
384  self.get_project.invalidate(self, project_id)
385  self.get_project_by_name.invalidate(self, original_project['name'],
386  original_project['domain_id'])
387  if ('domain_id' in project and
388  project['domain_id'] != original_project['domain_id']):
389  # If the project's domain_id has been updated, invalidate user
390  # role assignments cache region, as it may be caching inherited
391  # assignments from the old domain to the specified project
392  assignment.COMPUTED_ASSIGNMENTS_REGION.invalidate()
393  finally:
394  # attempt to send audit event even if the cache invalidation raises
395  notifications.Audit.updated(self._PROJECT, project_id, initiator)
396  if original_project['is_domain']:
397  notifications.Audit.updated(self._DOMAIN, project_id,
398  initiator)
399  # If the domain is being disabled, issue the disable
400  # notification as well
401  if original_project_enabled and not project_enabled:
402  # NOTE(lbragstad): When a domain is disabled, we have to
403  # invalidate the entire token cache. With persistent
404  # tokens, we did something similar where all tokens for a
405  # specific domain were deleted when that domain was
406  # disabled. This effectively offers the same behavior for
407  # non-persistent tokens by removing them from the cache and
408  # requiring the authorization context to be rebuilt the
409  # next time they're validated.
410  token_provider.TOKENS_REGION.invalidate()
411  notifications.Audit.disabled(self._DOMAIN, project_id,
412  public=False)
413 
414  return ret
415 
416  def _only_allow_enabled_to_update_cascade(self, project, original_project):
417  for attr in project:
418  if attr != 'enabled':
419  if project.get(attr) != original_project.get(attr):
421  message=_('Cascade update is only allowed for '
422  'enabled attribute.'))
423 
424  def _update_project_enabled_cascade(self, project_id, enabled):
425  subtree = self.list_projects_in_subtree(project_id)
426  # Update enabled only if different from original value
427  subtree_to_update = [child for child in subtree
428  if child['enabled'] != enabled]
429  for child in subtree_to_update:
430  child['enabled'] = enabled
431 
432  if not enabled:
433  # Does not in fact disable the project, only emits a
434  # notification that it was disabled. The actual disablement
435  # is done in the next line.
436  notifications.Audit.disabled(self._PROJECT, child['id'],
437  public=False)
438 
439  self.driver.update_project(child['id'], child)
440 
441  def update_project(self, project_id, project, initiator=None,
442  cascade=False):
443  ret = self._update_project(project_id, project, initiator, cascade)
444  if ret['is_domain']:
445  self.get_domain.invalidate(self, project_id)
446  self.get_domain_by_name.invalidate(self, ret['name'])
447 
448  return ret
449 
450  def _post_delete_cleanup_project(self, project_id, project,
451  initiator=None):
452  try:
453  self.get_project.invalidate(self, project_id)
454  self.get_project_by_name.invalidate(self, project['name'],
455  project['domain_id'])
456  PROVIDERS.assignment_api.delete_project_assignments(project_id)
457  # Invalidate user role assignments cache region, as it may
458  # be caching role assignments where the target is
459  # the specified project
460  assignment.COMPUTED_ASSIGNMENTS_REGION.invalidate()
461  PROVIDERS.credential_api.delete_credentials_for_project(project_id)
462  PROVIDERS.trust_api.delete_trusts_for_project(project_id)
463  PROVIDERS.unified_limit_api.delete_limits_for_project(project_id)
464  finally:
465  # attempt to send audit event even if the cache invalidation raises
466  notifications.Audit.deleted(self._PROJECT, project_id, initiator)
467 
468  def delete_project(self, project_id, initiator=None, cascade=False):
469  """Delete one project or a subtree.
470 
471  :param cascade: If true, the specified project and all its
472  sub-projects are deleted. Otherwise, only the specified
473  project is deleted.
474  :type cascade: boolean
475  :raises keystone.exception.ValidationError: if project is a domain
476  :raises keystone.exception.Forbidden: if project is not a leaf
477  """
478  project = self.driver.get_project(project_id)
479  if project.get('is_domain'):
480  self._delete_domain(project, initiator)
481  else:
482  self._delete_project(project, initiator, cascade)
483 
484  def _delete_project(self, project, initiator=None, cascade=False):
485  # Prevent deletion of immutable projects
486  ro_opt.check_immutable_delete(
487  resource_ref=project,
488  resource_type='project',
489  resource_id=project['id'])
490  project_id = project['id']
491  if project['is_domain'] and project['enabled']:
493  message=_('cannot delete an enabled project acting as a '
494  'domain. Please disable the project %s first.')
495  % project.get('id'))
496 
497  if not self.is_leaf_project(project_id) and not cascade:
499  _('Cannot delete the project %s since it is not a leaf in the '
500  'hierarchy. Use the cascade option if you want to delete a '
501  'whole subtree.')
502  % project_id)
503 
504  if cascade:
505  # Getting reversed project's subtrees list, i.e. from the leaves
506  # to the root, so we do not break parent_id FK.
507  subtree_list = self.list_projects_in_subtree(project_id)
508  subtree_list.reverse()
510  project_id, subtree_list=subtree_list):
512  _('Cannot delete project %(project_id)s since its subtree '
513  'contains enabled projects.')
514  % {'project_id': project_id})
515 
516  project_list = subtree_list + [project]
517  projects_ids = [x['id'] for x in project_list]
518 
519  ret = self.driver.delete_projects_from_ids(projects_ids)
520  for prj in project_list:
521  self._post_delete_cleanup_project(prj['id'], prj, initiator)
522  else:
523  ret = self.driver.delete_project(project_id)
524  self._post_delete_cleanup_project(project_id, project, initiator)
525 
526  reason = (
527  'The token cache is being invalidate because project '
528  '%(project_id)s was deleted. Authorization will be recalculated '
529  'and enforced accordingly the next time users authenticate or '
530  'validate a token.' % {'project_id': project_id}
531  )
532  notifications.invalidate_token_cache_notification(reason)
533  return ret
534 
535  def _filter_projects_list(self, projects_list, user_id):
536  user_projects = PROVIDERS.assignment_api.list_projects_for_user(
537  user_id
538  )
539  user_projects_ids = set([proj['id'] for proj in user_projects])
540  # Keep only the projects present in user_projects
541  return [proj for proj in projects_list
542  if proj['id'] in user_projects_ids]
543 
544  def _assert_valid_project_id(self, project_id):
545  if project_id is None:
546  msg = _('Project field is required and cannot be empty.')
547  raise exception.ValidationError(message=msg)
548  # Check if project_id exists
549  self.get_project(project_id)
550 
551  def _include_limits(self, projects):
552  """Modify a list of projects to include limit information.
553 
554  :param projects: a list of project references including an `id`
555  :type projects: list of dictionaries
556  """
557  for project in projects:
558  hints = driver_hints.Hints()
559  hints.add_filter('project_id', project['id'])
560  limits = PROVIDERS.unified_limit_api.list_limits(hints)
561  project['limits'] = limits
562 
563  def list_project_parents(self, project_id, user_id=None,
564  include_limits=False):
565  self._assert_valid_project_id(project_id)
566  parents = self.driver.list_project_parents(project_id)
567  # If a user_id was provided, the returned list should be filtered
568  # against the projects this user has access to.
569  if user_id:
570  parents = self._filter_projects_list(parents, user_id)
571  if include_limits:
572  self._include_limits(parents)
573  return parents
574 
575  def _build_parents_as_ids_dict(self, project, parents_by_id):
576  # NOTE(rodrigods): we don't rely in the order of the projects returned
577  # by the list_project_parents() method. Thus, we create a project cache
578  # (parents_by_id) in order to access each parent in constant time and
579  # traverse up the hierarchy.
580  def traverse_parents_hierarchy(project):
581  parent_id = project.get('parent_id')
582  if not parent_id:
583  return None
584 
585  parent = parents_by_id[parent_id]
586  return {parent_id: traverse_parents_hierarchy(parent)}
587 
588  return traverse_parents_hierarchy(project)
589 
590  def get_project_parents_as_ids(self, project):
591  """Get the IDs from the parents from a given project.
592 
593  The project IDs are returned as a structured dictionary traversing up
594  the hierarchy to the top level project. For example, considering the
595  following project hierarchy::
596 
597  A
598  |
599  +-B-+
600  | |
601  C D
602 
603  If we query for project C parents, the expected return is the following
604  dictionary::
605 
606  'parents': {
607  B['id']: {
608  A['id']: None
609  }
610  }
611 
612  """
613  parents_list = self.list_project_parents(project['id'])
614  parents_as_ids = self._build_parents_as_ids_dict(
615  project, {proj['id']: proj for proj in parents_list})
616  return parents_as_ids
617 
618  def list_projects_in_subtree(self, project_id, user_id=None,
619  include_limits=False):
620  self._assert_valid_project_id(project_id)
621  subtree = self.driver.list_projects_in_subtree(project_id)
622  # If a user_id was provided, the returned list should be filtered
623  # against the projects this user has access to.
624  if user_id:
625  subtree = self._filter_projects_list(subtree, user_id)
626  if include_limits:
627  self._include_limits(subtree)
628  return subtree
629 
630  def _build_subtree_as_ids_dict(self, project_id, subtree_by_parent):
631  # NOTE(rodrigods): we perform a depth first search to construct the
632  # dictionaries representing each level of the subtree hierarchy. In
633  # order to improve this traversal performance, we create a cache of
634  # projects (subtree_py_parent) that accesses in constant time the
635  # direct children of a given project.
636  def traverse_subtree_hierarchy(project_id):
637  children = subtree_by_parent.get(project_id)
638  if not children:
639  return None
640 
641  children_ids = {}
642  for child in children:
643  children_ids[child['id']] = traverse_subtree_hierarchy(
644  child['id'])
645  return children_ids
646 
647  return traverse_subtree_hierarchy(project_id)
648 
649  def get_projects_in_subtree_as_ids(self, project_id):
650  """Get the IDs from the projects in the subtree from a given project.
651 
652  The project IDs are returned as a structured dictionary representing
653  their hierarchy. For example, considering the following project
654  hierarchy::
655 
656  A
657  |
658  +-B-+
659  | |
660  C D
661 
662  If we query for project A subtree, the expected return is the following
663  dictionary::
664 
665  'subtree': {
666  B['id']: {
667  C['id']: None,
668  D['id']: None
669  }
670  }
671 
672  """
673  def _projects_indexed_by_parent(projects_list):
674  projects_by_parent = {}
675  for proj in projects_list:
676  parent_id = proj.get('parent_id')
677  if parent_id:
678  if parent_id in projects_by_parent:
679  projects_by_parent[parent_id].append(proj)
680  else:
681  projects_by_parent[parent_id] = [proj]
682  return projects_by_parent
683 
684  subtree_list = self.list_projects_in_subtree(project_id)
685  subtree_as_ids = self._build_subtree_as_ids_dict(
686  project_id, _projects_indexed_by_parent(subtree_list))
687  return subtree_as_ids
688 
689  def list_domains_from_ids(self, domain_ids):
690  """List domains for the provided list of ids.
691 
692  :param domain_ids: list of ids
693 
694  :returns: a list of domain_refs.
695 
696  This method is used internally by the assignment manager to bulk read
697  a set of domains given their ids.
698 
699  """
700  # Retrieve the projects acting as domains get their correspondent
701  # domains
702  projects = self.list_projects_from_ids(domain_ids)
703  domains = [self._get_domain_from_project(project)
704  for project in projects]
705 
706  return domains
707 
708  @MEMOIZE
709  def get_domain(self, domain_id):
710  try:
711  # Retrieve the corresponding project that acts as a domain
712  project = self.driver.get_project(domain_id)
713  # the DB backend might not operate in case sensitive mode,
714  # therefore verify for exact match of IDs
715  if domain_id != project['id']:
716  raise exception.DomainNotFound(domain_id=domain_id)
718  raise exception.DomainNotFound(domain_id=domain_id)
719 
720  # Return its correspondent domain
721  return self._get_domain_from_project(project)
722 
723  @MEMOIZE
724  def get_domain_by_name(self, domain_name):
725  try:
726  # Retrieve the corresponding project that acts as a domain
727  project = self.driver.get_project_by_name(domain_name,
728  domain_id=None)
730  raise exception.DomainNotFound(domain_id=domain_name)
731 
732  # Return its correspondent domain
733  return self._get_domain_from_project(project)
734 
735  def _get_domain_from_project(self, project_ref):
736  """Create a domain ref from a project ref.
737 
738  Based on the provided project ref, create a domain ref, so that the
739  result can be returned in response to a domain API call.
740  """
741  if not project_ref['is_domain']:
742  LOG.error('Asked to convert a non-domain project into a '
743  'domain - Domain: %(domain_id)s, Project ID: '
744  '%(id)s, Project Name: %(project_name)s',
745  {'domain_id': project_ref['domain_id'],
746  'id': project_ref['id'],
747  'project_name': project_ref['name']})
748  raise exception.DomainNotFound(domain_id=project_ref['id'])
749 
750  domain_ref = project_ref.copy()
751  # As well as the project specific attributes that we need to remove,
752  # there is an old compatibility issue in that update project (as well
753  # as extracting an extra attributes), also includes a copy of the
754  # actual extra dict as well - something that update domain does not do.
755  for k in ['parent_id', 'domain_id', 'is_domain', 'extra']:
756  domain_ref.pop(k, None)
757 
758  return domain_ref
759 
760  def create_domain(self, domain_id, domain, initiator=None):
761  if (CONF.resource.domain_name_url_safe != 'off' and
762  utils.is_not_url_safe(domain['name'])):
763  self._raise_reserved_character_exception('Domain', domain['name'])
764  project_from_domain = base.get_project_from_domain(domain)
765  is_domain_project = self.create_project(
766  domain_id, project_from_domain, initiator)
767 
768  return self._get_domain_from_project(is_domain_project)
769 
770  @manager.response_truncated
771  def list_domains(self, hints=None):
772  projects = self.list_projects_acting_as_domain(hints)
773  domains = [self._get_domain_from_project(project)
774  for project in projects]
775  return domains
776 
777  def update_domain(self, domain_id, domain, initiator=None):
778  # TODO(henry-nash): We shouldn't have to check for the federated domain
779  # here as well as _update_project, but currently our tests assume the
780  # checks are done in a specific order. The tests should be refactored.
781  self.assert_domain_not_federated(domain_id, domain)
782  project = base.get_project_from_domain(domain)
783  try:
784  original_domain = self.driver.get_project(domain_id)
785  project = self._update_project(domain_id, project, initiator)
787  raise exception.DomainNotFound(domain_id=domain_id)
788 
789  domain_from_project = self._get_domain_from_project(project)
790  self.get_domain.invalidate(self, domain_id)
791  self.get_domain_by_name.invalidate(self, original_domain['name'])
792 
793  return domain_from_project
794 
795  def delete_domain(self, domain_id, initiator=None):
796  # Use the driver directly to get the project that acts as a domain and
797  # prevent using old cached value.
798  try:
799  domain = self.driver.get_project(domain_id)
801  raise exception.DomainNotFound(domain_id=domain_id)
802  self._delete_domain(domain, initiator)
803 
804  def _delete_domain(self, domain, initiator=None):
805  # Disallow deletion of immutable domains
806  ro_opt.check_immutable_delete(
807  resource_ref=domain,
808  resource_type='domain',
809  resource_id=domain['id'])
810  # To help avoid inadvertent deletes, we insist that the domain
811  # has been previously disabled. This also prevents a user deleting
812  # their own domain since, once it is disabled, they won't be able
813  # to get a valid token to issue this delete.
814  if domain['enabled']:
816  _('Cannot delete a domain that is enabled, please disable it '
817  'first.'))
818 
819  domain_id = domain['id']
820  self._delete_domain_contents(domain_id)
821  notifications.Audit.internal(
822  notifications.DOMAIN_DELETED, domain_id
823  )
824  self._delete_project(domain, initiator)
825  try:
826  self.get_domain.invalidate(self, domain_id)
827  self.get_domain_by_name.invalidate(self, domain['name'])
828  # Delete any database stored domain config
829  PROVIDERS.domain_config_api.delete_config_options(domain_id)
830  PROVIDERS.domain_config_api.release_registration(domain_id)
831  finally:
832  # attempt to send audit event even if the cache invalidation raises
833  notifications.Audit.deleted(self._DOMAIN, domain_id, initiator)
834 
835  def _delete_domain_contents(self, domain_id):
836  """Delete the contents of a domain.
837 
838  Before we delete a domain, we need to remove all the entities
839  that are owned by it, i.e. Projects. To do this we
840  call the delete function for these entities, which are
841  themselves responsible for deleting any credentials and role grants
842  associated with them as well as revoking any relevant tokens.
843 
844  """
845  def _delete_projects(project, projects, examined):
846  if project['id'] in examined:
847  msg = ('Circular reference or a repeated entry found '
848  'projects hierarchy - %(project_id)s.')
849  LOG.error(msg, {'project_id': project['id']})
850  return
851 
852  examined.add(project['id'])
853  children = [proj for proj in projects
854  if proj.get('parent_id') == project['id']]
855  for proj in children:
856  _delete_projects(proj, projects, examined)
857 
858  try:
859  self._delete_project(project, initiator=None)
861  LOG.debug(('Project %(projectid)s not found when '
862  'deleting domain contents for %(domainid)s, '
863  'continuing with cleanup.'),
864  {'projectid': project['id'],
865  'domainid': domain_id})
866 
867  proj_refs = self.list_projects_in_domain(domain_id)
868 
869  # Deleting projects recursively
870  roots = [x for x in proj_refs if x.get('parent_id') == domain_id]
871  examined = set()
872  for project in roots:
873  _delete_projects(project, proj_refs, examined)
874 
875  @manager.response_truncated
876  def list_projects(self, hints=None):
877  if hints:
878  tag_filters = {}
879  # Handle project tag filters separately
880  for f in list(hints.filters):
881  if f['name'] in TAG_SEARCH_FILTERS:
882  tag_filters[f['name']] = f['value']
883  hints.filters.remove(f)
884  if tag_filters:
885  tag_refs = self.driver.list_projects_by_tags(tag_filters)
886  project_refs = self.driver.list_projects(hints)
887  ref_ids = [ref['id'] for ref in tag_refs]
888  return [ref for ref in project_refs if ref['id'] in ref_ids]
889  return self.driver.list_projects(hints or driver_hints.Hints())
890 
891  # NOTE(henry-nash): list_projects_in_domain is actually an internal method
892  # and not exposed via the API. Therefore there is no need to support
893  # driver hints for it.
894  def list_projects_in_domain(self, domain_id):
895  return self.driver.list_projects_in_domain(domain_id)
896 
897  def list_projects_acting_as_domain(self, hints=None):
898  return self.driver.list_projects_acting_as_domain(
899  hints or driver_hints.Hints())
900 
901  @MEMOIZE
902  def get_project(self, project_id):
903  return self.driver.get_project(project_id)
904 
905  @MEMOIZE
906  def get_project_by_name(self, project_name, domain_id):
907  return self.driver.get_project_by_name(project_name, domain_id)
908 
909  def _require_matching_domain_id(self, new_ref, orig_ref):
910  """Ensure the current domain ID matches the reference one, if any.
911 
912  Provided we want domain IDs to be immutable, check whether any
913  domain_id specified in the ref dictionary matches the existing
914  domain_id for this entity.
915 
916  :param new_ref: the dictionary of new values proposed for this entity
917  :param orig_ref: the dictionary of original values proposed for this
918  entity
919  :raises: :class:`keystone.exception.ValidationError`
920  """
921  if 'domain_id' in new_ref:
922  if new_ref['domain_id'] != orig_ref['domain_id']:
923  raise exception.ValidationError(_('Cannot change Domain ID'))
924 
925  def create_project_tag(self, project_id, tag, initiator=None):
926  """Create a new tag on project.
927 
928  :param project_id: ID of a project to create a tag for
929  :param tag: The string value of a tag to add
930 
931  :returns: The value of the created tag
932  """
933  project = self.driver.get_project(project_id)
934  if ro_opt.check_resource_immutable(resource_ref=project):
936  message=_(
937  'Cannot create project tags for %(project_id)s, project '
938  'is immutable. Set "immutable" option to false before '
939  'creating project tags.') % {'project_id': project_id})
940  tag_name = tag.strip()
941  project['tags'].append(tag_name)
942  self.update_project(project_id, {'tags': project['tags']})
943 
944  notifications.Audit.created(
945  self._PROJECT_TAG, tag_name, initiator)
946  return tag_name
947 
948  def get_project_tag(self, project_id, tag_name):
949  """Return information for a single tag on a project.
950 
951  :param project_id: ID of a project to retrive a tag from
952  :param tag_name: Name of a tag to return
953 
954  :raises keystone.exception.ProjectTagNotFound: If the tag name
955  does not exist on the project
956  :returns: The tag value
957  """
958  project = self.driver.get_project(project_id)
959  if tag_name not in project.get('tags'):
960  raise exception.ProjectTagNotFound(project_tag=tag_name)
961  return tag_name
962 
963  def list_project_tags(self, project_id):
964  """List all tags on project.
965 
966  :param project_id: The ID of a project
967 
968  :returns: A list of tags from a project
969  """
970  project = self.driver.get_project(project_id)
971  return project.get('tags', [])
972 
973  def update_project_tags(self, project_id, tags, initiator=None):
974  """Update all tags on a project.
975 
976  :param project_id: The ID of the project to update
977  :param tags: A list of tags to update on the project
978 
979  :returns: A list of tags
980  """
981  project = self.driver.get_project(project_id)
982  if ro_opt.check_resource_immutable(resource_ref=project):
984  message=_(
985  'Cannot update project tags for %(project_id)s, project '
986  'is immutable. Set "immutable" option to false before '
987  'creating project tags.') % {'project_id': project_id})
988  tag_list = [t.strip() for t in tags]
989  project = {'tags': tag_list}
990  self.update_project(project_id, project)
991  return tag_list
992 
993  def delete_project_tag(self, project_id, tag):
994  """Delete single tag from project.
995 
996  :param project_id: The ID of the project
997  :param tag: The tag value to delete
998 
999  :raises keystone.exception.ProjectTagNotFound: If the tag name
1000  does not exist on the project
1001  """
1002  project = self.driver.get_project(project_id)
1003  if ro_opt.check_resource_immutable(resource_ref=project):
1005  message=_(
1006  'Cannot delete project tags for %(project_id)s, project '
1007  'is immutable. Set "immutable" option to false before '
1008  'creating project tags.') % {'project_id': project_id})
1009  try:
1010  project['tags'].remove(tag)
1011  except ValueError:
1012  raise exception.ProjectTagNotFound(project_tag=tag)
1013  self.update_project(project_id, project)
1014  notifications.Audit.deleted(self._PROJECT_TAG, tag)
1015 
1016  def check_project_depth(self, max_depth=None):
1017  """Check project depth whether greater than input or not."""
1018  if max_depth:
1019  exceeded_project_ids = self.driver.check_project_depth(max_depth)
1020  if exceeded_project_ids:
1021  raise exception.LimitTreeExceedError(exceeded_project_ids,
1022  max_depth)
1023 
1024 
1025 MEMOIZE_CONFIG = cache.get_memoization_decorator(group='domain_config')
1026 
1027 
1028 class DomainConfigManager(manager.Manager):
1029  """Default pivot point for the Domain Config backend."""
1030 
1031  # NOTE(henry-nash): In order for a config option to be stored in the
1032  # standard table, it must be explicitly whitelisted. Options marked as
1033  # sensitive are stored in a separate table. Attempting to store options
1034  # that are not listed as either whitelisted or sensitive will raise an
1035  # exception.
1036  #
1037  # Only those options that affect the domain-specific driver support in
1038  # the identity manager are supported.
1039 
1040  driver_namespace = 'keystone.resource.domain_config'
1041  _provides_api = 'domain_config_api'
1042 
1043  # We explicitly state each whitelisted option instead of pulling all ldap
1044  # options from CONF and selectively pruning them to prevent a security
1045  # lapse. That way if a new ldap CONF key/value were to be added it wouldn't
1046  # automatically be added to the whitelisted options unless that is what was
1047  # intended. In which case, we explicitly add it to the list ourselves.
1048  whitelisted_options = {
1049  'identity': ['driver', 'list_limit'],
1050  'ldap': [
1051  'url', 'user', 'suffix', 'query_scope', 'page_size',
1052  'alias_dereferencing', 'debug_level', 'chase_referrals',
1053  'user_tree_dn', 'user_filter', 'user_objectclass',
1054  'user_id_attribute', 'user_name_attribute', 'user_mail_attribute',
1055  'user_description_attribute', 'user_pass_attribute',
1056  'user_enabled_attribute', 'user_enabled_invert',
1057  'user_enabled_mask', 'user_enabled_default',
1058  'user_attribute_ignore', 'user_default_project_id_attribute',
1059  'user_enabled_emulation', 'user_enabled_emulation_dn',
1060  'user_enabled_emulation_use_group_config',
1061  'user_additional_attribute_mapping', 'group_tree_dn',
1062  'group_filter', 'group_objectclass', 'group_id_attribute',
1063  'group_name_attribute', 'group_members_are_ids',
1064  'group_member_attribute', 'group_desc_attribute',
1065  'group_attribute_ignore', 'group_additional_attribute_mapping',
1066  'tls_cacertfile', 'tls_cacertdir', 'use_tls', 'tls_req_cert',
1067  'use_pool', 'pool_size', 'pool_retry_max', 'pool_retry_delay',
1068  'pool_connection_timeout', 'pool_connection_lifetime',
1069  'use_auth_pool', 'auth_pool_size', 'auth_pool_connection_lifetime'
1070  ]
1071  }
1072  sensitive_options = {
1073  'identity': [],
1074  'ldap': ['password']
1075  }
1076 
1077  def __init__(self):
1078  super(DomainConfigManager, self).__init__(CONF.domain_config.driver)
1079 
1080  def _assert_valid_config(self, config):
1081  """Ensure the options in the config are valid.
1082 
1083  This method is called to validate the request config in create and
1084  update manager calls.
1085 
1086  :param config: config structure being created or updated
1087 
1088  """
1089  # Something must be defined in the request
1090  if not config:
1092  reason=_('No options specified'))
1093 
1094  # Make sure the groups/options defined in config itself are valid
1095  for group in config:
1096  if (not config[group] or not
1097  isinstance(config[group], dict)):
1098  msg = _('The value of group %(group)s specified in the '
1099  'config should be a dictionary of options') % {
1100  'group': group}
1101  raise exception.InvalidDomainConfig(reason=msg)
1102  for option in config[group]:
1103  self._assert_valid_group_and_option(group, option)
1104 
1105  def _assert_valid_group_and_option(self, group, option):
1106  """Ensure the combination of group and option is valid.
1107 
1108  :param group: optional group name, if specified it must be one
1109  we support
1110  :param option: optional option name, if specified it must be one
1111  we support and a group must also be specified
1112 
1113  """
1114  if not group and not option:
1115  # For all calls, it's OK for neither to be defined, it means you
1116  # are operating on all config options for that domain.
1117  return
1118 
1119  if not group and option:
1120  # Our API structure should prevent this from ever happening, so if
1121  # it does, then this is coding error.
1122  msg = _('Option %(option)s found with no group specified while '
1123  'checking domain configuration request') % {
1124  'option': option}
1125  raise exception.UnexpectedError(exception=msg)
1126 
1127  if (group and group not in self.whitelisted_options and
1128  group not in self.sensitive_options):
1129  msg = _('Group %(group)s is not supported '
1130  'for domain specific configurations') % {'group': group}
1131  raise exception.InvalidDomainConfig(reason=msg)
1132 
1133  if option:
1134  if (option not in self.whitelisted_options[group] and option not in
1135  self.sensitive_options[group]):
1136  msg = _('Option %(option)s in group %(group)s is not '
1137  'supported for domain specific configurations') % {
1138  'group': group, 'option': option}
1139  raise exception.InvalidDomainConfig(reason=msg)
1140 
1141  def _is_sensitive(self, group, option):
1142  return option in self.sensitive_options[group]
1143 
1144  def _config_to_list(self, config):
1145  """Build list of options for use by backend drivers."""
1146  option_list = []
1147  for group in config:
1148  for option in config[group]:
1149  option_list.append({
1150  'group': group, 'option': option,
1151  'value': config[group][option],
1152  'sensitive': self._is_sensitive(group, option)})
1153 
1154  return option_list
1155 
1156  def _option_dict(self, group, option):
1157  group_attr = getattr(CONF, group)
1158  return {'group': group, 'option': option,
1159  'value': getattr(group_attr, option)}
1160 
1161  def _list_to_config(self, whitelisted, sensitive=None, req_option=None):
1162  """Build config dict from a list of option dicts.
1163 
1164  :param whitelisted: list of dicts containing options and their groups,
1165  this has already been filtered to only contain
1166  those options to include in the output.
1167  :param sensitive: list of dicts containing sensitive options and their
1168  groups, this has already been filtered to only
1169  contain those options to include in the output.
1170  :param req_option: the individual option requested
1171 
1172  :returns: a config dict, including sensitive if specified
1173 
1174  """
1175  the_list = whitelisted + (sensitive or [])
1176  if not the_list:
1177  return {}
1178 
1179  if req_option:
1180  # The request was specific to an individual option, so
1181  # no need to include the group in the output. We first check that
1182  # there is only one option in the answer (and that it's the right
1183  # one) - if not, something has gone wrong and we raise an error
1184  if len(the_list) > 1 or the_list[0]['option'] != req_option:
1185  LOG.error('Unexpected results in response for domain '
1186  'config - %(count)s responses, first option is '
1187  '%(option)s, expected option %(expected)s',
1188  {'count': len(the_list), 'option': list[0]['option'],
1189  'expected': req_option})
1191  _('An unexpected error occurred when retrieving domain '
1192  'configs'))
1193  return {the_list[0]['option']: the_list[0]['value']}
1194 
1195  config = {}
1196  for option in the_list:
1197  config.setdefault(option['group'], {})
1198  config[option['group']][option['option']] = option['value']
1199 
1200  return config
1201 
1202  def create_config(self, domain_id, config):
1203  """Create config for a domain.
1204 
1205  :param domain_id: the domain in question
1206  :param config: the dict of config groups/options to assign to the
1207  domain
1208 
1209  Creates a new config, overwriting any previous config (no Conflict
1210  error will be generated).
1211 
1212  :returns: a dict of group dicts containing the options, with any that
1213  are sensitive removed
1214  :raises keystone.exception.InvalidDomainConfig: when the config
1215  contains options we do not support
1216 
1217  """
1218  self._assert_valid_config(config)
1219  option_list = self._config_to_list(config)
1220  self.create_config_options(domain_id, option_list)
1221  # Since we are caching on the full substituted config, we just
1222  # invalidate here, rather than try and create the right result to
1223  # cache.
1224  self.get_config_with_sensitive_info.invalidate(self, domain_id)
1225  return self._list_to_config(self.list_config_options(domain_id))
1226 
1227  def get_config(self, domain_id, group=None, option=None):
1228  """Get config, or partial config, for a domain.
1229 
1230  :param domain_id: the domain in question
1231  :param group: an optional specific group of options
1232  :param option: an optional specific option within the group
1233 
1234  :returns: a dict of group dicts containing the whitelisted options,
1235  filtered by group and option specified
1236  :raises keystone.exception.DomainConfigNotFound: when no config found
1237  that matches domain_id, group and option specified
1238  :raises keystone.exception.InvalidDomainConfig: when the config
1239  and group/option parameters specify an option we do not
1240  support
1241 
1242  An example response::
1243 
1244  {
1245  'ldap': {
1246  'url': 'myurl'
1247  'user_tree_dn': 'OU=myou'},
1248  'identity': {
1249  'driver': 'ldap'}
1250 
1251  }
1252 
1253  """
1254  self._assert_valid_group_and_option(group, option)
1255  whitelisted = self.list_config_options(domain_id, group, option)
1256  if whitelisted:
1257  return self._list_to_config(whitelisted, req_option=option)
1258 
1259  if option:
1260  msg = _('option %(option)s in group %(group)s') % {
1261  'group': group, 'option': option}
1262  elif group:
1263  msg = _('group %(group)s') % {'group': group}
1264  else:
1265  msg = _('any options')
1267  domain_id=domain_id, group_or_option=msg)
1268 
1269  def get_security_compliance_config(self, domain_id, group, option=None):
1270  r"""Get full or partial security compliance config from configuration.
1271 
1272  :param domain_id: the domain in question
1273  :param group: a specific group of options
1274  :param option: an optional specific option within the group
1275 
1276  :returns: a dict of group dicts containing the whitelisted options,
1277  filtered by group and option specified
1278  :raises keystone.exception.InvalidDomainConfig: when the config
1279  and group/option parameters specify an option we do not
1280  support
1281 
1282  An example response::
1283 
1284  {
1285  'security_compliance': {
1286  'password_regex': '^(?=.*\d)(?=.*[a-zA-Z]).{7,}$'
1287  'password_regex_description':
1288  'A password must consist of at least 1 letter, '
1289  '1 digit, and have a minimum length of 7 characters'
1290  }
1291  }
1292 
1293  """
1294  if domain_id != CONF.identity.default_domain_id:
1295  msg = _('Reading security compliance information for any domain '
1296  'other than the default domain is not allowed or '
1297  'supported.')
1298  raise exception.InvalidDomainConfig(reason=msg)
1299 
1300  config_list = []
1301  readable_options = ['password_regex', 'password_regex_description']
1302  if option and option not in readable_options:
1303  msg = _('Reading security compliance values other than '
1304  'password_regex and password_regex_description is not '
1305  'allowed.')
1306  raise exception.InvalidDomainConfig(reason=msg)
1307  elif option and option in readable_options:
1308  config_list.append(self._option_dict(group, option))
1309  elif not option:
1310  for op in readable_options:
1311  config_list.append(self._option_dict(group, op))
1312  # We already validated that the group is the security_compliance group
1313  # so we can move along and start validating the options
1314  return self._list_to_config(config_list, req_option=option)
1315 
1316  def update_config(self, domain_id, config, group=None, option=None):
1317  """Update config, or partial config, for a domain.
1318 
1319  :param domain_id: the domain in question
1320  :param config: the config dict containing and groups/options being
1321  updated
1322  :param group: an optional specific group of options, which if specified
1323  must appear in config, with no other groups
1324  :param option: an optional specific option within the group, which if
1325  specified must appear in config, with no other options
1326 
1327  The contents of the supplied config will be merged with the existing
1328  config for this domain, updating or creating new options if these did
1329  not previously exist. If group or option is specified, then the update
1330  will be limited to those specified items and the inclusion of other
1331  options in the supplied config will raise an exception, as will the
1332  situation when those options do not already exist in the current
1333  config.
1334 
1335  :returns: a dict of groups containing all whitelisted options
1336  :raises keystone.exception.InvalidDomainConfig: when the config
1337  and group/option parameters specify an option we do not
1338  support or one that does not exist in the original config
1339 
1340  """
1341  def _assert_valid_update(domain_id, config, group=None, option=None):
1342  """Ensure the combination of config, group and option is valid."""
1343  self._assert_valid_config(config)
1344  self._assert_valid_group_and_option(group, option)
1345 
1346  # If a group has been specified, then the request is to
1347  # explicitly only update the options in that group - so the config
1348  # must not contain anything else. Further, that group must exist in
1349  # the original config. Likewise, if an option has been specified,
1350  # then the group in the config must only contain that option and it
1351  # also must exist in the original config.
1352  if group:
1353  if len(config) != 1 or (option and len(config[group]) != 1):
1354  if option:
1355  msg = _('Trying to update option %(option)s in group '
1356  '%(group)s, so that, and only that, option '
1357  'must be specified in the config') % {
1358  'group': group, 'option': option}
1359  else:
1360  msg = _('Trying to update group %(group)s, so that, '
1361  'and only that, group must be specified in '
1362  'the config') % {'group': group}
1363  raise exception.InvalidDomainConfig(reason=msg)
1364 
1365  # So we now know we have the right number of entries in the
1366  # config that align with a group/option being specified, but we
1367  # must also make sure they match.
1368  if group not in config:
1369  msg = _('request to update group %(group)s, but config '
1370  'provided contains group %(group_other)s '
1371  'instead') % {
1372  'group': group,
1373  'group_other': list(config.keys())[0]}
1374  raise exception.InvalidDomainConfig(reason=msg)
1375  if option and option not in config[group]:
1376  msg = _('Trying to update option %(option)s in group '
1377  '%(group)s, but config provided contains option '
1378  '%(option_other)s instead') % {
1379  'group': group, 'option': option,
1380  'option_other': list(config[group].keys())[0]}
1381  raise exception.InvalidDomainConfig(reason=msg)
1382 
1383  # Finally, we need to check if the group/option specified
1384  # already exists in the original config - since if not, to keep
1385  # with the semantics of an update, we need to fail with
1386  # a DomainConfigNotFound
1387  if not self._get_config_with_sensitive_info(domain_id,
1388  group, option):
1389  if option:
1390  msg = _('option %(option)s in group %(group)s') % {
1391  'group': group, 'option': option}
1393  domain_id=domain_id, group_or_option=msg)
1394  else:
1395  msg = _('group %(group)s') % {'group': group}
1397  domain_id=domain_id, group_or_option=msg)
1398 
1399  update_config = config
1400  if group and option:
1401  # The config will just be a dict containing the option and
1402  # its value, so make it look like a single option under the
1403  # group in question
1404  update_config = {group: config}
1405 
1406  _assert_valid_update(domain_id, update_config, group, option)
1407 
1408  option_list = self._config_to_list(update_config)
1409  self.update_config_options(domain_id, option_list)
1410 
1411  self.get_config_with_sensitive_info.invalidate(self, domain_id)
1412  return self.get_config(domain_id)
1413 
1414  def delete_config(self, domain_id, group=None, option=None):
1415  """Delete config, or partial config, for the domain.
1416 
1417  :param domain_id: the domain in question
1418  :param group: an optional specific group of options
1419  :param option: an optional specific option within the group
1420 
1421  If group and option are None, then the entire config for the domain
1422  is deleted. If group is not None, then just that group of options will
1423  be deleted. If group and option are both specified, then just that
1424  option is deleted.
1425 
1426  :raises keystone.exception.InvalidDomainConfig: when group/option
1427  parameters specify an option we do not support or one that
1428  does not exist in the original config.
1429 
1430  """
1431  self._assert_valid_group_and_option(group, option)
1432  if group:
1433  # As this is a partial delete, then make sure the items requested
1434  # are valid and exist in the current config
1435  current_config = self._get_config_with_sensitive_info(domain_id)
1436  # Raise an exception if the group/options specified don't exist in
1437  # the current config so that the delete method provides the
1438  # correct error semantics.
1439  current_group = current_config.get(group)
1440  if not current_group:
1441  msg = _('group %(group)s') % {'group': group}
1443  domain_id=domain_id, group_or_option=msg)
1444  if option and not current_group.get(option):
1445  msg = _('option %(option)s in group %(group)s') % {
1446  'group': group, 'option': option}
1448  domain_id=domain_id, group_or_option=msg)
1449 
1450  self.delete_config_options(domain_id, group, option)
1451  self.get_config_with_sensitive_info.invalidate(self, domain_id)
1452 
1453  def _get_config_with_sensitive_info(self, domain_id, group=None,
1454  option=None):
1455  """Get config for a domain/group/option with sensitive info included.
1456 
1457  This is only used by the methods within this class, which may need to
1458  check individual groups or options.
1459 
1460  """
1461  whitelisted = self.list_config_options(domain_id, group, option)
1462  sensitive = self.list_config_options(domain_id, group, option,
1463  sensitive=True)
1464 
1465  # Check if there are any sensitive substitutions needed. We first try
1466  # and simply ensure any sensitive options that have valid substitution
1467  # references in the whitelisted options are substituted. We then check
1468  # the resulting whitelisted option and raise a warning if there
1469  # appears to be an unmatched or incorrectly constructed substitution
1470  # reference. To avoid the risk of logging any sensitive options that
1471  # have already been substituted, we first take a copy of the
1472  # whitelisted option.
1473 
1474  # Build a dict of the sensitive options ready to try substitution
1475  sensitive_dict = {s['option']: s['value'] for s in sensitive}
1476 
1477  for each_whitelisted in whitelisted:
1478  if not isinstance(each_whitelisted['value'], str):
1479  # We only support substitutions into string types, if its an
1480  # integer, list etc. then just continue onto the next one
1481  continue
1482 
1483  # Store away the original value in case we need to raise a warning
1484  # after substitution.
1485  original_value = each_whitelisted['value']
1486  warning_msg = ''
1487  try:
1488  each_whitelisted['value'] = (
1489  each_whitelisted['value'] % sensitive_dict)
1490  except KeyError:
1491  warning_msg = (
1492  'Found what looks like an unmatched config option '
1493  'substitution reference - domain: %(domain)s, group: '
1494  '%(group)s, option: %(option)s, value: %(value)s. Perhaps '
1495  'the config option to which it refers has yet to be '
1496  'added?')
1497  except (ValueError, TypeError):
1498  warning_msg = (
1499  'Found what looks like an incorrectly constructed '
1500  'config option substitution reference - domain: '
1501  '%(domain)s, group: %(group)s, option: %(option)s, '
1502  'value: %(value)s.')
1503 
1504  if warning_msg:
1505  LOG.warning(warning_msg, {
1506  'domain': domain_id,
1507  'group': each_whitelisted['group'],
1508  'option': each_whitelisted['option'],
1509  'value': original_value})
1510 
1511  return self._list_to_config(whitelisted, sensitive)
1512 
1513  @MEMOIZE_CONFIG
1514  def get_config_with_sensitive_info(self, domain_id):
1515  """Get config for a domain with sensitive info included.
1516 
1517  This method is not exposed via the public API, but is used by the
1518  identity manager to initialize a domain with the fully formed config
1519  options.
1520 
1521  """
1522  return self._get_config_with_sensitive_info(domain_id)
1523 
1524  def get_config_default(self, group=None, option=None):
1525  """Get default config, or partial default config.
1526 
1527  :param group: an optional specific group of options
1528  :param option: an optional specific option within the group
1529 
1530  :returns: a dict of group dicts containing the default options,
1531  filtered by group and option if specified
1532  :raises keystone.exception.InvalidDomainConfig: when the config
1533  and group/option parameters specify an option we do not
1534  support (or one that is not whitelisted).
1535 
1536  An example response::
1537 
1538  {
1539  'ldap': {
1540  'url': 'myurl',
1541  'user_tree_dn': 'OU=myou',
1542  ....},
1543  'identity': {
1544  'driver': 'ldap'}
1545 
1546  }
1547 
1548  """
1549  self._assert_valid_group_and_option(group, option)
1550  config_list = []
1551  if group:
1552  if option:
1553  if option not in self.whitelisted_options[group]:
1554  msg = _('Reading the default for option %(option)s in '
1555  'group %(group)s is not supported') % {
1556  'option': option, 'group': group}
1557  raise exception.InvalidDomainConfig(reason=msg)
1558  config_list.append(self._option_dict(group, option))
1559  else:
1560  for each_option in self.whitelisted_options[group]:
1561  config_list.append(self._option_dict(group, each_option))
1562  else:
1563  for each_group in self.whitelisted_options:
1564  for each_option in self.whitelisted_options[each_group]:
1565  config_list.append(
1566  self._option_dict(each_group, each_option)
1567  )
1568 
1569  return self._list_to_config(config_list, req_option=option)
keystone.exception.ProjectTagNotFound
Definition: exception.py:461
keystone.exception.InvalidDomainConfig
Definition: exception.py:377
keystone.resource.core.Manager._get_domain_from_project
def _get_domain_from_project(self, project_ref)
Definition: core.py:735
keystone.resource.core.DomainConfigManager.get_config
def get_config(self, domain_id, group=None, option=None)
Definition: core.py:1227
keystone.resource.core.DomainConfigManager.create_config
def create_config(self, domain_id, config)
Definition: core.py:1202
keystone.resource.core.Manager.create_project
def create_project(self, project_id, project, initiator=None)
Definition: core.py:187
keystone.resource.core.Manager._assert_all_parents_are_enabled
def _assert_all_parents_are_enabled(self, project_id)
Definition: core.py:280
keystone.resource.core.Manager._only_allow_enabled_to_update_cascade
def _only_allow_enabled_to_update_cascade(self, project, original_project)
Definition: core.py:416
keystone.resource.core.Manager.get_domain
def get_domain(self, domain_id)
Definition: core.py:709
keystone.resource.core.DomainConfigManager.get_security_compliance_config
def get_security_compliance_config(self, domain_id, group, option=None)
Definition: core.py:1269
keystone.resource.core.Manager.get_project
def get_project(self, project_id)
Definition: core.py:902
keystone.resource.core.Manager.create_domain
def create_domain(self, domain_id, domain, initiator=None)
Definition: core.py:760
keystone.resource.core.DomainConfigManager.get_config_with_sensitive_info
def get_config_with_sensitive_info(self, domain_id)
Definition: core.py:1514
keystone.resource.core.Manager.list_projects_in_domain
def list_projects_in_domain(self, domain_id)
Definition: core.py:894
keystone.resource.core.Manager._delete_domain_contents
def _delete_domain_contents(self, domain_id)
Definition: core.py:835
keystone.token
Definition: __init__.py:1
keystone.resource.core.Manager.get_projects_in_subtree_as_ids
def get_projects_in_subtree_as_ids(self, project_id)
Definition: core.py:649
keystone.resource.core.Manager._get_hierarchy_depth
def _get_hierarchy_depth(self, parents_list)
Definition: core.py:59
keystone.resource.core.Manager._check_whole_subtree_is_disabled
def _check_whole_subtree_is_disabled(self, project_id, subtree_list=None)
Definition: core.py:292
keystone.resource.core.DomainConfigManager._option_dict
def _option_dict(self, group, option)
Definition: core.py:1156
keystone.exception.ForbiddenNotSecurity
Definition: exception.py:197
keystone.resource.core.Manager._build_subtree_as_ids_dict
def _build_subtree_as_ids_dict(self, project_id, subtree_by_parent)
Definition: core.py:630
keystone.resource.core.Manager._DOMAIN
string _DOMAIN
Definition: core.py:51
keystone.resource.core.DomainConfigManager.sensitive_options
dictionary sensitive_options
Definition: core.py:1072
keystone.resource.core.Manager.delete_project
def delete_project(self, project_id, initiator=None, cascade=False)
Definition: core.py:468
keystone.resource.core.Manager._post_delete_cleanup_project
def _post_delete_cleanup_project(self, project_id, project, initiator=None)
Definition: core.py:451
keystone.resource.core.Manager.assert_domain_enabled
def assert_domain_enabled(self, domain_id, domain=None)
Definition: core.py:235
keystone.resource.core.DomainConfigManager.whitelisted_options
dictionary whitelisted_options
Definition: core.py:1048
keystone.resource.core.Manager._raise_reserved_character_exception
def _raise_reserved_character_exception(self, entity_type, name)
Definition: core.py:166
keystone.resource.core.DomainConfigManager.update_config
def update_config(self, domain_id, config, group=None, option=None)
Definition: core.py:1316
keystone.resource.core.DomainConfigManager._config_to_list
def _config_to_list(self, config)
Definition: core.py:1144
keystone.resource.core.Manager._is_immutable
def _is_immutable(self, project_ref)
Definition: core.py:288
keystone.resource.core.Manager._delete_domain
def _delete_domain(self, domain, initiator=None)
Definition: core.py:804
keystone.resource.core.Manager.list_project_parents
def list_project_parents(self, project_id, user_id=None, include_limits=False)
Definition: core.py:564
keystone.resource.core.Manager.update_domain
def update_domain(self, domain_id, domain, initiator=None)
Definition: core.py:777
keystone.resource.core.Manager._delete_project
def _delete_project(self, project, initiator=None, cascade=False)
Definition: core.py:484
keystone.exception.ValidationError
Definition: exception.py:98
keystone.resource.core.Manager.list_domains
def list_domains(self, hints=None)
Definition: core.py:771
keystone.resource.core.Manager._PROJECT_TAG
string _PROJECT_TAG
Definition: core.py:53
keystone.exception.UnexpectedError
Definition: exception.py:566
keystone.exception.ResourceUpdateForbidden
Definition: exception.py:715
keystone.resource.core.Manager.check_project_depth
def check_project_depth(self, max_depth=None)
Definition: core.py:1016
keystone.resource.core.DomainConfigManager._is_sensitive
def _is_sensitive(self, group, option)
Definition: core.py:1141
keystone.resource.core.Manager._build_parents_as_ids_dict
def _build_parents_as_ids_dict(self, project, parents_by_id)
Definition: core.py:575
keystone.resource.core.Manager._include_limits
def _include_limits(self, projects)
Definition: core.py:551
keystone.resource.core.Manager
Definition: core.py:40
keystone.resource.core.Manager.get_project_tag
def get_project_tag(self, project_id, tag_name)
Definition: core.py:948
keystone.resource.core.Manager._assert_regular_project_constraints
def _assert_regular_project_constraints(self, project_ref)
Definition: core.py:109
keystone.exception.Conflict
Definition: exception.py:559
keystone.resource.core.Manager._generate_project_name_conflict_msg
def _generate_project_name_conflict_msg(self, project)
Definition: core.py:175
keystone.resource.core.Manager.__init__
def __init__(self)
Definition: core.py:55
keystone.resource.core.DomainConfigManager.get_config_default
def get_config_default(self, group=None, option=None)
Definition: core.py:1524
keystone.resource.core.Manager.list_project_tags
def list_project_tags(self, project_id)
Definition: core.py:963
keystone.exception.DomainNotFound
Definition: exception.py:453
keystone.resource.core.Manager._update_project
def _update_project(self, project_id, project, initiator=None, cascade=False)
Definition: core.py:299
keystone.exception.DomainConfigNotFound
Definition: exception.py:538
keystone.resource.core.Manager.create_project_tag
def create_project_tag(self, project_id, tag, initiator=None)
Definition: core.py:925
keystone.resource.core.DomainConfigManager._assert_valid_group_and_option
def _assert_valid_group_and_option(self, group, option)
Definition: core.py:1105
keystone.resource.core.DomainConfigManager._list_to_config
def _list_to_config(self, whitelisted, sensitive=None, req_option=None)
Definition: core.py:1161
keystone.resource.core.Manager.delete_project_tag
def delete_project_tag(self, project_id, tag)
Definition: core.py:993
keystone.resource.core.Manager.assert_domain_not_federated
def assert_domain_not_federated(self, domain_id, domain)
Definition: core.py:245
keystone.resource.core.Manager.list_projects_acting_as_domain
def list_projects_acting_as_domain(self, hints=None)
Definition: core.py:897
keystone.resource.core.Manager._assert_is_domain_project_constraints
def _assert_is_domain_project_constraints(self, project_ref)
Definition: core.py:84
keystone.resource.core.Manager._assert_max_hierarchy_depth
def _assert_max_hierarchy_depth(self, project_id, parents_list=None)
Definition: core.py:62
keystone.resource.core.DomainConfigManager.__init__
def __init__(self)
Definition: core.py:1077
keystone.resource.core.Manager.assert_project_enabled
def assert_project_enabled(self, project_id, project=None)
Definition: core.py:266
keystone.exception.LimitTreeExceedError
Definition: exception.py:385
keystone.resource.core.Manager._update_project_enabled_cascade
def _update_project_enabled_cascade(self, project_id, enabled)
Definition: core.py:424
keystone.resource.core.DomainConfigManager
Definition: core.py:1028
keystone.conf
Definition: __init__.py:1
keystone.resource.core.DomainConfigManager._get_config_with_sensitive_info
def _get_config_with_sensitive_info(self, domain_id, group=None, option=None)
Definition: core.py:1454
keystone.resource.core.DomainConfigManager.delete_config
def delete_config(self, domain_id, group=None, option=None)
Definition: core.py:1414
keystone.resource.backends
Definition: __init__.py:1
keystone.i18n._
_
Definition: i18n.py:29
keystone.resource.core.Manager._filter_projects_list
def _filter_projects_list(self, projects_list, user_id)
Definition: core.py:535
keystone.resource.core.Manager.update_project_tags
def update_project_tags(self, project_id, tags, initiator=None)
Definition: core.py:973
keystone.resource.core.Manager.list_projects_in_subtree
def list_projects_in_subtree(self, project_id, user_id=None, include_limits=False)
Definition: core.py:619
keystone.resource.core.Manager._require_matching_domain_id
def _require_matching_domain_id(self, new_ref, orig_ref)
Definition: core.py:909
keystone.resource.core.Manager.update_project
def update_project(self, project_id, project, initiator=None, cascade=False)
Definition: core.py:442
keystone.common
Definition: __init__.py:1
keystone.resource.core.Manager.delete_domain
def delete_domain(self, domain_id, initiator=None)
Definition: core.py:795
keystone.common.resource_options
Definition: __init__.py:1
keystone.exception.ProjectNotFound
Definition: exception.py:457
keystone.i18n
Definition: i18n.py:1
keystone.resource.core.Manager.get_domain_by_name
def get_domain_by_name(self, domain_name)
Definition: core.py:724
keystone.resource.core.Manager._PROJECT
string _PROJECT
Definition: core.py:52
keystone.resource.core.Manager.get_project_parents_as_ids
def get_project_parents_as_ids(self, project)
Definition: core.py:590
keystone.resource.core.Manager.get_project_by_name
def get_project_by_name(self, project_name, domain_id)
Definition: core.py:906
keystone.resource.core.DomainConfigManager._assert_valid_config
def _assert_valid_config(self, config)
Definition: core.py:1080
keystone.resource.core.Manager._enforce_project_constraints
def _enforce_project_constraints(self, project_ref)
Definition: core.py:146
keystone.resource.core.Manager._assert_valid_project_id
def _assert_valid_project_id(self, project_id)
Definition: core.py:544
keystone.resource.core.Manager.list_projects
def list_projects(self, hints=None)
Definition: core.py:876
keystone.resource.core.Manager.list_domains_from_ids
def list_domains_from_ids(self, domain_ids)
Definition: core.py:689