summaryrefslogtreecommitdiffstats
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/__init__.py15
-rw-r--r--keystone-moon/keystone/resource/backends/__init__.py0
-rw-r--r--keystone-moon/keystone/resource/backends/ldap.py196
-rw-r--r--keystone-moon/keystone/resource/backends/sql.py260
-rw-r--r--keystone-moon/keystone/resource/config_backends/__init__.py0
-rw-r--r--keystone-moon/keystone/resource/config_backends/sql.py119
-rw-r--r--keystone-moon/keystone/resource/controllers.py281
-rw-r--r--keystone-moon/keystone/resource/core.py1324
-rw-r--r--keystone-moon/keystone/resource/routers.py94
-rw-r--r--keystone-moon/keystone/resource/schema.py75
10 files changed, 2364 insertions, 0 deletions
diff --git a/keystone-moon/keystone/resource/__init__.py b/keystone-moon/keystone/resource/__init__.py
new file mode 100644
index 00000000..c0070a12
--- /dev/null
+++ b/keystone-moon/keystone/resource/__init__.py
@@ -0,0 +1,15 @@
+# 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 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/__init__.py b/keystone-moon/keystone/resource/backends/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone-moon/keystone/resource/backends/__init__.py
diff --git a/keystone-moon/keystone/resource/backends/ldap.py b/keystone-moon/keystone/resource/backends/ldap.py
new file mode 100644
index 00000000..434c2b04
--- /dev/null
+++ b/keystone-moon/keystone/resource/backends/ldap.py
@@ -0,0 +1,196 @@
+# 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 __future__ import absolute_import
+
+import uuid
+
+from oslo_config import cfg
+from oslo_log import log
+
+from keystone import clean
+from keystone.common import driver_hints
+from keystone.common import ldap as common_ldap
+from keystone.common import models
+from keystone import exception
+from keystone.i18n import _
+from keystone.identity.backends import ldap as ldap_identity
+from keystone import resource
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+
+class Resource(resource.Driver):
+ def __init__(self):
+ super(Resource, self).__init__()
+ self.LDAP_URL = CONF.ldap.url
+ self.LDAP_USER = CONF.ldap.user
+ self.LDAP_PASSWORD = CONF.ldap.password
+ self.suffix = CONF.ldap.suffix
+
+ # This is the only deep dependency from resource back to identity.
+ # This is safe to do since if you are using LDAP for resource, it is
+ # required that you are using it for identity as well.
+ self.user = ldap_identity.UserApi(CONF)
+
+ self.project = ProjectApi(CONF)
+
+ def default_assignment_driver(self):
+ return 'keystone.assignment.backends.ldap.Assignment'
+
+ def _set_default_parent_project(self, ref):
+ """If the parent project ID has not been set, set it to None."""
+ if isinstance(ref, dict):
+ if 'parent_id' not in ref:
+ ref = dict(ref, parent_id=None)
+ return ref
+ elif isinstance(ref, list):
+ return [self._set_default_parent_project(x) for x in ref]
+ else:
+ raise ValueError(_('Expected dict or list: %s') % type(ref))
+
+ def _validate_parent_project_is_none(self, ref):
+ """If a parent_id different from None was given,
+ raises InvalidProjectException.
+
+ """
+ parent_id = ref.get('parent_id')
+ if parent_id is not None:
+ raise exception.InvalidParentProject(parent_id)
+
+ def _set_default_attributes(self, project_ref):
+ project_ref = self._set_default_domain(project_ref)
+ return self._set_default_parent_project(project_ref)
+
+ def get_project(self, tenant_id):
+ return self._set_default_attributes(
+ self.project.get(tenant_id))
+
+ def list_projects(self, hints):
+ return self._set_default_attributes(
+ self.project.get_all_filtered(hints))
+
+ def list_projects_in_domain(self, domain_id):
+ # We don't support multiple domains within this driver, so ignore
+ # any domain specified
+ return self.list_projects(driver_hints.Hints())
+
+ def list_projects_in_subtree(self, project_id):
+ # We don't support projects hierarchy within this driver, so a
+ # project will never have children
+ return []
+
+ def list_project_parents(self, project_id):
+ # We don't support projects hierarchy within this driver, so a
+ # project will never have parents
+ return []
+
+ def is_leaf_project(self, project_id):
+ # We don't support projects hierarchy within this driver, so a
+ # project will always be a root and a leaf at the same time
+ return True
+
+ def list_projects_from_ids(self, ids):
+ return [self.get_project(id) for id in ids]
+
+ def list_project_ids_from_domain_ids(self, domain_ids):
+ # We don't support multiple domains within this driver, so ignore
+ # any domain specified
+ return [x.id for x in self.list_projects(driver_hints.Hints())]
+
+ def get_project_by_name(self, tenant_name, domain_id):
+ self._validate_default_domain_id(domain_id)
+ return self._set_default_attributes(
+ self.project.get_by_name(tenant_name))
+
+ def create_project(self, tenant_id, tenant):
+ self.project.check_allow_create()
+ tenant = self._validate_default_domain(tenant)
+ self._validate_parent_project_is_none(tenant)
+ tenant['name'] = clean.project_name(tenant['name'])
+ data = tenant.copy()
+ if 'id' not in data or data['id'] is None:
+ data['id'] = str(uuid.uuid4().hex)
+ if 'description' in data and data['description'] in ['', None]:
+ data.pop('description')
+ return self._set_default_attributes(
+ self.project.create(data))
+
+ def update_project(self, tenant_id, tenant):
+ self.project.check_allow_update()
+ tenant = self._validate_default_domain(tenant)
+ if 'name' in tenant:
+ tenant['name'] = clean.project_name(tenant['name'])
+ return self._set_default_attributes(
+ self.project.update(tenant_id, tenant))
+
+ def delete_project(self, tenant_id):
+ self.project.check_allow_delete()
+ if self.project.subtree_delete_enabled:
+ self.project.deleteTree(tenant_id)
+ else:
+ # The manager layer will call assignments to delete the
+ # role assignments, so we just have to delete the project itself.
+ self.project.delete(tenant_id)
+
+ def create_domain(self, domain_id, domain):
+ if domain_id == CONF.identity.default_domain_id:
+ msg = _('Duplicate ID, %s.') % domain_id
+ raise exception.Conflict(type='domain', details=msg)
+ raise exception.Forbidden(_('Domains are read-only against LDAP'))
+
+ def get_domain(self, domain_id):
+ self._validate_default_domain_id(domain_id)
+ return resource.calc_default_domain()
+
+ def update_domain(self, domain_id, domain):
+ self._validate_default_domain_id(domain_id)
+ raise exception.Forbidden(_('Domains are read-only against LDAP'))
+
+ def delete_domain(self, domain_id):
+ self._validate_default_domain_id(domain_id)
+ raise exception.Forbidden(_('Domains are read-only against LDAP'))
+
+ def list_domains(self, hints):
+ return [resource.calc_default_domain()]
+
+ def list_domains_from_ids(self, ids):
+ return [resource.calc_default_domain()]
+
+ def get_domain_by_name(self, domain_name):
+ default_domain = resource.calc_default_domain()
+ if domain_name != default_domain['name']:
+ raise exception.DomainNotFound(domain_id=domain_name)
+ return default_domain
+
+
+# TODO(termie): turn this into a data object and move logic to driver
+class ProjectApi(common_ldap.ProjectLdapStructureMixin,
+ common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap):
+
+ model = models.Project
+
+ def create(self, values):
+ data = values.copy()
+ if data.get('id') is None:
+ data['id'] = uuid.uuid4().hex
+ return super(ProjectApi, self).create(data)
+
+ def update(self, project_id, values):
+ old_obj = self.get(project_id)
+ return super(ProjectApi, self).update(project_id, values, old_obj)
+
+ def get_all_filtered(self, hints):
+ query = self.filter_query(hints)
+ return super(ProjectApi, self).get_all(query)
diff --git a/keystone-moon/keystone/resource/backends/sql.py b/keystone-moon/keystone/resource/backends/sql.py
new file mode 100644
index 00000000..fb117240
--- /dev/null
+++ b/keystone-moon/keystone/resource/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_config import cfg
+from oslo_log import log
+
+from keystone import clean
+from keystone.common import sql
+from keystone import exception
+from keystone.i18n import _LE
+from keystone import resource as keystone_resource
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+
+class Resource(keystone_resource.Driver):
+
+ def default_assignment_driver(self):
+ return 'keystone.assignment.backends.sql.Assignment'
+
+ 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.transaction() as session:
+ return self._get_project(session, tenant_id).to_dict()
+
+ def get_project_by_name(self, tenant_name, domain_id):
+ with sql.transaction() 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()
+
+ @sql.truncated
+ def list_projects(self, hints):
+ with sql.transaction() 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.transaction() 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.transaction() 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.transaction() 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.transaction() as session:
+ project = self._get_project(session, project_id).to_dict()
+ 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.union(children_ids)
+ subtree += children
+ children = self._get_children(session, children_ids)
+ return subtree
+
+ def list_project_parents(self, project_id):
+ with sql.transaction() 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.transaction() 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()
+
+ @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]
+ 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.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_domain(self, domain_id):
+ with sql.transaction() 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']
+ 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'))
+ # 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/config_backends/__init__.py b/keystone-moon/keystone/resource/config_backends/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone-moon/keystone/resource/config_backends/__init__.py
diff --git a/keystone-moon/keystone/resource/config_backends/sql.py b/keystone-moon/keystone/resource/config_backends/sql.py
new file mode 100644
index 00000000..e54bf22b
--- /dev/null
+++ b/keystone-moon/keystone/resource/config_backends/sql.py
@@ -0,0 +1,119 @@
+# 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 keystone.common import sql
+from keystone import exception
+from keystone.i18n import _
+from keystone import resource
+
+
+class WhiteListedConfig(sql.ModelBase, sql.ModelDictMixin):
+ __tablename__ = 'whitelisted_config'
+ domain_id = sql.Column(sql.String(64), primary_key=True)
+ group = sql.Column(sql.String(255), primary_key=True)
+ option = sql.Column(sql.String(255), primary_key=True)
+ value = sql.Column(sql.JsonBlob(), nullable=False)
+
+ def to_dict(self):
+ d = super(WhiteListedConfig, self).to_dict()
+ d.pop('domain_id')
+ return d
+
+
+class SensitiveConfig(sql.ModelBase, sql.ModelDictMixin):
+ __tablename__ = 'sensitive_config'
+ domain_id = sql.Column(sql.String(64), primary_key=True)
+ group = sql.Column(sql.String(255), primary_key=True)
+ option = sql.Column(sql.String(255), primary_key=True)
+ value = sql.Column(sql.JsonBlob(), nullable=False)
+
+ def to_dict(self):
+ d = super(SensitiveConfig, self).to_dict()
+ d.pop('domain_id')
+ return d
+
+
+class DomainConfig(resource.DomainConfigDriver):
+
+ def choose_table(self, sensitive):
+ if sensitive:
+ return SensitiveConfig
+ else:
+ return WhiteListedConfig
+
+ @sql.handle_conflicts(conflict_type='domain_config')
+ def create_config_option(self, domain_id, group, option, value,
+ sensitive=False):
+ with sql.transaction() 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()
+
+ def _get_config_option(self, session, domain_id, group, option, sensitive):
+ try:
+ config_table = self.choose_table(sensitive)
+ ref = (session.query(config_table).
+ filter_by(domain_id=domain_id, group=group,
+ option=option).one())
+ except sql.NotFound:
+ msg = _('option %(option)s in group %(group)s') % {
+ 'group': group, 'option': option}
+ raise exception.DomainConfigNotFound(
+ domain_id=domain_id, group_or_option=msg)
+ return ref
+
+ def get_config_option(self, domain_id, group, option, sensitive=False):
+ with sql.transaction() as session:
+ ref = self._get_config_option(session, domain_id, group, option,
+ sensitive)
+ return ref.to_dict()
+
+ def list_config_options(self, domain_id, group=None, option=None,
+ sensitive=False):
+ with sql.transaction() as session:
+ config_table = self.choose_table(sensitive)
+ query = session.query(config_table)
+ query = query.filter_by(domain_id=domain_id)
+ if group:
+ query = query.filter_by(group=group)
+ if option:
+ query = query.filter_by(option=option)
+ return [ref.to_dict() for ref in query.all()]
+
+ def update_config_option(self, domain_id, group, option, value,
+ sensitive=False):
+ with sql.transaction() as session:
+ ref = self._get_config_option(session, domain_id, group, option,
+ sensitive)
+ ref.value = value
+ return ref.to_dict()
+
+ def delete_config_options(self, domain_id, group=None, option=None,
+ sensitive=False):
+ """Deletes config options that match the filter parameters.
+
+ Since the public API is broken down into calls for delete in both the
+ whitelisted and sensitive methods, we are silent at the driver level
+ if there was nothing to delete.
+
+ """
+ with sql.transaction() as session:
+ config_table = self.choose_table(sensitive)
+ query = session.query(config_table)
+ query = query.filter_by(domain_id=domain_id)
+ if group:
+ query = query.filter_by(group=group)
+ if option:
+ query = query.filter_by(option=option)
+ query.delete(False)
diff --git a/keystone-moon/keystone/resource/controllers.py b/keystone-moon/keystone/resource/controllers.py
new file mode 100644
index 00000000..886b5eb1
--- /dev/null
+++ b/keystone-moon/keystone/resource/controllers.py
@@ -0,0 +1,281 @@
+# Copyright 2013 Metacloud, Inc.
+# Copyright 2012 OpenStack Foundation
+#
+# 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.
+
+"""Workflow Logic the Resource service."""
+
+import uuid
+
+from oslo_config import cfg
+from oslo_log import log
+
+from keystone.common import controller
+from keystone.common import dependency
+from keystone.common import validation
+from keystone.common import wsgi
+from keystone import exception
+from keystone.i18n import _
+from keystone import notifications
+from keystone.resource import schema
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+
+@dependency.requires('resource_api')
+class Tenant(controller.V2Controller):
+
+ @controller.v2_deprecated
+ def get_all_projects(self, context, **kw):
+ """Gets a list of all tenants for an admin user."""
+ if 'name' in context['query_string']:
+ return self.get_project_by_name(
+ context, context['query_string'].get('name'))
+
+ self.assert_admin(context)
+ tenant_refs = self.resource_api.list_projects_in_domain(
+ CONF.identity.default_domain_id)
+ for tenant_ref in tenant_refs:
+ tenant_ref = self.filter_domain_id(tenant_ref)
+ params = {
+ 'limit': context['query_string'].get('limit'),
+ 'marker': context['query_string'].get('marker'),
+ }
+ return self.format_project_list(tenant_refs, **params)
+
+ @controller.v2_deprecated
+ def get_project(self, context, tenant_id):
+ # TODO(termie): this stuff should probably be moved to middleware
+ self.assert_admin(context)
+ ref = self.resource_api.get_project(tenant_id)
+ return {'tenant': self.filter_domain_id(ref)}
+
+ @controller.v2_deprecated
+ def get_project_by_name(self, context, tenant_name):
+ self.assert_admin(context)
+ ref = self.resource_api.get_project_by_name(
+ tenant_name, CONF.identity.default_domain_id)
+ return {'tenant': self.filter_domain_id(ref)}
+
+ # CRUD Extension
+ @controller.v2_deprecated
+ def create_project(self, context, tenant):
+ tenant_ref = self._normalize_dict(tenant)
+
+ if 'name' not in tenant_ref or not tenant_ref['name']:
+ msg = _('Name field is required and cannot be empty')
+ raise exception.ValidationError(message=msg)
+
+ self.assert_admin(context)
+ tenant_ref['id'] = tenant_ref.get('id', uuid.uuid4().hex)
+ tenant = self.resource_api.create_project(
+ tenant_ref['id'],
+ self._normalize_domain_id(context, tenant_ref))
+ return {'tenant': self.filter_domain_id(tenant)}
+
+ @controller.v2_deprecated
+ def update_project(self, context, tenant_id, tenant):
+ self.assert_admin(context)
+ # Remove domain_id if specified - a v2 api caller should not
+ # be specifying that
+ clean_tenant = tenant.copy()
+ clean_tenant.pop('domain_id', None)
+
+ tenant_ref = self.resource_api.update_project(
+ tenant_id, clean_tenant)
+ return {'tenant': tenant_ref}
+
+ @controller.v2_deprecated
+ def delete_project(self, context, tenant_id):
+ self.assert_admin(context)
+ self.resource_api.delete_project(tenant_id)
+
+
+@dependency.requires('resource_api')
+class DomainV3(controller.V3Controller):
+ collection_name = 'domains'
+ member_name = 'domain'
+
+ def __init__(self):
+ super(DomainV3, self).__init__()
+ self.get_member_from_driver = self.resource_api.get_domain
+
+ @controller.protected()
+ @validation.validated(schema.domain_create, 'domain')
+ def create_domain(self, context, domain):
+ ref = self._assign_unique_id(self._normalize_dict(domain))
+ initiator = notifications._get_request_audit_info(context)
+ ref = self.resource_api.create_domain(ref['id'], ref, initiator)
+ return DomainV3.wrap_member(context, ref)
+
+ @controller.filterprotected('enabled', 'name')
+ def list_domains(self, context, filters):
+ hints = DomainV3.build_driver_hints(context, filters)
+ refs = self.resource_api.list_domains(hints=hints)
+ return DomainV3.wrap_collection(context, refs, hints=hints)
+
+ @controller.protected()
+ def get_domain(self, context, domain_id):
+ ref = self.resource_api.get_domain(domain_id)
+ return DomainV3.wrap_member(context, ref)
+
+ @controller.protected()
+ @validation.validated(schema.domain_update, 'domain')
+ def update_domain(self, context, domain_id, domain):
+ self._require_matching_id(domain_id, domain)
+ initiator = notifications._get_request_audit_info(context)
+ ref = self.resource_api.update_domain(domain_id, domain, initiator)
+ return DomainV3.wrap_member(context, ref)
+
+ @controller.protected()
+ def delete_domain(self, context, domain_id):
+ initiator = notifications._get_request_audit_info(context)
+ return self.resource_api.delete_domain(domain_id, initiator)
+
+
+@dependency.requires('domain_config_api')
+class DomainConfigV3(controller.V3Controller):
+ member_name = 'config'
+
+ @controller.protected()
+ def create_domain_config(self, context, domain_id, config):
+ original_config = (
+ self.domain_config_api.get_config_with_sensitive_info(domain_id))
+ ref = self.domain_config_api.create_config(domain_id, config)
+ if original_config:
+ # Return status code 200, since config already existed
+ return wsgi.render_response(body={self.member_name: ref})
+ else:
+ return wsgi.render_response(body={self.member_name: ref},
+ status=('201', 'Created'))
+
+ @controller.protected()
+ def get_domain_config(self, context, domain_id, group=None, option=None):
+ 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):
+ 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):
+ return self.update_domain_config(
+ context, domain_id, config, group, option=None)
+
+ def update_domain_config_only(self, context, domain_id, config):
+ 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.domain_config_api.delete_config(domain_id, group, option)
+
+
+@dependency.requires('resource_api')
+class ProjectV3(controller.V3Controller):
+ collection_name = 'projects'
+ member_name = 'project'
+
+ def __init__(self):
+ super(ProjectV3, self).__init__()
+ self.get_member_from_driver = self.resource_api.get_project
+
+ @controller.protected()
+ @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)
+ initiator = notifications._get_request_audit_info(context)
+ ref = self.resource_api.create_project(ref['id'], ref,
+ initiator=initiator)
+ return ProjectV3.wrap_member(context, ref)
+
+ @controller.filterprotected('domain_id', 'enabled', 'name',
+ 'parent_id')
+ def list_projects(self, context, filters):
+ hints = ProjectV3.build_driver_hints(context, filters)
+ refs = self.resource_api.list_projects(hints=hints)
+ return ProjectV3.wrap_collection(context, refs, hints=hints)
+
+ def _expand_project_ref(self, context, ref):
+ params = context['query_string']
+
+ parents_as_list = 'parents_as_list' in params and (
+ self.query_filter_is_true(params['parents_as_list']))
+ parents_as_ids = 'parents_as_ids' in params and (
+ self.query_filter_is_true(params['parents_as_ids']))
+
+ subtree_as_list = 'subtree_as_list' in params and (
+ self.query_filter_is_true(params['subtree_as_list']))
+ subtree_as_ids = 'subtree_as_ids' in params and (
+ self.query_filter_is_true(params['subtree_as_ids']))
+
+ # parents_as_list and parents_as_ids are mutually exclusive
+ if parents_as_list and parents_as_ids:
+ msg = _('Cannot use parents_as_list and parents_as_ids query '
+ 'params at the same time.')
+ raise exception.ValidationError(msg)
+
+ # subtree_as_list and subtree_as_ids are mutually exclusive
+ if subtree_as_list and subtree_as_ids:
+ msg = _('Cannot use subtree_as_list and subtree_as_ids query '
+ 'params at the same time.')
+ raise exception.ValidationError(msg)
+
+ user_id = self.get_auth_context(context).get('user_id')
+
+ if parents_as_list:
+ parents = self.resource_api.list_project_parents(
+ ref['id'], user_id)
+ ref['parents'] = [ProjectV3.wrap_member(context, p)
+ for p in parents]
+ elif parents_as_ids:
+ ref['parents'] = self.resource_api.get_project_parents_as_ids(ref)
+
+ if subtree_as_list:
+ subtree = self.resource_api.list_projects_in_subtree(
+ ref['id'], user_id)
+ ref['subtree'] = [ProjectV3.wrap_member(context, p)
+ for p in subtree]
+ elif subtree_as_ids:
+ ref['subtree'] = self.resource_api.get_projects_in_subtree_as_ids(
+ ref['id'])
+
+ @controller.protected()
+ def get_project(self, context, project_id):
+ ref = self.resource_api.get_project(project_id)
+ self._expand_project_ref(context, ref)
+ return ProjectV3.wrap_member(context, ref)
+
+ @controller.protected()
+ @validation.validated(schema.project_update, 'project')
+ def update_project(self, context, project_id, project):
+ self._require_matching_id(project_id, project)
+ self._require_matching_domain_id(
+ project_id, project, self.resource_api.get_project)
+ initiator = notifications._get_request_audit_info(context)
+ ref = self.resource_api.update_project(project_id, project,
+ initiator=initiator)
+ return ProjectV3.wrap_member(context, ref)
+
+ @controller.protected()
+ def delete_project(self, context, project_id):
+ initiator = notifications._get_request_audit_info(context)
+ return self.resource_api.delete_project(project_id,
+ initiator=initiator)
diff --git a/keystone-moon/keystone/resource/core.py b/keystone-moon/keystone/resource/core.py
new file mode 100644
index 00000000..017eb4e7
--- /dev/null
+++ b/keystone-moon/keystone/resource/core.py
@@ -0,0 +1,1324 @@
+# 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.
+
+"""Main entry point into the resource service."""
+
+import abc
+
+from oslo_config import cfg
+from oslo_log import log
+import six
+
+from keystone import clean
+from keystone.common import cache
+from keystone.common import dependency
+from keystone.common import driver_hints
+from keystone.common import manager
+from keystone.contrib import federation
+from keystone import exception
+from keystone.i18n import _, _LE, _LW
+from keystone import notifications
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+MEMOIZE = cache.get_memoization_decorator(section='resource')
+
+
+def calc_default_domain():
+ return {'description':
+ (u'Owns users and tenants (i.e. projects)'
+ ' available on Identity API v2.'),
+ 'enabled': True,
+ 'id': CONF.identity.default_domain_id,
+ 'name': u'Default'}
+
+
+@dependency.provider('resource_api')
+@dependency.requires('assignment_api', 'credential_api', 'domain_config_api',
+ 'identity_api', 'revoke_api')
+class Manager(manager.Manager):
+ """Default pivot point for the resource backend.
+
+ See :mod:`keystone.common.manager.Manager` for more details on how this
+ dynamically calls the backend.
+
+ """
+ _DOMAIN = 'domain'
+ _PROJECT = 'project'
+
+ def __init__(self):
+ # If there is a specific driver specified for resource, then use it.
+ # Otherwise retrieve the driver type from the assignment driver.
+ resource_driver = CONF.resource.driver
+
+ if resource_driver is None:
+ assignment_driver = (
+ dependency.get_provider('assignment_api').driver)
+ resource_driver = assignment_driver.default_resource_driver()
+
+ super(Manager, self).__init__(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
+ 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)
+
+ 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'])
+ parents_list.append(parent_ref)
+ for ref in parents_list:
+ if ref.get('domain_id') != tenant.get('domain_id'):
+ raise exception.ForbiddenAction(
+ action=_('cannot create a project within a different '
+ 'domain than its parents.'))
+ if not ref.get('enabled', True):
+ raise exception.ForbiddenAction(
+ action=_('cannot create a project in a '
+ 'branch containing a disabled '
+ 'project: %s') % ref['id'])
+ self._assert_max_hierarchy_depth(tenant.get('parent_id'),
+ parents_list)
+
+ ret = self.driver.create_project(tenant_id, tenant)
+ notifications.Audit.created(self._PROJECT, tenant_id, initiator)
+ if MEMOIZE.should_cache(ret):
+ self.get_project.set(ret, self, tenant_id)
+ self.get_project_by_name.set(ret, self, ret['name'],
+ ret['domain_id'])
+ return ret
+
+ def assert_domain_enabled(self, domain_id, domain=None):
+ """Assert the Domain is enabled.
+
+ :raise AssertionError if domain is disabled.
+ """
+ if domain is None:
+ domain = self.get_domain(domain_id)
+ if not domain.get('enabled', True):
+ raise AssertionError(_('Domain is disabled: %s') % domain_id)
+
+ def assert_domain_not_federated(self, domain_id, domain):
+ """Assert the Domain's name and id do not match the reserved keyword.
+
+ Note that the reserved keyword is defined in the configuration file,
+ by default, it is 'Federated', it is also case insensitive.
+ If config's option is empty the default hardcoded value 'Federated'
+ will be used.
+
+ :raise AssertionError if domain named match the value in the config.
+
+ """
+ # NOTE(marek-denis): We cannot create this attribute in the __init__ as
+ # config values are always initialized to default value.
+ federated_domain = (CONF.federation.federated_domain_name or
+ federation.FEDERATED_DOMAIN_KEYWORD).lower()
+ if (domain.get('name') and domain['name'].lower() == federated_domain):
+ raise AssertionError(_('Domain cannot be named %s')
+ % federated_domain)
+ if (domain_id.lower() == federated_domain):
+ raise AssertionError(_('Domain cannot have ID %s')
+ % federated_domain)
+
+ def assert_project_enabled(self, project_id, project=None):
+ """Assert the project is enabled and its associated domain is enabled.
+
+ :raise AssertionError if the project or domain is disabled.
+ """
+ if project is None:
+ project = self.get_project(project_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.driver.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 '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'])
+ 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)
+
+ project = self.driver.get_project(tenant_id)
+ project_user_ids = (
+ self.assignment_api.list_user_ids_for_project(tenant_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)
+ 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)
+ return ret
+
+ def _filter_projects_list(self, projects_list, user_id):
+ user_projects = self.assignment_api.list_projects_for_user(user_id)
+ user_projects_ids = set([proj['id'] for proj in user_projects])
+ # Keep only the projects present in user_projects
+ projects_list = [proj for proj in projects_list
+ if proj['id'] in user_projects_ids]
+
+ def list_project_parents(self, project_id, user_id=None):
+ parents = self.driver.list_project_parents(project_id)
+ # If a user_id was provided, the returned list should be filtered
+ # against the projects this user has access to.
+ if user_id:
+ self._filter_projects_list(parents, user_id)
+ return parents
+
+ def _build_parents_as_ids_dict(self, project, parents_by_id):
+ # NOTE(rodrigods): we don't rely in the order of the projects returned
+ # by the list_project_parents() method. Thus, we create a project cache
+ # (parents_by_id) in order to access each parent in constant time and
+ # traverse up the hierarchy.
+ def traverse_parents_hierarchy(project):
+ parent_id = project.get('parent_id')
+ if not parent_id:
+ return None
+
+ parent = parents_by_id[parent_id]
+ return {parent_id: traverse_parents_hierarchy(parent)}
+
+ return traverse_parents_hierarchy(project)
+
+ def get_project_parents_as_ids(self, project):
+ """Gets the IDs from the parents from a given project.
+
+ The project IDs are returned as a structured dictionary traversing up
+ the hierarchy to the top level project. For example, considering the
+ following project hierarchy::
+
+ A
+ |
+ +-B-+
+ | |
+ C D
+
+ If we query for project C parents, the expected return is the following
+ dictionary::
+
+ 'parents': {
+ B['id']: {
+ A['id']: None
+ }
+ }
+
+ """
+ parents_list = self.list_project_parents(project['id'])
+ parents_as_ids = self._build_parents_as_ids_dict(
+ project, {proj['id']: proj for proj in parents_list})
+ return parents_as_ids
+
+ def list_projects_in_subtree(self, project_id, user_id=None):
+ subtree = self.driver.list_projects_in_subtree(project_id)
+ # If a user_id was provided, the returned list should be filtered
+ # against the projects this user has access to.
+ if user_id:
+ self._filter_projects_list(subtree, user_id)
+ return subtree
+
+ def _build_subtree_as_ids_dict(self, project_id, subtree_by_parent):
+ # NOTE(rodrigods): we perform a depth first search to construct the
+ # dictionaries representing each level of the subtree hierarchy. In
+ # order to improve this traversal performance, we create a cache of
+ # projects (subtree_py_parent) that accesses in constant time the
+ # direct children of a given project.
+ def traverse_subtree_hierarchy(project_id):
+ children = subtree_by_parent.get(project_id)
+ if not children:
+ return None
+
+ children_ids = {}
+ for child in children:
+ children_ids[child['id']] = traverse_subtree_hierarchy(
+ child['id'])
+ return children_ids
+
+ return traverse_subtree_hierarchy(project_id)
+
+ def get_projects_in_subtree_as_ids(self, project_id):
+ """Gets the IDs from the projects in the subtree from a given project.
+
+ The project IDs are returned as a structured dictionary representing
+ their hierarchy. For example, considering the following project
+ hierarchy::
+
+ A
+ |
+ +-B-+
+ | |
+ C D
+
+ If we query for project A subtree, the expected return is the following
+ dictionary::
+
+ 'subtree': {
+ B['id']: {
+ C['id']: None,
+ D['id']: None
+ }
+ }
+
+ """
+ def _projects_indexed_by_parent(projects_list):
+ projects_by_parent = {}
+ for proj in projects_list:
+ parent_id = proj.get('parent_id')
+ if parent_id:
+ if parent_id in projects_by_parent:
+ projects_by_parent[parent_id].append(proj)
+ else:
+ projects_by_parent[parent_id] = [proj]
+ return projects_by_parent
+
+ subtree_list = self.list_projects_in_subtree(project_id)
+ subtree_as_ids = self._build_subtree_as_ids_dict(
+ project_id, _projects_indexed_by_parent(subtree_list))
+ return subtree_as_ids
+
+ @MEMOIZE
+ def get_domain(self, domain_id):
+ return self.driver.get_domain(domain_id)
+
+ @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)
+
+ notifications.Audit.created(self._DOMAIN, domain_id, initiator)
+
+ 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
+
+ @manager.response_truncated
+ def list_domains(self, hints=None):
+ return self.driver.list_domains(hints or driver_hints.Hints())
+
+ @notifications.disabled(_DOMAIN, public=False)
+ def _disable_domain(self, domain_id):
+ """Emit a notification to the callback system domain 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
+ domain scoped tokens) when a domain is disabled.
+
+ :param domain_id: domain identifier
+ :type domain_id: string
+ """
+ pass
+
+ def update_domain(self, domain_id, domain, initiator=None):
+ 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)
+
+ 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'))
+
+ domain = self.driver.get_domain(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.'))
+
+ self._delete_domain_contents(domain_id)
+ # 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)
+ # TODO(henry-nash): Although the controller will ensure deletion of
+ # all users & groups within the domain (which will cause all
+ # assignments for those users/groups to also be deleted), there
+ # could still be assignments on this domain for users/groups in
+ # 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'])
+
+ def _delete_domain_contents(self, domain_id):
+ """Delete the contents of a domain.
+
+ Before we delete a domain, we need to remove all the entities
+ that are owned by it, i.e. Projects. To do this we
+ call the delete function for these entities, which are
+ themselves responsible for deleting any credentials and role grants
+ 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 '
+ 'projects hierarchy - %(project_id)s.')
+ LOG.error(msg, {'project_id': project['id']})
+ return
+
+ examined.add(project['id'])
+ children = [proj for proj in projects
+ if proj.get('parent_id') == project['id']]
+ for proj in children:
+ _delete_projects(proj, projects, examined)
+
+ try:
+ self.delete_project(project['id'])
+ except exception.ProjectNotFound:
+ LOG.debug(('Project %(projectid)s not found when '
+ 'deleting domain contents for %(domainid)s, '
+ 'continuing with cleanup.'),
+ {'projectid': project['id'],
+ 'domainid': domain_id})
+
+ 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]
+ examined = set()
+ for project in roots:
+ _delete_projects(project, proj_refs, examined)
+
+ @manager.response_truncated
+ def list_projects(self, hints=None):
+ return self.driver.list_projects(hints or driver_hints.Hints())
+
+ # NOTE(henry-nash): list_projects_in_domain is actually an internal method
+ # and not exposed via the API. Therefore there is no need to support
+ # driver hints for it.
+ def list_projects_in_domain(self, domain_id):
+ return self.driver.list_projects_in_domain(domain_id)
+
+ @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)
+
+ @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
+
+
+@six.add_metaclass(abc.ABCMeta)
+class Driver(object):
+
+ def _get_list_limit(self):
+ return CONF.resource.list_limit or CONF.list_limit
+
+ @abc.abstractmethod
+ def get_project_by_name(self, tenant_name, domain_id):
+ """Get a tenant by name.
+
+ :returns: tenant_ref
+ :raises: keystone.exception.ProjectNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ # domain crud
+ @abc.abstractmethod
+ def create_domain(self, domain_id, domain):
+ """Creates a new domain.
+
+ :raises: keystone.exception.Conflict
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_domains(self, hints):
+ """List domains in the system.
+
+ :param hints: filter hints which the driver should
+ implement if at all possible.
+
+ :returns: a list of domain_refs or an empty list.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ 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.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def get_domain(self, domain_id):
+ """Get a domain by ID.
+
+ :returns: domain_ref
+ :raises: keystone.exception.DomainNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def get_domain_by_name(self, domain_name):
+ """Get a domain by name.
+
+ :returns: domain_ref
+ :raises: keystone.exception.DomainNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def update_domain(self, domain_id, domain):
+ """Updates an existing domain.
+
+ :raises: keystone.exception.DomainNotFound,
+ keystone.exception.Conflict
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def delete_domain(self, domain_id):
+ """Deletes an existing domain.
+
+ :raises: keystone.exception.DomainNotFound
+
+ """
+ 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
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @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
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def update_project(self, project_id, project):
+ """Updates an existing project.
+
+ :raises: keystone.exception.ProjectNotFound,
+ keystone.exception.Conflict
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def delete_project(self, project_id):
+ """Deletes an existing project.
+
+ :raises: keystone.exception.ProjectNotFound
+
+ """
+ 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
+
+ """
+ raise exception.NotImplemented()
+
+ @abc.abstractmethod
+ def list_projects_in_subtree(self, project_id):
+ """List all projects in the subtree below the hierarchy of the
+ 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
+
+ """
+ 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
+
+ """
+ raise exception.NotImplemented()
+
+ # 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]
+ else:
+ raise ValueError(_('Expected dict or list: %s') % type(ref))
+
+ 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 specified belongs to the default domain.
+
+ """
+ if domain_id != CONF.identity.default_domain_id:
+ raise exception.DomainNotFound(domain_id=domain_id)
+
+
+@dependency.provider('domain_config_api')
+class DomainConfigManager(manager.Manager):
+ """Default pivot point for the Domain Config backend."""
+
+ # NOTE(henry-nash): In order for a config option to be stored in the
+ # standard table, it must be explicitly whitelisted. Options marked as
+ # sensitive are stored in a separate table. Attempting to store options
+ # that are not listed as either whitelisted or sensitive will raise an
+ # exception.
+ #
+ # Only those options that affect the domain-specific driver support in
+ # the identity manager are supported.
+
+ whitelisted_options = {
+ 'identity': ['driver'],
+ '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_attribute_ignore', 'user_default_project_id_attribute',
+ 'user_allow_create', 'user_allow_update', 'user_allow_delete',
+ 'user_enabled_emulation', 'user_enabled_emulation_dn',
+ 'user_additional_attribute_mapping', 'group_tree_dn',
+ 'group_filter', 'group_objectclass', 'group_id_attribute',
+ 'group_name_attribute', 'group_member_attribute',
+ 'group_desc_attribute', 'group_attribute_ignore',
+ 'group_allow_create', 'group_allow_update', 'group_allow_delete',
+ 'group_additional_attribute_mapping', 'tls_cacertfile',
+ 'tls_cacertdir', 'use_tls', 'tls_req_cert', 'use_pool',
+ 'pool_size', 'pool_retry_max', 'pool_retry_delay',
+ 'pool_connection_timeout', 'pool_connection_lifetime',
+ 'use_auth_pool', 'auth_pool_size', 'auth_pool_connection_lifetime'
+ ]
+ }
+ sensitive_options = {
+ 'identity': [],
+ 'ldap': ['password']
+ }
+
+ def __init__(self):
+ super(DomainConfigManager, self).__init__(CONF.domain_config.driver)
+
+ def _assert_valid_config(self, config):
+ """Ensure the options in the config are valid.
+
+ This method is called to validate the request config in create and
+ update manager calls.
+
+ :param config: config structure being created or updated
+
+ """
+ # Something must be defined in the request
+ if not config:
+ raise exception.InvalidDomainConfig(
+ reason=_('No options specified'))
+
+ # Make sure the groups/options defined in config itself are valid
+ for group in config:
+ if (not config[group] or not
+ isinstance(config[group], dict)):
+ msg = _('The value of group %(group)s specified in the '
+ 'config should be a dictionary of options') % {
+ 'group': group}
+ raise exception.InvalidDomainConfig(reason=msg)
+ for option in config[group]:
+ self._assert_valid_group_and_option(group, option)
+
+ def _assert_valid_group_and_option(self, group, option):
+ """Ensure the combination of group and option is valid.
+
+ :param group: optional group name, if specified it must be one
+ we support
+ :param option: optional option name, if specified it must be one
+ we support and a group must also be specified
+
+ """
+ if not group and not option:
+ # For all calls, it's OK for neither to be defined, it means you
+ # are operating on all config options for that domain.
+ return
+
+ if not group and option:
+ # Our API structure should prevent this from ever happening, so if
+ # it does, then this is coding error.
+ msg = _('Option %(option)s found with no group specified while '
+ 'checking domain configuration request') % {
+ 'option': option}
+ raise exception.UnexpectedError(exception=msg)
+
+ if (group and group not in self.whitelisted_options and
+ group not in self.sensitive_options):
+ msg = _('Group %(group)s is not supported '
+ 'for domain specific configurations') % {'group': group}
+ raise exception.InvalidDomainConfig(reason=msg)
+
+ if option:
+ if (option not in self.whitelisted_options[group] and option not in
+ self.sensitive_options[group]):
+ msg = _('Option %(option)s in group %(group)s is not '
+ 'supported for domain specific configurations') % {
+ 'group': group, 'option': option}
+ raise exception.InvalidDomainConfig(reason=msg)
+
+ def _is_sensitive(self, group, option):
+ return option in self.sensitive_options[group]
+
+ def _config_to_list(self, config):
+ """Build whitelisted and sensitive lists for use by backend drivers."""
+
+ whitelisted = []
+ sensitive = []
+ for group in config:
+ for option in config[group]:
+ the_list = (sensitive if self._is_sensitive(group, option)
+ else whitelisted)
+ the_list.append({
+ 'group': group, 'option': option,
+ 'value': config[group][option]})
+
+ return whitelisted, sensitive
+
+ def _list_to_config(self, whitelisted, sensitive=None, req_option=None):
+ """Build config dict from a list of option dicts.
+
+ :param whitelisted: list of dicts containing options and their groups,
+ this has already been filtered to only contain
+ those options to include in the output.
+ :param sensitive: list of dicts containing sensitive options and their
+ groups, this has already been filtered to only
+ contain those options to include in the output.
+ :param req_option: the individual option requested
+
+ :returns: a config dict, including sensitive if specified
+
+ """
+ the_list = whitelisted + (sensitive or [])
+ if not the_list:
+ return {}
+
+ if req_option:
+ # The request was specific to an individual option, so
+ # no need to include the group in the output. We first check that
+ # there is only one option in the answer (and that it's the right
+ # one) - if not, something has gone wrong and we raise an error
+ if len(the_list) > 1 or the_list[0]['option'] != req_option:
+ LOG.error(_LE('Unexpected results in response for domain '
+ 'config - %(count)s responses, first option is '
+ '%(option)s, expected option %(expected)s'),
+ {'count': len(the_list), 'option': list[0]['option'],
+ 'expected': req_option})
+ raise exception.UnexpectedError(
+ _('An unexpected error occurred when retrieving domain '
+ 'configs'))
+ return {the_list[0]['option']: the_list[0]['value']}
+
+ config = {}
+ for option in the_list:
+ config.setdefault(option['group'], {})
+ config[option['group']][option['option']] = option['value']
+
+ return config
+
+ def create_config(self, domain_id, config):
+ """Create config for a domain
+
+ :param domain_id: the domain in question
+ :param config: the dict of config groups/options to assign to the
+ domain
+
+ Creates a new config, overwriting any previous config (no Conflict
+ error will be generated).
+
+ :returns: a dict of group dicts containing the options, with any that
+ are sensitive removed
+ :raises keystone.exception.InvalidDomainConfig: when the config
+ contains options we do not support
+
+ """
+ self._assert_valid_config(config)
+ whitelisted, sensitive = self._config_to_list(config)
+ # Delete any existing config
+ self.delete_config_options(domain_id)
+ self.delete_config_options(domain_id, sensitive=True)
+ # ...and create the new one
+ for option in whitelisted:
+ self.create_config_option(
+ domain_id, option['group'], option['option'], option['value'])
+ for option in sensitive:
+ self.create_config_option(
+ domain_id, option['group'], option['option'], option['value'],
+ sensitive=True)
+ return self._list_to_config(whitelisted)
+
+ def get_config(self, domain_id, group=None, option=None):
+ """Get config, or partial config, for a domain
+
+ :param domain_id: the domain in question
+ :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 whitelisted options,
+ filtered by group and option specified
+ :raises keystone.exception.DomainConfigNotFound: when no config found
+ that matches domain_id, group and option specified
+ :raises keystone.exception.InvalidDomainConfig: when the config
+ and group/option parameters specify an option we do not
+ support
+
+ An example response::
+
+ {
+ 'ldap': {
+ 'url': 'myurl'
+ 'user_tree_dn': 'OU=myou'},
+ 'identity': {
+ 'driver': 'keystone.identity.backends.ldap.Identity'}
+
+ }
+
+ """
+ self._assert_valid_group_and_option(group, option)
+ whitelisted = self.list_config_options(domain_id, group, option)
+ if whitelisted:
+ return self._list_to_config(whitelisted, req_option=option)
+
+ if option:
+ msg = _('option %(option)s in group %(group)s') % {
+ 'group': group, 'option': option}
+ elif group:
+ msg = _('group %(group)s') % {'group': group}
+ else:
+ msg = _('any options')
+ raise exception.DomainConfigNotFound(
+ domain_id=domain_id, group_or_option=msg)
+
+ def update_config(self, domain_id, config, group=None, option=None):
+ """Update config, or partial config, for a domain
+
+ :param domain_id: the domain in question
+ :param config: the config dict containing and groups/options being
+ updated
+ :param group: an optional specific group of options, which if specified
+ must appear in config, with no other groups
+ :param option: an optional specific option within the group, which if
+ specified must appear in config, with no other options
+
+ The contents of the supplied config will be merged with the existing
+ config for this domain, updating or creating new options if these did
+ not previously exist. If group or option is specified, then the update
+ will be limited to those specified items and the inclusion of other
+ options in the supplied config will raise an exception, as will the
+ situation when those options do not already exist in the current
+ config.
+
+ :returns: a dict of groups containing all whitelisted options
+ :raises keystone.exception.InvalidDomainConfig: when the config
+ and group/option parameters specify an option we do not
+ support or one that does not exist in the original config
+
+ """
+ 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)
+
+ # If a group has been specified, then the request is to
+ # explicitly only update the options in that group - so the config
+ # must not contain anything else. Further, that group must exist in
+ # the original config. Likewise, if an option has been specified,
+ # then the group in the config must only contain that option and it
+ # also must exist in the original config.
+ if group:
+ if len(config) != 1 or (option and len(config[group]) != 1):
+ if option:
+ msg = _('Trying to update option %(option)s in group '
+ '%(group)s, so that, and only that, option '
+ 'must be specified in the config') % {
+ 'group': group, 'option': option}
+ else:
+ msg = _('Trying to update group %(group)s, so that, '
+ 'and only that, group must be specified in '
+ 'the config') % {'group': group}
+ raise exception.InvalidDomainConfig(reason=msg)
+
+ # So we now know we have the right number of entries in the
+ # config that align with a group/option being specified, but we
+ # must also make sure they match.
+ if group not in config:
+ msg = _('request to update group %(group)s, but config '
+ 'provided contains group %(group_other)s '
+ 'instead') % {
+ 'group': group,
+ 'group_other': config.keys()[0]}
+ raise exception.InvalidDomainConfig(reason=msg)
+ if option and option not in config[group]:
+ msg = _('Trying to update option %(option)s in group '
+ '%(group)s, but config provided contains option '
+ '%(option_other)s instead') % {
+ 'group': group, 'option': option,
+ 'option_other': config[group].keys()[0]}
+ raise exception.InvalidDomainConfig(reason=msg)
+
+ # Finally, we need to check if the group/option specified
+ # already exists in the original config - since if not, to keep
+ # with the semantics of an update, we need to fail with
+ # a DomainConfigNotFound
+ if not self.get_config_with_sensitive_info(domain_id,
+ group, option):
+ if option:
+ msg = _('option %(option)s in group %(group)s') % {
+ 'group': group, 'option': option}
+ raise exception.DomainConfigNotFound(
+ domain_id=domain_id, group_or_option=msg)
+ else:
+ msg = _('group %(group)s') % {'group': group}
+ raise exception.DomainConfigNotFound(
+ domain_id=domain_id, group_or_option=msg)
+
+ 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'],
+ option['value'], sensitive=sensitive)
+ except exception.Conflict:
+ self.update_config_option(
+ domain_id, option['group'], option['option'],
+ option['value'], sensitive=sensitive)
+
+ update_config = config
+ if group and option:
+ # The config will just be a dict containing the option and
+ # its value, so make it look like a single option under the
+ # group in question
+ update_config = {group: config}
+
+ _assert_valid_update(domain_id, update_config, group, option)
+
+ whitelisted, sensitive = self._config_to_list(update_config)
+
+ for new_option in whitelisted:
+ _update_or_create(domain_id, new_option, sensitive=False)
+ for new_option in sensitive:
+ _update_or_create(domain_id, new_option, sensitive=True)
+
+ return self.get_config(domain_id)
+
+ def delete_config(self, domain_id, group=None, option=None):
+ """Delete config, or partial config, for the domain.
+
+ :param domain_id: the domain in question
+ :param group: an optional specific group of options
+ :param option: an optional specific option within the group
+
+ If group and option are None, then the entire config for the domain
+ is deleted. If group is not None, then just that group of options will
+ be deleted. If group and option are both specified, then just that
+ option is deleted.
+
+ :raises keystone.exception.InvalidDomainConfig: when group/option
+ parameters specify an option we do not support or one that
+ does not exist in the original config.
+
+ """
+ self._assert_valid_group_and_option(group, option)
+ if group:
+ # As this is a partial delete, then make sure the items requested
+ # are valid and exist in the current config
+ current_config = self.get_config_with_sensitive_info(domain_id)
+ # Raise an exception if the group/options specified don't exist in
+ # the current config so that the delete method provides the
+ # correct error semantics.
+ current_group = current_config.get(group)
+ if not current_group:
+ msg = _('group %(group)s') % {'group': group}
+ raise exception.DomainConfigNotFound(
+ domain_id=domain_id, group_or_option=msg)
+ if option and not current_group.get(option):
+ msg = _('option %(option)s in group %(group)s') % {
+ 'group': group, 'option': option}
+ raise exception.DomainConfigNotFound(
+ domain_id=domain_id, group_or_option=msg)
+
+ self.delete_config_options(domain_id, group, option)
+ self.delete_config_options(domain_id, group, option, sensitive=True)
+
+ def get_config_with_sensitive_info(self, domain_id, group=None,
+ option=None):
+ """Get config for a domain with sensitive info included.
+
+ This method is not exposed via the public API, but is used by the
+ identity manager to initialize a domain with the fully formed config
+ options.
+
+ """
+ whitelisted = self.list_config_options(domain_id, group, option)
+ sensitive = self.list_config_options(domain_id, group, option,
+ sensitive=True)
+
+ # Check if there are any sensitive substitutions needed. We first try
+ # and simply ensure any sensitive options that have valid substitution
+ # references in the whitelisted options are substituted. We then check
+ # the resulting whitelisted option and raise a warning if there
+ # appears to be an unmatched or incorrectly constructed substitution
+ # reference. To avoid the risk of logging any sensitive options that
+ # have already been substituted, we first take a copy of the
+ # whitelisted option.
+
+ # Build a dict of the sensitive options ready to try substitution
+ sensitive_dict = {s['option']: s['value'] for s in sensitive}
+
+ for each_whitelisted in whitelisted:
+ if not isinstance(each_whitelisted['value'], six.string_types):
+ # We only support substitutions into string types, if its an
+ # integer, list etc. then just continue onto the next one
+ continue
+
+ # Store away the original value in case we need to raise a warning
+ # after substitution.
+ original_value = each_whitelisted['value']
+ warning_msg = ''
+ try:
+ each_whitelisted['value'] = (
+ each_whitelisted['value'] % sensitive_dict)
+ except KeyError:
+ warning_msg = _LW(
+ 'Found what looks like an unmatched config option '
+ 'substitution reference - domain: %(domain)s, group: '
+ '%(group)s, option: %(option)s, value: %(value)s. Perhaps '
+ 'the config option to which it refers has yet to be '
+ 'added?')
+ except (ValueError, TypeError):
+ warning_msg = _LW(
+ 'Found what looks like an incorrectly constructed '
+ 'config option substitution reference - domain: '
+ '%(domain)s, group: %(group)s, option: %(option)s, '
+ 'value: %(value)s.')
+
+ if warning_msg:
+ LOG.warn(warning_msg % {
+ 'domain': domain_id,
+ 'group': each_whitelisted['group'],
+ 'option': each_whitelisted['option'],
+ 'value': original_value})
+
+ return self._list_to_config(whitelisted, sensitive)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class DomainConfigDriver(object):
+ """Interface description for a Domain Config driver."""
+
+ @abc.abstractmethod
+ def create_config_option(self, domain_id, group, option, value,
+ sensitive=False):
+ """Creates a config option for a domain.
+
+ :param domain_id: the domain for this option
+ :param group: the group name
+ :param option: the option name
+ :param value: the value to assign to this option
+ :param sensitive: whether the option is sensitive
+
+ :returns: dict containing group, option and value
+ :raises: keystone.exception.Conflict
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def get_config_option(self, domain_id, group, option, sensitive=False):
+ """Gets the config option for a domain.
+
+ :param domain_id: the domain for this option
+ :param group: the group name
+ :param option: the option name
+ :param sensitive: whether the option is sensitive
+
+ :returns: dict containing group, option and value
+ :raises: keystone.exception.DomainConfigNotFound: the option doesn't
+ exist.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_config_options(self, domain_id, group=None, option=False,
+ sensitive=False):
+ """Gets a config options for a domain.
+
+ :param domain_id: the domain for this option
+ :param group: optional group option name
+ :param option: optional option name. If group is None, then this
+ parameter is ignored
+ :param sensitive: whether the option is sensitive
+
+ :returns: list of dicts containing group, option and value
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def update_config_option(self, domain_id, group, option, value,
+ sensitive=False):
+ """Updates a config option for a domain.
+
+ :param domain_id: the domain for this option
+ :param group: the group option name
+ :param option: the option name
+ :param value: the value to assign to this option
+ :param sensitive: whether the option is sensitive
+
+ :returns: dict containing updated group, option and value
+ :raises: keystone.exception.DomainConfigNotFound: the option doesn't
+ exist.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def delete_config_options(self, domain_id, group=None, option=None,
+ sensitive=False):
+ """Deletes config options for a domain.
+
+ Allows deletion of all options for a domain, all options in a group
+ or a specific option. The driver is silent if there are no options
+ to delete.
+
+ :param domain_id: the domain for this option
+ :param group: optional group option name
+ :param option: optional option name. If group is None, then this
+ parameter is ignored
+ :param sensitive: whether the option is sensitive
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
diff --git a/keystone-moon/keystone/resource/routers.py b/keystone-moon/keystone/resource/routers.py
new file mode 100644
index 00000000..8ccd10aa
--- /dev/null
+++ b/keystone-moon/keystone/resource/routers.py
@@ -0,0 +1,94 @@
+# Copyright 2013 Metacloud, Inc.
+# Copyright 2012 OpenStack Foundation
+#
+# 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.
+
+"""WSGI Routers for the Resource service."""
+
+from keystone.common import json_home
+from keystone.common import router
+from keystone.common import wsgi
+from keystone.resource import controllers
+
+
+class Admin(wsgi.ComposableRouter):
+ def add_routes(self, mapper):
+ # Tenant Operations
+ tenant_controller = controllers.Tenant()
+ mapper.connect('/tenants',
+ controller=tenant_controller,
+ action='get_all_projects',
+ conditions=dict(method=['GET']))
+ mapper.connect('/tenants/{tenant_id}',
+ controller=tenant_controller,
+ action='get_project',
+ conditions=dict(method=['GET']))
+
+
+class Routers(wsgi.RoutersBase):
+
+ def append_v3_routers(self, mapper, routers):
+ routers.append(
+ router.Router(controllers.DomainV3(),
+ 'domains', 'domain',
+ resource_descriptions=self.v3_resources))
+
+ config_controller = controllers.DomainConfigV3()
+
+ self._add_resource(
+ mapper, config_controller,
+ path='/domains/{domain_id}/config',
+ get_head_action='get_domain_config',
+ put_action='create_domain_config',
+ patch_action='update_domain_config_only',
+ delete_action='delete_domain_config',
+ rel=json_home.build_v3_resource_relation('domain_config'),
+ status=json_home.Status.EXPERIMENTAL,
+ path_vars={
+ 'domain_id': json_home.Parameters.DOMAIN_ID
+ })
+
+ config_group_param = (
+ json_home.build_v3_parameter_relation('config_group'))
+ self._add_resource(
+ mapper, config_controller,
+ path='/domains/{domain_id}/config/{group}',
+ get_head_action='get_domain_config',
+ patch_action='update_domain_config_group',
+ delete_action='delete_domain_config',
+ rel=json_home.build_v3_resource_relation('domain_config_group'),
+ status=json_home.Status.EXPERIMENTAL,
+ path_vars={
+ 'domain_id': json_home.Parameters.DOMAIN_ID,
+ 'group': config_group_param
+ })
+
+ self._add_resource(
+ mapper, config_controller,
+ path='/domains/{domain_id}/config/{group}/{option}',
+ get_head_action='get_domain_config',
+ patch_action='update_domain_config',
+ delete_action='delete_domain_config',
+ rel=json_home.build_v3_resource_relation('domain_config_option'),
+ status=json_home.Status.EXPERIMENTAL,
+ path_vars={
+ 'domain_id': json_home.Parameters.DOMAIN_ID,
+ 'group': config_group_param,
+ 'option': json_home.build_v3_parameter_relation(
+ 'config_option')
+ })
+
+ routers.append(
+ router.Router(controllers.ProjectV3(),
+ 'projects', 'project',
+ resource_descriptions=self.v3_resources))
diff --git a/keystone-moon/keystone/resource/schema.py b/keystone-moon/keystone/resource/schema.py
new file mode 100644
index 00000000..0fd59e3f
--- /dev/null
+++ b/keystone-moon/keystone/resource/schema.py
@@ -0,0 +1,75 @@
+# 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 keystone.common import validation
+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,
+ 'enabled': parameter_types.boolean,
+ 'parent_id': validation.nullable(parameter_types.id_string),
+ 'name': {
+ 'type': 'string',
+ 'minLength': 1,
+ 'maxLength': 64
+ }
+}
+
+project_create = {
+ 'type': 'object',
+ 'properties': _project_properties,
+ # NOTE(lbragstad): A project name is the only parameter required for
+ # project creation according to the Identity V3 API. We should think
+ # about using the maxProperties validator here, and in update.
+ 'required': ['name'],
+ 'additionalProperties': True
+}
+
+project_update = {
+ 'type': 'object',
+ 'properties': _project_properties,
+ # NOTE(lbragstad) Make sure at least one property is being updated
+ 'minProperties': 1,
+ 'additionalProperties': True
+}
+
+_domain_properties = {
+ 'description': validation.nullable(parameter_types.description),
+ 'enabled': parameter_types.boolean,
+ 'name': {
+ 'type': 'string',
+ 'minLength': 1,
+ 'maxLength': 64
+ }
+}
+
+domain_create = {
+ 'type': 'object',
+ 'properties': _domain_properties,
+ # TODO(lbragstad): According to the V3 API spec, name isn't required but
+ # the current implementation in assignment.controller:DomainV3 requires a
+ # name for the domain.
+ 'required': ['name'],
+ 'additionalProperties': True
+}
+
+domain_update = {
+ 'type': 'object',
+ 'properties': _domain_properties,
+ 'minProperties': 1,
+ 'additionalProperties': True
+}