"Fossies" - the Fresh Open Source Software Archive

Member "keystone-17.0.0/keystone/tests/unit/fakeldap.py" (13 May 2020, 23446 Bytes) of package /linux/misc/openstack/keystone-17.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "fakeldap.py": 16.0.1_vs_17.0.0.

    1 # Copyright 2010 United States Government as represented by the
    2 # Administrator of the National Aeronautics and Space Administration.
    3 # All Rights Reserved.
    4 #
    5 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    6 #    not use this file except in compliance with the License. You may obtain
    7 #    a copy of the License at
    8 #
    9 #         http://www.apache.org/licenses/LICENSE-2.0
   10 #
   11 #    Unless required by applicable law or agreed to in writing, software
   12 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   13 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   14 #    License for the specific language governing permissions and limitations
   15 #    under the License.
   16 
   17 """Fake LDAP server for test harness.
   18 
   19 This class does very little error checking, and knows nothing about ldap
   20 class definitions.  It implements the minimum emulation of the python ldap
   21 library to work with keystone.
   22 
   23 """
   24 
   25 import random
   26 import re
   27 import shelve
   28 
   29 import ldap
   30 from oslo_log import log
   31 
   32 import keystone.conf
   33 from keystone import exception
   34 from keystone.identity.backends.ldap import common
   35 
   36 SCOPE_NAMES = {
   37     ldap.SCOPE_BASE: 'SCOPE_BASE',
   38     ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL',
   39     ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE',
   40 }
   41 
   42 LOG = log.getLogger(__name__)
   43 CONF = keystone.conf.CONF
   44 
   45 
   46 def _internal_attr(attr_name, value_or_values):
   47     def normalize_value(value):
   48         return common.utf8_decode(value)
   49 
   50     def normalize_dn(dn):
   51         # Capitalize the attribute names as an LDAP server might.
   52 
   53         # NOTE(blk-u): Special case for this tested value, used with
   54         # test_user_id_comma. The call to str2dn here isn't always correct
   55         # here, because `dn` is escaped for an LDAP filter. str2dn() normally
   56         # works only because there's no special characters in `dn`.
   57         if dn == 'cn=Doe\\5c, John,ou=Users,cn=example,cn=com':
   58             return 'CN=Doe\\, John,OU=Users,CN=example,CN=com'
   59 
   60         # NOTE(blk-u): Another special case for this tested value. When a
   61         # roleOccupant has an escaped comma, it gets converted to \2C.
   62         if dn == 'cn=Doe\\, John,ou=Users,cn=example,cn=com':
   63             return 'CN=Doe\\2C John,OU=Users,CN=example,CN=com'
   64 
   65         try:
   66             dn = ldap.dn.str2dn(dn)
   67         except ldap.DECODING_ERROR:
   68             # NOTE(amakarov): In case of IDs instead of DNs in group members
   69             # they must be handled as regular values.
   70             return normalize_value(dn)
   71 
   72         norm = []
   73         for part in dn:
   74             name, val, i = part[0]
   75             name = name.upper()
   76             norm.append([(name, val, i)])
   77         return ldap.dn.dn2str(norm)
   78 
   79     if attr_name in ('member', 'roleOccupant'):
   80         attr_fn = normalize_dn
   81     else:
   82         attr_fn = normalize_value
   83 
   84     if isinstance(value_or_values, list):
   85         return [attr_fn(x) for x in value_or_values]
   86     return [attr_fn(value_or_values)]
   87 
   88 
   89 def _match_query(query, attrs, attrs_checked):
   90     """Match an ldap query to an attribute dictionary.
   91 
   92     The characters &, |, and ! are supported in the query. No syntax checking
   93     is performed, so malformed queries will not work correctly.
   94     """
   95     # cut off the parentheses
   96     inner = query[1:-1]
   97     if inner.startswith(('&', '|')):
   98         if inner[0] == '&':
   99             matchfn = all
  100         else:
  101             matchfn = any
  102         # cut off the & or |
  103         groups = _paren_groups(inner[1:])
  104         return matchfn(_match_query(group, attrs, attrs_checked)
  105                        for group in groups)
  106     if inner.startswith('!'):
  107         # cut off the ! and the nested parentheses
  108         return not _match_query(query[2:-1], attrs, attrs_checked)
  109 
  110     (k, _sep, v) = inner.partition('=')
  111     attrs_checked.add(k.lower())
  112     return _match(k, v, attrs)
  113 
  114 
  115 def _paren_groups(source):
  116     """Split a string into parenthesized groups."""
  117     count = 0
  118     start = 0
  119     result = []
  120     for pos in range(len(source)):
  121         if source[pos] == '(':
  122             if count == 0:
  123                 start = pos
  124             count += 1
  125         if source[pos] == ')':
  126             count -= 1
  127             if count == 0:
  128                 result.append(source[start:pos + 1])
  129     return result
  130 
  131 
  132 def _match(key, value, attrs):
  133     """Match a given key and value against an attribute list."""
  134     def match_with_wildcards(norm_val, val_list):
  135         # Case insensitive checking with wildcards
  136         if norm_val.startswith('*'):
  137             if norm_val.endswith('*'):
  138                 # Is the string anywhere in the target?
  139                 for x in val_list:
  140                     if norm_val[1:-1] in x:
  141                         return True
  142             else:
  143                 # Is the string at the end of the target?
  144                 for x in val_list:
  145                     if (norm_val[1:] ==
  146                             x[len(x) - len(norm_val) + 1:]):
  147                         return True
  148         elif norm_val.endswith('*'):
  149             # Is the string at the start of the target?
  150             for x in val_list:
  151                 if norm_val[:-1] == x[:len(norm_val) - 1]:
  152                     return True
  153         else:
  154             # Is the string an exact match?
  155             for x in val_list:
  156                 if check_value == x:
  157                     return True
  158         return False
  159 
  160     if key not in attrs:
  161         return False
  162     # This is a pure wild card search, so the answer must be yes!
  163     if value == '*':
  164         return True
  165     if key == 'serviceId':
  166         # For serviceId, the backend is returning a list of numbers.
  167         # Make sure we convert them to strings first before comparing
  168         # them.
  169         str_sids = [str(x) for x in attrs[key]]
  170         return str(value) in str_sids
  171     if key != 'objectclass':
  172         check_value = _internal_attr(key, value)[0].lower()
  173         norm_values = list(
  174             _internal_attr(key, x)[0].lower() for x in attrs[key])
  175         return match_with_wildcards(check_value, norm_values)
  176     # It is an objectclass check, so check subclasses
  177     values = _subs(value)
  178     for v in values:
  179         if v in attrs[key]:
  180             return True
  181     return False
  182 
  183 
  184 def _subs(value):
  185     """Return a list of subclass strings.
  186 
  187     The strings represent the ldap objectclass plus any subclasses that
  188     inherit from it. Fakeldap doesn't know about the ldap object structure,
  189     so subclasses need to be defined manually in the dictionary below.
  190 
  191     """
  192     subs = {'groupOfNames': ['keystoneProject',
  193                              'keystoneRole',
  194                              'keystoneProjectRole']}
  195     if value in subs:
  196         return [value] + subs[value]
  197     return [value]
  198 
  199 
  200 server_fail = False
  201 
  202 
  203 class FakeShelve(dict):
  204 
  205     def sync(self):
  206         pass
  207 
  208 
  209 FakeShelves = {}
  210 PendingRequests = {}
  211 
  212 
  213 class FakeLdap(common.LDAPHandler):
  214     """Emulate the python-ldap API.
  215 
  216     The python-ldap API requires all strings to be UTF-8 encoded with the
  217     exception of [1]. This is assured by the caller of this interface
  218     (i.e. KeystoneLDAPHandler).
  219 
  220     However, internally this emulation MUST process and store strings
  221     in a canonical form which permits operations on
  222     characters. Encoded strings do not provide the ability to operate
  223     on characters. Therefore this emulation accepts UTF-8 encoded
  224     strings, decodes them to unicode for operations internal to this
  225     emulation, and encodes them back to UTF-8 when returning values
  226     from the emulation.
  227 
  228     [1] Some fields (DNs, RDNs, attribute names, queries) are represented
  229     as text in python-ldap for Python 3, and for Python 2 when
  230     bytes_mode=False. For more details see:
  231     http://www.python-ldap.org/en/latest/bytes_mode.html#bytes-mode
  232 
  233     """
  234 
  235     __prefix = 'ldap:'
  236 
  237     def __init__(self, conn=None):
  238         super(FakeLdap, self).__init__(conn=conn)
  239         self._ldap_options = {ldap.OPT_DEREF: ldap.DEREF_NEVER}
  240 
  241     def connect(self, url, page_size=0, alias_dereferencing=None,
  242                 use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
  243                 tls_req_cert='demand', chase_referrals=None, debug_level=None,
  244                 use_pool=None, pool_size=None, pool_retry_max=None,
  245                 pool_retry_delay=None, pool_conn_timeout=None,
  246                 pool_conn_lifetime=None,
  247                 conn_timeout=None):
  248         if url.startswith('fake://memory'):
  249             if url not in FakeShelves:
  250                 FakeShelves[url] = FakeShelve()
  251             self.db = FakeShelves[url]
  252         else:
  253             self.db = shelve.open(url[7:])
  254 
  255         using_ldaps = url.lower().startswith("ldaps")
  256 
  257         if use_tls and using_ldaps:
  258             raise AssertionError('Invalid TLS / LDAPS combination')
  259 
  260         if use_tls:
  261             if tls_cacertfile:
  262                 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, tls_cacertfile)
  263             elif tls_cacertdir:
  264                 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, tls_cacertdir)
  265             if tls_req_cert in list(common.LDAP_TLS_CERTS.values()):
  266                 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_cert)
  267             else:
  268                 raise ValueError("invalid TLS_REQUIRE_CERT tls_req_cert=%s",
  269                                  tls_req_cert)
  270 
  271         if alias_dereferencing is not None:
  272             self.set_option(ldap.OPT_DEREF, alias_dereferencing)
  273         self.page_size = page_size
  274 
  275         self.use_pool = use_pool
  276         self.pool_size = pool_size
  277         self.pool_retry_max = pool_retry_max
  278         self.pool_retry_delay = pool_retry_delay
  279         self.pool_conn_timeout = pool_conn_timeout
  280         self.pool_conn_lifetime = pool_conn_lifetime
  281         self.conn_timeout = conn_timeout
  282 
  283     def _dn_to_id_attr(self, dn):
  284         return ldap.dn.str2dn(dn)[0][0][0]
  285 
  286     def _dn_to_id_value(self, dn):
  287         return ldap.dn.str2dn(dn)[0][0][1]
  288 
  289     def key(self, dn):
  290         return '%s%s' % (self.__prefix, dn)
  291 
  292     def simple_bind_s(self, who='', cred='',
  293                       serverctrls=None, clientctrls=None):
  294         """Provide for compatibility but this method is ignored."""
  295         if server_fail:
  296             raise ldap.SERVER_DOWN
  297         whos = ['cn=Admin', CONF.ldap.user]
  298         if (who in whos and cred in ['password', CONF.ldap.password]):
  299             return
  300 
  301         attrs = self.db.get(self.key(who))
  302         if not attrs:
  303             LOG.debug('who=%s not found, binding anonymously', who)
  304 
  305         db_password = ''
  306         if attrs:
  307             try:
  308                 db_password = attrs['userPassword'][0]
  309             except (KeyError, IndexError):
  310                 LOG.debug('bind fail: password for who=%s not found', who)
  311                 raise ldap.INAPPROPRIATE_AUTH
  312 
  313         if cred != db_password:
  314             LOG.debug('bind fail: password for who=%s does not match', who)
  315             raise ldap.INVALID_CREDENTIALS
  316 
  317     def unbind_s(self):
  318         """Provide for compatibility but this method is ignored."""
  319         if server_fail:
  320             raise ldap.SERVER_DOWN
  321 
  322     def add_s(self, dn, modlist):
  323         """Add an object with the specified attributes at dn."""
  324         if server_fail:
  325             raise ldap.SERVER_DOWN
  326 
  327         id_attr_in_modlist = False
  328         id_attr = self._dn_to_id_attr(dn)
  329         id_value = self._dn_to_id_value(dn)
  330 
  331         # The LDAP API raises a TypeError if attr name is None.
  332         for k, dummy_v in modlist:
  333             if k is None:
  334                 raise TypeError('must be string, not None. modlist=%s' %
  335                                 modlist)
  336 
  337             if k == id_attr:
  338                 for val in dummy_v:
  339                     if common.utf8_decode(val) == id_value:
  340                         id_attr_in_modlist = True
  341 
  342         if not id_attr_in_modlist:
  343             LOG.debug('id_attribute=%(attr)s missing, attributes=%(attrs)s',
  344                       {'attr': id_attr, 'attrs': modlist})
  345             raise ldap.NAMING_VIOLATION
  346         key = self.key(dn)
  347         LOG.debug('add item: dn=%(dn)s, attrs=%(attrs)s', {
  348             'dn': dn, 'attrs': modlist})
  349         if key in self.db:
  350             LOG.debug('add item failed: dn=%s is already in store.', dn)
  351             raise ldap.ALREADY_EXISTS(dn)
  352 
  353         self.db[key] = {k: _internal_attr(k, v) for k, v in modlist}
  354         self.db.sync()
  355 
  356     def delete_s(self, dn):
  357         """Remove the ldap object at specified dn."""
  358         return self.delete_ext_s(dn, serverctrls=[])
  359 
  360     def _getChildren(self, dn):
  361         return [k for k, v in self.db.items()
  362                 if re.match('%s.*,%s' % (
  363                             re.escape(self.__prefix),
  364                             re.escape(dn)), k)]
  365 
  366     def delete_ext_s(self, dn, serverctrls, clientctrls=None):
  367         """Remove the ldap object at specified dn."""
  368         if server_fail:
  369             raise ldap.SERVER_DOWN
  370 
  371         try:
  372             key = self.key(dn)
  373             LOG.debug('FakeLdap delete item: dn=%s', dn)
  374             del self.db[key]
  375         except KeyError:
  376             LOG.debug('delete item failed: dn=%s not found.', dn)
  377             raise ldap.NO_SUCH_OBJECT
  378         self.db.sync()
  379 
  380     def modify_s(self, dn, modlist):
  381         """Modify the object at dn using the attribute list.
  382 
  383         :param dn: an LDAP DN
  384         :param modlist: a list of tuples in the following form:
  385                       ([MOD_ADD | MOD_DELETE | MOD_REPACE], attribute, value)
  386         """
  387         if server_fail:
  388             raise ldap.SERVER_DOWN
  389 
  390         key = self.key(dn)
  391         LOG.debug('modify item: dn=%(dn)s attrs=%(attrs)s', {
  392             'dn': dn, 'attrs': modlist})
  393         try:
  394             entry = self.db[key]
  395         except KeyError:
  396             LOG.debug('modify item failed: dn=%s not found.', dn)
  397             raise ldap.NO_SUCH_OBJECT
  398 
  399         for cmd, k, v in modlist:
  400             values = entry.setdefault(k, [])
  401             if cmd == ldap.MOD_ADD:
  402                 v = _internal_attr(k, v)
  403                 for x in v:
  404                     if x in values:
  405                         raise ldap.TYPE_OR_VALUE_EXISTS
  406                 values += v
  407             elif cmd == ldap.MOD_REPLACE:
  408                 values[:] = _internal_attr(k, v)
  409             elif cmd == ldap.MOD_DELETE:
  410                 if v is None:
  411                     if not values:
  412                         LOG.debug('modify item failed: '
  413                                   'item has no attribute "%s" to delete', k)
  414                         raise ldap.NO_SUCH_ATTRIBUTE
  415                     values[:] = []
  416                 else:
  417                     for val in _internal_attr(k, v):
  418                         try:
  419                             values.remove(val)
  420                         except ValueError:
  421                             LOG.debug('modify item failed: '
  422                                       'item has no attribute "%(k)s" with '
  423                                       'value "%(v)s" to delete', {
  424                                           'k': k, 'v': val})
  425                             raise ldap.NO_SUCH_ATTRIBUTE
  426             else:
  427                 LOG.debug('modify item failed: unknown command %s', cmd)
  428                 raise NotImplementedError('modify_s action %s not'
  429                                           ' implemented' % cmd)
  430         self.db[key] = entry
  431         self.db.sync()
  432 
  433     def search_s(self, base, scope,
  434                  filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
  435         """Search for all matching objects under base using the query.
  436 
  437         Args:
  438         base -- dn to search under
  439         scope -- search scope (base, subtree, onelevel)
  440         filterstr -- filter objects by
  441         attrlist -- attrs to return. Returns all attrs if not specified
  442 
  443         """
  444         if server_fail:
  445             raise ldap.SERVER_DOWN
  446 
  447         if (not filterstr) and (scope != ldap.SCOPE_BASE):
  448             raise AssertionError('Search without filter on onelevel or '
  449                                  'subtree scope')
  450 
  451         if scope == ldap.SCOPE_BASE:
  452             try:
  453                 item_dict = self.db[self.key(base)]
  454             except KeyError:
  455                 LOG.debug('search fail: dn not found for SCOPE_BASE')
  456                 raise ldap.NO_SUCH_OBJECT
  457             results = [(base, item_dict)]
  458         elif scope == ldap.SCOPE_SUBTREE:
  459             # FIXME - LDAP search with SUBTREE scope must return the base
  460             # entry, but the code below does _not_. Unfortunately, there are
  461             # several tests that depend on this broken behavior, and fail
  462             # when the base entry is returned in the search results. The
  463             # fix is easy here, just initialize results as above for
  464             # the SCOPE_BASE case.
  465             # https://bugs.launchpad.net/keystone/+bug/1368772
  466             try:
  467                 item_dict = self.db[self.key(base)]
  468             except KeyError:
  469                 LOG.debug('search fail: dn not found for SCOPE_SUBTREE')
  470                 raise ldap.NO_SUCH_OBJECT
  471             results = [(base, item_dict)]
  472             extraresults = [(k[len(self.__prefix):], v)
  473                             for k, v in self.db.items()
  474                             if re.match('%s.*,%s' %
  475                                         (re.escape(self.__prefix),
  476                                          re.escape(base)), k)]
  477             results.extend(extraresults)
  478         elif scope == ldap.SCOPE_ONELEVEL:
  479 
  480             def get_entries():
  481                 base_dn = ldap.dn.str2dn(base)
  482                 base_len = len(base_dn)
  483 
  484                 for k, v in self.db.items():
  485                     if not k.startswith(self.__prefix):
  486                         continue
  487                     k_dn_str = k[len(self.__prefix):]
  488                     k_dn = ldap.dn.str2dn(k_dn_str)
  489                     if len(k_dn) != base_len + 1:
  490                         continue
  491                     if k_dn[-base_len:] != base_dn:
  492                         continue
  493                     yield (k_dn_str, v)
  494 
  495             results = list(get_entries())
  496 
  497         else:
  498             # openldap client/server raises PROTOCOL_ERROR for unexpected scope
  499             raise ldap.PROTOCOL_ERROR
  500 
  501         objects = []
  502         for dn, attrs in results:
  503             # filter the objects by filterstr
  504             id_attr, id_val, _ = ldap.dn.str2dn(dn)[0][0]
  505             match_attrs = attrs.copy()
  506             match_attrs[id_attr] = [id_val]
  507             attrs_checked = set()
  508             if not filterstr or _match_query(filterstr,
  509                                              match_attrs,
  510                                              attrs_checked):
  511                 if (filterstr and
  512                         (scope != ldap.SCOPE_BASE) and
  513                         ('objectclass' not in attrs_checked)):
  514                     raise AssertionError('No objectClass in search filter')
  515                 # filter the attributes by attrlist
  516                 attrs = {k: v for k, v in attrs.items()
  517                          if not attrlist or k in attrlist}
  518                 objects.append((dn, attrs))
  519 
  520         return objects
  521 
  522     def set_option(self, option, invalue):
  523         self._ldap_options[option] = invalue
  524 
  525     def get_option(self, option):
  526         value = self._ldap_options.get(option)
  527         return value
  528 
  529     def search_ext(self, base, scope,
  530                    filterstr='(objectClass=*)', attrlist=None, attrsonly=0,
  531                    serverctrls=None, clientctrls=None,
  532                    timeout=-1, sizelimit=0):
  533         if clientctrls is not None or timeout != -1 or sizelimit != 0:
  534             raise exception.NotImplemented()
  535 
  536         # only passing a single server control is supported by this fake ldap
  537         if len(serverctrls) > 1:
  538             raise exception.NotImplemented()
  539 
  540         # search_ext is async and returns an identifier used for
  541         # retrieving the results via result3(). This will be emulated by
  542         # storing the request in a variable with random integer key and
  543         # performing the real lookup in result3()
  544         msgid = random.randint(0, 1000)
  545         PendingRequests[msgid] = (base, scope, filterstr, attrlist, attrsonly,
  546                                   serverctrls)
  547         return msgid
  548 
  549     def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None,
  550                 resp_ctrl_classes=None):
  551         """Execute async request.
  552 
  553         Only msgid param is supported. Request info is fetched from global
  554         variable `PendingRequests` by msgid, executed using search_s and
  555         limited if requested.
  556         """
  557         if all != 1 or timeout is not None or resp_ctrl_classes is not None:
  558             raise exception.NotImplemented()
  559 
  560         params = PendingRequests[msgid]
  561         # search_s accepts a subset of parameters of search_ext,
  562         # that's why we use only the first 5.
  563         results = self.search_s(*params[:5])
  564 
  565         # extract limit from serverctrl
  566         serverctrls = params[5]
  567         ctrl = serverctrls[0]
  568 
  569         if ctrl.size:
  570             rdata = results[:ctrl.size]
  571         else:
  572             rdata = results
  573 
  574         # real result3 returns various service info -- rtype, rmsgid,
  575         # serverctrls. Now this info is not used, so all this info is None
  576         rtype = None
  577         rmsgid = None
  578         serverctrls = None
  579         return (rtype, rdata, rmsgid, serverctrls)
  580 
  581 
  582 class FakeLdapPool(FakeLdap):
  583     """Emulate the python-ldap API with pooled connections.
  584 
  585     This class is used as connector class in PooledLDAPHandler.
  586 
  587     """
  588 
  589     def __init__(self, uri, retry_max=None, retry_delay=None, conn=None):
  590         super(FakeLdapPool, self).__init__(conn=conn)
  591         self.url = uri
  592         self.connected = None
  593         self.conn = self
  594         self._connection_time = 5  # any number greater than 0
  595 
  596     def get_lifetime(self):
  597         return self._connection_time
  598 
  599     def simple_bind_s(self, who=None, cred=None,
  600                       serverctrls=None, clientctrls=None):
  601         if self.url.startswith('fakepool://memory'):
  602             if self.url not in FakeShelves:
  603                 FakeShelves[self.url] = FakeShelve()
  604             self.db = FakeShelves[self.url]
  605         else:
  606             self.db = shelve.open(self.url[11:])
  607 
  608         if not who:
  609             who = 'cn=Admin'
  610         if not cred:
  611             cred = 'password'
  612 
  613         super(FakeLdapPool, self).simple_bind_s(who=who, cred=cred,
  614                                                 serverctrls=serverctrls,
  615                                                 clientctrls=clientctrls)
  616 
  617     def unbind_ext_s(self):
  618         """Added to extend FakeLdap as connector class."""
  619         pass
  620 
  621 
  622 class FakeLdapNoSubtreeDelete(FakeLdap):
  623     """FakeLdap subclass that does not support subtree delete.
  624 
  625     Same as FakeLdap except delete will throw the LDAP error
  626     ldap.NOT_ALLOWED_ON_NONLEAF if there is an attempt to delete
  627     an entry that has children.
  628     """
  629 
  630     def delete_ext_s(self, dn, serverctrls, clientctrls=None):
  631         """Remove the ldap object at specified dn."""
  632         if server_fail:
  633             raise ldap.SERVER_DOWN
  634 
  635         try:
  636             children = self._getChildren(dn)
  637             if children:
  638                 raise ldap.NOT_ALLOWED_ON_NONLEAF
  639 
  640         except KeyError:
  641             LOG.debug('delete item failed: dn=%s not found.', dn)
  642             raise ldap.NO_SUCH_OBJECT
  643         super(FakeLdapNoSubtreeDelete, self).delete_ext_s(dn,
  644                                                           serverctrls,
  645                                                           clientctrls)