~ubuntu-branches/ubuntu/precise/keystone/precise-security

« back to all changes in this revision

Viewing changes to keystone/backends/ldap/fakeldap.py

  • Committer: Bazaar Package Importer
  • Author(s): Chuck Short
  • Date: 2011-08-23 10:18:22 UTC
  • Revision ID: james.westby@ubuntu.com-20110823101822-enve6zceb3lqhuvj
Tags: upstream-1.0~d4~20110823.1078
ImportĀ upstreamĀ versionĀ 1.0~d4~20110823.1078

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
2
 
 
3
# Copyright 2010 United States Government as represented by the
 
4
# Administrator of the National Aeronautics and Space Administration.
 
5
# All Rights Reserved.
 
6
#
 
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
 
10
#
 
11
#         http://www.apache.org/licenses/LICENSE-2.0
 
12
#
 
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
 
17
#    under the License.
 
18
"""Fake LDAP server for test harness.
 
19
 
 
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.
 
23
 
 
24
"""
 
25
 
 
26
import logging
 
27
import re
 
28
import shelve
 
29
 
 
30
import ldap
 
31
 
 
32
 
 
33
scope_names = {
 
34
    ldap.SCOPE_BASE: 'SCOPE_BASE',
 
35
    ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL',
 
36
    ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE',
 
37
}
 
38
 
 
39
 
 
40
LOG = logging.getLogger('keystone.backends.ldap.fakeldap')
 
41
 
 
42
 
 
43
def initialize(uri):
 
44
    """Opens a fake connection with an LDAP server."""
 
45
    return FakeLDAP(uri)
 
46
 
 
47
 
 
48
def _match_query(query, attrs):
 
49
    """Match an ldap query to an attribute dictionary.
 
50
 
 
51
    The characters &, |, and ! are supported in the query. No syntax checking
 
52
    is performed, so malformed querys will not work correctly.
 
53
    """
 
54
    # cut off the parentheses
 
55
    inner = query[1:-1]
 
56
    if inner.startswith('&'):
 
57
        # cut off the &
 
58
        l, r = _paren_groups(inner[1:])
 
59
        return _match_query(l, attrs) and _match_query(r, attrs)
 
60
    if inner.startswith('|'):
 
61
        # cut off the |
 
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)
 
67
 
 
68
    (k, _sep, v) = inner.partition('=')
 
69
    return _match(k, v, attrs)
 
70
 
 
71
 
 
72
def _paren_groups(source):
 
73
    """Split a string into parenthesized groups."""
 
74
    count = 0
 
75
    start = 0
 
76
    result = []
 
77
    for pos in xrange(len(source)):
 
78
        if source[pos] == '(':
 
79
            if count == 0:
 
80
                start = pos
 
81
            count += 1
 
82
        if source[pos] == ')':
 
83
            count -= 1
 
84
            if count == 0:
 
85
                result.append(source[start:pos + 1])
 
86
    return result
 
87
 
 
88
 
 
89
def _match(key, value, attrs):
 
90
    """Match a given key and value against an attribute list."""
 
91
    if key not in attrs:
 
92
        return False
 
93
    # This is a wild card search. Implemented as all or nothing for now.
 
94
    if value == "*":
 
95
        return True
 
96
    if key != "objectclass":
 
97
        return value in attrs[key]
 
98
    # it is an objectclass check, so check subclasses
 
99
    values = _subs(value)
 
100
    for v in values:
 
101
        if v in attrs[key]:
 
102
            return True
 
103
    return False
 
104
 
 
105
 
 
106
def _subs(value):
 
107
    """Returns a list of subclass strings.
 
108
 
 
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.
 
112
 
 
113
    """
 
114
    subs = {'groupOfNames': [
 
115
        'keystoneTenant',
 
116
        'keystoneRole',
 
117
        'keystoneTenantRole']}
 
118
    if value in subs:
 
119
        return [value] + subs[value]
 
120
    return [value]
 
121
 
 
122
 
 
123
server_fail = False
 
124
 
 
125
 
 
126
class FakeLDAP(object):
 
127
    """Fake LDAP connection."""
 
128
 
 
129
    def __init__(self, url):
 
130
        LOG.debug("FakeLDAP initialize url=%s" % (url,))
 
131
        self.db = shelve.open(url[7:])
 
132
 
 
133
    def simple_bind_s(self, dn, password):
 
134
        """This method is ignored, but provided for compatibility."""
 
135
        if server_fail:
 
136
            raise ldap.SERVER_DOWN
 
137
        LOG.debug("FakeLDAP bind dn=%s" % (dn,))
 
138
        if dn == 'cn=Admin' and password == 'password':
 
139
            return
 
140
        try:
 
141
            attrs = self.db["%s%s" % (self.__prefix, dn)]
 
142
        except KeyError:
 
143
            LOG.error("FakeLDAP bind fail: dn=%s not found" % (dn,))
 
144
            raise ldap.NO_SUCH_OBJECT
 
145
        db_passwd = None
 
146
        try:
 
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" %
 
153
                dn)
 
154
            raise ldap.INVALID_CREDENTIALS
 
155
 
 
156
    def unbind_s(self):
 
157
        """This method is ignored, but provided for compatibility."""
 
158
        if server_fail:
 
159
            raise ldap.SERVER_DOWN
 
160
 
 
161
    def add_s(self, dn, attrs):
 
162
        """Add an object with the specified attributes at dn."""
 
163
        if server_fail:
 
164
            raise ldap.SERVER_DOWN
 
165
 
 
166
        key = "%s%s" % (self.__prefix, dn)
 
167
        LOG.debug("FakeLDAP add item: dn=%s, attrs=%s" % (dn, attrs))
 
168
        if key in self.db:
 
169
            LOG.error(
 
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])
 
173
                             for k, v in attrs])
 
174
        self.db.sync()
 
175
 
 
176
    def delete_s(self, dn):
 
177
        """Remove the ldap object at specified dn."""
 
178
        if server_fail:
 
179
            raise ldap.SERVER_DOWN
 
180
 
 
181
        key = "%s%s" % (self.__prefix, dn)
 
182
        LOG.debug("FakeLDAP delete item: dn=%s" % (dn,))
 
183
        try:
 
184
            del self.db[key]
 
185
        except KeyError:
 
186
            LOG.error("FakeLDAP delete item failed: dn '%s' not found." % dn)
 
187
            raise ldap.NO_SUCH_OBJECT
 
188
        self.db.sync()
 
189
 
 
190
    def modify_s(self, dn, attrs):
 
191
        """Modify the object at dn using the attribute list.
 
192
 
 
193
        Args:
 
194
        dn -- a dn
 
195
        attrs -- a list of tuples in the following form:
 
196
            ([MOD_ADD | MOD_DELETE | MOD_REPACE], attribute, value)
 
197
 
 
198
        """
 
199
        if server_fail:
 
200
            raise ldap.SERVER_DOWN
 
201
 
 
202
        key = "%s%s" % (self.__prefix, dn)
 
203
        LOG.debug("FakeLDAP modify item: dn=%s attrs=%s" % (dn, attrs))
 
204
        try:
 
205
            entry = self.db[key]
 
206
        except KeyError:
 
207
            LOG.error("FakeLDAP modify item failed: dn '%s' not found." % dn)
 
208
            raise ldap.NO_SUCH_OBJECT
 
209
 
 
210
        for cmd, k, v in attrs:
 
211
            values = entry.setdefault(k, [])
 
212
            if cmd == ldap.MOD_ADD:
 
213
                if isinstance(v, list):
 
214
                    values += v
 
215
                else:
 
216
                    values.append(v)
 
217
            elif cmd == ldap.MOD_REPLACE:
 
218
                values[:] = v if isinstance(v, list) else [v]
 
219
            elif cmd == ldap.MOD_DELETE:
 
220
                if v is None:
 
221
                    if len(values) == 0:
 
222
                        LOG.error("FakeLDAP modify item failed: "
 
223
                                  "item has no attribute '%s' to delete" % k)
 
224
                        raise ldap.NO_SUCH_ATTRIBUTE
 
225
                    values[:] = []
 
226
                else:
 
227
                    if not isinstance(v, list):
 
228
                        v = [v]
 
229
                    for val in v:
 
230
                        try:
 
231
                            values.remove(val)
 
232
                        except ValueError:
 
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
 
237
            else:
 
238
                LOG.error("FakeLDAP modify item failed: unknown command %s"
 
239
                    % (cmd,))
 
240
                raise NotImplementedError(\
 
241
                    "modify_s action %s not implemented" % (cmd,))
 
242
        self.db[key] = entry
 
243
        self.db.sync()
 
244
 
 
245
    def search_s(self, dn, scope, query=None, fields=None):
 
246
        """Search for all matching objects under dn using the query.
 
247
 
 
248
        Args:
 
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
 
253
 
 
254
        """
 
255
        if server_fail:
 
256
            raise ldap.SERVER_DOWN
 
257
 
 
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:
 
261
            try:
 
262
                item_dict = self.db["%s%s" % (self.__prefix, dn)]
 
263
            except KeyError:
 
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)]
 
275
        else:
 
276
            LOG.error("FakeLDAP search fail: unknown scope %s" % (scope,))
 
277
            raise NotImplementedError("Search scope %s not implemented." %
 
278
                                                                    (scope,))
 
279
 
 
280
        objects = []
 
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,))
 
290
        return objects
 
291
 
 
292
    @property
 
293
    def __prefix(self):  # pylint: disable=R0201
 
294
        """Get the prefix to use for all keys."""
 
295
        return 'ldap:'