Move projects and domains to their own backend

This is the part of the more comprehensive split of
assignments, which rationalizes both the backend and controllers.
In order to make this change easier for reviewers, it is divided
into a number of smaller patches.

Previous patches:

- Move role management into its own manager and drivers
  (see: https://review.openstack.org/#/c/144239/)
- Fix incorrect doc strings for grant driver methods
  (see: https://review.openstack.org/#/c/144403/)
- Make controllers call the new, split out, role manager
  (see: https://review.openstack.org/#/c/144494/)
- Make unit tests call the new, split out, role manager
  (see: https://review.openstack.org/#/c/144548/)
- Refactor the assignment manager and drivers, enabling
  projects/domains to be split out
  (see: https://review.openstack.org/#/c/144650/)
- Fix incorrect comment about circular dependency between
  assignment and identity
  (see: https://review.openstack.org/#/c/144850/)

This patch moves the now logically separated project and domain
functionality into their own manager/backend (called resource).

Future patches will:

- Remove unused pointer to assignment in identity driver
- Update the controllers to call the new resource manager
- Update the tests to call the new resource manager
- Split the assignment controller, giving projects/domains
  their own controller

Partially implements: bp pluggable-assignments

Change-Id: I0ff1c2fa30237734d0a25d03dad5be03eb166367
This commit is contained in:
Henry Nash 2015-01-02 00:22:04 +00:00
parent d8e855016a
commit 0e05353d09
19 changed files with 1482 additions and 1131 deletions

View File

@ -139,6 +139,7 @@ implementations. The drivers for the services are:
* :mod:`keystone.catalog.core.Driver`
* :mod:`keystone.identity.core.Driver`
* :mod:`keystone.policy.core.Driver`
* :mod:`keystone.resource.core.Driver`
* :mod:`keystone.token.core.Driver`
If you implement a backend driver for one of the Keystone services, you're

View File

@ -108,6 +108,7 @@ configuration file is organized into the following sections:
* ``[os_inherit]`` - Inherited role assignment extension
* ``[paste_deploy]`` - Pointer to the PasteDeploy configuration file
* ``[policy]`` - Policy system driver configuration for RBAC
* ``[resource]`` - Resource system driver configuration
* ``[revoke]`` - Revocation system driver configuration
* ``[role]`` - Role system driver configuration
* ``[saml]`` - SAML configuration options
@ -499,29 +500,29 @@ Current Keystone systems that have caching capabilities:
revocation list is refreshed whenever a token is revoked. It typically
sees significantly more requests than specific token retrievals or
token validation calls.
* ``assignment``
The assignment system has a separate ``cache_time`` configuration
option, that can be set to a value above or below the global
* ``resource``
The resource system has a separate ``cache_time`` configuration option,
that can be set to a value above or below the global
``expiration_time`` default, allowing for different caching behavior
from the other systems in ``Keystone``. This option is set in the
``[assignment]`` section of the configuration file.
``[resource]`` section of the configuration file.
Currently ``assignment`` has caching for ``project`` and ``domain``
specific requests (primarily around the CRUD actions). Caching is
currently not implemented on grants. The list (``list_projects``,
``list_domains``, etc) methods are not subject to caching.
Currently ``resource`` has caching for ``project`` and ``domain``
specific requests (primarily around the CRUD actions). The
``list_projects`` and ``list_domains`` methods are not subject to
caching.
.. WARNING::
Be aware that if a read-only ``assignment`` backend is in use, the
cache will not immediately reflect changes on the backend. Any
Be aware that if a read-only ``resource`` backend is in use, the
cache will not immediately reflect changes on the back end. Any
given change may take up to the ``cache_time`` (if set in the
``[assignment]`` section of the configuration) or the global
``[resource]`` section of the configuration) or the global
``expiration_time`` (set in the ``[cache]`` section of the
configuration) before it is reflected. If this type of delay (when
using a read-only ``assignment`` backend) is an issue, it is
recommended that caching be disabled on ``assignment``. To disable
caching specifically on ``assignment``, in the ``[assignment]``
section of the configuration set ``caching`` to ``False``.
using a read-only ``resource`` backend) is an issue, it is
recommended that caching be disabled on ``resource``. To disable
caching specifically on ``resource``, in the ``[resource]`` section
of the configuration set ``caching`` to ``False``.
* ``role``
Currently ``role`` has caching for ``get_role``, but not for ``list_roles``.
The role system has a separate ``cache_time`` configuration option,
@ -952,7 +953,7 @@ override this global value with a specific limit, for example:
.. code-block:: ini
[assignment]
[resource]
list_limit = 100
If a response to ``list_{entity}`` call has been truncated, then the response
@ -1527,15 +1528,18 @@ directories in conjunction with reading user and group information.
Keystone now provides an option whereby these read-only directories can be
easily integrated as it now enables its identity entities (which comprises
users, groups, and group memberships) to be served out of directories while
assignments (which comprises projects, role assignments, and domains) and roles
are to be served from different Keystone backends (i.e. SQL). To enable this
option, you must have the following ``keystone.conf`` options set:
resource (which comprises projects and domains), assignment and role
entities are to be served from different Keystone backends (i.e. SQL). To
enable this option, you must have the following ``keystone.conf`` options set:
.. code-block:: ini
[identity]
driver = keystone.identity.backends.ldap.Identity
[resource]
driver = keystone.resource.backends.sql.Resource
[assignment]
driver = keystone.assignment.backends.sql.Assignment
@ -1544,15 +1548,16 @@ option, you must have the following ``keystone.conf`` options set:
With the above configuration, Keystone will only lookup identity related
information such users, groups, and group membership from the directory, while
assignment related information will be provided by the SQL backend. Also note
that if there is an LDAP Identity, and no assignment or role backend is
specified, they will default to LDAP. Although this may seem counterintuitive,
it is provided for backwards compatibility. Nonetheless, the explicit option
will always override the implicit option, so specifying the options as shown
above will always be correct. Finally, it is also worth noting that whether or
not the LDAP accessible directory is to be considered read only is still
configured as described in a previous section above by setting values such as
the following in the ``[ldap]`` configuration section:
resources, roles and assignment related information will be provided by the SQL
backend. Also note that if there is an LDAP Identity, and no resource,
assignment or role backend is specified, they will default to LDAP. Although
this may seem counterintuitive, it is provided for backwards compatibility.
Nonetheless, the explicit option will always override the implicit option, so
specifying the options as shown above will always be correct. Finally, it is
also worth noting that whether or not the LDAP accessible directory is to be
considered read only is still configured as described in a previous section
above by setting values such as the following in the ``[ldap]`` configuration
section:
.. code-block:: ini

View File

@ -295,9 +295,9 @@
# able to take everything down, as it requires a clean break. (integer value)
#qpid_topology_version = 1
# SSL version to use (valid only if SSL enabled). valid values are TLSv1 and
# SSLv23. SSLv2 and SSLv3 may be available on some distributions. (string
# value)
# SSL version to use (valid only if SSL enabled). Valid values are TLSv1 and
# SSLv23. SSLv2, SSLv3, TLSv1_1, and TLSv1_2 may be available on some
# distributions. (string value)
#kombu_ssl_version =
# SSL key file (valid only if SSL enabled). (string value)
@ -360,7 +360,7 @@
#rpc_zmq_bind_address = *
# MatchMaker driver. (string value)
#rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker.MatchMakerLocalhost
#rpc_zmq_matchmaker = oslo_messaging._drivers.matchmaker.MatchMakerLocalhost
# ZeroMQ receiver listening port. (integer value)
#rpc_zmq_port = 9501
@ -389,7 +389,7 @@
# Heartbeat time-to-live. (integer value)
#matchmaker_heartbeat_ttl = 600
# Size of RPC greenthread pool. (integer value)
# Size of RPC thread pool. (integer value)
#rpc_thread_pool_size = 64
# Driver or drivers to handle sending notifications. (multi valued)
@ -425,18 +425,6 @@
# Assignment backend driver. (string value)
#driver = <None>
# Toggle for assignment caching. This has no effect unless global caching is
# enabled. (boolean value)
#caching = true
# TTL (in seconds) to cache assignment data. This has no effect unless global
# caching is enabled. (integer value)
#cache_time = <None>
# Maximum number of entities that will be returned in an assignment collection.
# (integer value)
#list_limit = <None>
[auth]
@ -1270,6 +1258,32 @@
#list_limit = <None>
[resource]
#
# From keystone
#
# Resource backend driver. If a resource driver is not specified, the
# assignment driver will choose the resource driver. (string value)
#driver = <None>
# Toggle for resource caching. This has no effect unless global caching is
# enabled. (boolean value)
# Deprecated group/name - [assignment]/caching
#caching = true
# TTL (in seconds) to cache resource data. This has no effect unless global
# caching is enabled. (integer value)
# Deprecated group/name - [assignment]/cache_time
#cache_time = <None>
# Maximum number of entities that will be returned in a resource collection.
# (integer value)
# Deprecated group/name - [assignment]/list_limit
#list_limit = <None>
[revoke]
#

View File

@ -13,15 +13,11 @@
# under the License.
from __future__ import absolute_import
import uuid
import ldap as ldap
import ldap.filter
from keystone import assignment
from keystone.assignment.role_backends import ldap as ldap_role
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 config
@ -55,91 +51,8 @@ class Assignment(assignment.Driver):
def default_role_driver(self):
return 'keystone.assignment.role_backends.ldap.Role'
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())
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 default_resource_driver(self):
return 'keystone.resource.backends.ldap.Resource'
def list_role_ids_for_groups_on_project(
self, groups, project_id, project_domain_id, project_parents):
@ -251,15 +164,6 @@ class Assignment(assignment.Driver):
role_dn=role_dn,
tenant_dn=tenant_dn)
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 remove_role_from_user_and_project(self, user_id, tenant_id, role_id):
role_dn = self._subrole_id_to_dn(role_id, tenant_id)
return self.role.delete_user(role_dn,
@ -271,30 +175,6 @@ class Assignment(assignment.Driver):
return self.role.delete_user(role_dn,
self.group._id_to_dn(group_id), role_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 assignment.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 [assignment.calc_default_domain()]
def list_domains_from_ids(self, ids):
return [assignment.calc_default_domain()]
# Bulk actions on User From identity
def delete_user(self, user_id):
user_dn = self.user._id_to_dn(user_id)
@ -384,12 +264,6 @@ class Assignment(assignment.Driver):
return self._roles_from_role_dicts(metadata_ref.get('roles', []),
inherited_to_projects)
def get_domain_by_name(self, domain_name):
default_domain = assignment.calc_default_domain()
if domain_name != default_domain['name']:
raise exception.DomainNotFound(domain_id=domain_name)
return default_domain
def list_role_assignments(self):
role_assignments = []
for a in self.role.list_role_assignments(self.project.tree_dn):
@ -415,20 +289,9 @@ class Assignment(assignment.Driver):
# TODO(termie): turn this into a data object and move logic to driver
class ProjectApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap):
DEFAULT_OU = 'ou=Groups'
DEFAULT_STRUCTURAL_CLASSES = []
DEFAULT_OBJECTCLASS = 'groupOfNames'
DEFAULT_ID_ATTR = 'cn'
DEFAULT_MEMBER_ATTRIBUTE = 'member'
NotFound = exception.ProjectNotFound
notfound_arg = 'project_id' # NOTE(yorik-sar): while options_name = tenant
options_name = 'project'
attribute_options_names = {'name': 'name',
'description': 'desc',
'enabled': 'enabled',
'domain_id': 'domain_id'}
immutable_attrs = ['name']
class ProjectApi(common_ldap.ProjectLdapStructureMixin,
common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap):
model = models.Project
def __init__(self, conf):
@ -436,12 +299,6 @@ class ProjectApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap):
self.member_attribute = (getattr(conf.ldap, 'project_member_attribute')
or self.DEFAULT_MEMBER_ATTRIBUTE)
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 get_user_projects(self, user_dn, associations):
"""Returns list of tenants a user has access to
"""
@ -472,10 +329,6 @@ class ProjectApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap):
res.add(rolegrant.user_dn)
return list(res)
def update(self, project_id, values):
old_obj = self.get(project_id)
return super(ProjectApi, self).update(project_id, values, old_obj)
class UserRoleAssociation(object):
"""Role Grant model."""

View File

@ -17,11 +17,10 @@ import sqlalchemy
from sqlalchemy.sql.expression import false
from keystone import assignment as keystone_assignment
from keystone import clean
from keystone.common import sql
from keystone import config
from keystone import exception
from keystone.i18n import _, _LE
from keystone.i18n import _
from keystone.openstack.common import log
@ -56,26 +55,8 @@ class Assignment(keystone_assignment.Driver):
def default_role_driver(self):
return "keystone.assignment.role_backends.sql.Role"
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()
def default_resource_driver(self):
return 'keystone.resource.backends.sql.Resource'
def list_user_ids_for_project(self, tenant_id):
with sql.transaction() as session:
@ -190,39 +171,6 @@ class Assignment(keystone_assignment.Driver):
if not q.delete(False):
raise exception.RoleNotFound(role_id=role_id)
@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 _list_project_ids_for_actor(self, actors, hints, inherited,
group_only=False):
# TODO(henry-nash): Now that we have a single assignment table, we
@ -278,59 +226,6 @@ class Assignment(keystone_assignment.Driver):
return [assignment.target_id for assignment in query.all()]
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
def list_role_ids_for_groups_on_domain(self, group_ids, domain_id):
if not group_ids:
# If there's no groups then there will be no domain roles.
@ -462,102 +357,6 @@ class Assignment(keystone_assignment.Driver):
refs = session.query(RoleAssignment).all()
return [denormalize_role(ref) for ref in 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)
def delete_project_assignments(self, project_id):
with sql.transaction() as session:
q = session.query(RoleAssignment)
@ -583,33 +382,6 @@ class Assignment(keystone_assignment.Driver):
q.delete(False)
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'), {})
class RoleAssignment(sql.ModelBase, sql.DictBase):
__tablename__ = 'assignment'
attributes = ['type', 'actor_id', 'target_id', 'role_id', 'inherited']

View File

@ -18,7 +18,6 @@ import abc
import six
from keystone import clean
from keystone.common import cache
from keystone.common import dependency
from keystone.common import driver_hints
@ -26,7 +25,7 @@ from keystone.common import manager
from keystone import config
from keystone import exception
from keystone.i18n import _
from keystone.i18n import _LE, _LI
from keystone.i18n import _LI
from keystone import notifications
from keystone.openstack.common import log
from keystone.openstack.common import versionutils
@ -34,21 +33,10 @@ from keystone.openstack.common import versionutils
CONF = config.CONF
LOG = log.getLogger(__name__)
ASSIGNMENT_SHOULD_CACHE = cache.should_cache_fn('assignment')
ROLE_SHOULD_CACHE = cache.should_cache_fn('role')
SHOULD_CACHE = cache.should_cache_fn('role')
# NOTE(blk-u): The config option is not available at import time.
ASSIGNMENT_EXPIRATION_TIME = lambda: CONF.assignment.cache_time
ROLE_EXPIRATION_TIME = lambda: CONF.role.cache_time
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'}
EXPIRATION_TIME = lambda: CONF.role.cache_time
def deprecated_to_role_api(f):
@ -68,9 +56,27 @@ def deprecated_to_role_api(f):
return wrapper()
def deprecated_to_resource_api(f):
"""Specialized deprecation wrapper for assignment to resource api.
This wraps the standard deprecation wrapper and fills in the method
names automatically.
"""
@six.wraps(f)
def wrapper(*args, **kwargs):
x = versionutils.deprecated(
what='assignment.' + f.__name__ + '()',
as_of=versionutils.deprecated.KILO,
in_favor_of='resource.' + f.__name__ + '()')
return x(f)
return wrapper()
@dependency.provider('assignment_api')
@dependency.optional('revoke_api')
@dependency.requires('credential_api', 'identity_api', 'role_api')
@dependency.requires('credential_api', 'identity_api', 'resource_api',
'role_api')
class Manager(manager.Manager):
"""Default pivot point for the Assignment backend.
@ -87,8 +93,8 @@ class Manager(manager.Manager):
# If there is no explicit assignment driver specified, we let the
# identity driver tell us what to use. This is for backward
# compatibility reasons from the time when identity and assignment
# were all part of identity.
# compatibility reasons from the time when identity, resource and
# assignment were all part of identity.
if assignment_driver is None:
identity_driver = dependency.REGISTRY['identity_api'].driver
assignment_driver = identity_driver.default_assignment_driver()
@ -102,159 +108,13 @@ class Manager(manager.Manager):
x in self.identity_api.list_groups_for_user(user_id)]
def list_user_ids_for_project(self, tenant_id):
self.driver.get_project(tenant_id)
self.resource_api.get_project(tenant_id)
return self.driver.list_user_ids_for_project(tenant_id)
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)
@notifications.created(_PROJECT)
def create_project(self, tenant_id, tenant):
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)
if ASSIGNMENT_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_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)
@notifications.updated(_PROJECT)
def update_project(self, tenant_id, tenant):
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)
self.get_project.invalidate(self, tenant_id)
self.get_project_by_name.invalidate(self, original_tenant['name'],
original_tenant['domain_id'])
return ret
@notifications.deleted(_PROJECT)
def delete_project(self, tenant_id):
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.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)
self.driver.delete_project_assignments(tenant_id)
ret = self.driver.delete_project(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)
return ret
def _list_parent_ids_of_project(self, project_id):
if CONF.os_inherit.enabled:
return [x['id'] for x in (
self.driver.list_project_parents(project_id))]
self.resource_api.list_project_parents(project_id))]
else:
return []
@ -307,7 +167,7 @@ class Manager(manager.Manager):
return role_list
project_ref = self.get_project(tenant_id)
project_ref = self.resource_api.get_project(tenant_id)
user_role_list = _get_user_project_roles(user_id, project_ref)
group_role_list = _get_group_project_roles(user_id, project_ref)
# Use set() to process the list to remove any duplicates
@ -361,7 +221,7 @@ class Manager(manager.Manager):
"""Get a list of roles for this group on domain and/or project."""
if project_id is not None:
project = self.driver.get_project(project_id)
project = self.resource_api.get_project(project_id)
role_ids = self.driver.list_role_ids_for_groups_on_project(
group_ids, project_id, project['domain_id'],
self._list_parent_ids_of_project(project_id))
@ -380,7 +240,7 @@ class Manager(manager.Manager):
keystone.exception.UserNotFound
"""
self.driver.get_project(tenant_id)
self.resource_api.get_project(tenant_id)
try:
self.role_api.get_role(config.CONF.member_role_id)
self.driver.add_role_to_user_and_project(
@ -401,7 +261,7 @@ class Manager(manager.Manager):
config.CONF.member_role_id)
def add_role_to_user_and_project(self, user_id, tenant_id, role_id):
self.driver.get_project(tenant_id)
self.resource_api.get_project(tenant_id)
self.role_api.get_role(role_id)
self.driver.add_role_to_user_and_project(user_id, tenant_id, role_id)
@ -443,7 +303,7 @@ class Manager(manager.Manager):
user_id, group_ids, hints or driver_hints.Hints())
if not CONF.os_inherit.enabled:
return self.driver.list_projects_from_ids(project_ids)
return self.resource_api.list_projects_from_ids(project_ids)
# Inherited roles are enabled, so check to see if this user has any
# inherited role (direct or group) on any parent project, in which
@ -454,66 +314,16 @@ class Manager(manager.Manager):
for proj_id in project_ids_inherited:
project_ids.update(
(x['id'] for x in
self.driver.list_projects_in_subtree(proj_id)))
self.resource_api.list_projects_in_subtree(proj_id)))
# Now do the same for any domain inherited roles
domain_ids = self.driver.list_domain_ids_for_user(
user_id, group_ids, hints or driver_hints.Hints(),
inherited=True)
project_ids.update(
self.driver.list_project_ids_from_domain_ids(domain_ids))
self.resource_api.list_project_ids_from_domain_ids(domain_ids))
return self.driver.list_projects_from_ids(list(project_ids))
def _filter_projects_list(self, projects_list, user_id):
user_projects = self.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 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
@cache.on_arguments(should_cache_fn=ASSIGNMENT_SHOULD_CACHE,
expiration_time=ASSIGNMENT_EXPIRATION_TIME)
def get_domain(self, domain_id):
return self.driver.get_domain(domain_id)
@cache.on_arguments(should_cache_fn=ASSIGNMENT_SHOULD_CACHE,
expiration_time=ASSIGNMENT_EXPIRATION_TIME)
def get_domain_by_name(self, domain_name):
return self.driver.get_domain_by_name(domain_name)
@notifications.created('domain')
def create_domain(self, domain_id, domain):
if (not self.identity_api.multiple_domains_supported and
domain_id != CONF.identity.default_domain_id):
raise exception.Forbidden(_('Multiple domains are not supported'))
domain.setdefault('enabled', True)
domain['enabled'] = clean.domain_enabled(domain['enabled'])
ret = self.driver.create_domain(domain_id, domain)
if ASSIGNMENT_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())
return self.resource_api.list_projects_from_ids(list(project_ids))
# TODO(henry-nash): We might want to consider list limiting this at some
# point in the future.
@ -527,163 +337,18 @@ class Manager(manager.Manager):
group_ids = self._get_group_ids_for_user_id(user_id)
domain_ids = self.driver.list_domain_ids_for_user(
user_id, group_ids, hints or driver_hints.Hints())
return self.driver.list_domains_from_ids(domain_ids)
return self.resource_api.list_domains_from_ids(domain_ids)
def list_domains_for_groups(self, group_ids):
domain_ids = self.driver.list_domain_ids_for_groups(group_ids)
return self.driver.list_domains_from_ids(domain_ids)
@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
@notifications.updated('domain')
def update_domain(self, 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)
# disable owned users & projects when the API user specifically set
# enabled=False
if (original_domain.get('enabled', True) and
not domain.get('enabled', True)):
self._disable_domain(domain_id)
self.get_domain.invalidate(self, domain_id)
self.get_domain_by_name.invalidate(self, original_domain['name'])
return ret
@notifications.deleted('domain')
def delete_domain(self, domain_id):
# 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)
# 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)
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. Users, Groups & Projects. To do this we
call the respective delete functions for these entities, which are
themselves responsible for deleting any credentials and role grants
associated with them as well as revoking any relevant tokens.
The order we delete entities is also important since some types
of backend may need to maintain referential integrity
throughout, and many of the entities have relationship with each
other. The following deletion order is therefore used:
Projects: Reference user and groups for grants
Groups: Reference users for membership and domains for grants
Users: Reference domains for grants
"""
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})
user_refs = self.identity_api.list_users(domain_scope=domain_id)
proj_refs = self.list_projects_in_domain(domain_id)
group_refs = self.identity_api.list_groups(domain_scope=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)
for group in group_refs:
# Cleanup any existing groups.
if group['domain_id'] == domain_id:
try:
self.identity_api.delete_group(group['id'])
except exception.GroupNotFound:
LOG.debug(('Group %(groupid)s not found when deleting '
'domain contents for %(domainid)s, continuing '
'with cleanup.'),
{'groupid': group['id'], 'domainid': domain_id})
# And finally, delete the users themselves
for user in user_refs:
if user['domain_id'] == domain_id:
try:
self.identity_api.delete_user(user['id'])
except exception.UserNotFound:
LOG.debug(('User %(userid)s not found when '
'deleting domain contents for %(domainid)s, '
'continuing with cleanup.'),
{'userid': user['id'],
'domainid': domain_id})
@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)
return self.resource_api.list_domains_from_ids(domain_ids)
def list_projects_for_groups(self, group_ids):
project_ids = (
self.driver.list_project_ids_for_groups(group_ids,
driver_hints.Hints()))
if not CONF.os_inherit.enabled:
return self.driver.list_projects_from_ids(project_ids)
return self.resource_api.list_projects_from_ids(project_ids)
# Inherited roles are enabled, so check to see if these groups have any
# roles on any domain, in which case we must add in all the projects
@ -693,21 +358,11 @@ class Manager(manager.Manager):
group_ids, inherited=True)
project_ids_from_domains = (
self.driver.list_project_ids_from_domain_ids(domain_ids))
self.resource_api.list_project_ids_from_domain_ids(domain_ids))
return self.driver.list_projects_from_ids(
return self.resource_api.list_projects_from_ids(
list(set(project_ids + project_ids_from_domains)))
@cache.on_arguments(should_cache_fn=ASSIGNMENT_SHOULD_CACHE,
expiration_time=ASSIGNMENT_EXPIRATION_TIME)
def get_project(self, project_id):
return self.driver.get_project(project_id)
@cache.on_arguments(should_cache_fn=ASSIGNMENT_SHOULD_CACHE,
expiration_time=ASSIGNMENT_EXPIRATION_TIME)
def get_project_by_name(self, tenant_name, domain_id):
return self.driver.get_project_by_name(tenant_name, domain_id)
def list_role_assignments_for_role(self, role_id=None):
# NOTE(henry-nash): Currently the efficiency of the key driver
# implementation (SQL) of list_role_assignments is severely hampered by
@ -737,9 +392,9 @@ class Manager(manager.Manager):
inherited_to_projects=False, context=None):
self.role_api.get_role(role_id)
if domain_id:
self.driver.get_domain(domain_id)
self.resource_api.get_domain(domain_id)
if project_id:
self.driver.get_project(project_id)
self.resource_api.get_project(project_id)
self.driver.create_grant(role_id, user_id, group_id, domain_id,
project_id, inherited_to_projects)
@ -748,9 +403,9 @@ class Manager(manager.Manager):
inherited_to_projects=False):
role_ref = self.role_api.get_role(role_id)
if domain_id:
self.driver.get_domain(domain_id)
self.resource_api.get_domain(domain_id)
if project_id:
self.driver.get_project(project_id)
self.resource_api.get_project(project_id)
self.driver.check_grant_role_id(
role_id, user_id, group_id, domain_id, project_id,
inherited_to_projects)
@ -760,9 +415,9 @@ class Manager(manager.Manager):
domain_id=None, project_id=None,
inherited_to_projects=False):
if domain_id:
self.driver.get_domain(domain_id)
self.resource_api.get_domain(domain_id)
if project_id:
self.driver.get_project(project_id)
self.resource_api.get_project(project_id)
grant_ids = self.driver.list_grant_role_ids(
user_id, group_id, domain_id, project_id, inherited_to_projects)
return self.role_api.list_roles_from_ids(grant_ids)
@ -800,9 +455,9 @@ class Manager(manager.Manager):
self.role_api.get_role(role_id)
if domain_id:
self.driver.get_domain(domain_id)
self.resource_api.get_domain(domain_id)
if project_id:
self.driver.get_project(project_id)
self.resource_api.get_project(project_id)
self.driver.delete_grant(role_id, user_id, group_id, domain_id,
project_id, inherited_to_projects)
if user_id is not None:
@ -900,6 +555,78 @@ class Manager(manager.Manager):
def list_roles(self, hints=None):
return self.role_api.list_roles(hints=hints)
@deprecated_to_resource_api
def create_project(self, project_id, project):
return self.resource_api.create_project(project_id, project)
@deprecated_to_resource_api
def get_project_by_name(self, tenant_name, domain_id):
return self.resource_api.get_project_by_name(tenant_name, domain_id)
@deprecated_to_resource_api
def get_project(self, project_id):
return self.resource_api.get_project(project_id)
@deprecated_to_resource_api
def update_project(self, project_id, project):
return self.resource_api.update_project(project_id, project)
@deprecated_to_resource_api
def delete_project(self, project_id):
return self.resource_api.delete_project(project_id)
@deprecated_to_resource_api
def list_projects(self, hints=None):
return self.resource_api.list_projects(hints=hints)
@deprecated_to_resource_api
def list_projects_in_domain(self, domain_id):
return self.resource_api.list_projects_in_domain(domain_id)
@deprecated_to_resource_api
def create_domain(self, domain_id, domain):
return self.resource_api.create_domain(domain_id, domain)
@deprecated_to_resource_api
def get_domain_by_name(self, domain_name):
return self.resource_api.get_domain_by_name(domain_name)
@deprecated_to_resource_api
def get_domain(self, domain_id):
return self.resource_api.get_domain(domain_id)
@deprecated_to_resource_api
def update_domain(self, domain_id, domain):
return self.resource_api.update_domain(domain_id, domain)
@deprecated_to_resource_api
def delete_domain(self, domain_id):
return self.resource_api.delete_domain(domain_id)
@deprecated_to_resource_api
def list_domains(self, hints=None):
return self.resource_api.list_domains(hints=hints)
@deprecated_to_resource_api
def assert_domain_enabled(self, domain_id, domain=None):
return self.resource_api.assert_domain_enabled(domain_id, domain)
@deprecated_to_resource_api
def assert_project_enabled(self, project_id, project=None):
return self.resource_api.assert_project_enabled(project_id, project)
@deprecated_to_resource_api
def is_leaf_project(self, project_id):
return self.resource_api.is_leaf_project(project_id)
@deprecated_to_resource_api
def list_project_parents(self, project_id, user_id=None):
return self.resource_api.list_project_parents(project_id, user_id)
@deprecated_to_resource_api
def list_projects_in_subtree(self, project_id, user_id=None):
return self.resource_api.list_projects_in_subtree(project_id, user_id)
@six.add_metaclass(abc.ABCMeta)
class Driver(object):
@ -941,16 +668,6 @@ class Driver(object):
def _get_list_limit(self):
return CONF.assignment.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
@abc.abstractmethod
def list_user_ids_for_project(self, tenant_id):
"""Lists all user IDs with a role assignment in the specified project.
@ -1030,143 +747,6 @@ class Driver(object):
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 list_project_ids_for_user(self, user_id, group_ids, hints,
inherited=False):
@ -1190,45 +770,6 @@ class Driver(object):
"""
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()
@abc.abstractmethod
def list_project_ids_for_groups(self, group_ids, hints,
inherited=False):
@ -1279,35 +820,6 @@ class Driver(object):
"""
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_role_ids_for_groups_on_project(
self, group_ids, project_id, project_domain_id, project_parents):
@ -1379,40 +891,6 @@ class Driver(object):
"""
raise exception.NotImplemented() # pragma: no cover
# 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('role_api')
@dependency.requires('assignment_api')
@ -1430,15 +908,15 @@ class RoleManager(manager.Manager):
super(RoleManager, self).__init__(role_driver)
@cache.on_arguments(should_cache_fn=ROLE_SHOULD_CACHE,
expiration_time=ROLE_EXPIRATION_TIME)
@cache.on_arguments(should_cache_fn=SHOULD_CACHE,
expiration_time=EXPIRATION_TIME)
def get_role(self, role_id):
return self.driver.get_role(role_id)
@notifications.created('role')
def create_role(self, role_id, role):
ret = self.driver.create_role(role_id, role)
if ROLE_SHOULD_CACHE(ret):
if SHOULD_CACHE(ret):
self.get_role.set(ret, self, role_id)
return ret

View File

@ -19,6 +19,7 @@ from keystone.contrib import endpoint_policy
from keystone import credential
from keystone import identity
from keystone import policy
from keystone import resource
from keystone import token
from keystone import trust
@ -28,13 +29,15 @@ def load_backends():
# Configure and build the cache
cache.configure_cache_region(cache.REGION)
# Ensure that the identity driver is created before the assignment manager.
# The default assignment driver is determined by the identity driver, so
# the identity driver must be available to the assignment manager.
# Ensure that the identity driver is created before the assignment manager
# and that the assignment driver is created before the resource manager.
# The default resource driver depends on assignment, which in turn
# depends on identity - hence we need to ensure the chain is available.
_IDENTITY_API = identity.Manager()
_ASSIGNMENT_API = assignment.Manager()
DRIVERS = dict(
assignment_api=assignment.Manager(),
assignment_api=_ASSIGNMENT_API,
catalog_api=catalog.Manager(),
credential_api=credential.Manager(),
endpoint_filter_api=endpoint_filter.Manager(),
@ -43,6 +46,7 @@ def load_backends():
id_mapping_api=identity.MappingManager(),
identity_api=_IDENTITY_API,
policy_api=policy.Manager(),
resource_api=resource.Manager(),
role_api=assignment.RoleManager(),
token_api=token.persistence.Manager(),
trust_api=trust.Manager(),

View File

@ -439,15 +439,27 @@ FILE_OPTIONS = {
# the backend
cfg.StrOpt('driver',
help='Assignment backend driver.'),
],
'resource': [
cfg.StrOpt('driver',
help='Resource backend driver. If a resource driver is '
'not specified, the assignment driver will choose '
'the resource driver.'),
cfg.BoolOpt('caching', default=True,
help='Toggle for assignment caching. This has no effect '
deprecated_opts=[cfg.DeprecatedOpt('caching',
group='assignment')],
help='Toggle for resource caching. This has no effect '
'unless global caching is enabled.'),
cfg.IntOpt('cache_time',
help='TTL (in seconds) to cache assignment data. This has '
deprecated_opts=[cfg.DeprecatedOpt('cache_time',
group='assignment')],
help='TTL (in seconds) to cache resource data. This has '
'no effect unless global caching is enabled.'),
cfg.IntOpt('list_limit',
deprecated_opts=[cfg.DeprecatedOpt('list_limit',
group='assignment')],
help='Maximum number of entities that will be returned '
'in an assignment collection.'),
'in a resource collection.'),
],
'role': [
# The role driver has no default for backward compatibility reasons.

View File

@ -1804,3 +1804,23 @@ class EnabledEmuMixIn(BaseLdap):
if self.enabled_emulation:
self._remove_enabled(object_id)
super(EnabledEmuMixIn, self).delete(object_id)
class ProjectLdapStructureMixin(object):
"""Project LDAP Structure shared between LDAP backends.
This is shared between the resource and assignment LDAP backends.
"""
DEFAULT_OU = 'ou=Groups'
DEFAULT_STRUCTURAL_CLASSES = []
DEFAULT_OBJECTCLASS = 'groupOfNames'
DEFAULT_ID_ATTR = 'cn'
NotFound = exception.ProjectNotFound
notfound_arg = 'project_id' # NOTE(yorik-sar): while options_name = tenant
options_name = 'project'
attribute_options_names = {'name': 'name',
'description': 'desc',
'enabled': 'enabled',
'domain_id': 'domain_id'}
immutable_attrs = ['name']

View File

@ -19,9 +19,9 @@ from keystone import exception
from keystone.i18n import _
from keystone import identity
# Import assignment sql to ensure that the models defined in there are
# Import resource sql to ensure that the models defined in there are
# available for the reference from User and Group to Domain.id.
from keystone.assignment.backends import sql as assignment_sql # noqa
from keystone.resource.backends import sql as resource_sql # noqa
CONF = config.CONF

View File

@ -0,0 +1,13 @@
# 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.core import * # noqa

View File

View File

@ -0,0 +1,191 @@
# 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 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 config
from keystone import exception
from keystone.i18n import _
from keystone.identity.backends import ldap as ldap_identity
from keystone.openstack.common import log
from keystone import resource
CONF = config.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. The assumption is that if you are using
# LDAP for resource, 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())
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)

View File

@ -0,0 +1,259 @@
# 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 import clean
from keystone.common import sql
from keystone import config
from keystone import exception
from keystone.i18n import _LE
from keystone.openstack.common import log
from keystone import resource as keystone_resource
CONF = config.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'), {})

685
keystone/resource/core.py Normal file
View File

@ -0,0 +1,685 @@
# 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
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 import config
from keystone import exception
from keystone.i18n import _, _LE
from keystone import notifications
from keystone.openstack.common import log
CONF = config.CONF
LOG = log.getLogger(__name__)
SHOULD_CACHE = cache.should_cache_fn('resource')
# NOTE(blk-u): The config options are not available at import time.
EXPIRATION_TIME = lambda: CONF.resource.cache_time
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.optional('revoke_api')
@dependency.requires('assignment_api', 'credential_api', 'identity_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.
"""
_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.REGISTRY['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)
@notifications.created(_PROJECT)
def create_project(self, tenant_id, tenant):
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)
if 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_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)
@notifications.updated(_PROJECT)
def update_project(self, tenant_id, tenant):
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)
self.get_project.invalidate(self, tenant_id)
self.get_project_by_name.invalidate(self, original_tenant['name'],
original_tenant['domain_id'])
return ret
@notifications.deleted(_PROJECT)
def delete_project(self, tenant_id):
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)
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 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
@cache.on_arguments(should_cache_fn=SHOULD_CACHE,
expiration_time=EXPIRATION_TIME)
def get_domain(self, domain_id):
return self.driver.get_domain(domain_id)
@cache.on_arguments(should_cache_fn=SHOULD_CACHE,
expiration_time=EXPIRATION_TIME)
def get_domain_by_name(self, domain_name):
return self.driver.get_domain_by_name(domain_name)
@notifications.created('domain')
def create_domain(self, domain_id, domain):
if (not self.identity_api.multiple_domains_supported and
domain_id != CONF.identity.default_domain_id):
raise exception.Forbidden(_('Multiple domains are not supported'))
domain.setdefault('enabled', True)
domain['enabled'] = clean.domain_enabled(domain['enabled'])
ret = self.driver.create_domain(domain_id, domain)
if 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
@notifications.updated('domain')
def update_domain(self, 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)
# disable owned users & projects when the API user specifically set
# enabled=False
if (original_domain.get('enabled', True) and
not domain.get('enabled', True)):
self._disable_domain(domain_id)
self.get_domain.invalidate(self, domain_id)
self.get_domain_by_name.invalidate(self, original_domain['name'])
return ret
@notifications.deleted('domain')
def delete_domain(self, domain_id):
# 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)
# 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)
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. Users, Groups & Projects. To do this we
call the respective delete functions for these entities, which are
themselves responsible for deleting any credentials and role grants
associated with them as well as revoking any relevant tokens.
The order we delete entities is also important since some types
of backend may need to maintain referential integrity
throughout, and many of the entities have relationship with each
other. The following deletion order is therefore used:
Projects: Reference user and groups for grants
Groups: Reference users for membership and domains for grants
Users: Reference domains for grants
"""
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})
user_refs = self.identity_api.list_users(domain_scope=domain_id)
proj_refs = self.list_projects_in_domain(domain_id)
group_refs = self.identity_api.list_groups(domain_scope=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)
for group in group_refs:
# Cleanup any existing groups.
if group['domain_id'] == domain_id:
try:
self.identity_api.delete_group(group['id'])
except exception.GroupNotFound:
LOG.debug(('Group %(groupid)s not found when deleting '
'domain contents for %(domainid)s, continuing '
'with cleanup.'),
{'groupid': group['id'], 'domainid': domain_id})
# And finally, delete the users themselves
for user in user_refs:
if user['domain_id'] == domain_id:
try:
self.identity_api.delete_user(user['id'])
except exception.UserNotFound:
LOG.debug(('User %(userid)s not found when '
'deleting domain contents for %(domainid)s, '
'continuing with cleanup.'),
{'userid': user['id'],
'domainid': domain_id})
@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)
@cache.on_arguments(should_cache_fn=SHOULD_CACHE,
expiration_time=EXPIRATION_TIME)
def get_project(self, project_id):
return self.driver.get_project(project_id)
@cache.on_arguments(should_cache_fn=SHOULD_CACHE,
expiration_time=EXPIRATION_TIME)
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)

View File

@ -3071,7 +3071,7 @@ class IdentityTests(object):
user_projects = self.assignment_api.list_projects_for_user(user1['id'])
self.assertEqual(3, len(user_projects))
@tests.skip_if_cache_disabled('assignment')
@tests.skip_if_cache_disabled('resource')
@tests.skip_if_no_multiple_domains_support
def test_domain_rename_invalidates_get_domain_by_name_cache(self):
domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
@ -3086,7 +3086,7 @@ class IdentityTests(object):
self.assignment_api.get_domain_by_name,
domain_name)
@tests.skip_if_cache_disabled('assignment')
@tests.skip_if_cache_disabled('resource')
def test_cache_layer_domain_crud(self):
domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'enabled': True}
@ -3096,14 +3096,14 @@ class IdentityTests(object):
domain_ref = self.assignment_api.get_domain(domain_id)
updated_domain_ref = copy.deepcopy(domain_ref)
updated_domain_ref['name'] = uuid.uuid4().hex
# Update domain, bypassing assignment api manager
self.assignment_api.driver.update_domain(domain_id, updated_domain_ref)
# Update domain, bypassing resource api manager
self.resource_api.driver.update_domain(domain_id, updated_domain_ref)
# Verify get_domain still returns the domain
self.assertDictContainsSubset(
domain_ref, self.assignment_api.get_domain(domain_id))
# Invalidate cache
self.assignment_api.get_domain.invalidate(self.assignment_api,
domain_id)
self.resource_api.get_domain.invalidate(self.resource_api,
domain_id)
# Verify get_domain returns the updated domain
self.assertDictContainsSubset(
updated_domain_ref, self.assignment_api.get_domain(domain_id))
@ -3112,28 +3112,28 @@ class IdentityTests(object):
self.assignment_api.update_domain(domain_id, domain_ref)
self.assertDictContainsSubset(
domain_ref, self.assignment_api.get_domain(domain_id))
# Make sure domain is 'disabled', bypass assignment api manager
# Make sure domain is 'disabled', bypass resource api manager
domain_ref_disabled = domain_ref.copy()
domain_ref_disabled['enabled'] = False
self.assignment_api.driver.update_domain(domain_id,
domain_ref_disabled)
# Delete domain, bypassing assignment api manager
self.assignment_api.driver.delete_domain(domain_id)
self.resource_api.driver.update_domain(domain_id,
domain_ref_disabled)
# Delete domain, bypassing resource api manager
self.resource_api.driver.delete_domain(domain_id)
# Verify get_domain still returns the domain
self.assertDictContainsSubset(
domain_ref, self.assignment_api.get_domain(domain_id))
# Invalidate cache
self.assignment_api.get_domain.invalidate(self.assignment_api,
domain_id)
self.resource_api.get_domain.invalidate(self.resource_api,
domain_id)
# Verify get_domain now raises DomainNotFound
self.assertRaises(exception.DomainNotFound,
self.assignment_api.get_domain, domain_id)
# Recreate Domain
self.assignment_api.create_domain(domain_id, domain)
self.assignment_api.get_domain(domain_id)
# Make sure domain is 'disabled', bypass assignment api manager
# Make sure domain is 'disabled', bypass resource api manager
domain['enabled'] = False
self.assignment_api.driver.update_domain(domain_id, domain)
self.resource_api.driver.update_domain(domain_id, domain)
# Delete domain
self.assignment_api.delete_domain(domain_id)
# verify DomainNotFound raised
@ -3141,7 +3141,7 @@ class IdentityTests(object):
self.assignment_api.get_domain,
domain_id)
@tests.skip_if_cache_disabled('assignment')
@tests.skip_if_cache_disabled('resource')
@tests.skip_if_no_multiple_domains_support
def test_project_rename_invalidates_get_project_by_name_cache(self):
domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
@ -3161,7 +3161,7 @@ class IdentityTests(object):
project_name,
domain['id'])
@tests.skip_if_cache_disabled('assignment')
@tests.skip_if_cache_disabled('resource')
@tests.skip_if_no_multiple_domains_support
def test_cache_layer_project_crud(self):
domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
@ -3175,15 +3175,15 @@ class IdentityTests(object):
self.assignment_api.get_project(project_id)
updated_project = copy.deepcopy(project)
updated_project['name'] = uuid.uuid4().hex
# Update project, bypassing assignment_api manager
self.assignment_api.driver.update_project(project_id,
updated_project)
# Update project, bypassing resource manager
self.resource_api.driver.update_project(project_id,
updated_project)
# Verify get_project still returns the original project_ref
self.assertDictContainsSubset(
project, self.assignment_api.get_project(project_id))
# Invalidate cache
self.assignment_api.get_project.invalidate(self.assignment_api,
project_id)
self.resource_api.get_project.invalidate(self.resource_api,
project_id)
# Verify get_project now returns the new project
self.assertDictContainsSubset(
updated_project,
@ -3193,14 +3193,14 @@ class IdentityTests(object):
# Verify get_project returns the original project_ref
self.assertDictContainsSubset(
project, self.assignment_api.get_project(project_id))
# Delete project bypassing assignment_api
self.assignment_api.driver.delete_project(project_id)
# Delete project bypassing resource
self.resource_api.driver.delete_project(project_id)
# Verify get_project still returns the project_ref
self.assertDictContainsSubset(
project, self.assignment_api.get_project(project_id))
# Invalidate cache
self.assignment_api.get_project.invalidate(self.assignment_api,
project_id)
self.resource_api.get_project.invalidate(self.resource_api,
project_id)
# Verify ProjectNotFound now raised
self.assertRaises(exception.ProjectNotFound,
self.assignment_api.get_project,
@ -5486,7 +5486,7 @@ class LimitTests(filtering.FilterTests):
# Override with driver specific limit
if entity == 'project':
self.config_fixture.config(group='assignment', list_limit=5)
self.config_fixture.config(group='resource', list_limit=5)
else:
self.config_fixture.config(group='identity', list_limit=5)

View File

@ -21,7 +21,6 @@ import ldap
import mock
from testtools import matchers
from keystone import assignment
from keystone.common import cache
from keystone.common import ldap as common_ldap
from keystone.common.ldap import core as common_ldap_core
@ -30,6 +29,7 @@ from keystone import config
from keystone import exception
from keystone import identity
from keystone.identity.mapping_backends import mapping as map
from keystone import resource
from keystone import tests
from keystone.tests import default_fixtures
from keystone.tests import fakeldap
@ -585,7 +585,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests):
def test_list_domains(self):
domains = self.assignment_api.list_domains()
self.assertEqual(
[assignment.calc_default_domain()],
[resource.calc_default_domain()],
domains)
def test_list_domains_non_default_domain_id(self):
@ -999,8 +999,8 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase):
self.role_api.get_role.invalidate(self.assignment_api,
self.role_member['id'])
self.role_api.get_role(self.role_member['id'])
self.assignment_api.get_project.invalidate(self.assignment_api,
self.tenant_bar['id'])
self.resource_api.get_project.invalidate(self.resource_api,
self.tenant_bar['id'])
self.assertRaises(exception.ProjectNotFound,
self.assignment_api.get_project,
self.tenant_bar['id'])
@ -1050,8 +1050,8 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase):
# could affect what the drivers would return up to the manager. This
# solves this assumption when working with aggressive (on-create)
# cache population.
self.assignment_api.get_project.invalidate(self.assignment_api,
self.tenant_baz['id'])
self.resource_api.get_project.invalidate(self.resource_api,
self.tenant_baz['id'])
tenant_ref = self.assignment_api.get_project(self.tenant_baz['id'])
self.assertEqual(self.tenant_baz['id'], tenant_ref['id'])
self.assertEqual(self.tenant_baz['name'], tenant_ref['name'])
@ -1072,8 +1072,8 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase):
# could affect what the drivers would return up to the manager. This
# solves this assumption when working with aggressive (on-create)
# cache population.
self.assignment_api.get_project.invalidate(self.assignment_api,
self.tenant_baz['id'])
self.resource_api.get_project.invalidate(self.resource_api,
self.tenant_baz['id'])
tenant_ref = self.assignment_api.get_project(self.tenant_baz['id'])
self.assertEqual(self.tenant_baz['id'], tenant_ref['id'])
self.assertEqual(self.tenant_baz['description'], tenant_ref['name'])
@ -1093,8 +1093,8 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase):
# that could affect what the drivers would return up to the manager.
# This solves this assumption when working with aggressive (on-create)
# cache population.
self.assignment_api.get_project.invalidate(self.assignment_api,
self.tenant_baz['id'])
self.resource_api.get_project.invalidate(self.resource_api,
self.tenant_baz['id'])
tenant_ref = self.assignment_api.get_project(self.tenant_baz['id'])
self.assertEqual(self.tenant_baz['id'], tenant_ref['id'])
self.assertNotIn('name', tenant_ref)
@ -1566,15 +1566,15 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase):
self.assignment_api.get_project(project_id)
updated_project = copy.deepcopy(project)
updated_project['description'] = uuid.uuid4().hex
# Update project, bypassing assignment_api manager
self.assignment_api.driver.update_project(project_id,
updated_project)
# Update project, bypassing resource manager
self.resource_api.driver.update_project(project_id,
updated_project)
# Verify get_project still returns the original project_ref
self.assertDictContainsSubset(
project, self.assignment_api.get_project(project_id))
# Invalidate cache
self.assignment_api.get_project.invalidate(self.assignment_api,
project_id)
self.resource_api.get_project.invalidate(self.resource_api,
project_id)
# Verify get_project now returns the new project
self.assertDictContainsSubset(
updated_project,
@ -1584,14 +1584,14 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase):
# Verify get_project returns the original project_ref
self.assertDictContainsSubset(
project, self.assignment_api.get_project(project_id))
# Delete project bypassing assignment_api
self.assignment_api.driver.delete_project(project_id)
# Delete project bypassing resource_api
self.resource_api.driver.delete_project(project_id)
# Verify get_project still returns the project_ref
self.assertDictContainsSubset(
project, self.assignment_api.get_project(project_id))
# Invalidate cache
self.assignment_api.get_project.invalidate(self.assignment_api,
project_id)
self.resource_api.get_project.invalidate(self.resource_api,
project_id)
# Verify ProjectNotFound now raised
self.assertRaises(exception.ProjectNotFound,
self.assignment_api.get_project,
@ -2115,6 +2115,9 @@ class LdapIdentitySqlAssignment(BaseLDAPIdentity, tests.SQLDriverOverrides,
self.config_fixture.config(
group='identity',
driver='keystone.identity.backends.ldap.Identity')
self.config_fixture.config(
group='resource',
driver='keystone.resource.backends.sql.Resource')
self.config_fixture.config(
group='assignment',
driver='keystone.assignment.backends.sql.Assignment')
@ -2124,7 +2127,7 @@ class LdapIdentitySqlAssignment(BaseLDAPIdentity, tests.SQLDriverOverrides,
def test_list_domains(self):
domains = self.assignment_api.list_domains()
self.assertEqual([assignment.calc_default_domain()], domains)
self.assertEqual([resource.calc_default_domain()], domains)
def test_list_domains_non_default_domain_id(self):
# If change the default_domain_id, the ID of the default domain
@ -2340,7 +2343,7 @@ class BaseMultiLDAPandSQLIdentity(object):
self.domains[domain] = create_domain(
{'id': uuid.uuid4().hex, 'name': domain})
self.domains['domain_default'] = create_domain(
assignment.calc_default_domain())
resource.calc_default_domain())
def test_authenticate_to_each_domain(self):
"""Test that a user in each domain can authenticate."""
@ -2408,6 +2411,9 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, tests.SQLDriverOverrides,
self.config_fixture.config(
group='identity',
driver='keystone.identity.backends.sql.Identity')
self.config_fixture.config(
group='resource',
driver='keystone.resource.backends.sql.Resource')
self.config_fixture.config(
group='assignment',
driver='keystone.assignment.backends.sql.Assignment')
@ -2672,8 +2678,11 @@ class DomainSpecificLDAPandSQLIdentity(
def config_overrides(self):
super(DomainSpecificLDAPandSQLIdentity, self).config_overrides()
# Make sure assignment is actually an SQL driver,
# Make sure resource & assignment are actually SQL drivers,
# BaseLDAPIdentity causes this option to use LDAP.
self.config_fixture.config(
group='resource',
driver='keystone.resource.backends.sql.Resource')
self.config_fixture.config(
group='assignment',
driver='keystone.assignment.backends.sql.Assignment')
@ -2833,6 +2842,9 @@ class DomainSpecificSQLIdentity(DomainSpecificLDAPandSQLIdentity):
self.config_fixture.config(
group='identity',
driver='keystone.identity.backends.ldap.Identity')
self.config_fixture.config(
group='resource',
driver='keystone.resource.backends.sql.Resource')
self.config_fixture.config(
group='assignment',
driver='keystone.assignment.backends.sql.Assignment')

View File

@ -838,3 +838,35 @@ class DeprecatedDecorators(SqlTests):
self.assertRaises(logging.DeprecatedConfig,
self.assignment_api.create_role,
role_ref['id'], role_ref)
def test_assignment_to_resource_api(self):
"""Test that calling one of the methods does call LOG.deprecated.
This method is really generic to the type of backend, but we need
one to execute the test, so the SQL backend is as good as any.
"""
# Rather than try and check that a log message is issued, we
# enable fatal_deprecations so that we can check for the
# raising of the exception.
# First try to create a project without enabling fatal deprecations,
# which should work due to the cross manager deprecated calls.
project_ref = {
'id': uuid.uuid4().hex,
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID}
self.assignment_api.create_project(project_ref['id'], project_ref)
self.resource_api.get_project(project_ref['id'])
# Now enable fatal exceptions - creating a project by calling the
# old manager should now fail.
self.config_fixture.config(fatal_deprecations=True)
project_ref = {
'id': uuid.uuid4().hex,
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID}
self.assertRaises(logging.DeprecatedConfig,
self.assignment_api.create_project,
project_ref['id'], project_ref)

View File

@ -400,7 +400,7 @@ class IdentityTestListLimitCase(IdentityTestFilteredCase):
self._test_entity_list_limit('group', 'identity')
def test_projects_list_limit(self):
self._test_entity_list_limit('project', 'assignment')
self._test_entity_list_limit('project', 'resource')
def test_services_list_limit(self):
self._test_entity_list_limit('service', 'catalog')