diff options
| author | Christian Heimes <cheimes@redhat.com> | 2019-04-08 08:05:52 +0200 |
|---|---|---|
| committer | Christian Heimes <cheimes@redhat.com> | 2019-04-26 12:53:23 +0200 |
| commit | 2a459ce0f2c5e2af2dbe028afcf1d4e83875ce60 (patch) | |
| tree | 8d71dfc6611ea433511d257282e21dac6aa0ed1d /ipapython | |
| parent | c3144111306a2886893b598db695026fc05c8a1f (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.py | 10 | ||||
| -rw-r--r-- | ipapython/dn_ctypes.py | 165 | ||||
| -rw-r--r-- | ipapython/setup.py | 2 |
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"], }, |
