From b8c756ecdd7cced1db4300935484e8c83701c82e Mon Sep 17 00:00:00 2001 From: WuKong Date: Tue, 30 Jun 2015 18:47:29 +0200 Subject: migrate moon code from github to opnfv Change-Id: Ice53e368fd1114d56a75271aa9f2e598e3eba604 Signed-off-by: WuKong --- keystone-moon/keystone/identity/__init__.py | 18 + .../keystone/identity/backends/__init__.py | 0 keystone-moon/keystone/identity/backends/ldap.py | 402 +++++++ keystone-moon/keystone/identity/backends/sql.py | 314 +++++ keystone-moon/keystone/identity/controllers.py | 335 ++++++ keystone-moon/keystone/identity/core.py | 1259 ++++++++++++++++++++ keystone-moon/keystone/identity/generator.py | 52 + .../keystone/identity/id_generators/__init__.py | 0 .../keystone/identity/id_generators/sha256.py | 28 + .../keystone/identity/mapping_backends/__init__.py | 0 .../keystone/identity/mapping_backends/mapping.py | 18 + .../keystone/identity/mapping_backends/sql.py | 97 ++ keystone-moon/keystone/identity/routers.py | 84 ++ 13 files changed, 2607 insertions(+) create mode 100644 keystone-moon/keystone/identity/__init__.py create mode 100644 keystone-moon/keystone/identity/backends/__init__.py create mode 100644 keystone-moon/keystone/identity/backends/ldap.py create mode 100644 keystone-moon/keystone/identity/backends/sql.py create mode 100644 keystone-moon/keystone/identity/controllers.py create mode 100644 keystone-moon/keystone/identity/core.py create mode 100644 keystone-moon/keystone/identity/generator.py create mode 100644 keystone-moon/keystone/identity/id_generators/__init__.py create mode 100644 keystone-moon/keystone/identity/id_generators/sha256.py create mode 100644 keystone-moon/keystone/identity/mapping_backends/__init__.py create mode 100644 keystone-moon/keystone/identity/mapping_backends/mapping.py create mode 100644 keystone-moon/keystone/identity/mapping_backends/sql.py create mode 100644 keystone-moon/keystone/identity/routers.py (limited to 'keystone-moon/keystone/identity') 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 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..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 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 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, + }) -- cgit 1.2.3-korg