Dne 9.4.2015 v 17:28 Petr Viktorin napsal(a):
On 04/08/2015 03:18 PM, Jan Cholasta wrote:
Hi,

the attached patches remove IPASimpleLDAPObject from ipaldap.

As a result, the one and only IPA LDAP API is the LDAPClient API.

This is definitely an improvement :)

0408: ACK  (woohoo!)
0409: ACK
0410:
I quite like the new __init__ signature, and the context manager
functionality.
Can you add a comment for the `object.__setattr__(self, '_conn', None)`
in _disconnect? It's a real eyesore.

Added.

0411: ACK
0412: Can _force_schema_updates be set already in __init__?

Unfortunately not. ldap2 is now used with different API instances, and the current API instance is not available in __init__.

I'm working on an additional patch for <https://fedorahosted.org/freeipa/ticket/3090> to pass the API object to plugins in their __init__ (because why do it somewhere else), which will fix this.

0413: ACK
0414: ACK
0415: ACK
0416: I think you should show off the `with` statement support here.

Fixed.

0417: ... and here

Fixed.

0418: ACK
0419: ACK
0420: ACK
0421: ACK

Added a comment about ldap2's locking here as well.

Also moved LDAPClient.schema back to its original location to save some lines in the patch.

0422: ACK, and good riddance

You missed 423 :-)

Thanks for the review.

Updated patches attached.

--
Jan Cholasta
>From fc74c1b7dfac2d8dfb2cf638dd53852746227c7b Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 23 Oct 2014 15:44:40 +0200
Subject: [PATCH 01/16] ldap: Drop python-ldap tuple compatibility

---
 ipapython/ipaldap.py | 69 +++-------------------------------------------------
 1 file changed, 3 insertions(+), 66 deletions(-)

diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index ce07006..5ba2b8b 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -90,12 +90,6 @@ def value_to_utf8(val):
 
     return unicode(val).encode('utf-8')
 
-# FIXME: Remove when python-ldap tuple compatibility is dropped
-def raise_deprecation_error():
-    raise RuntimeError(
-        "this API has been deprecated, see http://www.freeipa.org/page/";
-        "HowTo/Migrate_your_code_to_the_new_LDAP_API")
-
 class _ServerSchema(object):
     '''
     Properties of a schema retrieved from an LDAP server.
@@ -670,13 +664,6 @@ class IPASimpleLDAPObject(object):
         return self.conn.unbind_s()
 
 
-# Make python-ldap tuple style result compatible with Entry and Entity
-# objects by allowing access to the dn (tuple index 0) via the 'dn'
-# attribute name and the attr dict (tuple index 1) via the 'data'
-# attribute name. Thus:
-# r = result[0]
-# r[0] == r.dn
-# r[1] == r.data
 class LDAPEntry(collections.MutableMapping):
     __slots__ = ('_conn', '_dn', '_names', '_nice', '_raw', '_sync',
                  '_not_list', '_orig', '_raw_view', '_single_value_view')
@@ -935,10 +922,6 @@ class LDAPEntry(collections.MutableMapping):
         return value
 
     def __getitem__(self, name):
-        # FIXME: Remove when python-ldap tuple compatibility is dropped
-        if name in (0, 1):
-            raise_deprecation_error()
-
         return self._get_nice(name)
 
     def __delitem__(self, name):
@@ -1023,49 +1006,9 @@ class LDAPEntry(collections.MutableMapping):
 
         return modlist
 
-    # FIXME: Remove when python-ldap tuple compatibility is dropped
     def __iter__(self):
-        raise_deprecation_error()
-
-    # FIXME: Remove when python-ldap tuple compatibility is dropped
-    def iterkeys(self):
-        return self._nice.iterkeys()
-
-    # FIXME: Remove when python-ldap tuple compatibility is dropped
-    def itervalues(self):
-        for name in self.iterkeys():
-            yield self[name]
-
-    # FIXME: Remove when python-ldap tuple compatibility is dropped
-    def iteritems(self):
-        for name in self.iterkeys():
-            yield (name, self[name])
-
-    # FIXME: Remove when python-ldap tuple compatibility is dropped
-    def keys(self):
-        return list(self.iterkeys())
-
-    # FIXME: Remove when python-ldap tuple compatibility is dropped
-    def values(self):
-        return list(self.itervalues())
+        return iter(self._nice)
 
-    # FIXME: Remove when python-ldap tuple compatibility is dropped
-    def items(self):
-        return list(self.iteritems())
-
-    # FIXME: Remove when python-ldap tuple compatibility is dropped
-    def update(self, _obj={}, **kwargs):
-        _obj = dict(_obj, **kwargs)
-        super(LDAPEntry, self).update(_obj)
-
-    # FIXME: Remove when python-ldap tuple compatibility is dropped
-    def popitem(self):
-        try:
-            name = self.iterkeys().next()
-        except StopIteration:
-            raise KeyError
-
-        return name, self.pop(name)
 
 class LDAPEntryView(collections.MutableMapping):
     __slots__ = ('_entry',)
@@ -1577,14 +1520,11 @@ class LDAPClient(object):
             raise errors.LimitsExceeded()
         return entries[0]
 
-    def add_entry(self, entry, entry_attrs=None):
+    def add_entry(self, entry):
         """Create a new entry.
 
         This should be called as add_entry(entry).
         """
-        if entry_attrs is not None:
-            raise_deprecation_error()
-
         # remove all [] values (python-ldap hates 'em)
         attrs = dict((k, v) for k, v in entry.raw.iteritems() if v)
 
@@ -1610,14 +1550,11 @@ class LDAPClient(object):
             self.conn.rename_s(dn, new_rdn, delold=int(del_old))
             time.sleep(.3)  # Give memberOf plugin a chance to work
 
-    def update_entry(self, entry, entry_attrs=None):
+    def update_entry(self, entry):
         """Update entry's attributes.
 
         This should be called as update_entry(entry).
         """
-        if entry_attrs is not None:
-            raise_deprecation_error()
-
         # generate modlist
         modlist = entry.generate_modlist()
         if not modlist:
-- 
2.1.0

>From 955eb892b6563ecde1f28b2f7520a67cad199bb9 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Wed, 21 Jan 2015 11:14:19 +0000
Subject: [PATCH 02/16] ldap: Remove unused IPAdmin methods

---
 ipapython/ipaldap.py | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index 5ba2b8b..d4beaee 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -1720,13 +1720,5 @@ class IPAdmin(LDAPClient):
         # FIXME: for backwards compatibility only
         return self.conn.modify_s(*args, **kwargs)
 
-    def set_option(self, *args, **kwargs):
-        # FIXME: for backwards compatibility only
-        return self.conn.set_option(*args, **kwargs)
-
-    def encode(self, *args, **kwargs):
-        # FIXME: for backwards compatibility only
-        return self.conn.encode(*args, **kwargs)
-
     def unbind(self, *args, **kwargs):
         return self.conn.unbind_s(*args, **kwargs)
-- 
2.1.0

>From 0bf04200baf01434fd948e4293d7fe10b309a7ec Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 26 Jan 2015 15:16:01 +0000
Subject: [PATCH 03/16] ldap: Add connection management to LDAPClient

---
 ipapython/ipaldap.py       | 73 ++++++++++++++++++++++++++++++++++++++++------
 ipaserver/plugins/ldap2.py |  5 +++-
 2 files changed, 68 insertions(+), 10 deletions(-)

diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index d4beaee..683c5e1 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -1083,13 +1083,22 @@ class LDAPClient(object):
     SCOPE_ONELEVEL = ldap.SCOPE_ONELEVEL
     SCOPE_SUBTREE = ldap.SCOPE_SUBTREE
 
-    def __init__(self, ldap_uri):
+    def __init__(self, ldap_uri, start_tls=False, force_schema_updates=False,
+                 no_schema=False, decode_attrs=True):
         self.ldap_uri = ldap_uri
+        self._start_tls = start_tls
+        self._force_schema_updates = force_schema_updates
+        self._no_schema = no_schema
+        self._decode_attrs = decode_attrs
+
         self.log = log_mgr.get_logger(self)
-        self._init_connection()
+        self._conn = None
 
-    def _init_connection(self):
-        self.conn = None
+        self._connect()
+
+    @property
+    def conn(self):
+        return self._conn
 
     @contextlib.contextmanager
     def error_handler(self, arg_desc=None):
@@ -1186,6 +1195,46 @@ class LDAPClient(object):
                     reason=_('objectclass %s not found') % oc)
         return [unicode(a).lower() for a in list(set(allowed_attributes))]
 
+    def __del__(self):
+        self.close()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self.close()
+
+    def close(self):
+        """
+        Close the connection.
+        """
+        if self._conn is not None:
+            self._disconnect()
+
+    def _connect(self):
+        if self._conn is not None:
+            raise errors.DatabaseError(
+                desc="Can't connect to server", info="Already connected")
+
+        with self.error_handler():
+            # bypass ldap2's locking
+            object.__setattr__(self, '_conn',
+                               IPASimpleLDAPObject(self.ldap_uri,
+                                                   self._force_schema_updates,
+                                                   self._no_schema,
+                                                   self._decode_attrs))
+
+            if self._start_tls:
+                self._conn.start_tls_s()
+
+    def _disconnect(self):
+        if self._conn is None:
+            raise errors.DatabaseError(
+                desc="Can't disconnect from server", info="Not connected")
+
+        # bypass ldap2's locking
+        object.__setattr__(self, '_conn', None)
+
     def make_dn_from_attr(self, attr, value, parent_dn=None):
         """
         Make distinguished name from attribute.
@@ -1626,7 +1675,7 @@ class IPAdmin(LDAPClient):
                  realm=None, protocol=None, force_schema_updates=True,
                  start_tls=False, ldap_uri=None, no_schema=False,
                  decode_attrs=True, sasl_nocanon=False, demand_cert=False):
-        self.conn = None
+        self._conn = None
         log_mgr.get_logger(self, True)
         if debug and debug.lower() == "on":
             ldap.set_option(ldap.OPT_DEBUG_LEVEL,255)
@@ -1646,10 +1695,10 @@ class IPAdmin(LDAPClient):
         LDAPClient.__init__(self, ldap_uri)
 
         with self.error_handler():
-            self.conn = IPASimpleLDAPObject(ldap_uri,
-                                            force_schema_updates=True,
-                                            no_schema=no_schema,
-                                            decode_attrs=decode_attrs)
+            self._conn = IPASimpleLDAPObject(ldap_uri,
+                                             force_schema_updates=True,
+                                             no_schema=no_schema,
+                                             decode_attrs=decode_attrs)
 
             if demand_cert:
                 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, True)
@@ -1661,6 +1710,12 @@ class IPAdmin(LDAPClient):
             if start_tls:
                 self.conn.start_tls_s()
 
+    def _connect(self):
+        pass
+
+    def _disconnect(self):
+        pass
+
     def __str__(self):
         return self.host + ":" + str(self.port)
 
diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py
index fd4ed29..2671571 100644
--- a/ipaserver/plugins/ldap2.py
+++ b/ipaserver/plugins/ldap2.py
@@ -98,11 +98,14 @@ class ldap2(LDAPClient, CrudBackend):
         except AttributeError:
             return DN()
 
-    def _init_connection(self):
+    def _connect(self):
         # Connectible.conn is a proxy to thread-local storage;
         # do not set it
         pass
 
+    def _disconnect(self):
+        pass
+
     def __del__(self):
         if self.isconnected():
             self.disconnect()
-- 
2.1.0

>From 0dad2a1245c15154884c43cf0b70b08043cb81c7 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Fri, 21 Nov 2014 19:45:34 +0100
Subject: [PATCH 04/16] ldap: Use LDAPClient connection management in IPAdmin

---
 ipapython/ipaldap.py | 15 +++------------
 1 file changed, 3 insertions(+), 12 deletions(-)

diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index 683c5e1..9ddbb4d 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -1692,14 +1692,11 @@ class IPAdmin(LDAPClient):
         if not ldap_uri:
             ldap_uri = self.__get_ldap_uri(protocol or self.__guess_protocol())
 
-        LDAPClient.__init__(self, ldap_uri)
+        super(IPAdmin, self).__init__(
+            ldap_uri, force_schema_updates=force_schema_updates,
+            no_schema=no_schema, decode_attrs=decode_attrs)
 
         with self.error_handler():
-            self._conn = IPASimpleLDAPObject(ldap_uri,
-                                             force_schema_updates=True,
-                                             no_schema=no_schema,
-                                             decode_attrs=decode_attrs)
-
             if demand_cert:
                 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, True)
                 self.conn.set_option(ldap.OPT_X_TLS_DEMAND, True)
@@ -1710,12 +1707,6 @@ class IPAdmin(LDAPClient):
             if start_tls:
                 self.conn.start_tls_s()
 
-    def _connect(self):
-        pass
-
-    def _disconnect(self):
-        pass
-
     def __str__(self):
         return self.host + ":" + str(self.port)
 
-- 
2.1.0

>From 2f430ccf13466cf905e1f83707b2f81e70be48e2 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Fri, 21 Nov 2014 20:03:29 +0100
Subject: [PATCH 05/16] ldap: Use LDAPClient connection management in ldap2

---
 ipaserver/plugins/ldap2.py | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py
index 2671571..98b038a 100644
--- a/ipaserver/plugins/ldap2.py
+++ b/ipaserver/plugins/ldap2.py
@@ -145,10 +145,12 @@ class ldap2(LDAPClient, CrudBackend):
         if debug_level:
             _ldap.set_option(_ldap.OPT_DEBUG_LEVEL, debug_level)
 
+        object.__setattr__(self, '_force_schema_updates',
+                           self.api.env.context in ('installer', 'updates'))
+        LDAPClient._connect(self)
+        conn = self._conn
+
         with self.error_handler():
-            force_updates = self.api.env.context in ('installer', 'updates')
-            conn = IPASimpleLDAPObject(
-                self.ldap_uri, force_schema_updates=force_updates)
             if self.ldap_uri.startswith('ldapi://') and ccache:
                 conn.set_option(_ldap.OPT_HOST_NAME, self.api.env.host)
             minssf = conn.get_option(_ldap.OPT_X_SASL_SSF_MIN)
@@ -200,6 +202,11 @@ class ldap2(LDAPClient, CrudBackend):
             # ignore when trying to unbind multiple times
             pass
 
+        try:
+            LDAPClient._disconnect(self)
+        except errors.PublicError:
+            # ignore when trying to unbind multiple times
+            pass
 
     def find_entries(self, filter=None, attrs_list=None, base_dn=None,
                      scope=_ldap.SCOPE_SUBTREE, time_limit=None,
-- 
2.1.0

>From e47af0adee748f34bb9f62eddc44dee1fa4e6d89 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Fri, 21 Nov 2014 20:08:17 +0100
Subject: [PATCH 06/16] ldap: Add bind and unbind methods to LDAPClient

---
 ipapython/ipaldap.py | 35 +++++++++++++++++++++++++++++++++++
 1 file changed, 35 insertions(+)

diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index 9ddbb4d..09f0c10 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -1235,6 +1235,41 @@ class LDAPClient(object):
         # bypass ldap2's locking
         object.__setattr__(self, '_conn', None)
 
+    def simple_bind(self, bind_dn, bind_password, server_controls=None,
+                    client_controls=None):
+        """
+        Perform simple bind operation.
+        """
+        with self.error_handler():
+            self._conn.simple_bind_s(
+                bind_dn, bind_password, server_controls, client_controls)
+
+    def external_bind(self, user_name, server_controls=None,
+                      client_controls=None):
+        """
+        Perform SASL bind operation using the SASL EXTERNAL mechanism.
+        """
+        with self.error_handler():
+            auth_tokens = ldap.sasl.external(user_name)
+            self._conn.sasl_interactive_bind_s(
+                None, auth_tokens, server_controls, client_controls)
+
+    def gssapi_bind(self, server_controls=None, client_controls=None):
+        """
+        Perform SASL bind operation using the SASL GSSAPI mechanism.
+        """
+        with self.error_handler():
+            auth_tokens = ldap.sasl.sasl({}, 'GSSAPI')
+            self._conn.sasl_interactive_bind_s(
+                None, auth_tokens, server_controls, client_controls)
+
+    def unbind(self):
+        """
+        Perform unbind operation.
+        """
+        with self.error_handler():
+            self.conn.unbind_s()
+
     def make_dn_from_attr(self, attr, value, parent_dn=None):
         """
         Make distinguished name from attribute.
-- 
2.1.0

>From 6a3ce076b9b3fce5d44df7c03d85debbd9984c9c Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Fri, 21 Nov 2014 20:10:23 +0100
Subject: [PATCH 07/16] ldap: Use LDAPClient bind and unbind methods in IPAdmin

---
 ipapython/ipaldap.py | 35 ++++++++++++++++-------------------
 1 file changed, 16 insertions(+), 19 deletions(-)

diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index 09f0c10..d28d8b4 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -1754,29 +1754,29 @@ class IPAdmin(LDAPClient):
             wait_for_open_ports(host, int(port), timeout)
 
     def __bind_with_wait(self, bind_func, timeout, *args, **kwargs):
-        with self.error_handler():
-            try:
-                bind_func(*args, **kwargs)
-            except (ldap.CONNECT_ERROR, ldap.SERVER_DOWN), e:
-                if not timeout or 'TLS' in e.args[0].get('info', ''):
-                    # No connection to continue on if we have a TLS failure
-                    # https://bugzilla.redhat.com/show_bug.cgi?id=784989
-                    raise
-                self.__wait_for_connection(timeout)
-                bind_func(*args, **kwargs)
+        try:
+            bind_func(*args, **kwargs)
+        except errors.NetworkError as e:
+            if not timeout and 'TLS' in e.error:
+                # No connection to continue on if we have a TLS failure
+                # https://bugzilla.redhat.com/show_bug.cgi?id=784989
+                raise
+        except errors.DatabaseError:
+            pass
+        else:
+            return
+        self.__wait_for_connection(timeout)
+        bind_func(*args, **kwargs)
 
     def do_simple_bind(self, binddn=DN(('cn', 'directory manager')), bindpw="",
                        timeout=DEFAULT_TIMEOUT):
-        self.__bind_with_wait(self.conn.simple_bind_s, timeout, binddn, bindpw)
+        self.__bind_with_wait(self.simple_bind, timeout, binddn, bindpw)
 
     def do_sasl_gssapi_bind(self, timeout=DEFAULT_TIMEOUT):
-        self.__bind_with_wait(
-            self.conn.sasl_interactive_bind_s, timeout, None, SASL_GSSAPI)
+        self.__bind_with_wait(self.gssapi_bind, timeout)
 
     def do_external_bind(self, user_name=None, timeout=DEFAULT_TIMEOUT):
-        auth_tokens = ldap.sasl.external(user_name)
-        self.__bind_with_wait(
-            self.conn.sasl_interactive_bind_s, timeout, None, auth_tokens)
+        self.__bind_with_wait(self.external_bind, timeout, user_name)
 
     def do_bind(self, dm_password="", autobind=AUTOBIND_AUTO, timeout=DEFAULT_TIMEOUT):
         if dm_password:
@@ -1800,6 +1800,3 @@ class IPAdmin(LDAPClient):
     def modify_s(self, *args, **kwargs):
         # FIXME: for backwards compatibility only
         return self.conn.modify_s(*args, **kwargs)
-
-    def unbind(self, *args, **kwargs):
-        return self.conn.unbind_s(*args, **kwargs)
-- 
2.1.0

>From 291f747809bdd846900a4f5d934ae04e513d5ffe Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Fri, 21 Nov 2014 20:14:12 +0100
Subject: [PATCH 08/16] ldap: Use LDAPClient bind and unbind methods in ldap2

---
 ipaserver/plugins/ldap2.py | 62 +++++++++++++++++++++-------------------------
 1 file changed, 28 insertions(+), 34 deletions(-)

diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py
index 98b038a..1e103dc 100644
--- a/ipaserver/plugins/ldap2.py
+++ b/ipaserver/plugins/ldap2.py
@@ -162,47 +162,41 @@ class ldap2(LDAPClient, CrudBackend):
                 conn.set_option(_ldap.OPT_X_SASL_SSF_MIN, minssf)
                 if maxssf < minssf:
                     conn.set_option(_ldap.OPT_X_SASL_SSF_MAX, minssf)
-            if ccache is not None:
-                if isinstance(ccache, krbV.CCache):
-                    principal = ccache.principal().name
-                    # Get a fully qualified CCACHE name (schema+name)
-                    # As we do not use the krbV.CCache object later,
-                    # we can safely overwrite it
-                    ccache = "%(type)s:%(name)s" % dict(type=ccache.type,
-                                                        name=ccache.name)
-                else:
-                    principal = krbV.CCache(name=ccache,
-                        context=krbV.default_context()).principal().name
-
-                os.environ['KRB5CCNAME'] = ccache
-                conn.sasl_interactive_bind_s(None, SASL_GSSAPI,
-                                             serverctrls=serverctrls,
-                                             clientctrls=clientctrls)
-                setattr(context, 'principal', principal)
+
+        if ccache is not None:
+            if isinstance(ccache, krbV.CCache):
+                principal = ccache.principal().name
+                # Get a fully qualified CCACHE name (schema+name)
+                # As we do not use the krbV.CCache object later,
+                # we can safely overwrite it
+                ccache = "%(type)s:%(name)s" % dict(type=ccache.type,
+                                                    name=ccache.name)
             else:
-                # no kerberos ccache, use simple bind or external sasl
-                if autobind:
-                    pent = pwd.getpwuid(os.geteuid())
-                    auth_tokens = _ldap.sasl.external(pent.pw_name)
-                    conn.sasl_interactive_bind_s(None, auth_tokens,
-                                                 serverctrls=serverctrls,
-                                                 clientctrls=clientctrls)
-                else:
-                    conn.simple_bind_s(bind_dn, bind_pw,
-                                       serverctrls=serverctrls,
-                                       clientctrls=clientctrls)
+                principal = krbV.CCache(name=ccache,
+                    context=krbV.default_context()).principal().name
+
+            os.environ['KRB5CCNAME'] = ccache
+            self.gssapi_bind(server_controls=serverctrls,
+                             client_controls=clientctrls)
+            setattr(context, 'principal', principal)
+        else:
+            # no kerberos ccache, use simple bind or external sasl
+            if autobind:
+                pent = pwd.getpwuid(os.geteuid())
+                self.external_bind(pent.pw_name,
+                                   server_controls=serverctrls,
+                                   client_controls=clientctrls)
+            else:
+                self.simple_bind(bind_dn, bind_pw,
+                                 server_controls=serverctrls,
+                                 client_controls=clientctrls)
 
         return conn
 
     def destroy_connection(self):
         """Disconnect from LDAP server."""
         try:
-            self.conn.unbind_s()
-        except _ldap.LDAPError:
-            # ignore when trying to unbind multiple times
-            pass
-
-        try:
+            self.unbind()
             LDAPClient._disconnect(self)
         except errors.PublicError:
             # ignore when trying to unbind multiple times
-- 
2.1.0

>From 1c1daa37f74473f33ec0c287d464dc41ab85c577 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Wed, 8 Apr 2015 11:31:15 +0000
Subject: [PATCH 09/16] ldap: Use LDAPClient instead of IPASimpleLDAPObject in
 ldap2.modify_password

---
 ipaserver/plugins/ldap2.py | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py
index 1e103dc..9ab33e2 100644
--- a/ipaserver/plugins/ldap2.py
+++ b/ipaserver/plugins/ldap2.py
@@ -34,7 +34,7 @@ import krbV
 import ldap as _ldap
 
 from ipapython.dn import DN
-from ipapython.ipaldap import SASL_GSSAPI, IPASimpleLDAPObject, LDAPClient
+from ipapython.ipaldap import SASL_GSSAPI, LDAPClient
 
 
 try:
@@ -471,11 +471,10 @@ class ldap2(LDAPClient, CrudBackend):
             pw = old_pass
             if (otp):
                 pw = old_pass+otp
-            with self.error_handler():
-                conn = IPASimpleLDAPObject(
-                    self.ldap_uri, force_schema_updates=False)
-                conn.simple_bind_s(dn, pw)
-                conn.unbind_s()
+
+            with LDAPClient(self.ldap_uri, force_schema_updates=False) as conn:
+                conn.simple_bind(dn, pw)
+                conn.unbind()
 
         with self.error_handler():
             self.conn.passwd_s(dn, old_pass, new_pass)
-- 
2.1.0

>From 7ff0b977d9dd1fbb5871fb03a87b03daa58ad35b Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Wed, 8 Apr 2015 11:32:21 +0000
Subject: [PATCH 10/16] cainstance: Use LDAPClient instead of
 IPASimpleLDAPObject

---
 ipaserver/install/cainstance.py | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 8ba6e46..59a6b5f 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -1643,13 +1643,14 @@ def replica_ca_install_check(config):
     objectclass = 'ipaObject'
     root_logger.debug('Checking if IPA schema is present in %s', ca_ldap_url)
     try:
-        connection = ldap2.IPASimpleLDAPObject(
-            ca_ldap_url, force_schema_updates=False)
-        connection.start_tls_s()
-        connection.simple_bind_s(DN(('cn', 'Directory Manager')),
-                                config.dirman_password)
-        rschema = connection.schema
-        result = rschema.get_obj(ldap.schema.models.ObjectClass, objectclass)
+        with ipaldap.LDAPClient(ca_ldap_url,
+                                start_tls=True,
+                                force_schema_updates=False) as connection:
+            connection.simple_bind(DN(('cn', 'Directory Manager')),
+                                   config.dirman_password)
+            rschema = connection.schema
+            result = rschema.get_obj(ldap.schema.models.ObjectClass,
+                                     objectclass)
     except Exception:
         root_logger.critical(
             'CA DS schema check failed. Make sure the PKI service on the '
-- 
2.1.0

>From 8c830a7d6c523bde020fd6e225be812ea502486d Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Wed, 8 Apr 2015 11:32:54 +0000
Subject: [PATCH 11/16] makeaci: Use LDAPClient instead of IPASimpleLDAPObject

---
 makeaci | 13 ++-----------
 1 file changed, 2 insertions(+), 11 deletions(-)

diff --git a/makeaci b/makeaci
index c4c6f8a..ab65a99 100755
--- a/makeaci
+++ b/makeaci
@@ -30,16 +30,7 @@ from argparse import ArgumentParser
 
 from ipalib import api
 from ipapython.dn import DN
-from ipapython.ipaldap import LDAPEntry, IPASimpleLDAPObject, LDAPClient
-
-
-class FakeLDAPClient(LDAPClient):
-    """A LDAP client that can't do any LDAP operations
-
-    Used to create and manipulate entries without an LDAP connection.
-    """
-    def _init_connection(self):
-        self.conn = IPASimpleLDAPObject('', False, no_schema=True)
+from ipapython.ipaldap import LDAPEntry, LDAPClient
 
 
 def parse_options():
@@ -57,7 +48,7 @@ def generate_aci_lines(api):
     """Yields ACI lines as they appear in ACI.txt, with trailing newline"""
     update_plugin = api.Updater['update_managed_permissions']
     perm_plugin = api.Object['permission']
-    fake_ldap = FakeLDAPClient('')
+    fake_ldap = LDAPClient('', force_schema_updates=False, no_schema=True)
     for name, template, obj in update_plugin.get_templates():
         dn = perm_plugin.get_dn(name)
         entry = fake_ldap.make_entry(dn)
-- 
2.1.0

>From 7d44d21abbc7eeffae451218eddb103cd5c91e98 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Wed, 14 Jan 2015 15:51:52 +0000
Subject: [PATCH 12/16] ldap: Move value encoding from IPASimpleLDAPObject to
 LDAPClient

---
 ipapython/ipaldap.py       | 208 +++++++++++++++++----------------------------
 ipaserver/plugins/ldap2.py |  22 +++--
 2 files changed, 94 insertions(+), 136 deletions(-)

diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index d28d8b4..042d2de 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -462,153 +462,64 @@ class IPASimpleLDAPObject(object):
         else:
             raise TypeError("attempt to pass unsupported type from ldap, value=%s type=%s" %(val, type(val)))
 
-    def convert_result(self, result):
-        '''
-        result is a python-ldap result tuple 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.
-
-        We convert the tuple to an LDAPEntry object.
-        '''
-
-        ipa_result = []
-        for dn_tuple in result:
-            original_dn = dn_tuple[0]
-            original_attrs = dn_tuple[1]
-
-            # original_dn is None if referral instead of an entry was
-            # returned from the LDAP server, we need to skip this item
-            if original_dn is None:
-                log_msg = 'Referral entry ignored: {ref}'\
-                          .format(ref=str(original_attrs))
-                self.log.debug(log_msg)
-
-                continue
-
-            ipa_entry = LDAPEntry(self, DN(original_dn))
-
-            for attr, original_values in original_attrs.items():
-                ipa_entry.raw[attr] = original_values
-            ipa_entry.reset_modlist()
-
-            ipa_result.append(ipa_entry)
-
-        if _debug_log_ldap:
-            self.log.debug('ldap.result: %s', ipa_result)
-        return ipa_result
-
     #---------- python-ldap emulations ----------
 
     def add(self, dn, modlist):
-        assert isinstance(dn, DN)
-        dn = str(dn)
-        modlist = self.encode(modlist)
         return self.conn.add(dn, modlist)
 
     def add_ext(self, dn, modlist, serverctrls=None, clientctrls=None):
-        assert isinstance(dn, DN)
-        dn = str(dn)
-        modlist = self.encode(modlist)
         return self.conn.add_ext(dn, modlist, serverctrls, clientctrls)
 
     def add_ext_s(self, dn, modlist, serverctrls=None, clientctrls=None):
-        assert isinstance(dn, DN)
-        dn = str(dn)
-        modlist = self.encode(modlist)
         return self.conn.add_ext_s(dn, modlist, serverctrls, clientctrls)
 
     def add_s(self, dn, modlist):
-        assert isinstance(dn, DN)
-        dn = str(dn)
-        modlist = self.encode(modlist)
         return self.conn.add_s(dn, modlist)
 
     def bind(self, who, cred, method=ldap.AUTH_SIMPLE):
         self.flush_cached_schema()
-        if who is None:
-            who = DN()
-        assert isinstance(who, DN)
-        who = str(who)
-        cred = self.encode(cred)
         return self.conn.bind(who, cred, method)
 
     def delete(self, dn):
-        assert isinstance(dn, DN)
-        dn = str(dn)
         return self.conn.delete(dn)
 
     def delete_s(self, dn):
-        assert isinstance(dn, DN)
-        dn = str(dn)
         return self.conn.delete_s(dn)
 
     def get_option(self, option):
         return self.conn.get_option(option)
 
     def modify_s(self, dn, modlist):
-        assert isinstance(dn, DN)
-        dn = str(dn)
-        modlist = [(x[0], self.encode(x[1]), self.encode(x[2])) for x in modlist]
         return self.conn.modify_s(dn, modlist)
 
     def modrdn_s(self, dn, newrdn, delold=1):
-        assert isinstance(dn, DN)
-        dn = str(dn)
-        assert isinstance(newrdn, (DN, RDN))
-        newrdn = str(newrdn)
         return self.conn.modrdn_s(dn, newrdn, delold)
 
     def passwd_s(self, dn, oldpw, newpw, serverctrls=None, clientctrls=None):
-        assert isinstance(dn, DN)
-        dn = str(dn)
-        oldpw = self.encode(oldpw)
-        newpw = self.encode(newpw)
         return self.conn.passwd_s(dn, oldpw, newpw, serverctrls, clientctrls)
 
     def rename_s(self, dn, newrdn, newsuperior=None, delold=1):
         # NOTICE: python-ldap of version 2.3.10 and lower does not support
         # serverctrls and clientctrls for rename_s operation. Thus, these
         # options are ommited from this command until really needed
-        assert isinstance(dn, DN)
-        dn = str(dn)
-        assert isinstance(newrdn, (DN, RDN))
-        newrdn = str(newrdn)
         return self.conn.rename_s(dn, newrdn, newsuperior, delold)
 
     def result(self, msgid=ldap.RES_ANY, all=1, timeout=None):
-        resp_type, resp_data = self.conn.result(msgid, all, timeout)
-        resp_data = self.convert_result(resp_data)
-        return resp_type, resp_data
+        return self.conn.result(msgid, all, timeout)
 
     def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None):
-        rtype, rdata, rmsgid, rctrls = self.conn.result3(msgid, all, timeout)
-        rdata = self.convert_result(rdata)
-        return rtype, rdata, rmsgid, rctrls
+        return self.conn.result3(msgid, all, timeout)
 
     def sasl_interactive_bind_s(self, who, auth, serverctrls=None,
                                 clientctrls=None, sasl_flags=ldap.SASL_QUIET):
         self.flush_cached_schema()
-        if who is None:
-            who = DN()
-        assert isinstance(who, DN)
-        who = str(who)
-        return self.conn.sasl_interactive_bind_s(who, auth, serverctrls, clientctrls, sasl_flags)
+        return self.conn.sasl_interactive_bind_s(who, auth, serverctrls,
+                                                 clientctrls, sasl_flags)
 
     def search(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
-        assert isinstance(base, DN)
-        base = str(base)
-        filterstr = self.encode(filterstr)
-        attrlist = self.encode(attrlist)
         return self.conn.search(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):
-        assert isinstance(base, DN)
-        base = str(base)
-        filterstr = self.encode(filterstr)
-        attrlist = self.encode(attrlist)
-
         if _debug_log_ldap:
             self.log.debug(
                 "ldap.search_ext: dn: %s\nfilter: %s\nattrs_list: %s",
@@ -618,42 +529,23 @@ class IPASimpleLDAPObject(object):
         return self.conn.search_ext(base, scope, filterstr, attrlist, attrsonly, serverctrls, clientctrls, timeout, sizelimit)
 
     def search_ext_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0, serverctrls=None, clientctrls=None, timeout=-1, sizelimit=0):
-        assert isinstance(base, DN)
-        base = str(base)
-        filterstr = self.encode(filterstr)
-        attrlist = self.encode(attrlist)
-        ldap_result = self.conn.search_ext_s(base, scope, filterstr, attrlist, attrsonly, serverctrls, clientctrls, timeout, sizelimit)
-        ipa_result = self.convert_result(ldap_result)
-        return ipa_result
+        return self.conn.search_ext_s(base, scope, filterstr, attrlist,
+                                      attrsonly, serverctrls, clientctrls,
+                                      timeout, sizelimit)
 
     def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
-        assert isinstance(base, DN)
-        base = str(base)
-        filterstr = self.encode(filterstr)
-        attrlist = self.encode(attrlist)
-        ldap_result = self.conn.search_s(base, scope, filterstr, attrlist, attrsonly)
-        ipa_result = self.convert_result(ldap_result)
-        return ipa_result
+        return self.conn.search_s(base, scope, filterstr, attrlist,
+                                  attrsonly)
 
     def search_st(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0, timeout=-1):
-        assert isinstance(base, DN)
-        base = str(base)
-        filterstr = self.encode(filterstr)
-        attrlist = self.encode(attrlist)
-        ldap_result = self.conn.search_st(base, scope, filterstr, attrlist, attrsonly, timeout)
-        ipa_result = self.convert_result(ldap_result)
-        return ipa_result
+        return self.conn.search_st(base, scope, filterstr, attrlist, attrsonly,
+                                   timeout)
 
     def set_option(self, option, invalue):
         return self.conn.set_option(option, invalue)
 
     def simple_bind_s(self, who=None, cred='', serverctrls=None, clientctrls=None):
         self.flush_cached_schema()
-        if who is None:
-            who = DN()
-        assert isinstance(who, DN)
-        who = str(who)
-        cred = self.encode(cred)
         return self.conn.simple_bind_s(who, cred, serverctrls, clientctrls)
 
     def start_tls_s(self):
@@ -1100,6 +992,49 @@ class LDAPClient(object):
     def conn(self):
         return self._conn
 
+    def encode(self, val):
+        return self.conn.encode(val)
+
+    def decode(self, val, attr):
+        return self.conn.decode(val, attr)
+
+    def _convert_result(self, result):
+        '''
+        result is a python-ldap result tuple 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.
+
+        We convert the tuple to an LDAPEntry object.
+        '''
+
+        ipa_result = []
+        for dn_tuple in result:
+            original_dn = dn_tuple[0]
+            original_attrs = dn_tuple[1]
+
+            # original_dn is None if referral instead of an entry was
+            # returned from the LDAP server, we need to skip this item
+            if original_dn is None:
+                log_msg = 'Referral entry ignored: {ref}'\
+                          .format(ref=str(original_attrs))
+                self.log.debug(log_msg)
+
+                continue
+
+            ipa_entry = LDAPEntry(self.conn, DN(original_dn))
+
+            for attr, original_values in original_attrs.items():
+                ipa_entry.raw[attr] = original_values
+            ipa_entry.reset_modlist()
+
+            ipa_result.append(ipa_entry)
+
+        if _debug_log_ldap:
+            self.log.debug('ldap.result: %s', ipa_result)
+        return ipa_result
+
     @contextlib.contextmanager
     def error_handler(self, arg_desc=None):
         """Context manager that handles LDAPErrors
@@ -1241,6 +1176,11 @@ class LDAPClient(object):
         Perform simple bind operation.
         """
         with self.error_handler():
+            if bind_dn is None:
+                bind_dn = DN()
+            assert isinstance(bind_dn, DN)
+            bind_dn = str(bind_dn)
+            bind_password = self.encode(bind_password)
             self._conn.simple_bind_s(
                 bind_dn, bind_password, server_controls, client_controls)
 
@@ -1252,7 +1192,7 @@ class LDAPClient(object):
         with self.error_handler():
             auth_tokens = ldap.sasl.external(user_name)
             self._conn.sasl_interactive_bind_s(
-                None, auth_tokens, server_controls, client_controls)
+                '', auth_tokens, server_controls, client_controls)
 
     def gssapi_bind(self, server_controls=None, client_controls=None):
         """
@@ -1261,7 +1201,7 @@ class LDAPClient(object):
         with self.error_handler():
             auth_tokens = ldap.sasl.sasl({}, 'GSSAPI')
             self._conn.sasl_interactive_bind_s(
-                None, auth_tokens, server_controls, client_controls)
+                '', auth_tokens, server_controls, client_controls)
 
     def unbind(self):
         """
@@ -1501,19 +1441,23 @@ class LDAPClient(object):
 
         # pass arguments to python-ldap
         with self.error_handler():
+            filter = self.encode(filter)
+            attrs_list = self.encode(attrs_list)
+
             while True:
                 if paged_search:
                     sctrls = [SimplePagedResultsControl(0, page_size, cookie)]
 
                 try:
                     id = self.conn.search_ext(
-                        base_dn, scope, filter, attrs_list,
+                        str(base_dn), scope, filter, attrs_list,
                         serverctrls=sctrls, timeout=time_limit,
                         sizelimit=size_limit
                     )
                     while True:
                         result = self.conn.result3(id, 0)
                         objtype, res_list, res_id, res_ctrls = result
+                        res_list = self._convert_result(res_list)
                         if not res_list:
                             break
                         if (objtype == ldap.RES_SEARCH_ENTRY or
@@ -1535,7 +1479,7 @@ class LDAPClient(object):
                         sctrls = [SimplePagedResultsControl(0, 0, cookie)]
                         try:
                             self.conn.search_ext_s(
-                                base_dn, scope, filter, attrs_list,
+                                str(base_dn), scope, filter, attrs_list,
                                 serverctrls=sctrls, timeout=time_limit,
                                 sizelimit=size_limit)
                         except ldap.LDAPError, e:
@@ -1613,7 +1557,8 @@ class LDAPClient(object):
         attrs = dict((k, v) for k, v in entry.raw.iteritems() if v)
 
         with self.error_handler():
-            self.conn.add_s(entry.dn, attrs.items())
+            attrs = self.encode(attrs)
+            self.conn.add_s(str(entry.dn), attrs.items())
 
         entry.reset_modlist()
 
@@ -1631,7 +1576,7 @@ class LDAPClient(object):
         if dn[0] == new_rdn:
             raise errors.EmptyModlist()
         with self.error_handler():
-            self.conn.rename_s(dn, new_rdn, delold=int(del_old))
+            self.conn.rename_s(str(dn), str(new_rdn), delold=int(del_old))
             time.sleep(.3)  # Give memberOf plugin a chance to work
 
     def update_entry(self, entry):
@@ -1646,7 +1591,9 @@ class LDAPClient(object):
 
         # pass arguments to python-ldap
         with self.error_handler():
-            self.conn.modify_s(entry.dn, modlist)
+            modlist = [(a, self.encode(b), self.encode(c))
+                       for a, b, c in modlist]
+            self.conn.modify_s(str(entry.dn), modlist)
 
         entry.reset_modlist()
 
@@ -1658,7 +1605,7 @@ class LDAPClient(object):
             dn = entry_or_dn.dn
 
         with self.error_handler():
-            self.conn.delete_s(dn)
+            self.conn.delete_s(str(dn))
 
     def entry_exists(self, dn):
         """
@@ -1797,6 +1744,9 @@ class IPAdmin(LDAPClient):
         #fall back
         self.do_sasl_gssapi_bind(timeout=timeout)
 
-    def modify_s(self, *args, **kwargs):
+    def modify_s(self, dn, modlist):
         # FIXME: for backwards compatibility only
-        return self.conn.modify_s(*args, **kwargs)
+        assert isinstance(dn, DN)
+        dn = str(dn)
+        modlist = [(a, self.encode(b), self.encode(c)) for a, b, c in modlist]
+        return self.conn.modify_s(dn, modlist)
diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py
index 9ab33e2..15e07f2 100644
--- a/ipaserver/plugins/ldap2.py
+++ b/ipaserver/plugins/ldap2.py
@@ -360,9 +360,11 @@ class ldap2(LDAPClient, CrudBackend):
                     ('cn', 'etc'), self.api.env.basedn)
 
         try:
-            upg_entries = self.conn.search_s(upg_dn, _ldap.SCOPE_BASE,
-                                             attrlist=['*'])
-        except _ldap.NO_SUCH_OBJECT:
+            with self.error_handler():
+                upg_entries = self.conn.search_s(str(upg_dn), _ldap.SCOPE_BASE,
+                                                 attrlist=['*'])
+                upg_entries = self._convert_result(upg_entries)
+        except errors.NotFound:
             upg_entries = None
         if not upg_entries or 'originfilter' not in upg_entries[0]:
             raise errors.ACIError(info=_(
@@ -477,7 +479,9 @@ class ldap2(LDAPClient, CrudBackend):
                 conn.unbind()
 
         with self.error_handler():
-            self.conn.passwd_s(dn, old_pass, new_pass)
+            old_pass = self.encode(old_pass)
+            new_pass = self.encode(new_pass)
+            self.conn.passwd_s(str(dn), old_pass, new_pass)
 
     def add_entry_to_group(self, dn, group_dn, member_attr='member', allow_same=False):
         """
@@ -509,7 +513,9 @@ class ldap2(LDAPClient, CrudBackend):
         # update group entry
         try:
             with self.error_handler():
-                self.conn.modify_s(group_dn, modlist)
+                modlist = [(a, self.encode(b), self.encode(c))
+                           for a, b, c in modlist]
+                self.conn.modify_s(str(group_dn), modlist)
         except errors.DatabaseError:
             raise errors.AlreadyGroupMember()
 
@@ -529,7 +535,9 @@ class ldap2(LDAPClient, CrudBackend):
         # update group entry
         try:
             with self.error_handler():
-                self.conn.modify_s(group_dn, modlist)
+                modlist = [(a, self.encode(b), self.encode(c))
+                           for a, b, c in modlist]
+                self.conn.modify_s(str(group_dn), modlist)
         except errors.MidairCollision:
             raise errors.NotGroupMember()
 
@@ -583,7 +591,7 @@ class ldap2(LDAPClient, CrudBackend):
                (_ldap.MOD_REPLACE, 'krblastpwdchange', None)]
 
         with self.error_handler():
-            self.conn.modify_s(dn, mod)
+            self.conn.modify_s(str(dn), mod)
 
     # CrudBackend methods
 
-- 
2.1.0

>From 558b19dc21bb3a2bcb824aec0b19c0ee559226ea Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Wed, 14 Jan 2015 16:49:35 +0000
Subject: [PATCH 13/16] ldap: Use LDAPClient instead of IPASimpleLDAPObject in
 LDAPEntry

---
 ipapython/ipaldap.py                         | 11 +++++++----
 ipatests/test_xmlrpc/test_baseldap_plugin.py |  8 +++++++-
 2 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index 042d2de..b8b0617 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -571,7 +571,7 @@ class LDAPEntry(collections.MutableMapping):
           * LDAPEntry(dn, entry) - create a shallow copy of an existing
             LDAPEntry with a different DN.
           * LDAPEntry(conn, dn, mapping) - create a new LDAPEntry using the
-            specified IPASimpleLDAPObject and DN and optionally initialize
+            specified LDAPClient and DN and optionally initialize
             attributes from the specified mapping object.
 
         Keyword arguments can be used to override values of specific attributes.
@@ -582,7 +582,7 @@ class LDAPEntry(collections.MutableMapping):
             assert _dn is None
             _dn = _conn
             _conn = _conn._conn
-        assert isinstance(_conn, IPASimpleLDAPObject)
+        assert isinstance(_conn, LDAPClient)
 
         if isinstance(_dn, LDAPEntry):
             assert _obj is None
@@ -992,6 +992,9 @@ class LDAPClient(object):
     def conn(self):
         return self._conn
 
+    def get_single_value(self, attr):
+        return self.conn.get_single_value(attr)
+
     def encode(self, val):
         return self.conn.encode(val)
 
@@ -1023,7 +1026,7 @@ class LDAPClient(object):
 
                 continue
 
-            ipa_entry = LDAPEntry(self.conn, DN(original_dn))
+            ipa_entry = LDAPEntry(self, DN(original_dn))
 
             for attr, original_values in original_attrs.items():
                 ipa_entry.raw[attr] = original_values
@@ -1240,7 +1243,7 @@ class LDAPClient(object):
         return DN((primary_key, entry_attrs[primary_key]), parent_dn)
 
     def make_entry(self, _dn=None, _obj=None, **kwargs):
-        return LDAPEntry(self.conn, _dn, _obj, **kwargs)
+        return LDAPEntry(self, _dn, _obj, **kwargs)
 
     # generating filters for find_entry
     # some examples:
diff --git a/ipatests/test_xmlrpc/test_baseldap_plugin.py b/ipatests/test_xmlrpc/test_baseldap_plugin.py
index d5b3388..3ffc041 100644
--- a/ipatests/test_xmlrpc/test_baseldap_plugin.py
+++ b/ipatests/test_xmlrpc/test_baseldap_plugin.py
@@ -187,7 +187,13 @@ def test_entry_to_dict():
             self._schema = FakeSchema()
             self._has_schema = True
 
-    conn = FakeIPASimpleLDAPObject()
+    class FakeLDAPClient(ipaldap.LDAPClient):
+        def __init__(self):
+            super(FakeLDAPClient, self).__init__('ldap://test',
+                                                 force_schema_updates=False)
+            self._conn = FakeIPASimpleLDAPObject()
+
+    conn = FakeLDAPClient()
     rights = {'nothing': 'is'}
 
     entry = ipaldap.LDAPEntry(
-- 
2.1.0

>From bd869739f49ff7313522da40793c9cd62e0bbf33 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 26 Jan 2015 16:33:50 +0000
Subject: [PATCH 14/16] ldap: Move schema handling from IPASimpleLDAPObject to
 LDAPClient

---
 ipalib/plugins/baseldap.py                   |   2 +-
 ipapython/ipaldap.py                         | 541 +++++++++++++--------------
 ipatests/test_xmlrpc/test_baseldap_plugin.py |   9 +-
 3 files changed, 264 insertions(+), 288 deletions(-)

diff --git a/ipalib/plugins/baseldap.py b/ipalib/plugins/baseldap.py
index 4b1c701..85065c6 100644
--- a/ipalib/plugins/baseldap.py
+++ b/ipalib/plugins/baseldap.py
@@ -243,7 +243,7 @@ def entry_to_dict(entry, **options):
         for attr in entry.iterkeys():
             if attr.lower() == 'attributelevelrights':
                 value = entry[attr]
-            elif entry.conn.get_type(attr) is str:
+            elif entry.conn.get_attribute_type(attr) is str:
                 value = entry.raw[attr]
             else:
                 value = list(entry.raw[attr])
diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index b8b0617..d135aa7 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -50,7 +50,6 @@ from ipapython.dnsutil import DNSName
 SASL_GSSAPI = ldap.sasl.sasl({}, 'GSSAPI')
 
 DEFAULT_TIMEOUT = 10
-DN_SYNTAX_OID = '1.3.6.1.4.1.1466.115.121.1.12'
 _debug_log_ldap = False
 
 _missing = object()
@@ -190,277 +189,18 @@ schema_cache = SchemaCache()
 
 class IPASimpleLDAPObject(object):
     '''
-    The purpose of this class is to provide a boundary between IPA and
-    python-ldap. In IPA we use IPA defined types because they are
-    richer and are designed to meet our needs. We also require that we
-    consistently use those types without exception. On the other hand
-    python-ldap uses different types. The goal is to be able to have
-    IPA code call python-ldap methods using the types native to
-    IPA. This class accomplishes that goal by exposing python-ldap
-    methods which take IPA types, convert them to python-ldap types,
-    call python-ldap, and then convert the results returned by
-    python-ldap into IPA types.
-
     IPA code should never call python-ldap directly, it should only
     call python-ldap methods in this class.
     '''
 
-    # Note: the oid for dn syntax is: 1.3.6.1.4.1.1466.115.121.1.12
-
-    _SYNTAX_MAPPING = {
-        '1.3.6.1.4.1.1466.115.121.1.1'   : str, # ACI item
-        '1.3.6.1.4.1.1466.115.121.1.4'   : str, # Audio
-        '1.3.6.1.4.1.1466.115.121.1.5'   : str, # Binary
-        '1.3.6.1.4.1.1466.115.121.1.8'   : str, # Certificate
-        '1.3.6.1.4.1.1466.115.121.1.9'   : str, # Certificate List
-        '1.3.6.1.4.1.1466.115.121.1.10'  : str, # Certificate Pair
-        '1.3.6.1.4.1.1466.115.121.1.23'  : str, # Fax
-        '1.3.6.1.4.1.1466.115.121.1.28'  : str, # JPEG
-        '1.3.6.1.4.1.1466.115.121.1.40'  : str, # OctetString (same as Binary)
-        '1.3.6.1.4.1.1466.115.121.1.49'  : str, # Supported Algorithm
-        '1.3.6.1.4.1.1466.115.121.1.51'  : str, # Teletext Terminal Identifier
-
-        DN_SYNTAX_OID                    : DN,  # DN, member, memberof
-        '2.16.840.1.113730.3.8.3.3'      : DN,  # enrolledBy
-        '2.16.840.1.113730.3.8.3.18'     : DN,  # managedBy
-        '2.16.840.1.113730.3.8.3.5'      : DN,  # memberUser
-        '2.16.840.1.113730.3.8.3.7'      : DN,  # memberHost
-        '2.16.840.1.113730.3.8.3.20'     : DN,  # memberService
-        '2.16.840.1.113730.3.8.11.4'     : DN,  # ipaNTFallbackPrimaryGroup
-        '2.16.840.1.113730.3.8.11.21'    : DN,  # ipaAllowToImpersonate
-        '2.16.840.1.113730.3.8.11.22'    : DN,  # ipaAllowedTarget
-        '2.16.840.1.113730.3.8.7.1'      : DN,  # memberAllowCmd
-        '2.16.840.1.113730.3.8.7.2'      : DN,  # memberDenyCmd
-
-        '2.16.840.1.113719.1.301.4.14.1' : DN,  # krbRealmReferences
-        '2.16.840.1.113719.1.301.4.17.1' : DN,  # krbKdcServers
-        '2.16.840.1.113719.1.301.4.18.1' : DN,  # krbPwdServers
-        '2.16.840.1.113719.1.301.4.26.1' : DN,  # krbPrincipalReferences
-        '2.16.840.1.113719.1.301.4.29.1' : DN,  # krbAdmServers
-        '2.16.840.1.113719.1.301.4.36.1' : DN,  # krbPwdPolicyReference
-        '2.16.840.1.113719.1.301.4.40.1' : DN,  # krbTicketPolicyReference
-        '2.16.840.1.113719.1.301.4.41.1' : DN,  # krbSubTrees
-        '2.16.840.1.113719.1.301.4.52.1' : DN,  # krbObjectReferences
-        '2.16.840.1.113719.1.301.4.53.1' : DN,  # krbPrincContainerRef
-        '1.3.6.1.4.1.1466.115.121.1.24'  : datetime.datetime,
-    }
-
-    # In most cases we lookup the syntax from the schema returned by
-    # the server. However, sometimes attributes may not be defined in
-    # the schema (e.g. extensibleObject which permits undefined
-    # attributes), or the schema was incorrectly defined (i.e. giving
-    # an attribute the syntax DirectoryString when in fact it's really
-    # a DN). This (hopefully sparse) table allows us to trap these
-    # anomalies and force them to be the syntax we know to be in use.
-    #
-    # FWIW, many entries under cn=config are undefined :-(
-
-    _SYNTAX_OVERRIDE = CIDict({
-        'managedtemplate': DN,
-        'managedbase':     DN,
-        'originscope':     DN,
-        'idnsname':        DNSName,
-        'idnssoamname':    DNSName,
-        'idnssoarname':    DNSName,
-        'dnszoneidnsname': DNSName,
-        'nsds5replicalastupdatestart': unicode,
-        'nsds5replicalastupdateend': unicode,
-        'nsds5replicalastinitstart': unicode,
-        'nsds5replicalastinitend': unicode,
-    })
-    _SINGLE_VALUE_OVERRIDE = CIDict({
-        'nsslapd-ssl-check-hostname': True,
-        'nsslapd-lookthroughlimit': True,
-        'nsslapd-idlistscanlimit': True,
-        'nsslapd-anonlimitsdn': True,
-        'nsslapd-minssf-exclude-rootdse': True,
-    })
-
-    def __init__(self, uri, force_schema_updates, no_schema=False,
-                 decode_attrs=True):
+    def __init__(self, uri):
         """An internal LDAP connection object
 
         :param uri: The LDAP URI to connect to
-        :param force_schema_updates:
-            If true, this object will always request a new schema from the
-            server. If false, a cached schema will be reused if it exists.
-
-            Generally, it should be true if the API context is 'installer' or
-            'updates', but it must be given explicitly since the API object
-            is not always available
-        :param no_schema: If true, schema is never requested from the server.
-        :param decode_attrs:
-            If true, attributes are decoded to Python types according to their
-            syntax.
         """
         self.log = log_mgr.get_logger(self)
         self.uri = uri
         self.conn = SimpleLDAPObject(uri)
-        self._no_schema = no_schema
-        self._has_schema = False
-        self._schema = None
-        self._force_schema_updates = force_schema_updates
-        self._decode_attrs = decode_attrs
-
-    def _get_schema(self):
-        if self._no_schema:
-            return None
-        if not self._has_schema:
-            try:
-                self._schema = schema_cache.get_schema(
-                    self.uri, self.conn,
-                    force_update=self._force_schema_updates)
-            except (errors.ExecutionError, IndexError):
-                pass
-            self._has_schema = True
-        return self._schema
-
-    schema = property(_get_schema, None, None, 'schema associated with this LDAP server')
-
-
-    def flush_cached_schema(self):
-        '''
-        Force this instance to forget it's cached schema and reacquire
-        it from the schema cache.
-        '''
-
-        # Currently this is called during bind operations to assure
-        # we're working with valid schema for a specific
-        # connection. This causes self._get_schema() to query the
-        # schema cache for the server's schema passing along a flag
-        # indicating if we're in a context that requires freshly
-        # loading the schema vs. returning the last cached version of
-        # the schema. If we're in a mode that permits use of
-        # previously cached schema the flush and reacquire is a very
-        # low cost operation.
-        #
-        # The schema is reacquired whenever this object is
-        # instantiated or when binding occurs. The schema is not
-        # reacquired for operations during a bound connection, it is
-        # presumed schema cannot change during this interval. This
-        # provides for maximum efficiency in contexts which do need
-        # schema refreshing by only peforming the refresh inbetween
-        # logical operations that have the potential to cause a schema
-        # change.
-
-        self._has_schema = False
-        self._schema = None
-
-    def get_type(self, attr):
-        if isinstance(attr, unicode):
-            attr = attr.encode('utf-8')
-
-        # Is this a special case attribute?
-        if attr in self._SYNTAX_OVERRIDE:
-            return self._SYNTAX_OVERRIDE[attr]
-
-        if self.schema is None:
-            return unicode
-
-        # Try to lookup the syntax in the schema returned by the server
-        obj = self.schema.get_obj(ldap.schema.AttributeType, attr)
-        if obj is None:
-            return unicode
-
-        return self._SYNTAX_MAPPING.get(obj.syntax, unicode)
-
-    def has_dn_syntax(self, attr):
-        """
-        Check the schema to see if the attribute uses DN syntax.
-
-        Returns True/False
-        """
-        return self.get_type(attr) is DN
-
-    def get_single_value(self, attr):
-        """
-        Check the schema to see if the attribute is single-valued.
-
-        If the attribute is in the schema then returns True/False
-
-        If there is a problem loading the schema or the attribute is
-        not in the schema return None
-        """
-        if isinstance(attr, unicode):
-            attr = attr.encode('utf-8')
-
-        if attr in self._SINGLE_VALUE_OVERRIDE:
-            return self._SINGLE_VALUE_OVERRIDE[attr]
-
-        if self.schema is None:
-            return None
-
-        obj = self.schema.get_obj(ldap.schema.AttributeType, attr)
-        if obj is None:
-            return None
-
-        return obj.single_value
-
-
-    def encode(self, val):
-        """
-        Encode attribute value to LDAP representation (str).
-        """
-        # Booleans are both an instance of bool and int, therefore
-        # test for bool before int otherwise the int clause will be
-        # entered for a boolean value instead of the boolean clause.
-        if isinstance(val, bool):
-            if val:
-                return 'TRUE'
-            else:
-                return 'FALSE'
-        elif isinstance(val, (unicode, float, int, long, Decimal, DN)):
-            return value_to_utf8(val)
-        elif isinstance(val, DNSName):
-            return str(val)
-        elif isinstance(val, str):
-            return val
-        elif isinstance(val, list):
-            return [self.encode(m) for m in val]
-        elif isinstance(val, tuple):
-            return tuple(self.encode(m) for m in val)
-        elif isinstance(val, dict):
-            dct = dict((self.encode(k), self.encode(v)) for k, v in val.iteritems())
-            return dct
-        elif isinstance(val, datetime.datetime):
-            return val.strftime(LDAP_GENERALIZED_TIME_FORMAT)
-        elif val is None:
-            return None
-        else:
-            raise TypeError("attempt to pass unsupported type to ldap, value=%s type=%s" %(val, type(val)))
-
-    def decode(self, val, attr):
-        """
-        Decode attribute value from LDAP representation (str).
-        """
-        if isinstance(val, str):
-            if not self._decode_attrs:
-                return val
-            target_type = self.get_type(attr)
-            try:
-                if target_type is str:
-                    return val
-                elif target_type is unicode:
-                    return val.decode('utf-8')
-                elif target_type is datetime.datetime:
-                    return datetime.datetime.strptime(val, LDAP_GENERALIZED_TIME_FORMAT)
-                else:
-                    return target_type(val)
-            except Exception, e:
-                msg = 'unable to convert the attribute %r value %r to type %s' % (attr, val, target_type)
-                self.log.error(msg)
-                raise ValueError(msg)
-        elif isinstance(val, list):
-            return [self.decode(m, attr) for m in val]
-        elif isinstance(val, tuple):
-            return tuple(self.decode(m, attr) for m in val)
-        elif isinstance(val, dict):
-            dct = dict((unicode_from_utf8(k), self.decode(v, k)) for k, v in val.iteritems())
-            return dct
-        elif val is None:
-            return None
-        else:
-            raise TypeError("attempt to pass unsupported type from ldap, value=%s type=%s" %(val, type(val)))
 
     #---------- python-ldap emulations ----------
 
@@ -477,7 +217,6 @@ class IPASimpleLDAPObject(object):
         return self.conn.add_s(dn, modlist)
 
     def bind(self, who, cred, method=ldap.AUTH_SIMPLE):
-        self.flush_cached_schema()
         return self.conn.bind(who, cred, method)
 
     def delete(self, dn):
@@ -512,7 +251,6 @@ class IPASimpleLDAPObject(object):
 
     def sasl_interactive_bind_s(self, who, auth, serverctrls=None,
                                 clientctrls=None, sasl_flags=ldap.SASL_QUIET):
-        self.flush_cached_schema()
         return self.conn.sasl_interactive_bind_s(who, auth, serverctrls,
                                                  clientctrls, sasl_flags)
 
@@ -545,14 +283,12 @@ class IPASimpleLDAPObject(object):
         return self.conn.set_option(option, invalue)
 
     def simple_bind_s(self, who=None, cred='', serverctrls=None, clientctrls=None):
-        self.flush_cached_schema()
         return self.conn.simple_bind_s(who, cred, serverctrls, clientctrls)
 
     def start_tls_s(self):
         return self.conn.start_tls_s()
 
     def unbind_s(self):
-        self.flush_cached_schema()
         return self.conn.unbind_s()
 
 
@@ -880,7 +616,7 @@ class LDAPEntry(collections.MutableMapping):
             # particularly for schema
             adds = [value for value in new if value not in old]
             dels = [value for value in old if value not in new]
-            if adds and self.conn.get_single_value(name):
+            if adds and self.conn.get_attribute_single_value(name):
                 if len(adds) > 1:
                     raise errors.OnlyOneValueAllowed(attr=name)
                 modlist.append((ldap.MOD_REPLACE, name, adds))
@@ -961,8 +697,16 @@ class LDAPClient(object):
     This class abstracts a LDAP connection, providing methods that work with
     LADPEntries.
 
-    This class is not intended to be used directly; instead, use one of its
-    subclasses, IPAdmin or the ldap2 plugin.
+    The purpose of this class is to provide a boundary between IPA and
+    python-ldap. In IPA we use IPA defined types because they are
+    richer and are designed to meet our needs. We also require that we
+    consistently use those types without exception. On the other hand
+    python-ldap uses different types. The goal is to be able to have
+    IPA code call python-ldap methods using the types native to
+    IPA. This class accomplishes that goal by exposing python-ldap
+    methods which take IPA types, convert them to python-ldap types,
+    call python-ldap, and then convert the results returned by
+    python-ldap into IPA types.
     """
 
     # rules for generating filters from entries
@@ -975,8 +719,93 @@ class LDAPClient(object):
     SCOPE_ONELEVEL = ldap.SCOPE_ONELEVEL
     SCOPE_SUBTREE = ldap.SCOPE_SUBTREE
 
+    _SYNTAX_MAPPING = {
+        '1.3.6.1.4.1.1466.115.121.1.1'   : str, # ACI item
+        '1.3.6.1.4.1.1466.115.121.1.4'   : str, # Audio
+        '1.3.6.1.4.1.1466.115.121.1.5'   : str, # Binary
+        '1.3.6.1.4.1.1466.115.121.1.8'   : str, # Certificate
+        '1.3.6.1.4.1.1466.115.121.1.9'   : str, # Certificate List
+        '1.3.6.1.4.1.1466.115.121.1.10'  : str, # Certificate Pair
+        '1.3.6.1.4.1.1466.115.121.1.12'  : DN,  # Distinguished Name
+        '1.3.6.1.4.1.1466.115.121.1.23'  : str, # Fax
+        '1.3.6.1.4.1.1466.115.121.1.24'  : datetime.datetime,
+        '1.3.6.1.4.1.1466.115.121.1.28'  : str, # JPEG
+        '1.3.6.1.4.1.1466.115.121.1.40'  : str, # OctetString (same as Binary)
+        '1.3.6.1.4.1.1466.115.121.1.49'  : str, # Supported Algorithm
+        '1.3.6.1.4.1.1466.115.121.1.51'  : str, # Teletext Terminal Identifier
+
+        '2.16.840.1.113730.3.8.3.3'      : DN,  # enrolledBy
+        '2.16.840.1.113730.3.8.3.18'     : DN,  # managedBy
+        '2.16.840.1.113730.3.8.3.5'      : DN,  # memberUser
+        '2.16.840.1.113730.3.8.3.7'      : DN,  # memberHost
+        '2.16.840.1.113730.3.8.3.20'     : DN,  # memberService
+        '2.16.840.1.113730.3.8.11.4'     : DN,  # ipaNTFallbackPrimaryGroup
+        '2.16.840.1.113730.3.8.11.21'    : DN,  # ipaAllowToImpersonate
+        '2.16.840.1.113730.3.8.11.22'    : DN,  # ipaAllowedTarget
+        '2.16.840.1.113730.3.8.7.1'      : DN,  # memberAllowCmd
+        '2.16.840.1.113730.3.8.7.2'      : DN,  # memberDenyCmd
+
+        '2.16.840.1.113719.1.301.4.14.1' : DN,  # krbRealmReferences
+        '2.16.840.1.113719.1.301.4.17.1' : DN,  # krbKdcServers
+        '2.16.840.1.113719.1.301.4.18.1' : DN,  # krbPwdServers
+        '2.16.840.1.113719.1.301.4.26.1' : DN,  # krbPrincipalReferences
+        '2.16.840.1.113719.1.301.4.29.1' : DN,  # krbAdmServers
+        '2.16.840.1.113719.1.301.4.36.1' : DN,  # krbPwdPolicyReference
+        '2.16.840.1.113719.1.301.4.40.1' : DN,  # krbTicketPolicyReference
+        '2.16.840.1.113719.1.301.4.41.1' : DN,  # krbSubTrees
+        '2.16.840.1.113719.1.301.4.52.1' : DN,  # krbObjectReferences
+        '2.16.840.1.113719.1.301.4.53.1' : DN,  # krbPrincContainerRef
+    }
+
+    # In most cases we lookup the syntax from the schema returned by
+    # the server. However, sometimes attributes may not be defined in
+    # the schema (e.g. extensibleObject which permits undefined
+    # attributes), or the schema was incorrectly defined (i.e. giving
+    # an attribute the syntax DirectoryString when in fact it's really
+    # a DN). This (hopefully sparse) table allows us to trap these
+    # anomalies and force them to be the syntax we know to be in use.
+    #
+    # FWIW, many entries under cn=config are undefined :-(
+
+    _SYNTAX_OVERRIDE = CIDict({
+        'managedtemplate': DN,
+        'managedbase':     DN,
+        'originscope':     DN,
+        'idnsname':        DNSName,
+        'idnssoamname':    DNSName,
+        'idnssoarname':    DNSName,
+        'dnszoneidnsname': DNSName,
+        'nsds5replicalastupdatestart': unicode,
+        'nsds5replicalastupdateend': unicode,
+        'nsds5replicalastinitstart': unicode,
+        'nsds5replicalastinitend': unicode,
+    })
+    _SINGLE_VALUE_OVERRIDE = CIDict({
+        'nsslapd-ssl-check-hostname': True,
+        'nsslapd-lookthroughlimit': True,
+        'nsslapd-idlistscanlimit': True,
+        'nsslapd-anonlimitsdn': True,
+        'nsslapd-minssf-exclude-rootdse': True,
+    })
+
     def __init__(self, ldap_uri, start_tls=False, force_schema_updates=False,
                  no_schema=False, decode_attrs=True):
+        """Create LDAPClient object.
+
+        :param ldap_uri: The LDAP URI to connect to
+        :param start_tls: Use STARTTLS
+        :param force_schema_updates:
+            If true, this object will always request a new schema from the
+            server. If false, a cached schema will be reused if it exists.
+
+            Generally, it should be true if the API context is 'installer' or
+            'updates', but it must be given explicitly since the API object
+            is not always available
+        :param no_schema: If true, schema is never requested from the server.
+        :param decode_attrs:
+            If true, attributes are decoded to Python types according to their
+            syntax.
+        """
         self.ldap_uri = ldap_uri
         self._start_tls = start_tls
         self._force_schema_updates = force_schema_updates
@@ -985,6 +814,8 @@ class LDAPClient(object):
 
         self.log = log_mgr.get_logger(self)
         self._conn = None
+        self._has_schema = False
+        self._schema = None
 
         self._connect()
 
@@ -992,14 +823,166 @@ class LDAPClient(object):
     def conn(self):
         return self._conn
 
-    def get_single_value(self, attr):
-        return self.conn.get_single_value(attr)
+    def _get_schema(self):
+        if self._no_schema:
+            return None
+
+        if not self._has_schema:
+            try:
+                schema = schema_cache.get_schema(
+                    self.conn.uri, self.conn.conn,
+                    force_update=self._force_schema_updates)
+            except (errors.ExecutionError, IndexError):
+                schema = None
+
+            # bypass ldap2's locking
+            object.__setattr__(self, '_schema', schema)
+            object.__setattr__(self, '_has_schema', True)
+
+        return self._schema
+
+    def _flush_schema(self):
+        '''
+        Force this instance to forget it's cached schema and reacquire
+        it from the schema cache.
+        '''
+
+        # Currently this is called during bind operations to assure
+        # we're working with valid schema for a specific
+        # connection. This causes self._get_schema() to query the
+        # schema cache for the server's schema passing along a flag
+        # indicating if we're in a context that requires freshly
+        # loading the schema vs. returning the last cached version of
+        # the schema. If we're in a mode that permits use of
+        # previously cached schema the flush and reacquire is a very
+        # low cost operation.
+        #
+        # The schema is reacquired whenever this object is
+        # instantiated or when binding occurs. The schema is not
+        # reacquired for operations during a bound connection, it is
+        # presumed schema cannot change during this interval. This
+        # provides for maximum efficiency in contexts which do need
+        # schema refreshing by only peforming the refresh inbetween
+        # logical operations that have the potential to cause a schema
+        # change.
+
+        # bypass ldap2's locking
+        object.__setattr__(self, '_has_schema', False)
+        object.__setattr__(self, '_schema', None)
+
+    def get_attribute_type(self, name_or_oid):
+        if not self._decode_attrs:
+            return str
+
+        if isinstance(name_or_oid, unicode):
+            name_or_oid = name_or_oid.encode('utf-8')
+
+        # Is this a special case attribute?
+        if name_or_oid in self._SYNTAX_OVERRIDE:
+            return self._SYNTAX_OVERRIDE[name_or_oid]
+
+        schema = self._get_schema()
+        if schema is not None:
+            # Try to lookup the syntax in the schema returned by the server
+            obj = schema.get_obj(ldap.schema.AttributeType, name_or_oid)
+            if obj is not None and obj.syntax in self._SYNTAX_MAPPING:
+                return self._SYNTAX_MAPPING[obj.syntax]
+
+        return unicode
+
+    def has_dn_syntax(self, name_or_oid):
+        """
+        Check the schema to see if the attribute uses DN syntax.
+
+        Returns True/False
+        """
+        return self.get_attribute_type(name_or_oid) is DN
+
+    def get_attribute_single_value(self, name_or_oid):
+        """
+        Check the schema to see if the attribute is single-valued.
+
+        If the attribute is in the schema then returns True/False
+
+        If there is a problem loading the schema or the attribute is
+        not in the schema return None
+        """
+        if isinstance(name_or_oid, unicode):
+            name_or_oid = name_or_oid.encode('utf-8')
+
+        if name_or_oid in self._SINGLE_VALUE_OVERRIDE:
+            return self._SINGLE_VALUE_OVERRIDE[name_or_oid]
+
+        schema = self._get_schema()
+        if schema is not None:
+            obj = schema.get_obj(ldap.schema.AttributeType, name_or_oid)
+            if obj is not None:
+                return obj.single_value
+
+        return None
 
     def encode(self, val):
-        return self.conn.encode(val)
+        """
+        Encode attribute value to LDAP representation (str).
+        """
+        # Booleans are both an instance of bool and int, therefore
+        # test for bool before int otherwise the int clause will be
+        # entered for a boolean value instead of the boolean clause.
+        if isinstance(val, bool):
+            if val:
+                return 'TRUE'
+            else:
+                return 'FALSE'
+        elif isinstance(val, (unicode, float, int, long, Decimal, DN)):
+            return value_to_utf8(val)
+        elif isinstance(val, DNSName):
+            return str(val)
+        elif isinstance(val, str):
+            return val
+        elif isinstance(val, list):
+            return [self.encode(m) for m in val]
+        elif isinstance(val, tuple):
+            return tuple(self.encode(m) for m in val)
+        elif isinstance(val, dict):
+            dct = dict((self.encode(k), self.encode(v)) for k, v in val.iteritems())
+            return dct
+        elif isinstance(val, datetime.datetime):
+            return val.strftime(LDAP_GENERALIZED_TIME_FORMAT)
+        elif val is None:
+            return None
+        else:
+            raise TypeError("attempt to pass unsupported type to ldap, value=%s type=%s" %(val, type(val)))
 
     def decode(self, val, attr):
-        return self.conn.decode(val, attr)
+        """
+        Decode attribute value from LDAP representation (str).
+        """
+        if isinstance(val, str):
+            target_type = self.get_attribute_type(attr)
+            try:
+                if target_type is str:
+                    return val
+                elif target_type is unicode:
+                    return val.decode('utf-8')
+                elif target_type is datetime.datetime:
+                    return datetime.datetime.strptime(val, LDAP_GENERALIZED_TIME_FORMAT)
+                else:
+                    return target_type(val)
+            except Exception, e:
+                msg = 'unable to convert the attribute %r value %r to type %s' % (attr, val, target_type)
+                self.log.error(msg)
+                raise ValueError(msg)
+        elif isinstance(val, list):
+            return [self.decode(m, attr) for m in val]
+        elif isinstance(val, tuple):
+            return tuple(self.decode(m, attr) for m in val)
+        elif isinstance(val, dict):
+            dct = dict((unicode_from_utf8(k), self.decode(v, k)) for k, v in val.iteritems())
+            return dct
+        elif val is None:
+            return None
+        else:
+            raise TypeError("attempt to pass unsupported type from ldap, value=%s type=%s" %(val, type(val)))
 
     def _convert_result(self, result):
         '''
@@ -1115,10 +1098,7 @@ class LDAPClient(object):
     @property
     def schema(self):
         """schema associated with this LDAP server"""
-        return self.conn.schema
-
-    def has_dn_syntax(self, attr):
-        return self.conn.has_dn_syntax(attr)
+        return self._get_schema()
 
     def get_allowed_attributes(self, objectclasses, raise_on_unknown=False):
         if self.schema is None:
@@ -1157,10 +1137,7 @@ class LDAPClient(object):
         with self.error_handler():
             # bypass ldap2's locking
             object.__setattr__(self, '_conn',
-                               IPASimpleLDAPObject(self.ldap_uri,
-                                                   self._force_schema_updates,
-                                                   self._no_schema,
-                                                   self._decode_attrs))
+                               IPASimpleLDAPObject(self.ldap_uri))
 
             if self._start_tls:
                 self._conn.start_tls_s()
@@ -1179,6 +1156,7 @@ class LDAPClient(object):
         Perform simple bind operation.
         """
         with self.error_handler():
+            self._flush_schema()
             if bind_dn is None:
                 bind_dn = DN()
             assert isinstance(bind_dn, DN)
@@ -1194,6 +1172,7 @@ class LDAPClient(object):
         """
         with self.error_handler():
             auth_tokens = ldap.sasl.external(user_name)
+            self._flush_schema()
             self._conn.sasl_interactive_bind_s(
                 '', auth_tokens, server_controls, client_controls)
 
@@ -1203,6 +1182,7 @@ class LDAPClient(object):
         """
         with self.error_handler():
             auth_tokens = ldap.sasl.sasl({}, 'GSSAPI')
+            self._flush_schema()
             self._conn.sasl_interactive_bind_s(
                 '', auth_tokens, server_controls, client_controls)
 
@@ -1211,6 +1191,7 @@ class LDAPClient(object):
         Perform unbind operation.
         """
         with self.error_handler():
+            self._flush_schema()
             self.conn.unbind_s()
 
     def make_dn_from_attr(self, attr, value, parent_dn=None):
diff --git a/ipatests/test_xmlrpc/test_baseldap_plugin.py b/ipatests/test_xmlrpc/test_baseldap_plugin.py
index 3ffc041..6b19e57 100644
--- a/ipatests/test_xmlrpc/test_baseldap_plugin.py
+++ b/ipatests/test_xmlrpc/test_baseldap_plugin.py
@@ -181,17 +181,12 @@ def test_entry_to_dict():
             elif name == 'dnattr':
                 return FakeAttributeType(name, '1.3.6.1.4.1.1466.115.121.1.12')
 
-    class FakeIPASimpleLDAPObject(ipaldap.IPASimpleLDAPObject):
-        def __init__(self):
-            super(FakeIPASimpleLDAPObject, self).__init__('ldap://test', False)
-            self._schema = FakeSchema()
-            self._has_schema = True
-
     class FakeLDAPClient(ipaldap.LDAPClient):
         def __init__(self):
             super(FakeLDAPClient, self).__init__('ldap://test',
                                                  force_schema_updates=False)
-            self._conn = FakeIPASimpleLDAPObject()
+            self._has_schema = True
+            self._schema = FakeSchema()
 
     conn = FakeLDAPClient()
     rights = {'nothing': 'is'}
-- 
2.1.0

>From 9076defa131433e33f3534e912ae8e558d91f093 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 20 Jan 2015 12:36:14 +0000
Subject: [PATCH 15/16] ldap: Use SimpleLDAPObject instead of
 IPASimpleLDAPObject in LDAPClient

---
 ipapython/ipaldap.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index d135aa7..939ce15 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -830,7 +830,7 @@ class LDAPClient(object):
         if not self._has_schema:
             try:
                 schema = schema_cache.get_schema(
-                    self.conn.uri, self.conn.conn,
+                    self.ldap_uri, self.conn,
                     force_update=self._force_schema_updates)
             except (errors.ExecutionError, IndexError):
                 schema = None
@@ -1137,7 +1137,7 @@ class LDAPClient(object):
         with self.error_handler():
             # bypass ldap2's locking
             object.__setattr__(self, '_conn',
-                               IPASimpleLDAPObject(self.ldap_uri))
+                               ldap.initialize(self.ldap_uri))
 
             if self._start_tls:
                 self._conn.start_tls_s()
-- 
2.1.0

>From b9fae2c2a5b358f638982dea15479b90d3e49719 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Wed, 14 Jan 2015 17:09:58 +0000
Subject: [PATCH 16/16] ldap: Remove IPASimpleLDAPObject

---
 ipapython/ipaldap.py | 105 ---------------------------------------------------
 1 file changed, 105 deletions(-)

diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index 939ce15..730b6bd 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -187,111 +187,6 @@ class SchemaCache(object):
 schema_cache = SchemaCache()
 
 
-class IPASimpleLDAPObject(object):
-    '''
-    IPA code should never call python-ldap directly, it should only
-    call python-ldap methods in this class.
-    '''
-
-    def __init__(self, uri):
-        """An internal LDAP connection object
-
-        :param uri: The LDAP URI to connect to
-        """
-        self.log = log_mgr.get_logger(self)
-        self.uri = uri
-        self.conn = SimpleLDAPObject(uri)
-
-    #---------- python-ldap emulations ----------
-
-    def add(self, dn, modlist):
-        return self.conn.add(dn, modlist)
-
-    def add_ext(self, dn, modlist, serverctrls=None, clientctrls=None):
-        return self.conn.add_ext(dn, modlist, serverctrls, clientctrls)
-
-    def add_ext_s(self, dn, modlist, serverctrls=None, clientctrls=None):
-        return self.conn.add_ext_s(dn, modlist, serverctrls, clientctrls)
-
-    def add_s(self, dn, modlist):
-        return self.conn.add_s(dn, modlist)
-
-    def bind(self, who, cred, method=ldap.AUTH_SIMPLE):
-        return self.conn.bind(who, cred, method)
-
-    def delete(self, dn):
-        return self.conn.delete(dn)
-
-    def delete_s(self, dn):
-        return self.conn.delete_s(dn)
-
-    def get_option(self, option):
-        return self.conn.get_option(option)
-
-    def modify_s(self, dn, modlist):
-        return self.conn.modify_s(dn, modlist)
-
-    def modrdn_s(self, dn, newrdn, delold=1):
-        return self.conn.modrdn_s(dn, newrdn, delold)
-
-    def passwd_s(self, dn, oldpw, newpw, serverctrls=None, clientctrls=None):
-        return self.conn.passwd_s(dn, oldpw, newpw, serverctrls, clientctrls)
-
-    def rename_s(self, dn, newrdn, newsuperior=None, delold=1):
-        # NOTICE: python-ldap of version 2.3.10 and lower does not support
-        # serverctrls and clientctrls for rename_s operation. Thus, these
-        # options are ommited from this command until really needed
-        return self.conn.rename_s(dn, newrdn, newsuperior, delold)
-
-    def result(self, msgid=ldap.RES_ANY, all=1, timeout=None):
-        return self.conn.result(msgid, all, timeout)
-
-    def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None):
-        return self.conn.result3(msgid, all, timeout)
-
-    def sasl_interactive_bind_s(self, who, auth, serverctrls=None,
-                                clientctrls=None, sasl_flags=ldap.SASL_QUIET):
-        return self.conn.sasl_interactive_bind_s(who, auth, serverctrls,
-                                                 clientctrls, sasl_flags)
-
-    def search(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
-        return self.conn.search(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):
-        if _debug_log_ldap:
-            self.log.debug(
-                "ldap.search_ext: dn: %s\nfilter: %s\nattrs_list: %s",
-                base, filterstr, attrlist)
-
-
-        return self.conn.search_ext(base, scope, filterstr, attrlist, attrsonly, serverctrls, clientctrls, timeout, sizelimit)
-
-    def search_ext_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0, serverctrls=None, clientctrls=None, timeout=-1, sizelimit=0):
-        return self.conn.search_ext_s(base, scope, filterstr, attrlist,
-                                      attrsonly, serverctrls, clientctrls,
-                                      timeout, sizelimit)
-
-    def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
-        return self.conn.search_s(base, scope, filterstr, attrlist,
-                                  attrsonly)
-
-    def search_st(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0, timeout=-1):
-        return self.conn.search_st(base, scope, filterstr, attrlist, attrsonly,
-                                   timeout)
-
-    def set_option(self, option, invalue):
-        return self.conn.set_option(option, invalue)
-
-    def simple_bind_s(self, who=None, cred='', serverctrls=None, clientctrls=None):
-        return self.conn.simple_bind_s(who, cred, serverctrls, clientctrls)
-
-    def start_tls_s(self):
-        return self.conn.start_tls_s()
-
-    def unbind_s(self):
-        return self.conn.unbind_s()
-
-
 class LDAPEntry(collections.MutableMapping):
     __slots__ = ('_conn', '_dn', '_names', '_nice', '_raw', '_sync',
                  '_not_list', '_orig', '_raw_view', '_single_value_view')
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to