"Fossies" - the Fresh Open Source Software Archive

Member "keystone-16.0.2/keystone/identity/backends/ldap/common.py" (7 Jun 2021, 80191 Bytes) of package /linux/misc/openstack/keystone-16.0.2.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: 16.0.1_vs_16.0.2.

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