"Fossies" - the Fresh Open Source Software Archive

Member "keystone-18.0.0/keystone/identity/backends/ldap/common.py" (14 Oct 2020, 78287 Bytes) of package /linux/misc/openstack/keystone-18.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "common.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 17.0.0_vs_18.0.0.

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