Skip to content

Commit

Permalink
make LDAP query scope configurable
Browse files Browse the repository at this point in the history
Get the DN from the LDAP server itself rather than hardcoding its format.

Fixes bug 1122181

Change-Id: I6f70c480b5c6f1b064e74d3cbd2cd8ca5ee82b0a
  • Loading branch information
iartarisi committed Feb 18, 2013
1 parent 63f6e87 commit 159ffe4
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 10 deletions.
4 changes: 4 additions & 0 deletions etc/keystone.conf.sample
Expand Up @@ -130,6 +130,10 @@
# allow_subtree_delete = False
# dumb_member = cn=dumb,dc=example,dc=com

# The LDAP scope for queries, this can be either 'one'
# (onelevel/singleLevel) or 'sub' (subtree/wholeSubtree)
# query_scope = one

# user_tree_dn = ou=Users,dc=example,dc=com
# user_filter =
# user_objectclass = inetOrgPerson
Expand Down
42 changes: 32 additions & 10 deletions keystone/common/ldap/core.py
Expand Up @@ -26,6 +26,8 @@

LDAP_VALUES = {'TRUE': True, 'FALSE': False}
CONTROL_TREEDELETE = '1.2.840.113556.1.4.805'
LDAP_SCOPES = {'one': ldap.SCOPE_ONELEVEL,
'sub': ldap.SCOPE_SUBTREE}


def py2ldap(val):
Expand Down Expand Up @@ -59,6 +61,14 @@ def safe_iter(attrs):
yield attrs


def ldap_scope(scope):
try:
return LDAP_SCOPES[scope]
except KeyError:
raise ValueError(_('Invalid LDAP scope: %s. Choose one of: ' % scope) +
', '.join(LDAP_SCOPES.keys()))


class BaseLdap(object):
DEFAULT_SUFFIX = "dc=example,dc=com"
DEFAULT_OU = None
Expand All @@ -77,6 +87,7 @@ def __init__(self, conf):
self.LDAP_URL = conf.ldap.url
self.LDAP_USER = conf.ldap.user
self.LDAP_PASSWORD = conf.ldap.password
self.LDAP_SCOPE = ldap_scope(conf.ldap.query_scope)

if self.options_name is not None:
self.suffix = conf.ldap.suffix
Expand Down Expand Up @@ -133,9 +144,18 @@ def get_connection(self, user=None, password=None):
return conn

def _id_to_dn(self, id):
return '%s=%s,%s' % (self.id_attr,
ldap.dn.escape_dn_chars(str(id)),
self.tree_dn)
conn = self.get_connection()
try:
dn, attrs = conn.search_s(
self.tree_dn, self.LDAP_SCOPE,
'(&(%(id_attr)s=%(id)s)(objectclass=%(objclass)s))' %
{'id_attr': self.id_attr,
'id': ldap.filter.escape_filter_chars(str(id)),
'objclass': self.object_class})[0]
except ValueError, IndexError:
raise ldap.NO_SUCH_OBJECT
else:
return dn

@staticmethod
def _dn_to_id(dn):
Expand Down Expand Up @@ -203,16 +223,18 @@ def create(self, values):

def _ldap_get(self, id, filter=None):
conn = self.get_connection()
query = '(&%s(objectClass=%s))' % (filter or self.filter or '',
self.object_class)
query = ('(&(%(id_attr)s=%(id)s)'
'%(filter)s'
'(objectClass=%(object_class)s))'
% {'id_attr': self.id_attr,
'id': ldap.filter.escape_filter_chars(str(id)),
'filter': (filter or self.filter or ''),
'object_class': self.object_class})
try:
res = conn.search_s(self._id_to_dn(id),
ldap.SCOPE_BASE,
query,
res = conn.search_s(self.tree_dn, self.LDAP_SCOPE, query,
self.attribute_mapping.values())
except ldap.NO_SUCH_OBJECT:
return None

try:
return res[0]
except IndexError:
Expand All @@ -224,7 +246,7 @@ def _ldap_get_all(self, filter=None):
self.object_class)
try:
return conn.search_s(self.tree_dn,
ldap.SCOPE_ONELEVEL,
self.LDAP_SCOPE,
query,
self.attribute_mapping.values())
except ldap.NO_SUCH_OBJECT:
Expand Down
1 change: 1 addition & 0 deletions keystone/config.py
Expand Up @@ -245,6 +245,7 @@ def register_cli_int(*args, **kw):
register_bool('use_dumb_member', group='ldap', default=False)
register_str('dumb_member', group='ldap', default='cn=dumb,dc=nonexistent')
register_bool('allow_subtree_delete', group='ldap', default=False)
register_str('query_scope', group='ldap', default='one')

register_str('user_tree_dn', group='ldap', default=None)
register_str('user_filter', group='ldap', default=None)
Expand Down
12 changes: 12 additions & 0 deletions tests/test_backend_ldap.py
Expand Up @@ -14,9 +14,11 @@
# License for the specific language governing permissions and limitations
# under the License.

import ldap
import uuid
import nose.exc

from keystone.common import ldap as ldap_common
from keystone.common.ldap import fakeldap
from keystone import config
from keystone import exception
Expand All @@ -42,6 +44,9 @@ def setUp(self):
test.testsdir('test_overrides.conf'),
test.testsdir('backend_ldap.conf')])
clear_database()
self.stubs.Set(ldap_common.BaseLdap, "_id_to_dn",
lambda self, id: '%s=%s,%s' % (self.id_attr,
str(id), self.tree_dn))
self.identity_api = identity_ldap.Identity()
self.load_fixtures(default_fixtures)

Expand Down Expand Up @@ -347,6 +352,13 @@ def test_user_api_get_connection_no_user_password(self):

user_api.get_connection(user=None, password=None)

def test_wrong_ldap_scope(self):
CONF.ldap.query_scope = uuid.uuid4().hex
self.assertRaisesRegexp(
ValueError,
'Invalid LDAP scope: %s. *' % CONF.ldap.query_scope,
identity_ldap.Identity)

# TODO (henry-nash) These need to be removed when the full LDAP implementation
# is submitted - see Bugs 1092187, 1101287, 1101276, 1101289
def test_group_crud(self):
Expand Down

0 comments on commit 159ffe4

Please sign in to comment.