aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/identity
diff options
context:
space:
mode:
authorWuKong <rebirthmonkey@gmail.com>2015-06-30 18:47:29 +0200
committerWuKong <rebirthmonkey@gmail.com>2015-06-30 18:47:29 +0200
commitb8c756ecdd7cced1db4300935484e8c83701c82e (patch)
tree87e51107d82b217ede145de9d9d59e2100725bd7 /keystone-moon/keystone/identity
parentc304c773bae68fb854ed9eab8fb35c4ef17cf136 (diff)
migrate moon code from github to opnfv
Change-Id: Ice53e368fd1114d56a75271aa9f2e598e3eba604 Signed-off-by: WuKong <rebirthmonkey@gmail.com>
Diffstat (limited to 'keystone-moon/keystone/identity')
-rw-r--r--keystone-moon/keystone/identity/__init__.py18
-rw-r--r--keystone-moon/keystone/identity/backends/__init__.py0
-rw-r--r--keystone-moon/keystone/identity/backends/ldap.py402
-rw-r--r--keystone-moon/keystone/identity/backends/sql.py314
-rw-r--r--keystone-moon/keystone/identity/controllers.py335
-rw-r--r--keystone-moon/keystone/identity/core.py1259
-rw-r--r--keystone-moon/keystone/identity/generator.py52
-rw-r--r--keystone-moon/keystone/identity/id_generators/__init__.py0
-rw-r--r--keystone-moon/keystone/identity/id_generators/sha256.py28
-rw-r--r--keystone-moon/keystone/identity/mapping_backends/__init__.py0
-rw-r--r--keystone-moon/keystone/identity/mapping_backends/mapping.py18
-rw-r--r--keystone-moon/keystone/identity/mapping_backends/sql.py97
-rw-r--r--keystone-moon/keystone/identity/routers.py84
13 files changed, 2607 insertions, 0 deletions
diff --git a/keystone-moon/keystone/identity/__init__.py b/keystone-moon/keystone/identity/__init__.py
new file mode 100644
index 00000000..3063b5ca
--- /dev/null
+++ b/keystone-moon/keystone/identity/__init__.py
@@ -0,0 +1,18 @@
+# Copyright 2012 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from keystone.identity import controllers # noqa
+from keystone.identity.core import * # noqa
+from keystone.identity import generator # noqa
+from keystone.identity import routers # noqa
diff --git a/keystone-moon/keystone/identity/backends/__init__.py b/keystone-moon/keystone/identity/backends/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone-moon/keystone/identity/backends/__init__.py
diff --git a/keystone-moon/keystone/identity/backends/ldap.py b/keystone-moon/keystone/identity/backends/ldap.py
new file mode 100644
index 00000000..0f7ee450
--- /dev/null
+++ b/keystone-moon/keystone/identity/backends/ldap.py
@@ -0,0 +1,402 @@
+# Copyright 2012 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+from __future__ import absolute_import
+import uuid
+
+import ldap
+import ldap.filter
+from oslo_config import cfg
+from oslo_log import log
+import six
+
+from keystone import clean
+from keystone.common import driver_hints
+from keystone.common import ldap as common_ldap
+from keystone.common import models
+from keystone import exception
+from keystone.i18n import _
+from keystone import identity
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+
+class Identity(identity.Driver):
+ def __init__(self, conf=None):
+ super(Identity, self).__init__()
+ if conf is None:
+ conf = CONF
+ self.user = UserApi(conf)
+ self.group = GroupApi(conf)
+
+ def default_assignment_driver(self):
+ return "keystone.assignment.backends.ldap.Assignment"
+
+ def is_domain_aware(self):
+ return False
+
+ def generates_uuids(self):
+ return False
+
+ # Identity interface
+
+ def authenticate(self, user_id, password):
+ try:
+ user_ref = self._get_user(user_id)
+ except exception.UserNotFound:
+ raise AssertionError(_('Invalid user / password'))
+ if not user_id or not password:
+ raise AssertionError(_('Invalid user / password'))
+ conn = None
+ try:
+ conn = self.user.get_connection(user_ref['dn'],
+ password, end_user_auth=True)
+ if not conn:
+ raise AssertionError(_('Invalid user / password'))
+ except Exception:
+ raise AssertionError(_('Invalid user / password'))
+ finally:
+ if conn:
+ conn.unbind_s()
+ return self.user.filter_attributes(user_ref)
+
+ def _get_user(self, user_id):
+ return self.user.get(user_id)
+
+ def get_user(self, user_id):
+ return self.user.get_filtered(user_id)
+
+ def list_users(self, hints):
+ return self.user.get_all_filtered(hints)
+
+ def get_user_by_name(self, user_name, domain_id):
+ # domain_id will already have been handled in the Manager layer,
+ # parameter left in so this matches the Driver specification
+ return self.user.filter_attributes(self.user.get_by_name(user_name))
+
+ # CRUD
+ def create_user(self, user_id, user):
+ self.user.check_allow_create()
+ user_ref = self.user.create(user)
+ return self.user.filter_attributes(user_ref)
+
+ def update_user(self, user_id, user):
+ self.user.check_allow_update()
+ old_obj = self.user.get(user_id)
+ if 'name' in user and old_obj.get('name') != user['name']:
+ raise exception.Conflict(_('Cannot change user name'))
+
+ if self.user.enabled_mask:
+ self.user.mask_enabled_attribute(user)
+ elif self.user.enabled_invert and not self.user.enabled_emulation:
+ # We need to invert the enabled value for the old model object
+ # to prevent the LDAP update code from thinking that the enabled
+ # values are already equal.
+ user['enabled'] = not user['enabled']
+ old_obj['enabled'] = not old_obj['enabled']
+
+ self.user.update(user_id, user, old_obj)
+ return self.user.get_filtered(user_id)
+
+ def delete_user(self, user_id):
+ self.user.check_allow_delete()
+ user = self.user.get(user_id)
+ user_dn = user['dn']
+ groups = self.group.list_user_groups(user_dn)
+ for group in groups:
+ self.group.remove_user(user_dn, group['id'], user_id)
+
+ if hasattr(user, 'tenant_id'):
+ self.project.remove_user(user.tenant_id, user_dn)
+ self.user.delete(user_id)
+
+ def create_group(self, group_id, group):
+ self.group.check_allow_create()
+ group['name'] = clean.group_name(group['name'])
+ return common_ldap.filter_entity(self.group.create(group))
+
+ def get_group(self, group_id):
+ return self.group.get_filtered(group_id)
+
+ def get_group_by_name(self, group_name, domain_id):
+ # domain_id will already have been handled in the Manager layer,
+ # parameter left in so this matches the Driver specification
+ return self.group.get_filtered_by_name(group_name)
+
+ def update_group(self, group_id, group):
+ self.group.check_allow_update()
+ if 'name' in group:
+ group['name'] = clean.group_name(group['name'])
+ return common_ldap.filter_entity(self.group.update(group_id, group))
+
+ def delete_group(self, group_id):
+ self.group.check_allow_delete()
+ return self.group.delete(group_id)
+
+ def add_user_to_group(self, user_id, group_id):
+ user_ref = self._get_user(user_id)
+ user_dn = user_ref['dn']
+ self.group.add_user(user_dn, group_id, user_id)
+
+ def remove_user_from_group(self, user_id, group_id):
+ user_ref = self._get_user(user_id)
+ user_dn = user_ref['dn']
+ self.group.remove_user(user_dn, group_id, user_id)
+
+ def list_groups_for_user(self, user_id, hints):
+ user_ref = self._get_user(user_id)
+ user_dn = user_ref['dn']
+ return self.group.list_user_groups_filtered(user_dn, hints)
+
+ def list_groups(self, hints):
+ return self.group.get_all_filtered(hints)
+
+ def list_users_in_group(self, group_id, hints):
+ users = []
+ for user_dn in self.group.list_group_users(group_id):
+ user_id = self.user._dn_to_id(user_dn)
+ try:
+ users.append(self.user.get_filtered(user_id))
+ except exception.UserNotFound:
+ LOG.debug(("Group member '%(user_dn)s' not found in"
+ " '%(group_id)s'. The user should be removed"
+ " from the group. The user will be ignored."),
+ dict(user_dn=user_dn, group_id=group_id))
+ return users
+
+ def check_user_in_group(self, user_id, group_id):
+ user_refs = self.list_users_in_group(group_id, driver_hints.Hints())
+ for x in user_refs:
+ if x['id'] == user_id:
+ break
+ else:
+ # Try to fetch the user to see if it even exists. This
+ # will raise a more accurate exception.
+ self.get_user(user_id)
+ raise exception.NotFound(_("User '%(user_id)s' not found in"
+ " group '%(group_id)s'") %
+ {'user_id': user_id,
+ 'group_id': group_id})
+
+
+# TODO(termie): turn this into a data object and move logic to driver
+class UserApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap):
+ DEFAULT_OU = 'ou=Users'
+ DEFAULT_STRUCTURAL_CLASSES = ['person']
+ DEFAULT_ID_ATTR = 'cn'
+ DEFAULT_OBJECTCLASS = 'inetOrgPerson'
+ NotFound = exception.UserNotFound
+ options_name = 'user'
+ attribute_options_names = {'password': 'pass',
+ 'email': 'mail',
+ 'name': 'name',
+ 'enabled': 'enabled',
+ 'default_project_id': 'default_project_id'}
+ immutable_attrs = ['id']
+
+ model = models.User
+
+ def __init__(self, conf):
+ super(UserApi, self).__init__(conf)
+ self.enabled_mask = conf.ldap.user_enabled_mask
+ self.enabled_default = conf.ldap.user_enabled_default
+ self.enabled_invert = conf.ldap.user_enabled_invert
+ self.enabled_emulation = conf.ldap.user_enabled_emulation
+
+ def _ldap_res_to_model(self, res):
+ obj = super(UserApi, self)._ldap_res_to_model(res)
+ if self.enabled_mask != 0:
+ enabled = int(obj.get('enabled', self.enabled_default))
+ obj['enabled'] = ((enabled & self.enabled_mask) !=
+ self.enabled_mask)
+ elif self.enabled_invert and not self.enabled_emulation:
+ # This could be a bool or a string. If it's a string,
+ # we need to convert it so we can invert it properly.
+ enabled = obj.get('enabled', self.enabled_default)
+ if isinstance(enabled, six.string_types):
+ if enabled.lower() == 'true':
+ enabled = True
+ else:
+ enabled = False
+ obj['enabled'] = not enabled
+ obj['dn'] = res[0]
+
+ return obj
+
+ def mask_enabled_attribute(self, values):
+ value = values['enabled']
+ values.setdefault('enabled_nomask', int(self.enabled_default))
+ if value != ((values['enabled_nomask'] & self.enabled_mask) !=
+ self.enabled_mask):
+ values['enabled_nomask'] ^= self.enabled_mask
+ values['enabled'] = values['enabled_nomask']
+ del values['enabled_nomask']
+
+ def create(self, values):
+ if self.enabled_mask:
+ orig_enabled = values['enabled']
+ self.mask_enabled_attribute(values)
+ elif self.enabled_invert and not self.enabled_emulation:
+ orig_enabled = values['enabled']
+ if orig_enabled is not None:
+ values['enabled'] = not orig_enabled
+ else:
+ values['enabled'] = self.enabled_default
+ values = super(UserApi, self).create(values)
+ if self.enabled_mask or (self.enabled_invert and
+ not self.enabled_emulation):
+ values['enabled'] = orig_enabled
+ return values
+
+ def get_filtered(self, user_id):
+ user = self.get(user_id)
+ return self.filter_attributes(user)
+
+ def get_all_filtered(self, hints):
+ query = self.filter_query(hints)
+ return [self.filter_attributes(user) for user in self.get_all(query)]
+
+ def filter_attributes(self, user):
+ return identity.filter_user(common_ldap.filter_entity(user))
+
+ def is_user(self, dn):
+ """Returns True if the entry is a user."""
+
+ # NOTE(blk-u): It's easy to check if the DN is under the User tree,
+ # but may not be accurate. A more accurate test would be to fetch the
+ # entry to see if it's got the user objectclass, but this could be
+ # really expensive considering how this is used.
+
+ return common_ldap.dn_startswith(dn, self.tree_dn)
+
+
+class GroupApi(common_ldap.BaseLdap):
+ DEFAULT_OU = 'ou=UserGroups'
+ DEFAULT_STRUCTURAL_CLASSES = []
+ DEFAULT_OBJECTCLASS = 'groupOfNames'
+ DEFAULT_ID_ATTR = 'cn'
+ DEFAULT_MEMBER_ATTRIBUTE = 'member'
+ NotFound = exception.GroupNotFound
+ options_name = 'group'
+ attribute_options_names = {'description': 'desc',
+ 'name': 'name'}
+ immutable_attrs = ['name']
+ model = models.Group
+
+ def _ldap_res_to_model(self, res):
+ model = super(GroupApi, self)._ldap_res_to_model(res)
+ model['dn'] = res[0]
+ return model
+
+ def __init__(self, conf):
+ super(GroupApi, self).__init__(conf)
+ self.member_attribute = (conf.ldap.group_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
+ if 'description' in data and data['description'] in ['', None]:
+ data.pop('description')
+ return super(GroupApi, self).create(data)
+
+ def delete(self, group_id):
+ if self.subtree_delete_enabled:
+ super(GroupApi, self).deleteTree(group_id)
+ else:
+ # TODO(spzala): this is only placeholder for group and domain
+ # role support which will be added under bug 1101287
+
+ group_ref = self.get(group_id)
+ group_dn = group_ref['dn']
+ if group_dn:
+ self._delete_tree_nodes(group_dn, ldap.SCOPE_ONELEVEL)
+ super(GroupApi, self).delete(group_id)
+
+ def update(self, group_id, values):
+ old_obj = self.get(group_id)
+ return super(GroupApi, self).update(group_id, values, old_obj)
+
+ def add_user(self, user_dn, group_id, user_id):
+ group_ref = self.get(group_id)
+ group_dn = group_ref['dn']
+ try:
+ super(GroupApi, self).add_member(user_dn, group_dn)
+ except exception.Conflict:
+ raise exception.Conflict(_(
+ 'User %(user_id)s is already a member of group %(group_id)s') %
+ {'user_id': user_id, 'group_id': group_id})
+
+ def remove_user(self, user_dn, group_id, user_id):
+ group_ref = self.get(group_id)
+ group_dn = group_ref['dn']
+ try:
+ super(GroupApi, self).remove_member(user_dn, group_dn)
+ except ldap.NO_SUCH_ATTRIBUTE:
+ raise exception.UserNotFound(user_id=user_id)
+
+ def list_user_groups(self, user_dn):
+ """Return a list of groups for which the user is a member."""
+
+ user_dn_esc = ldap.filter.escape_filter_chars(user_dn)
+ query = '(&(objectClass=%s)(%s=%s)%s)' % (self.object_class,
+ self.member_attribute,
+ user_dn_esc,
+ self.ldap_filter or '')
+ return self.get_all(query)
+
+ def list_user_groups_filtered(self, user_dn, hints):
+ """Return a filtered list of groups for which the user is a member."""
+
+ user_dn_esc = ldap.filter.escape_filter_chars(user_dn)
+ query = '(&(objectClass=%s)(%s=%s)%s)' % (self.object_class,
+ self.member_attribute,
+ user_dn_esc,
+ self.ldap_filter or '')
+ return self.get_all_filtered(hints, query)
+
+ def list_group_users(self, group_id):
+ """Return a list of user dns which are members of a group."""
+ group_ref = self.get(group_id)
+ group_dn = group_ref['dn']
+
+ try:
+ attrs = self._ldap_get_list(group_dn, ldap.SCOPE_BASE,
+ attrlist=[self.member_attribute])
+ except ldap.NO_SUCH_OBJECT:
+ raise self.NotFound(group_id=group_id)
+
+ users = []
+ for dn, member in attrs:
+ user_dns = member.get(self.member_attribute, [])
+ for user_dn in user_dns:
+ if self._is_dumb_member(user_dn):
+ continue
+ users.append(user_dn)
+ return users
+
+ def get_filtered(self, group_id):
+ group = self.get(group_id)
+ return common_ldap.filter_entity(group)
+
+ def get_filtered_by_name(self, group_name):
+ group = self.get_by_name(group_name)
+ return common_ldap.filter_entity(group)
+
+ def get_all_filtered(self, hints, query=None):
+ query = self.filter_query(hints, query)
+ return [common_ldap.filter_entity(group)
+ for group in self.get_all(query)]
diff --git a/keystone-moon/keystone/identity/backends/sql.py b/keystone-moon/keystone/identity/backends/sql.py
new file mode 100644
index 00000000..39868416
--- /dev/null
+++ b/keystone-moon/keystone/identity/backends/sql.py
@@ -0,0 +1,314 @@
+# Copyright 2012 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_config import cfg
+
+from keystone.common import sql
+from keystone.common import utils
+from keystone import exception
+from keystone.i18n import _
+from keystone import identity
+
+
+CONF = cfg.CONF
+
+
+class User(sql.ModelBase, sql.DictBase):
+ __tablename__ = 'user'
+ attributes = ['id', 'name', 'domain_id', 'password', 'enabled',
+ 'default_project_id']
+ id = sql.Column(sql.String(64), primary_key=True)
+ name = sql.Column(sql.String(255), nullable=False)
+ domain_id = sql.Column(sql.String(64), nullable=False)
+ password = sql.Column(sql.String(128))
+ enabled = sql.Column(sql.Boolean)
+ extra = sql.Column(sql.JsonBlob())
+ default_project_id = sql.Column(sql.String(64))
+ # Unique constraint across two columns to create the separation
+ # rather than just only 'name' being unique
+ __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {})
+
+ def to_dict(self, include_extra_dict=False):
+ d = super(User, self).to_dict(include_extra_dict=include_extra_dict)
+ if 'default_project_id' in d and d['default_project_id'] is None:
+ del d['default_project_id']
+ return d
+
+
+class Group(sql.ModelBase, sql.DictBase):
+ __tablename__ = 'group'
+ attributes = ['id', 'name', 'domain_id', 'description']
+ id = sql.Column(sql.String(64), primary_key=True)
+ name = sql.Column(sql.String(64), nullable=False)
+ domain_id = sql.Column(sql.String(64), nullable=False)
+ description = sql.Column(sql.Text())
+ extra = sql.Column(sql.JsonBlob())
+ # Unique constraint across two columns to create the separation
+ # rather than just only 'name' being unique
+ __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {})
+
+
+class UserGroupMembership(sql.ModelBase, sql.DictBase):
+ """Group membership join table."""
+ __tablename__ = 'user_group_membership'
+ user_id = sql.Column(sql.String(64),
+ sql.ForeignKey('user.id'),
+ primary_key=True)
+ group_id = sql.Column(sql.String(64),
+ sql.ForeignKey('group.id'),
+ primary_key=True)
+
+
+class Identity(identity.Driver):
+ # NOTE(henry-nash): Override the __init__() method so as to take a
+ # config parameter to enable sql to be used as a domain-specific driver.
+ def __init__(self, conf=None):
+ super(Identity, self).__init__()
+
+ def default_assignment_driver(self):
+ return "keystone.assignment.backends.sql.Assignment"
+
+ @property
+ def is_sql(self):
+ return True
+
+ def _check_password(self, password, user_ref):
+ """Check the specified password against the data store.
+
+ Note that we'll pass in the entire user_ref in case the subclass
+ needs things like user_ref.get('name')
+ For further justification, please see the follow up suggestion at
+ https://blueprints.launchpad.net/keystone/+spec/sql-identiy-pam
+
+ """
+ return utils.check_password(password, user_ref.password)
+
+ # Identity interface
+ def authenticate(self, user_id, password):
+ session = sql.get_session()
+ user_ref = None
+ try:
+ user_ref = self._get_user(session, user_id)
+ except exception.UserNotFound:
+ raise AssertionError(_('Invalid user / password'))
+ if not self._check_password(password, user_ref):
+ raise AssertionError(_('Invalid user / password'))
+ return identity.filter_user(user_ref.to_dict())
+
+ # user crud
+
+ @sql.handle_conflicts(conflict_type='user')
+ def create_user(self, user_id, user):
+ user = utils.hash_user_password(user)
+ session = sql.get_session()
+ with session.begin():
+ user_ref = User.from_dict(user)
+ session.add(user_ref)
+ return identity.filter_user(user_ref.to_dict())
+
+ @sql.truncated
+ def list_users(self, hints):
+ session = sql.get_session()
+ query = session.query(User)
+ user_refs = sql.filter_limit_query(User, query, hints)
+ return [identity.filter_user(x.to_dict()) for x in user_refs]
+
+ def _get_user(self, session, user_id):
+ user_ref = session.query(User).get(user_id)
+ if not user_ref:
+ raise exception.UserNotFound(user_id=user_id)
+ return user_ref
+
+ def get_user(self, user_id):
+ session = sql.get_session()
+ return identity.filter_user(self._get_user(session, user_id).to_dict())
+
+ def get_user_by_name(self, user_name, domain_id):
+ session = sql.get_session()
+ query = session.query(User)
+ query = query.filter_by(name=user_name)
+ query = query.filter_by(domain_id=domain_id)
+ try:
+ user_ref = query.one()
+ except sql.NotFound:
+ raise exception.UserNotFound(user_id=user_name)
+ return identity.filter_user(user_ref.to_dict())
+
+ @sql.handle_conflicts(conflict_type='user')
+ def update_user(self, user_id, user):
+ session = sql.get_session()
+
+ with session.begin():
+ user_ref = self._get_user(session, user_id)
+ old_user_dict = user_ref.to_dict()
+ user = utils.hash_user_password(user)
+ for k in user:
+ old_user_dict[k] = user[k]
+ new_user = User.from_dict(old_user_dict)
+ for attr in User.attributes:
+ if attr != 'id':
+ setattr(user_ref, attr, getattr(new_user, attr))
+ user_ref.extra = new_user.extra
+ return identity.filter_user(user_ref.to_dict(include_extra_dict=True))
+
+ def add_user_to_group(self, user_id, group_id):
+ session = sql.get_session()
+ self.get_group(group_id)
+ self.get_user(user_id)
+ query = session.query(UserGroupMembership)
+ query = query.filter_by(user_id=user_id)
+ query = query.filter_by(group_id=group_id)
+ rv = query.first()
+ if rv:
+ return
+
+ with session.begin():
+ session.add(UserGroupMembership(user_id=user_id,
+ group_id=group_id))
+
+ def check_user_in_group(self, user_id, group_id):
+ session = sql.get_session()
+ self.get_group(group_id)
+ self.get_user(user_id)
+ query = session.query(UserGroupMembership)
+ query = query.filter_by(user_id=user_id)
+ query = query.filter_by(group_id=group_id)
+ if not query.first():
+ raise exception.NotFound(_("User '%(user_id)s' not found in"
+ " group '%(group_id)s'") %
+ {'user_id': user_id,
+ 'group_id': group_id})
+
+ def remove_user_from_group(self, user_id, group_id):
+ session = sql.get_session()
+ # We don't check if user or group are still valid and let the remove
+ # be tried anyway - in case this is some kind of clean-up operation
+ query = session.query(UserGroupMembership)
+ query = query.filter_by(user_id=user_id)
+ query = query.filter_by(group_id=group_id)
+ membership_ref = query.first()
+ if membership_ref is None:
+ # Check if the group and user exist to return descriptive
+ # exceptions.
+ self.get_group(group_id)
+ self.get_user(user_id)
+ raise exception.NotFound(_("User '%(user_id)s' not found in"
+ " group '%(group_id)s'") %
+ {'user_id': user_id,
+ 'group_id': group_id})
+ with session.begin():
+ session.delete(membership_ref)
+
+ def list_groups_for_user(self, user_id, hints):
+ # TODO(henry-nash) We could implement full filtering here by enhancing
+ # the join below. However, since it is likely to be a fairly rare
+ # occurrence to filter on more than the user_id already being used
+ # here, this is left as future enhancement and until then we leave
+ # it for the controller to do for us.
+ session = sql.get_session()
+ self.get_user(user_id)
+ query = session.query(Group).join(UserGroupMembership)
+ query = query.filter(UserGroupMembership.user_id == user_id)
+ return [g.to_dict() for g in query]
+
+ def list_users_in_group(self, group_id, hints):
+ # TODO(henry-nash) We could implement full filtering here by enhancing
+ # the join below. However, since it is likely to be a fairly rare
+ # occurrence to filter on more than the group_id already being used
+ # here, this is left as future enhancement and until then we leave
+ # it for the controller to do for us.
+ session = sql.get_session()
+ self.get_group(group_id)
+ query = session.query(User).join(UserGroupMembership)
+ query = query.filter(UserGroupMembership.group_id == group_id)
+
+ return [identity.filter_user(u.to_dict()) for u in query]
+
+ def delete_user(self, user_id):
+ session = sql.get_session()
+
+ with session.begin():
+ ref = self._get_user(session, user_id)
+
+ q = session.query(UserGroupMembership)
+ q = q.filter_by(user_id=user_id)
+ q.delete(False)
+
+ session.delete(ref)
+
+ # group crud
+
+ @sql.handle_conflicts(conflict_type='group')
+ def create_group(self, group_id, group):
+ session = sql.get_session()
+ with session.begin():
+ ref = Group.from_dict(group)
+ session.add(ref)
+ return ref.to_dict()
+
+ @sql.truncated
+ def list_groups(self, hints):
+ session = sql.get_session()
+ query = session.query(Group)
+ refs = sql.filter_limit_query(Group, query, hints)
+ return [ref.to_dict() for ref in refs]
+
+ def _get_group(self, session, group_id):
+ ref = session.query(Group).get(group_id)
+ if not ref:
+ raise exception.GroupNotFound(group_id=group_id)
+ return ref
+
+ def get_group(self, group_id):
+ session = sql.get_session()
+ return self._get_group(session, group_id).to_dict()
+
+ def get_group_by_name(self, group_name, domain_id):
+ session = sql.get_session()
+ query = session.query(Group)
+ query = query.filter_by(name=group_name)
+ query = query.filter_by(domain_id=domain_id)
+ try:
+ group_ref = query.one()
+ except sql.NotFound:
+ raise exception.GroupNotFound(group_id=group_name)
+ return group_ref.to_dict()
+
+ @sql.handle_conflicts(conflict_type='group')
+ def update_group(self, group_id, group):
+ session = sql.get_session()
+
+ with session.begin():
+ ref = self._get_group(session, group_id)
+ old_dict = ref.to_dict()
+ for k in group:
+ old_dict[k] = group[k]
+ new_group = Group.from_dict(old_dict)
+ for attr in Group.attributes:
+ if attr != 'id':
+ setattr(ref, attr, getattr(new_group, attr))
+ ref.extra = new_group.extra
+ return ref.to_dict()
+
+ def delete_group(self, group_id):
+ session = sql.get_session()
+
+ with session.begin():
+ ref = self._get_group(session, group_id)
+
+ q = session.query(UserGroupMembership)
+ q = q.filter_by(group_id=group_id)
+ q.delete(False)
+
+ session.delete(ref)
diff --git a/keystone-moon/keystone/identity/controllers.py b/keystone-moon/keystone/identity/controllers.py
new file mode 100644
index 00000000..a2676c41
--- /dev/null
+++ b/keystone-moon/keystone/identity/controllers.py
@@ -0,0 +1,335 @@
+# Copyright 2012 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Workflow Logic the Identity service."""
+
+from oslo_config import cfg
+from oslo_log import log
+
+from keystone.common import controller
+from keystone.common import dependency
+from keystone import exception
+from keystone.i18n import _, _LW
+from keystone import notifications
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+
+@dependency.requires('assignment_api', 'identity_api', 'resource_api')
+class User(controller.V2Controller):
+
+ @controller.v2_deprecated
+ def get_user(self, context, user_id):
+ self.assert_admin(context)
+ ref = self.identity_api.get_user(user_id)
+ return {'user': self.v3_to_v2_user(ref)}
+
+ @controller.v2_deprecated
+ def get_users(self, context):
+ # NOTE(termie): i can't imagine that this really wants all the data
+ # about every single user in the system...
+ if 'name' in context['query_string']:
+ return self.get_user_by_name(
+ context, context['query_string'].get('name'))
+
+ self.assert_admin(context)
+ user_list = self.identity_api.list_users(
+ CONF.identity.default_domain_id)
+ return {'users': self.v3_to_v2_user(user_list)}
+
+ @controller.v2_deprecated
+ def get_user_by_name(self, context, user_name):
+ self.assert_admin(context)
+ ref = self.identity_api.get_user_by_name(
+ user_name, CONF.identity.default_domain_id)
+ return {'user': self.v3_to_v2_user(ref)}
+
+ # CRUD extension
+ @controller.v2_deprecated
+ def create_user(self, context, user):
+ user = self._normalize_OSKSADM_password_on_request(user)
+ user = self.normalize_username_in_request(user)
+ user = self._normalize_dict(user)
+ self.assert_admin(context)
+
+ if 'name' not in user or not user['name']:
+ msg = _('Name field is required and cannot be empty')
+ raise exception.ValidationError(message=msg)
+ if 'enabled' in user and not isinstance(user['enabled'], bool):
+ msg = _('Enabled field must be a boolean')
+ raise exception.ValidationError(message=msg)
+
+ default_project_id = user.pop('tenantId', None)
+ if default_project_id is not None:
+ # Check to see if the project is valid before moving on.
+ self.resource_api.get_project(default_project_id)
+ user['default_project_id'] = default_project_id
+
+ # The manager layer will generate the unique ID for users
+ user_ref = self._normalize_domain_id(context, user.copy())
+ new_user_ref = self.v3_to_v2_user(
+ self.identity_api.create_user(user_ref))
+
+ if default_project_id is not None:
+ self.assignment_api.add_user_to_project(default_project_id,
+ new_user_ref['id'])
+ return {'user': new_user_ref}
+
+ @controller.v2_deprecated
+ def update_user(self, context, user_id, user):
+ # NOTE(termie): this is really more of a patch than a put
+ user = self.normalize_username_in_request(user)
+ self.assert_admin(context)
+
+ if 'enabled' in user and not isinstance(user['enabled'], bool):
+ msg = _('Enabled field should be a boolean')
+ raise exception.ValidationError(message=msg)
+
+ default_project_id = user.pop('tenantId', None)
+ if default_project_id is not None:
+ user['default_project_id'] = default_project_id
+
+ old_user_ref = self.v3_to_v2_user(
+ self.identity_api.get_user(user_id))
+
+ # Check whether a tenant is being added or changed for the user.
+ # Catch the case where the tenant is being changed for a user and also
+ # where a user previously had no tenant but a tenant is now being
+ # added for the user.
+ if (('tenantId' in old_user_ref and
+ old_user_ref['tenantId'] != default_project_id and
+ default_project_id is not None) or
+ ('tenantId' not in old_user_ref and
+ default_project_id is not None)):
+ # Make sure the new project actually exists before we perform the
+ # user update.
+ self.resource_api.get_project(default_project_id)
+
+ user_ref = self.v3_to_v2_user(
+ self.identity_api.update_user(user_id, user))
+
+ # If 'tenantId' is in either ref, we might need to add or remove the
+ # user from a project.
+ if 'tenantId' in user_ref or 'tenantId' in old_user_ref:
+ if user_ref['tenantId'] != old_user_ref.get('tenantId'):
+ if old_user_ref.get('tenantId'):
+ try:
+ member_role_id = CONF.member_role_id
+ self.assignment_api.remove_role_from_user_and_project(
+ user_id, old_user_ref['tenantId'], member_role_id)
+ except exception.NotFound:
+ # NOTE(morganfainberg): This is not a critical error it
+ # just means that the user cannot be removed from the
+ # old tenant. This could occur if roles aren't found
+ # or if the project is invalid or if there are no roles
+ # for the user on that project.
+ msg = _LW('Unable to remove user %(user)s from '
+ '%(tenant)s.')
+ LOG.warning(msg, {'user': user_id,
+ 'tenant': old_user_ref['tenantId']})
+
+ if user_ref['tenantId']:
+ try:
+ self.assignment_api.add_user_to_project(
+ user_ref['tenantId'], user_id)
+ except exception.Conflict:
+ # We are already a member of that tenant
+ pass
+ except exception.NotFound:
+ # NOTE(morganfainberg): Log this and move on. This is
+ # not the end of the world if we can't add the user to
+ # the appropriate tenant. Most of the time this means
+ # that the project is invalid or roles are some how
+ # incorrect. This shouldn't prevent the return of the
+ # new ref.
+ msg = _LW('Unable to add user %(user)s to %(tenant)s.')
+ LOG.warning(msg, {'user': user_id,
+ 'tenant': user_ref['tenantId']})
+
+ return {'user': user_ref}
+
+ @controller.v2_deprecated
+ def delete_user(self, context, user_id):
+ self.assert_admin(context)
+ self.identity_api.delete_user(user_id)
+
+ @controller.v2_deprecated
+ def set_user_enabled(self, context, user_id, user):
+ return self.update_user(context, user_id, user)
+
+ @controller.v2_deprecated
+ def set_user_password(self, context, user_id, user):
+ user = self._normalize_OSKSADM_password_on_request(user)
+ return self.update_user(context, user_id, user)
+
+ @staticmethod
+ def _normalize_OSKSADM_password_on_request(ref):
+ """Sets the password from the OS-KSADM Admin Extension.
+
+ The OS-KSADM Admin Extension documentation says that
+ `OS-KSADM:password` can be used in place of `password`.
+
+ """
+ if 'OS-KSADM:password' in ref:
+ ref['password'] = ref.pop('OS-KSADM:password')
+ return ref
+
+
+@dependency.requires('identity_api')
+class UserV3(controller.V3Controller):
+ collection_name = 'users'
+ member_name = 'user'
+
+ def __init__(self):
+ super(UserV3, self).__init__()
+ self.get_member_from_driver = self.identity_api.get_user
+
+ def _check_user_and_group_protection(self, context, prep_info,
+ user_id, group_id):
+ ref = {}
+ ref['user'] = self.identity_api.get_user(user_id)
+ ref['group'] = self.identity_api.get_group(group_id)
+ self.check_protection(context, prep_info, ref)
+
+ @controller.protected()
+ def create_user(self, context, user):
+ self._require_attribute(user, 'name')
+
+ # The manager layer will generate the unique ID for users
+ ref = self._normalize_dict(user)
+ ref = self._normalize_domain_id(context, ref)
+ initiator = notifications._get_request_audit_info(context)
+ ref = self.identity_api.create_user(ref, initiator)
+ return UserV3.wrap_member(context, ref)
+
+ @controller.filterprotected('domain_id', 'enabled', 'name')
+ def list_users(self, context, filters):
+ hints = UserV3.build_driver_hints(context, filters)
+ refs = self.identity_api.list_users(
+ domain_scope=self._get_domain_id_for_list_request(context),
+ hints=hints)
+ return UserV3.wrap_collection(context, refs, hints=hints)
+
+ @controller.filterprotected('domain_id', 'enabled', 'name')
+ def list_users_in_group(self, context, filters, group_id):
+ hints = UserV3.build_driver_hints(context, filters)
+ refs = self.identity_api.list_users_in_group(group_id, hints=hints)
+ return UserV3.wrap_collection(context, refs, hints=hints)
+
+ @controller.protected()
+ def get_user(self, context, user_id):
+ ref = self.identity_api.get_user(user_id)
+ return UserV3.wrap_member(context, ref)
+
+ def _update_user(self, context, user_id, user):
+ self._require_matching_id(user_id, user)
+ self._require_matching_domain_id(
+ user_id, user, self.identity_api.get_user)
+ initiator = notifications._get_request_audit_info(context)
+ ref = self.identity_api.update_user(user_id, user, initiator)
+ return UserV3.wrap_member(context, ref)
+
+ @controller.protected()
+ def update_user(self, context, user_id, user):
+ return self._update_user(context, user_id, user)
+
+ @controller.protected(callback=_check_user_and_group_protection)
+ def add_user_to_group(self, context, user_id, group_id):
+ self.identity_api.add_user_to_group(user_id, group_id)
+
+ @controller.protected(callback=_check_user_and_group_protection)
+ def check_user_in_group(self, context, user_id, group_id):
+ return self.identity_api.check_user_in_group(user_id, group_id)
+
+ @controller.protected(callback=_check_user_and_group_protection)
+ def remove_user_from_group(self, context, user_id, group_id):
+ self.identity_api.remove_user_from_group(user_id, group_id)
+
+ @controller.protected()
+ def delete_user(self, context, user_id):
+ initiator = notifications._get_request_audit_info(context)
+ return self.identity_api.delete_user(user_id, initiator)
+
+ @controller.protected()
+ def change_password(self, context, user_id, user):
+ original_password = user.get('original_password')
+ if original_password is None:
+ raise exception.ValidationError(target='user',
+ attribute='original_password')
+
+ password = user.get('password')
+ if password is None:
+ raise exception.ValidationError(target='user',
+ attribute='password')
+ try:
+ self.identity_api.change_password(
+ context, user_id, original_password, password)
+ except AssertionError:
+ raise exception.Unauthorized()
+
+
+@dependency.requires('identity_api')
+class GroupV3(controller.V3Controller):
+ collection_name = 'groups'
+ member_name = 'group'
+
+ def __init__(self):
+ super(GroupV3, self).__init__()
+ self.get_member_from_driver = self.identity_api.get_group
+
+ @controller.protected()
+ def create_group(self, context, group):
+ self._require_attribute(group, 'name')
+
+ # The manager layer will generate the unique ID for groups
+ ref = self._normalize_dict(group)
+ ref = self._normalize_domain_id(context, ref)
+ initiator = notifications._get_request_audit_info(context)
+ ref = self.identity_api.create_group(ref, initiator)
+ return GroupV3.wrap_member(context, ref)
+
+ @controller.filterprotected('domain_id', 'name')
+ def list_groups(self, context, filters):
+ hints = GroupV3.build_driver_hints(context, filters)
+ refs = self.identity_api.list_groups(
+ domain_scope=self._get_domain_id_for_list_request(context),
+ hints=hints)
+ return GroupV3.wrap_collection(context, refs, hints=hints)
+
+ @controller.filterprotected('name')
+ def list_groups_for_user(self, context, filters, user_id):
+ hints = GroupV3.build_driver_hints(context, filters)
+ refs = self.identity_api.list_groups_for_user(user_id, hints=hints)
+ return GroupV3.wrap_collection(context, refs, hints=hints)
+
+ @controller.protected()
+ def get_group(self, context, group_id):
+ ref = self.identity_api.get_group(group_id)
+ return GroupV3.wrap_member(context, ref)
+
+ @controller.protected()
+ def update_group(self, context, group_id, group):
+ self._require_matching_id(group_id, group)
+ self._require_matching_domain_id(
+ group_id, group, self.identity_api.get_group)
+ initiator = notifications._get_request_audit_info(context)
+ ref = self.identity_api.update_group(group_id, group, initiator)
+ return GroupV3.wrap_member(context, ref)
+
+ @controller.protected()
+ def delete_group(self, context, group_id):
+ initiator = notifications._get_request_audit_info(context)
+ self.identity_api.delete_group(group_id, initiator)
diff --git a/keystone-moon/keystone/identity/core.py b/keystone-moon/keystone/identity/core.py
new file mode 100644
index 00000000..988df78b
--- /dev/null
+++ b/keystone-moon/keystone/identity/core.py
@@ -0,0 +1,1259 @@
+# Copyright 2012 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Main entry point into the Identity service."""
+
+import abc
+import functools
+import os
+import uuid
+
+from oslo_config import cfg
+from oslo_log import log
+from oslo_utils import importutils
+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 _, _LW
+from keystone.identity.mapping_backends import mapping
+from keystone import notifications
+
+
+CONF = cfg.CONF
+
+LOG = log.getLogger(__name__)
+
+MEMOIZE = cache.get_memoization_decorator(section='identity')
+
+DOMAIN_CONF_FHEAD = 'keystone.'
+DOMAIN_CONF_FTAIL = '.conf'
+
+
+def filter_user(user_ref):
+ """Filter out private items in a user dict.
+
+ 'password', 'tenants' and 'groups' are never returned.
+
+ :returns: user_ref
+
+ """
+ if user_ref:
+ user_ref = user_ref.copy()
+ user_ref.pop('password', None)
+ user_ref.pop('tenants', None)
+ user_ref.pop('groups', None)
+ user_ref.pop('domains', None)
+ try:
+ user_ref['extra'].pop('password', None)
+ user_ref['extra'].pop('tenants', None)
+ except KeyError:
+ pass
+ return user_ref
+
+
+@dependency.requires('domain_config_api')
+class DomainConfigs(dict):
+ """Discover, store and provide access to domain specific configs.
+
+ The setup_domain_drivers() call will be made via the wrapper from
+ the first call to any driver function handled by this manager.
+
+ Domain specific configurations are only supported for the identity backend
+ and the individual configurations are either specified in the resource
+ database or in individual domain configuration files, depending on the
+ setting of the 'domain_configurations_from_database' config option.
+
+ The result will be that for each domain with a specific configuration,
+ this class will hold a reference to a ConfigOpts and driver object that
+ the identity manager and driver can use.
+
+ """
+ configured = False
+ driver = None
+ _any_sql = False
+
+ def _load_driver(self, domain_config):
+ return importutils.import_object(
+ domain_config['cfg'].identity.driver, domain_config['cfg'])
+
+ def _assert_no_more_than_one_sql_driver(self, domain_id, new_config,
+ config_file=None):
+ """Ensure there is more than one sql driver.
+
+ Check to see if the addition of the driver in this new config
+ would cause there to now be more than one sql driver.
+
+ If we are loading from configuration files, the config_file will hold
+ the name of the file we have just loaded.
+
+ """
+ if (new_config['driver'].is_sql and
+ (self.driver.is_sql or self._any_sql)):
+ # The addition of this driver would cause us to have more than
+ # one sql driver, so raise an exception.
+ if not config_file:
+ config_file = _('Database at /domains/%s/config') % domain_id
+ raise exception.MultipleSQLDriversInConfig(source=config_file)
+ self._any_sql = new_config['driver'].is_sql
+
+ def _load_config_from_file(self, resource_api, file_list, domain_name):
+
+ try:
+ domain_ref = resource_api.get_domain_by_name(domain_name)
+ except exception.DomainNotFound:
+ LOG.warning(
+ _LW('Invalid domain name (%s) found in config file name'),
+ domain_name)
+ return
+
+ # Create a new entry in the domain config dict, which contains
+ # a new instance of both the conf environment and driver using
+ # options defined in this set of config files. Later, when we
+ # service calls via this Manager, we'll index via this domain
+ # config dict to make sure we call the right driver
+ domain_config = {}
+ domain_config['cfg'] = cfg.ConfigOpts()
+ config.configure(conf=domain_config['cfg'])
+ domain_config['cfg'](args=[], project='keystone',
+ default_config_files=file_list)
+ domain_config['driver'] = self._load_driver(domain_config)
+ self._assert_no_more_than_one_sql_driver(domain_ref['id'],
+ domain_config,
+ config_file=file_list)
+ self[domain_ref['id']] = domain_config
+
+ def _setup_domain_drivers_from_files(self, standard_driver, resource_api):
+ """Read the domain specific configuration files and load the drivers.
+
+ Domain configuration files are stored in the domain config directory,
+ and must be named of the form:
+
+ keystone.<domain_name>.conf
+
+ For each file, call the load config method where the domain_name
+ will be turned into a domain_id and then:
+
+ - Create a new config structure, adding in the specific additional
+ options defined in this config file
+ - Initialise a new instance of the required driver with this new config
+
+ """
+ conf_dir = CONF.identity.domain_config_dir
+ if not os.path.exists(conf_dir):
+ LOG.warning(_LW('Unable to locate domain config directory: %s'),
+ conf_dir)
+ return
+
+ for r, d, f in os.walk(conf_dir):
+ for fname in f:
+ if (fname.startswith(DOMAIN_CONF_FHEAD) and
+ fname.endswith(DOMAIN_CONF_FTAIL)):
+ if fname.count('.') >= 2:
+ self._load_config_from_file(
+ resource_api, [os.path.join(r, fname)],
+ fname[len(DOMAIN_CONF_FHEAD):
+ -len(DOMAIN_CONF_FTAIL)])
+ else:
+ LOG.debug(('Ignoring file (%s) while scanning domain '
+ 'config directory'),
+ fname)
+
+ def _load_config_from_database(self, domain_id, specific_config):
+ domain_config = {}
+ domain_config['cfg'] = cfg.ConfigOpts()
+ config.configure(conf=domain_config['cfg'])
+ domain_config['cfg'](args=[], project='keystone')
+
+ # Override any options that have been passed in as specified in the
+ # database.
+ for group in specific_config:
+ for option in specific_config[group]:
+ domain_config['cfg'].set_override(
+ option, specific_config[group][option], group)
+
+ domain_config['driver'] = self._load_driver(domain_config)
+ self._assert_no_more_than_one_sql_driver(domain_id, domain_config)
+ self[domain_id] = domain_config
+
+ def _setup_domain_drivers_from_database(self, standard_driver,
+ resource_api):
+ """Read domain specific configuration from database and load drivers.
+
+ Domain configurations are stored in the domain-config backend,
+ so we go through each domain to find those that have a specific config
+ defined, and for those that do we:
+
+ - Create a new config structure, overriding any specific options
+ defined in the resource backend
+ - Initialise a new instance of the required driver with this new config
+
+ """
+ for domain in resource_api.list_domains():
+ domain_config_options = (
+ self.domain_config_api.
+ get_config_with_sensitive_info(domain['id']))
+ if domain_config_options:
+ self._load_config_from_database(domain['id'],
+ domain_config_options)
+
+ def setup_domain_drivers(self, standard_driver, resource_api):
+ # This is called by the api call wrapper
+ self.configured = True
+ self.driver = standard_driver
+
+ if CONF.identity.domain_configurations_from_database:
+ self._setup_domain_drivers_from_database(standard_driver,
+ resource_api)
+ else:
+ self._setup_domain_drivers_from_files(standard_driver,
+ resource_api)
+
+ def get_domain_driver(self, domain_id):
+ if domain_id in self:
+ return self[domain_id]['driver']
+
+ def get_domain_conf(self, domain_id):
+ if domain_id in self:
+ return self[domain_id]['cfg']
+ else:
+ return CONF
+
+ def reload_domain_driver(self, domain_id):
+ # Only used to support unit tests that want to set
+ # new config values. This should only be called once
+ # the domains have been configured, since it relies on
+ # the fact that the configuration files/database have already been
+ # read.
+ if self.configured:
+ if domain_id in self:
+ self[domain_id]['driver'] = (
+ self._load_driver(self[domain_id]))
+ else:
+ # The standard driver
+ self.driver = self.driver()
+
+
+def domains_configured(f):
+ """Wraps API calls to lazy load domain configs after init.
+
+ This is required since the assignment manager needs to be initialized
+ before this manager, and yet this manager's init wants to be
+ able to make assignment calls (to build the domain configs). So
+ instead, we check if the domains have been initialized on entry
+ to each call, and if requires load them,
+
+ """
+ @functools.wraps(f)
+ def wrapper(self, *args, **kwargs):
+ if (not self.domain_configs.configured and
+ CONF.identity.domain_specific_drivers_enabled):
+ self.domain_configs.setup_domain_drivers(
+ self.driver, self.resource_api)
+ return f(self, *args, **kwargs)
+ return wrapper
+
+
+def exception_translated(exception_type):
+ """Wraps API calls to map to correct exception."""
+
+ def _exception_translated(f):
+ @functools.wraps(f)
+ def wrapper(self, *args, **kwargs):
+ try:
+ return f(self, *args, **kwargs)
+ except exception.PublicIDNotFound as e:
+ if exception_type == 'user':
+ raise exception.UserNotFound(user_id=str(e))
+ elif exception_type == 'group':
+ raise exception.GroupNotFound(group_id=str(e))
+ elif exception_type == 'assertion':
+ raise AssertionError(_('Invalid user / password'))
+ else:
+ raise
+ return wrapper
+ return _exception_translated
+
+
+@dependency.provider('identity_api')
+@dependency.requires('assignment_api', 'credential_api', 'id_mapping_api',
+ 'resource_api', 'revoke_api')
+class Manager(manager.Manager):
+ """Default pivot point for the Identity backend.
+
+ See :mod:`keystone.common.manager.Manager` for more details on how this
+ dynamically calls the backend.
+
+ This class also handles the support of domain specific backends, by using
+ the DomainConfigs class. The setup call for DomainConfigs is called
+ from with the @domains_configured wrapper in a lazy loading fashion
+ to get around the fact that we can't satisfy the assignment api it needs
+ from within our __init__() function since the assignment driver is not
+ itself yet initialized.
+
+ Each of the identity calls are pre-processed here to choose, based on
+ domain, which of the drivers should be called. The non-domain-specific
+ driver is still in place, and is used if there is no specific driver for
+ the domain in question (or we are not using multiple domain drivers).
+
+ Starting with Juno, in order to be able to obtain the domain from
+ just an ID being presented as part of an API call, a public ID to domain
+ and local ID mapping is maintained. This mapping also allows for the local
+ ID of drivers that do not provide simple UUIDs (such as LDAP) to be
+ referenced via a public facing ID. The mapping itself is automatically
+ generated as entities are accessed via the driver.
+
+ This mapping is only used when:
+ - the entity is being handled by anything other than the default driver, or
+ - the entity is being handled by the default LDAP driver and backward
+ compatible IDs are not required.
+
+ This means that in the standard case of a single SQL backend or the default
+ settings of a single LDAP backend (since backward compatible IDs is set to
+ True by default), no mapping is used. An alternative approach would be to
+ always use the mapping table, but in the cases where we don't need it to
+ make the public and local IDs the same. It is felt that not using the
+ mapping by default is a more prudent way to introduce this functionality.
+
+ """
+ _USER = 'user'
+ _GROUP = 'group'
+
+ def __init__(self):
+ super(Manager, self).__init__(CONF.identity.driver)
+ self.domain_configs = DomainConfigs()
+
+ self.event_callbacks = {
+ notifications.ACTIONS.deleted: {
+ 'domain': [self._domain_deleted],
+ },
+ }
+
+ def _domain_deleted(self, service, resource_type, operation,
+ payload):
+ domain_id = payload['resource_info']
+
+ user_refs = self.list_users(domain_scope=domain_id)
+ group_refs = self.list_groups(domain_scope=domain_id)
+
+ for group in group_refs:
+ # Cleanup any existing groups.
+ try:
+ self.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:
+ try:
+ self.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})
+
+ # Domain ID normalization methods
+
+ def _set_domain_id_and_mapping(self, ref, domain_id, driver,
+ entity_type):
+ """Patch the domain_id/public_id into the resulting entity(ies).
+
+ :param ref: the entity or list of entities to post process
+ :param domain_id: the domain scope used for the call
+ :param driver: the driver used to execute the call
+ :param entity_type: whether this is a user or group
+
+ :returns: post processed entity or list or entities
+
+ Called to post-process the entity being returned, using a mapping
+ to substitute a public facing ID as necessary. This method must
+ take into account:
+
+ - If the driver is not domain aware, then we must set the domain
+ attribute of all entities irrespective of mapping.
+ - If the driver does not support UUIDs, then we always want to provide
+ a mapping, except for the special case of this being the default
+ driver and backward_compatible_ids is set to True. This is to ensure
+ that entity IDs do not change for an existing LDAP installation (only
+ single domain/driver LDAP configurations were previously supported).
+ - If the driver does support UUIDs, then we always create a mapping
+ entry, but use the local UUID as the public ID. The exception to
+ - this is that if we just have single driver (i.e. not using specific
+ multi-domain configs), then we don't both with the mapping at all.
+
+ """
+ conf = CONF.identity
+
+ if not self._needs_post_processing(driver):
+ # a classic case would be when running with a single SQL driver
+ return ref
+
+ LOG.debug('ID Mapping - Domain ID: %(domain)s, '
+ 'Default Driver: %(driver)s, '
+ 'Domains: %(aware)s, UUIDs: %(generate)s, '
+ 'Compatible IDs: %(compat)s',
+ {'domain': domain_id,
+ 'driver': (driver == self.driver),
+ 'aware': driver.is_domain_aware(),
+ 'generate': driver.generates_uuids(),
+ 'compat': CONF.identity_mapping.backward_compatible_ids})
+
+ if isinstance(ref, dict):
+ return self._set_domain_id_and_mapping_for_single_ref(
+ ref, domain_id, driver, entity_type, conf)
+ elif isinstance(ref, list):
+ return [self._set_domain_id_and_mapping(
+ x, domain_id, driver, entity_type) for x in ref]
+ else:
+ raise ValueError(_('Expected dict or list: %s') % type(ref))
+
+ def _needs_post_processing(self, driver):
+ """Returns whether entity from driver needs domain added or mapping."""
+ return (driver is not self.driver or not driver.generates_uuids() or
+ not driver.is_domain_aware())
+
+ def _set_domain_id_and_mapping_for_single_ref(self, ref, domain_id,
+ driver, entity_type, conf):
+ LOG.debug('Local ID: %s', ref['id'])
+ ref = ref.copy()
+
+ self._insert_domain_id_if_needed(ref, driver, domain_id, conf)
+
+ if self._is_mapping_needed(driver):
+ local_entity = {'domain_id': ref['domain_id'],
+ 'local_id': ref['id'],
+ 'entity_type': entity_type}
+ public_id = self.id_mapping_api.get_public_id(local_entity)
+ if public_id:
+ ref['id'] = public_id
+ LOG.debug('Found existing mapping to public ID: %s',
+ ref['id'])
+ else:
+ # Need to create a mapping. If the driver generates UUIDs
+ # then pass the local UUID in as the public ID to use.
+ if driver.generates_uuids():
+ public_id = ref['id']
+ ref['id'] = self.id_mapping_api.create_id_mapping(
+ local_entity, public_id)
+ LOG.debug('Created new mapping to public ID: %s',
+ ref['id'])
+ return ref
+
+ def _insert_domain_id_if_needed(self, ref, driver, domain_id, conf):
+ """Inserts the domain ID into the ref, if required.
+
+ If the driver can't handle domains, then we need to insert the
+ domain_id into the entity being returned. If the domain_id is
+ None that means we are running in a single backend mode, so to
+ remain backwardly compatible, we put in the default domain ID.
+ """
+ if not driver.is_domain_aware():
+ if domain_id is None:
+ domain_id = conf.default_domain_id
+ ref['domain_id'] = domain_id
+
+ def _is_mapping_needed(self, driver):
+ """Returns whether mapping is needed.
+
+ There are two situations where we must use the mapping:
+ - this isn't the default driver (i.e. multiple backends), or
+ - we have a single backend that doesn't use UUIDs
+ The exception to the above is that we must honor backward
+ compatibility if this is the default driver (e.g. to support
+ current LDAP)
+ """
+ is_not_default_driver = driver is not self.driver
+ return (is_not_default_driver or (
+ not driver.generates_uuids() and
+ not CONF.identity_mapping.backward_compatible_ids))
+
+ def _clear_domain_id_if_domain_unaware(self, driver, ref):
+ """Clear domain_id details if driver is not domain aware."""
+ if not driver.is_domain_aware() and 'domain_id' in ref:
+ ref = ref.copy()
+ ref.pop('domain_id')
+ return ref
+
+ def _select_identity_driver(self, domain_id):
+ """Choose a backend driver for the given domain_id.
+
+ :param domain_id: The domain_id for which we want to find a driver. If
+ the domain_id is specified as None, then this means
+ we need a driver that handles multiple domains.
+
+ :returns: chosen backend driver
+
+ If there is a specific driver defined for this domain then choose it.
+ If the domain is None, or there no specific backend for the given
+ domain is found, then we chose the default driver.
+
+ """
+ if domain_id is None:
+ driver = self.driver
+ else:
+ driver = (self.domain_configs.get_domain_driver(domain_id) or
+ self.driver)
+
+ # If the driver is not domain aware (e.g. LDAP) then check to
+ # ensure we are not mapping multiple domains onto it - the only way
+ # that would happen is that the default driver is LDAP and the
+ # domain is anything other than None or the default domain.
+ if (not driver.is_domain_aware() and driver == self.driver and
+ domain_id != CONF.identity.default_domain_id and
+ domain_id is not None):
+ LOG.warning('Found multiple domains being mapped to a '
+ 'driver that does not support that (e.g. '
+ 'LDAP) - Domain ID: %(domain)s, '
+ 'Default Driver: %(driver)s',
+ {'domain': domain_id,
+ 'driver': (driver == self.driver)})
+ raise exception.DomainNotFound(domain_id=domain_id)
+ return driver
+
+ def _get_domain_driver_and_entity_id(self, public_id):
+ """Look up details using the public ID.
+
+ :param public_id: the ID provided in the call
+
+ :returns: domain_id, which can be None to indicate that the driver
+ in question supports multiple domains
+ driver selected based on this domain
+ entity_id which will is understood by the driver.
+
+ Use the mapping table to look up the domain, driver and local entity
+ that is represented by the provided public ID. Handle the situations
+ were we do not use the mapping (e.g. single driver that understands
+ UUIDs etc.)
+
+ """
+ conf = CONF.identity
+ # First, since we don't know anything about the entity yet, we must
+ # assume it needs mapping, so long as we are using domain specific
+ # drivers.
+ if conf.domain_specific_drivers_enabled:
+ local_id_ref = self.id_mapping_api.get_id_mapping(public_id)
+ if local_id_ref:
+ return (
+ local_id_ref['domain_id'],
+ self._select_identity_driver(local_id_ref['domain_id']),
+ local_id_ref['local_id'])
+
+ # So either we are using multiple drivers but the public ID is invalid
+ # (and hence was not found in the mapping table), or the public ID is
+ # being handled by the default driver. Either way, the only place left
+ # to look is in that standard driver. However, we don't yet know if
+ # this driver also needs mapping (e.g. LDAP in non backward
+ # compatibility mode).
+ driver = self.driver
+ if driver.generates_uuids():
+ if driver.is_domain_aware:
+ # No mapping required, and the driver can handle the domain
+ # information itself. The classic case of this is the
+ # current SQL driver.
+ return (None, driver, public_id)
+ else:
+ # Although we don't have any drivers of this type, i.e. that
+ # understand UUIDs but not domains, conceptually you could.
+ return (conf.default_domain_id, driver, public_id)
+
+ # So the only place left to find the ID is in the default driver which
+ # we now know doesn't generate UUIDs
+ if not CONF.identity_mapping.backward_compatible_ids:
+ # We are not running in backward compatibility mode, so we
+ # must use a mapping.
+ local_id_ref = self.id_mapping_api.get_id_mapping(public_id)
+ if local_id_ref:
+ return (
+ local_id_ref['domain_id'],
+ driver,
+ local_id_ref['local_id'])
+ else:
+ raise exception.PublicIDNotFound(id=public_id)
+
+ # If we reach here, this means that the default driver
+ # requires no mapping - but also doesn't understand domains
+ # (e.g. the classic single LDAP driver situation). Hence we pass
+ # back the public_ID unmodified and use the default domain (to
+ # keep backwards compatibility with existing installations).
+ #
+ # It is still possible that the public ID is just invalid in
+ # which case we leave this to the caller to check.
+ return (conf.default_domain_id, driver, public_id)
+
+ def _assert_user_and_group_in_same_backend(
+ self, user_entity_id, user_driver, group_entity_id, group_driver):
+ """Ensures that user and group IDs are backed by the same backend.
+
+ Raise a CrossBackendNotAllowed exception if they are not from the same
+ backend, otherwise return None.
+
+ """
+ if user_driver is not group_driver:
+ # Determine first if either IDs don't exist by calling
+ # the driver.get methods (which will raise a NotFound
+ # exception).
+ user_driver.get_user(user_entity_id)
+ group_driver.get_group(group_entity_id)
+ # If we get here, then someone is attempting to create a cross
+ # backend membership, which is not allowed.
+ raise exception.CrossBackendNotAllowed(group_id=group_entity_id,
+ user_id=user_entity_id)
+
+ def _mark_domain_id_filter_satisfied(self, hints):
+ if hints:
+ for filter in hints.filters:
+ if (filter['name'] == 'domain_id' and
+ filter['comparator'] == 'equals'):
+ hints.filters.remove(filter)
+
+ def _ensure_domain_id_in_hints(self, hints, domain_id):
+ if (domain_id is not None and
+ not hints.get_exact_filter_by_name('domain_id')):
+ hints.add_filter('domain_id', domain_id)
+
+ # The actual driver calls - these are pre/post processed here as
+ # part of the Manager layer to make sure we:
+ #
+ # - select the right driver for this domain
+ # - clear/set domain_ids for drivers that do not support domains
+ # - create any ID mapping that might be required
+
+ @notifications.emit_event('authenticate')
+ @domains_configured
+ @exception_translated('assertion')
+ def authenticate(self, context, user_id, password):
+ domain_id, driver, entity_id = (
+ self._get_domain_driver_and_entity_id(user_id))
+ ref = driver.authenticate(entity_id, password)
+ return self._set_domain_id_and_mapping(
+ ref, domain_id, driver, mapping.EntityType.USER)
+
+ @domains_configured
+ @exception_translated('user')
+ def create_user(self, user_ref, initiator=None):
+ user = user_ref.copy()
+ user['name'] = clean.user_name(user['name'])
+ user.setdefault('enabled', True)
+ user['enabled'] = clean.user_enabled(user['enabled'])
+ domain_id = user['domain_id']
+ self.resource_api.get_domain(domain_id)
+
+ # For creating a user, the domain is in the object itself
+ domain_id = user_ref['domain_id']
+ driver = self._select_identity_driver(domain_id)
+ user = self._clear_domain_id_if_domain_unaware(driver, user)
+ # Generate a local ID - in the future this might become a function of
+ # the underlying driver so that it could conform to rules set down by
+ # that particular driver type.
+ user['id'] = uuid.uuid4().hex
+ ref = driver.create_user(user['id'], user)
+ notifications.Audit.created(self._USER, user['id'], initiator)
+ return self._set_domain_id_and_mapping(
+ ref, domain_id, driver, mapping.EntityType.USER)
+
+ @domains_configured
+ @exception_translated('user')
+ @MEMOIZE
+ def get_user(self, user_id):
+ domain_id, driver, entity_id = (
+ self._get_domain_driver_and_entity_id(user_id))
+ ref = driver.get_user(entity_id)
+ return self._set_domain_id_and_mapping(
+ ref, domain_id, driver, mapping.EntityType.USER)
+
+ def assert_user_enabled(self, user_id, user=None):
+ """Assert the user and the user's domain are enabled.
+
+ :raise AssertionError if the user or the user's domain is disabled.
+ """
+ if user is None:
+ user = self.get_user(user_id)
+ self.resource_api.assert_domain_enabled(user['domain_id'])
+ if not user.get('enabled', True):
+ raise AssertionError(_('User is disabled: %s') % user_id)
+
+ @domains_configured
+ @exception_translated('user')
+ @MEMOIZE
+ def get_user_by_name(self, user_name, domain_id):
+ driver = self._select_identity_driver(domain_id)
+ ref = driver.get_user_by_name(user_name, domain_id)
+ return self._set_domain_id_and_mapping(
+ ref, domain_id, driver, mapping.EntityType.USER)
+
+ @manager.response_truncated
+ @domains_configured
+ @exception_translated('user')
+ def list_users(self, domain_scope=None, hints=None):
+ driver = self._select_identity_driver(domain_scope)
+ hints = hints or driver_hints.Hints()
+ if driver.is_domain_aware():
+ # Force the domain_scope into the hint to ensure that we only get
+ # back domains for that scope.
+ self._ensure_domain_id_in_hints(hints, domain_scope)
+ else:
+ # We are effectively satisfying any domain_id filter by the above
+ # driver selection, so remove any such filter.
+ self._mark_domain_id_filter_satisfied(hints)
+ ref_list = driver.list_users(hints)
+ return self._set_domain_id_and_mapping(
+ ref_list, domain_scope, driver, mapping.EntityType.USER)
+
+ @domains_configured
+ @exception_translated('user')
+ def update_user(self, user_id, user_ref, initiator=None):
+ old_user_ref = self.get_user(user_id)
+ user = user_ref.copy()
+ if 'name' in user:
+ user['name'] = clean.user_name(user['name'])
+ if 'enabled' in user:
+ user['enabled'] = clean.user_enabled(user['enabled'])
+ if 'domain_id' in user:
+ self.resource_api.get_domain(user['domain_id'])
+ if 'id' in user:
+ if user_id != user['id']:
+ raise exception.ValidationError(_('Cannot change user ID'))
+ # Since any ID in the user dict is now irrelevant, remove its so as
+ # the driver layer won't be confused by the fact the this is the
+ # public ID not the local ID
+ user.pop('id')
+
+ domain_id, driver, entity_id = (
+ self._get_domain_driver_and_entity_id(user_id))
+ user = self._clear_domain_id_if_domain_unaware(driver, user)
+ self.get_user.invalidate(self, old_user_ref['id'])
+ self.get_user_by_name.invalidate(self, old_user_ref['name'],
+ old_user_ref['domain_id'])
+
+ ref = driver.update_user(entity_id, user)
+
+ notifications.Audit.updated(self._USER, user_id, initiator)
+
+ enabled_change = ((user.get('enabled') is False) and
+ user['enabled'] != old_user_ref.get('enabled'))
+ if enabled_change or user.get('password') is not None:
+ self.emit_invalidate_user_token_persistence(user_id)
+
+ return self._set_domain_id_and_mapping(
+ ref, domain_id, driver, mapping.EntityType.USER)
+
+ @domains_configured
+ @exception_translated('user')
+ def delete_user(self, user_id, initiator=None):
+ domain_id, driver, entity_id = (
+ self._get_domain_driver_and_entity_id(user_id))
+ # Get user details to invalidate the cache.
+ user_old = self.get_user(user_id)
+ driver.delete_user(entity_id)
+ self.assignment_api.delete_user(user_id)
+ self.get_user.invalidate(self, user_id)
+ self.get_user_by_name.invalidate(self, user_old['name'],
+ user_old['domain_id'])
+ self.credential_api.delete_credentials_for_user(user_id)
+ self.id_mapping_api.delete_id_mapping(user_id)
+ notifications.Audit.deleted(self._USER, user_id, initiator)
+
+ @domains_configured
+ @exception_translated('group')
+ def create_group(self, group_ref, initiator=None):
+ group = group_ref.copy()
+ group.setdefault('description', '')
+ domain_id = group['domain_id']
+ self.resource_api.get_domain(domain_id)
+
+ # For creating a group, the domain is in the object itself
+ domain_id = group_ref['domain_id']
+ driver = self._select_identity_driver(domain_id)
+ group = self._clear_domain_id_if_domain_unaware(driver, group)
+ # Generate a local ID - in the future this might become a function of
+ # the underlying driver so that it could conform to rules set down by
+ # that particular driver type.
+ group['id'] = uuid.uuid4().hex
+ ref = driver.create_group(group['id'], group)
+
+ notifications.Audit.created(self._GROUP, group['id'], initiator)
+
+ return self._set_domain_id_and_mapping(
+ ref, domain_id, driver, mapping.EntityType.GROUP)
+
+ @domains_configured
+ @exception_translated('group')
+ @MEMOIZE
+ def get_group(self, group_id):
+ domain_id, driver, entity_id = (
+ self._get_domain_driver_and_entity_id(group_id))
+ ref = driver.get_group(entity_id)
+ return self._set_domain_id_and_mapping(
+ ref, domain_id, driver, mapping.EntityType.GROUP)
+
+ @domains_configured
+ @exception_translated('group')
+ def get_group_by_name(self, group_name, domain_id):
+ driver = self._select_identity_driver(domain_id)
+ ref = driver.get_group_by_name(group_name, domain_id)
+ return self._set_domain_id_and_mapping(
+ ref, domain_id, driver, mapping.EntityType.GROUP)
+
+ @domains_configured
+ @exception_translated('group')
+ def update_group(self, group_id, group, initiator=None):
+ if 'domain_id' in group:
+ self.resource_api.get_domain(group['domain_id'])
+ domain_id, driver, entity_id = (
+ self._get_domain_driver_and_entity_id(group_id))
+ group = self._clear_domain_id_if_domain_unaware(driver, group)
+ ref = driver.update_group(entity_id, group)
+ self.get_group.invalidate(self, group_id)
+ notifications.Audit.updated(self._GROUP, group_id, initiator)
+ return self._set_domain_id_and_mapping(
+ ref, domain_id, driver, mapping.EntityType.GROUP)
+
+ @domains_configured
+ @exception_translated('group')
+ def delete_group(self, group_id, initiator=None):
+ domain_id, driver, entity_id = (
+ self._get_domain_driver_and_entity_id(group_id))
+ user_ids = (u['id'] for u in self.list_users_in_group(group_id))
+ driver.delete_group(entity_id)
+ self.get_group.invalidate(self, group_id)
+ self.id_mapping_api.delete_id_mapping(group_id)
+ self.assignment_api.delete_group(group_id)
+
+ notifications.Audit.deleted(self._GROUP, group_id, initiator)
+
+ for uid in user_ids:
+ self.emit_invalidate_user_token_persistence(uid)
+
+ @domains_configured
+ @exception_translated('group')
+ def add_user_to_group(self, user_id, group_id):
+ @exception_translated('user')
+ def get_entity_info_for_user(public_id):
+ return self._get_domain_driver_and_entity_id(public_id)
+
+ _domain_id, group_driver, group_entity_id = (
+ self._get_domain_driver_and_entity_id(group_id))
+ # Get the same info for the user_id, taking care to map any
+ # exceptions correctly
+ _domain_id, user_driver, user_entity_id = (
+ get_entity_info_for_user(user_id))
+
+ self._assert_user_and_group_in_same_backend(
+ user_entity_id, user_driver, group_entity_id, group_driver)
+
+ group_driver.add_user_to_group(user_entity_id, group_entity_id)
+
+ @domains_configured
+ @exception_translated('group')
+ def remove_user_from_group(self, user_id, group_id):
+ @exception_translated('user')
+ def get_entity_info_for_user(public_id):
+ return self._get_domain_driver_and_entity_id(public_id)
+
+ _domain_id, group_driver, group_entity_id = (
+ self._get_domain_driver_and_entity_id(group_id))
+ # Get the same info for the user_id, taking care to map any
+ # exceptions correctly
+ _domain_id, user_driver, user_entity_id = (
+ get_entity_info_for_user(user_id))
+
+ self._assert_user_and_group_in_same_backend(
+ user_entity_id, user_driver, group_entity_id, group_driver)
+
+ group_driver.remove_user_from_group(user_entity_id, group_entity_id)
+ self.emit_invalidate_user_token_persistence(user_id)
+
+ @notifications.internal(notifications.INVALIDATE_USER_TOKEN_PERSISTENCE)
+ def emit_invalidate_user_token_persistence(self, user_id):
+ """Emit a notification to the callback system to revoke user tokens.
+
+ This method and associated callback listener removes the need for
+ making a direct call to another manager to delete and revoke tokens.
+
+ :param user_id: user identifier
+ :type user_id: string
+ """
+ pass
+
+ @manager.response_truncated
+ @domains_configured
+ @exception_translated('user')
+ def list_groups_for_user(self, user_id, hints=None):
+ domain_id, driver, entity_id = (
+ self._get_domain_driver_and_entity_id(user_id))
+ hints = hints or driver_hints.Hints()
+ if not driver.is_domain_aware():
+ # We are effectively satisfying any domain_id filter by the above
+ # driver selection, so remove any such filter
+ self._mark_domain_id_filter_satisfied(hints)
+ ref_list = driver.list_groups_for_user(entity_id, hints)
+ return self._set_domain_id_and_mapping(
+ ref_list, domain_id, driver, mapping.EntityType.GROUP)
+
+ @manager.response_truncated
+ @domains_configured
+ @exception_translated('group')
+ def list_groups(self, domain_scope=None, hints=None):
+ driver = self._select_identity_driver(domain_scope)
+ hints = hints or driver_hints.Hints()
+ if driver.is_domain_aware():
+ # Force the domain_scope into the hint to ensure that we only get
+ # back domains for that scope.
+ self._ensure_domain_id_in_hints(hints, domain_scope)
+ else:
+ # We are effectively satisfying any domain_id filter by the above
+ # driver selection, so remove any such filter.
+ self._mark_domain_id_filter_satisfied(hints)
+ ref_list = driver.list_groups(hints)
+ return self._set_domain_id_and_mapping(
+ ref_list, domain_scope, driver, mapping.EntityType.GROUP)
+
+ @manager.response_truncated
+ @domains_configured
+ @exception_translated('group')
+ def list_users_in_group(self, group_id, hints=None):
+ domain_id, driver, entity_id = (
+ self._get_domain_driver_and_entity_id(group_id))
+ hints = hints or driver_hints.Hints()
+ if not driver.is_domain_aware():
+ # We are effectively satisfying any domain_id filter by the above
+ # driver selection, so remove any such filter
+ self._mark_domain_id_filter_satisfied(hints)
+ ref_list = driver.list_users_in_group(entity_id, hints)
+ return self._set_domain_id_and_mapping(
+ ref_list, domain_id, driver, mapping.EntityType.USER)
+
+ @domains_configured
+ @exception_translated('group')
+ def check_user_in_group(self, user_id, group_id):
+ @exception_translated('user')
+ def get_entity_info_for_user(public_id):
+ return self._get_domain_driver_and_entity_id(public_id)
+
+ _domain_id, group_driver, group_entity_id = (
+ self._get_domain_driver_and_entity_id(group_id))
+ # Get the same info for the user_id, taking care to map any
+ # exceptions correctly
+ _domain_id, user_driver, user_entity_id = (
+ get_entity_info_for_user(user_id))
+
+ self._assert_user_and_group_in_same_backend(
+ user_entity_id, user_driver, group_entity_id, group_driver)
+
+ return group_driver.check_user_in_group(user_entity_id,
+ group_entity_id)
+
+ @domains_configured
+ def change_password(self, context, user_id, original_password,
+ new_password):
+
+ # authenticate() will raise an AssertionError if authentication fails
+ self.authenticate(context, user_id, original_password)
+
+ update_dict = {'password': new_password}
+ self.update_user(user_id, update_dict)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class Driver(object):
+ """Interface description for an Identity driver."""
+
+ def _get_list_limit(self):
+ return CONF.identity.list_limit or CONF.list_limit
+
+ def is_domain_aware(self):
+ """Indicates if Driver supports domains."""
+ return True
+
+ @property
+ def is_sql(self):
+ """Indicates if this Driver uses SQL."""
+ return False
+
+ @property
+ def multiple_domains_supported(self):
+ return (self.is_domain_aware() or
+ CONF.identity.domain_specific_drivers_enabled)
+
+ def generates_uuids(self):
+ """Indicates if Driver generates UUIDs as the local entity ID."""
+ return True
+
+ @abc.abstractmethod
+ def authenticate(self, user_id, password):
+ """Authenticate a given user and password.
+ :returns: user_ref
+ :raises: AssertionError
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ # user crud
+
+ @abc.abstractmethod
+ def create_user(self, user_id, user):
+ """Creates a new user.
+
+ :raises: keystone.exception.Conflict
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_users(self, hints):
+ """List users in the system.
+
+ :param hints: filter hints which the driver should
+ implement if at all possible.
+
+ :returns: a list of user_refs or an empty list.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_users_in_group(self, group_id, hints):
+ """List users in a group.
+
+ :param group_id: the group in question
+ :param hints: filter hints which the driver should
+ implement if at all possible.
+
+ :returns: a list of user_refs or an empty list.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def get_user(self, user_id):
+ """Get a user by ID.
+
+ :returns: user_ref
+ :raises: keystone.exception.UserNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def update_user(self, user_id, user):
+ """Updates an existing user.
+
+ :raises: keystone.exception.UserNotFound,
+ keystone.exception.Conflict
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def add_user_to_group(self, user_id, group_id):
+ """Adds a user to a group.
+
+ :raises: keystone.exception.UserNotFound,
+ keystone.exception.GroupNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def check_user_in_group(self, user_id, group_id):
+ """Checks if a user is a member of a group.
+
+ :raises: keystone.exception.UserNotFound,
+ keystone.exception.GroupNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def remove_user_from_group(self, user_id, group_id):
+ """Removes a user from a group.
+
+ :raises: keystone.exception.NotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def delete_user(self, user_id):
+ """Deletes an existing user.
+
+ :raises: keystone.exception.UserNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def get_user_by_name(self, user_name, domain_id):
+ """Get a user by name.
+
+ :returns: user_ref
+ :raises: keystone.exception.UserNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ # group crud
+
+ @abc.abstractmethod
+ def create_group(self, group_id, group):
+ """Creates a new group.
+
+ :raises: keystone.exception.Conflict
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_groups(self, hints):
+ """List groups in the system.
+
+ :param hints: filter hints which the driver should
+ implement if at all possible.
+
+ :returns: a list of group_refs or an empty list.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_groups_for_user(self, user_id, hints):
+ """List groups a user is in
+
+ :param user_id: the user in question
+ :param hints: filter hints which the driver should
+ implement if at all possible.
+
+ :returns: a list of group_refs or an empty list.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def get_group(self, group_id):
+ """Get a group by ID.
+
+ :returns: group_ref
+ :raises: keystone.exception.GroupNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def get_group_by_name(self, group_name, domain_id):
+ """Get a group by name.
+
+ :returns: group_ref
+ :raises: keystone.exception.GroupNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def update_group(self, group_id, group):
+ """Updates an existing group.
+
+ :raises: keystone.exceptionGroupNotFound,
+ keystone.exception.Conflict
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def delete_group(self, group_id):
+ """Deletes an existing group.
+
+ :raises: keystone.exception.GroupNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ # end of identity
+
+
+@dependency.provider('id_mapping_api')
+class MappingManager(manager.Manager):
+ """Default pivot point for the ID Mapping backend."""
+
+ def __init__(self):
+ super(MappingManager, self).__init__(CONF.identity_mapping.driver)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class MappingDriver(object):
+ """Interface description for an ID Mapping driver."""
+
+ @abc.abstractmethod
+ def get_public_id(self, local_entity):
+ """Returns the public ID for the given local entity.
+
+ :param dict local_entity: Containing the entity domain, local ID and
+ type ('user' or 'group').
+ :returns: public ID, or None if no mapping is found.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def get_id_mapping(self, public_id):
+ """Returns the local mapping.
+
+ :param public_id: The public ID for the mapping required.
+ :returns dict: Containing the entity domain, local ID and type. If no
+ mapping is found, it returns None.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def create_id_mapping(self, local_entity, public_id=None):
+ """Create and store a mapping to a public_id.
+
+ :param dict local_entity: Containing the entity domain, local ID and
+ type ('user' or 'group').
+ :param public_id: If specified, this will be the public ID. If this
+ is not specified, a public ID will be generated.
+ :returns: public ID
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def delete_id_mapping(self, public_id):
+ """Deletes an entry for the given public_id.
+
+ :param public_id: The public ID for the mapping to be deleted.
+
+ The method is silent if no mapping is found.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def purge_mappings(self, purge_filter):
+ """Purge selected identity mappings.
+
+ :param dict purge_filter: Containing the attributes of the filter that
+ defines which entries to purge. An empty
+ filter means purge all mappings.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
diff --git a/keystone-moon/keystone/identity/generator.py b/keystone-moon/keystone/identity/generator.py
new file mode 100644
index 00000000..d25426ce
--- /dev/null
+++ b/keystone-moon/keystone/identity/generator.py
@@ -0,0 +1,52 @@
+# Copyright 2014 IBM Corp.
+#
+# 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.
+
+"""ID Generator provider interface."""
+
+import abc
+
+from oslo_config import cfg
+import six
+
+from keystone.common import dependency
+from keystone.common import manager
+from keystone import exception
+
+CONF = cfg.CONF
+
+
+@dependency.provider('id_generator_api')
+class Manager(manager.Manager):
+ """Default pivot point for the identifier generator backend."""
+
+ def __init__(self):
+ super(Manager, self).__init__(CONF.identity_mapping.generator)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class IDGenerator(object):
+ """Interface description for an ID Generator provider."""
+
+ @abc.abstractmethod
+ def generate_public_ID(self, mapping):
+ """Return a Public ID for the given mapping dict.
+
+ :param dict mapping: The items to be hashed.
+
+ The ID must be reproducible and no more than 64 chars in length.
+ The ID generated should be independent of the order of the items
+ in the mapping dict.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
diff --git a/keystone-moon/keystone/identity/id_generators/__init__.py b/keystone-moon/keystone/identity/id_generators/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone-moon/keystone/identity/id_generators/__init__.py
diff --git a/keystone-moon/keystone/identity/id_generators/sha256.py b/keystone-moon/keystone/identity/id_generators/sha256.py
new file mode 100644
index 00000000..e3a8b416
--- /dev/null
+++ b/keystone-moon/keystone/identity/id_generators/sha256.py
@@ -0,0 +1,28 @@
+# Copyright 2014 IBM Corp.
+#
+# 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.
+
+import hashlib
+
+import six
+
+from keystone.identity import generator
+
+
+class Generator(generator.IDGenerator):
+
+ def generate_public_ID(self, mapping):
+ m = hashlib.sha256()
+ for key in sorted(six.iterkeys(mapping)):
+ m.update(mapping[key].encode('utf-8'))
+ return m.hexdigest()
diff --git a/keystone-moon/keystone/identity/mapping_backends/__init__.py b/keystone-moon/keystone/identity/mapping_backends/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone-moon/keystone/identity/mapping_backends/__init__.py
diff --git a/keystone-moon/keystone/identity/mapping_backends/mapping.py b/keystone-moon/keystone/identity/mapping_backends/mapping.py
new file mode 100644
index 00000000..dddf36c1
--- /dev/null
+++ b/keystone-moon/keystone/identity/mapping_backends/mapping.py
@@ -0,0 +1,18 @@
+# Copyright 2014 IBM Corp.
+#
+# 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.
+
+
+class EntityType(object):
+ USER = 'user'
+ GROUP = 'group'
diff --git a/keystone-moon/keystone/identity/mapping_backends/sql.py b/keystone-moon/keystone/identity/mapping_backends/sql.py
new file mode 100644
index 00000000..b2f9cb95
--- /dev/null
+++ b/keystone-moon/keystone/identity/mapping_backends/sql.py
@@ -0,0 +1,97 @@
+# Copyright 2014 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from keystone.common import dependency
+from keystone.common import sql
+from keystone import identity
+from keystone.identity.mapping_backends import mapping as identity_mapping
+
+
+class IDMapping(sql.ModelBase, sql.ModelDictMixin):
+ __tablename__ = 'id_mapping'
+ public_id = sql.Column(sql.String(64), primary_key=True)
+ domain_id = sql.Column(sql.String(64), nullable=False)
+ local_id = sql.Column(sql.String(64), nullable=False)
+ # NOTE(henry-nash); Postgres requires a name to be defined for an Enum
+ entity_type = sql.Column(
+ sql.Enum(identity_mapping.EntityType.USER,
+ identity_mapping.EntityType.GROUP,
+ name='entity_type'),
+ nullable=False)
+ # Unique constraint to ensure you can't store more than one mapping to the
+ # same underlying values
+ __table_args__ = (
+ sql.UniqueConstraint('domain_id', 'local_id', 'entity_type'), {})
+
+
+@dependency.requires('id_generator_api')
+class Mapping(identity.MappingDriver):
+
+ def get_public_id(self, local_entity):
+ # NOTE(henry-nash): Since the Public ID is regeneratable, rather
+ # than search for the entry using the local entity values, we
+ # could create the hash and do a PK lookup. However this would only
+ # work if we hashed all the entries, even those that already generate
+ # UUIDs, like SQL. Further, this would only work if the generation
+ # algorithm was immutable (e.g. it had always been sha256).
+ session = sql.get_session()
+ query = session.query(IDMapping.public_id)
+ query = query.filter_by(domain_id=local_entity['domain_id'])
+ query = query.filter_by(local_id=local_entity['local_id'])
+ query = query.filter_by(entity_type=local_entity['entity_type'])
+ try:
+ public_ref = query.one()
+ public_id = public_ref.public_id
+ return public_id
+ except sql.NotFound:
+ return None
+
+ def get_id_mapping(self, public_id):
+ session = sql.get_session()
+ mapping_ref = session.query(IDMapping).get(public_id)
+ if mapping_ref:
+ return mapping_ref.to_dict()
+
+ def create_id_mapping(self, local_entity, public_id=None):
+ entity = local_entity.copy()
+ with sql.transaction() as session:
+ if public_id is None:
+ public_id = self.id_generator_api.generate_public_ID(entity)
+ entity['public_id'] = public_id
+ mapping_ref = IDMapping.from_dict(entity)
+ session.add(mapping_ref)
+ return public_id
+
+ def delete_id_mapping(self, public_id):
+ with sql.transaction() as session:
+ try:
+ session.query(IDMapping).filter(
+ IDMapping.public_id == public_id).delete()
+ except sql.NotFound:
+ # NOTE(morganfainberg): There is nothing to delete and nothing
+ # to do.
+ pass
+
+ def purge_mappings(self, purge_filter):
+ session = sql.get_session()
+ query = session.query(IDMapping)
+ if 'domain_id' in purge_filter:
+ query = query.filter_by(domain_id=purge_filter['domain_id'])
+ if 'public_id' in purge_filter:
+ query = query.filter_by(public_id=purge_filter['public_id'])
+ if 'local_id' in purge_filter:
+ query = query.filter_by(local_id=purge_filter['local_id'])
+ if 'entity_type' in purge_filter:
+ query = query.filter_by(entity_type=purge_filter['entity_type'])
+ query.delete()
diff --git a/keystone-moon/keystone/identity/routers.py b/keystone-moon/keystone/identity/routers.py
new file mode 100644
index 00000000..e274d6f4
--- /dev/null
+++ b/keystone-moon/keystone/identity/routers.py
@@ -0,0 +1,84 @@
+# Copyright 2012 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""WSGI Routers for the Identity service."""
+
+from keystone.common import json_home
+from keystone.common import router
+from keystone.common import wsgi
+from keystone.identity import controllers
+
+
+class Admin(wsgi.ComposableRouter):
+ def add_routes(self, mapper):
+ # User Operations
+ user_controller = controllers.User()
+ mapper.connect('/users/{user_id}',
+ controller=user_controller,
+ action='get_user',
+ conditions=dict(method=['GET']))
+
+
+class Routers(wsgi.RoutersBase):
+
+ def append_v3_routers(self, mapper, routers):
+ user_controller = controllers.UserV3()
+ routers.append(
+ router.Router(user_controller,
+ 'users', 'user',
+ resource_descriptions=self.v3_resources))
+
+ self._add_resource(
+ mapper, user_controller,
+ path='/users/{user_id}/password',
+ post_action='change_password',
+ rel=json_home.build_v3_resource_relation('user_change_password'),
+ path_vars={
+ 'user_id': json_home.Parameters.USER_ID,
+ })
+
+ self._add_resource(
+ mapper, user_controller,
+ path='/groups/{group_id}/users',
+ get_action='list_users_in_group',
+ rel=json_home.build_v3_resource_relation('group_users'),
+ path_vars={
+ 'group_id': json_home.Parameters.GROUP_ID,
+ })
+
+ self._add_resource(
+ mapper, user_controller,
+ path='/groups/{group_id}/users/{user_id}',
+ put_action='add_user_to_group',
+ get_head_action='check_user_in_group',
+ delete_action='remove_user_from_group',
+ rel=json_home.build_v3_resource_relation('group_user'),
+ path_vars={
+ 'group_id': json_home.Parameters.GROUP_ID,
+ 'user_id': json_home.Parameters.USER_ID,
+ })
+
+ group_controller = controllers.GroupV3()
+ routers.append(
+ router.Router(group_controller,
+ 'groups', 'group',
+ resource_descriptions=self.v3_resources))
+
+ self._add_resource(
+ mapper, group_controller,
+ path='/users/{user_id}/groups',
+ get_action='list_groups_for_user',
+ rel=json_home.build_v3_resource_relation('user_groups'),
+ path_vars={
+ 'user_id': json_home.Parameters.USER_ID,
+ })