1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2010 United States Government as represented by the
4
# Administrator of the National Aeronautics and Space Administration.
7
# Licensed under the Apache License, Version 2.0 (the "License"); you may
8
# not use this file except in compliance with the License. You may obtain
9
# a copy of the License at
11
# http://www.apache.org/licenses/LICENSE-2.0
13
# Unless required by applicable law or agreed to in writing, software
14
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
# License for the specific language governing permissions and limitations
18
"""Fake LDAP server for test harness.
20
This class does very little error checking, and knows nothing about ldap
21
class definitions. It implements the minimum emulation of the python ldap
22
library to work with nova.
34
ldap.SCOPE_BASE: 'SCOPE_BASE',
35
ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL',
36
ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE',
40
LOG = logging.getLogger('keystone.backends.ldap.fakeldap')
44
"""Opens a fake connection with an LDAP server."""
48
def _match_query(query, attrs):
49
"""Match an ldap query to an attribute dictionary.
51
The characters &, |, and ! are supported in the query. No syntax checking
52
is performed, so malformed querys will not work correctly.
54
# cut off the parentheses
56
if inner.startswith('&'):
58
l, r = _paren_groups(inner[1:])
59
return _match_query(l, attrs) and _match_query(r, attrs)
60
if inner.startswith('|'):
62
l, r = _paren_groups(inner[1:])
63
return _match_query(l, attrs) or _match_query(r, attrs)
64
if inner.startswith('!'):
65
# cut off the ! and the nested parentheses
66
return not _match_query(query[2:-1], attrs)
68
(k, _sep, v) = inner.partition('=')
69
return _match(k, v, attrs)
72
def _paren_groups(source):
73
"""Split a string into parenthesized groups."""
77
for pos in xrange(len(source)):
78
if source[pos] == '(':
82
if source[pos] == ')':
85
result.append(source[start:pos + 1])
89
def _match(key, value, attrs):
90
"""Match a given key and value against an attribute list."""
93
# This is a wild card search. Implemented as all or nothing for now.
96
if key != "objectclass":
97
return value in attrs[key]
98
# it is an objectclass check, so check subclasses
107
"""Returns a list of subclass strings.
109
The strings represent the ldap objectclass plus any subclasses that
110
inherit from it. Fakeldap doesn't know about the ldap object structure,
111
so subclasses need to be defined manually in the dictionary below.
114
subs = {'groupOfNames': [
117
'keystoneTenantRole']}
119
return [value] + subs[value]
126
class FakeLDAP(object):
127
"""Fake LDAP connection."""
129
def __init__(self, url):
130
LOG.debug("FakeLDAP initialize url=%s" % (url,))
131
self.db = shelve.open(url[7:])
133
def simple_bind_s(self, dn, password):
134
"""This method is ignored, but provided for compatibility."""
136
raise ldap.SERVER_DOWN
137
LOG.debug("FakeLDAP bind dn=%s" % (dn,))
138
if dn == 'cn=Admin' and password == 'password':
141
attrs = self.db["%s%s" % (self.__prefix, dn)]
143
LOG.error("FakeLDAP bind fail: dn=%s not found" % (dn,))
144
raise ldap.NO_SUCH_OBJECT
147
db_passwd = attrs['userPassword'][0]
148
except (KeyError, IndexError):
149
LOG.error("FakeLDAP bind fail: password for dn=%s not found" % dn)
150
raise ldap.INAPPROPRIATE_AUTH
151
if db_passwd != password:
152
LOG.error("FakeLDAP bind fail: password for dn=%s does not match" %
154
raise ldap.INVALID_CREDENTIALS
157
"""This method is ignored, but provided for compatibility."""
159
raise ldap.SERVER_DOWN
161
def add_s(self, dn, attrs):
162
"""Add an object with the specified attributes at dn."""
164
raise ldap.SERVER_DOWN
166
key = "%s%s" % (self.__prefix, dn)
167
LOG.debug("FakeLDAP add item: dn=%s, attrs=%s" % (dn, attrs))
170
"FakeLDAP add item failed: dn '%s' is already in store." % dn)
171
raise ldap.ALREADY_EXISTS
172
self.db[key] = dict([(k, v if isinstance(v, list) else [v])
176
def delete_s(self, dn):
177
"""Remove the ldap object at specified dn."""
179
raise ldap.SERVER_DOWN
181
key = "%s%s" % (self.__prefix, dn)
182
LOG.debug("FakeLDAP delete item: dn=%s" % (dn,))
186
LOG.error("FakeLDAP delete item failed: dn '%s' not found." % dn)
187
raise ldap.NO_SUCH_OBJECT
190
def modify_s(self, dn, attrs):
191
"""Modify the object at dn using the attribute list.
195
attrs -- a list of tuples in the following form:
196
([MOD_ADD | MOD_DELETE | MOD_REPACE], attribute, value)
200
raise ldap.SERVER_DOWN
202
key = "%s%s" % (self.__prefix, dn)
203
LOG.debug("FakeLDAP modify item: dn=%s attrs=%s" % (dn, attrs))
207
LOG.error("FakeLDAP modify item failed: dn '%s' not found." % dn)
208
raise ldap.NO_SUCH_OBJECT
210
for cmd, k, v in attrs:
211
values = entry.setdefault(k, [])
212
if cmd == ldap.MOD_ADD:
213
if isinstance(v, list):
217
elif cmd == ldap.MOD_REPLACE:
218
values[:] = v if isinstance(v, list) else [v]
219
elif cmd == ldap.MOD_DELETE:
222
LOG.error("FakeLDAP modify item failed: "
223
"item has no attribute '%s' to delete" % k)
224
raise ldap.NO_SUCH_ATTRIBUTE
227
if not isinstance(v, list):
233
LOG.error("FakeLDAP modify item failed: "
234
"item has no attribute '%s' with value '%s'"
235
" to delete" % (k, val))
236
raise ldap.NO_SUCH_ATTRIBUTE
238
LOG.error("FakeLDAP modify item failed: unknown command %s"
240
raise NotImplementedError(\
241
"modify_s action %s not implemented" % (cmd,))
245
def search_s(self, dn, scope, query=None, fields=None):
246
"""Search for all matching objects under dn using the query.
249
dn -- dn to search under
250
scope -- only SCOPE_BASE and SCOPE_SUBTREE are supported
251
query -- query to filter objects by
252
fields -- fields to return. Returns all fields if not specified
256
raise ldap.SERVER_DOWN
258
LOG.debug("FakeLDAP search at dn=%s scope=%s query='%s'" %
259
(dn, scope_names.get(scope, scope), query))
260
if scope == ldap.SCOPE_BASE:
262
item_dict = self.db["%s%s" % (self.__prefix, dn)]
264
LOG.debug("FakeLDAP search fail: dn not found for SCOPE_BASE")
265
raise ldap.NO_SUCH_OBJECT
266
results = [(dn, item_dict)]
267
elif scope == ldap.SCOPE_SUBTREE:
268
results = [(k[len(self.__prefix):], v)
269
for k, v in self.db.iteritems()
270
if re.match("%s.*,%s" % (self.__prefix, dn), k)]
271
elif scope == ldap.SCOPE_ONELEVEL:
272
results = [(k[len(self.__prefix):], v)
273
for k, v in self.db.iteritems()
274
if re.match("%s\w+=[^,]+,%s" % (self.__prefix, dn), k)]
276
LOG.error("FakeLDAP search fail: unknown scope %s" % (scope,))
277
raise NotImplementedError("Search scope %s not implemented." %
281
for dn, attrs in results:
282
# filter the objects by query
283
if not query or _match_query(query, attrs):
284
# filter the attributes by fields
285
attrs = dict([(k, v) for k, v in attrs.iteritems()
286
if not fields or k in fields])
287
objects.append((dn, attrs))
288
# pylint: enable=E1103
289
LOG.debug("FakeLDAP search result: %s" % (objects,))
293
def __prefix(self): # pylint: disable=R0201
294
"""Get the prefix to use for all keys."""