keystone/keystone/identity/backends/ldap.py

358 lines
13 KiB
Python

# 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
from keystone import clean
from keystone.common import dependency
from keystone.common import driver_hints
from keystone.common import ldap as common_ldap
from keystone.common import models
from keystone.common import utils
from keystone import config
from keystone import exception
from keystone import identity
from keystone.openstack.common.gettextutils import _
from keystone.openstack.common import log
CONF = config.CONF
LOG = log.getLogger(__name__)
@dependency.requires('assignment_api')
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
# 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(self.user._id_to_dn(user_id),
password)
if not conn:
raise AssertionError(_('Invalid user / password'))
except Exception:
raise AssertionError(_('Invalid user / password'))
finally:
if conn:
conn.unbind_s()
return identity.filter_user(user_ref)
def _get_user(self, user_id):
return self.user.get(user_id)
def get_user(self, user_id):
return identity.filter_user(self._get_user(user_id))
def list_users(self, hints):
return self.user.get_all_filtered()
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 identity.filter_user(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 identity.filter_user(user_ref)
def update_user(self, user_id, user):
self.user.check_allow_update()
if 'id' in user and user['id'] != user_id:
raise exception.ValidationError(_('Cannot change user ID'))
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'))
user = utils.hash_ldap_user_password(user)
if self.user.enabled_mask:
self.user.mask_enabled_attribute(user)
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()
self.assignment_api.delete_user(user_id)
user_dn = self.user._id_to_dn(user_id)
groups = self.group.list_user_groups(user_dn)
for group in groups:
self.group.remove_user(user_dn, group['id'], user_id)
user = self.user.get(user_id)
if hasattr(user, 'tenant_id'):
self.project.remove_user(user.tenant_id,
self.user._id_to_dn(user_id))
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 self.group.create(group)
def get_group(self, group_id):
return self.group.get(group_id)
def update_group(self, group_id, group):
self.group.check_allow_update()
if 'name' in group:
group['name'] = clean.group_name(group['name'])
return 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):
self.get_user(user_id)
self.get_group(group_id)
user_dn = self.user._id_to_dn(user_id)
self.group.add_user(user_dn, group_id, user_id)
def remove_user_from_group(self, user_id, group_id):
self.get_user(user_id)
self.get_group(group_id)
user_dn = self.user._id_to_dn(user_id)
self.group.remove_user(user_dn, group_id, user_id)
def list_groups_for_user(self, user_id, hints):
self.get_user(user_id)
user_dn = self.user._id_to_dn(user_id)
return self.group.list_user_groups(user_dn)
def list_groups(self, hints):
return self.group.get_all()
def list_users_in_group(self, group_id, hints):
self.get_group(group_id)
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):
self.get_user(user_id)
self.get_group(group_id)
user_refs = self.list_users_in_group(group_id, driver_hints.Hints())
found = False
for x in user_refs:
if x['id'] == user_id:
found = True
break
return found
# 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
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)
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):
values = utils.hash_ldap_user_password(values)
if self.enabled_mask:
orig_enabled = values['enabled']
self.mask_enabled_attribute(values)
values = super(UserApi, self).create(values)
if self.enabled_mask:
values['enabled'] = orig_enabled
return values
def check_password(self, user_id, password):
user = self.get(user_id)
return utils.check_password(password, user.password)
def get_filtered(self, user_id):
user = self.get(user_id)
return identity.filter_user(user)
def get_all_filtered(self):
return [identity.filter_user(user) for user in self.get_all()]
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 __init__(self, conf):
super(GroupApi, self).__init__(conf)
self.member_attribute = (getattr(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
query = '(objectClass=%s)' % self.object_class
dn = None
dn = self._id_to_dn(group_id)
if dn:
try:
conn = self.get_connection()
roles = conn.search_s(dn, ldap.SCOPE_ONELEVEL,
query, ['%s' % '1.1'])
for role_dn, _ in roles:
conn.delete_s(role_dn)
except ldap.NO_SUCH_OBJECT:
pass
finally:
conn.unbind_s()
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):
conn = self.get_connection()
try:
conn.modify_s(
self._id_to_dn(group_id),
[(ldap.MOD_ADD,
self.member_attribute,
user_dn)])
except ldap.TYPE_OR_VALUE_EXISTS:
raise exception.Conflict(_(
'User %(user_id)s is already a member of group %(group_id)s') %
{'user_id': user_id, 'group_id': group_id})
finally:
conn.unbind_s()
def remove_user(self, user_dn, group_id, user_id):
conn = self.get_connection()
try:
conn.modify_s(
self._id_to_dn(group_id),
[(ldap.MOD_DELETE,
self.member_attribute,
user_dn)])
except ldap.NO_SUCH_ATTRIBUTE:
raise exception.UserNotFound(user_id=user_id)
finally:
conn.unbind_s()
def list_user_groups(self, user_dn):
"""Return a list of groups for which the user is a member."""
query = '(&(objectClass=%s)(%s=%s)%s)' % (self.object_class,
self.member_attribute,
user_dn,
self.ldap_filter or '')
memberships = self.get_all(query)
return memberships
def list_group_users(self, group_id):
"""Return a list of user dns which are members of a group."""
query = '(objectClass=%s)' % self.object_class
conn = self.get_connection()
group_dn = self._id_to_dn(group_id)
try:
attrs = conn.search_s(group_dn,
ldap.SCOPE_BASE,
query, ['%s' % self.member_attribute])
except ldap.NO_SUCH_OBJECT:
return []
finally:
conn.unbind_s()
users = []
for dn, member in attrs:
user_dns = member.get(self.member_attribute, [])
for user_dn in user_dns:
if self.use_dumb_member and user_dn == self.dumb_member:
continue
users.append(user_dn)
return users