mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
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>
This commit is contained in:
parent
c314411130
commit
2a459ce0f2
@ -64,6 +64,7 @@ if __name__ == '__main__':
|
|||||||
"install": ["ipaplatform"],
|
"install": ["ipaplatform"],
|
||||||
"otptoken_yubikey": ["python-yubico", "pyusb"],
|
"otptoken_yubikey": ["python-yubico", "pyusb"],
|
||||||
"csrgen": ["cffi", "jinja2"],
|
"csrgen": ["cffi", "jinja2"],
|
||||||
|
"ldap": ["python-ldap"], # ipapython.ipaldap
|
||||||
},
|
},
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
)
|
)
|
||||||
|
@ -423,10 +423,16 @@ import sys
|
|||||||
import functools
|
import functools
|
||||||
|
|
||||||
import cryptography.x509
|
import cryptography.x509
|
||||||
from ldap.dn import str2dn, dn2str
|
|
||||||
from ldap import DECODING_ERROR
|
|
||||||
import six
|
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:
|
if six.PY3:
|
||||||
unicode = str
|
unicode = str
|
||||||
|
|
||||||
|
165
ipapython/dn_ctypes.py
Normal file
165
ipapython/dn_ctypes.py
Normal file
@ -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)
|
@ -43,11 +43,11 @@ if __name__ == '__main__':
|
|||||||
# "ipalib", # circular dependency
|
# "ipalib", # circular dependency
|
||||||
"ipaplatform",
|
"ipaplatform",
|
||||||
"netaddr",
|
"netaddr",
|
||||||
"python-ldap",
|
|
||||||
"six",
|
"six",
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
"install": ["dbus-python"], # for certmonger
|
"install": ["dbus-python"], # for certmonger
|
||||||
|
"ldap": ["python-ldap"], # ipapython.ipaldap
|
||||||
# CheckedIPAddress.get_matching_interface
|
# CheckedIPAddress.get_matching_interface
|
||||||
"netifaces": ["netifaces"],
|
"netifaces": ["netifaces"],
|
||||||
},
|
},
|
||||||
|
@ -70,14 +70,13 @@ if __name__ == '__main__':
|
|||||||
"polib",
|
"polib",
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest_multihost",
|
"pytest_multihost",
|
||||||
"python-ldap",
|
|
||||||
"six",
|
"six",
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
"integration": ["dbus-python", "pyyaml", "ipaserver"],
|
"integration": ["dbus-python", "pyyaml", "ipaserver"],
|
||||||
"ipaserver": ["ipaserver"],
|
"ipaserver": ["ipaserver"],
|
||||||
"webui": ["selenium", "pyyaml", "ipaserver"],
|
"webui": ["selenium", "pyyaml", "ipaserver"],
|
||||||
"xmlrpc": ["ipaserver"],
|
"xmlrpc": ["ipaserver", "python-ldap"],
|
||||||
":python_version<'3'": ["mock"],
|
":python_version<'3'": ["mock"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import unittest
|
import unittest
|
||||||
import pytest
|
import pytest
|
||||||
@ -5,7 +6,9 @@ import pytest
|
|||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ipapython.dn import DN, RDN, AVA
|
from ipapython.dn import DN, RDN, AVA, str2dn, dn2str, DECODING_ERROR
|
||||||
|
from ipapython import dn_ctypes
|
||||||
|
|
||||||
|
|
||||||
if six.PY3:
|
if six.PY3:
|
||||||
unicode = str
|
unicode = str
|
||||||
@ -1345,5 +1348,58 @@ class TestInternationalization(unittest.TestCase):
|
|||||||
self.assertEqual(str(dn1), b'cn=' + self.arabic_hello_utf8)
|
self.assertEqual(str(dn1), b'cn=' + self.arabic_hello_utf8)
|
||||||
|
|
||||||
|
|
||||||
|
# 1: LDAP_AVA_STRING
|
||||||
|
# 4: LDAP_AVA_NONPRINTABLE
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'dnstring,expected',
|
||||||
|
[
|
||||||
|
('', []),
|
||||||
|
('cn=bob', [[('cn', 'bob', 1)]]),
|
||||||
|
('cn=Bob', [[('cn', 'Bob', 1)]]),
|
||||||
|
(u'cn=b\xf6b', [[('cn', u'b\xf6b', 4)]]),
|
||||||
|
('cn=bob,sn=builder', [[('cn', 'bob', 1)], [('sn', 'builder', 1)]]),
|
||||||
|
(u'cn=b\xf6b,sn=builder', [
|
||||||
|
[('cn', u'b\xf6b', 4)], [('sn', 'builder', 1)]
|
||||||
|
]),
|
||||||
|
('cn=bob+sn=builder', [[('cn', 'bob', 1), ('sn', 'builder', 1)]]),
|
||||||
|
('dc=ipa,dc=example', [[('dc', 'ipa', 1)], [('dc', 'example', 1)]]),
|
||||||
|
('cn=R\\,W privilege', [[('cn', 'R,W privilege', 1)]]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_str2dn2str(dnstring, expected):
|
||||||
|
dn = str2dn(dnstring)
|
||||||
|
assert dn == expected
|
||||||
|
assert dn2str(dn) == dnstring
|
||||||
|
assert dn_ctypes.str2dn(dnstring) == dn
|
||||||
|
assert dn_ctypes.dn2str(dn) == dnstring
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'dnstring',
|
||||||
|
[
|
||||||
|
'cn',
|
||||||
|
'cn=foo,',
|
||||||
|
'cn=foo+bar',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_str2dn_errors(dnstring):
|
||||||
|
with pytest.raises(DECODING_ERROR):
|
||||||
|
str2dn(dnstring)
|
||||||
|
with pytest.raises(dn_ctypes.DECODING_ERROR):
|
||||||
|
dn_ctypes.str2dn(dnstring)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dn2str_special():
|
||||||
|
dnstring = 'cn=R\\2cW privilege'
|
||||||
|
dnstring2 = 'cn=R\\,W privilege'
|
||||||
|
expected = [[('cn', 'R,W privilege', 1)]]
|
||||||
|
|
||||||
|
dn = str2dn(dnstring)
|
||||||
|
assert dn == expected
|
||||||
|
assert dn2str(dn) == dnstring2
|
||||||
|
assert dn_ctypes.str2dn(dnstring) == dn
|
||||||
|
assert dn_ctypes.dn2str(dn) == dnstring2
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -35,19 +35,14 @@ from contextlib import contextmanager
|
|||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
import six
|
import six
|
||||||
import ldap
|
|
||||||
import ldap.sasl
|
|
||||||
import ldap.modlist
|
|
||||||
|
|
||||||
import ipalib
|
import ipalib
|
||||||
from ipalib import api
|
from ipalib import api
|
||||||
from ipalib.plugable import Plugin
|
from ipalib.plugable import Plugin
|
||||||
from ipalib.request import context
|
from ipalib.request import context
|
||||||
from ipapython.dn import DN
|
from ipapython.dn import DN
|
||||||
from ipapython.ipaldap import ldap_initialize
|
|
||||||
from ipapython.ipautil import run
|
from ipapython.ipautil import run
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# not available with client-only wheel packages
|
# not available with client-only wheel packages
|
||||||
from ipalib.install.kinit import kinit_keytab, kinit_password
|
from ipalib.install.kinit import kinit_keytab, kinit_password
|
||||||
@ -60,6 +55,15 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
paths = None
|
paths = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# not available with optional python-ldap
|
||||||
|
import ldap
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
import ldap.sasl
|
||||||
|
import ldap.modlist
|
||||||
|
from ipapython.ipaldap import ldap_initialize
|
||||||
|
|
||||||
if six.PY3:
|
if six.PY3:
|
||||||
unicode = str
|
unicode = str
|
||||||
|
Loading…
Reference in New Issue
Block a user