aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/common/ldap/core.py
diff options
context:
space:
mode:
authorRHE <rebirthmonkey@gmail.com>2017-11-24 13:54:26 +0100
committerRHE <rebirthmonkey@gmail.com>2017-11-24 13:54:26 +0100
commit920a49cfa055733d575282973e23558c33087a4a (patch)
treed371dab34efa5028600dad2e7ca58063626e7ba4 /keystone-moon/keystone/common/ldap/core.py
parentef3eefca70d8abb4a00dafb9419ad32738e934b2 (diff)
remove keystone-moon
Change-Id: I80d7c9b669f19d5f6607e162de8e0e55c2f80fdd Signed-off-by: RHE <rebirthmonkey@gmail.com>
Diffstat (limited to 'keystone-moon/keystone/common/ldap/core.py')
-rw-r--r--keystone-moon/keystone/common/ldap/core.py1955
1 files changed, 0 insertions, 1955 deletions
diff --git a/keystone-moon/keystone/common/ldap/core.py b/keystone-moon/keystone/common/ldap/core.py
deleted file mode 100644
index d94aa04c..00000000
--- a/keystone-moon/keystone/common/ldap/core.py
+++ /dev/null
@@ -1,1955 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import abc
-import codecs
-import functools
-import os.path
-import re
-import sys
-import weakref
-
-import ldap.controls
-import ldap.filter
-import ldappool
-from oslo_log import log
-from oslo_utils import reflection
-import six
-from six.moves import map, zip
-
-from keystone.common import driver_hints
-from keystone import exception
-from keystone.i18n import _
-from keystone.i18n import _LW
-
-
-LOG = log.getLogger(__name__)
-
-LDAP_VALUES = {'TRUE': True, 'FALSE': False}
-CONTROL_TREEDELETE = '1.2.840.113556.1.4.805'
-LDAP_SCOPES = {'one': ldap.SCOPE_ONELEVEL,
- 'sub': ldap.SCOPE_SUBTREE}
-LDAP_DEREF = {'always': ldap.DEREF_ALWAYS,
- 'default': None,
- 'finding': ldap.DEREF_FINDING,
- 'never': ldap.DEREF_NEVER,
- 'searching': ldap.DEREF_SEARCHING}
-LDAP_TLS_CERTS = {'never': ldap.OPT_X_TLS_NEVER,
- 'demand': ldap.OPT_X_TLS_DEMAND,
- 'allow': ldap.OPT_X_TLS_ALLOW}
-
-
-# RFC 4511 (The LDAP Protocol) defines a list containing only the OID '1.1' to
-# indicate that no attributes should be returned besides the DN.
-DN_ONLY = ['1.1']
-
-_utf8_encoder = codecs.getencoder('utf-8')
-
-
-def utf8_encode(value):
- """Encode a basestring to UTF-8.
-
- If the string is unicode encode it to UTF-8, if the string is
- str then assume it's already encoded. Otherwise raise a TypeError.
-
- :param value: A basestring
- :returns: UTF-8 encoded version of value
- :raises TypeError: If value is not basestring
- """
- if isinstance(value, six.text_type):
- return _utf8_encoder(value)[0]
- elif isinstance(value, six.binary_type):
- return value
- else:
- value_cls_name = reflection.get_class_name(
- value, fully_qualified=False)
- raise TypeError("value must be basestring, "
- "not %s" % value_cls_name)
-
-_utf8_decoder = codecs.getdecoder('utf-8')
-
-
-def utf8_decode(value):
- """Decode a from UTF-8 into unicode.
-
- If the value is a binary string assume it's UTF-8 encoded and decode
- it into a unicode string. Otherwise convert the value from its
- type into a unicode string.
-
- :param value: value to be returned as unicode
- :returns: value as unicode
- :raises UnicodeDecodeError: for invalid UTF-8 encoding
- """
- if isinstance(value, six.binary_type):
- return _utf8_decoder(value)[0]
- return six.text_type(value)
-
-
-def py2ldap(val):
- """Type convert a Python value to a type accepted by LDAP (unicode).
-
- The LDAP API only accepts strings for values therefore convert
- the value's type to a unicode string. A subsequent type conversion
- will encode the unicode as UTF-8 as required by the python-ldap API,
- but for now we just want a string representation of the value.
-
- :param val: The value to convert to a LDAP string representation
- :returns: unicode string representation of value.
- """
- if isinstance(val, bool):
- return u'TRUE' if val else u'FALSE'
- else:
- return six.text_type(val)
-
-
-def enabled2py(val):
- """Similar to ldap2py, only useful for the enabled attribute."""
- try:
- return LDAP_VALUES[val]
- except KeyError: # nosec
- # It wasn't a boolean value, will try as an int instead.
- pass
- try:
- return int(val)
- except ValueError: # nosec
- # It wasn't an int either, will try as utf8 instead.
- pass
- return utf8_decode(val)
-
-
-def ldap2py(val):
- """Convert an LDAP formatted value to Python type used by OpenStack.
-
- Virtually all LDAP values are stored as UTF-8 encoded strings.
- OpenStack prefers values which are unicode friendly.
-
- :param val: LDAP formatted value
- :returns: val converted to preferred Python type
- """
- return utf8_decode(val)
-
-
-def convert_ldap_result(ldap_result):
- """Convert LDAP search result to Python types used by OpenStack.
-
- Each result tuple is of the form (dn, attrs), where dn is a string
- containing the DN (distinguished name) of the entry, and attrs is
- a dictionary containing the attributes associated with the
- entry. The keys of attrs are strings, and the associated values
- are lists of strings.
-
- OpenStack wants to use Python types of its choosing. Strings will
- be unicode, truth values boolean, whole numbers int's, etc. DN's will
- also be decoded from UTF-8 to unicode.
-
- :param ldap_result: LDAP search result
- :returns: list of 2-tuples containing (dn, attrs) where dn is unicode
- and attrs is a dict whose values are type converted to
- OpenStack preferred types.
- """
- py_result = []
- at_least_one_referral = False
- for dn, attrs in ldap_result:
- ldap_attrs = {}
- if dn is None:
- # this is a Referral object, rather than an Entry object
- at_least_one_referral = True
- continue
-
- for kind, values in attrs.items():
- try:
- val2py = enabled2py if kind == 'enabled' else ldap2py
- ldap_attrs[kind] = [val2py(x) for x in values]
- except UnicodeDecodeError:
- LOG.debug('Unable to decode value for attribute %s', kind)
-
- py_result.append((utf8_decode(dn), ldap_attrs))
- if at_least_one_referral:
- LOG.debug(('Referrals were returned and ignored. Enable referral '
- 'chasing in keystone.conf via [ldap] chase_referrals'))
-
- return py_result
-
-
-def safe_iter(attrs):
- if attrs is None:
- return
- elif isinstance(attrs, list):
- for e in attrs:
- yield e
- else:
- yield attrs
-
-
-def parse_deref(opt):
- try:
- return LDAP_DEREF[opt]
- except KeyError:
- raise ValueError(_('Invalid LDAP deref option: %(option)s. '
- 'Choose one of: %(options)s') %
- {'option': opt,
- 'options': ', '.join(LDAP_DEREF.keys()), })
-
-
-def parse_tls_cert(opt):
- try:
- return LDAP_TLS_CERTS[opt]
- except KeyError:
- raise ValueError(_(
- 'Invalid LDAP TLS certs option: %(option)s. '
- 'Choose one of: %(options)s') % {
- 'option': opt,
- 'options': ', '.join(LDAP_TLS_CERTS.keys())})
-
-
-def ldap_scope(scope):
- try:
- return LDAP_SCOPES[scope]
- except KeyError:
- raise ValueError(
- _('Invalid LDAP scope: %(scope)s. Choose one of: %(options)s') % {
- 'scope': scope,
- 'options': ', '.join(LDAP_SCOPES.keys())})
-
-
-def prep_case_insensitive(value):
- """Prepare a string for case-insensitive comparison.
-
- This is defined in RFC4518. For simplicity, all this function does is
- lowercase all the characters, strip leading and trailing whitespace,
- and compress sequences of spaces to a single space.
- """
- value = re.sub(r'\s+', ' ', value.strip().lower())
- return value
-
-
-def is_ava_value_equal(attribute_type, val1, val2):
- """Returns True if and only if the AVAs are equal.
-
- When comparing AVAs, the equality matching rule for the attribute type
- should be taken into consideration. For simplicity, this implementation
- does a case-insensitive comparison.
-
- Note that this function uses prep_case_insenstive so the limitations of
- that function apply here.
-
- """
- return prep_case_insensitive(val1) == prep_case_insensitive(val2)
-
-
-def is_rdn_equal(rdn1, rdn2):
- """Returns True if and only if the RDNs are equal.
-
- * RDNs must have the same number of AVAs.
- * Each AVA of the RDNs must be the equal for the same attribute type. The
- order isn't significant. Note that an attribute type will only be in one
- AVA in an RDN, otherwise the DN wouldn't be valid.
- * Attribute types aren't case sensitive. Note that attribute type
- comparison is more complicated than implemented. This function only
- compares case-insentive. The code should handle multiple names for an
- attribute type (e.g., cn, commonName, and 2.5.4.3 are the same).
-
- Note that this function uses is_ava_value_equal to compare AVAs so the
- limitations of that function apply here.
-
- """
- if len(rdn1) != len(rdn2):
- return False
-
- for attr_type_1, val1, dummy in rdn1:
- found = False
- for attr_type_2, val2, dummy in rdn2:
- if attr_type_1.lower() != attr_type_2.lower():
- continue
-
- found = True
- if not is_ava_value_equal(attr_type_1, val1, val2):
- return False
- break
- if not found:
- return False
-
- return True
-
-
-def is_dn_equal(dn1, dn2):
- """Returns True if and only if the DNs are equal.
-
- Two DNs are equal if they've got the same number of RDNs and if the RDNs
- are the same at each position. See RFC4517.
-
- Note that this function uses is_rdn_equal to compare RDNs so the
- limitations of that function apply here.
-
- :param dn1: Either a string DN or a DN parsed by ldap.dn.str2dn.
- :param dn2: Either a string DN or a DN parsed by ldap.dn.str2dn.
-
- """
- if not isinstance(dn1, list):
- dn1 = ldap.dn.str2dn(utf8_encode(dn1))
- if not isinstance(dn2, list):
- dn2 = ldap.dn.str2dn(utf8_encode(dn2))
-
- if len(dn1) != len(dn2):
- return False
-
- for rdn1, rdn2 in zip(dn1, dn2):
- if not is_rdn_equal(rdn1, rdn2):
- return False
- return True
-
-
-def dn_startswith(descendant_dn, dn):
- """Returns True if and only if the descendant_dn is under the dn.
-
- :param descendant_dn: Either a string DN or a DN parsed by ldap.dn.str2dn.
- :param dn: Either a string DN or a DN parsed by ldap.dn.str2dn.
-
- """
- if not isinstance(descendant_dn, list):
- descendant_dn = ldap.dn.str2dn(utf8_encode(descendant_dn))
- if not isinstance(dn, list):
- dn = ldap.dn.str2dn(utf8_encode(dn))
-
- if len(descendant_dn) <= len(dn):
- return False
-
- # Use the last len(dn) RDNs.
- return is_dn_equal(descendant_dn[-len(dn):], dn)
-
-
-@six.add_metaclass(abc.ABCMeta)
-class LDAPHandler(object):
- """Abstract class which defines methods for a LDAP API provider.
-
- Native Keystone values cannot be passed directly into and from the
- python-ldap API. Type conversion must occur at the LDAP API
- boudary, examples of type conversions are:
-
- * booleans map to the strings 'TRUE' and 'FALSE'
-
- * integer values map to their string representation.
-
- * unicode strings are encoded in UTF-8
-
- In addition to handling type conversions at the API boundary we
- have the requirement to support more than one LDAP API
- provider. Currently we have:
-
- * python-ldap, this is the standard LDAP API for Python, it
- requires access to a live LDAP server.
-
- * Fake LDAP which emulates python-ldap. This is used for
- testing without requiring a live LDAP server.
-
- To support these requirements we need a layer that performs type
- conversions and then calls another LDAP API which is configurable
- (e.g. either python-ldap or the fake emulation).
-
- We have an additional constraint at the time of this writing due to
- limitations in the logging module. The logging module is not
- capable of accepting UTF-8 encoded strings, it will throw an
- encoding exception. Therefore all logging MUST be performed prior
- to UTF-8 conversion. This means no logging can be performed in the
- ldap APIs that implement the python-ldap API because those APIs
- are defined to accept only UTF-8 strings. Thus the layer which
- performs type conversions must also do the logging. We do the type
- conversions in two steps, once to convert all Python types to
- unicode strings, then log, then convert the unicode strings to
- UTF-8.
-
- There are a variety of ways one could accomplish this, we elect to
- use a chaining technique whereby instances of this class simply
- call the next member in the chain via the "conn" attribute. The
- chain is constructed by passing in an existing instance of this
- class as the conn attribute when the class is instantiated.
-
- Here is a brief explanation of why other possible approaches were
- not used:
-
- subclassing
-
- To perform the wrapping operations in the correct order
- the type convesion class would have to subclass each of
- the API providers. This is awkward, doubles the number of
- classes, and does not scale well. It requires the type
- conversion class to be aware of all possible API
- providers.
-
- decorators
-
- Decorators provide an elegant solution to wrap methods and
- would be an ideal way to perform type conversions before
- calling the wrapped function and then converting the
- values returned from the wrapped function. However
- decorators need to be aware of the method signature, it
- has to know what input parameters need conversion and how
- to convert the result. For an API like python-ldap which
- has a large number of different method signatures it would
- require a large number of specialized
- decorators. Experience has shown it's very easy to apply
- the wrong decorator due to the inherent complexity and
- tendency to cut-n-paste code. Another option is to
- parameterize the decorator to make it "smart". Experience
- has shown such decorators become insanely complicated and
- difficult to understand and debug. Also decorators tend to
- hide what's really going on when a method is called, the
- operations being performed are not visible when looking at
- the implemation of a decorated method, this too experience
- has shown leads to mistakes.
-
- Chaining simplifies both wrapping to perform type conversion as
- well as the substitution of alternative API providers. One simply
- creates a new instance of the API interface and insert it at the
- front of the chain. Type conversions are explicit and obvious.
-
- If a new method needs to be added to the API interface one adds it
- to the abstract class definition. Should one miss adding the new
- method to any derivations of the abstract class the code will fail
- to load and run making it impossible to forget updating all the
- derived classes.
-
- """
-
- @abc.abstractmethod
- def __init__(self, conn=None):
- self.conn = conn
-
- @abc.abstractmethod
- def connect(self, url, page_size=0, alias_dereferencing=None,
- use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
- tls_req_cert='demand', chase_referrals=None, debug_level=None,
- use_pool=None, pool_size=None, pool_retry_max=None,
- pool_retry_delay=None, pool_conn_timeout=None,
- pool_conn_lifetime=None):
- raise exception.NotImplemented() # pragma: no cover
-
- @abc.abstractmethod
- def set_option(self, option, invalue):
- raise exception.NotImplemented() # pragma: no cover
-
- @abc.abstractmethod
- def get_option(self, option):
- raise exception.NotImplemented() # pragma: no cover
-
- @abc.abstractmethod
- def simple_bind_s(self, who='', cred='',
- serverctrls=None, clientctrls=None):
- raise exception.NotImplemented() # pragma: no cover
-
- @abc.abstractmethod
- def unbind_s(self):
- raise exception.NotImplemented() # pragma: no cover
-
- @abc.abstractmethod
- def add_s(self, dn, modlist):
- raise exception.NotImplemented() # pragma: no cover
-
- @abc.abstractmethod
- def search_s(self, base, scope,
- filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
- raise exception.NotImplemented() # pragma: no cover
-
- @abc.abstractmethod
- def search_ext(self, base, scope,
- filterstr='(objectClass=*)', attrlist=None, attrsonly=0,
- serverctrls=None, clientctrls=None,
- timeout=-1, sizelimit=0):
- raise exception.NotImplemented() # pragma: no cover
-
- @abc.abstractmethod
- def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None,
- resp_ctrl_classes=None):
- raise exception.NotImplemented() # pragma: no cover
-
- @abc.abstractmethod
- def modify_s(self, dn, modlist):
- raise exception.NotImplemented() # pragma: no cover
-
- @abc.abstractmethod
- def delete_s(self, dn):
- raise exception.NotImplemented() # pragma: no cover
-
- @abc.abstractmethod
- def delete_ext_s(self, dn, serverctrls=None, clientctrls=None):
- raise exception.NotImplemented() # pragma: no cover
-
-
-class PythonLDAPHandler(LDAPHandler):
- """LDAPHandler implementation which calls the python-ldap API.
-
- Note, the python-ldap API requires all string values to be UTF-8 encoded.
- The KeystoneLDAPHandler enforces this prior to invoking the methods in this
- class.
-
- """
-
- def __init__(self, conn=None):
- super(PythonLDAPHandler, self).__init__(conn=conn)
-
- def connect(self, url, page_size=0, alias_dereferencing=None,
- use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
- tls_req_cert='demand', chase_referrals=None, debug_level=None,
- use_pool=None, pool_size=None, pool_retry_max=None,
- pool_retry_delay=None, pool_conn_timeout=None,
- pool_conn_lifetime=None):
-
- _common_ldap_initialization(url=url,
- use_tls=use_tls,
- tls_cacertfile=tls_cacertfile,
- tls_cacertdir=tls_cacertdir,
- tls_req_cert=tls_req_cert,
- debug_level=debug_level)
-
- self.conn = ldap.initialize(url)
- self.conn.protocol_version = ldap.VERSION3
-
- if alias_dereferencing is not None:
- self.conn.set_option(ldap.OPT_DEREF, alias_dereferencing)
- self.page_size = page_size
-
- if use_tls:
- self.conn.start_tls_s()
-
- if chase_referrals is not None:
- self.conn.set_option(ldap.OPT_REFERRALS, int(chase_referrals))
-
- def set_option(self, option, invalue):
- return self.conn.set_option(option, invalue)
-
- def get_option(self, option):
- return self.conn.get_option(option)
-
- def simple_bind_s(self, who='', cred='',
- serverctrls=None, clientctrls=None):
- return self.conn.simple_bind_s(who, cred, serverctrls, clientctrls)
-
- def unbind_s(self):
- return self.conn.unbind_s()
-
- def add_s(self, dn, modlist):
- return self.conn.add_s(dn, modlist)
-
- def search_s(self, base, scope,
- filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
- return self.conn.search_s(base, scope, filterstr,
- attrlist, attrsonly)
-
- def search_ext(self, base, scope,
- filterstr='(objectClass=*)', attrlist=None, attrsonly=0,
- serverctrls=None, clientctrls=None,
- timeout=-1, sizelimit=0):
- return self.conn.search_ext(base, scope,
- filterstr, attrlist, attrsonly,
- serverctrls, clientctrls,
- timeout, sizelimit)
-
- def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None,
- resp_ctrl_classes=None):
- # The resp_ctrl_classes parameter is a recent addition to the
- # API. It defaults to None. We do not anticipate using it.
- # To run with older versions of python-ldap we do not pass it.
- return self.conn.result3(msgid, all, timeout)
-
- def modify_s(self, dn, modlist):
- return self.conn.modify_s(dn, modlist)
-
- def delete_s(self, dn):
- return self.conn.delete_s(dn)
-
- def delete_ext_s(self, dn, serverctrls=None, clientctrls=None):
- return self.conn.delete_ext_s(dn, serverctrls, clientctrls)
-
-
-def _common_ldap_initialization(url, use_tls=False, tls_cacertfile=None,
- tls_cacertdir=None, tls_req_cert=None,
- debug_level=None):
- """LDAP initialization for PythonLDAPHandler and PooledLDAPHandler."""
- LOG.debug("LDAP init: url=%s", url)
- LOG.debug('LDAP init: use_tls=%s tls_cacertfile=%s tls_cacertdir=%s '
- 'tls_req_cert=%s tls_avail=%s',
- use_tls, tls_cacertfile, tls_cacertdir,
- tls_req_cert, ldap.TLS_AVAIL)
-
- if debug_level is not None:
- ldap.set_option(ldap.OPT_DEBUG_LEVEL, debug_level)
-
- using_ldaps = url.lower().startswith("ldaps")
-
- if use_tls and using_ldaps:
- raise AssertionError(_('Invalid TLS / LDAPS combination'))
-
- # The certificate trust options apply for both LDAPS and TLS.
- if use_tls or using_ldaps:
- if not ldap.TLS_AVAIL:
- raise ValueError(_('Invalid LDAP TLS_AVAIL option: %s. TLS '
- 'not available') % ldap.TLS_AVAIL)
- if tls_cacertfile:
- # NOTE(topol)
- # python ldap TLS does not verify CACERTFILE or CACERTDIR
- # so we add some extra simple sanity check verification
- # Also, setting these values globally (i.e. on the ldap object)
- # works but these values are ignored when setting them on the
- # connection
- if not os.path.isfile(tls_cacertfile):
- raise IOError(_("tls_cacertfile %s not found "
- "or is not a file") %
- tls_cacertfile)
- ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, tls_cacertfile)
- elif tls_cacertdir:
- # NOTE(topol)
- # python ldap TLS does not verify CACERTFILE or CACERTDIR
- # so we add some extra simple sanity check verification
- # Also, setting these values globally (i.e. on the ldap object)
- # works but these values are ignored when setting them on the
- # connection
- if not os.path.isdir(tls_cacertdir):
- raise IOError(_("tls_cacertdir %s not found "
- "or is not a directory") %
- tls_cacertdir)
- ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, tls_cacertdir)
- if tls_req_cert in list(LDAP_TLS_CERTS.values()):
- ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_cert)
- else:
- LOG.debug("LDAP TLS: invalid TLS_REQUIRE_CERT Option=%s",
- tls_req_cert)
-
-
-class MsgId(list):
- """Wrapper class to hold connection and msgid."""
-
- pass
-
-
-def use_conn_pool(func):
- """Use this only for connection pool specific ldap API.
-
- This adds connection object to decorated API as next argument after self.
-
- """
- def wrapper(self, *args, **kwargs):
- # assert isinstance(self, PooledLDAPHandler)
- with self._get_pool_connection() as conn:
- self._apply_options(conn)
- return func(self, conn, *args, **kwargs)
- return wrapper
-
-
-class PooledLDAPHandler(LDAPHandler):
- """LDAPHandler implementation which uses pooled connection manager.
-
- Pool specific configuration is defined in [ldap] section.
- All other LDAP configuration is still used from [ldap] section
-
- Keystone LDAP authentication logic authenticates an end user using its DN
- and password via LDAP bind to establish supplied password is correct.
- This can fill up the pool quickly (as pool re-uses existing connection
- based on its bind data) and would not leave space in pool for connection
- re-use for other LDAP operations.
- Now a separate pool can be established for those requests when related flag
- 'use_auth_pool' is enabled. That pool can have its own size and
- connection lifetime. Other pool attributes are shared between those pools.
- If 'use_pool' is disabled, then 'use_auth_pool' does not matter.
- If 'use_auth_pool' is not enabled, then connection pooling is not used for
- those LDAP operations.
-
- Note, the python-ldap API requires all string values to be UTF-8
- encoded. The KeystoneLDAPHandler enforces this prior to invoking
- the methods in this class.
-
- """
-
- # Added here to allow override for testing
- Connector = ldappool.StateConnector
- auth_pool_prefix = 'auth_pool_'
-
- connection_pools = {} # static connector pool dict
-
- def __init__(self, conn=None, use_auth_pool=False):
- super(PooledLDAPHandler, self).__init__(conn=conn)
- self.who = ''
- self.cred = ''
- self.conn_options = {} # connection specific options
- self.page_size = None
- self.use_auth_pool = use_auth_pool
- self.conn_pool = None
-
- def connect(self, url, page_size=0, alias_dereferencing=None,
- use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
- tls_req_cert='demand', chase_referrals=None, debug_level=None,
- use_pool=None, pool_size=None, pool_retry_max=None,
- pool_retry_delay=None, pool_conn_timeout=None,
- pool_conn_lifetime=None):
-
- _common_ldap_initialization(url=url,
- use_tls=use_tls,
- tls_cacertfile=tls_cacertfile,
- tls_cacertdir=tls_cacertdir,
- tls_req_cert=tls_req_cert,
- debug_level=debug_level)
-
- self.page_size = page_size
-
- # Following two options are not added in common initialization as they
- # need to follow a sequence in PythonLDAPHandler code.
- if alias_dereferencing is not None:
- self.set_option(ldap.OPT_DEREF, alias_dereferencing)
- if chase_referrals is not None:
- self.set_option(ldap.OPT_REFERRALS, int(chase_referrals))
-
- if self.use_auth_pool: # separate pool when use_auth_pool enabled
- pool_url = self.auth_pool_prefix + url
- else:
- pool_url = url
- try:
- self.conn_pool = self.connection_pools[pool_url]
- except KeyError:
- self.conn_pool = ldappool.ConnectionManager(
- url,
- size=pool_size,
- retry_max=pool_retry_max,
- retry_delay=pool_retry_delay,
- timeout=pool_conn_timeout,
- connector_cls=self.Connector,
- use_tls=use_tls,
- max_lifetime=pool_conn_lifetime)
- self.connection_pools[pool_url] = self.conn_pool
-
- def set_option(self, option, invalue):
- self.conn_options[option] = invalue
-
- def get_option(self, option):
- value = self.conn_options.get(option)
- # if option was not specified explicitly, then use connection default
- # value for that option if there.
- if value is None:
- with self._get_pool_connection() as conn:
- value = conn.get_option(option)
- return value
-
- def _apply_options(self, conn):
- # if connection has a lifetime, then it already has options specified
- if conn.get_lifetime() > 30:
- return
- for option, invalue in self.conn_options.items():
- conn.set_option(option, invalue)
-
- def _get_pool_connection(self):
- return self.conn_pool.connection(self.who, self.cred)
-
- def simple_bind_s(self, who='', cred='',
- serverctrls=None, clientctrls=None):
- # Not using use_conn_pool decorator here as this API takes cred as
- # input.
- self.who = who
- self.cred = cred
- with self._get_pool_connection() as conn:
- self._apply_options(conn)
-
- def unbind_s(self):
- # After connection generator is done `with` statement execution block
- # connection is always released via finally block in ldappool.
- # So this unbind is a no op.
- pass
-
- @use_conn_pool
- def add_s(self, conn, dn, modlist):
- return conn.add_s(dn, modlist)
-
- @use_conn_pool
- def search_s(self, conn, base, scope,
- filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
- return conn.search_s(base, scope, filterstr, attrlist,
- attrsonly)
-
- def search_ext(self, base, scope,
- filterstr='(objectClass=*)', attrlist=None, attrsonly=0,
- serverctrls=None, clientctrls=None,
- timeout=-1, sizelimit=0):
- """Asynchronous API to return a ``MsgId`` instance.
-
- The ``MsgId`` instance can be safely used in a call to ``result3()``.
-
- To work with ``result3()`` API in predictable manner, the same LDAP
- connection is needed which originally provided the ``msgid``. So, this
- method wraps the existing connection and ``msgid`` in a new ``MsgId``
- instance. The connection associated with ``search_ext`` is released
- once last hard reference to the ``MsgId`` instance is freed.
-
- """
- conn_ctxt = self._get_pool_connection()
- conn = conn_ctxt.__enter__()
- try:
- msgid = conn.search_ext(base, scope,
- filterstr, attrlist, attrsonly,
- serverctrls, clientctrls,
- timeout, sizelimit)
- except Exception:
- conn_ctxt.__exit__(*sys.exc_info())
- raise
- res = MsgId((conn, msgid))
- weakref.ref(res, functools.partial(conn_ctxt.__exit__,
- None, None, None))
- return res
-
- def result3(self, msgid, all=1, timeout=None,
- resp_ctrl_classes=None):
- """This method is used to wait for and return result.
-
- This method returns the result of an operation previously initiated by
- one of the LDAP asynchronous operation routines (eg search_ext()). It
- returned an invocation identifier (a message id) upon successful
- initiation of their operation.
-
- Input msgid is expected to be instance of class MsgId which has LDAP
- session/connection used to execute search_ext and message idenfier.
-
- The connection associated with search_ext is released once last hard
- reference to MsgId object is freed. This will happen when function
- which requested msgId and used it in result3 exits.
-
- """
- conn, msg_id = msgid
- return conn.result3(msg_id, all, timeout)
-
- @use_conn_pool
- def modify_s(self, conn, dn, modlist):
- return conn.modify_s(dn, modlist)
-
- @use_conn_pool
- def delete_s(self, conn, dn):
- return conn.delete_s(dn)
-
- @use_conn_pool
- def delete_ext_s(self, conn, dn, serverctrls=None, clientctrls=None):
- return conn.delete_ext_s(dn, serverctrls, clientctrls)
-
-
-class KeystoneLDAPHandler(LDAPHandler):
- """Convert data types and perform logging.
-
- This LDAP inteface wraps the python-ldap based interfaces. The
- python-ldap interfaces require string values encoded in UTF-8. The
- OpenStack logging framework at the time of this writing is not
- capable of accepting strings encoded in UTF-8, the log functions
- will throw decoding errors if a non-ascii character appears in a
- string.
-
- Prior to the call Python data types are converted to a string
- representation as required by the LDAP APIs.
-
- Then logging is performed so we can track what is being
- sent/received from LDAP. Also the logging filters security
- sensitive items (i.e. passwords).
-
- Then the string values are encoded into UTF-8.
-
- Then the LDAP API entry point is invoked.
-
- Data returned from the LDAP call is converted back from UTF-8
- encoded strings into the Python data type used internally in
- OpenStack.
-
- """
-
- def __init__(self, conn=None):
- super(KeystoneLDAPHandler, self).__init__(conn=conn)
- self.page_size = 0
-
- def __enter__(self):
- return self
-
- def _disable_paging(self):
- # Disable the pagination from now on
- self.page_size = 0
-
- def connect(self, url, page_size=0, alias_dereferencing=None,
- use_tls=False, tls_cacertfile=None, tls_cacertdir=None,
- tls_req_cert='demand', chase_referrals=None, debug_level=None,
- use_pool=None, pool_size=None,
- pool_retry_max=None, pool_retry_delay=None,
- pool_conn_timeout=None, pool_conn_lifetime=None):
- self.page_size = page_size
- return self.conn.connect(url, page_size, alias_dereferencing,
- use_tls, tls_cacertfile, tls_cacertdir,
- tls_req_cert, chase_referrals,
- debug_level=debug_level,
- use_pool=use_pool,
- pool_size=pool_size,
- pool_retry_max=pool_retry_max,
- pool_retry_delay=pool_retry_delay,
- pool_conn_timeout=pool_conn_timeout,
- pool_conn_lifetime=pool_conn_lifetime)
-
- def set_option(self, option, invalue):
- return self.conn.set_option(option, invalue)
-
- def get_option(self, option):
- return self.conn.get_option(option)
-
- def simple_bind_s(self, who='', cred='',
- serverctrls=None, clientctrls=None):
- LOG.debug("LDAP bind: who=%s", who)
- who_utf8 = utf8_encode(who)
- cred_utf8 = utf8_encode(cred)
- return self.conn.simple_bind_s(who_utf8, cred_utf8,
- serverctrls=serverctrls,
- clientctrls=clientctrls)
-
- def unbind_s(self):
- LOG.debug("LDAP unbind")
- return self.conn.unbind_s()
-
- def add_s(self, dn, modlist):
- ldap_attrs = [(kind, [py2ldap(x) for x in safe_iter(values)])
- for kind, values in modlist]
- logging_attrs = [(kind, values
- if kind != 'userPassword'
- else ['****'])
- for kind, values in ldap_attrs]
- LOG.debug('LDAP add: dn=%s attrs=%s',
- dn, logging_attrs)
- dn_utf8 = utf8_encode(dn)
- ldap_attrs_utf8 = [(kind, [utf8_encode(x) for x in safe_iter(values)])
- for kind, values in ldap_attrs]
- return self.conn.add_s(dn_utf8, ldap_attrs_utf8)
-
- def search_s(self, base, scope,
- filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
- # NOTE(morganfainberg): Remove "None" singletons from this list, which
- # allows us to set mapped attributes to "None" as defaults in config.
- # Without this filtering, the ldap query would raise a TypeError since
- # attrlist is expected to be an iterable of strings.
- if attrlist is not None:
- attrlist = [attr for attr in attrlist if attr is not None]
- LOG.debug('LDAP search: base=%s scope=%s filterstr=%s '
- 'attrs=%s attrsonly=%s',
- base, scope, filterstr, attrlist, attrsonly)
- if self.page_size:
- ldap_result = self._paged_search_s(base, scope,
- filterstr, attrlist)
- else:
- base_utf8 = utf8_encode(base)
- filterstr_utf8 = utf8_encode(filterstr)
- if attrlist is None:
- attrlist_utf8 = None
- else:
- attrlist_utf8 = list(map(utf8_encode, attrlist))
- ldap_result = self.conn.search_s(base_utf8, scope,
- filterstr_utf8,
- attrlist_utf8, attrsonly)
-
- py_result = convert_ldap_result(ldap_result)
-
- return py_result
-
- def search_ext(self, base, scope,
- filterstr='(objectClass=*)', attrlist=None, attrsonly=0,
- serverctrls=None, clientctrls=None,
- timeout=-1, sizelimit=0):
- if attrlist is not None:
- attrlist = [attr for attr in attrlist if attr is not None]
- LOG.debug('LDAP search_ext: base=%s scope=%s filterstr=%s '
- 'attrs=%s attrsonly=%s '
- 'serverctrls=%s clientctrls=%s timeout=%s sizelimit=%s',
- base, scope, filterstr, attrlist, attrsonly,
- serverctrls, clientctrls, timeout, sizelimit)
- return self.conn.search_ext(base, scope,
- filterstr, attrlist, attrsonly,
- serverctrls, clientctrls,
- timeout, sizelimit)
-
- def _paged_search_s(self, base, scope, filterstr, attrlist=None):
- res = []
- use_old_paging_api = False
- # The API for the simple paged results control changed between
- # python-ldap 2.3 and 2.4. We need to detect the capabilities
- # of the python-ldap version we are using.
- if hasattr(ldap, 'LDAP_CONTROL_PAGE_OID'):
- use_old_paging_api = True
- lc = ldap.controls.SimplePagedResultsControl(
- controlType=ldap.LDAP_CONTROL_PAGE_OID,
- criticality=True,
- controlValue=(self.page_size, ''))
- page_ctrl_oid = ldap.LDAP_CONTROL_PAGE_OID
- else:
- lc = ldap.controls.libldap.SimplePagedResultsControl(
- criticality=True,
- size=self.page_size,
- cookie='')
- page_ctrl_oid = ldap.controls.SimplePagedResultsControl.controlType
-
- base_utf8 = utf8_encode(base)
- filterstr_utf8 = utf8_encode(filterstr)
- if attrlist is None:
- attrlist_utf8 = None
- else:
- attrlist = [attr for attr in attrlist if attr is not None]
- attrlist_utf8 = list(map(utf8_encode, attrlist))
- msgid = self.conn.search_ext(base_utf8,
- scope,
- filterstr_utf8,
- attrlist_utf8,
- serverctrls=[lc])
- # Endless loop request pages on ldap server until it has no data
- while True:
- # Request to the ldap server a page with 'page_size' entries
- rtype, rdata, rmsgid, serverctrls = self.conn.result3(msgid)
- # Receive the data
- res.extend(rdata)
- pctrls = [c for c in serverctrls
- if c.controlType == page_ctrl_oid]
- if pctrls:
- # LDAP server supports pagination
- if use_old_paging_api:
- est, cookie = pctrls[0].controlValue
- lc.controlValue = (self.page_size, cookie)
- else:
- cookie = lc.cookie = pctrls[0].cookie
-
- if cookie:
- # There is more data still on the server
- # so we request another page
- msgid = self.conn.search_ext(base_utf8,
- scope,
- filterstr_utf8,
- attrlist_utf8,
- serverctrls=[lc])
- else:
- # Exit condition no more data on server
- break
- else:
- LOG.warning(_LW('LDAP Server does not support paging. '
- 'Disable paging in keystone.conf to '
- 'avoid this message.'))
- self._disable_paging()
- break
- return res
-
- def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None,
- resp_ctrl_classes=None):
- ldap_result = self.conn.result3(msgid, all, timeout, resp_ctrl_classes)
-
- LOG.debug('LDAP result3: msgid=%s all=%s timeout=%s '
- 'resp_ctrl_classes=%s ldap_result=%s',
- msgid, all, timeout, resp_ctrl_classes, ldap_result)
-
- # ldap_result returned from result3 is a tuple of
- # (rtype, rdata, rmsgid, serverctrls). We don't need use of these,
- # except rdata.
- rtype, rdata, rmsgid, serverctrls = ldap_result
- py_result = convert_ldap_result(rdata)
- return py_result
-
- def modify_s(self, dn, modlist):
- ldap_modlist = [
- (op, kind, (None if values is None
- else [py2ldap(x) for x in safe_iter(values)]))
- for op, kind, values in modlist]
-
- logging_modlist = [(op, kind, (values if kind != 'userPassword'
- else ['****']))
- for op, kind, values in ldap_modlist]
- LOG.debug('LDAP modify: dn=%s modlist=%s',
- dn, logging_modlist)
-
- dn_utf8 = utf8_encode(dn)
- ldap_modlist_utf8 = [
- (op, kind, (None if values is None
- else [utf8_encode(x) for x in safe_iter(values)]))
- for op, kind, values in ldap_modlist]
- return self.conn.modify_s(dn_utf8, ldap_modlist_utf8)
-
- def delete_s(self, dn):
- LOG.debug("LDAP delete: dn=%s", dn)
- dn_utf8 = utf8_encode(dn)
- return self.conn.delete_s(dn_utf8)
-
- def delete_ext_s(self, dn, serverctrls=None, clientctrls=None):
- LOG.debug('LDAP delete_ext: dn=%s serverctrls=%s clientctrls=%s',
- dn, serverctrls, clientctrls)
- dn_utf8 = utf8_encode(dn)
- return self.conn.delete_ext_s(dn_utf8, serverctrls, clientctrls)
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.unbind_s()
-
-
-_HANDLERS = {}
-
-
-def register_handler(prefix, handler):
- _HANDLERS[prefix] = handler
-
-
-def _get_connection(conn_url, use_pool=False, use_auth_pool=False):
- for prefix, handler in _HANDLERS.items():
- if conn_url.startswith(prefix):
- return handler()
-
- if use_pool:
- return PooledLDAPHandler(use_auth_pool=use_auth_pool)
- else:
- return PythonLDAPHandler()
-
-
-def filter_entity(entity_ref):
- """Filter out private items in an entity dict.
-
- :param entity_ref: the entity dictionary. The 'dn' field will be removed.
- 'dn' is used in LDAP, but should not be returned to the user. This
- value may be modified.
-
- :returns: entity_ref
-
- """
- if entity_ref:
- entity_ref.pop('dn', None)
- return entity_ref
-
-
-class BaseLdap(object):
- DEFAULT_OU = None
- DEFAULT_STRUCTURAL_CLASSES = None
- DEFAULT_ID_ATTR = 'cn'
- DEFAULT_OBJECTCLASS = None
- DEFAULT_FILTER = None
- DEFAULT_EXTRA_ATTR_MAPPING = []
- DUMB_MEMBER_DN = 'cn=dumb,dc=nonexistent'
- NotFound = None
- notfound_arg = None
- options_name = None
- model = None
- attribute_options_names = {}
- immutable_attrs = []
- attribute_ignore = []
- tree_dn = None
-
- def __init__(self, conf):
- self.LDAP_URL = conf.ldap.url
- self.LDAP_USER = conf.ldap.user
- self.LDAP_PASSWORD = conf.ldap.password
- self.LDAP_SCOPE = ldap_scope(conf.ldap.query_scope)
- self.alias_dereferencing = parse_deref(conf.ldap.alias_dereferencing)
- self.page_size = conf.ldap.page_size
- self.use_tls = conf.ldap.use_tls
- self.tls_cacertfile = conf.ldap.tls_cacertfile
- self.tls_cacertdir = conf.ldap.tls_cacertdir
- self.tls_req_cert = parse_tls_cert(conf.ldap.tls_req_cert)
- self.attribute_mapping = {}
- self.chase_referrals = conf.ldap.chase_referrals
- self.debug_level = conf.ldap.debug_level
-
- # LDAP Pool specific attribute
- self.use_pool = conf.ldap.use_pool
- self.pool_size = conf.ldap.pool_size
- self.pool_retry_max = conf.ldap.pool_retry_max
- self.pool_retry_delay = conf.ldap.pool_retry_delay
- self.pool_conn_timeout = conf.ldap.pool_connection_timeout
- self.pool_conn_lifetime = conf.ldap.pool_connection_lifetime
-
- # End user authentication pool specific config attributes
- self.use_auth_pool = self.use_pool and conf.ldap.use_auth_pool
- self.auth_pool_size = conf.ldap.auth_pool_size
- self.auth_pool_conn_lifetime = conf.ldap.auth_pool_connection_lifetime
-
- if self.options_name is not None:
- self.suffix = conf.ldap.suffix
- dn = '%s_tree_dn' % self.options_name
- self.tree_dn = (getattr(conf.ldap, dn)
- or '%s,%s' % (self.DEFAULT_OU, self.suffix))
-
- idatt = '%s_id_attribute' % self.options_name
- self.id_attr = getattr(conf.ldap, idatt) or self.DEFAULT_ID_ATTR
-
- objclass = '%s_objectclass' % self.options_name
- self.object_class = (getattr(conf.ldap, objclass)
- or self.DEFAULT_OBJECTCLASS)
-
- for k, v in self.attribute_options_names.items():
- v = '%s_%s_attribute' % (self.options_name, v)
- self.attribute_mapping[k] = getattr(conf.ldap, v)
-
- attr_mapping_opt = ('%s_additional_attribute_mapping' %
- self.options_name)
- attr_mapping = (getattr(conf.ldap, attr_mapping_opt)
- or self.DEFAULT_EXTRA_ATTR_MAPPING)
- self.extra_attr_mapping = self._parse_extra_attrs(attr_mapping)
-
- ldap_filter = '%s_filter' % self.options_name
- self.ldap_filter = getattr(conf.ldap,
- ldap_filter) or self.DEFAULT_FILTER
-
- allow_create = '%s_allow_create' % self.options_name
- self.allow_create = getattr(conf.ldap, allow_create)
-
- allow_update = '%s_allow_update' % self.options_name
- self.allow_update = getattr(conf.ldap, allow_update)
-
- allow_delete = '%s_allow_delete' % self.options_name
- self.allow_delete = getattr(conf.ldap, allow_delete)
-
- member_attribute = '%s_member_attribute' % self.options_name
- self.member_attribute = getattr(conf.ldap, member_attribute, None)
-
- self.structural_classes = self.DEFAULT_STRUCTURAL_CLASSES
-
- if self.notfound_arg is None:
- self.notfound_arg = self.options_name + '_id'
-
- attribute_ignore = '%s_attribute_ignore' % self.options_name
- self.attribute_ignore = getattr(conf.ldap, attribute_ignore)
-
- self.use_dumb_member = conf.ldap.use_dumb_member
- self.dumb_member = (conf.ldap.dumb_member or
- self.DUMB_MEMBER_DN)
-
- self.subtree_delete_enabled = conf.ldap.allow_subtree_delete
-
- def _not_found(self, object_id):
- if self.NotFound is None:
- return exception.NotFound(target=object_id)
- else:
- return self.NotFound(**{self.notfound_arg: object_id})
-
- def _parse_extra_attrs(self, option_list):
- mapping = {}
- for item in option_list:
- try:
- ldap_attr, attr_map = item.split(':')
- except Exception:
- LOG.warning(_LW(
- 'Invalid additional attribute mapping: "%s". '
- 'Format must be <ldap_attribute>:<keystone_attribute>'),
- item)
- continue
- mapping[ldap_attr] = attr_map
- return mapping
-
- def _is_dumb_member(self, member_dn):
- """Checks that member is a dumb member.
-
- :param member_dn: DN of member to be checked.
- """
- return (self.use_dumb_member
- and is_dn_equal(member_dn, self.dumb_member))
-
- def get_connection(self, user=None, password=None, end_user_auth=False):
- use_pool = self.use_pool
- pool_size = self.pool_size
- pool_conn_lifetime = self.pool_conn_lifetime
-
- if end_user_auth:
- if not self.use_auth_pool:
- use_pool = False
- else:
- pool_size = self.auth_pool_size
- pool_conn_lifetime = self.auth_pool_conn_lifetime
-
- conn = _get_connection(self.LDAP_URL, use_pool,
- use_auth_pool=end_user_auth)
-
- conn = KeystoneLDAPHandler(conn=conn)
-
- conn.connect(self.LDAP_URL,
- page_size=self.page_size,
- alias_dereferencing=self.alias_dereferencing,
- use_tls=self.use_tls,
- tls_cacertfile=self.tls_cacertfile,
- tls_cacertdir=self.tls_cacertdir,
- tls_req_cert=self.tls_req_cert,
- chase_referrals=self.chase_referrals,
- debug_level=self.debug_level,
- use_pool=use_pool,
- pool_size=pool_size,
- pool_retry_max=self.pool_retry_max,
- pool_retry_delay=self.pool_retry_delay,
- pool_conn_timeout=self.pool_conn_timeout,
- pool_conn_lifetime=pool_conn_lifetime
- )
-
- if user is None:
- user = self.LDAP_USER
-
- if password is None:
- password = self.LDAP_PASSWORD
-
- # not all LDAP servers require authentication, so we don't bind
- # if we don't have any user/pass
- if user and password:
- conn.simple_bind_s(user, password)
-
- return conn
-
- def _id_to_dn_string(self, object_id):
- return u'%s=%s,%s' % (self.id_attr,
- ldap.dn.escape_dn_chars(
- six.text_type(object_id)),
- self.tree_dn)
-
- def _id_to_dn(self, object_id):
- if self.LDAP_SCOPE == ldap.SCOPE_ONELEVEL:
- return self._id_to_dn_string(object_id)
- with self.get_connection() as conn:
- search_result = conn.search_s(
- self.tree_dn, self.LDAP_SCOPE,
- u'(&(%(id_attr)s=%(id)s)(objectclass=%(objclass)s))' %
- {'id_attr': self.id_attr,
- 'id': ldap.filter.escape_filter_chars(
- six.text_type(object_id)),
- 'objclass': self.object_class},
- attrlist=DN_ONLY)
- if search_result:
- dn, attrs = search_result[0]
- return dn
- else:
- return self._id_to_dn_string(object_id)
-
- @staticmethod
- def _dn_to_id(dn):
- return utf8_decode(ldap.dn.str2dn(utf8_encode(dn))[0][0][1])
-
- def _ldap_res_to_model(self, res):
- # LDAP attribute names may be returned in a different case than
- # they are defined in the mapping, so we need to check for keys
- # in a case-insensitive way. We use the case specified in the
- # mapping for the model to ensure we have a predictable way of
- # retrieving values later.
- lower_res = {k.lower(): v for k, v in res[1].items()}
-
- id_attrs = lower_res.get(self.id_attr.lower())
- if not id_attrs:
- message = _('ID attribute %(id_attr)s not found in LDAP '
- 'object %(dn)s') % ({'id_attr': self.id_attr,
- 'dn': res[0]})
- raise exception.NotFound(message=message)
- if len(id_attrs) > 1:
- # FIXME(gyee): if this is a multi-value attribute and it has
- # multiple values, we can't use it as ID. Retain the dn_to_id
- # logic here so it does not potentially break existing
- # deployments. We need to fix our read-write LDAP logic so
- # it does not get the ID from DN.
- message = _LW('ID attribute %(id_attr)s for LDAP object %(dn)s '
- 'has multiple values and therefore cannot be used '
- 'as an ID. Will get the ID from DN instead') % (
- {'id_attr': self.id_attr,
- 'dn': res[0]})
- LOG.warning(message)
- id_val = self._dn_to_id(res[0])
- else:
- id_val = id_attrs[0]
- obj = self.model(id=id_val)
-
- for k in obj.known_keys:
- if k in self.attribute_ignore:
- continue
-
- try:
- map_attr = self.attribute_mapping.get(k, k)
- if map_attr is None:
- # Ignore attributes that are mapped to None.
- continue
-
- v = lower_res[map_attr.lower()]
- except KeyError: # nosec
- # Didn't find the attr, so don't add it.
- pass
- else:
- try:
- obj[k] = v[0]
- except IndexError:
- obj[k] = None
-
- return obj
-
- def check_allow_create(self):
- if not self.allow_create:
- action = _('LDAP %s create') % self.options_name
- raise exception.ForbiddenAction(action=action)
-
- def check_allow_update(self):
- if not self.allow_update:
- action = _('LDAP %s update') % self.options_name
- raise exception.ForbiddenAction(action=action)
-
- def check_allow_delete(self):
- if not self.allow_delete:
- action = _('LDAP %s delete') % self.options_name
- raise exception.ForbiddenAction(action=action)
-
- def affirm_unique(self, values):
- if values.get('name') is not None:
- try:
- self.get_by_name(values['name'])
- except exception.NotFound: # nosec
- # Didn't find it so it's unique, good.
- pass
- else:
- raise exception.Conflict(type=self.options_name,
- details=_('Duplicate name, %s.') %
- values['name'])
-
- if values.get('id') is not None:
- try:
- self.get(values['id'])
- except exception.NotFound: # nosec
- # Didn't find it, so it's unique, good.
- pass
- else:
- raise exception.Conflict(type=self.options_name,
- details=_('Duplicate ID, %s.') %
- values['id'])
-
- def create(self, values):
- self.affirm_unique(values)
- object_classes = self.structural_classes + [self.object_class]
- attrs = [('objectClass', object_classes)]
- for k, v in values.items():
- if k in self.attribute_ignore:
- continue
- if k == 'id':
- # no need to check if v is None as 'id' will always have
- # a value
- attrs.append((self.id_attr, [v]))
- elif v is not None:
- attr_type = self.attribute_mapping.get(k, k)
- if attr_type is not None:
- attrs.append((attr_type, [v]))
- extra_attrs = [attr for attr, name
- in self.extra_attr_mapping.items()
- if name == k]
- for attr in extra_attrs:
- attrs.append((attr, [v]))
-
- if 'groupOfNames' in object_classes and self.use_dumb_member:
- attrs.append(('member', [self.dumb_member]))
- with self.get_connection() as conn:
- conn.add_s(self._id_to_dn(values['id']), attrs)
- return values
-
- def _ldap_get(self, object_id, ldap_filter=None):
- query = (u'(&(%(id_attr)s=%(id)s)'
- u'%(filter)s'
- u'(objectClass=%(object_class)s))'
- % {'id_attr': self.id_attr,
- 'id': ldap.filter.escape_filter_chars(
- six.text_type(object_id)),
- 'filter': (ldap_filter or self.ldap_filter or ''),
- 'object_class': self.object_class})
- with self.get_connection() as conn:
- try:
- attrs = list(set(([self.id_attr] +
- list(self.attribute_mapping.values()) +
- list(self.extra_attr_mapping.keys()))))
- res = conn.search_s(self.tree_dn,
- self.LDAP_SCOPE,
- query,
- attrs)
- except ldap.NO_SUCH_OBJECT:
- return None
- try:
- return res[0]
- except IndexError:
- return None
-
- def _ldap_get_limited(self, base, scope, filterstr, attrlist, sizelimit):
- with self.get_connection() as conn:
- try:
- control = ldap.controls.libldap.SimplePagedResultsControl(
- criticality=True,
- size=sizelimit,
- cookie='')
- msgid = conn.search_ext(base, scope, filterstr, attrlist,
- serverctrls=[control])
- rdata = conn.result3(msgid)
- return rdata
- except ldap.NO_SUCH_OBJECT:
- return []
-
- @driver_hints.truncated
- def _ldap_get_all(self, hints, ldap_filter=None):
- query = u'(&%s(objectClass=%s)(%s=*))' % (
- ldap_filter or self.ldap_filter or '',
- self.object_class,
- self.id_attr)
- sizelimit = 0
- attrs = list(set(([self.id_attr] +
- list(self.attribute_mapping.values()) +
- list(self.extra_attr_mapping.keys()))))
- if hints.limit:
- sizelimit = hints.limit['limit']
- return self._ldap_get_limited(self.tree_dn,
- self.LDAP_SCOPE,
- query,
- attrs,
- sizelimit)
- with self.get_connection() as conn:
- try:
- return conn.search_s(self.tree_dn,
- self.LDAP_SCOPE,
- query,
- attrs)
- except ldap.NO_SUCH_OBJECT:
- return []
-
- def _ldap_get_list(self, search_base, scope, query_params=None,
- attrlist=None):
- query = u'(objectClass=%s)' % self.object_class
- if query_params:
-
- def calc_filter(attrname, value):
- val_esc = ldap.filter.escape_filter_chars(value)
- return '(%s=%s)' % (attrname, val_esc)
-
- query = (u'(&%s%s)' %
- (query, ''.join([calc_filter(k, v) for k, v in
- query_params.items()])))
- with self.get_connection() as conn:
- return conn.search_s(search_base, scope, query, attrlist)
-
- def get(self, object_id, ldap_filter=None):
- res = self._ldap_get(object_id, ldap_filter)
- if res is None:
- raise self._not_found(object_id)
- else:
- return self._ldap_res_to_model(res)
-
- def get_by_name(self, name, ldap_filter=None):
- query = (u'(%s=%s)' % (self.attribute_mapping['name'],
- ldap.filter.escape_filter_chars(
- six.text_type(name))))
- res = self.get_all(query)
- try:
- return res[0]
- except IndexError:
- raise self._not_found(name)
-
- def get_all(self, ldap_filter=None, hints=None):
- hints = hints or driver_hints.Hints()
- return [self._ldap_res_to_model(x)
- for x in self._ldap_get_all(hints, ldap_filter)]
-
- def update(self, object_id, values, old_obj=None):
- if old_obj is None:
- old_obj = self.get(object_id)
-
- modlist = []
- for k, v in values.items():
- if k == 'id':
- # id can't be modified.
- continue
-
- if k in self.attribute_ignore:
-
- # Handle 'enabled' specially since can't disable if ignored.
- if k == 'enabled' and (not v):
- action = _("Disabling an entity where the 'enable' "
- "attribute is ignored by configuration.")
- raise exception.ForbiddenAction(action=action)
-
- continue
-
- # attribute value has not changed
- if k in old_obj and old_obj[k] == v:
- continue
-
- if k in self.immutable_attrs:
- msg = (_("Cannot change %(option_name)s %(attr)s") %
- {'option_name': self.options_name, 'attr': k})
- raise exception.ValidationError(msg)
-
- if v is None:
- if old_obj.get(k) is not None:
- modlist.append((ldap.MOD_DELETE,
- self.attribute_mapping.get(k, k),
- None))
- continue
-
- current_value = old_obj.get(k)
- if current_value is None:
- op = ldap.MOD_ADD
- modlist.append((op, self.attribute_mapping.get(k, k), [v]))
- elif current_value != v:
- op = ldap.MOD_REPLACE
- modlist.append((op, self.attribute_mapping.get(k, k), [v]))
-
- if modlist:
- with self.get_connection() as conn:
- try:
- conn.modify_s(self._id_to_dn(object_id), modlist)
- except ldap.NO_SUCH_OBJECT:
- raise self._not_found(object_id)
-
- return self.get(object_id)
-
- def delete(self, object_id):
- with self.get_connection() as conn:
- try:
- conn.delete_s(self._id_to_dn(object_id))
- except ldap.NO_SUCH_OBJECT:
- raise self._not_found(object_id)
-
- def delete_tree(self, object_id):
- tree_delete_control = ldap.controls.LDAPControl(CONTROL_TREEDELETE,
- 0,
- None)
- with self.get_connection() as conn:
- try:
- conn.delete_ext_s(self._id_to_dn(object_id),
- serverctrls=[tree_delete_control])
- except ldap.NO_SUCH_OBJECT:
- raise self._not_found(object_id)
- except ldap.NOT_ALLOWED_ON_NONLEAF:
- # Most LDAP servers do not support the tree_delete_control.
- # In these servers, the usual idiom is to first perform a
- # search to get the entries to delete, then delete them in
- # in order of child to parent, since LDAP forbids the
- # deletion of a parent entry before deleting the children
- # of that parent. The simplest way to do that is to delete
- # the entries in order of the length of the DN, from longest
- # to shortest DN.
- dn = self._id_to_dn(object_id)
- scope = ldap.SCOPE_SUBTREE
- # With some directory servers, an entry with objectclass
- # ldapsubentry will not be returned unless it is explicitly
- # requested, by specifying the objectclass in the search
- # filter. We must specify this, with objectclass=*, in an
- # LDAP filter OR clause, in order to return all entries
- filt = '(|(objectclass=*)(objectclass=ldapsubentry))'
- # We only need the DNs of the entries. Since no attributes
- # will be returned, we do not have to specify attrsonly=1.
- entries = conn.search_s(dn, scope, filt, attrlist=DN_ONLY)
- if entries:
- for dn in sorted((e[0] for e in entries),
- key=len, reverse=True):
- conn.delete_s(dn)
- else:
- LOG.debug('No entries in LDAP subtree %s', dn)
-
- def add_member(self, member_dn, member_list_dn):
- """Add member to the member list.
-
- :param member_dn: DN of member to be added.
- :param member_list_dn: DN of group to which the
- member will be added.
-
- :raises keystone.exception.Conflict: If the user was already a member.
- :raises self.NotFound: If the group entry didn't exist.
- """
- with self.get_connection() as conn:
- try:
- mod = (ldap.MOD_ADD, self.member_attribute, member_dn)
- conn.modify_s(member_list_dn, [mod])
- except ldap.TYPE_OR_VALUE_EXISTS:
- raise exception.Conflict(_('Member %(member)s '
- 'is already a member'
- ' of group %(group)s') % {
- 'member': member_dn,
- 'group': member_list_dn})
- except ldap.NO_SUCH_OBJECT:
- raise self._not_found(member_list_dn)
-
- def remove_member(self, member_dn, member_list_dn):
- """Remove member from the member list.
-
- :param member_dn: DN of member to be removed.
- :param member_list_dn: DN of group from which the
- member will be removed.
-
- :raises self.NotFound: If the group entry didn't exist.
- :raises ldap.NO_SUCH_ATTRIBUTE: If the user wasn't a member.
- """
- with self.get_connection() as conn:
- try:
- mod = (ldap.MOD_DELETE, self.member_attribute, member_dn)
- conn.modify_s(member_list_dn, [mod])
- except ldap.NO_SUCH_OBJECT:
- raise self._not_found(member_list_dn)
-
- def _delete_tree_nodes(self, search_base, scope, query_params=None):
- query = u'(objectClass=%s)' % self.object_class
- if query_params:
- query = (u'(&%s%s)' %
- (query, ''.join(['(%s=%s)'
- % (k, ldap.filter.escape_filter_chars(v))
- for k, v in
- query_params.items()])))
- not_deleted_nodes = []
- with self.get_connection() as conn:
- try:
- nodes = conn.search_s(search_base, scope, query,
- attrlist=DN_ONLY)
- except ldap.NO_SUCH_OBJECT:
- LOG.debug('Could not find entry with dn=%s', search_base)
- raise self._not_found(self._dn_to_id(search_base))
- else:
- for node_dn, _t in nodes:
- try:
- conn.delete_s(node_dn)
- except ldap.NO_SUCH_OBJECT:
- not_deleted_nodes.append(node_dn)
-
- if not_deleted_nodes:
- LOG.warning(_LW("When deleting entries for %(search_base)s, "
- "could not delete nonexistent entries "
- "%(entries)s%(dots)s"),
- {'search_base': search_base,
- 'entries': not_deleted_nodes[:3],
- 'dots': '...' if len(not_deleted_nodes) > 3 else ''})
-
- def filter_query(self, hints, query=None):
- """Applies filtering to a query.
-
- :param hints: contains the list of filters, which may be None,
- indicating that there are no filters to be applied.
- If it's not None, then any filters satisfied here will be
- removed so that the caller will know if any filters
- remain to be applied.
- :param query: LDAP query into which to include filters
-
- :returns query: LDAP query, updated with any filters satisfied
-
- """
- def build_filter(filter_, hints):
- """Build a filter for the query.
-
- :param filter_: the dict that describes this filter
- :param hints: contains the list of filters yet to be satisfied.
-
- :returns query: LDAP query term to be added
-
- """
- ldap_attr = self.attribute_mapping[filter_['name']]
- val_esc = ldap.filter.escape_filter_chars(filter_['value'])
-
- if filter_['case_sensitive']:
- # NOTE(henry-nash): Although dependent on the schema being
- # used, most LDAP attributes are configured with case
- # insensitive matching rules, so we'll leave this to the
- # controller to filter.
- return
-
- if filter_['name'] == 'enabled':
- # NOTE(henry-nash): Due to the different options for storing
- # the enabled attribute (e,g, emulated or not), for now we
- # don't try and filter this at the driver level - we simply
- # leave the filter to be handled by the controller. It seems
- # unlikley that this will cause a signifcant performance
- # issue.
- return
-
- # TODO(henry-nash): Currently there are no booleans (other than
- # 'enabled' that is handled above) on which you can filter. If
- # there were, we would need to add special handling here to
- # convert the booleans values to 'TRUE' and 'FALSE'. To do that
- # we would also need to know which filter keys were actually
- # booleans (this is related to bug #1411478).
-
- if filter_['comparator'] == 'equals':
- query_term = (u'(%(attr)s=%(val)s)'
- % {'attr': ldap_attr, 'val': val_esc})
- elif filter_['comparator'] == 'contains':
- query_term = (u'(%(attr)s=*%(val)s*)'
- % {'attr': ldap_attr, 'val': val_esc})
- elif filter_['comparator'] == 'startswith':
- query_term = (u'(%(attr)s=%(val)s*)'
- % {'attr': ldap_attr, 'val': val_esc})
- elif filter_['comparator'] == 'endswith':
- query_term = (u'(%(attr)s=*%(val)s)'
- % {'attr': ldap_attr, 'val': val_esc})
- else:
- # It's a filter we don't understand, so let the caller
- # work out if they need to do something with it.
- return
-
- return query_term
-
- if query is None:
- # make sure query is a string so the ldap filter is properly
- # constructed from filter_list later
- query = ''
-
- if hints is None:
- return query
-
- filter_list = []
- satisfied_filters = []
-
- for filter_ in hints.filters:
- if filter_['name'] not in self.attribute_mapping:
- continue
- new_filter = build_filter(filter_, hints)
- if new_filter is not None:
- filter_list.append(new_filter)
- satisfied_filters.append(filter_)
-
- if filter_list:
- query = u'(&%s%s)' % (query, ''.join(filter_list))
-
- # Remove satisfied filters, then the caller will know remaining filters
- for filter_ in satisfied_filters:
- hints.filters.remove(filter_)
-
- return query
-
-
-class EnabledEmuMixIn(BaseLdap):
- """Emulates boolean 'enabled' attribute if turned on.
-
- Creates a group holding all enabled objects of this class, all missing
- objects are considered disabled.
-
- Options:
-
- * $name_enabled_emulation - boolean, on/off
- * $name_enabled_emulation_dn - DN of that group, default is
- cn=enabled_${name}s,${tree_dn}
- * $name_enabled_emulation_use_group_config - boolean, on/off
-
- Where ${name}s is the plural of self.options_name ('users' or 'tenants'),
- ${tree_dn} is self.tree_dn.
- """
-
- DEFAULT_GROUP_OBJECTCLASS = 'groupOfNames'
- DEFAULT_MEMBER_ATTRIBUTE = 'member'
-
- def __init__(self, conf):
- super(EnabledEmuMixIn, self).__init__(conf)
- enabled_emulation = '%s_enabled_emulation' % self.options_name
- self.enabled_emulation = getattr(conf.ldap, enabled_emulation)
-
- enabled_emulation_dn = '%s_enabled_emulation_dn' % self.options_name
- self.enabled_emulation_dn = getattr(conf.ldap, enabled_emulation_dn)
-
- use_group_config = ('%s_enabled_emulation_use_group_config' %
- self.options_name)
- self.use_group_config = getattr(conf.ldap, use_group_config)
-
- if not self.use_group_config:
- self.member_attribute = self.DEFAULT_MEMBER_ATTRIBUTE
- self.group_objectclass = self.DEFAULT_GROUP_OBJECTCLASS
- else:
- self.member_attribute = conf.ldap.group_member_attribute
- self.group_objectclass = conf.ldap.group_objectclass
-
- if not self.enabled_emulation_dn:
- naming_attr_name = 'cn'
- naming_attr_value = 'enabled_%ss' % self.options_name
- sub_vals = (naming_attr_name, naming_attr_value, self.tree_dn)
- self.enabled_emulation_dn = '%s=%s,%s' % sub_vals
- naming_attr = (naming_attr_name, [naming_attr_value])
- else:
- # Extract the attribute name and value from the configured DN.
- naming_dn = ldap.dn.str2dn(utf8_encode(self.enabled_emulation_dn))
- naming_rdn = naming_dn[0][0]
- naming_attr = (utf8_decode(naming_rdn[0]),
- utf8_decode(naming_rdn[1]))
- self.enabled_emulation_naming_attr = naming_attr
-
- def _get_enabled(self, object_id, conn):
- dn = self._id_to_dn(object_id)
- query = '(%s=%s)' % (self.member_attribute,
- ldap.filter.escape_filter_chars(dn))
- try:
- enabled_value = conn.search_s(self.enabled_emulation_dn,
- ldap.SCOPE_BASE,
- query, attrlist=DN_ONLY)
- except ldap.NO_SUCH_OBJECT:
- return False
- else:
- return bool(enabled_value)
-
- def _add_enabled(self, object_id):
- with self.get_connection() as conn:
- if not self._get_enabled(object_id, conn):
- modlist = [(ldap.MOD_ADD,
- self.member_attribute,
- [self._id_to_dn(object_id)])]
- try:
- conn.modify_s(self.enabled_emulation_dn, modlist)
- except ldap.NO_SUCH_OBJECT:
- attr_list = [('objectClass', [self.group_objectclass]),
- (self.member_attribute,
- [self._id_to_dn(object_id)]),
- self.enabled_emulation_naming_attr]
- if self.use_dumb_member:
- attr_list[1][1].append(self.dumb_member)
- conn.add_s(self.enabled_emulation_dn, attr_list)
-
- def _remove_enabled(self, object_id):
- modlist = [(ldap.MOD_DELETE,
- self.member_attribute,
- [self._id_to_dn(object_id)])]
- with self.get_connection() as conn:
- try:
- conn.modify_s(self.enabled_emulation_dn, modlist)
- except (ldap.NO_SUCH_OBJECT, ldap.NO_SUCH_ATTRIBUTE): # nosec
- # It's already gone, good.
- pass
-
- def create(self, values):
- if self.enabled_emulation:
- enabled_value = values.pop('enabled', True)
- ref = super(EnabledEmuMixIn, self).create(values)
- if 'enabled' not in self.attribute_ignore:
- if enabled_value:
- self._add_enabled(ref['id'])
- ref['enabled'] = enabled_value
- return ref
- else:
- return super(EnabledEmuMixIn, self).create(values)
-
- def get(self, object_id, ldap_filter=None):
- with self.get_connection() as conn:
- ref = super(EnabledEmuMixIn, self).get(object_id, ldap_filter)
- if ('enabled' not in self.attribute_ignore and
- self.enabled_emulation):
- ref['enabled'] = self._get_enabled(object_id, conn)
- return ref
-
- def get_all(self, ldap_filter=None, hints=None):
- hints = hints or driver_hints.Hints()
- if 'enabled' not in self.attribute_ignore and self.enabled_emulation:
- # had to copy BaseLdap.get_all here to ldap_filter by DN
- tenant_list = [self._ldap_res_to_model(x)
- for x in self._ldap_get_all(hints, ldap_filter)
- if x[0] != self.enabled_emulation_dn]
- with self.get_connection() as conn:
- for tenant_ref in tenant_list:
- tenant_ref['enabled'] = self._get_enabled(
- tenant_ref['id'], conn)
- return tenant_list
- else:
- return super(EnabledEmuMixIn, self).get_all(ldap_filter, hints)
-
- def update(self, object_id, values, old_obj=None):
- if 'enabled' not in self.attribute_ignore and self.enabled_emulation:
- data = values.copy()
- enabled_value = data.pop('enabled', None)
- ref = super(EnabledEmuMixIn, self).update(object_id, data, old_obj)
- if enabled_value is not None:
- if enabled_value:
- self._add_enabled(object_id)
- else:
- self._remove_enabled(object_id)
- ref['enabled'] = enabled_value
- return ref
- else:
- return super(EnabledEmuMixIn, self).update(
- object_id, values, old_obj)
-
- def delete(self, object_id):
- if self.enabled_emulation:
- self._remove_enabled(object_id)
- super(EnabledEmuMixIn, self).delete(object_id)