summaryrefslogtreecommitdiffstats
path: root/ipapython
diff options
context:
space:
mode:
authorChristian Heimes <cheimes@redhat.com>2019-04-08 08:05:52 +0200
committerChristian Heimes <cheimes@redhat.com>2019-04-26 12:53:23 +0200
commit2a459ce0f2c5e2af2dbe028afcf1d4e83875ce60 (patch)
tree8d71dfc6611ea433511d257282e21dac6aa0ed1d /ipapython
parentc3144111306a2886893b598db695026fc05c8a1f (diff)
Make python-ldap optional for PyPI packages
python-ldap is a Python package with heavy C extensions. In order to build python-ldap, not only OpenLDAP development headers are necessary, but also OpenSSL, Cyrus SASL, and MIT KRB5 development headers. A fully functional ipaclient doesn't need an LDAP driver. It talks JSON RPC over HTTPS to a server. python-ldap is only used by ipapython.dn.DN to convert a string to a DN with ldap_str2dn(). The function is simple and can be wrapped with ctypes in a bunch of lines. Related: https://pagure.io/freeipa/issue/6468 Signed-off-by: Christian Heimes <cheimes@redhat.com> Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
Diffstat (limited to 'ipapython')
-rw-r--r--ipapython/dn.py10
-rw-r--r--ipapython/dn_ctypes.py165
-rw-r--r--ipapython/setup.py2
3 files changed, 174 insertions, 3 deletions
diff --git a/ipapython/dn.py b/ipapython/dn.py
index 6747a0ece..145f33a87 100644
--- a/ipapython/dn.py
+++ b/ipapython/dn.py
@@ -423,10 +423,16 @@ import sys
import functools
import cryptography.x509
-from ldap.dn import str2dn, dn2str
-from ldap import DECODING_ERROR
import six
+try:
+ from ldap import DECODING_ERROR
+except ImportError:
+ from ipapython.dn_ctypes import str2dn, dn2str, DECODING_ERROR
+else:
+ from ldap.dn import str2dn, dn2str
+
+
if six.PY3:
unicode = str
diff --git a/ipapython/dn_ctypes.py b/ipapython/dn_ctypes.py
new file mode 100644
index 000000000..8417dc534
--- /dev/null
+++ b/ipapython/dn_ctypes.py
@@ -0,0 +1,165 @@
+#
+# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
+#
+"""ctypes wrapper for libldap_str2dn
+"""
+from __future__ import absolute_import
+
+import ctypes
+import ctypes.util
+
+import six
+
+__all__ = ("str2dn", "dn2str", "DECODING_ERROR", "LDAPError")
+
+# load reentrant libldap
+ldap_r_lib = ctypes.util.find_library("ldap_r")
+if ldap_r_lib is None:
+ raise ImportError("libldap_r shared library missing")
+try:
+ lib = ctypes.CDLL(ldap_r_lib)
+except OSError as e:
+ raise ImportError(str(e))
+
+# constants
+LDAP_AVA_FREE_ATTR = 0x0010
+LDAP_AVA_FREE_VALUE = 0x0020
+LDAP_DECODING_ERROR = -4
+
+# mask for AVA flags
+AVA_MASK = ~(LDAP_AVA_FREE_ATTR | LDAP_AVA_FREE_VALUE)
+
+
+class berval(ctypes.Structure):
+ __slots__ = ()
+ _fields_ = [("bv_len", ctypes.c_ulong), ("bv_value", ctypes.c_char_p)]
+
+ def __bytes__(self):
+ buf = ctypes.create_string_buffer(self.bv_value, self.bv_len)
+ return buf.raw
+
+ def __str__(self):
+ return self.__bytes__().decode("utf-8")
+
+ if six.PY2:
+ __unicode__ = __str__
+ __str__ = __bytes__
+
+
+class LDAPAVA(ctypes.Structure):
+ __slots__ = ()
+ _fields_ = [
+ ("la_attr", berval),
+ ("la_value", berval),
+ ("la_flags", ctypes.c_uint16),
+ ]
+
+
+# typedef LDAPAVA** LDAPRDN;
+LDAPRDN = ctypes.POINTER(ctypes.POINTER(LDAPAVA))
+# typedef LDAPRDN* LDAPDN;
+LDAPDN = ctypes.POINTER(LDAPRDN)
+
+
+def errcheck(result, func, arguments):
+ if result != 0:
+ if result == LDAP_DECODING_ERROR:
+ raise DECODING_ERROR
+ else:
+ msg = ldap_err2string(result)
+ raise LDAPError(msg.decode("utf-8"))
+ return result
+
+
+ldap_str2dn = lib.ldap_str2dn
+ldap_str2dn.argtypes = (
+ ctypes.c_char_p,
+ ctypes.POINTER(LDAPDN),
+ ctypes.c_uint16,
+)
+ldap_str2dn.restype = ctypes.c_int16
+ldap_str2dn.errcheck = errcheck
+
+ldap_dnfree = lib.ldap_dnfree
+ldap_dnfree.argtypes = (LDAPDN,)
+ldap_dnfree.restype = None
+
+ldap_err2string = lib.ldap_err2string
+ldap_err2string.argtypes = (ctypes.c_int16,)
+ldap_err2string.restype = ctypes.c_char_p
+
+
+class LDAPError(Exception):
+ pass
+
+
+class DECODING_ERROR(LDAPError):
+ pass
+
+
+# RFC 4514, 2.4
+_ESCAPE_CHARS = {'"', "+", ",", ";", "<", ">", "'", "\x00"}
+
+
+def _escape_dn(dn):
+ if not dn:
+ return ""
+ result = []
+ # a space or number sign occurring at the beginning of the string
+ if dn[0] in {"#", " "}:
+ result.append("\\")
+ for c in dn:
+ if c in _ESCAPE_CHARS:
+ result.append("\\")
+ result.append(c)
+ # a space character occurring at the end of the string
+ if len(dn) > 1 and result[-1] == " ":
+ # insert before last entry
+ result.insert(-1, "\\")
+ return "".join(result)
+
+
+def dn2str(dn):
+ return ",".join(
+ "+".join(
+ "=".join((attr, _escape_dn(value))) for attr, value, _flag in rdn
+ )
+ for rdn in dn
+ )
+
+
+def str2dn(dn, flags=0):
+ if dn is None:
+ return []
+ if isinstance(dn, six.text_type):
+ dn = dn.encode("utf-8")
+
+ ldapdn = LDAPDN()
+ try:
+ ldap_str2dn(dn, ctypes.byref(ldapdn), flags)
+
+ result = []
+ if not ldapdn:
+ # empty DN, str2dn("") == []
+ return result
+
+ for rdn in ldapdn:
+ if not rdn:
+ break
+ avas = []
+ for ava_p in rdn:
+ if not ava_p:
+ break
+ ava = ava_p[0]
+ avas.append(
+ (
+ six.text_type(ava.la_attr),
+ six.text_type(ava.la_value),
+ ava.la_flags & AVA_MASK,
+ )
+ )
+ result.append(avas)
+
+ return result
+ finally:
+ ldap_dnfree(ldapdn)
diff --git a/ipapython/setup.py b/ipapython/setup.py
index 8bba2fe4f..228e2040e 100644
--- a/ipapython/setup.py
+++ b/ipapython/setup.py
@@ -43,11 +43,11 @@ if __name__ == '__main__':
# "ipalib", # circular dependency
"ipaplatform",
"netaddr",
- "python-ldap",
"six",
],
extras_require={
"install": ["dbus-python"], # for certmonger
+ "ldap": ["python-ldap"], # ipapython.ipaldap
# CheckedIPAddress.get_matching_interface
"netifaces": ["netifaces"],
},