"Fossies" - the Fresh Open Source Software Archive

Member "privacyidea-3.6.2/privacyidea/lib/resolvers/LDAPIdResolver.py" (22 Jul 2021, 56273 Bytes) of package /linux/misc/privacyidea-3.6.2.tar.gz:


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

    1 # -*- coding: utf-8 -*-
    2 #  Copyright (C) 2014 Cornelius Kölbel
    3 #  contact:  corny@cornelinux.de
    4 #
    5 #  2018-12-14 Cornelius Kölbel <cornelius.koelbel@netknights.it>
    6 #             Add censored password functionality
    7 #  2017-12-22 Cornelius Kölbel <cornelius.koelbel@netknights.it>
    8 #             Add configurable multi-value-attributes
    9 #             with the help of Nomen Nescio
   10 #  2017-07-20 Cornelius Kölbel <cornelius.koelbel@netknights.it>
   11 #             Fix unicode usernames
   12 #  2017-01-23 Cornelius Kölbel <cornelius.koelbel@netknights.it>
   13 #             Add certificate verification
   14 #  2017-01-07 Cornelius Kölbel <cornelius.koelbel@netknights.it>
   15 #             Use get_info=ldap3.NONE for binds to avoid querying of subschema
   16 #             Remove LDAPFILTER and self.reversefilter
   17 #  2016-07-14 Cornelius Kölbel <cornelius.koelbel@netknights.it>
   18 #             Adding getUserId cache.
   19 #  2016-04-13 Cornelius Kölbel <cornelius.koelbel@netknights.it>
   20 #             Add object_classes and dn_composition to configuration
   21 #             to allow flexible user_add
   22 #  2016-04-10 Martin Wheldon <martin.wheldon@greenhills-it.co.uk>
   23 #             Allow user accounts held in LDAP to be edited, providing
   24 #             that the account they are using has permission to edit
   25 #             those attributes in the LDAP directory
   26 #  2016-02-22 Salvo Rapisarda
   27 #             Allow objectGUID to be a users attribute
   28 #  2016-02-19 Cornelius Kölbel <cornelius.koelbel@netknights.it>
   29 #             Allow objectGUID to be the uid.
   30 #  2015-10-05 Cornelius Kölbel <cornelius.koelbel@netknights.it>
   31 #             Remove reverse_map, so that one LDAP field can map
   32 #             to several privacyIDEA fields.
   33 #  2015-04-16 Cornelius Kölbel <cornelius.koelbel@netknights.it>
   34 #             Add redundancy with LDAP3 Server pools. Round Robin Strategy
   35 #  2015-04-15 Cornelius Kölbel <cornelius.koelbel@netknights.it>
   36 #             Increase test coverage
   37 #  2014-12-25 Cornelius Kölbel <cornelius@privacyidea.org>
   38 #             Rewrite for flask migration
   39 #
   40 # This code is free software; you can redistribute it and/or
   41 # modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
   42 # License as published by the Free Software Foundation; either
   43 # version 3 of the License, or any later version.
   44 #
   45 # This code is distributed in the hope that it will be useful,
   46 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   47 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   48 # GNU AFFERO GENERAL PUBLIC LICENSE for more details.
   49 #
   50 # You should have received a copy of the GNU Affero General Public
   51 # License along with this program.  If not, see <http://www.gnu.org/licenses/>.
   52 #
   53 __doc__ = """This is the resolver to find users in LDAP directories like
   54 OpenLDAP and Active Directory.
   55 
   56 The file is tested in tests/test_lib_resolver.py
   57 """
   58 
   59 import logging
   60 import yaml
   61 import threading
   62 import functools
   63 import six
   64 
   65 from .UserIdResolver import UserIdResolver
   66 
   67 import ldap3
   68 from ldap3 import MODIFY_REPLACE, MODIFY_ADD, MODIFY_DELETE
   69 from ldap3 import Tls
   70 from ldap3.core.exceptions import LDAPOperationResult
   71 from ldap3.core.results import RESULT_SIZE_LIMIT_EXCEEDED
   72 import ssl
   73 
   74 import os.path
   75 
   76 import traceback
   77 from passlib.hash import ldap_salted_sha1
   78 import hashlib
   79 import binascii
   80 from privacyidea.lib.utils import is_true
   81 from privacyidea.lib.framework import get_app_local_store, get_app_config_value
   82 import datetime
   83 
   84 from privacyidea.lib import _
   85 from privacyidea.lib.utils import to_utf8, to_unicode, convert_column_to_unicode
   86 from privacyidea.lib.error import privacyIDEAError
   87 import uuid
   88 from ldap3.utils.conv import escape_bytes
   89 from operator import itemgetter
   90 
   91 CACHE = {}
   92 
   93 log = logging.getLogger(__name__)
   94 ENCODING = "utf-8"
   95 # The number of rounds the resolver tries to reach a responding server in the
   96 #  pool
   97 SERVERPOOL_ROUNDS = 2
   98 # The number of seconds a non-responding server is removed from the server pool
   99 SERVERPOOL_SKIP = 30
  100 
  101 # 1 sec == 10^9 nano secs == 10^7 * (100 nano secs)
  102 MS_AD_MULTIPLYER = 10 ** 7
  103 MS_AD_START = datetime.datetime(1601, 1, 1)
  104 
  105 if os.path.isfile("/etc/privacyidea/ldap-ca.crt"):
  106     DEFAULT_CA_FILE = "/etc/privacyidea/ldap-ca.crt"
  107 elif os.path.isfile("/etc/ssl/certs/ca-certificates.crt"):
  108     DEFAULT_CA_FILE = "/etc/ssl/certs/ca-certificates.crt"
  109 elif os.path.isfile("/etc/ssl/certs/ca-bundle.crt"):
  110     DEFAULT_CA_FILE = "/etc/ssl/certs/ca-bundle.crt"
  111 else:
  112     DEFAULT_CA_FILE = "/etc/privacyidea/ldap-ca.crt"
  113 
  114 try:
  115     TLS_NEGOTIATE_PROTOCOL = ssl.PROTOCOL_TLS
  116 except AttributeError as _e:
  117     # this is Python < 2.7.13, it does not provide ssl.PROTOCOL_TLS
  118     TLS_NEGOTIATE_PROTOCOL = ssl.PROTOCOL_SSLv23
  119 
  120 DEFAULT_TLS_PROTOCOL = TLS_NEGOTIATE_PROTOCOL
  121 
  122 TLS_OPTIONS_1_3 = (ssl.OP_NO_TLSv1_2, ssl.OP_NO_TLSv1_1, ssl.OP_NO_TLSv1, ssl.OP_NO_SSLv3)
  123 
  124 
  125 class LockingServerPool(ldap3.ServerPool):
  126     """
  127     A ``ServerPool`` subclass that uses a RLock to synchronize invocations of
  128     ``initialize``, ``get_server`` and ``get_current_server``.
  129 
  130     We synchronize invocations to rule out race conditions when multiple threads
  131     try to manipulate the server pool state concurrently.
  132 
  133     We use a ``RLock`` instead of a simple ``Lock`` to avoid locking ourselves.
  134     """
  135     def __init__(self, *args, **kwargs):
  136         ldap3.ServerPool.__init__(self, *args, **kwargs)
  137         self._lock = threading.RLock()
  138 
  139     def initialize(self, connection):
  140         with self._lock:
  141             return ldap3.ServerPool.initialize(self, connection)
  142 
  143     def get_server(self, connection):
  144         with self._lock:
  145             return ldap3.ServerPool.get_server(self, connection)
  146 
  147     def get_current_server(self, connection):
  148         with self._lock:
  149             return ldap3.ServerPool.get_current_server(self, connection)
  150 
  151 
  152 def get_ad_timestamp_now():
  153     """
  154     returns the current UTC time as it is used in Active Directory in the
  155     attribute accountExpires.
  156     This is 100-nano-secs since 1.1.1601
  157 
  158     :return: time
  159     :rtype: int
  160     """
  161     utc_now = datetime.datetime.utcnow()
  162     elapsed_time = utc_now - MS_AD_START
  163     total_seconds = elapsed_time.total_seconds()
  164     # convert this to (100 nanoseconds)
  165     return int(MS_AD_MULTIPLYER * total_seconds)
  166 
  167 
  168 def trim_objectGUID(userId):
  169     userId = uuid.UUID(u"{{{0!s}}}".format(userId)).bytes_le
  170     userId = escape_bytes(userId)
  171     return userId
  172 
  173 
  174 def get_info_configuration(noschemas):
  175     """
  176     Given the value of the NOSCHEMAS config option, return the value that should
  177     be passed as ldap3's `get_info` argument.
  178     :param noschemas: a boolean
  179     :return: one of ldap3.SCHEMA or ldap3.NONE
  180     """
  181     get_schema_info = ldap3.SCHEMA
  182     if noschemas:
  183         get_schema_info = ldap3.NONE
  184     log.debug("Get LDAP schema info: {0!r}".format(get_schema_info))
  185     return get_schema_info
  186 
  187 
  188 def ignore_sizelimit_exception(conn, generator):
  189     """
  190     Wrapper for ``paged_search``, which (since ldap3 2.3) throws an exception if the size limit has been
  191     reached. This function wraps the generator and ignores this exception.
  192 
  193     Additionally, this checks ``conn.response`` for any leftover entries that were not yet returned
  194     by the generator and yields them.
  195     """
  196     last_entry = None
  197     while True:
  198         try:
  199             last_entry = next(generator)
  200             yield last_entry
  201         except StopIteration:
  202             # If the generator is exceed, we stop
  203             break
  204         except LDAPOperationResult as e:
  205             # If the size limit has been reached, we stop. All other exceptions are re-raised.
  206             if e.result == RESULT_SIZE_LIMIT_EXCEEDED:
  207                 # Workaround: In ldap3 <= 2.4.1, the generator may "forget" to yield some entries that
  208                 # were transmitted just before the "size limit exceeded" message. In other words,
  209                 # the exception is raised *before* the generator has yielded those entries.
  210                 # These leftover entries can still be found in ``conn.response``, so we
  211                 # just yield them here.
  212                 # However, as future versions of ldap3 may fix this behavior and
  213                 # may actually yield those elements as well, this workaround may result in
  214                 # duplicate entries.
  215                 # Thus, we check if the last entry we got from the generator can be found
  216                 # in ``conn.response``. If that is the case, we assume the generator works correctly
  217                 # and *all* of ``conn.response`` have been yielded already.
  218                 if last_entry is None or last_entry not in conn.response:
  219                     for entry in conn.response:
  220                         yield entry
  221                 break
  222             else:
  223                 raise
  224 
  225 
  226 def cache(func):
  227     """
  228     cache the user with his loginname, resolver and UID in a local 
  229     dictionary cache.
  230     This is a per process cache.
  231     """
  232     @functools.wraps(func)
  233     def cache_wrapper(self, *args, **kwds):
  234         # Only run the code, in case we have a configured cache!
  235         if self.cache_timeout > 0:
  236             # If it does not exist, create the node for this instance
  237             resolver_id = self.getResolverId()
  238             now = datetime.datetime.now()
  239             tdelta = datetime.timedelta(seconds=self.cache_timeout)
  240             if not resolver_id in CACHE:
  241                 CACHE[resolver_id] = {"getUserId": {},
  242                                       "getUserInfo": {},
  243                                       "_getDN": {}}
  244             else:
  245                 # Clean up the cache in the current resolver and the current function
  246                 _to_be_deleted = []
  247                 try:
  248                     for user, cached_result in CACHE[resolver_id].get(func.__name__).items():
  249                         if now > cached_result.get("timestamp") + tdelta:
  250                             _to_be_deleted.append(user)
  251                 except RuntimeError:
  252                     # This might happen if thread A evicts an expired
  253                     # cache entry while thread B looks for expired cache entries
  254                     pass
  255                 for user in _to_be_deleted:
  256                     try:
  257                         del CACHE[resolver_id][func.__name__][user]
  258                     except KeyError:
  259                         pass
  260                 del _to_be_deleted
  261 
  262             # get the portion of the cache for this very LDAP resolver
  263             r_cache = CACHE.get(resolver_id).get(func.__name__)
  264             entry = r_cache.get(args[0])
  265             if entry and now < entry.get("timestamp") + tdelta:
  266                 log.debug("Reading {0!r} from cache for {1!r}".format(args[0], func.__name__))
  267                 return entry.get("value")
  268 
  269         f_result = func(self, *args, **kwds)
  270 
  271         if self.cache_timeout > 0:
  272             # now we cache the result
  273             CACHE[resolver_id][func.__name__][args[0]] = {
  274                 "value": f_result,
  275                 "timestamp": now}
  276 
  277         return f_result
  278 
  279     return cache_wrapper
  280 
  281 
  282 class AUTHTYPE(object):
  283     SIMPLE = "Simple"
  284     SASL_DIGEST_MD5 = "SASL Digest-MD5"
  285     NTLM = "NTLM"
  286 
  287 
  288 class IdResolver (UserIdResolver):
  289 
  290     # If the resolver could be configured editable
  291     updateable = True
  292 
  293     def __init__(self):
  294         self.i_am_bound = False
  295         self.uri = ""
  296         self.basedn = ""
  297         self.binddn = ""
  298         self.bindpw = ""
  299         self.object_classes = []
  300         self.dn_template = ""
  301         self.timeout = 5.0  # seconds!
  302         self.sizelimit = 500
  303         self.loginname_attribute = [""]
  304         self.searchfilter = u""
  305         self.userinfo = {}
  306         self.multivalueattributes = []
  307         self.uidtype = ""
  308         self.noreferrals = False
  309         self._editable = False
  310         self.resolverId = self.uri
  311         self.scope = ldap3.SUBTREE
  312         self.cache_timeout = 120
  313         self.tls_context = None
  314         self.start_tls = False
  315         self.serverpool_persistent = False
  316         self.serverpool_rounds = SERVERPOOL_ROUNDS
  317         self.serverpool_skip = SERVERPOOL_SKIP
  318         self.serverpool = None
  319         # The number of seconds that ldap3 waits if no server is left in the pool, before
  320         # starting the next round
  321         pooling_loop_timeout = get_app_config_value("PI_LDAP_POOLING_LOOP_TIMEOUT", 10)
  322         log.info("Setting system wide POOLING_LOOP_TIMEOUT to {0!s}.".format(pooling_loop_timeout))
  323         ldap3.set_config_parameter("POOLING_LOOP_TIMEOUT", pooling_loop_timeout)
  324 
  325     def checkPass(self, uid, password):
  326         """
  327         This function checks the password for a given uid.
  328         - returns true in case of success
  329         -         false if password does not match
  330 
  331         """
  332         if self.authtype == AUTHTYPE.NTLM:  # pragma: no cover
  333             # fetch the PreWindows 2000 Domain from the self.binddn
  334             # which would be of the format DOMAIN\username and compose the
  335             # bind_user to DOMAIN\sAMAcountName
  336             domain_name = self.binddn.split('\\')[0]
  337             uinfo = self.getUserInfo(uid)
  338             # In fact we need the sAMAccountName. If the username mapping is
  339             # another attribute than the sAMAccountName the authentication
  340             # will fail!
  341             bind_user = u"{0!s}\\{1!s}".format(domain_name, uinfo.get("username"))
  342         else:
  343             bind_user = self._getDN(uid)
  344 
  345         if not self.serverpool:
  346             self.serverpool = self.get_serverpool_instance(get_info=ldap3.NONE)
  347 
  348         try:
  349             log.debug("Authtype: {0!r}".format(self.authtype))
  350             log.debug("user    : {0!r}".format(bind_user))
  351             # Whatever happens. If we have an empty bind_user, we must break
  352             # since we must avoid anonymous binds!
  353             if not bind_user or len(bind_user) < 1:
  354                 raise Exception("No valid user. Empty bind_user.")
  355             l = self.create_connection(authtype=self.authtype,
  356                                        server=self.serverpool,
  357                                        user=bind_user,
  358                                        password=password,
  359                                        receive_timeout=self.timeout,
  360                                        auto_referrals=not self.noreferrals,
  361                                        start_tls=self.start_tls)
  362             r = l.bind()
  363             log.debug("bind result: {0!r}".format(r))
  364             if not r:
  365                 raise Exception("Wrong credentials")
  366             log.debug("bind seems successful.")
  367             l.unbind()
  368             log.debug("unbind successful.")
  369         except Exception as e:
  370             log.warning("failed to check password for {0!r}/{1!r}: {2!r}".format(uid, bind_user, e))
  371             log.debug(traceback.format_exc())
  372             return False
  373 
  374         return True
  375 
  376     def _trim_result(self, result_list):
  377         """
  378         The resultlist can contain entries of type:searchResEntry and of
  379         type:searchResRef. If self.noreferrals is true, all type:searchResRef
  380         will be removed.
  381 
  382         :param result_list: The result list of a LDAP search
  383         :type result_list: resultlist (list of dicts)
  384         :return: new resultlist
  385         """
  386         if self.noreferrals:
  387             new_list = []
  388             for result in result_list:
  389                 if result.get("type") == "searchResEntry":
  390                     new_list.append(result)
  391                 elif result.get("type") == "searchResRef":
  392                     # This is a Referral
  393                     pass
  394         else:
  395             new_list = result_list
  396 
  397         return new_list
  398 
  399     @staticmethod
  400     def _escape_loginname(loginname):
  401         """
  402         This function escapes the loginname according to
  403         https://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx
  404         This is to avoid username guessing by trying to login as user
  405            a*
  406            ac*
  407            ach*
  408            achm*
  409            achme*
  410            achemd*
  411 
  412         :param loginname: The loginname
  413         :return: The escaped loginname
  414         """
  415         return loginname.replace("\\", "\\5c").replace("*", "\\2a").replace(
  416             "(", "\\28").replace(")", "\\29").replace("/", "\\2f")
  417 
  418     @staticmethod
  419     def _get_uid(entry, uidtype):
  420         uid = None
  421         if uidtype.lower() == "dn":
  422            uid = entry.get("dn")
  423         else:
  424             attributes = entry.get("attributes")
  425             if type(attributes.get(uidtype)) == list:
  426                 uid = attributes.get(uidtype)[0]
  427             else:
  428                 uid = attributes.get(uidtype)
  429             if uidtype.lower() == "objectguid":
  430                 # For ldap3 versions <= 2.4.1, objectGUID attribute values are returned as UUID strings.
  431                 # For versions greater than 2.4.1, they are returned in the curly-braced string
  432                 # representation, i.e. objectGUID := "{" UUID "}"
  433                 # In order to ensure backwards compatibility for user mappings,
  434                 # we strip the curly braces from objectGUID values.
  435                 # If we are using ldap3 <= 2.4.1, there are no curly braces and we leave the value unchanged.
  436                 try:
  437                     uid = convert_column_to_unicode(uid).strip("{").strip("}")
  438                 except UnicodeDecodeError as e:
  439                     # in some weird cases we sometimes get a byte-array here
  440                     # which resembles a uuid. So we just convert it to one...
  441                     # We might run into endian-issues here depending on what
  442                     # ldap3/AD returns.
  443                     log.warning('Found a byte-array as uid ({0!s}), trying to '
  444                                 'convert it to a UUID. ({1!s})'.format(binascii.hexlify(uid),
  445                                                                        e))
  446                     log.debug(traceback.format_exc())
  447                     uid = str(uuid.UUID(bytes=uid))
  448         return convert_column_to_unicode(uid)
  449 
  450     def _trim_user_id(self, userId):
  451         """
  452         If we search for the objectGUID we can not search for the normal
  453         string representation but we need to search for the bytestring in AD.
  454         :param userId: The userId
  455         :return: the trimmed userId
  456         """
  457         if self.uidtype == "objectGUID":
  458             userId = trim_objectGUID(userId)
  459         return userId
  460 
  461     @cache
  462     def _getDN(self, userId):
  463         """
  464         This function returns the DN of a userId.
  465         Therefor it evaluates the self.uidtype.
  466 
  467         :param userId: The userid of a user
  468         :type userId: string
  469 
  470         :return: The DN of the object.
  471         """
  472         dn = ""
  473         if self.uidtype.lower() == "dn":
  474             dn = userId
  475         else:
  476             # get the DN for the Object
  477             self._bind()
  478             search_userId = self._trim_user_id(userId)
  479             filter = u"(&{0!s}({1!s}={2!s}))".format(self.searchfilter,
  480                                                      self.uidtype,
  481                                                      search_userId)
  482             self.l.search(search_base=self.basedn,
  483                           search_scope=self.scope,
  484                           search_filter=filter,
  485                           attributes=list(self.userinfo.values()))
  486             r = self.l.response
  487             r = self._trim_result(r)
  488             if len(r) > 1:  # pragma: no cover
  489                 raise Exception("Found more than one object for uid {0!r}".format(userId))
  490             elif len(r) == 1:
  491                 dn = r[0].get("dn")
  492             else:
  493                 log.info("The filter {0!r} returned no DN.".format(filter))
  494 
  495         return dn
  496 
  497     def _bind(self):
  498         if not self.i_am_bound:
  499             if not self.serverpool:
  500                 self.serverpool = self.get_serverpool_instance(self.get_info)
  501             self.l = self.create_connection(authtype=self.authtype,
  502                                             server=self.serverpool,
  503                                             user=self.binddn,
  504                                             password=self.bindpw,
  505                                             receive_timeout=self.timeout,
  506                                             auto_referrals=not
  507                                             self.noreferrals,
  508                                             start_tls=self.start_tls)
  509             #log.error("LDAP Server Pool States: %s" % server_pool.pool_states)
  510             if not self.l.bind():
  511                 raise Exception("Wrong credentials")
  512             self.i_am_bound = True
  513 
  514     @staticmethod
  515     def _get_tls_context(ldap_uri=None, start_tls=False, tls_version=None, tls_verify=None,
  516                          tls_ca_file=None, tls_options=None):
  517         """
  518         This method creates the Tls object to be used with ldap3.
  519 
  520         """
  521         if ldap_uri.lower().startswith("ldaps") or is_true(start_tls):
  522             if not tls_version:
  523                 tls_version = int(DEFAULT_TLS_PROTOCOL)
  524             # If TLS_VERSION is 2, set tls_options to use TLS v1.3
  525             if not tls_options:
  526                 tls_options = TLS_OPTIONS_1_3 if int(tls_version) == int(TLS_NEGOTIATE_PROTOCOL) else None
  527             if tls_verify:
  528                 tls_ca_file = tls_ca_file or DEFAULT_CA_FILE
  529             else:
  530                 tls_verify = ssl.CERT_NONE
  531             tls_context = Tls(validate=tls_verify,
  532                               version=int(tls_version),
  533                               ssl_options=tls_options,
  534                               ca_certs_file=tls_ca_file)
  535         else:
  536             tls_context = None
  537 
  538         return tls_context
  539 
  540     @cache
  541     def getUserInfo(self, userId):
  542         """
  543         This function returns all user info for a given userid/object.
  544 
  545         :param userId: The userid of the object
  546         :type userId: string
  547         :return: A dictionary with the keys defined in self.userinfo
  548         :rtype: dict
  549         """
  550         ret = {}
  551         self._bind()
  552 
  553         if self.uidtype.lower() == "dn":
  554             # encode utf8, so that also german umlauts work in the DN
  555             self.l.search(search_base=userId,
  556                           search_scope=self.scope,
  557                           search_filter=u"(&" + self.searchfilter + u")",
  558                           attributes=list(self.userinfo.values()))
  559         else:
  560             search_userId = to_unicode(self._trim_user_id(userId))
  561             filter = u"(&{0!s}({1!s}={2!s}))".format(self.searchfilter,
  562                                                      self.uidtype,
  563                                                      search_userId)
  564             self.l.search(search_base=self.basedn,
  565                               search_scope=self.scope,
  566                               search_filter=filter,
  567                               attributes=list(self.userinfo.values()))
  568 
  569         r = self.l.response
  570         r = self._trim_result(r)
  571         if len(r) > 1:  # pragma: no cover
  572             raise Exception("Found more than one object for uid {0!r}".format(userId))
  573 
  574         for entry in r:
  575             attributes = entry.get("attributes")
  576             ret = self._ldap_attributes_to_user_object(attributes)
  577 
  578         return ret
  579 
  580     def _ldap_attributes_to_user_object(self, attributes):
  581         """
  582         This helper function converts the LDAP attributes to a dictionary for
  583         the privacyIDEA user. The LDAP Userinfo mapping is used to do so.
  584 
  585         :param attributes:
  586         :return: dict with privacyIDEA users.
  587         """
  588         ret = {}
  589         for ldap_k, ldap_v in attributes.items():
  590             for map_k, map_v in self.userinfo.items():
  591                 if ldap_k == map_v:
  592                     if ldap_k == "objectGUID":
  593                         # An objectGUID should be no list, since it is unique
  594                         if isinstance(ldap_v, six.string_types):
  595                             ret[map_k] = ldap_v.strip("{").strip("}")
  596                         else:
  597                             raise Exception("The LDAP returns an objectGUID, that is no string: {0!s}".format(type(ldap_v)))
  598                     elif type(ldap_v) == list and map_k not in self.multivalueattributes:
  599                         # lists that are not in self.multivalueattributes return first value
  600                         # as a string. Multi-value-attributes are returned as a list
  601                         if ldap_v:
  602                             ret[map_k] = ldap_v[0]
  603                         else:
  604                             ret[map_k] = ""
  605                     else:
  606                         ret[map_k] = ldap_v
  607         return ret
  608 
  609     def getUsername(self, user_id):
  610         """
  611         Returns the username/loginname for a given user_id
  612         :param user_id: The user_id in this resolver
  613         :type user_id: string
  614         :return: username
  615         :rtype: string
  616         """
  617         info = self.getUserInfo(user_id)
  618         return info.get('username', "")
  619 
  620     @cache
  621     def getUserId(self, LoginName):
  622         """
  623         resolve the loginname to the userid.
  624 
  625         :param LoginName: The login name from the credentials
  626         :type LoginName: str
  627         :return: UserId as found for the LoginName
  628         :rtype: str
  629         """
  630         userid = ""
  631         self._bind()
  632         LoginName = to_unicode(LoginName)
  633         login_name = self._escape_loginname(LoginName)
  634 
  635         if len(self.loginname_attribute) > 1:
  636             loginname_filter = u""
  637             for l_attribute in self.loginname_attribute:
  638                 # Special case if we have a guid
  639                 try:
  640                     if l_attribute.lower() == "objectguid":
  641                         search_login_name = trim_objectGUID(login_name)
  642                     else:
  643                         search_login_name = login_name
  644                     loginname_filter += u"({!s}={!s})".format(l_attribute.strip(),
  645                                                               search_login_name)
  646                 except ValueError:
  647                     # This happens if we have a self.loginname_attribute like ["sAMAccountName","objectGUID"],
  648                     # the user logs in with his sAMAccountName, which can
  649                     # not be transformed to a UUID
  650                     log.debug(u"Can not transform {0!s} to a objectGUID.".format(login_name))
  651 
  652             loginname_filter = u"|" + loginname_filter
  653         else:
  654             if self.loginname_attribute[0].lower() == "objectguid":
  655                 search_login_name = trim_objectGUID(login_name)
  656             else:
  657                 search_login_name = login_name
  658             loginname_filter = u"{!s}={!s}".format(self.loginname_attribute[0],
  659                                                    search_login_name)
  660 
  661         log.debug("login name filter: {!r}".format(loginname_filter))
  662         filter = u"(&{0!s}({1!s}))".format(self.searchfilter, loginname_filter)
  663 
  664         # create search attributes
  665         attributes = list(self.userinfo.values())
  666         if self.uidtype.lower() != "dn":
  667             attributes.append(str(self.uidtype))
  668 
  669         log.debug("Searching user {0!r} in LDAP.".format(LoginName))
  670         self.l.search(search_base=self.basedn,
  671                       search_scope=self.scope,
  672                       search_filter=filter,
  673                       attributes=attributes)
  674 
  675         r = self.l.response
  676         r = self._trim_result(r)
  677         if len(r) > 1:  # pragma: no cover
  678             raise Exception("Found more than one object for Loginname {0!r}".format(
  679                             LoginName))
  680 
  681         for entry in r:
  682             userid = self._get_uid(entry, self.uidtype)
  683 
  684         return userid
  685 
  686     def getUserList(self, searchDict=None):
  687         """
  688         :param searchDict: A dictionary with search parameters
  689         :type searchDict: dict
  690         :return: list of users, where each user is a dictionary
  691         """
  692         ret = []
  693         self._bind()
  694         attributes = list(self.userinfo.values())
  695         ad_timestamp = get_ad_timestamp_now()
  696         if self.uidtype.lower() != "dn":
  697             attributes.append(str(self.uidtype))
  698 
  699         # do the filter depending on the searchDict
  700         filter = u"(&" + self.searchfilter
  701         for search_key in searchDict.keys():
  702             # convert to unicode
  703             searchDict[search_key] = to_unicode(searchDict[search_key])
  704             if search_key == "accountExpires":
  705                 comperator = ">="
  706                 if searchDict[search_key] in ["1", 1]:
  707                     comperator = "<="
  708                 filter += u"(&({0!s}{1!s}{2!s})(!({3!s}=0)))".format(
  709                     self.userinfo[search_key], comperator,
  710                     get_ad_timestamp_now(), self.userinfo[search_key])
  711             else:
  712                 filter += u"({0!s}={1!s})".format(self.userinfo[search_key],
  713                                                   searchDict[search_key])
  714         filter += ")"
  715 
  716         g = self.l.extend.standard.paged_search(search_base=self.basedn,
  717                                                 search_filter=filter,
  718                                                 search_scope=self.scope,
  719                                                 attributes=attributes,
  720                                                 paged_size=100,
  721                                                 size_limit=self.sizelimit,
  722                                                 generator=True)
  723         # returns a generator of dictionaries
  724         for entry in ignore_sizelimit_exception(self.l, g):
  725             # Simple fix for ignored sizelimit with Active Directory
  726             if len(ret) >= self.sizelimit:
  727                 break
  728             # Fix for searchResRef entries which have no attributes
  729             if entry.get('type') == 'searchResRef':
  730                 continue
  731             try:
  732                 attributes = entry.get("attributes")
  733                 user = self._ldap_attributes_to_user_object(attributes)
  734                 user['userid'] = self._get_uid(entry, self.uidtype)
  735                 ret.append(user)
  736             except Exception as exx:  # pragma: no cover
  737                 log.error("Error during fetching LDAP objects: {0!r}".format(exx))
  738                 log.debug("{0!s}".format(traceback.format_exc()))
  739 
  740         return ret
  741 
  742     def getResolverId(self):
  743         """
  744         Returns the resolver Id
  745         This should be an Identifier of the resolver, preferable the type
  746         and the name of the resolver.
  747 
  748         :return: the id of the resolver
  749         :rtype: str
  750         """
  751         s = u"{0!s}{1!s}{2!s}{3!s}".format(self.uri, self.basedn,
  752                                            self.searchfilter,
  753                                            sorted(self.userinfo.items(), key=itemgetter(0)))
  754         r = binascii.hexlify(hashlib.sha1(s.encode("utf-8")).digest())
  755         return r.decode('utf8')
  756 
  757     @staticmethod
  758     def getResolverClassType():
  759         return 'ldapresolver'
  760 
  761     @staticmethod
  762     def getResolverDescriptor():
  763         return IdResolver.getResolverClassDescriptor()
  764 
  765     @staticmethod
  766     def getResolverType():
  767         return IdResolver.getResolverClassType()
  768 
  769     def loadConfig(self, config):
  770         """
  771         Load the config from conf.
  772 
  773         :param config: The configuration from the Config Table
  774         :type config: dict
  775 
  776 
  777         '#ldap_uri': 'LDAPURI',
  778         '#ldap_basedn': 'LDAPBASE',
  779         '#ldap_binddn': 'BINDDN',
  780         '#ldap_password': 'BINDPW',
  781         '#ldap_timeout': 'TIMEOUT',
  782         '#ldap_sizelimit': 'SIZELIMIT',
  783         '#ldap_loginattr': 'LOGINNAMEATTRIBUTE',
  784         '#ldap_searchfilter': 'LDAPSEARCHFILTER',
  785         '#ldap_mapping': 'USERINFO',
  786         '#ldap_uidtype': 'UIDTYPE',
  787         '#ldap_noreferrals' : 'NOREFERRALS',
  788         '#ldap_editable' : 'EDITABLE',
  789         '#ldap_certificate': 'CACERTIFICATE',
  790 
  791         """
  792         self.uri = config.get("LDAPURI")
  793         self.basedn = config.get("LDAPBASE")
  794         self.binddn = config.get("BINDDN")
  795         # object_classes is a comma separated list like
  796         # ["top", "person", "organizationalPerson", "user", "inetOrgPerson"]
  797         self.object_classes = [cl.strip() for cl in config.get("OBJECT_CLASSES", "").split(",")]
  798         self.dn_template = config.get("DN_TEMPLATE", "")
  799         self.bindpw = config.get("BINDPW")
  800         self.timeout = float(config.get("TIMEOUT", 5))
  801         self.cache_timeout = int(config.get("CACHE_TIMEOUT", 120))
  802         self.sizelimit = int(config.get("SIZELIMIT", 500))
  803         self.loginname_attribute = [la.strip() for la in config.get("LOGINNAMEATTRIBUTE","").split(",")]
  804         self.searchfilter = config.get("LDAPSEARCHFILTER")
  805         userinfo = config.get("USERINFO", "{}")
  806         self.userinfo = yaml.safe_load(userinfo)
  807         self.userinfo["username"] = self.loginname_attribute[0]
  808         multivalueattributes = config.get("MULTIVALUEATTRIBUTES") or '["mobile"]'
  809         self.multivalueattributes = yaml.safe_load(multivalueattributes)
  810         self.map = yaml.safe_load(userinfo)
  811         self.uidtype = config.get("UIDTYPE", "DN")
  812         self.noreferrals = is_true(config.get("NOREFERRALS", False))
  813         self.start_tls = is_true(config.get("START_TLS", False))
  814         self.get_info = get_info_configuration(is_true(config.get("NOSCHEMAS", False)))
  815         self._editable = config.get("EDITABLE", False)
  816         self.scope = config.get("SCOPE") or ldap3.SUBTREE
  817         self.resolverId = self.uri
  818         self.authtype = config.get("AUTHTYPE", AUTHTYPE.SIMPLE)
  819         self.tls_verify = is_true(config.get("TLS_VERIFY", False))
  820         # Fallback to DEFAULT_TLS_PROTOCOL (TLSv1: 3, TLSv1.1: 4, v1.2: 5, TLS negotiation: 2)
  821         self.tls_version = int(config.get("TLS_VERSION") or DEFAULT_TLS_PROTOCOL)
  822         self.tls_ca_file = config.get("TLS_CA_FILE") or DEFAULT_CA_FILE
  823         self.tls_context = self._get_tls_context(ldap_uri=self.uri, start_tls=self.start_tls,
  824                                             tls_version=self.tls_version,
  825                                             tls_verify=self.tls_verify,
  826                                             tls_ca_file=self.tls_ca_file)
  827         self.serverpool_persistent = is_true(config.get("SERVERPOOL_PERSISTENT", False))
  828         self.serverpool_rounds = int(config.get("SERVERPOOL_ROUNDS") or SERVERPOOL_ROUNDS)
  829         self.serverpool_skip = int(config.get("SERVERPOOL_SKIP") or SERVERPOOL_SKIP)
  830         # The configuration might have changed. We reset the serverpool
  831         self.serverpool = None
  832         self.i_am_bound = False
  833 
  834         return self
  835 
  836     @property
  837     def has_multiple_loginnames(self):
  838         """
  839         Return if this resolver has multiple loginname attributes
  840         :return: bool
  841         """
  842         return len(self.loginname_attribute) > 1
  843 
  844     @staticmethod
  845     def split_uri(uri):
  846         """
  847         Splits LDAP URIs like:
  848         * ldap://server
  849         * ldaps://server
  850         * ldap[s]://server:1234
  851         * server
  852         :param uri: The LDAP URI
  853         :return: Returns a tuple of Servername, Port and SSL(bool)
  854         """
  855         port = None
  856         ssl = False
  857         ldap_elems = uri.split(":")
  858         if len(ldap_elems) == 3:
  859             server = ldap_elems[1].strip("/")
  860             port = int(ldap_elems[2])
  861             if ldap_elems[0].lower() == "ldaps":
  862                 ssl = True
  863             else:
  864                 ssl = False
  865         elif len(ldap_elems) == 2:
  866             server = ldap_elems[1].strip("/")
  867             port = None
  868             if ldap_elems[0].lower() == "ldaps":
  869                 ssl = True
  870             else:
  871                 ssl = False
  872         else:
  873             server = uri
  874 
  875         return server, port, ssl
  876 
  877     @classmethod
  878     def create_serverpool(cls, urilist, timeout, get_info=None, tls_context=None, rounds=SERVERPOOL_ROUNDS,
  879                           exhaust=SERVERPOOL_SKIP, pool_cls=ldap3.ServerPool):
  880         """
  881         This create the serverpool for the ldap3 connection.
  882         The URI from the LDAP resolver can contain a comma separated list of
  883         LDAP servers. These are split and then added to the pool.
  884 
  885         See
  886         https://github.com/cannatag/ldap3/blob/master/docs/manual/source/servers.rst#server-pool
  887 
  888         :param urilist: The list of LDAP URIs, comma separated
  889         :type urilist: basestring
  890         :param timeout: The connection timeout
  891         :type timeout: float
  892         :param get_info: The get_info type passed to the ldap3.Sever
  893             constructor. default: ldap3.SCHEMA, should be ldap3.NONE in case
  894             of a bind.
  895         :param tls_context: A ldap3.tls object, which defines if certificate
  896             verification should be performed
  897         :param rounds: The number of rounds we should cycle through the server pool
  898             before giving up
  899         :param exhaust: The seconds, for how long a non-reachable server should be
  900             removed from the serverpool
  901         :param pool_cls: ``ldap3.ServerPool`` subclass that should be instantiated
  902         :return: Server Pool
  903         :rtype: serverpool_cls
  904         """
  905         get_info = get_info or ldap3.SCHEMA
  906         server_pool = pool_cls(None, ldap3.ROUND_ROBIN,
  907                                active=rounds,
  908                                exhaust=exhaust)
  909         for uri in urilist.split(","):
  910             uri = uri.strip()
  911             host, port, ssl = cls.split_uri(uri)
  912             server = ldap3.Server(host, port=port,
  913                                   use_ssl=ssl,
  914                                   connect_timeout=float(timeout),
  915                                   get_info=get_info,
  916                                   tls=tls_context)
  917             server_pool.add(server)
  918             log.debug("Added {0!s}, {1!s}, {2!s} to server pool.".format(host, port, ssl))
  919         return server_pool
  920 
  921     def get_serverpool_instance(self, get_info=None):
  922         """
  923         Return a ``ServerPool`` instance that should be used. If ``SERVERPOOL_PERSISTENT``
  924         is enabled, invoke ``get_persistent_serverpool`` to retrieve a per-process
  925         server pool instance. If it is not enabled, invoke ``create_serverpool``
  926         to retrieve a per-request server pool instance.
  927         :param get_info: one of ldap3.SCHEMA, ldap3.NONE, ldap3.ALL
  928         :return: a ``ServerPool``/``LockingServerPool`` instance
  929         """
  930         if self.serverpool_persistent:
  931             return self.get_persistent_serverpool(get_info)
  932         else:
  933             return self.create_serverpool(self.uri, self.timeout, get_info,
  934                                           self.tls_context, self.serverpool_rounds, self.serverpool_skip)
  935 
  936     def get_persistent_serverpool(self, get_info=None):
  937         """
  938         Return a process-level instance of ``LockingServerPool`` for the current LDAP resolver
  939         configuration. Retrieve it from the app-local store. If such an instance does not exist
  940         yet, create one.
  941         :param get_info: one of ldap3.SCHEMA, ldap3.NONE, ldap3.ALL
  942         :return: a ``LockingServerPool`` instance
  943         """
  944         if not get_info:
  945             get_info = ldap3.SCHEMA
  946         pools = get_app_local_store().setdefault('ldap_server_pools', {})
  947         # Create a hashable tuple that describes the current server pool configuration
  948         pool_description = (self.uri,
  949                             self.timeout,
  950                             get_info,
  951                             repr(self.tls_context),  # this is the string representation of the TLS context
  952                             self.serverpool_rounds,
  953                             self.serverpool_skip)
  954         if pool_description not in pools:
  955             log.debug("Creating a persistent server pool instance for {!r} ...".format(pool_description))
  956             # Create a suitable instance of ``LockingServerPool``
  957             server_pool = self.create_serverpool(self.uri, self.timeout, get_info,
  958                                                  self.tls_context, self.serverpool_rounds, self.serverpool_skip,
  959                                                  pool_cls=LockingServerPool)
  960             # It may happen that another thread tries to add an instance to the dictionary concurrently.
  961             # However, only one of them will win, and the other ``LockingServerPool`` instance will be
  962             # garbage-collected eventually.
  963             return pools.setdefault(pool_description, server_pool)
  964         else:
  965             # If there is already a ``LockingServerPool`` instance, return it.
  966             # We never remove instances from the dictionary, so a ``KeyError`` cannot occur.
  967             # As a side effect, when we change the LDAP Id resolver configuration,
  968             # outdated ``LockingServerPool`` instances will survive until the next server restart.
  969             return pools[pool_description]
  970 
  971     @classmethod
  972     def getResolverClassDescriptor(cls):
  973         """
  974         return the descriptor of the resolver, which is
  975         - the class name and
  976         - the config description
  977 
  978         :return: resolver description dict
  979         :rtype:  dict
  980         """
  981         descriptor = {}
  982         typ = cls.getResolverType()
  983         descriptor['clazz'] = "useridresolver.LDAPIdResolver.IdResolver"
  984         descriptor['config'] = {'LDAPURI': 'string',
  985                                 'LDAPBASE': 'string',
  986                                 'BINDDN': 'string',
  987                                 'BINDPW': 'password',
  988                                 'TIMEOUT': 'int',
  989                                 'SIZELIMIT': 'int',
  990                                 'LOGINNAMEATTRIBUTE': 'string',
  991                                 'LDAPSEARCHFILTER': 'string',
  992                                 'USERINFO': 'string',
  993                                 'UIDTYPE': 'string',
  994                                 'NOREFERRALS': 'bool',
  995                                 'NOSCHEMAS': 'bool',
  996                                 'CACERTIFICATE': 'string',
  997                                 'EDITABLE': 'bool',
  998                                 'SCOPE': 'string',
  999                                 'AUTHTYPE': 'string',
 1000                                 'TLS_VERIFY': 'bool',
 1001                                 'TLS_VERSION': 'int',
 1002                                 'TLS_CA_FILE': 'string',
 1003                                 'START_TLS': 'bool',
 1004                                 'CACHE_TIMEOUT': 'int',
 1005                                 'SERVERPOOL_ROUNDS': 'int',
 1006                                 'SERVERPOOL_SKIP': 'int',
 1007                                 'SERVERPOOL_PERSISTENT': 'bool',
 1008                                 'OBJECT_CLASSES': 'string',
 1009                                 'DN_TEMPLATE': 'string'}
 1010         return {typ: descriptor}
 1011 
 1012     @classmethod
 1013     def testconnection(cls, param):
 1014         """
 1015         This function lets you test the to be saved LDAP connection.
 1016 
 1017         :param param: A dictionary with all necessary parameter to test
 1018                         the connection.
 1019         :type param: dict
 1020         :return: Tuple of success and a description
 1021         :rtype: (bool, string)
 1022 
 1023         Parameters are:
 1024             BINDDN, BINDPW, LDAPURI, TIMEOUT, LDAPBASE, LOGINNAMEATTRIBUTE,
 1025             LDAPSEARCHFILTER, USERINFO, SIZELIMIT, NOREFERRALS, CACERTIFICATE,
 1026             AUTHTYPE, TLS_VERIFY, TLS_VERSION, TLS_CA_FILE, SERVERPOOL_ROUNDS, SERVERPOOL_SKIP
 1027         """
 1028         success = False
 1029         uidtype = param.get("UIDTYPE")
 1030         timeout = float(param.get("TIMEOUT", 5))
 1031         ldap_uri = param.get("LDAPURI")
 1032         size_limit = int(param.get("SIZELIMIT", 500))
 1033         serverpool_rounds = int(param.get("SERVERPOOL_ROUNDS") or SERVERPOOL_ROUNDS)
 1034         serverpool_skip = int(param.get("SERVERPOOL_SKIP") or SERVERPOOL_SKIP)
 1035         tls_context = cls._get_tls_context(ldap_uri=ldap_uri,
 1036                                            start_tls=param.get("START_TLS"),
 1037                                            tls_version=param.get("TLS_VERSION"),
 1038                                            tls_verify=param.get("TLS_VERIFY"),
 1039                                            tls_ca_file=param.get("TLS_CA_FILE"),
 1040                                            tls_options=None)
 1041         get_info = get_info_configuration(is_true(param.get("NOSCHEMAS")))
 1042         try:
 1043             server_pool = cls.create_serverpool(ldap_uri, timeout,
 1044                                                 tls_context=tls_context,
 1045                                                 get_info=get_info,
 1046                                                 rounds=serverpool_rounds,
 1047                                                 exhaust=serverpool_skip)
 1048             l = cls.create_connection(authtype=param.get("AUTHTYPE",
 1049                                                           AUTHTYPE.SIMPLE),
 1050                                       server=server_pool,
 1051                                       user=param.get("BINDDN"),
 1052                                       password=param.get("BINDPW"),
 1053                                       receive_timeout=timeout,
 1054                                       auto_referrals=not param.get(
 1055                                            "NOREFERRALS"),
 1056                                       start_tls=is_true(param.get("START_TLS", False)))
 1057             #log.error("LDAP Server Pool States: %s" % server_pool.pool_states)
 1058             if not l.bind():
 1059                 raise Exception("Wrong credentials")
 1060             # create searchattributes
 1061             attributes = list(yaml.safe_load(param["USERINFO"]).values())
 1062             if uidtype.lower() != "dn":
 1063                 attributes.append(str(uidtype))
 1064             # search for users...
 1065             g = l.extend.standard.paged_search(
 1066                 search_base=param["LDAPBASE"],
 1067                 search_filter=u"(&" + param["LDAPSEARCHFILTER"] + ")",
 1068                 search_scope=param.get("SCOPE") or ldap3.SUBTREE,
 1069                 attributes=attributes,
 1070                 paged_size=100,
 1071                 size_limit=size_limit,
 1072                 generator=True)
 1073             # returns a generator of dictionaries
 1074             count = 0
 1075             uidtype_count = 0
 1076             for entry in ignore_sizelimit_exception(l, g):
 1077                 try:
 1078                     userid = cls._get_uid(entry, uidtype)
 1079                     count += 1
 1080                     if userid:
 1081                         uidtype_count += 1
 1082                 except Exception as exx:  # pragma: no cover
 1083                     log.warning("Error during fetching LDAP objects:"
 1084                                 " {0!r}".format(exx))
 1085                     log.debug("{0!s}".format(traceback.format_exc()))
 1086 
 1087             if uidtype_count < count:  # pragma: no cover
 1088                 desc = _("Your LDAP config found {0!s} user objects, but only {1!s} "
 1089                          "with the specified uidtype").format(count, uidtype_count)
 1090             else:
 1091                 desc = _("Your LDAP config seems to be OK, {0!s} user objects "
 1092                          "found.").format(count)
 1093 
 1094             l.unbind()
 1095             success = True
 1096 
 1097         except Exception as e:
 1098             desc = "{0!r}".format(e)
 1099             log.debug("{0!s}".format(traceback.format_exc()))
 1100 
 1101         return success, desc
 1102 
 1103     def add_user(self, attributes=None):
 1104         """
 1105         Add a new user to the LDAP directory.
 1106         The user can only be created in the LDAP using a DN.
 1107         So we have to construct the DN out of the given attributes.
 1108 
 1109         attributes are these
 1110         "username", "surname", "givenname", "email",
 1111         "mobile", "phone", "password"
 1112 
 1113         :param attributes: Attributes according to the attribute mapping
 1114         :type attributes: dict
 1115         :return: The new UID of the user. The UserIdResolver needs to
 1116         determine the way how to create the UID.
 1117         """
 1118         # TODO: We still have some utf8 issues creating users with special characters.
 1119         attributes = attributes or {}
 1120 
 1121         dn = self.dn_template
 1122         dn = dn.replace("<basedn>", self.basedn)
 1123         dn = dn.replace("<username>", attributes.get("username", ""))
 1124         dn = dn.replace("<givenname>", attributes.get("givenname", ""))
 1125         dn = dn.replace("<surname>", attributes.get("surname", ""))
 1126 
 1127         try:
 1128             self._bind()
 1129             params = self._attributes_to_ldap_attributes(attributes)
 1130             self.l.add(dn, self.object_classes, params)
 1131 
 1132         except Exception as e:
 1133             log.error("Error accessing LDAP server: {0!r}".format(e))
 1134             log.debug("{0}".format(traceback.format_exc()))
 1135             raise privacyIDEAError(e)
 1136 
 1137         if self.l.result.get('result') != 0:
 1138             log.error("Error during adding of user {0!r}: "
 1139                       "{1!r}".format(dn, self.l.result.get('message')))
 1140             raise privacyIDEAError(self.l.result.get('message'))
 1141 
 1142         return self.getUserId(attributes.get("username"))
 1143 
 1144     def delete_user(self, uid):
 1145         """
 1146         Delete a user from the LDAP Directory.
 1147 
 1148         The user is referenced by the user id.
 1149         :param uid: The uid of the user object, that should be deleted.
 1150         :type uid: basestring
 1151         :return: Returns True in case of success
 1152         :rtype: bool
 1153         """
 1154         res = True
 1155         try:
 1156             self._bind()
 1157 
 1158             self.l.delete(self._getDN(uid))
 1159         except Exception as exx:
 1160             log.error("Error deleting user: {0!r}".format(exx))
 1161             res = False
 1162         return res
 1163 
 1164     def _attributes_to_ldap_attributes(self, attributes):
 1165         """
 1166         takes the attributes and maps them to the LDAP attributes
 1167         :param attributes: Attributes to be updated
 1168         :type attributes: dict
 1169         :return: dict with attribute name as keys and values
 1170         """
 1171         ldap_attributes = {}
 1172         for fieldname, value in attributes.items():
 1173             if self.map.get(fieldname):
 1174                 if fieldname == "password":
 1175                     # Variable value may be either a string or a list
 1176                     # so catch the TypeError exception if we get the wrong
 1177                     # variable type
 1178                     try:
 1179                         pw_hash = ldap_salted_sha1.hash(value[1][0])
 1180                         value[1][0] = pw_hash
 1181                         ldap_attributes[self.map.get(fieldname)] = value
 1182                     except TypeError as e:
 1183                         pw_hash = ldap_salted_sha1.hash(value)
 1184                         ldap_attributes[self.map.get(fieldname)] = pw_hash
 1185                 else:
 1186                     ldap_attributes[self.map.get(fieldname)] = value
 1187 
 1188         return ldap_attributes
 1189 
 1190     def _create_ldap_modify_changes(self, attributes, uid):
 1191         """
 1192         Identifies if an LDAP attribute already exists and if the value needs to be updated, deleted or added.
 1193 
 1194         :param attributes: Attributes to be updated
 1195         :type attributes: dict
 1196         :param uid: The uid of the user object in the resolver
 1197         :type uid: basestring
 1198         :return: dict with attribute name as keys and values
 1199         """
 1200         modify_changes = {}
 1201         uinfo = self.getUserInfo(uid)
 1202 
 1203         for fieldname, value in attributes.items():
 1204             if value:
 1205                 if fieldname in uinfo:
 1206                     modify_changes[fieldname] = [MODIFY_REPLACE, [value]]
 1207                 else:
 1208                     modify_changes[fieldname] = [MODIFY_ADD, [value]]
 1209             else:
 1210                 modify_changes[fieldname] = [MODIFY_DELETE, [value]]
 1211 
 1212         return modify_changes
 1213 
 1214     def update_user(self, uid, attributes=None):
 1215         """
 1216         Update an existing user.
 1217         This function is also used to update the password. Since the
 1218         attribute mapping know, which field contains the password,
 1219         this function can also take care for password changing.
 1220 
 1221         Attributes that are not contained in the dict attributes are not
 1222         modified.
 1223 
 1224         :param uid: The uid of the user object in the resolver.
 1225         :type uid: basestring
 1226         :param attributes: Attributes to be updated.
 1227         :type attributes: dict
 1228         :return: True in case of success
 1229         """
 1230         attributes = attributes or {}
 1231         try:
 1232             self._bind()
 1233 
 1234             mapped = self._create_ldap_modify_changes(attributes, uid)
 1235             params = self._attributes_to_ldap_attributes(mapped)
 1236             self.l.modify(self._getDN(uid), params)
 1237         except Exception as e:
 1238             log.error("Error accessing LDAP server: {0!r}".format(e))
 1239             log.debug("{0!s}".format(traceback.format_exc()))
 1240             return False
 1241 
 1242         if self.l.result.get('result') != 0:
 1243             log.error("Error during update of user {0!r}: "
 1244                       "{1!r}".format(uid, self.l.result.get("message")))
 1245             return False
 1246 
 1247         return True
 1248 
 1249     @staticmethod
 1250     def create_connection(authtype=None, server=None, user=None,
 1251                           password=None, auto_bind=False,
 1252                           client_strategy=ldap3.SYNC,
 1253                           check_names=True,
 1254                           auto_referrals=False,
 1255                           receive_timeout=5,
 1256                           start_tls=False):
 1257         """
 1258         Create a connection to the LDAP server.
 1259 
 1260         :param authtype:
 1261         :param server:
 1262         :param user:
 1263         :param password:
 1264         :param auto_bind:
 1265         :param client_strategy:
 1266         :param check_names:
 1267         :param auto_referrals:
 1268         :param receive_timeout: At the moment we do not use this,
 1269             since receive_timeout is not supported by ldap3 < 2.
 1270         :return:
 1271         """
 1272 
 1273         authentication = None
 1274         if not user:
 1275             authentication = ldap3.ANONYMOUS
 1276 
 1277         if authtype == AUTHTYPE.SIMPLE:
 1278             if not authentication:
 1279                 authentication = ldap3.SIMPLE
 1280             # SIMPLE works with passwords as UTF8 and unicode
 1281             l = ldap3.Connection(server, user=user,
 1282                                  password=password,
 1283                                  auto_bind=auto_bind,
 1284                                  client_strategy=client_strategy,
 1285                                  authentication=authentication,
 1286                                  check_names=check_names,
 1287                                  # receive_timeout=receive_timeout,
 1288                                  auto_referrals=auto_referrals)
 1289         elif authtype == AUTHTYPE.NTLM:  # pragma: no cover
 1290             if not authentication:
 1291                 authentication = ldap3.NTLM
 1292             # NTLM requires the password to be unicode
 1293             l = ldap3.Connection(server,
 1294                                  user=user,
 1295                                  password=password,
 1296                                  auto_bind=auto_bind,
 1297                                  client_strategy=client_strategy,
 1298                                  authentication=authentication,
 1299                                  check_names=check_names,
 1300                                  # receive_timeout=receive_timeout,
 1301                                  auto_referrals=auto_referrals)
 1302         elif authtype == AUTHTYPE.SASL_DIGEST_MD5:  # pragma: no cover
 1303             if not authentication:
 1304                 authentication = ldap3.SASL
 1305             password = to_utf8(password)
 1306             sasl_credentials = (str(user), str(password))
 1307             l = ldap3.Connection(server,
 1308                                  sasl_mechanism="DIGEST-MD5",
 1309                                  sasl_credentials=sasl_credentials,
 1310                                  auto_bind=auto_bind,
 1311                                  client_strategy=client_strategy,
 1312                                  authentication=authentication,
 1313                                  check_names=check_names,
 1314                                  # receive_timeout=receive_timeout,
 1315                                  auto_referrals=auto_referrals)
 1316         else:
 1317             raise Exception("Authtype {0!s} not supported".format(authtype))
 1318 
 1319         if start_tls:
 1320             l.open(read_server_info=False)
 1321             log.debug("Doing start_tls")
 1322             r = l.start_tls(read_server_info=False)
 1323 
 1324         return l
 1325 
 1326     @property
 1327     def editable(self):
 1328         """
 1329         Return true, if the instance of the resolver is configured editable
 1330         :return:
 1331         """
 1332         # Depending on the database this might look different
 1333         # Usually this is "1"
 1334         return is_true(self._editable)