aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/resource
diff options
context:
space:
mode:
Diffstat (limited to 'keystone-moon/keystone/resource')
-rw-r--r--keystone-moon/keystone/resource/V8_backends/__init__.py0
-rw-r--r--keystone-moon/keystone/resource/V8_backends/sql.py260
-rw-r--r--keystone-moon/keystone/resource/__init__.py1
-rw-r--r--keystone-moon/keystone/resource/backends/sql.py239
-rw-r--r--keystone-moon/keystone/resource/config_backends/sql.py28
-rw-r--r--keystone-moon/keystone/resource/controllers.py62
-rw-r--r--keystone-moon/keystone/resource/core.py1321
-rw-r--r--keystone-moon/keystone/resource/routers.py31
-rw-r--r--keystone-moon/keystone/resource/schema.py8
9 files changed, 1508 insertions, 442 deletions
diff --git a/keystone-moon/keystone/resource/V8_backends/__init__.py b/keystone-moon/keystone/resource/V8_backends/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone-moon/keystone/resource/V8_backends/__init__.py
diff --git a/keystone-moon/keystone/resource/V8_backends/sql.py b/keystone-moon/keystone/resource/V8_backends/sql.py
new file mode 100644
index 00000000..6c9b7912
--- /dev/null
+++ b/keystone-moon/keystone/resource/V8_backends/sql.py
@@ -0,0 +1,260 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_log import log
+
+from keystone.common import clean
+from keystone.common import driver_hints
+from keystone.common import sql
+from keystone import exception
+from keystone.i18n import _LE
+from keystone import resource as keystone_resource
+
+
+LOG = log.getLogger(__name__)
+
+
+class Resource(keystone_resource.ResourceDriverV8):
+
+ def default_assignment_driver(self):
+ return 'sql'
+
+ def _get_project(self, session, project_id):
+ project_ref = session.query(Project).get(project_id)
+ if project_ref is None:
+ raise exception.ProjectNotFound(project_id=project_id)
+ return project_ref
+
+ def get_project(self, tenant_id):
+ with sql.session_for_read() as session:
+ return self._get_project(session, tenant_id).to_dict()
+
+ def get_project_by_name(self, tenant_name, domain_id):
+ with sql.session_for_read() as session:
+ query = session.query(Project)
+ query = query.filter_by(name=tenant_name)
+ query = query.filter_by(domain_id=domain_id)
+ try:
+ project_ref = query.one()
+ except sql.NotFound:
+ raise exception.ProjectNotFound(project_id=tenant_name)
+ return project_ref.to_dict()
+
+ @driver_hints.truncated
+ def list_projects(self, hints):
+ with sql.session_for_read() as session:
+ query = session.query(Project)
+ project_refs = sql.filter_limit_query(Project, query, hints)
+ return [project_ref.to_dict() for project_ref in project_refs]
+
+ def list_projects_from_ids(self, ids):
+ if not ids:
+ return []
+ else:
+ with sql.session_for_read() as session:
+ query = session.query(Project)
+ query = query.filter(Project.id.in_(ids))
+ return [project_ref.to_dict() for project_ref in query.all()]
+
+ def list_project_ids_from_domain_ids(self, domain_ids):
+ if not domain_ids:
+ return []
+ else:
+ with sql.session_for_read() as session:
+ query = session.query(Project.id)
+ query = (
+ query.filter(Project.domain_id.in_(domain_ids)))
+ return [x.id for x in query.all()]
+
+ def list_projects_in_domain(self, domain_id):
+ with sql.session_for_read() as session:
+ self._get_domain(session, domain_id)
+ query = session.query(Project)
+ project_refs = query.filter_by(domain_id=domain_id)
+ return [project_ref.to_dict() for project_ref in project_refs]
+
+ def _get_children(self, session, project_ids):
+ query = session.query(Project)
+ query = query.filter(Project.parent_id.in_(project_ids))
+ project_refs = query.all()
+ return [project_ref.to_dict() for project_ref in project_refs]
+
+ def list_projects_in_subtree(self, project_id):
+ with sql.session_for_read() as session:
+ children = self._get_children(session, [project_id])
+ subtree = []
+ examined = set([project_id])
+ while children:
+ children_ids = set()
+ for ref in children:
+ if ref['id'] in examined:
+ msg = _LE('Circular reference or a repeated '
+ 'entry found in projects hierarchy - '
+ '%(project_id)s.')
+ LOG.error(msg, {'project_id': ref['id']})
+ return
+ children_ids.add(ref['id'])
+
+ examined.update(children_ids)
+ subtree += children
+ children = self._get_children(session, children_ids)
+ return subtree
+
+ def list_project_parents(self, project_id):
+ with sql.session_for_read() as session:
+ project = self._get_project(session, project_id).to_dict()
+ 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(
+ session, project['parent_id']).to_dict()
+ parents.append(parent_project)
+ project = parent_project
+ return parents
+
+ def is_leaf_project(self, project_id):
+ with sql.session_for_read() as session:
+ project_refs = self._get_children(session, [project_id])
+ return not project_refs
+
+ # CRUD
+ @sql.handle_conflicts(conflict_type='project')
+ def create_project(self, tenant_id, tenant):
+ tenant['name'] = clean.project_name(tenant['name'])
+ with sql.session_for_write() as session:
+ tenant_ref = Project.from_dict(tenant)
+ session.add(tenant_ref)
+ return tenant_ref.to_dict()
+
+ @sql.handle_conflicts(conflict_type='project')
+ def update_project(self, tenant_id, tenant):
+ if 'name' in tenant:
+ tenant['name'] = clean.project_name(tenant['name'])
+
+ with sql.session_for_write() as session:
+ tenant_ref = self._get_project(session, tenant_id)
+ old_project_dict = tenant_ref.to_dict()
+ for k in tenant:
+ old_project_dict[k] = tenant[k]
+ new_project = Project.from_dict(old_project_dict)
+ for attr in Project.attributes:
+ if attr != 'id':
+ setattr(tenant_ref, attr, getattr(new_project, attr))
+ tenant_ref.extra = new_project.extra
+ return tenant_ref.to_dict(include_extra_dict=True)
+
+ @sql.handle_conflicts(conflict_type='project')
+ def delete_project(self, tenant_id):
+ with sql.session_for_write() as session:
+ tenant_ref = self._get_project(session, tenant_id)
+ session.delete(tenant_ref)
+
+ # domain crud
+
+ @sql.handle_conflicts(conflict_type='domain')
+ def create_domain(self, domain_id, domain):
+ with sql.session_for_write() as session:
+ ref = Domain.from_dict(domain)
+ session.add(ref)
+ return ref.to_dict()
+
+ @driver_hints.truncated
+ def list_domains(self, hints):
+ with sql.session_for_read() as session:
+ query = session.query(Domain)
+ refs = sql.filter_limit_query(Domain, query, hints)
+ return [ref.to_dict() for ref in refs]
+
+ def list_domains_from_ids(self, ids):
+ if not ids:
+ return []
+ else:
+ with sql.session_for_read() as session:
+ query = session.query(Domain)
+ query = query.filter(Domain.id.in_(ids))
+ domain_refs = query.all()
+ return [domain_ref.to_dict() for domain_ref in domain_refs]
+
+ def _get_domain(self, session, domain_id):
+ ref = session.query(Domain).get(domain_id)
+ if ref is None:
+ raise exception.DomainNotFound(domain_id=domain_id)
+ return ref
+
+ def get_domain(self, domain_id):
+ with sql.session_for_read() as session:
+ return self._get_domain(session, domain_id).to_dict()
+
+ def get_domain_by_name(self, domain_name):
+ with sql.session_for_read() as session:
+ try:
+ ref = (session.query(Domain).
+ filter_by(name=domain_name).one())
+ except sql.NotFound:
+ raise exception.DomainNotFound(domain_id=domain_name)
+ return ref.to_dict()
+
+ @sql.handle_conflicts(conflict_type='domain')
+ def update_domain(self, domain_id, domain):
+ with sql.session_for_write() as session:
+ ref = self._get_domain(session, domain_id)
+ old_dict = ref.to_dict()
+ for k in domain:
+ old_dict[k] = domain[k]
+ new_domain = Domain.from_dict(old_dict)
+ for attr in Domain.attributes:
+ if attr != 'id':
+ setattr(ref, attr, getattr(new_domain, attr))
+ ref.extra = new_domain.extra
+ return ref.to_dict()
+
+ def delete_domain(self, domain_id):
+ with sql.session_for_write() as session:
+ ref = self._get_domain(session, domain_id)
+ session.delete(ref)
+
+
+class Domain(sql.ModelBase, sql.DictBase):
+ __tablename__ = 'domain'
+ attributes = ['id', 'name', 'enabled']
+ id = sql.Column(sql.String(64), primary_key=True)
+ name = sql.Column(sql.String(64), nullable=False)
+ enabled = sql.Column(sql.Boolean, default=True, nullable=False)
+ extra = sql.Column(sql.JsonBlob())
+ __table_args__ = (sql.UniqueConstraint('name'),)
+
+
+class Project(sql.ModelBase, sql.DictBase):
+ __tablename__ = 'project'
+ attributes = ['id', 'name', 'domain_id', 'description', 'enabled',
+ 'parent_id', 'is_domain']
+ id = sql.Column(sql.String(64), primary_key=True)
+ name = sql.Column(sql.String(64), nullable=False)
+ domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'),
+ nullable=False)
+ description = sql.Column(sql.Text())
+ enabled = sql.Column(sql.Boolean)
+ extra = sql.Column(sql.JsonBlob())
+ parent_id = sql.Column(sql.String(64), sql.ForeignKey('project.id'))
+ is_domain = sql.Column(sql.Boolean, default=False, nullable=False,
+ server_default='0')
+ # Unique constraint across two columns to create the separation
+ # rather than just only 'name' being unique
+ __table_args__ = (sql.UniqueConstraint('domain_id', 'name'),)
diff --git a/keystone-moon/keystone/resource/__init__.py b/keystone-moon/keystone/resource/__init__.py
index c0070a12..7f879f4b 100644
--- a/keystone-moon/keystone/resource/__init__.py
+++ b/keystone-moon/keystone/resource/__init__.py
@@ -12,4 +12,3 @@
from keystone.resource import controllers # noqa
from keystone.resource.core import * # noqa
-from keystone.resource import routers # noqa
diff --git a/keystone-moon/keystone/resource/backends/sql.py b/keystone-moon/keystone/resource/backends/sql.py
index 59bab372..39bb4f3b 100644
--- a/keystone-moon/keystone/resource/backends/sql.py
+++ b/keystone-moon/keystone/resource/backends/sql.py
@@ -10,87 +10,123 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_config import cfg
from oslo_log import log
from keystone.common import clean
+from keystone.common import driver_hints
from keystone.common import sql
from keystone import exception
-from keystone.i18n import _LE
+from keystone.i18n import _LE, _LW
from keystone import resource as keystone_resource
-CONF = cfg.CONF
LOG = log.getLogger(__name__)
-class Resource(keystone_resource.ResourceDriverV8):
+class Resource(keystone_resource.ResourceDriverV9):
def default_assignment_driver(self):
return 'sql'
+ def _encode_domain_id(self, ref):
+ if 'domain_id' in ref and ref['domain_id'] is None:
+ new_ref = ref.copy()
+ new_ref['domain_id'] = keystone_resource.NULL_DOMAIN_ID
+ return new_ref
+ else:
+ return ref
+
+ def _is_hidden_ref(self, ref):
+ return ref.id == keystone_resource.NULL_DOMAIN_ID
+
def _get_project(self, session, project_id):
project_ref = session.query(Project).get(project_id)
- if project_ref is None:
+ if project_ref is None or self._is_hidden_ref(project_ref):
raise exception.ProjectNotFound(project_id=project_id)
return project_ref
- def get_project(self, tenant_id):
- with sql.transaction() as session:
- return self._get_project(session, tenant_id).to_dict()
+ def get_project(self, project_id):
+ with sql.session_for_read() as session:
+ return self._get_project(session, project_id).to_dict()
- def get_project_by_name(self, tenant_name, domain_id):
- with sql.transaction() as session:
+ def get_project_by_name(self, project_name, domain_id):
+ with sql.session_for_read() as session:
query = session.query(Project)
- query = query.filter_by(name=tenant_name)
- query = query.filter_by(domain_id=domain_id)
+ query = query.filter_by(name=project_name)
+ if domain_id is None:
+ query = query.filter_by(
+ domain_id=keystone_resource.NULL_DOMAIN_ID)
+ else:
+ query = query.filter_by(domain_id=domain_id)
try:
project_ref = query.one()
except sql.NotFound:
- raise exception.ProjectNotFound(project_id=tenant_name)
+ raise exception.ProjectNotFound(project_id=project_name)
+
+ if self._is_hidden_ref(project_ref):
+ raise exception.ProjectNotFound(project_id=project_name)
return project_ref.to_dict()
- @sql.truncated
+ @driver_hints.truncated
def list_projects(self, hints):
- with sql.transaction() as session:
+ # If there is a filter on domain_id and the value is None, then to
+ # ensure that the sql filtering works correctly, we need to patch
+ # the value to be NULL_DOMAIN_ID. This is safe to do here since we
+ # know we are able to satisfy any filter of this type in the call to
+ # filter_limit_query() below, which will remove the filter from the
+ # hints (hence ensuring our substitution is not exposed to the caller).
+ for f in hints.filters:
+ if (f['name'] == 'domain_id' and f['value'] is None):
+ f['value'] = keystone_resource.NULL_DOMAIN_ID
+ with sql.session_for_read() as session:
query = session.query(Project)
project_refs = sql.filter_limit_query(Project, query, hints)
- return [project_ref.to_dict() for project_ref in project_refs]
+ return [project_ref.to_dict() for project_ref in project_refs
+ if not self._is_hidden_ref(project_ref)]
def list_projects_from_ids(self, ids):
if not ids:
return []
else:
- with sql.transaction() as session:
+ with sql.session_for_read() as session:
query = session.query(Project)
query = query.filter(Project.id.in_(ids))
- return [project_ref.to_dict() for project_ref in query.all()]
+ return [project_ref.to_dict() for project_ref in query.all()
+ if not self._is_hidden_ref(project_ref)]
def list_project_ids_from_domain_ids(self, domain_ids):
if not domain_ids:
return []
else:
- with sql.transaction() as session:
+ with sql.session_for_read() as session:
query = session.query(Project.id)
query = (
query.filter(Project.domain_id.in_(domain_ids)))
- return [x.id for x in query.all()]
+ return [x.id for x in query.all()
+ if not self._is_hidden_ref(x)]
def list_projects_in_domain(self, domain_id):
- with sql.transaction() as session:
- self._get_domain(session, domain_id)
+ with sql.session_for_read() as session:
+ try:
+ self._get_project(session, domain_id)
+ except exception.ProjectNotFound:
+ raise exception.DomainNotFound(domain_id=domain_id)
query = session.query(Project)
- project_refs = query.filter_by(domain_id=domain_id)
+ project_refs = query.filter(Project.domain_id == domain_id)
return [project_ref.to_dict() for project_ref in project_refs]
- def _get_children(self, session, project_ids):
+ def list_projects_acting_as_domain(self, hints):
+ hints.add_filter('is_domain', True)
+ return self.list_projects(hints)
+
+ def _get_children(self, session, project_ids, domain_id=None):
query = session.query(Project)
query = query.filter(Project.parent_id.in_(project_ids))
project_refs = query.all()
return [project_ref.to_dict() for project_ref in project_refs]
def list_projects_in_subtree(self, project_id):
- with sql.transaction() as session:
+ with sql.session_for_read() as session:
children = self._get_children(session, [project_id])
subtree = []
examined = set([project_id])
@@ -111,7 +147,7 @@ class Resource(keystone_resource.ResourceDriverV8):
return subtree
def list_project_parents(self, project_id):
- with sql.transaction() as session:
+ with sql.session_for_read() as session:
project = self._get_project(session, project_id).to_dict()
parents = []
examined = set()
@@ -131,105 +167,61 @@ class Resource(keystone_resource.ResourceDriverV8):
return parents
def is_leaf_project(self, project_id):
- with sql.transaction() as session:
+ with sql.session_for_read() as session:
project_refs = self._get_children(session, [project_id])
return not project_refs
# CRUD
@sql.handle_conflicts(conflict_type='project')
- def create_project(self, tenant_id, tenant):
- tenant['name'] = clean.project_name(tenant['name'])
- with sql.transaction() as session:
- tenant_ref = Project.from_dict(tenant)
- session.add(tenant_ref)
- return tenant_ref.to_dict()
+ def create_project(self, project_id, project):
+ project['name'] = clean.project_name(project['name'])
+ new_project = self._encode_domain_id(project)
+ with sql.session_for_write() as session:
+ project_ref = Project.from_dict(new_project)
+ session.add(project_ref)
+ return project_ref.to_dict()
@sql.handle_conflicts(conflict_type='project')
- def update_project(self, tenant_id, tenant):
- if 'name' in tenant:
- tenant['name'] = clean.project_name(tenant['name'])
-
- with sql.transaction() as session:
- tenant_ref = self._get_project(session, tenant_id)
- old_project_dict = tenant_ref.to_dict()
- for k in tenant:
- old_project_dict[k] = tenant[k]
+ def update_project(self, project_id, project):
+ if 'name' in project:
+ project['name'] = clean.project_name(project['name'])
+
+ update_project = self._encode_domain_id(project)
+ with sql.session_for_write() as session:
+ project_ref = self._get_project(session, project_id)
+ old_project_dict = project_ref.to_dict()
+ for k in update_project:
+ old_project_dict[k] = update_project[k]
+ # When we read the old_project_dict, any "null" domain_id will have
+ # been decoded, so we need to re-encode it
+ old_project_dict = self._encode_domain_id(old_project_dict)
new_project = Project.from_dict(old_project_dict)
for attr in Project.attributes:
if attr != 'id':
- setattr(tenant_ref, attr, getattr(new_project, attr))
- tenant_ref.extra = new_project.extra
- return tenant_ref.to_dict(include_extra_dict=True)
+ setattr(project_ref, attr, getattr(new_project, attr))
+ project_ref.extra = new_project.extra
+ return project_ref.to_dict(include_extra_dict=True)
@sql.handle_conflicts(conflict_type='project')
- def delete_project(self, tenant_id):
- with sql.transaction() as session:
- tenant_ref = self._get_project(session, tenant_id)
- session.delete(tenant_ref)
-
- # domain crud
-
- @sql.handle_conflicts(conflict_type='domain')
- def create_domain(self, domain_id, domain):
- with sql.transaction() as session:
- ref = Domain.from_dict(domain)
- session.add(ref)
- return ref.to_dict()
-
- @sql.truncated
- def list_domains(self, hints):
- with sql.transaction() as session:
- query = session.query(Domain)
- refs = sql.filter_limit_query(Domain, query, hints)
- return [ref.to_dict() for ref in refs]
-
- def list_domains_from_ids(self, ids):
- if not ids:
- return []
- else:
- with sql.transaction() as session:
- query = session.query(Domain)
- query = query.filter(Domain.id.in_(ids))
- domain_refs = query.all()
- return [domain_ref.to_dict() for domain_ref in domain_refs]
-
- def _get_domain(self, session, domain_id):
- ref = session.query(Domain).get(domain_id)
- if ref is None:
- raise exception.DomainNotFound(domain_id=domain_id)
- return ref
-
- def get_domain(self, domain_id):
- with sql.transaction() as session:
- return self._get_domain(session, domain_id).to_dict()
-
- def get_domain_by_name(self, domain_name):
- with sql.transaction() as session:
- try:
- ref = (session.query(Domain).
- filter_by(name=domain_name).one())
- except sql.NotFound:
- raise exception.DomainNotFound(domain_id=domain_name)
- return ref.to_dict()
-
- @sql.handle_conflicts(conflict_type='domain')
- def update_domain(self, domain_id, domain):
- with sql.transaction() as session:
- ref = self._get_domain(session, domain_id)
- old_dict = ref.to_dict()
- for k in domain:
- old_dict[k] = domain[k]
- new_domain = Domain.from_dict(old_dict)
- for attr in Domain.attributes:
- if attr != 'id':
- setattr(ref, attr, getattr(new_domain, attr))
- ref.extra = new_domain.extra
- return ref.to_dict()
+ def delete_project(self, project_id):
+ with sql.session_for_write() as session:
+ project_ref = self._get_project(session, project_id)
+ session.delete(project_ref)
- def delete_domain(self, domain_id):
- with sql.transaction() as session:
- ref = self._get_domain(session, domain_id)
- session.delete(ref)
+ @sql.handle_conflicts(conflict_type='project')
+ def delete_projects_from_ids(self, project_ids):
+ if not project_ids:
+ return
+ with sql.session_for_write() as session:
+ query = session.query(Project).filter(Project.id.in_(
+ project_ids))
+ project_ids_from_bd = [p['id'] for p in query.all()]
+ for project_id in project_ids:
+ if (project_id not in project_ids_from_bd or
+ project_id == keystone_resource.NULL_DOMAIN_ID):
+ LOG.warning(_LW('Project %s does not exist and was not '
+ 'deleted.') % project_id)
+ query.delete(synchronize_session=False)
class Domain(sql.ModelBase, sql.DictBase):
@@ -239,22 +231,37 @@ class Domain(sql.ModelBase, sql.DictBase):
name = sql.Column(sql.String(64), nullable=False)
enabled = sql.Column(sql.Boolean, default=True, nullable=False)
extra = sql.Column(sql.JsonBlob())
- __table_args__ = (sql.UniqueConstraint('name'), {})
+ __table_args__ = (sql.UniqueConstraint('name'),)
class Project(sql.ModelBase, sql.DictBase):
+ # NOTE(henry-nash): From the manager and above perspective, the domain_id
+ # is nullable. However, to ensure uniqueness in multi-process
+ # configurations, it is better to still use the sql uniqueness constraint.
+ # Since the support for a nullable component of a uniqueness constraint
+ # across different sql databases is mixed, we instead store a special value
+ # to represent null, as defined in NULL_DOMAIN_ID above.
+
+ def to_dict(self, include_extra_dict=False):
+ d = super(Project, self).to_dict(
+ include_extra_dict=include_extra_dict)
+ if d['domain_id'] == keystone_resource.NULL_DOMAIN_ID:
+ d['domain_id'] = None
+ return d
+
__tablename__ = 'project'
attributes = ['id', 'name', 'domain_id', 'description', 'enabled',
'parent_id', 'is_domain']
id = sql.Column(sql.String(64), primary_key=True)
name = sql.Column(sql.String(64), nullable=False)
- domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'),
+ domain_id = sql.Column(sql.String(64), sql.ForeignKey('project.id'),
nullable=False)
description = sql.Column(sql.Text())
enabled = sql.Column(sql.Boolean)
extra = sql.Column(sql.JsonBlob())
parent_id = sql.Column(sql.String(64), sql.ForeignKey('project.id'))
- is_domain = sql.Column(sql.Boolean, default=False, nullable=False)
+ is_domain = sql.Column(sql.Boolean, default=False, nullable=False,
+ server_default='0')
# Unique constraint across two columns to create the separation
# rather than just only 'name' being unique
- __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {})
+ __table_args__ = (sql.UniqueConstraint('domain_id', 'name'),)
diff --git a/keystone-moon/keystone/resource/config_backends/sql.py b/keystone-moon/keystone/resource/config_backends/sql.py
index 7c296074..6413becc 100644
--- a/keystone-moon/keystone/resource/config_backends/sql.py
+++ b/keystone-moon/keystone/resource/config_backends/sql.py
@@ -59,12 +59,12 @@ class DomainConfig(resource.DomainConfigDriverV8):
@sql.handle_conflicts(conflict_type='domain_config')
def create_config_option(self, domain_id, group, option, value,
sensitive=False):
- with sql.transaction() as session:
+ with sql.session_for_write() as session:
config_table = self.choose_table(sensitive)
ref = config_table(domain_id=domain_id, group=group,
option=option, value=value)
session.add(ref)
- return ref.to_dict()
+ return ref.to_dict()
def _get_config_option(self, session, domain_id, group, option, sensitive):
try:
@@ -80,14 +80,14 @@ class DomainConfig(resource.DomainConfigDriverV8):
return ref
def get_config_option(self, domain_id, group, option, sensitive=False):
- with sql.transaction() as session:
+ with sql.session_for_read() as session:
ref = self._get_config_option(session, domain_id, group, option,
sensitive)
- return ref.to_dict()
+ return ref.to_dict()
def list_config_options(self, domain_id, group=None, option=None,
sensitive=False):
- with sql.transaction() as session:
+ with sql.session_for_read() as session:
config_table = self.choose_table(sensitive)
query = session.query(config_table)
query = query.filter_by(domain_id=domain_id)
@@ -99,11 +99,11 @@ class DomainConfig(resource.DomainConfigDriverV8):
def update_config_option(self, domain_id, group, option, value,
sensitive=False):
- with sql.transaction() as session:
+ with sql.session_for_write() as session:
ref = self._get_config_option(session, domain_id, group, option,
sensitive)
ref.value = value
- return ref.to_dict()
+ return ref.to_dict()
def delete_config_options(self, domain_id, group=None, option=None,
sensitive=False):
@@ -114,7 +114,7 @@ class DomainConfig(resource.DomainConfigDriverV8):
if there was nothing to delete.
"""
- with sql.transaction() as session:
+ with sql.session_for_write() as session:
config_table = self.choose_table(sensitive)
query = session.query(config_table)
query = query.filter_by(domain_id=domain_id)
@@ -126,25 +126,25 @@ class DomainConfig(resource.DomainConfigDriverV8):
def obtain_registration(self, domain_id, type):
try:
- with sql.transaction() as session:
+ with sql.session_for_write() as session:
ref = ConfigRegister(type=type, domain_id=domain_id)
session.add(ref)
return True
- except sql.DBDuplicateEntry:
+ except sql.DBDuplicateEntry: # nosec
+ # Continue on and return False to indicate failure.
pass
return False
def read_registration(self, type):
- with sql.transaction() as session:
+ with sql.session_for_read() as session:
ref = session.query(ConfigRegister).get(type)
if not ref:
raise exception.ConfigRegistrationNotFound()
- return ref.domain_id
+ return ref.domain_id
def release_registration(self, domain_id, type=None):
"""Silently delete anything registered for the domain specified."""
-
- with sql.transaction() as session:
+ with sql.session_for_write() as session:
query = session.query(ConfigRegister)
if type:
query = query.filter_by(type=type)
diff --git a/keystone-moon/keystone/resource/controllers.py b/keystone-moon/keystone/resource/controllers.py
index 4fbeb715..5cabe064 100644
--- a/keystone-moon/keystone/resource/controllers.py
+++ b/keystone-moon/keystone/resource/controllers.py
@@ -18,7 +18,6 @@
import uuid
from oslo_config import cfg
-from oslo_log import log
from keystone.common import controller
from keystone.common import dependency
@@ -31,7 +30,6 @@ from keystone.resource import schema
CONF = cfg.CONF
-LOG = log.getLogger(__name__)
@dependency.requires('resource_api')
@@ -40,13 +38,18 @@ class Tenant(controller.V2Controller):
@controller.v2_deprecated
def get_all_projects(self, context, **kw):
"""Gets a list of all tenants for an admin user."""
+ self.assert_admin(context)
+
if 'name' in context['query_string']:
- return self.get_project_by_name(
- context, context['query_string'].get('name'))
+ return self._get_project_by_name(context['query_string']['name'])
- self.assert_admin(context)
- tenant_refs = self.resource_api.list_projects_in_domain(
- CONF.identity.default_domain_id)
+ try:
+ tenant_refs = self.resource_api.list_projects_in_domain(
+ CONF.identity.default_domain_id)
+ except exception.DomainNotFound:
+ # If the default domain doesn't exist then there are no V2
+ # projects.
+ tenant_refs = []
tenant_refs = [self.v3_to_v2_project(tenant_ref)
for tenant_ref in tenant_refs
if not tenant_ref.get('is_domain')]
@@ -71,12 +74,11 @@ class Tenant(controller.V2Controller):
self._assert_not_is_domain_project(tenant_id, ref)
return {'tenant': self.v3_to_v2_project(ref)}
- @controller.v2_deprecated
- def get_project_by_name(self, context, tenant_name):
- self.assert_admin(context)
+ def _get_project_by_name(self, tenant_name):
# Projects acting as a domain should not be visible via v2
ref = self.resource_api.get_project_by_name(
tenant_name, CONF.identity.default_domain_id)
+ self._assert_not_is_domain_project(ref['id'], ref)
return {'tenant': self.v3_to_v2_project(ref)}
# CRUD Extension
@@ -88,7 +90,15 @@ class Tenant(controller.V2Controller):
msg = _('Name field is required and cannot be empty')
raise exception.ValidationError(message=msg)
+ if 'is_domain' in tenant_ref:
+ msg = _('The creation of projects acting as domains is not '
+ 'allowed in v2.')
+ raise exception.ValidationError(message=msg)
+
self.assert_admin(context)
+
+ self.resource_api.ensure_default_domain_exists()
+
tenant_ref['id'] = tenant_ref.get('id', uuid.uuid4().hex)
initiator = notifications._get_request_audit_info(context)
tenant = self.resource_api.create_project(
@@ -162,11 +172,13 @@ class DomainV3(controller.V3Controller):
@dependency.requires('domain_config_api')
+@dependency.requires('resource_api')
class DomainConfigV3(controller.V3Controller):
member_name = 'config'
@controller.protected()
def create_domain_config(self, context, domain_id, config):
+ self.resource_api.get_domain(domain_id)
original_config = (
self.domain_config_api.get_config_with_sensitive_info(domain_id))
ref = self.domain_config_api.create_config(domain_id, config)
@@ -179,29 +191,39 @@ class DomainConfigV3(controller.V3Controller):
@controller.protected()
def get_domain_config(self, context, domain_id, group=None, option=None):
+ self.resource_api.get_domain(domain_id)
ref = self.domain_config_api.get_config(domain_id, group, option)
return {self.member_name: ref}
@controller.protected()
def update_domain_config(
self, context, domain_id, config, group, option):
+ self.resource_api.get_domain(domain_id)
ref = self.domain_config_api.update_config(
domain_id, config, group, option)
return wsgi.render_response(body={self.member_name: ref})
def update_domain_config_group(self, context, domain_id, group, config):
+ self.resource_api.get_domain(domain_id)
return self.update_domain_config(
context, domain_id, config, group, option=None)
def update_domain_config_only(self, context, domain_id, config):
+ self.resource_api.get_domain(domain_id)
return self.update_domain_config(
context, domain_id, config, group=None, option=None)
@controller.protected()
def delete_domain_config(
self, context, domain_id, group=None, option=None):
+ self.resource_api.get_domain(domain_id)
self.domain_config_api.delete_config(domain_id, group, option)
+ @controller.protected()
+ def get_domain_config_default(self, context, group=None, option=None):
+ ref = self.domain_config_api.get_config_default(group, option)
+ return {self.member_name: ref}
+
@dependency.requires('resource_api')
class ProjectV3(controller.V3Controller):
@@ -216,25 +238,31 @@ class ProjectV3(controller.V3Controller):
@validation.validated(schema.project_create, 'project')
def create_project(self, context, project):
ref = self._assign_unique_id(self._normalize_dict(project))
- ref = self._normalize_domain_id(context, ref)
- if ref.get('is_domain'):
- msg = _('The creation of projects acting as domains is not '
- 'allowed yet.')
- raise exception.NotImplemented(msg)
+ if not ref.get('is_domain'):
+ ref = self._normalize_domain_id(context, ref)
+ # Our API requires that you specify the location in the hierarchy
+ # unambiguously. This could be by parent_id or, if it is a top level
+ # project, just by providing a domain_id.
+ if not ref.get('parent_id'):
+ ref['parent_id'] = ref.get('domain_id')
initiator = notifications._get_request_audit_info(context)
try:
ref = self.resource_api.create_project(ref['id'], ref,
initiator=initiator)
- except exception.DomainNotFound as e:
+ except (exception.DomainNotFound, exception.ProjectNotFound) as e:
raise exception.ValidationError(e)
return ProjectV3.wrap_member(context, ref)
@controller.filterprotected('domain_id', 'enabled', 'name',
- 'parent_id')
+ 'parent_id', 'is_domain')
def list_projects(self, context, filters):
hints = ProjectV3.build_driver_hints(context, filters)
+ # If 'is_domain' has not been included as a query, we default it to
+ # False (which in query terms means '0'
+ if 'is_domain' not in context['query_string']:
+ hints.add_filter('is_domain', '0')
refs = self.resource_api.list_projects(hints=hints)
return ProjectV3.wrap_collection(context, refs, hints=hints)
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
diff --git a/keystone-moon/keystone/resource/routers.py b/keystone-moon/keystone/resource/routers.py
index 8ccd10aa..d58474e2 100644
--- a/keystone-moon/keystone/resource/routers.py
+++ b/keystone-moon/keystone/resource/routers.py
@@ -88,6 +88,37 @@ class Routers(wsgi.RoutersBase):
'config_option')
})
+ self._add_resource(
+ mapper, config_controller,
+ path='/domains/config/default',
+ get_action='get_domain_config_default',
+ rel=json_home.build_v3_resource_relation('domain_config_default'),
+ status=json_home.Status.EXPERIMENTAL)
+
+ self._add_resource(
+ mapper, config_controller,
+ path='/domains/config/{group}/default',
+ get_action='get_domain_config_default',
+ rel=json_home.build_v3_resource_relation(
+ 'domain_config_default_group'),
+ status=json_home.Status.EXPERIMENTAL,
+ path_vars={
+ 'group': config_group_param
+ })
+
+ self._add_resource(
+ mapper, config_controller,
+ path='/domains/config/{group}/{option}/default',
+ get_action='get_domain_config_default',
+ rel=json_home.build_v3_resource_relation(
+ 'domain_config_default_option'),
+ status=json_home.Status.EXPERIMENTAL,
+ path_vars={
+ 'group': config_group_param,
+ 'option': json_home.build_v3_parameter_relation(
+ 'config_option')
+ })
+
routers.append(
router.Router(controllers.ProjectV3(),
'projects', 'project',
diff --git a/keystone-moon/keystone/resource/schema.py b/keystone-moon/keystone/resource/schema.py
index e26a9c4a..7e2cd667 100644
--- a/keystone-moon/keystone/resource/schema.py
+++ b/keystone-moon/keystone/resource/schema.py
@@ -16,10 +16,8 @@ from keystone.common.validation import parameter_types
_project_properties = {
'description': validation.nullable(parameter_types.description),
- # NOTE(lbragstad): domain_id isn't nullable according to some backends.
- # The identity-api should be updated to be consistent with the
- # implementation.
- 'domain_id': parameter_types.id_string,
+ # NOTE(htruta): domain_id is nullable for projects acting as a domain.
+ 'domain_id': validation.nullable(parameter_types.id_string),
'enabled': parameter_types.boolean,
'is_domain': parameter_types.boolean,
'parent_id': validation.nullable(parameter_types.id_string),
@@ -43,7 +41,7 @@ project_create = {
project_update = {
'type': 'object',
'properties': _project_properties,
- # NOTE(lbragstad) Make sure at least one property is being updated
+ # NOTE(lbragstad): Make sure at least one property is being updated
'minProperties': 1,
'additionalProperties': True
}