"Fossies" - the Fresh Open Source Software Archive

Member "keystone-19.0.0/keystone/identity/backends/ldap/common.py" (14 Apr 2021, 79646 Bytes) of package /linux/misc/openstack/keystone-19.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. For more information about "common.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 18.0.0_vs_19.0.0.

    1 # Copyright 2012 OpenStack Foundation
    2 #
    3 # Licensed under the Apache License, Version 2.0 (the "License"); you may
    4 # not use this file except in compliance with the License. You may obtain
    5 # a copy of the License at
    6 #
    7 #      http://www.apache.org/licenses/LICENSE-2.0
    8 #
    9 # Unless required by applicable law or agreed to in writing, software
   10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   12 # License for the specific language governing permissions and limitations
   13 # under the License.
   14 
   15 import abc
   16 import codecs
   17 import os.path
   18 import re
   19 import sys
   20 import uuid
   21 import weakref
   22 
   23 import ldap.controls
   24 import ldap.filter
   25 import ldappool
   26 from oslo_log import log
   27 from oslo_utils import reflection
   28 
   29 from keystone.common import driver_hints
   30 from keystone import exception
   31 from keystone.i18n import _
   32 
   33 
   34 LOG = log.getLogger(__name__)
   35 
   36 LDAP_VALUES = {'TRUE': True, 'FALSE': False}
   37 LDAP_SCOPES = {'one': ldap.SCOPE_ONELEVEL,
   38                'sub': ldap.SCOPE_SUBTREE}
   39 LDAP_DEREF = {'always': ldap.DEREF_ALWAYS,
   40               'default': None,
   41               'finding': ldap.DEREF_FINDING,
   42               'never': ldap.DEREF_NEVER,
   43               'searching': ldap.DEREF_SEARCHING}
   44 LDAP_TLS_CERTS = {'never': ldap.OPT_X_TLS_NEVER,
   45                   'demand': ldap.OPT_X_TLS_DEMAND,
   46                   'allow': ldap.OPT_X_TLS_ALLOW}
   47 
   48 
   49 # RFC 4511 (The LDAP Protocol) defines a list containing only the OID '1.1' to
   50 # indicate that no attributes should be returned besides the DN.
   51 DN_ONLY = ['1.1']
   52 
   53 _utf8_encoder = codecs.getencoder('utf-8')
   54 
   55 # FIXME(knikolla): This enables writing to the LDAP backend
   56 # Only enabled during tests and unsupported
   57 WRITABLE = False
   58 
   59 
   60 def utf8_encode(value):
   61     """Encode a basestring to UTF-8.
   62 
   63     If the string is unicode encode it to UTF-8, if the string is
   64     str then assume it's already encoded. Otherwise raise a TypeError.
   65 
   66     :param value: A basestring
   67     :returns: UTF-8 encoded version of value
   68     :raises TypeError: If value is not basestring
   69     """
   70     if isinstance(value, str):
   71         return _utf8_encoder(value)[0]
   72     elif isinstance(value, bytes):
   73         return value
   74     else:
   75         value_cls_name = reflection.get_class_name(
   76             value, fully_qualified=False)
   77         raise TypeError("value must be basestring, "
   78                         "not %s" % value_cls_name)
   79 
   80 
   81 _utf8_decoder = codecs.getdecoder('utf-8')
   82 
   83 
   84 def utf8_decode(value):
   85     """Decode a from UTF-8 into unicode.
   86 
   87     If the value is a binary string assume it's UTF-8 encoded and decode
   88     it into a unicode string. Otherwise convert the value from its
   89     type into a unicode string.
   90 
   91     :param value: value to be returned as unicode
   92     :returns: value as unicode
   93     :raises UnicodeDecodeError: for invalid UTF-8 encoding
   94     """
   95     if isinstance(value, bytes):
   96         try:
   97             return _utf8_decoder(value)[0]
   98         except UnicodeDecodeError:
   99             # NOTE(lbragstad): We could be dealing with a UUID in byte form,
  100             # which some LDAP implementations use.
  101             uuid_byte_string_length = 16
  102             if len(value) == uuid_byte_string_length:
  103                 return str(uuid.UUID(bytes_le=value))
  104             else:
  105                 raise
  106     return str(value)
  107 
  108 
  109 def py2ldap(val):
  110     """Type convert a Python value to a type accepted by LDAP (unicode).
  111 
  112     The LDAP API only accepts strings for values therefore convert
  113     the value's type to a unicode string. A subsequent type conversion
  114     will encode the unicode as UTF-8 as required by the python-ldap API,
  115     but for now we just want a string representation of the value.
  116 
  117     :param val: The value to convert to a LDAP string representation
  118     :returns: unicode string representation of value.
  119     """
  120     if isinstance(val, bool):
  121         return u'TRUE' if val else u'FALSE'
  122     else:
  123         return str(val)
  124 
  125 
  126 def enabled2py(val):
  127     """Similar to ldap2py, only useful for the enabled attribute."""
  128     try:
  129         return LDAP_VALUES[val]
  130     except KeyError:  # nosec
  131         # It wasn't a boolean value, will try as an int instead.
  132         pass
  133     try:
  134         return int(val)
  135     except ValueError:  # nosec
  136         # It wasn't an int either, will try as utf8 instead.
  137         pass
  138     return utf8_decode(val)
  139 
  140 
  141 def ldap2py(val):
  142     """Convert an LDAP formatted value to Python type used by OpenStack.
  143 
  144     Virtually all LDAP values are stored as UTF-8 encoded strings.
  145     OpenStack prefers values which are unicode friendly.
  146 
  147     :param val: LDAP formatted value
  148     :returns: val converted to preferred Python type
  149     """
  150     return utf8_decode(val)
  151 
  152 
  153 def convert_ldap_result(ldap_result):
  154     """Convert LDAP search result to Python types used by OpenStack.
  155 
  156     Each result tuple is of the form (dn, attrs), where dn is a string
  157     containing the DN (distinguished name) of the entry, and attrs is
  158     a dictionary containing the attributes associated with the
  159     entry. The keys of attrs are strings, and the associated values
  160     are lists of strings.
  161 
  162     OpenStack wants to use Python types of its choosing. Strings will
  163     be unicode, truth values boolean, whole numbers int's, etc. DN's are
  164     represented as text in python-ldap by default for Python 3 and when
  165     bytes_mode=False for Python 2, and therefore do not require decoding.
  166 
  167     :param ldap_result: LDAP search result
  168     :returns: list of 2-tuples containing (dn, attrs) where dn is unicode
  169               and attrs is a dict whose values are type converted to
  170               OpenStack preferred types.
  171     """
  172     py_result = []
  173     at_least_one_referral = False
  174     for dn, attrs in ldap_result:
  175         ldap_attrs = {}
  176         if dn is None:
  177             # this is a Referral object, rather than an Entry object
  178             at_least_one_referral = True
  179             continue
  180 
  181         for kind, values in attrs.items():
  182             try:
  183                 val2py = enabled2py if kind == 'enabled' else ldap2py
  184                 ldap_attrs[kind] = [val2py(x) for x in values]
  185             except UnicodeDecodeError:
  186                 LOG.debug('Unable to decode value for attribute %s', kind)
  187         py_result.append((dn, ldap_attrs))
  188     if at_least_one_referral:
  189         LOG.debug('Referrals were returned and ignored. Enable referral '
  190                   'chasing in keystone.conf via [ldap] chase_referrals')
  191 
  192     return py_result
  193 
  194 
  195 def safe_iter(attrs):
  196     if attrs is None:
  197         return
  198     elif isinstance(attrs, list):
  199         for e in attrs:
  200             yield e
  201     else:
  202         yield attrs
  203 
  204 
  205 def parse_deref(opt):
  206     try:
  207         return LDAP_DEREF[opt]
  208     except KeyError:
  209         raise ValueError(_('Invalid LDAP deref option: %(option)s. '
  210                            'Choose one of: %(options)s') %
  211                          {'option': opt,
  212                           'options': ', '.join(LDAP_DEREF.keys()), })
  213 
  214 
  215 def parse_tls_cert(opt):
  216     try:
  217         return LDAP_TLS_CERTS[opt]
  218     except KeyError:
  219         raise ValueError(_(
  220             'Invalid LDAP TLS certs option: %(option)s. '
  221             'Choose one of: %(options)s') % {
  222                 'option': opt,
  223                 'options': ', '.join(LDAP_TLS_CERTS.keys())})
  224 
  225 
  226 def ldap_scope(scope):
  227     try:
  228         return LDAP_SCOPES[scope]
  229     except KeyError:
  230         raise ValueError(
  231             _('Invalid LDAP scope: %(scope)s. Choose one of: %(options)s') % {
  232                 'scope': scope,
  233                 'options': ', '.join(LDAP_SCOPES.keys())})
  234 
  235 
  236 def prep_case_insensitive(value):
  237     """Prepare a string for case-insensitive comparison.
  238 
  239     This is defined in RFC4518. For simplicity, all this function does is
  240     lowercase all the characters, strip leading and trailing whitespace,
  241     and compress sequences of spaces to a single space.
  242     """
  243     value = re.sub(r'\s+', ' ', value.strip().lower())
  244     return value
  245 
  246 
  247 def is_ava_value_equal(attribute_type, val1, val2):
  248     """Return True if and only if the AVAs are equal.
  249 
  250     When comparing AVAs, the equality matching rule for the attribute type
  251     should be taken into consideration. For simplicity, this implementation
  252     does a case-insensitive comparison.
  253 
  254     Note that this function uses prep_case_insenstive so the limitations of
  255     that function apply here.
  256 
  257     """
  258     return prep_case_insensitive(val1) == prep_case_insensitive(val2)
  259 
  260 
  261 def is_rdn_equal(rdn1, rdn2):
  262     """Return True if and only if the RDNs are equal.
  263 
  264     * RDNs must have the same number of AVAs.
  265     * Each AVA of the RDNs must be the equal for the same attribute type. The
  266       order isn't significant. Note that an attribute type will only be in one
  267       AVA in an RDN, otherwise the DN wouldn't be valid.
  268     * Attribute types aren't case sensitive. Note that attribute type
  269       comparison is more complicated than implemented. This function only
  270       compares case-insentive. The code should handle multiple names for an
  271       attribute type (e.g., cn, commonName, and 2.5.4.3 are the same).
  272 
  273     Note that this function uses is_ava_value_equal to compare AVAs so the
  274     limitations of that function apply here.
  275 
  276     """
  277     if len(rdn1) != len(rdn2):
  278         return False
  279 
  280     for attr_type_1, val1, dummy in rdn1:
  281         found = False
  282         for attr_type_2, val2, dummy in rdn2:
  283             if attr_type_1.lower() != attr_type_2.lower():
  284                 continue
  285 
  286             found = True
  287             if not is_ava_value_equal(attr_type_1, val1, val2):
  288                 return False
  289             break
  290         if not found:
  291             return False
  292 
  293     return True
  294 
  295 
  296 def is_dn_equal(dn1, dn2):
  297     """Return True if and only if the DNs are equal.
  298 
  299     Two DNs are equal if they've got the same number of RDNs and if the RDNs
  300     are the same at each position. See RFC4517.
  301 
  302     Note that this function uses is_rdn_equal to compare RDNs so the
  303     limitations of that function apply here.
  304 
  305     :param dn1: Either a string DN or a DN parsed by ldap.dn.str2dn.
  306     :param dn2: Either a string DN or a DN parsed by ldap.dn.str2dn.
  307 
  308     """
  309     if not isinstance(dn1, list):
  310         dn1 = ldap.dn.str2dn(dn1)
  311     if not isinstance(dn2, list):
  312         dn2 = ldap.dn.str2dn(dn2)
  313 
  314     if len(dn1) != len(dn2):
  315         return False
  316 
  317     for rdn1, rdn2 in zip(dn1, dn2):
  318         if not is_rdn_equal(rdn1, rdn2):
  319             return False
  320     return True
  321 
  322 
  323 def dn_startswith(descendant_dn, dn):
  324     """Return True if and only if the descendant_dn is under the dn.
  325 
  326     :param descendant_dn: Either a string DN or a DN parsed by ldap.dn.str2dn.
  327     :param dn: Either a string DN or a DN parsed by ldap.dn.str2dn.
  328 
  329     """
  330     if not isinstance(descendant_dn, list):
  331         descendant_dn = ldap.dn.str2dn(descendant_dn)
  332     if not isinstance(dn, list):
  333         dn = ldap.dn.str2dn(dn)
  334 
  335     if len(descendant_dn) <= len(dn):
  336         return False
  337 
  338     # Use the last len(dn) RDNs.
  339     return is_dn_equal(descendant_dn[-len(dn):], dn)
  340 
  341 
  342 class LDAPHandler(object, metaclass=abc.ABCMeta):
  343     """Abstract class which defines methods for a LDAP API provider.
  344 
  345     Native Keystone values cannot be passed directly into and from the
  346     python-ldap API. Type conversion must occur at the LDAP API
  347     boundary, examples of type conversions are:
  348 
  349         * booleans map to the strings 'TRUE' and 'FALSE'
  350 
  351         * integer values map to their string representation.
  352 
  353         * unicode strings are encoded in UTF-8
  354 
  355     Note, in python-ldap some fields (DNs, RDNs, attribute names, queries)
  356     are represented as text (str on Python 3, unicode on Python 2 when
  357     bytes_mode=False). For more details see:
  358     http://www.python-ldap.org/en/latest/bytes_mode.html#bytes-mode
  359 
  360     In addition to handling type conversions at the API boundary we
  361     have the requirement to support more than one LDAP API
  362     provider. Currently we have:
  363 
  364         * python-ldap, this is the standard LDAP API for Python, it
  365           requires access to a live LDAP server.
  366 
  367         * Fake LDAP which emulates python-ldap. This is used for
  368           testing without requiring a live LDAP server.
  369 
  370     To support these requirements we need a layer that performs type
  371     conversions and then calls another LDAP API which is configurable
  372     (e.g. either python-ldap or the fake emulation).
  373 
  374     We have an additional constraint at the time of this writing due to
  375     limitations in the logging module. The logging module is not
  376     capable of accepting UTF-8 encoded strings, it will throw an
  377     encoding exception. Therefore all logging MUST be performed prior
  378     to UTF-8 conversion. This means no logging can be performed in the
  379     ldap APIs that implement the python-ldap API because those APIs
  380     are defined to accept only UTF-8 strings. Thus the layer which
  381     performs type conversions must also do the logging. We do the type
  382     conversions in two steps, once to convert all Python types to
  383     unicode strings, then log, then convert the unicode strings to
  384     UTF-8.
  385 
  386     There are a variety of ways one could accomplish this, we elect to
  387     use a chaining technique whereby instances of this class simply
  388     call the next member in the chain via the "conn" attribute. The
  389     chain is constructed by passing in an existing instance of this
  390     class as the conn attribute when the class is instantiated.
  391 
  392     Here is a brief explanation of why other possible approaches were
  393     not used:
  394 
  395         subclassing
  396 
  397             To perform the wrapping operations in the correct order
  398             the type conversion class would have to subclass each of
  399             the API providers. This is awkward, doubles the number of
  400             classes, and does not scale well. It requires the type
  401             conversion class to be aware of all possible API
  402             providers.
  403 
  404         decorators
  405 
  406             Decorators provide an elegant solution to wrap methods and
  407             would be an ideal way to perform type conversions before
  408             calling the wrapped function and then converting the
  409             values returned from the wrapped function. However
  410             decorators need to be aware of the method signature, it
  411             has to know what input parameters need conversion and how
  412             to convert the result. For an API like python-ldap which
  413             has a large number of different method signatures it would
  414             require a large number of specialized
  415             decorators. Experience has shown it's very easy to apply
  416             the wrong decorator due to the inherent complexity and
  417             tendency to cut-n-paste code. Another option is to
  418             parameterize the decorator to make it "smart". Experience
  419             has shown such decorators become insanely complicated and
  420             difficult to understand and debug. Also decorators tend to
  421             hide what's really going on when a method is called, the
  422             operations being performed are not visible when looking at
  423             the implemation of a decorated method, this too experience
  424             has shown leads to mistakes.
  425 
  426     Chaining simplifies both wrapping to perform type conversion as
  427     well as the substitution of alternative API providers. One simply
  428     creates a new instance of the API interface and insert it at the
  429     front of the chain. Type conversions are explicit and obvious.
  430 
  431     If a new method needs to be added to the API interface one adds it
  432     to the abstract class definition. Should one miss adding the new
  433     method to any derivations of the abstract class the code will fail
  434     to load and run making it impossible to forget updating all the
  435     derived classes.
  436 
  437     """
  438 
  439     def __init__(self, conn=None):
  440         self.conn = conn
  441 
  442     @abc.abstractmethod
  443     def connect(self, url, page_size=0, alias_dereferencing=None,
  444                 use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
  445                 tls_req_cert=ldap.OPT_X_TLS_DEMAND, chase_referrals=None,
  446                 debug_level=None, conn_timeout=None, use_pool=None,
  447                 pool_size=None, pool_retry_max=None, pool_retry_delay=None,
  448                 pool_conn_timeout=None, pool_conn_lifetime=None):
  449         raise exception.NotImplemented()  # pragma: no cover
  450 
  451     @abc.abstractmethod
  452     def set_option(self, option, invalue):
  453         raise exception.NotImplemented()  # pragma: no cover
  454 
  455     @abc.abstractmethod
  456     def get_option(self, option):
  457         raise exception.NotImplemented()  # pragma: no cover
  458 
  459     @abc.abstractmethod
  460     def simple_bind_s(self, who='', cred='',
  461                       serverctrls=None, clientctrls=None):
  462         raise exception.NotImplemented()  # pragma: no cover
  463 
  464     @abc.abstractmethod
  465     def unbind_s(self):
  466         raise exception.NotImplemented()  # pragma: no cover
  467 
  468     @abc.abstractmethod
  469     def add_s(self, dn, modlist):
  470         raise exception.NotImplemented()  # pragma: no cover
  471 
  472     @abc.abstractmethod
  473     def search_s(self, base, scope,
  474                  filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
  475         raise exception.NotImplemented()  # pragma: no cover
  476 
  477     @abc.abstractmethod
  478     def search_ext(self, base, scope,
  479                    filterstr='(objectClass=*)', attrlist=None, attrsonly=0,
  480                    serverctrls=None, clientctrls=None,
  481                    timeout=-1, sizelimit=0):
  482         raise exception.NotImplemented()  # pragma: no cover
  483 
  484     @abc.abstractmethod
  485     def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None,
  486                 resp_ctrl_classes=None):
  487         raise exception.NotImplemented()  # pragma: no cover
  488 
  489     @abc.abstractmethod
  490     def modify_s(self, dn, modlist):
  491         raise exception.NotImplemented()  # pragma: no cover
  492 
  493 
  494 class PythonLDAPHandler(LDAPHandler):
  495     """LDAPHandler implementation which calls the python-ldap API.
  496 
  497     Note, the python-ldap API requires all string attribute values to be UTF-8
  498     encoded.
  499 
  500     Note, in python-ldap some fields (DNs, RDNs, attribute names, queries)
  501     are represented as text (str on Python 3, unicode on Python 2 when
  502     bytes_mode=False). For more details see:
  503     http://www.python-ldap.org/en/latest/bytes_mode.html#bytes-mode
  504 
  505     The KeystoneLDAPHandler enforces this prior to invoking the methods in this
  506     class.
  507 
  508     """
  509 
  510     def connect(self, url, page_size=0, alias_dereferencing=None,
  511                 use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
  512                 tls_req_cert=ldap.OPT_X_TLS_DEMAND, chase_referrals=None,
  513                 debug_level=None, conn_timeout=None, use_pool=None,
  514                 pool_size=None, pool_retry_max=None, pool_retry_delay=None,
  515                 pool_conn_timeout=None, pool_conn_lifetime=None):
  516 
  517         _common_ldap_initialization(url=url,
  518                                     use_tls=use_tls,
  519                                     tls_cacertfile=tls_cacertfile,
  520                                     tls_cacertdir=tls_cacertdir,
  521                                     tls_req_cert=tls_req_cert,
  522                                     debug_level=debug_level,
  523                                     timeout=conn_timeout)
  524 
  525         self.conn = ldap.initialize(url)
  526         self.conn.protocol_version = ldap.VERSION3
  527 
  528         if alias_dereferencing is not None:
  529             self.conn.set_option(ldap.OPT_DEREF, alias_dereferencing)
  530         self.page_size = page_size
  531 
  532         if use_tls:
  533             self.conn.start_tls_s()
  534 
  535         if chase_referrals is not None:
  536             self.conn.set_option(ldap.OPT_REFERRALS, int(chase_referrals))
  537 
  538     def set_option(self, option, invalue):
  539         return self.conn.set_option(option, invalue)
  540 
  541     def get_option(self, option):
  542         return self.conn.get_option(option)
  543 
  544     def simple_bind_s(self, who='', cred='',
  545                       serverctrls=None, clientctrls=None):
  546         return self.conn.simple_bind_s(who, cred, serverctrls, clientctrls)
  547 
  548     def unbind_s(self):
  549         return self.conn.unbind_s()
  550 
  551     def add_s(self, dn, modlist):
  552         return self.conn.add_s(dn, modlist)
  553 
  554     def search_s(self, base, scope,
  555                  filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
  556         return self.conn.search_s(base, scope, filterstr,
  557                                   attrlist, attrsonly)
  558 
  559     def search_ext(self, base, scope,
  560                    filterstr='(objectClass=*)', attrlist=None, attrsonly=0,
  561                    serverctrls=None, clientctrls=None,
  562                    timeout=-1, sizelimit=0):
  563         return self.conn.search_ext(base, scope,
  564                                     filterstr, attrlist, attrsonly,
  565                                     serverctrls, clientctrls,
  566                                     timeout, sizelimit)
  567 
  568     def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None,
  569                 resp_ctrl_classes=None):
  570         # The resp_ctrl_classes parameter is a recent addition to the
  571         # API. It defaults to None. We do not anticipate using it.
  572         # To run with older versions of python-ldap we do not pass it.
  573         return self.conn.result3(msgid, all, timeout)
  574 
  575     def modify_s(self, dn, modlist):
  576         return self.conn.modify_s(dn, modlist)
  577 
  578 
  579 def _common_ldap_initialization(url, use_tls=False, tls_cacertfile=None,
  580                                 tls_cacertdir=None, tls_req_cert=None,
  581                                 debug_level=None, timeout=None):
  582     """LDAP initialization for PythonLDAPHandler and PooledLDAPHandler."""
  583     LOG.debug('LDAP init: url=%s', url)
  584     LOG.debug('LDAP init: use_tls=%s tls_cacertfile=%s tls_cacertdir=%s '
  585               'tls_req_cert=%s tls_avail=%s',
  586               use_tls, tls_cacertfile, tls_cacertdir,
  587               tls_req_cert, ldap.TLS_AVAIL)
  588 
  589     if debug_level is not None:
  590         ldap.set_option(ldap.OPT_DEBUG_LEVEL, debug_level)
  591 
  592     using_ldaps = url.lower().startswith("ldaps")
  593 
  594     if timeout is not None and timeout > 0:
  595         # set network connection timeout
  596         ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, timeout)
  597 
  598     if use_tls and using_ldaps:
  599         raise AssertionError(_('Invalid TLS / LDAPS combination'))
  600 
  601     # The certificate trust options apply for both LDAPS and TLS.
  602     if use_tls or using_ldaps:
  603         if not ldap.TLS_AVAIL:
  604             raise ValueError(_('Invalid LDAP TLS_AVAIL option: %s. TLS '
  605                                'not available') % ldap.TLS_AVAIL)
  606         if tls_cacertfile:
  607             # NOTE(topol)
  608             # python ldap TLS does not verify CACERTFILE or CACERTDIR
  609             # so we add some extra simple sanity check verification
  610             # Also, setting these values globally (i.e. on the ldap object)
  611             # works but these values are ignored when setting them on the
  612             # connection
  613             if not os.path.isfile(tls_cacertfile):
  614                 raise IOError(_("tls_cacertfile %s not found "
  615                                 "or is not a file") %
  616                               tls_cacertfile)
  617             ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, tls_cacertfile)
  618         elif tls_cacertdir:
  619             # NOTE(topol)
  620             # python ldap TLS does not verify CACERTFILE or CACERTDIR
  621             # so we add some extra simple sanity check verification
  622             # Also, setting these values globally (i.e. on the ldap object)
  623             # works but these values are ignored when setting them on the
  624             # connection
  625             if not os.path.isdir(tls_cacertdir):
  626                 raise IOError(_("tls_cacertdir %s not found "
  627                                 "or is not a directory") %
  628                               tls_cacertdir)
  629             ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, tls_cacertdir)
  630         if tls_req_cert in list(LDAP_TLS_CERTS.values()):
  631             ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_cert)
  632         else:
  633             LOG.debug('LDAP TLS: invalid TLS_REQUIRE_CERT Option=%s',
  634                       tls_req_cert)
  635 
  636 
  637 class AsynchronousMessage(object):
  638     """A container for handling asynchronous LDAP responses.
  639 
  640     Some LDAP APIs, like `search_ext`, are asynchronous and return a message ID
  641     when the server successfully initiates the operation. Clients can use this
  642     message ID and the original connection to make the request to fetch the
  643     results using `result3`.
  644 
  645     This object holds the message ID, the original connection, and a callable
  646     weak reference Finalizer that cleans up context managers specific to the
  647     connection associated to the message ID.
  648 
  649     :param message_id: The message identifier (str).
  650     :param connection: The connection associated with the message identifier
  651                        (ldappool.StateConnector).
  652 
  653     The `clean` attribute is a callable that cleans up the context manager used
  654     to create or return the connection object (weakref.finalize).
  655 
  656     """
  657 
  658     def __init__(self, message_id, connection, context_manager):
  659         self.id = message_id
  660         self.connection = connection
  661         self.clean = weakref.finalize(
  662             self, self._cleanup_connection_context_manager, context_manager
  663         )
  664 
  665     def _cleanup_connection_context_manager(self, context_manager):
  666         context_manager.__exit__(None, None, None)
  667 
  668 
  669 def use_conn_pool(func):
  670     """Use this only for connection pool specific ldap API.
  671 
  672     This adds connection object to decorated API as next argument after self.
  673 
  674     """
  675     def wrapper(self, *args, **kwargs):
  676         # assert isinstance(self, PooledLDAPHandler)
  677         with self._get_pool_connection() as conn:
  678             self._apply_options(conn)
  679             return func(self, conn, *args, **kwargs)
  680     return wrapper
  681 
  682 
  683 class PooledLDAPHandler(LDAPHandler):
  684     """LDAPHandler implementation which uses pooled connection manager.
  685 
  686     Pool specific configuration is defined in [ldap] section.
  687     All other LDAP configuration is still used from [ldap] section
  688 
  689     Keystone LDAP authentication logic authenticates an end user using its DN
  690     and password via LDAP bind to establish supplied password is correct.
  691     This can fill up the pool quickly (as pool re-uses existing connection
  692     based on its bind data) and would not leave space in pool for connection
  693     re-use for other LDAP operations.
  694     Now a separate pool can be established for those requests when related flag
  695     'use_auth_pool' is enabled. That pool can have its own size and
  696     connection lifetime. Other pool attributes are shared between those pools.
  697     If 'use_pool' is disabled, then 'use_auth_pool' does not matter.
  698     If 'use_auth_pool' is not enabled, then connection pooling is not used for
  699     those LDAP operations.
  700 
  701     Note, the python-ldap API requires all string attribute values to be UTF-8
  702     encoded. The KeystoneLDAPHandler enforces this prior to invoking the
  703     methods in this class.
  704 
  705     Note, in python-ldap some fields (DNs, RDNs, attribute names, queries)
  706     are represented as text (str on Python 3, unicode on Python 2 when
  707     bytes_mode=False). For more details see:
  708     http://www.python-ldap.org/en/latest/bytes_mode.html#bytes-mode
  709 
  710     """
  711 
  712     # Added here to allow override for testing
  713     Connector = ldappool.StateConnector
  714     auth_pool_prefix = 'auth_pool_'
  715 
  716     connection_pools = {}  # static connector pool dict
  717 
  718     def __init__(self, conn=None, use_auth_pool=False):
  719         super(PooledLDAPHandler, self).__init__(conn=conn)
  720         self.who = ''
  721         self.cred = ''
  722         self.conn_options = {}  # connection specific options
  723         self.page_size = None
  724         self.use_auth_pool = use_auth_pool
  725         self.conn_pool = None
  726 
  727     def connect(self, url, page_size=0, alias_dereferencing=None,
  728                 use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
  729                 tls_req_cert=ldap.OPT_X_TLS_DEMAND, chase_referrals=None,
  730                 debug_level=None, conn_timeout=None, use_pool=None,
  731                 pool_size=None, pool_retry_max=None, pool_retry_delay=None,
  732                 pool_conn_timeout=None, pool_conn_lifetime=None):
  733 
  734         _common_ldap_initialization(url=url,
  735                                     use_tls=use_tls,
  736                                     tls_cacertfile=tls_cacertfile,
  737                                     tls_cacertdir=tls_cacertdir,
  738                                     tls_req_cert=tls_req_cert,
  739                                     debug_level=debug_level,
  740                                     timeout=pool_conn_timeout)
  741 
  742         self.page_size = page_size
  743 
  744         # Following two options are not added in common initialization as they
  745         # need to follow a sequence in PythonLDAPHandler code.
  746         if alias_dereferencing is not None:
  747             self.set_option(ldap.OPT_DEREF, alias_dereferencing)
  748         if chase_referrals is not None:
  749             self.set_option(ldap.OPT_REFERRALS, int(chase_referrals))
  750 
  751         if self.use_auth_pool:  # separate pool when use_auth_pool enabled
  752             pool_url = self.auth_pool_prefix + url
  753         else:
  754             pool_url = url
  755         try:
  756             self.conn_pool = self.connection_pools[pool_url]
  757         except KeyError:
  758             self.conn_pool = ldappool.ConnectionManager(
  759                 url,
  760                 size=pool_size,
  761                 retry_max=pool_retry_max,
  762                 retry_delay=pool_retry_delay,
  763                 timeout=pool_conn_timeout,
  764                 connector_cls=self.Connector,
  765                 use_tls=use_tls,
  766                 max_lifetime=pool_conn_lifetime)
  767             self.connection_pools[pool_url] = self.conn_pool
  768 
  769     def set_option(self, option, invalue):
  770         self.conn_options[option] = invalue
  771 
  772     def get_option(self, option):
  773         value = self.conn_options.get(option)
  774         # if option was not specified explicitly, then use connection default
  775         # value for that option if there.
  776         if value is None:
  777             with self._get_pool_connection() as conn:
  778                 value = conn.get_option(option)
  779         return value
  780 
  781     def _apply_options(self, conn):
  782         # if connection has a lifetime, then it already has options specified
  783         if conn.get_lifetime() > 30:
  784             return
  785         for option, invalue in self.conn_options.items():
  786             conn.set_option(option, invalue)
  787 
  788     def _get_pool_connection(self):
  789         return self.conn_pool.connection(self.who, self.cred)
  790 
  791     def simple_bind_s(self, who='', cred='',
  792                       serverctrls=None, clientctrls=None):
  793         # Not using use_conn_pool decorator here as this API takes cred as
  794         # input.
  795         self.who = who
  796         self.cred = cred
  797         with self._get_pool_connection() as conn:
  798             self._apply_options(conn)
  799 
  800     def unbind_s(self):
  801         # After connection generator is done `with` statement execution block
  802         # connection is always released via finally block in ldappool.
  803         # So this unbind is a no op.
  804         pass
  805 
  806     @use_conn_pool
  807     def add_s(self, conn, dn, modlist):
  808         return conn.add_s(dn, modlist)
  809 
  810     @use_conn_pool
  811     def search_s(self, conn, base, scope,
  812                  filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
  813         return conn.search_s(base, scope, filterstr, attrlist,
  814                              attrsonly)
  815 
  816     def search_ext(self, base, scope,
  817                    filterstr='(objectClass=*)', attrlist=None, attrsonly=0,
  818                    serverctrls=None, clientctrls=None,
  819                    timeout=-1, sizelimit=0):
  820         """Return an AsynchronousMessage instance, it asynchronous API.
  821 
  822         The AsynchronousMessage instance can be safely used in a call to
  823         `result3()`.
  824 
  825         To work with `result3()` API in predictable manner, the same LDAP
  826         connection is needed which originally provided the `msgid`. So, this
  827         method wraps the existing connection and `msgid` in a new
  828         `AsynchronousMessage` instance. The connection associated with
  829         `search_ext()` is released after `result3()` fetches the data
  830         associated with `msgid`.
  831 
  832         """
  833         conn_ctxt = self._get_pool_connection()
  834         conn = conn_ctxt.__enter__()
  835         try:
  836             msgid = conn.search_ext(base, scope,
  837                                     filterstr, attrlist, attrsonly,
  838                                     serverctrls, clientctrls,
  839                                     timeout, sizelimit)
  840         except Exception:
  841             conn_ctxt.__exit__(*sys.exc_info())
  842             raise
  843         return AsynchronousMessage(msgid, conn, conn_ctxt)
  844 
  845     def result3(self, message, all=1, timeout=None,
  846                 resp_ctrl_classes=None):
  847         """Wait for and return the result to an asynchronous message.
  848 
  849         This method returns the result of an operation previously initiated by
  850         one of the LDAP asynchronous operation routines (e.g., `search_ext()`).
  851         The `search_ext()` method in python-ldap returns an invocation
  852         identifier, or a message ID, upon successful initiation of the
  853         operation by the LDAP server.
  854 
  855         The `message` is expected to be instance of class
  856         `AsynchronousMessage`, which contains the message ID and the connection
  857         used to make the original request.
  858 
  859         The connection and context manager associated with `search_ext()` are
  860         cleaned up when message.clean() is called.
  861 
  862         """
  863         results = message.connection.result3(message.id, all, timeout)
  864 
  865         # Now that we have the results from the LDAP server for the message, we
  866         # don't need the the context manager used to create the connection.
  867         message.clean()
  868 
  869         return results
  870 
  871     @use_conn_pool
  872     def modify_s(self, conn, dn, modlist):
  873         return conn.modify_s(dn, modlist)
  874 
  875 
  876 class KeystoneLDAPHandler(LDAPHandler):
  877     """Convert data types and perform logging.
  878 
  879     This LDAP interface wraps the python-ldap based interfaces. The
  880     python-ldap interfaces require string values encoded in UTF-8 with
  881     the exception of [1]. The OpenStack logging framework at the time
  882     of this writing is not capable of accepting strings encoded in
  883     UTF-8, the log functions will throw decoding errors if a non-ascii
  884     character appears in a string.
  885 
  886     [1] In python-ldap, some fields (DNs, RDNs, attribute names,
  887     queries) are represented as text (str on Python 3, unicode on
  888     Python 2 when bytes_mode=False). For more details see:
  889     http://www.python-ldap.org/en/latest/bytes_mode.html#bytes-mode
  890 
  891     Prior to the call Python data types are converted to a string
  892     representation as required by the LDAP APIs.
  893 
  894     Then logging is performed so we can track what is being
  895     sent/received from LDAP. Also the logging filters security
  896     sensitive items (i.e. passwords).
  897 
  898     Then the string values are encoded into UTF-8.
  899 
  900     Then the LDAP API entry point is invoked.
  901 
  902     Data returned from the LDAP call is converted back from UTF-8
  903     encoded strings into the Python data type used internally in
  904     OpenStack.
  905 
  906     """
  907 
  908     def __init__(self, conn=None):
  909         super(KeystoneLDAPHandler, self).__init__(conn=conn)
  910         self.page_size = 0
  911 
  912     def __enter__(self):
  913         """Enter runtime context."""
  914         return self
  915 
  916     def _disable_paging(self):
  917         # Disable the pagination from now on
  918         self.page_size = 0
  919 
  920     def connect(self, url, page_size=0, alias_dereferencing=None,
  921                 use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
  922                 tls_req_cert=ldap.OPT_X_TLS_DEMAND, chase_referrals=None,
  923                 debug_level=None, conn_timeout=None, use_pool=None,
  924                 pool_size=None, pool_retry_max=None, pool_retry_delay=None,
  925                 pool_conn_timeout=None, pool_conn_lifetime=None):
  926         self.page_size = page_size
  927         return self.conn.connect(url, page_size, alias_dereferencing,
  928                                  use_tls, tls_cacertfile, tls_cacertdir,
  929                                  tls_req_cert, chase_referrals,
  930                                  debug_level=debug_level,
  931                                  conn_timeout=conn_timeout,
  932                                  use_pool=use_pool,
  933                                  pool_size=pool_size,
  934                                  pool_retry_max=pool_retry_max,
  935                                  pool_retry_delay=pool_retry_delay,
  936                                  pool_conn_timeout=pool_conn_timeout,
  937                                  pool_conn_lifetime=pool_conn_lifetime)
  938 
  939     def set_option(self, option, invalue):
  940         return self.conn.set_option(option, invalue)
  941 
  942     def get_option(self, option):
  943         return self.conn.get_option(option)
  944 
  945     def simple_bind_s(self, who='', cred='',
  946                       serverctrls=None, clientctrls=None):
  947         LOG.debug('LDAP bind: who=%s', who)
  948         return self.conn.simple_bind_s(who, cred,
  949                                        serverctrls=serverctrls,
  950                                        clientctrls=clientctrls)
  951 
  952     def unbind_s(self):
  953         LOG.debug('LDAP unbind')
  954         return self.conn.unbind_s()
  955 
  956     def add_s(self, dn, modlist):
  957         ldap_attrs = [(kind, [py2ldap(x) for x in safe_iter(values)])
  958                       for kind, values in modlist]
  959         logging_attrs = [(kind, values
  960                          if kind != 'userPassword'
  961                          else ['****'])
  962                          for kind, values in ldap_attrs]
  963         LOG.debug('LDAP add: dn=%s attrs=%s',
  964                   dn, logging_attrs)
  965         ldap_attrs_utf8 = [(kind, [utf8_encode(x) for x in safe_iter(values)])
  966                            for kind, values in ldap_attrs]
  967         return self.conn.add_s(dn, ldap_attrs_utf8)
  968 
  969     def search_s(self, base, scope,
  970                  filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
  971         # NOTE(morganfainberg): Remove "None" singletons from this list, which
  972         # allows us to set mapped attributes to "None" as defaults in config.
  973         # Without this filtering, the ldap query would raise a TypeError since
  974         # attrlist is expected to be an iterable of strings.
  975         if attrlist is not None:
  976             attrlist = [attr for attr in attrlist if attr is not None]
  977         LOG.debug('LDAP search: base=%s scope=%s filterstr=%s '
  978                   'attrs=%s attrsonly=%s',
  979                   base, scope, filterstr, attrlist, attrsonly)
  980         if self.page_size:
  981             ldap_result = self._paged_search_s(base, scope,
  982                                                filterstr, attrlist)
  983         else:
  984             try:
  985                 ldap_result = self.conn.search_s(base, scope, filterstr,
  986                                                  attrlist, attrsonly)
  987             except ldap.SIZELIMIT_EXCEEDED:
  988                 raise exception.LDAPSizeLimitExceeded()
  989 
  990         py_result = convert_ldap_result(ldap_result)
  991 
  992         return py_result
  993 
  994     def search_ext(self, base, scope,
  995                    filterstr='(objectClass=*)', attrlist=None, attrsonly=0,
  996                    serverctrls=None, clientctrls=None,
  997                    timeout=-1, sizelimit=0):
  998         if attrlist is not None:
  999             attrlist = [attr for attr in attrlist if attr is not None]
 1000         LOG.debug('LDAP search_ext: base=%s scope=%s filterstr=%s '
 1001                   'attrs=%s attrsonly=%s '
 1002                   'serverctrls=%s clientctrls=%s timeout=%s sizelimit=%s',
 1003                   base, scope, filterstr, attrlist, attrsonly,
 1004                   serverctrls, clientctrls, timeout, sizelimit)
 1005         return self.conn.search_ext(base, scope,
 1006                                     filterstr, attrlist, attrsonly,
 1007                                     serverctrls, clientctrls,
 1008                                     timeout, sizelimit)
 1009 
 1010     def _paged_search_s(self, base, scope, filterstr, attrlist=None):
 1011         res = []
 1012         use_old_paging_api = False
 1013         # The API for the simple paged results control changed between
 1014         # python-ldap 2.3 and 2.4.  We need to detect the capabilities
 1015         # of the python-ldap version we are using.
 1016         if hasattr(ldap, 'LDAP_CONTROL_PAGE_OID'):
 1017             use_old_paging_api = True
 1018             lc = ldap.controls.SimplePagedResultsControl(
 1019                 controlType=ldap.LDAP_CONTROL_PAGE_OID,
 1020                 criticality=True,
 1021                 controlValue=(self.page_size, ''))
 1022             page_ctrl_oid = ldap.LDAP_CONTROL_PAGE_OID
 1023         else:
 1024             lc = ldap.controls.libldap.SimplePagedResultsControl(
 1025                 criticality=True,
 1026                 size=self.page_size,
 1027                 cookie='')
 1028             page_ctrl_oid = ldap.controls.SimplePagedResultsControl.controlType
 1029 
 1030         message = self.conn.search_ext(base,
 1031                                        scope,
 1032                                        filterstr,
 1033                                        attrlist,
 1034                                        serverctrls=[lc])
 1035         # Endless loop request pages on ldap server until it has no data
 1036         while True:
 1037             # Request to the ldap server a page with 'page_size' entries
 1038             rtype, rdata, rmsgid, serverctrls = self.conn.result3(message)
 1039             # Receive the data
 1040             res.extend(rdata)
 1041             pctrls = [c for c in serverctrls
 1042                       if c.controlType == page_ctrl_oid]
 1043             if pctrls:
 1044                 # LDAP server supports pagination
 1045                 if use_old_paging_api:
 1046                     est, cookie = pctrls[0].controlValue
 1047                     lc.controlValue = (self.page_size, cookie)
 1048                 else:
 1049                     cookie = lc.cookie = pctrls[0].cookie
 1050 
 1051                 if cookie:
 1052                     # There is more data still on the server
 1053                     # so we request another page
 1054                     message = self.conn.search_ext(base,
 1055                                                    scope,
 1056                                                    filterstr,
 1057                                                    attrlist,
 1058                                                    serverctrls=[lc])
 1059                 else:
 1060                     # Exit condition no more data on server
 1061                     break
 1062             else:
 1063                 LOG.warning('LDAP Server does not support paging. '
 1064                             'Disable paging in keystone.conf to '
 1065                             'avoid this message.')
 1066                 self._disable_paging()
 1067                 break
 1068         return res
 1069 
 1070     def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None,
 1071                 resp_ctrl_classes=None):
 1072         ldap_result = self.conn.result3(msgid, all, timeout, resp_ctrl_classes)
 1073 
 1074         LOG.debug('LDAP result3: msgid=%s all=%s timeout=%s '
 1075                   'resp_ctrl_classes=%s ldap_result=%s',
 1076                   msgid, all, timeout, resp_ctrl_classes, ldap_result)
 1077 
 1078         # ldap_result returned from result3 is a tuple of
 1079         # (rtype, rdata, rmsgid, serverctrls). We don't need use of these,
 1080         # except rdata.
 1081         rtype, rdata, rmsgid, serverctrls = ldap_result
 1082         py_result = convert_ldap_result(rdata)
 1083         return py_result
 1084 
 1085     def modify_s(self, dn, modlist):
 1086         ldap_modlist = [
 1087             (op, kind, (None if values is None
 1088                         else [py2ldap(x) for x in safe_iter(values)]))
 1089             for op, kind, values in modlist]
 1090 
 1091         logging_modlist = [(op, kind, (values if kind != 'userPassword'
 1092                            else ['****']))
 1093                            for op, kind, values in ldap_modlist]
 1094         LOG.debug('LDAP modify: dn=%s modlist=%s',
 1095                   dn, logging_modlist)
 1096 
 1097         ldap_modlist_utf8 = [
 1098             (op, kind, (None if values is None
 1099                         else [utf8_encode(x) for x in safe_iter(values)]))
 1100             for op, kind, values in ldap_modlist]
 1101         return self.conn.modify_s(dn, ldap_modlist_utf8)
 1102 
 1103     def __exit__(self, exc_type, exc_val, exc_tb):
 1104         """Exit runtime context, unbind LDAP."""
 1105         self.unbind_s()
 1106 
 1107 
 1108 _HANDLERS = {}
 1109 
 1110 
 1111 def register_handler(prefix, handler):
 1112     _HANDLERS[prefix] = handler
 1113 
 1114 
 1115 def _get_connection(conn_url, use_pool=False, use_auth_pool=False):
 1116     for prefix, handler in _HANDLERS.items():
 1117         if conn_url.startswith(prefix):
 1118             return handler()
 1119 
 1120     if use_pool:
 1121         return PooledLDAPHandler(use_auth_pool=use_auth_pool)
 1122     else:
 1123         return PythonLDAPHandler()
 1124 
 1125 
 1126 def filter_entity(entity_ref):
 1127     """Filter out private items in an entity dict.
 1128 
 1129     :param entity_ref:  the entity dictionary. The 'dn' field will be removed.
 1130         'dn' is used in LDAP, but should not be returned to the user.  This
 1131         value may be modified.
 1132 
 1133     :returns: entity_ref
 1134 
 1135     """
 1136     if entity_ref:
 1137         entity_ref.pop('dn', None)
 1138     return entity_ref
 1139 
 1140 
 1141 class BaseLdap(object):
 1142     DEFAULT_OU = None
 1143     DEFAULT_STRUCTURAL_CLASSES = None
 1144     DEFAULT_ID_ATTR = 'cn'
 1145     DEFAULT_OBJECTCLASS = None
 1146     DEFAULT_FILTER = None
 1147     DEFAULT_EXTRA_ATTR_MAPPING = []
 1148     NotFound = None
 1149     notfound_arg = None
 1150     options_name = None
 1151     model = None
 1152     attribute_options_names = {}
 1153     immutable_attrs = []
 1154     attribute_ignore = []
 1155     tree_dn = None
 1156 
 1157     def __init__(self, conf):
 1158         self.LDAP_URL = conf.ldap.url
 1159         self.LDAP_USER = conf.ldap.user
 1160         self.LDAP_PASSWORD = conf.ldap.password
 1161         self.LDAP_SCOPE = ldap_scope(conf.ldap.query_scope)
 1162         self.alias_dereferencing = parse_deref(conf.ldap.alias_dereferencing)
 1163         self.page_size = conf.ldap.page_size
 1164         self.use_tls = conf.ldap.use_tls
 1165         self.tls_cacertfile = conf.ldap.tls_cacertfile
 1166         self.tls_cacertdir = conf.ldap.tls_cacertdir
 1167         self.tls_req_cert = parse_tls_cert(conf.ldap.tls_req_cert)
 1168         self.attribute_mapping = {}
 1169         self.chase_referrals = conf.ldap.chase_referrals
 1170         self.debug_level = conf.ldap.debug_level
 1171         self.conn_timeout = conf.ldap.connection_timeout
 1172 
 1173         # LDAP Pool specific attribute
 1174         self.use_pool = conf.ldap.use_pool
 1175         self.pool_size = conf.ldap.pool_size
 1176         self.pool_retry_max = conf.ldap.pool_retry_max
 1177         self.pool_retry_delay = conf.ldap.pool_retry_delay
 1178         self.pool_conn_timeout = conf.ldap.pool_connection_timeout
 1179         self.pool_conn_lifetime = conf.ldap.pool_connection_lifetime
 1180 
 1181         # End user authentication pool specific config attributes
 1182         self.use_auth_pool = self.use_pool and conf.ldap.use_auth_pool
 1183         self.auth_pool_size = conf.ldap.auth_pool_size
 1184         self.auth_pool_conn_lifetime = conf.ldap.auth_pool_connection_lifetime
 1185 
 1186         if self.options_name is not None:
 1187             self.tree_dn = (
 1188                 getattr(conf.ldap, '%s_tree_dn' % self.options_name)
 1189                 or '%s,%s' % (self.DEFAULT_OU, conf.ldap.suffix))
 1190 
 1191             idatt = '%s_id_attribute' % self.options_name
 1192             self.id_attr = getattr(conf.ldap, idatt) or self.DEFAULT_ID_ATTR
 1193 
 1194             objclass = '%s_objectclass' % self.options_name
 1195             self.object_class = (getattr(conf.ldap, objclass)
 1196                                  or self.DEFAULT_OBJECTCLASS)
 1197 
 1198             for k, v in self.attribute_options_names.items():
 1199                 v = '%s_%s_attribute' % (self.options_name, v)
 1200                 self.attribute_mapping[k] = getattr(conf.ldap, v)
 1201 
 1202             attr_mapping_opt = ('%s_additional_attribute_mapping' %
 1203                                 self.options_name)
 1204             attr_mapping = (getattr(conf.ldap, attr_mapping_opt)
 1205                             or self.DEFAULT_EXTRA_ATTR_MAPPING)
 1206             self.extra_attr_mapping = self._parse_extra_attrs(attr_mapping)
 1207 
 1208             ldap_filter = '%s_filter' % self.options_name
 1209             self.ldap_filter = getattr(conf.ldap,
 1210                                        ldap_filter) or self.DEFAULT_FILTER
 1211 
 1212             member_attribute = '%s_member_attribute' % self.options_name
 1213             self.member_attribute = getattr(conf.ldap, member_attribute, None)
 1214 
 1215             self.structural_classes = self.DEFAULT_STRUCTURAL_CLASSES
 1216 
 1217             if self.notfound_arg is None:
 1218                 self.notfound_arg = self.options_name + '_id'
 1219 
 1220             attribute_ignore = '%s_attribute_ignore' % self.options_name
 1221             self.attribute_ignore = getattr(conf.ldap, attribute_ignore)
 1222 
 1223     def _not_found(self, object_id):
 1224         if self.NotFound is None:
 1225             return exception.NotFound(target=object_id)
 1226         else:
 1227             return self.NotFound(**{self.notfound_arg: object_id})
 1228 
 1229     @staticmethod
 1230     def _parse_extra_attrs(option_list):
 1231         mapping = {}
 1232         for item in option_list:
 1233             try:
 1234                 ldap_attr, attr_map = item.split(':')
 1235             except ValueError:
 1236                 LOG.warning(
 1237                     'Invalid additional attribute mapping: "%s". '
 1238                     'Format must be <ldap_attribute>:<keystone_attribute>',
 1239                     item)
 1240                 continue
 1241             mapping[ldap_attr] = attr_map
 1242         return mapping
 1243 
 1244     def get_connection(self, user=None, password=None, end_user_auth=False):
 1245         use_pool = self.use_pool
 1246         pool_size = self.pool_size
 1247         pool_conn_lifetime = self.pool_conn_lifetime
 1248 
 1249         if end_user_auth:
 1250             if not self.use_auth_pool:
 1251                 use_pool = False
 1252             else:
 1253                 pool_size = self.auth_pool_size
 1254                 pool_conn_lifetime = self.auth_pool_conn_lifetime
 1255 
 1256         conn = _get_connection(self.LDAP_URL, use_pool,
 1257                                use_auth_pool=end_user_auth)
 1258 
 1259         conn = KeystoneLDAPHandler(conn=conn)
 1260 
 1261         # The LDAP server may be down or a connection may not
 1262         # exist. If that is the case, the bind attempt will
 1263         # fail with a server down exception.
 1264         try:
 1265             conn.connect(self.LDAP_URL,
 1266                          page_size=self.page_size,
 1267                          alias_dereferencing=self.alias_dereferencing,
 1268                          use_tls=self.use_tls,
 1269                          tls_cacertfile=self.tls_cacertfile,
 1270                          tls_cacertdir=self.tls_cacertdir,
 1271                          tls_req_cert=self.tls_req_cert,
 1272                          chase_referrals=self.chase_referrals,
 1273                          debug_level=self.debug_level,
 1274                          conn_timeout=self.conn_timeout,
 1275                          use_pool=use_pool,
 1276                          pool_size=pool_size,
 1277                          pool_retry_max=self.pool_retry_max,
 1278                          pool_retry_delay=self.pool_retry_delay,
 1279                          pool_conn_timeout=self.pool_conn_timeout,
 1280                          pool_conn_lifetime=pool_conn_lifetime)
 1281 
 1282             if user is None:
 1283                 user = self.LDAP_USER
 1284 
 1285             if password is None:
 1286                 password = self.LDAP_PASSWORD
 1287 
 1288             # not all LDAP servers require authentication, so we don't bind
 1289             # if we don't have any user/pass
 1290             if user and password:
 1291                 conn.simple_bind_s(user, password)
 1292             else:
 1293                 conn.simple_bind_s()
 1294 
 1295             return conn
 1296         except ldap.INVALID_CREDENTIALS:
 1297             raise exception.LDAPInvalidCredentialsError()
 1298         except ldap.SERVER_DOWN:
 1299             raise exception.LDAPServerConnectionError(
 1300                 url=self.LDAP_URL)
 1301 
 1302     def _id_to_dn_string(self, object_id):
 1303         return u'%s=%s,%s' % (self.id_attr,
 1304                               ldap.dn.escape_dn_chars(
 1305                                   str(object_id)),
 1306                               self.tree_dn)
 1307 
 1308     def _id_to_dn(self, object_id):
 1309         if self.LDAP_SCOPE == ldap.SCOPE_ONELEVEL:
 1310             return self._id_to_dn_string(object_id)
 1311         with self.get_connection() as conn:
 1312             search_result = conn.search_s(
 1313                 self.tree_dn, self.LDAP_SCOPE,
 1314                 u'(&(%(id_attr)s=%(id)s)(objectclass=%(objclass)s))' %
 1315                 {'id_attr': self.id_attr,
 1316                  'id': ldap.filter.escape_filter_chars(
 1317                      str(object_id)),
 1318                  'objclass': self.object_class},
 1319                 attrlist=DN_ONLY)
 1320         if search_result:
 1321             dn, attrs = search_result[0]
 1322             return dn
 1323         else:
 1324             return self._id_to_dn_string(object_id)
 1325 
 1326     def _dn_to_id(self, dn):
 1327         # Check if the naming attribute in the DN is the same as keystone's
 1328         # configured 'id' attribute'.  If so, extract the ID value from the DN
 1329         if self.id_attr == ldap.dn.str2dn(dn)[0][0][0].lower():
 1330             return ldap.dn.str2dn(dn)[0][0][1]
 1331         else:
 1332             # The 'ID' attribute is NOT in the DN, so we need to perform an
 1333             # LDAP search to look it up from the user entry itself.
 1334             with self.get_connection() as conn:
 1335                 search_result = conn.search_s(dn, ldap.SCOPE_BASE)
 1336 
 1337             if search_result:
 1338                 try:
 1339                     id_list = search_result[0][1][self.id_attr]
 1340                 except KeyError:
 1341                     message = ('ID attribute %(id_attr)s not found in LDAP '
 1342                                'object %(dn)s.') % ({'id_attr': self.id_attr,
 1343                                                      'dn': search_result})
 1344                     LOG.warning(message)
 1345                     raise exception.NotFound(message=message)
 1346                 if len(id_list) > 1:
 1347                     message = ('In order to keep backward compatibility, in '
 1348                                'the case of multivalued ids, we are '
 1349                                'returning the first id %(id_attr)s in the '
 1350                                'DN.') % ({'id_attr': id_list[0]})
 1351                     LOG.warning(message)
 1352                 return id_list[0]
 1353             else:
 1354                 message = _('DN attribute %(dn)s not found in LDAP') % (
 1355                     {'dn': dn})
 1356                 raise exception.NotFound(message=message)
 1357 
 1358     def _ldap_res_to_model(self, res):
 1359         # LDAP attribute names may be returned in a different case than
 1360         # they are defined in the mapping, so we need to check for keys
 1361         # in a case-insensitive way.  We use the case specified in the
 1362         # mapping for the model to ensure we have a predictable way of
 1363         # retrieving values later.
 1364         lower_res = {k.lower(): v for k, v in res[1].items()}
 1365 
 1366         id_attrs = lower_res.get(self.id_attr.lower())
 1367         if not id_attrs:
 1368             message = _('ID attribute %(id_attr)s not found in LDAP '
 1369                         'object %(dn)s') % ({'id_attr': self.id_attr,
 1370                                              'dn': res[0]})
 1371             raise exception.NotFound(message=message)
 1372         if len(id_attrs) > 1:
 1373             # FIXME(gyee): if this is a multi-value attribute and it has
 1374             # multiple values, we can't use it as ID. Retain the dn_to_id
 1375             # logic here so it does not potentially break existing
 1376             # deployments. We need to fix our read-write LDAP logic so
 1377             # it does not get the ID from DN.
 1378             message = ('ID attribute %(id_attr)s for LDAP object %(dn)s '
 1379                        'has multiple values and therefore cannot be used '
 1380                        'as an ID. Will get the ID from DN instead') % (
 1381                            {'id_attr': self.id_attr, 'dn': res[0]})
 1382             LOG.warning(message)
 1383             id_val = self._dn_to_id(res[0])
 1384         else:
 1385             id_val = id_attrs[0]
 1386         obj = self.model(id=id_val)
 1387 
 1388         for k in obj.known_keys:
 1389             if k in self.attribute_ignore:
 1390                 continue
 1391 
 1392             try:
 1393                 map_attr = self.attribute_mapping.get(k, k)
 1394                 if map_attr is None:
 1395                     # Ignore attributes that are mapped to None.
 1396                     continue
 1397 
 1398                 v = lower_res[map_attr.lower()]
 1399             except KeyError:  # nosec
 1400                 # Didn't find the attr, so don't add it.
 1401                 pass
 1402             else:
 1403                 try:
 1404                     obj[k] = v[0]
 1405                 except IndexError:
 1406                     obj[k] = None
 1407 
 1408         return obj
 1409 
 1410     def affirm_unique(self, values):
 1411         if values.get('name') is not None:
 1412             try:
 1413                 self.get_by_name(values['name'])
 1414             except exception.NotFound:  # nosec
 1415                 # Didn't find it so it's unique, good.
 1416                 pass
 1417             else:
 1418                 raise exception.Conflict(type=self.options_name,
 1419                                          details=_('Duplicate name, %s.') %
 1420                                          values['name'])
 1421 
 1422         if values.get('id') is not None:
 1423             try:
 1424                 self.get(values['id'])
 1425             except exception.NotFound:  # nosec
 1426                 # Didn't find it, so it's unique, good.
 1427                 pass
 1428             else:
 1429                 raise exception.Conflict(type=self.options_name,
 1430                                          details=_('Duplicate ID, %s.') %
 1431                                          values['id'])
 1432 
 1433     def create(self, values):
 1434         self.affirm_unique(values)
 1435         object_classes = self.structural_classes + [self.object_class]
 1436         attrs = [('objectClass', object_classes)]
 1437         for k, v in values.items():
 1438             if k in self.attribute_ignore:
 1439                 continue
 1440             if k == 'id':
 1441                 # no need to check if v is None as 'id' will always have
 1442                 # a value
 1443                 attrs.append((self.id_attr, [v]))
 1444             elif v is not None:
 1445                 attr_type = self.attribute_mapping.get(k, k)
 1446                 if attr_type is not None:
 1447                     attrs.append((attr_type, [v]))
 1448                 extra_attrs = [attr for attr, name
 1449                                in self.extra_attr_mapping.items()
 1450                                if name == k]
 1451                 for attr in extra_attrs:
 1452                     attrs.append((attr, [v]))
 1453 
 1454         with self.get_connection() as conn:
 1455             conn.add_s(self._id_to_dn(values['id']), attrs)
 1456         return values
 1457 
 1458     # NOTE(prashkre): Filter ldap search results on an attribute to ensure
 1459     # that attribute has a value set on ldap. This keeps keystone away
 1460     # from entities that don't have attribute value set on ldap.
 1461     # for e.g. In ldap configuration, if user_name_attribute = personName
 1462     # then it will ignore ldap users who don't have 'personName' attribute
 1463     # value set on user.
 1464     def _filter_ldap_result_by_attr(self, ldap_result, ldap_attr_name):
 1465         attr = self.attribute_mapping[ldap_attr_name]
 1466 
 1467         # To ensure that ldap attribute value is not empty in ldap config.
 1468         if not attr:
 1469             attr_name = ('%s_%s_attribute' %
 1470                          (self.options_name,
 1471                           self.attribute_options_names[ldap_attr_name]))
 1472             raise ValueError('"%(attr)s" is not a valid value for'
 1473                              ' "%(attr_name)s"' % {'attr': attr,
 1474                                                    'attr_name': attr_name})
 1475 
 1476         # consider attr = "cn" and
 1477         # ldap_result = [{'uid': ['fake_id1']}, , 'cN': ["name"]}]
 1478         # doing lower case on both user_name_attribute and ldap users
 1479         # attribute
 1480         result = []
 1481         # consider attr = "cn" and
 1482         # ldap_result = [(u'cn=fake1,o=ex_domain', {'uid': ['fake_id1']}),
 1483         #                (u'cn=fake2,o=ex_domain', {'uid': ['fake_id2'],
 1484         #                'cn': ['     ']}),
 1485         #                (u'cn=fake3,o=ex_domain', {'uid': ['fake_id3'],
 1486         #                'cn': ['']}),
 1487         #                (u'cn=fake4,o=ex_domain', {'uid': ['fake_id4'],
 1488         #                'cn': []}),
 1489         #                (u'cn=fake5,o=ex_domain', {'uid': ['fake_id5'],
 1490         #                'cn': ["name"]})]
 1491         for obj in ldap_result:
 1492             # ignore ldap object(user/group entry) which has no attr set
 1493             # in it or whose value is empty list.
 1494             ldap_res_low_keys_dict = {k.lower(): v for k, v in obj[1].items()}
 1495             result_attr_vals = ldap_res_low_keys_dict.get(attr.lower())
 1496             # ignore ldap object whose attr value has empty strings or
 1497             # contains only whitespaces.
 1498             if result_attr_vals:
 1499                 if result_attr_vals[0] and result_attr_vals[0].strip():
 1500                     result.append(obj)
 1501         # except {'uid': ['fake_id5'], 'cn': ["name"]}, all entries
 1502         # will be ignored in ldap_result
 1503         return result
 1504 
 1505     def _ldap_get(self, object_id, ldap_filter=None):
 1506         query = (u'(&(%(id_attr)s=%(id)s)'
 1507                  u'%(filter)s'
 1508                  u'(objectClass=%(object_class)s))'
 1509                  % {'id_attr': self.id_attr,
 1510                     'id': ldap.filter.escape_filter_chars(
 1511                         str(object_id)),
 1512                     'filter': (ldap_filter or self.ldap_filter or ''),
 1513                     'object_class': self.object_class})
 1514         with self.get_connection() as conn:
 1515             try:
 1516                 attrs = list(set(([self.id_attr] +
 1517                                   list(self.attribute_mapping.values()) +
 1518                                   list(self.extra_attr_mapping.keys()))))
 1519                 res = conn.search_s(self.tree_dn,
 1520                                     self.LDAP_SCOPE,
 1521                                     query,
 1522                                     attrs)
 1523             except ldap.NO_SUCH_OBJECT:
 1524                 return None
 1525 
 1526         # TODO(prashkre): add functional testing for missing name attibute
 1527         # on ldap entities.
 1528         # NOTE(prashkre): Filter ldap search result to keep keystone away from
 1529         # entities that don't have names. We can also do the same by appending
 1530         # a condition '(!(!(self.attribute_mapping.get('name')=*))' to ldap
 1531         # search query but the repsonse time of the query is pretty slow when
 1532         # compared to explicit filtering by 'name' through ldap result.
 1533         try:
 1534             return self._filter_ldap_result_by_attr(res[:1], 'name')[0]
 1535         except IndexError:
 1536             return None
 1537 
 1538     def _ldap_get_limited(self, base, scope, filterstr, attrlist, sizelimit):
 1539         with self.get_connection() as conn:
 1540             try:
 1541                 control = ldap.controls.libldap.SimplePagedResultsControl(
 1542                     criticality=True,
 1543                     size=sizelimit,
 1544                     cookie='')
 1545                 msgid = conn.search_ext(base, scope, filterstr, attrlist,
 1546                                         serverctrls=[control])
 1547                 rdata = conn.result3(msgid)
 1548                 return rdata
 1549             except ldap.NO_SUCH_OBJECT:
 1550                 return []
 1551 
 1552     @driver_hints.truncated
 1553     def _ldap_get_all(self, hints, ldap_filter=None):
 1554         query = u'(&%s(objectClass=%s)(%s=*))' % (
 1555             ldap_filter or self.ldap_filter or '',
 1556             self.object_class,
 1557             self.id_attr)
 1558         sizelimit = 0
 1559         attrs = list(set(([self.id_attr] +
 1560                           list(self.attribute_mapping.values()) +
 1561                           list(self.extra_attr_mapping.keys()))))
 1562         if hints.limit:
 1563             sizelimit = hints.limit['limit']
 1564             res = self._ldap_get_limited(self.tree_dn,
 1565                                          self.LDAP_SCOPE,
 1566                                          query,
 1567                                          attrs,
 1568                                          sizelimit)
 1569         else:
 1570             with self.get_connection() as conn:
 1571                 try:
 1572                     res = conn.search_s(self.tree_dn,
 1573                                         self.LDAP_SCOPE,
 1574                                         query,
 1575                                         attrs)
 1576                 except ldap.NO_SUCH_OBJECT:
 1577                     return []
 1578         # TODO(prashkre): add functional testing for missing name attribute
 1579         # on ldap entities.
 1580         # NOTE(prashkre): Filter ldap search result to keep keystone away from
 1581         # entities that don't have names. We can also do the same by appending
 1582         # a condition '(!(!(self.attribute_mapping.get('name')=*))' to ldap
 1583         # search query but the repsonse time of the query is pretty slow when
 1584         # compared to explicit filtering by 'name' through ldap result.
 1585         return self._filter_ldap_result_by_attr(res, 'name')
 1586 
 1587     def _ldap_get_list(self, search_base, scope, query_params=None,
 1588                        attrlist=None):
 1589         query = u'(objectClass=%s)' % self.object_class
 1590         if query_params:
 1591 
 1592             def calc_filter(attrname, value):
 1593                 val_esc = ldap.filter.escape_filter_chars(value)
 1594                 return '(%s=%s)' % (attrname, val_esc)
 1595 
 1596             query = (u'(&%s%s)' %
 1597                      (query, ''.join([calc_filter(k, v) for k, v in
 1598                                       query_params.items()])))
 1599         with self.get_connection() as conn:
 1600             return conn.search_s(search_base, scope, query, attrlist)
 1601 
 1602     def get(self, object_id, ldap_filter=None):
 1603         res = self._ldap_get(object_id, ldap_filter)
 1604         if res is None:
 1605             raise self._not_found(object_id)
 1606         else:
 1607             return self._ldap_res_to_model(res)
 1608 
 1609     def get_by_name(self, name, ldap_filter=None):
 1610         query = (u'(%s=%s)' % (self.attribute_mapping['name'],
 1611                                ldap.filter.escape_filter_chars(
 1612                                    str(name))))
 1613         res = self.get_all(query)
 1614         try:
 1615             return res[0]
 1616         except IndexError:
 1617             raise self._not_found(name)
 1618 
 1619     def get_all(self, ldap_filter=None, hints=None):
 1620         hints = hints or driver_hints.Hints()
 1621         return [self._ldap_res_to_model(x)
 1622                 for x in self._ldap_get_all(hints, ldap_filter)]
 1623 
 1624     def update(self, object_id, values, old_obj=None):
 1625         if old_obj is None:
 1626             old_obj = self.get(object_id)
 1627 
 1628         modlist = []
 1629         for k, v in values.items():
 1630             if k == 'id':
 1631                 # id can't be modified.
 1632                 continue
 1633 
 1634             if k in self.attribute_ignore:
 1635 
 1636                 # Handle 'enabled' specially since can't disable if ignored.
 1637                 if k == 'enabled' and (not v):
 1638                     action = _("Disabling an entity where the 'enable' "
 1639                                "attribute is ignored by configuration.")
 1640                     raise exception.ForbiddenAction(action=action)
 1641 
 1642                 continue
 1643 
 1644             # attribute value has not changed
 1645             if k in old_obj and old_obj[k] == v:
 1646                 continue
 1647 
 1648             if k in self.immutable_attrs:
 1649                 msg = (_("Cannot change %(option_name)s %(attr)s") %
 1650                        {'option_name': self.options_name, 'attr': k})
 1651                 raise exception.ValidationError(msg)
 1652 
 1653             if v is None:
 1654                 if old_obj.get(k) is not None:
 1655                     modlist.append((ldap.MOD_DELETE,
 1656                                     self.attribute_mapping.get(k, k),
 1657                                     None))
 1658                 continue
 1659 
 1660             current_value = old_obj.get(k)
 1661             if current_value is None:
 1662                 op = ldap.MOD_ADD
 1663                 modlist.append((op, self.attribute_mapping.get(k, k), [v]))
 1664             elif current_value != v:
 1665                 op = ldap.MOD_REPLACE
 1666                 modlist.append((op, self.attribute_mapping.get(k, k), [v]))
 1667 
 1668         if modlist:
 1669             with self.get_connection() as conn:
 1670                 try:
 1671                     conn.modify_s(self._id_to_dn(object_id), modlist)
 1672                 except ldap.NO_SUCH_OBJECT:
 1673                     raise self._not_found(object_id)
 1674 
 1675         return self.get(object_id)
 1676 
 1677     def add_member(self, member_dn, member_list_dn):
 1678         """Add member to the member list.
 1679 
 1680         :param member_dn: DN of member to be added.
 1681         :param member_list_dn: DN of group to which the
 1682                                member will be added.
 1683 
 1684         :raises keystone.exception.Conflict: If the user was already a member.
 1685         :raises self.NotFound: If the group entry didn't exist.
 1686         """
 1687         with self.get_connection() as conn:
 1688             try:
 1689                 mod = (ldap.MOD_ADD, self.member_attribute, member_dn)
 1690                 conn.modify_s(member_list_dn, [mod])
 1691             except ldap.TYPE_OR_VALUE_EXISTS:
 1692                 raise exception.Conflict(_('Member %(member)s '
 1693                                            'is already a member'
 1694                                            ' of group %(group)s') % {
 1695                                                'member': member_dn,
 1696                                                'group': member_list_dn})
 1697             except ldap.NO_SUCH_OBJECT:
 1698                 raise self._not_found(member_list_dn)
 1699 
 1700     def filter_query(self, hints, query=None):
 1701         """Apply filtering to a query.
 1702 
 1703         :param hints: contains the list of filters, which may be None,
 1704                       indicating that there are no filters to be applied.
 1705                       If it's not None, then any filters satisfied here will be
 1706                       removed so that the caller will know if any filters
 1707                       remain to be applied.
 1708         :param query: LDAP query into which to include filters
 1709 
 1710         :returns query: LDAP query, updated with any filters satisfied
 1711 
 1712         """
 1713         def build_filter(filter_):
 1714             """Build a filter for the query.
 1715 
 1716             :param filter_: the dict that describes this filter
 1717 
 1718             :returns query: LDAP query term to be added
 1719 
 1720             """
 1721             ldap_attr = self.attribute_mapping[filter_['name']]
 1722             val_esc = ldap.filter.escape_filter_chars(filter_['value'])
 1723 
 1724             if filter_['case_sensitive']:
 1725                 # NOTE(henry-nash): Although dependent on the schema being
 1726                 # used, most LDAP attributes are configured with case
 1727                 # insensitive matching rules, so we'll leave this to the
 1728                 # controller to filter.
 1729                 return
 1730 
 1731             if filter_['name'] == 'enabled':
 1732                 # NOTE(henry-nash): Due to the different options for storing
 1733                 # the enabled attribute (e,g, emulated or not), for now we
 1734                 # don't try and filter this at the driver level - we simply
 1735                 # leave the filter to be handled by the controller. It seems
 1736                 # unlikley that this will cause a signifcant performance
 1737                 # issue.
 1738                 return
 1739 
 1740             # TODO(henry-nash): Currently there are no booleans (other than
 1741             # 'enabled' that is handled above) on which you can filter. If
 1742             # there were, we would need to add special handling here to
 1743             # convert the booleans values to 'TRUE' and 'FALSE'. To do that
 1744             # we would also need to know which filter keys were actually
 1745             # booleans (this is related to bug #1411478).
 1746 
 1747             if filter_['comparator'] == 'equals':
 1748                 query_term = (u'(%(attr)s=%(val)s)'
 1749                               % {'attr': ldap_attr, 'val': val_esc})
 1750             elif filter_['comparator'] == 'contains':
 1751                 query_term = (u'(%(attr)s=*%(val)s*)'
 1752                               % {'attr': ldap_attr, 'val': val_esc})
 1753             elif filter_['comparator'] == 'startswith':
 1754                 query_term = (u'(%(attr)s=%(val)s*)'
 1755                               % {'attr': ldap_attr, 'val': val_esc})
 1756             elif filter_['comparator'] == 'endswith':
 1757                 query_term = (u'(%(attr)s=*%(val)s)'
 1758                               % {'attr': ldap_attr, 'val': val_esc})
 1759             else:
 1760                 # It's a filter we don't understand, so let the caller
 1761                 # work out if they need to do something with it.
 1762                 return
 1763 
 1764             return query_term
 1765 
 1766         if query is None:
 1767             # make sure query is a string so the ldap filter is properly
 1768             # constructed from filter_list later
 1769             query = ''
 1770 
 1771         if hints is None:
 1772             return query
 1773 
 1774         filter_list = []
 1775         satisfied_filters = []
 1776 
 1777         for filter_ in hints.filters:
 1778             if filter_['name'] not in self.attribute_mapping:
 1779                 continue
 1780             new_filter = build_filter(filter_)
 1781             if new_filter is not None:
 1782                 filter_list.append(new_filter)
 1783                 satisfied_filters.append(filter_)
 1784 
 1785         if filter_list:
 1786             query = u'(&%s%s)' % (query, ''.join(filter_list))
 1787 
 1788         # Remove satisfied filters, then the caller will know remaining filters
 1789         for filter_ in satisfied_filters:
 1790             hints.filters.remove(filter_)
 1791 
 1792         return query
 1793 
 1794 
 1795 class EnabledEmuMixIn(BaseLdap):
 1796     """Emulates boolean 'enabled' attribute if turned on.
 1797 
 1798     Creates a group holding all enabled objects of this class, all missing
 1799     objects are considered disabled.
 1800 
 1801     Options:
 1802 
 1803     * $name_enabled_emulation - boolean, on/off
 1804     * $name_enabled_emulation_dn - DN of that group, default is
 1805       cn=enabled_${name}s,${tree_dn}
 1806     * $name_enabled_emulation_use_group_config - boolean, on/off
 1807 
 1808     Where ${name}s is the plural of self.options_name ('users' or 'tenants'),
 1809     ${tree_dn} is self.tree_dn.
 1810     """
 1811 
 1812     DEFAULT_GROUP_OBJECTCLASS = 'groupOfNames'
 1813     DEFAULT_MEMBER_ATTRIBUTE = 'member'
 1814     DEFAULT_GROUP_MEMBERS_ARE_IDS = False
 1815 
 1816     def __init__(self, conf):
 1817         super(EnabledEmuMixIn, self).__init__(conf)
 1818         enabled_emulation = '%s_enabled_emulation' % self.options_name
 1819         self.enabled_emulation = getattr(conf.ldap, enabled_emulation)
 1820 
 1821         enabled_emulation_dn = '%s_enabled_emulation_dn' % self.options_name
 1822         self.enabled_emulation_dn = getattr(conf.ldap, enabled_emulation_dn)
 1823 
 1824         use_group_config = ('%s_enabled_emulation_use_group_config' %
 1825                             self.options_name)
 1826         self.use_group_config = getattr(conf.ldap, use_group_config)
 1827 
 1828         if not self.use_group_config:
 1829             self.member_attribute = self.DEFAULT_MEMBER_ATTRIBUTE
 1830             self.group_objectclass = self.DEFAULT_GROUP_OBJECTCLASS
 1831             self.group_members_are_ids = self.DEFAULT_GROUP_MEMBERS_ARE_IDS
 1832         else:
 1833             self.member_attribute = conf.ldap.group_member_attribute
 1834             self.group_objectclass = conf.ldap.group_objectclass
 1835             self.group_members_are_ids = conf.ldap.group_members_are_ids
 1836 
 1837         if not self.enabled_emulation_dn:
 1838             naming_attr_name = 'cn'
 1839             naming_attr_value = 'enabled_%ss' % self.options_name
 1840             sub_vals = (naming_attr_name, naming_attr_value, self.tree_dn)
 1841             self.enabled_emulation_dn = '%s=%s,%s' % sub_vals
 1842             naming_attr = (naming_attr_name, [naming_attr_value])
 1843         else:
 1844             # Extract the attribute name and value from the configured DN.
 1845             naming_dn = ldap.dn.str2dn(self.enabled_emulation_dn)
 1846             naming_rdn = naming_dn[0][0]
 1847             naming_attr = (naming_rdn[0],
 1848                            naming_rdn[1])
 1849         self.enabled_emulation_naming_attr = naming_attr
 1850 
 1851     def _id_to_member_attribute_value(self, object_id):
 1852         """Convert id to value expected by member_attribute."""
 1853         if self.group_members_are_ids:
 1854             return object_id
 1855         return self._id_to_dn(object_id)
 1856 
 1857     def _is_id_enabled(self, object_id, conn):
 1858         member_attr_val = self._id_to_member_attribute_value(object_id)
 1859         return self._is_member_enabled(member_attr_val, conn)
 1860 
 1861     def _is_member_enabled(self, member_attr_val, conn):
 1862         query = '(%s=%s)' % (self.member_attribute,
 1863                              ldap.filter.escape_filter_chars(member_attr_val))
 1864         try:
 1865             enabled_value = conn.search_s(self.enabled_emulation_dn,
 1866                                           ldap.SCOPE_BASE,
 1867                                           query, attrlist=DN_ONLY)
 1868         except ldap.NO_SUCH_OBJECT:
 1869             return False
 1870         else:
 1871             return bool(enabled_value)
 1872 
 1873     def _add_enabled(self, object_id):
 1874         member_attr_val = self._id_to_member_attribute_value(object_id)
 1875         with self.get_connection() as conn:
 1876             if not self._is_member_enabled(member_attr_val, conn):
 1877                 modlist = [(ldap.MOD_ADD,
 1878                             self.member_attribute,
 1879                             [member_attr_val])]
 1880                 try:
 1881                     conn.modify_s(self.enabled_emulation_dn, modlist)
 1882                 except ldap.NO_SUCH_OBJECT:
 1883                     attr_list = [('objectClass', [self.group_objectclass]),
 1884                                  (self.member_attribute,
 1885                                   [member_attr_val]),
 1886                                  self.enabled_emulation_naming_attr]
 1887                     conn.add_s(self.enabled_emulation_dn, attr_list)
 1888 
 1889     def _remove_enabled(self, object_id):
 1890         member_attr_val = self._id_to_member_attribute_value(object_id)
 1891         modlist = [(ldap.MOD_DELETE,
 1892                     self.member_attribute,
 1893                     [member_attr_val])]
 1894         with self.get_connection() as conn:
 1895             try:
 1896                 conn.modify_s(self.enabled_emulation_dn, modlist)
 1897             except (ldap.NO_SUCH_OBJECT, ldap.NO_SUCH_ATTRIBUTE):  # nosec
 1898                 # It's already gone, good.
 1899                 pass
 1900 
 1901     def create(self, values):
 1902         if self.enabled_emulation:
 1903             enabled_value = values.pop('enabled', True)
 1904             ref = super(EnabledEmuMixIn, self).create(values)
 1905             if 'enabled' not in self.attribute_ignore:
 1906                 if enabled_value:
 1907                     self._add_enabled(ref['id'])
 1908                 ref['enabled'] = enabled_value
 1909             return ref
 1910         else:
 1911             return super(EnabledEmuMixIn, self).create(values)
 1912 
 1913     def get(self, object_id, ldap_filter=None):
 1914         with self.get_connection() as conn:
 1915             ref = super(EnabledEmuMixIn, self).get(object_id, ldap_filter)
 1916             if ('enabled' not in self.attribute_ignore and
 1917                     self.enabled_emulation):
 1918                 ref['enabled'] = self._is_id_enabled(object_id, conn)
 1919             return ref
 1920 
 1921     def get_all(self, ldap_filter=None, hints=None):
 1922         hints = hints or driver_hints.Hints()
 1923         if 'enabled' not in self.attribute_ignore and self.enabled_emulation:
 1924             # had to copy BaseLdap.get_all here to ldap_filter by DN
 1925             obj_list = [self._ldap_res_to_model(x)
 1926                         for x in self._ldap_get_all(hints, ldap_filter)
 1927                         if x[0] != self.enabled_emulation_dn]
 1928             with self.get_connection() as conn:
 1929                 for obj_ref in obj_list:
 1930                     obj_ref['enabled'] = self._is_id_enabled(
 1931                         obj_ref['id'], conn)
 1932             return obj_list
 1933         else:
 1934             return super(EnabledEmuMixIn, self).get_all(ldap_filter, hints)
 1935 
 1936     def update(self, object_id, values, old_obj=None):
 1937         if 'enabled' not in self.attribute_ignore and self.enabled_emulation:
 1938             data = values.copy()
 1939             enabled_value = data.pop('enabled', None)
 1940             ref = super(EnabledEmuMixIn, self).update(object_id, data, old_obj)
 1941             if enabled_value is not None:
 1942                 if enabled_value:
 1943                     self._add_enabled(object_id)
 1944                 else:
 1945                     self._remove_enabled(object_id)
 1946                 ref['enabled'] = enabled_value
 1947             return ref
 1948         else:
 1949             return super(EnabledEmuMixIn, self).update(
 1950                 object_id, values, old_obj)