diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index fc432b9e..00000000 --- a/.coveragerc +++ /dev/null @@ -1,26 +0,0 @@ -[run] -branch = True -source = - Lib/ -omit = - Lib/slapdtest.py - -[paths] -source = - Lib/ - .tox/*/lib/python*/site-packages/ - -[report] -ignore_errors = False -precision = 1 -exclude_lines = - pragma: no cover - raise NotImplementedError - if 0: - if __name__ == .__main__.: - if PY2 - if not PY2 - -[html] -directory = build/htmlcov -title = python-ldap coverage report diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..36fde319 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +--- +name: CI + +on: + push: + pull_request: + schedule: + # every Monday + - cron: '30 4 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + distros: + name: "Ubuntu with Python ${{ matrix.python-version }}" + runs-on: "${{ matrix.image }}" + strategy: + fail-fast: false + matrix: + python-version: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "pypy3.9" + - "pypy3.10" + image: + - "ubuntu-22.04" + steps: + - name: Checkout + uses: "actions/checkout@v6" + - name: Install apt dependencies + run: | + set -ex + sudo apt update + sudo apt install -y ldap-utils slapd enchant-2 libldap2-dev libsasl2-dev apparmor-utils + - name: Disable AppArmor + run: sudo aa-disable /usr/sbin/slapd + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: "Install Python dependencies" + run: | + set -xe + python -V + python -m pip install --upgrade pip setuptools + python -m pip install --upgrade tox tox-gh-actions + - name: "Test tox with Python ${{ matrix.python-version }}" + run: "python -m tox" diff --git a/.github/workflows/tox-fedora.yml b/.github/workflows/tox-fedora.yml new file mode 100644 index 00000000..cef8df1a --- /dev/null +++ b/.github/workflows/tox-fedora.yml @@ -0,0 +1,35 @@ +on: [push, pull_request] + +name: Tox on Fedora + +permissions: + contents: read + +jobs: + tox_test: + name: Tox env "${{matrix.tox_env}}" on Fedora + steps: + - uses: actions/checkout@v6 + - name: Run Tox tests + uses: fedora-python/tox-github-action@main + with: + tox_env: ${{ matrix.tox_env }} + dnf_install: > + @c-development openldap-devel python3-devel + openldap-servers openldap-clients lcov clang-analyzer valgrind + enchant python3-setuptools + strategy: + matrix: + tox_env: + - py39 + - py310 + - py311 + - py312 + - py313 + - py3-nosasltls + - py3-trace + - pypy3 + - doc + + # Use GitHub's Linux Docker host + runs-on: ubuntu-22.04 diff --git a/.gitignore b/.gitignore index 5f6d0425..75a13538 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ *.pyc __pycache__/ .tox -.coverage* -!.coveragerc /.cache /.pytest_cache @@ -21,3 +19,6 @@ PKG-INFO # generated in the sample workflow /__venv__/ + +# test dirs +python-ldap-test-* diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..91fb6028 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: Doc/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: Doc/requirements.txt diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 81466393..00000000 --- a/.travis.yml +++ /dev/null @@ -1,92 +0,0 @@ -language: python - -sudo: false - -cache: pip - -addons: - apt: - packages: - - ldap-utils - - slapd - - enchant - -# Note: when updating Python versions, also change setup.py and tox.ini -matrix: - include: - - python: 2.7 - env: - - TOXENV=py27 - - WITH_GCOV=1 - - python: 3.4 - env: - - TOXENV=py34 - - WITH_GCOV=1 - - python: 3.5 - env: - - TOXENV=py35 - - WITH_GCOV=1 - - python: 3.6 - env: - - TOXENV=py36 - - WITH_GCOV=1 - - python: pypy - env: - - TOXENV=pypy - - python: 3.7 - env: - - TOXENV=py37 - - WITH_GCOV=1 - dist: xenial - sudo: true - - python: 3.8-dev - env: - - TOXENV=py38 - - CFLAGS_std="-std=c99" - - WITH_GCOV=1 - dist: xenial - sudo: true - - python: 2.7 - env: - - TOXENV=py2-nosasltls - - WITH_GCOV=1 - - python: 3.6 - env: - - TOXENV=py3-nosasltls - - WITH_GCOV=1 - - python: 3.6 - env: - - TOXENV=py3-trace - - python: 3.6 - env: TOXENV=doc - allow_failures: - - env: - - TOXENV=py38 - - CFLAGS_std="-std=c99" - - WITH_GCOV=1 - - env: - - TOXENV=pypy - -env: - global: - # -Wno-int-in-bool-context: don't complain about PyMem_MALLOC() - # -Werror: turn all warnings into fatal errors - # -Werror=declaration-after-statement: strict ISO C90 - - CFLAGS_warnings="-Wno-int-in-bool-context -Werror -Werror=declaration-after-statement" - # Keep C90 compatibility where possible. - # (Python 3.8+ headers use C99 features, so this needs to be overridable.) - - CFLAGS_std="-std=c90" - # pass CFLAGS, CI (for Travis CI) and WITH_GCOV to tox tasks - - TOX_TESTENV_PASSENV="CFLAGS CI WITH_GCOV" - -install: - - pip install "pip>=7.1.0" - - pip install tox-travis tox codecov coverage - -script: CFLAGS="$CFLAGS_warnings $CFLAGS_std" tox - -after_success: - # gather Python coverage - - python -m coverage combine - # send Python and GCOV coverage - - codecov diff --git a/CHANGES b/CHANGES index 2c04f801..05fcf4b6 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,228 @@ +Released 3.4.5 2025-10-10 + +Security fixes: +* CVE-2025-61911 (GHSA-r7r6-cc7p-4v5m): Enforce ``str`` input in + ``ldap.filter.escape_filter_chars`` with ``escape_mode=1``; ensure proper + escaping. (thanks to lukas-eu) +* CVE-2025-61912 (GHSA-p34h-wq7j-h5v6): Correct NUL escaping in + ``ldap.dn.escape_dn_chars`` to ``\00`` per RFC 4514. (thanks to aradona91) + +Fixes: +* ReconnectLDAPObject now properly reconnects on UNAVAILABLE, CONNECT_ERROR + and TIMEOUT exceptions (previously only SERVER_DOWN), fixing reconnection + issues especially during server restarts +* Fixed syncrepl.py to use named constants instead of raw decimal values + for result types +* Fixed error handling in SearchNoOpMixIn to prevent a undefined variable error + +Tests: +* Added comprehensive reconnection test cases including concurrent operation + handling and server restart scenarios + +Doc/ +* Updated installation docs and fixed various documentation typos +* Added ReadTheDocs configuration file + +Infrastructure: +* Add testing and document support for Python 3.13 + +---------------------------------------------------------------- +Released 3.4.4 2022-11-17 + +Fixes: +* Reconnect race condition in ReconnectLDAPObject is now fixed +* Socket ownership is now claimed once we've passed it to libldap +* LDAP_set_option string formats are now compatible with Python 3.12 + +Doc/ +* Security Policy was created +* Broken article links are fixed now +* Bring Conscious Language improvements + +Infrastructure: +* Add testing and document support for Python 3.10, 3.11, and 3.12 + + +---------------------------------------------------------------- +Released 3.4.3 2022-09-15 + +This is a minor release to bring back the removed OPT_X_TLS option. +Please note, it's still a deprecated option and it will be removed in 3.5.0. + +The following deprecated option has been brought back: +- ``OPT_X_TLS`` + +Fixes: +* Sphinx documentation is now successfully built +* pypy3 tests stability was improved +* setup.py deprecation warning is now resolved + + +---------------------------------------------------------------- +Released 3.4.2 2022-07-06 + +This is a minor release to provide out-of-the-box compatibility with the merge +of libldap and libldap_r that happened with OpenLDAP's 2.5 release. + +The following undocumented functions are deprecated and scheduled for removal: +- ``ldap.cidict.strlist_intersection`` +- ``ldap.cidict.strlist_minus`` +- ``ldap.cidict.strlist_union`` + +The following deprecated option has been removed: +- ``OPT_X_TLS`` + +Doc/ +* SASL option usage has been clarified + +Lib/ +* ppolicy control definition has been updated to match Behera draft 11 + +Modules/ +* By default, compile against libldap, checking whether it provides a + threadsafe implementation at runtime +* When decoding controls, the module can now distinguish between no value + (now exposed as ``None``) and an empty value (exposed as ``b''``) +* Several new OpenLDAP options are now supported: + * ``OPT_SOCKET_BIND_ADDRESSES`` + * ``OPT_TCP_USER_TIMEOUT`` + * ``OPT_X_SASL_MAXBUFSIZE`` + * ``OPT_X_SASL_SECPROPS`` + * ``OPT_X_TLS_ECNAME`` + * ``OPT_X_TLS_PEERCERT`` + * ``OPT_X_TLS_PROTOCOL``-related options and constants + +Fixes: +* Encoding/decoding of boolean controls has been corrected +* ldap.schema.models.Entry is now usable +* ``method`` keyword to ReconnectLDAPObject.bind_s is now usable + + +---------------------------------------------------------------- +Released 3.4.0 2021-11-26 + +This release requires Python 3.6 or above, +and is tested with Python 3.6 to 3.10. +Python 2 is no longer supported. + +New code in the python-ldap project is available under the MIT licence +(available in ``LICENCE.MIT`` in the source). Several contributors have agreed +to apply this licence their previous contributions as well. +See the ``README`` for details. + +The following undocumented functions are deprecated and scheduled for removal: +- ``ldap.cidict.strlist_intersection`` +- ``ldap.cidict.strlist_minus`` +- ``ldap.cidict.strlist_union`` + +Security fixes: +* Fix inefficient regular expression which allows denial-of-service attacks + when parsing specially-crafted LDAP schema. + (GHSL-2021-117) + +Changes: +* On MacOS, remove option to make LDAP connections from a file descriptor + when built with the system libldap (which lacks the underlying function, + ``ldap_init_fd``) +* Attribute values of the post read control are now ``bytes`` + instead of ISO8859-1 decoded ``str`` +* ``LDAPUrl`` now treats urlscheme as case-insensitive +* Several OpenLDAP options are now supported: + * ``OPT_X_TLS_REQUIRE_SAN`` + * ``OPT_X_SASL_SSF_EXTERNAL`` + * ``OPT_X_TLS_PEERCERT`` + +Fixes: +* The ``copy()`` method of ``cidict`` was added back. It was unintentionally + removed in 3.3.0 +* Fixed getting/setting ``SASL`` options on big endian platforms +* Unknown LDAP result code are now converted to ``LDAPexception``, + rather than raising a ``SystemError``. + +slapdtest: +* Show stderr of slapd -Ttest +* ``SlapdObject`` uses directory-based configuration of ``slapd`` +* ``SlapdObject`` startup is now faster + +Infrastructure: +* CI now runs on GitHub Actions rather than Travis CI. + + +---------------------------------------------------------------- +Released 3.3.0 2020-06-18 + +Highlights: +* ``LDAPError`` now contains additional fields, such as ctrls, result, msgid +* ``passwd_s`` can now extract the newly generated password +* LDAP connections can now be made from a file descriptor + +This release is tested on Python 3.8, and the beta of Python 3.9. + +The following undocumented functions are deprecated and scheduled for removal: +- ``ldap.cidict.strlist_intersection`` +- ``ldap.cidict.strlist_minus`` +- ``ldap.cidict.strlist_union`` + +Modules/ +* Ensure ReconnectLDAPObject is not left in an inconsistent state after + a reconnection timeout +* Syncrepl now correctly parses SyncInfoMessage when the message is a syncIdSet +* Release GIL around global get/set option call +* Do not leak serverctrls in result functions +* Don't overallocate memory in attrs_from_List() +* Fix thread support check for Python 3 +* With OpenLDAP 2.4.48, use the new header openldap.h + +Lib/ +* Fix some edge cases regarding quoting in the schema tokenizer +* Fix escaping a single space in ldap.escape_dn_chars +* Fix string formatting in ldap.compare_ext_s +* Prefer iterating dict instead of calling dict.keys() + +Doc/ +* Clarify the relationship between initialize() and LDAPObject() +* Improve documentation of TLS options +* Update FAQ to include Samba AD-DC error message + "Operation unavailable without authentication" +* Fix several incorrect examples and demos + (but note that these are not yet tested) +* Update Debian installation instructions for Debian Buster +* Typo fixes in docs and docstrings + +Test/ +* Test and document error cases in ldap.compare_s +* Test if reconnection is done after connection loss +* Make test certificates valid for the far future +* Use slapd -Tt instead of slaptest + +Infrastructure: +* Mark the LICENCE file as a license for setuptools +* Use "unittest discover" rather than "setup.py test" to run tests + + +---------------------------------------------------------------- +Released 3.2.0 2019-03-13 + +Lib/ +* Add support for X-ORIGIN in ldap.schema's ObjectClass +* Make initialize() pass extra keyword arguments to LDAPObject +* ldap.controls.sss: use str instead of basestring on Python 3 +* Provide ldap._trace_* atributes in non-debug mode + +Doc/ +* Fix ReST syntax for links to set_option and get_option + +Tests/ +* Use intersphinx to link to Python documentation +* Correct type of some attribute values to bytes +* Use system-specific ENOTCONN value + +Infrastructure: +* Add testing and document support for Python 3.7 +* Add Python 3.8-dev to Tox and CI configuration +* Add Doc/requirements.txt for building on Read the Docs + + ---------------------------------------------------------------- Released 3.1.0 2018-05-25 diff --git a/Demo/Lib/ldap/async/deltree.py b/Demo/Lib/ldap/async/deltree.py index d3fc6203..9db52c84 100644 --- a/Demo/Lib/ldap/async/deltree.py +++ b/Demo/Lib/ldap/async/deltree.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import ldap,ldap.async class DeleteLeafs(ldap.async.AsyncSearchHandler): diff --git a/Demo/Lib/ldapurl/urlsearch.py b/Demo/Lib/ldapurl/urlsearch.py index b293aa20..c58a1954 100644 --- a/Demo/Lib/ldapurl/urlsearch.py +++ b/Demo/Lib/ldapurl/urlsearch.py @@ -3,7 +3,6 @@ No output of LDAP data is produced except trace output. """ -from __future__ import print_function import sys,getpass,ldap,ldapurl try: diff --git a/Demo/initialize.py b/Demo/initialize.py index 952b3f4b..ab78cdb8 100644 --- a/Demo/initialize.py +++ b/Demo/initialize.py @@ -7,7 +7,6 @@ ldaps://localhost:1391 (LDAP over SSL) ldapi://%2ftmp%2fopenldap2 (domain socket /tmp/openldap2) """ -from __future__ import print_function import sys,os,ldap diff --git a/Demo/ldapcontrols.py b/Demo/ldapcontrols.py index a5ba8d34..eec86b4c 100644 --- a/Demo/ldapcontrols.py +++ b/Demo/ldapcontrols.py @@ -1,4 +1,3 @@ -from __future__ import print_function import ldap,ldapurl,pprint from ldap.controls import LDAPControl,BooleanControl diff --git a/Demo/ldapurl_search.py b/Demo/ldapurl_search.py index 07ffbca5..614ba1af 100644 --- a/Demo/ldapurl_search.py +++ b/Demo/ldapurl_search.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import sys,pprint,ldap from ldap.ldapobject import LDAPObject diff --git a/Demo/matchedvalues.py b/Demo/matchedvalues.py index 7a6e7b6b..bbc2a1bc 100644 --- a/Demo/matchedvalues.py +++ b/Demo/matchedvalues.py @@ -27,7 +27,6 @@ # Matched values control: (mail=*@example.org) # dn: uid=jsmith,ou=People,dc=example,dc=com # mail: jsmith@example.org -from __future__ import print_function import ldap from ldap.controls import MatchedValuesControl @@ -37,7 +36,7 @@ def print_result(search_result): print("dn: %s" % search_result[n][0]) for attr in search_result[n][1].keys(): for i in range(len(search_result[n][1][attr])): - print("%s: %s" % (attr, search_result[n][1][attr][i])) + print("{}: {}".format(attr, search_result[n][1][attr][i])) print diff --git a/Demo/options.py b/Demo/options.py index 28a04374..7a8ee9db 100644 --- a/Demo/options.py +++ b/Demo/options.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import ldap host="localhost:1390" diff --git a/Demo/page_control.py b/Demo/page_control.py index 8238ede3..b92cbf86 100644 --- a/Demo/page_control.py +++ b/Demo/page_control.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function - url = "ldap://localhost:1390" base = "dc=stroeder,dc=de" search_flt = r'(objectClass=*)' diff --git a/Demo/paged_search_ext_s.py b/Demo/paged_search_ext_s.py index d0f82918..3a1a4acd 100644 --- a/Demo/paged_search_ext_s.py +++ b/Demo/paged_search_ext_s.py @@ -1,4 +1,3 @@ -from __future__ import print_function url = "ldap://localhost:1390/" base = "dc=stroeder,dc=de" search_flt = r'(objectClass=*)' diff --git a/Demo/passwd_ext_op.py b/Demo/passwd_ext_op.py index cc5d22cd..6c695482 100644 --- a/Demo/passwd_ext_op.py +++ b/Demo/passwd_ext_op.py @@ -1,7 +1,6 @@ """ Example showing the use of the password extended operation. """ -from __future__ import print_function import sys,ldap,ldapurl,getpass diff --git a/Demo/pyasn1/dds.py b/Demo/pyasn1/dds.py index c803a1de..19270603 100644 --- a/Demo/pyasn1/dds.py +++ b/Demo/pyasn1/dds.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Demo script for Dynamic Entries (see RFC 2589) @@ -8,7 +7,6 @@ pyasn1-modules python-ldap 2.4+ """ -from __future__ import print_function from ldap.extop.dds import RefreshRequest,RefreshResponse diff --git a/Demo/pyasn1/derefcontrol.py b/Demo/pyasn1/derefcontrol.py index 0e7153de..9565a9e8 100644 --- a/Demo/pyasn1/derefcontrol.py +++ b/Demo/pyasn1/derefcontrol.py @@ -3,7 +3,6 @@ This sample script demonstrates the use of the dereference control (see https://tools.ietf.org/html/draft-masarati-ldap-deref) """ -from __future__ import print_function import pprint,ldap,ldap.modlist,ldap.resiter diff --git a/Demo/pyasn1/noopsearch.py b/Demo/pyasn1/noopsearch.py index 2045f50c..a239c0e7 100644 --- a/Demo/pyasn1/noopsearch.py +++ b/Demo/pyasn1/noopsearch.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Demo script for counting searching with OpenLDAP's no-op control @@ -9,7 +8,6 @@ pyasn1-modules python-ldap 2.4+ """ -from __future__ import print_function import sys,ldap,ldapurl,getpass diff --git a/Demo/pyasn1/ppolicy.py b/Demo/pyasn1/ppolicy.py index cf6b2ac9..c143bf16 100644 --- a/Demo/pyasn1/ppolicy.py +++ b/Demo/pyasn1/ppolicy.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Demo script for Password Policy Controls (see https://tools.ietf.org/html/draft-behera-ldap-password-policy) @@ -9,7 +8,6 @@ pyasn1-modules python-ldap 2.4+ """ -from __future__ import print_function import sys,ldap,ldapurl,getpass diff --git a/Demo/pyasn1/psearch.py b/Demo/pyasn1/psearch.py index 3bd59e6d..2703a253 100644 --- a/Demo/pyasn1/psearch.py +++ b/Demo/pyasn1/psearch.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Demo script for Persistent Search Control (see https://tools.ietf.org/html/draft-ietf-ldapext-psearch) @@ -10,7 +9,6 @@ pyasn1-modules python-ldap 2.4+ """ -from __future__ import print_function import sys,ldap,ldapurl,getpass diff --git a/Demo/pyasn1/readentrycontrol.py b/Demo/pyasn1/readentrycontrol.py index 10faa2b2..b3ea6e81 100644 --- a/Demo/pyasn1/readentrycontrol.py +++ b/Demo/pyasn1/readentrycontrol.py @@ -4,7 +4,6 @@ Originally contributed by Andreas Hasenack """ -from __future__ import print_function import pprint,ldap,ldap.modlist @@ -39,8 +38,8 @@ serverctrls = [pr] ) _,_,_,resp_ctrls = l.result3(msg_id) -print("resp_ctrls[0].dn:",resp_ctrls[0].dn) -print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) +print("resp_ctrls[0].dn:", resp_ctrls[0].dn) +print("resp_ctrls[0].entry:", pprint.pformat(resp_ctrls[0].entry)) print("""#--------------------------------------------------------------------------- # Modify entry @@ -56,7 +55,7 @@ ) _,_,_,resp_ctrls = l.result3(msg_id) print("resp_ctrls[0].dn:",resp_ctrls[0].dn) -print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) +print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry)) pr = PostReadControl(criticality=True,attrList=['uidNumber','gidNumber','entryCSN']) @@ -67,7 +66,7 @@ ) _,_,_,resp_ctrls = l.result3(msg_id) print("resp_ctrls[0].dn:",resp_ctrls[0].dn) -print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) +print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry)) print("""#--------------------------------------------------------------------------- # Rename entry @@ -83,7 +82,7 @@ ) _,_,_,resp_ctrls = l.result3(msg_id) print("resp_ctrls[0].dn:",resp_ctrls[0].dn) -print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) +print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry)) pr = PreReadControl(criticality=True,attrList=['uid']) msg_id = l.rename( @@ -94,7 +93,7 @@ ) _,_,_,resp_ctrls = l.result3(msg_id) print("resp_ctrls[0].dn:",resp_ctrls[0].dn) -print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) +print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry)) print("""#--------------------------------------------------------------------------- # Delete entry @@ -108,4 +107,4 @@ ) _,_,_,resp_ctrls = l.result3(msg_id) print("resp_ctrls[0].dn:",resp_ctrls[0].dn) -print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) +print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry)) diff --git a/Demo/pyasn1/sessiontrack.py b/Demo/pyasn1/sessiontrack.py index 58a34e82..491172c0 100644 --- a/Demo/pyasn1/sessiontrack.py +++ b/Demo/pyasn1/sessiontrack.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ demo_track_ldap_session.py @@ -8,7 +7,6 @@ https://tools.ietf.org/html/draft-wahl-ldap-session-03 """ -from __future__ import print_function __version__ = '0.1' diff --git a/Demo/pyasn1/sss_highest_number.py b/Demo/pyasn1/sss_highest_number.py index 5f5bdc50..020dcdb3 100644 --- a/Demo/pyasn1/sss_highest_number.py +++ b/Demo/pyasn1/sss_highest_number.py @@ -38,9 +38,9 @@ class MyLDAPObject(LDAPObject,ResultProcessor): except ldap.SIZELIMIT_EXCEEDED: pass # print result - print 'Highest value of %s' % (id_attr) + print('Highest value of %s' % (id_attr)) if ldap_result: dn,entry = ldap_result[0] - print '->',entry[id_attr] + print('->',entry[id_attr]) else: - print 'not found' + print('not found') diff --git a/Demo/pyasn1/syncrepl.py b/Demo/pyasn1/syncrepl.py index 7103bc64..754b237a 100644 --- a/Demo/pyasn1/syncrepl.py +++ b/Demo/pyasn1/syncrepl.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ This script implements a syncrepl consumer which syncs data from an OpenLDAP server to a local (shelve) database. @@ -8,7 +7,6 @@ The bound user needs read access to the attributes entryDN and entryCSN. """ -from __future__ import print_function # Import modules from Python standard lib import logging @@ -78,7 +76,7 @@ def syncrepl_entry(self, dn, attributes, uuid): logger.debug('Detected %s of entry %r', change_type, dn) # If we have a cookie then this is not our first time being run, # so it must be a change - if 'ldap_cookie' in self.__data: + if 'cookie' in self.__data: self.perform_application_sync(dn, attributes, previous_attributes) def syncrepl_delete(self,uuids): @@ -100,7 +98,7 @@ def syncrepl_present(self,uuids,refreshDeletes=False): deletedEntries = [ uuid for uuid in self.__data.keys() - if uuid not in self.__presentUUIDs and uuid != 'ldap_cookie' + if uuid not in self.__presentUUIDs and uuid != 'cookie' ] self.syncrepl_delete( deletedEntries ) # Phase is now completed, reset the list diff --git a/Demo/rename.py b/Demo/rename.py index 91d7528f..edb78a80 100644 --- a/Demo/rename.py +++ b/Demo/rename.py @@ -1,4 +1,3 @@ -from __future__ import print_function import ldap from getpass import getpass diff --git a/Demo/resiter.py b/Demo/resiter.py index 96e9c90d..9fc14e41 100644 --- a/Demo/resiter.py +++ b/Demo/resiter.py @@ -4,7 +4,6 @@ See https://www.python-ldap.org for details. """ -from __future__ import print_function import ldap,ldap.resiter diff --git a/Demo/sasl_bind.py b/Demo/sasl_bind.py index 667221c1..8453d087 100644 --- a/Demo/sasl_bind.py +++ b/Demo/sasl_bind.py @@ -1,6 +1,5 @@ # For documentation, see comments in Module/LDAPObject.c and the # ldap.sasl module documentation. -from __future__ import print_function import ldap,ldap.sasl diff --git a/Demo/schema.py b/Demo/schema.py index 03f90a83..bdad5e0c 100644 --- a/Demo/schema.py +++ b/Demo/schema.py @@ -1,4 +1,3 @@ -from __future__ import print_function import sys,ldap,ldap.schema schema_attrs = ldap.schema.SCHEMA_ATTRS diff --git a/Demo/schema_tree.py b/Demo/schema_tree.py index 648bb86f..2b182e98 100644 --- a/Demo/schema_tree.py +++ b/Demo/schema_tree.py @@ -4,7 +4,6 @@ Usage: schema_oc_tree.py [--html] [LDAP URL] """ -from __future__ import print_function import sys,getopt,ldap,ldap.schema @@ -15,9 +14,9 @@ def PrintSchemaTree(schema,se_class,se_tree,se_oid,level): """ASCII text output for console""" se_obj = schema.get_obj(se_class,se_oid) if se_obj!=None: - print('| '*(level-1)+'+---'*(level>0), \) - ', '.join(se_obj.names), \ - '(%s)' % se_obj.oid + print('| '*(level-1)+'+---'*(level>0), + ', '.join(se_obj.names), + '(%s)' % se_obj.oid) for sub_se_oid in se_tree[se_oid]: print('| '*(level+1)) PrintSchemaTree(schema,se_class,se_tree,sub_se_oid,level+1) @@ -28,10 +27,10 @@ def HTMLSchemaTree(schema,se_class,se_tree,se_oid,level): se_obj = schema.get_obj(se_class,se_oid) if se_obj!=None: print(""" -
%s (%s)
+
{} ({})
- %s - """ % (', '.join(se_obj.names),se_obj.oid,se_obj.desc)) + {} + """.format(', '.join(se_obj.names),se_obj.oid,se_obj.desc)) if se_tree[se_oid]: print('
') for sub_se_oid in se_tree[se_oid]: diff --git a/Demo/simple.py b/Demo/simple.py index 5fa04250..c82659c3 100644 --- a/Demo/simple.py +++ b/Demo/simple.py @@ -1,4 +1,3 @@ -from __future__ import print_function import sys,getpass import ldap @@ -86,7 +85,7 @@ ("labeleduri", ["labeleduri"]), ("manager", ["cn=Jaga Indulska"]), ("reports", ["reports"]), - ("jpegPhoto", [open("/www/leonard/leonard.jpg","r").read()]), + ("jpegPhoto", [open("/www/leonard/leonard.jpg").read()]), ("uid", ["leonard"]), ("userPassword", [""]) diff --git a/Demo/simplebrowse.py b/Demo/simplebrowse.py index 9d138003..fd4563ae 100644 --- a/Demo/simplebrowse.py +++ b/Demo/simplebrowse.py @@ -3,7 +3,6 @@ # # simple LDAP server browsing example # -from __future__ import print_function import ldap from traceback import print_exc diff --git a/Doc/bytes_mode.rst b/Doc/bytes_mode.rst index dcd3dcb2..3a984bf0 100644 --- a/Doc/bytes_mode.rst +++ b/Doc/bytes_mode.rst @@ -1,34 +1,12 @@ .. _text-bytes: +.. _bytes_mode: Bytes/text management ===================== -Python 3 introduces a hard distinction between *text* (``str``) – sequences of -characters (formally, *Unicode codepoints*) – and ``bytes`` – sequences of -8-bit values used to encode *any* kind of data for storage or transmission. - -Python 2 has the same distinction between ``str`` (bytes) and -``unicode`` (text). -However, values can be implicitly converted between these types as needed, -e.g. when comparing or writing to disk or the network. -The implicit encoding and decoding can be a source of subtle bugs when not -designed and tested adequately. - -In python-ldap 2.x (for Python 2), bytes were used for all fields, -including those guaranteed to be text. - -From version 3.0, python-ldap uses text where appropriate. -On Python 2, the :ref:`bytes mode ` setting influences how text is -handled. - - -What's text, and what's bytes ------------------------------ - The LDAP protocol states that some fields (distinguished names, relative distinguished names, attribute names, queries) be encoded in UTF-8. -In python-ldap, these are represented as text (``str`` on Python 3, -``unicode`` on Python 2). +In python-ldap, these are represented as text (``str`` on Python 3). Attribute *values*, on the other hand, **MAY** contain any type of data, including text. @@ -38,102 +16,26 @@ Thus, attribute values are *always* treated as ``bytes``. Encoding/decoding to other formats – text, images, etc. – is left to the caller. -.. _bytes_mode: - -The bytes mode --------------- - -In Python 3, text values are represented as ``str``, the Unicode text type. - -In Python 2, the behavior of python-ldap 3.0 is influenced by a ``bytes_mode`` -argument to :func:`ldap.initialize`: - -``bytes_mode=True`` (backwards compatible): - Text values are represented as bytes (``str``) encoded using UTF-8. - -``bytes_mode=False`` (future compatible): - Text values are represented as ``unicode``. - -If not given explicitly, python-ldap will default to ``bytes_mode=True``, -but if an ``unicode`` value supplied to it, if will warn and use that value. - -Backwards-compatible behavior is not scheduled for removal until Python 2 -itself reaches end of life. - - -Errors, warnings, and automatic encoding ----------------------------------------- - -While the type of values *returned* from python-ldap is always given by -``bytes_mode``, for Python 2 the behavior for “wrong-type” values *passed in* -can be controlled by the ``bytes_strictness`` argument to -:func:`ldap.initialize`: +Historical note +--------------- -``bytes_strictness='error'`` (default if ``bytes_mode`` is specified): - A ``TypeError`` is raised. - -``bytes_strictness='warn'`` (default when ``bytes_mode`` is not given explicitly): - A warning is raised, and the value is encoded/decoded - using the UTF-8 encoding. - - The warnings are of type :class:`~ldap.LDAPBytesWarning`, which - is a subclass of :class:`BytesWarning` designed to be easily - :ref:`filtered out ` if needed. - -``bytes_strictness='silent'``: - The value is automatically encoded/decoded using the UTF-8 encoding. - -On Python 3, ``bytes_strictness`` is ignored and a ``TypeError`` is always -raised. - -When setting ``bytes_strictness``, an explicit value for ``bytes_mode`` needs -to be given as well. - - -Porting recommendations ------------------------ - -Since end of life of Python 2 is coming in a few years, projects are strongly -urged to make their code compatible with Python 3. General instructions for -this are provided :ref:`in Python documentation ` and in the -`Conservative porting guide`_. - -.. _Conservative porting guide: https://portingguide.readthedocs.io/en/latest/ - - -When porting from python-ldap 2.x, users are advised to update their code -to set ``bytes_mode=False``, and fix any resulting failures. - -The typical usage is as follows. -Note that only the result's *values* are of the ``bytes`` type: - -.. code-block:: pycon - - >>> import ldap - >>> con = ldap.initialize('ldap://localhost:389', bytes_mode=False) - >>> con.simple_bind_s(u'login', u'secret_password') - >>> results = con.search_s(u'ou=people,dc=example,dc=org', ldap.SCOPE_SUBTREE, u"(cn=Raphaël)") - >>> results - [ - ("cn=Raphaël,ou=people,dc=example,dc=org", { - 'cn': [b'Rapha\xc3\xabl'], - 'sn': [b'Barrois'], - }), - ] - - -.. _filter-bytes-warning: - -Filtering warnings ------------------- +Python 3 introduced a hard distinction between *text* (``str``) – sequences of +characters (formally, *Unicode codepoints*) – and ``bytes`` – sequences of +8-bit values used to encode *any* kind of data for storage or transmission. -The bytes mode warnings can be filtered out and ignored with a -simple filter. +Python 2 had the same distinction between ``str`` (bytes) and +``unicode`` (text). +However, values could be implicitly converted between these types as needed, +e.g. when comparing or writing to disk or the network. +The implicit encoding and decoding can be a source of subtle bugs when not +designed and tested adequately. -.. code-block:: python +In python-ldap 2.x (for Python 2), bytes were used for all fields, +including those guaranteed to be text. - import warnings - import ldap +From version 3.0 to 3.3, python-ldap uses text where appropriate. +On Python 2, special ``bytes_mode`` and ``bytes_strictness`` settings +influenced how text was handled. - if hasattr(ldap, 'LDAPBytesWarning'): - warnings.simplefilter('ignore', ldap.LDAPBytesWarning) +From version 3.3 on, only Python 3 is supported. The “bytes mode” settings +are deprecated and do nothing. diff --git a/Doc/conf.py b/Doc/conf.py index e1fb9ee2..e79cfb34 100644 --- a/Doc/conf.py +++ b/Doc/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # python-ldap documentation build configuration file, created by # sphinx-quickstart on Sat Mar 29 15:08:17 2008. @@ -11,6 +10,7 @@ # All configuration values have a default value; values that are commented out # serve to show the default value. +import datetime import sys import os @@ -34,6 +34,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon', ] try: @@ -49,12 +50,12 @@ # The suffix of source filenames. source_suffix = '.rst' -# The master toctree document. -master_doc = 'index' +# The root toctree document. +root_doc = 'index' # General substitutions. project = 'python-ldap' -copyright = '2008-2017, python-ldap project team' +copyright = 'python-ldap project team' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. diff --git a/Doc/contributing.rst b/Doc/contributing.rst index e6481fec..de63a2e3 100644 --- a/Doc/contributing.rst +++ b/Doc/contributing.rst @@ -19,7 +19,7 @@ Communication Always keep in mind that python-ldap is developed and maintained by volunteers. We're happy to share our work, and to work with you to make the library better, -but (until you pay someone), there's obligation to provide assistance. +but (until you pay someone), there's no obligation to provide assistance. So, keep it friendly, respectful, and supportive! @@ -72,9 +72,6 @@ If you're used to open-source Python development with Git, here's the gist: .. _the bug tracker: https://github.com/python-ldap/python-ldap/issues .. _tox: https://tox.readthedocs.io/en/latest/ -Or, if you prefer to avoid closed-source services: - -* ``git clone https://pagure.io/python-ldap`` * Send bug reports and patches to the mailing list. * Run tests with `tox`_; ignore Python interpreters you don't have locally. * Read the documentation directly at `Read the Docs`_. @@ -108,7 +105,7 @@ Notable targets are: Note that no backups are made – please commit any other changes before using this target. - Requires the ``indent`` program and the ``autopep8`` Python module. + Requires the ``indent`` program and the ``black`` Python module. .. _PEP 7: https://www.python.org/dev/peps/pep-0007/ .. _PEP 8: https://www.python.org/dev/peps/pep-0008/ @@ -203,8 +200,6 @@ remember: * Consider making the summary line suitable for the CHANGES document, and starting it with a prefix like ``Lib:`` or ``Tests:``. -* Push to Pagure as well. - If you have good reason to break the “rules”, go ahead and break them, but mention why. @@ -218,13 +213,18 @@ If you are tasked with releasing python-ldap, remember to: * Go through all changes since last version, and add them to ``CHANGES``. * Run :ref:`additional tests` as appropriate, fix any regressions. * Change the release date in ``CHANGES``. +* Update ``__version__`` tags where appropriate (each module ``ldap``, + ``ldif``, ``ldapurl``, ``slapdtest`` has its own copy). * Merge all that (using pull requests). * Run ``python setup.py sdist``, and smoke-test the resulting package (install in a clean virtual environment, import ``ldap``). -* Create Git tag ``python-ldap-{version}``, and push it to GitHub and Pagure. +* Create GPG-signed Git tag: ``git tag -s python-ldap-{version}``. + Push it to GitHub. * Release the ``sdist`` on PyPI. * Announce the release on the mailing list. Mention the Git hash. * Add the release's log from ``CHANGES`` on the `GitHub release page`_. +* Check that python-ldap.org shows the latest version; if not, adjust + things at readthedocs.org .. _GitHub release page: https://github.com/python-ldap/python-ldap/releases diff --git a/Doc/faq.rst b/Doc/faq.rst index c2e7e153..39a6743c 100644 --- a/Doc/faq.rst +++ b/Doc/faq.rst @@ -13,7 +13,7 @@ Project **A3**: see file CHANGES in source distribution or `repository`_. -.. _repository: https://github.com/python-ldap/python-ldap/blob/master/CHANGES +.. _repository: https://github.com/python-ldap/python-ldap/blob/main/CHANGES Usage @@ -29,7 +29,7 @@ Usage .. _pyldap: https://pypi.org/project/pyldap/ -**Q**: Does it work with Python 2.6? (1.5|2.0|2.1|2.2|2.3|2.4|2.5)? +**Q**: Does it work with Python 2.7? (1.5|2.0|2.1|2.2|2.3|2.4|2.5|2.6|2.7)? **A**: No. Old versions of python-ldap are still available from PyPI, though. @@ -45,10 +45,10 @@ That used to work, but after an upgrade it does not work anymore. Why? providing the full functionality. **Q**: My script bound to MS Active Directory but a a search operation results -in the exception :exc:`ldap.OPERATIONS_ERROR` with the diagnostic messages text -“In order to perform this operation a successful bind must be -completed on the connection.” -What's happening here? +in the exception :exc:`ldap.OPERATIONS_ERROR` with the diagnostic message text +*“In order to perform this operation a successful bind must be completed on the +connection.”* Alternatively, a Samba 4 AD returns the diagnostic message +*"Operation unavailable without authentication"*. What's happening here? **A**: When searching from the domain level, MS AD returns referrals (search continuations) for some objects to indicate to the client where to look for these objects. @@ -65,6 +65,10 @@ What's happening here? l = ldap.initialize('ldap://foobar') l.set_option(ldap.OPT_REFERRALS,0) + Note that setting the above option does NOT prevent search continuations + from being returned, rather only that ``libldap`` won't attempt to resolve + referrals. + **Q**: Why am I seeing a ``ldap.SUCCESS`` traceback as output? **A**: Most likely, you are using one of the non-synchronous calls, and probably @@ -80,6 +84,11 @@ What's happening here? `LDAPv2 is considered historic `_ since many years. +**Q**: My TLS settings are ignored/TLS isn't working? + + **A**: Make sure you call `set_option( ldap.OPT_X_TLS_NEWCTX, 0 )` + after changing any of the `OPT_X_TLS_*` options. + Installing diff --git a/Doc/installing.rst b/Doc/installing.rst index 90187a93..1c7ec8c3 100644 --- a/Doc/installing.rst +++ b/Doc/installing.rst @@ -63,8 +63,8 @@ to get up to date information which versions are available. Windows ------- -Unofficial packages for Windows are available on -`Christoph Gohlke's page `_. +Unofficial binary builds for Windows are provided by Christoph Gohlke, available at +`python-ldap-build `_. `FreeBSD `_ @@ -76,12 +76,23 @@ The CVS repository of FreeBSD contains the package macOS ----- -You can install directly with pip:: +You can install directly with pip. First install Xcode command line tools:: $ xcode-select --install - $ pip install python-ldap \ - --global-option=build_ext \ - --global-option="-I$(xcrun --show-sdk-path)/usr/include/sasl" + +Then install python-ldap:: + + $ pip install python-ldap + +For custom installations, you may need to set environment variables:: + + $ export CPPFLAGS="-I$(xcrun --show-sdk-path)/usr/include/sasl" + $ pip install python-ldap + +If using Homebrew:: + + $ brew install openldap + $ pip install python-ldap .. _install-source: @@ -90,11 +101,14 @@ Installing from Source ====================== -python-ldap is built and installed using the Python setuptools. -From a source repository:: +python-ldap is built and installed using modern Python packaging standards +with pyproject.toml configuration. From a source repository:: + + $ pip install . - $ python -m pip install setuptools - $ python setup.py install +For development installation with editable mode:: + + $ pip install -e . If you have more than one Python interpreter installed locally, you should use the same one you plan to use python-ldap with. @@ -111,7 +125,7 @@ Build prerequisites The following software packages are required to be installed on the local system when building python-ldap: -- `Python`_ version 2.7, or 3.4 or later including its development files +- `Python`_ including its development files - C compiler corresponding to your Python version (on Linux, it is usually ``gcc``) - `OpenLDAP`_ client libs version 2.4.11 or later; it is not possible and not supported to build with prior versions. @@ -130,7 +144,7 @@ Alpine Packages for building:: - # apk add build-base openldap-dev python2-dev python3-dev + # apk add build-base openldap-dev python3-dev CentOS ------ @@ -143,19 +157,28 @@ Packages for building:: Debian ------ +Packages for building:: + + # apt-get install build-essential ldap-utils \ + libldap2-dev libsasl2-dev + Packages for building and testing:: - # apt-get install build-essential python3-dev python2.7-dev \ - libldap2-dev libsasl2-dev slapd ldap-utils python-tox \ + # apt-get install build-essential ldap-utils \ + libldap2-dev libsasl2-dev slapd python3-dev tox \ lcov valgrind +.. note:: + + On older releases ``tox`` was called ``python-tox``. + Fedora ------ Packages for building and testing:: # dnf install "@C Development Tools and Libraries" openldap-devel \ - python2-devel python3-devel python3-tox \ + python3-devel python3-tox \ lcov clang-analyzer valgrind .. note:: @@ -164,10 +187,10 @@ Packages for building and testing:: newer are required. -setup.cfg -========= +``setup.cfg`` +============= -The file setup.cfg allows to set some build and installation parameters for +The file ``setup.cfg`` allows to set some build and installation parameters for reflecting the local installation of required software packages. Only section ``[_ldap]`` is described here. More information about other sections can be found in :ref:`Setuptools documentation `. diff --git a/Doc/reference/ldap-controls.rst b/Doc/reference/ldap-controls.rst index 1520b241..2206e101 100644 --- a/Doc/reference/ldap-controls.rst +++ b/Doc/reference/ldap-controls.rst @@ -171,6 +171,7 @@ search. .. autoclass:: ldap.controls.psearch.EntryChangeNotificationControl :members: +.. |ASN.1| replace:: Asn1Type :py:mod:`ldap.controls.sessiontrack` Session tracking control ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -197,6 +198,9 @@ search. :rfc:`4527` - Lightweight Directory Access Protocol (LDAP): Read Entry Controls +.. versionchanged:: 4.0 + The attribute values of the entry now consists of `bytes` instead of ISO8859-1 decoded `str`. + .. autoclass:: ldap.controls.readentry.ReadEntryControl :members: @@ -206,3 +210,16 @@ search. .. autoclass:: ldap.controls.readentry.PostReadControl :members: + + +:py:mod:`ldap.controls.ppolicy` Password Policy Control +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. seealso:: + `draft-behera-ldap-password-policy `_ + +.. py:module:: ldap.controls.ppolicy + :synopsis: passworld policies + +.. autoclass:: ldap.controls.ppolicy.PasswordPolicyControl + :members: diff --git a/Doc/reference/ldap-extop.rst b/Doc/reference/ldap-extop.rst index 8fe49f42..ad70e4e7 100644 --- a/Doc/reference/ldap-extop.rst +++ b/Doc/reference/ldap-extop.rst @@ -38,3 +38,5 @@ This requires :py:mod:`pyasn1` and :py:mod:`pyasn1_modules` to be installed. .. autoclass:: ldap.extop.dds.RefreshResponse :members: + +.. |ASN.1| replace:: Asn1Type diff --git a/Doc/reference/ldap-syncrepl.rst b/Doc/reference/ldap-syncrepl.rst index b3b2cf9a..046b15a9 100644 --- a/Doc/reference/ldap-syncrepl.rst +++ b/Doc/reference/ldap-syncrepl.rst @@ -20,3 +20,6 @@ This module defines the following classes: .. autoclass:: ldap.syncrepl.SyncreplConsumer :members: + +.. autoclass:: ldap.syncrepl.OpenLDAPSyncreplCookie + :members: diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index b13aa6f0..397b7663 100644 --- a/Doc/reference/ldap.rst +++ b/Doc/reference/ldap.rst @@ -29,16 +29,29 @@ Functions This module defines the following functions: -.. py:function:: initialize(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=None, [bytes_mode=None, [bytes_strictness=None]]]]]) -> LDAPObject object +.. py:function:: initialize(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=None, [fileno=None]]]]) -> LDAPObject object Initializes a new connection object for accessing the given LDAP server, - and return an LDAP object (see :ref:`ldap-objects`) used to perform operations + and return an :class:`~ldap.ldapobject.LDAPObject` used to perform operations on that server. The *uri* parameter may be a comma- or whitespace-separated list of URIs containing only the schema, the host, and the port fields. Note that when using multiple URIs you cannot determine to which URI your client - gets connected. + gets connected. If *uri* is :py:const:`None`, the default URIs from + ``ldap.conf`` or :py:const:`OPT_URI` global option will be used. + + If *fileno* parameter is given then the file descriptor will be used to + connect to an LDAP server. The *fileno* must either be a socket file + descriptor as :class:`int` or a file-like object with a *fileno()* method + that returns a socket file descriptor. The socket file descriptor must + already be connected. :class:`~ldap.ldapobject.LDAPObject` does not take + ownership of the file descriptor. It must be kept open during operations + and explicitly closed after the :class:`~ldap.ldapobject.LDAPObject` is + unbound. The internal connection type is determined from the URI, ``TCP`` + for ``ldap://`` / ``ldaps://``, ``IPC`` (``AF_UNIX``) for ``ldapi://``. + The parameter is not available on macOS when python-ldap is compiled with system + libldap, see :py:const:`INIT_FD_AVAIL`. Note that internally the OpenLDAP function `ldap_initialize(3) `_ @@ -53,22 +66,33 @@ This module defines the following functions: *trace_file* specifies a file-like object as target of the debug log and *trace_stack_limit* specifies the stack limit of tracebacks in debug log. - The *bytes_mode* and *bytes_strictness* arguments specify text/bytes - behavior under Python 2. - See :ref:`text-bytes` for a complete documentation. - Possible values for *trace_level* are :py:const:`0` for no logging, :py:const:`1` for only logging the method calls with arguments, :py:const:`2` for logging the method calls with arguments and the complete results and :py:const:`9` for also logging the traceback of method calls. - Additional keyword arguments are passed to :class:`LDAPObject`. + This function is a thin wrapper around instantiating + :class:`~ldap.ldapobject.LDAPObject`. + Any additional keyword arguments are passed to ``LDAPObject``. + It is also fine to instantiate a ``LDAPObject`` (or a subclass) directly. + + The function additionally takes *bytes_mode* and *bytes_strictness* keyword + arguments, which are deprecated and ignored. See :ref:`bytes_mode` for + details. .. seealso:: :rfc:`4516` - Lightweight Directory Access Protocol (LDAP): Uniform Resource Locator + .. versionadded:: 3.3 + + The *fileno* argument was added. + + .. deprecated:: 3.4 + + *bytes_mode* and *bytes_strictness* arguments are deprecated. + .. py:function:: get_option(option) -> int|string @@ -80,6 +104,12 @@ This module defines the following functions: This function sets the value of the global option specified by *option* to *invalue*. + .. note:: + + Most global settings do not affect existing :py:class:`LDAPObject` + connections. Applications should call :py:func:`set_option()` before + they establish connections with :py:func:`initialize`. + .. versionchanged:: 3.1 The deprecated functions ``ldap.init()`` and ``ldap.open()`` were removed. @@ -112,6 +142,12 @@ General Integer where a non-zero value indicates that python-ldap was built with support for SSL/TLS (OpenSSL or similar libs). +.. py:data:: INIT_FD_AVAIL + + Integer where a non-zero value indicates that python-ldap supports + :py:func:`initialize` from a file descriptor. The feature is generally + available except on macOS when python-ldap is compiled with system libldap. + .. _ldap-options: @@ -191,6 +227,9 @@ the following option identifiers are defined as constants: SASL options :::::::::::: +Unlike most other options, SASL options must be set on an +:py:class:`LDAPObject` instance. + .. py:data:: OPT_X_SASL_AUTHCID .. py:data:: OPT_X_SASL_AUTHZID @@ -199,7 +238,7 @@ SASL options .. py:data:: OPT_X_SASL_NOCANON - If set to zero SASL host name canonicalization is disabled. + If set to zero, SASL host name canonicalization is disabled. .. py:data:: OPT_X_SASL_REALM @@ -218,33 +257,237 @@ SASL options TLS options ::::::::::: -.. py:data:: OPT_X_TLS +.. warning:: + + libldap does not materialize all TLS settings immediately. You must use + :py:const:`OPT_X_TLS_NEWCTX` with value ``0`` to instruct libldap to + apply pending TLS settings and create a new internal TLS context:: + + conn = ldap.initialize("ldap://ldap.example") + conn.set_option(ldap.OPT_X_TLS_CACERTFILE, '/path/to/ca.pem') + conn.set_option(ldap.OPT_X_TLS_NEWCTX, 0) + conn.start_tls_s() + conn.simple_bind_s(dn, password) + + +.. py:data:: OPT_X_TLS_NEWCTX + + set and apply TLS settings to internal TLS context. Value ``0`` creates + a new client-side context. + +.. py:data:: OPT_X_TLS_PACKAGE + + Get TLS implementation, known values are + + * ``GnuTLS`` + * ``MozNSS`` (Mozilla NSS) + * ``OpenSSL`` -.. py:data:: OPT_X_TLS_ALLOW .. py:data:: OPT_X_TLS_CACERTDIR + get/set path to directory with CA certs + .. py:data:: OPT_X_TLS_CACERTFILE + get/set path to PEM file with CA certs + .. py:data:: OPT_X_TLS_CERTFILE -.. py:data:: OPT_X_TLS_CIPHER_SUITE + get/set path to file with PEM encoded cert for client cert authentication, + requires :py:const:`OPT_X_TLS_KEYFILE`. -.. py:data:: OPT_X_TLS_CTX +.. py:data:: OPT_X_TLS_KEYFILE + + get/set path to file with PEM encoded key for client cert authentication, + requires :py:const:`OPT_X_TLS_CERTFILE`. + + +.. py:data:: OPT_X_TLS_CRLCHECK + + get/set certificate revocation list (CRL) check mode. CRL validation + requires :py:const:`OPT_X_TLS_CRLFILE`. + + :py:const:`OPT_X_TLS_CRL_NONE` + Don't perform CRL checks + + :py:const:`OPT_X_TLS_CRL_PEER` + Perform CRL check for peer's end entity cert. + + :py:const:`OPT_X_TLS_CRL_ALL` + Perform CRL checks for the whole cert chain + +.. py:data:: OPT_X_TLS_CRLFILE + + get/set path to CRL file + +.. py:data:: OPT_X_TLS_CRL_ALL + + value for :py:const:`OPT_X_TLS_CRLCHECK` + +.. py:data:: OPT_X_TLS_CRL_NONE + + value for :py:const:`OPT_X_TLS_CRLCHECK` + +.. py:data:: OPT_X_TLS_CRL_PEER + + value for :py:const:`OPT_X_TLS_CRLCHECK` + + +.. py:data:: OPT_X_TLS_REQUIRE_CERT + + get/set validation strategy for server cert. + + :py:const:`OPT_X_TLS_NEVER` + Don't check server cert and host name + + :py:const:`OPT_X_TLS_ALLOW` + Used internally by slapd server. + + :py:const:`OPT_X_TLS_DEMAND` + Validate peer cert chain and host name + + :py:const:`OPT_X_TLS_HARD` + Same as :py:const:`OPT_X_TLS_DEMAND` + +.. py:data:: OPT_X_TLS_REQUIRE_SAN + + get/set how OpenLDAP validates subject alternative name extension, + available in OpenLDAP 2.4.52 and newer. + + :py:const:`OPT_X_TLS_NEVER` + Don't check SAN + + :py:const:`OPT_X_TLS_ALLOW` + Check SAN first, always fall back to subject common name (default) + + :py:const:`OPT_X_TLS_TRY` + Check SAN first, only fall back to subject common name, when no SAN + extension is present (:rfc:`6125` conform validation) + + :py:const:`OPT_X_TLS_DEMAND` + Validate peer cert chain and host name + + :py:const:`OPT_X_TLS_HARD` + Require SAN, don't fall back to subject common name + + .. versionadded:: 3.4.0 + +.. py:data:: OPT_X_TLS_ALLOW + + Value for :py:const:`OPT_X_TLS_REQUIRE_CERT` + and :py:const:`OPT_X_TLS_REQUIRE_SAN` .. py:data:: OPT_X_TLS_DEMAND + Value for :py:const:`OPT_X_TLS_REQUIRE_CERT` + and :py:const:`OPT_X_TLS_REQUIRE_SAN` + .. py:data:: OPT_X_TLS_HARD -.. py:data:: OPT_X_TLS_KEYFILE + Value for :py:const:`OPT_X_TLS_REQUIRE_CERT` + and :py:const:`OPT_X_TLS_REQUIRE_SAN` .. py:data:: OPT_X_TLS_NEVER + Value for :py:const:`OPT_X_TLS_REQUIRE_CERT` + and :py:const:`OPT_X_TLS_REQUIRE_SAN` + +.. py:data:: OPT_X_TLS_TRY + + Value for :py:const:`OPT_X_TLS_REQUIRE_CERT` + + .. deprecated:: 3.3.0 + This value is only used by slapd server internally. It will be removed + in the future. + + +.. py:data:: OPT_X_TLS_CIPHER + + get cipher suite name from TLS session + +.. py:data:: OPT_X_TLS_CIPHER_SUITE + + get/set allowed cipher suites + +.. py:data:: OPT_X_TLS_CTX + + get address of internal memory address of TLS context (**DO NOT USE**) + +.. py:data:: OPT_X_TLS_PEERCERT + + Get peer's certificate as binary ASN.1 data structure (DER) + + .. versionadded:: 3.4.1 + + .. note:: + The option leaks memory with OpenLDAP < 2.5.8. + +.. py:data:: OPT_X_TLS_PROTOCOL_MIN + + get/set minimum protocol version (wire protocol version as int) + +.. py:data:: OPT_X_TLS_PROTOCOL_MAX + + get/set maximum protocol version (wire protocol version as int), + available in OpenLDAP 2.5 and newer. + + .. versionadded:: 3.4.1 + +.. py:data:: OPT_X_TLS_PROTOCOL_SSL3 + + Value for :py:const:`OPT_X_TLS_PROTOCOL_MIN` and + :py:const:`OPT_X_TLS_PROTOCOL_MAX`, represents SSL 3 + + .. versionadded:: 3.4.1 + +.. py:data:: OPT_X_TLS_PROTOCOL_TLS1_0 + + Value for :py:const:`OPT_X_TLS_PROTOCOL_MIN` and + :py:const:`OPT_X_TLS_PROTOCOL_MAX`, represents TLS 1.0 + + .. versionadded:: 3.4.1 + +.. py:data:: OPT_X_TLS_PROTOCOL_TLS1_1 + + Value for :py:const:`OPT_X_TLS_PROTOCOL_MIN` and + :py:const:`OPT_X_TLS_PROTOCOL_MAX`, represents TLS 1.1 + + .. versionadded:: 3.4.1 + +.. py:data:: OPT_X_TLS_PROTOCOL_TLS1_2 + + Value for :py:const:`OPT_X_TLS_PROTOCOL_MIN` and + :py:const:`OPT_X_TLS_PROTOCOL_MAX`, represents TLS 1.2 + + .. versionadded:: 3.4.1 + +.. py:data:: OPT_X_TLS_PROTOCOL_TLS1_3 + + Value for :py:const:`OPT_X_TLS_PROTOCOL_MIN` and + :py:const:`OPT_X_TLS_PROTOCOL_MAX`, represents TLS 1.3 + + .. versionadded:: 3.4.1 + +.. py:data:: OPT_X_TLS_VERSION + + Get negotiated TLS protocol version as string + .. py:data:: OPT_X_TLS_RANDOM_FILE -.. py:data:: OPT_X_TLS_REQUIRE_CERT + get/set path to /dev/urandom (**DO NOT USE**) -.. py:data:: OPT_X_TLS_TRY +.. py:data:: OPT_X_TLS + + .. deprecated:: 3.3.0 + The option is deprecated in OpenLDAP and should no longer be used. It + will be removed in the future. + +.. note:: + + OpenLDAP supports several TLS/SSL libraries. OpenSSL is the most common + backend. Some options may not be available when libldap uses NSS, GnuTLS, + or Apple's Secure Transport backend. .. _ldap-keepalive-options: @@ -308,16 +551,27 @@ The module defines the following exceptions: are instead turned into exceptions, raised as soon an the error condition is detected. - The exceptions are accompanied by a dictionary possibly - containing an string value for the key :py:const:`desc` - (giving an English description of the error class) - and/or a string value for the key :py:const:`info` - (giving a string containing more information that the server may have sent). - - A third possible field of this dictionary is :py:const:`matched` and - is set to a truncated form of the name provided or alias dereferenced - for the lowest entry (object or alias) that was matched. - + The exceptions are accompanied by a dictionary with additional information. + All fields are optional and more fields may be added in the future. + Currently, ``python-ldap`` may set the following fields: + + * ``'result'``: a numeric code of the error class. + * ``'desc'``: string giving a description of the error class, as provided + by calling OpenLDAP's ``ldap_err2string`` on the ``result``. + * ``'info'``: string containing more information that the server may + have sent. The value is server-specific: for example, the OpenLDAP server + may send different info messages than Active Directory or 389-DS. + * ``'matched'``: truncated form of the name provided or alias. + dereferenced for the lowest entry (object or alias) that was matched. + * ``'msgid'``: ID of the matching asynchronous request. + This can be used in asynchronous code where :py:meth:`result()` raises the + result of an operation as an exception. For example, this is the case for + :py:meth:`~LDAPObject.compare()`, always raises the boolean result as an + exception (:py:exc:`COMPARE_TRUE` or :py:exc:`COMPARE_FALSE`). + * ``'ctrls'``: list of :py:class:`ldap.controls.LDAPControl` instances + attached to the error. + * ``'errno'``: the C ``errno``, usually set by system calls or ``libc`` + rather than the LDAP libraries. .. py:exception:: ADMINLIMIT_EXCEEDED @@ -351,14 +605,14 @@ The module defines the following exceptions: .. py:exception:: COMPARE_FALSE A compare operation returned false. - (This exception should never be seen because :py:meth:`compare()` returns - a boolean result.) + (This exception should only be seen in asynchronous operations, because + :py:meth:`~LDAPObject.compare_s()` returns a boolean result.) .. py:exception:: COMPARE_TRUE A compare operation returned true. - (This exception should never be seen because :py:meth:`compare()` returns - a boolean result.) + (This exception should only be seen in asynchronous operations, because + :py:meth:`~LDAPObject.compare_s()` returns a boolean result.) .. py:exception:: CONFIDENTIALITY_REQUIRED @@ -455,10 +709,6 @@ The module defines the following exceptions: .. py:exception:: NO_MEMORY -.. py:exception:: NO_OBJECT_CLASS_MODS - - Object class modifications are not allowed. - .. py:exception:: NO_RESULTS_RETURNED .. py:exception:: NO_SUCH_ATTRIBUTE @@ -562,31 +812,42 @@ The above exceptions are raised when a result code from an underlying API call does not indicate success. +.. _ldap-warnings: + Warnings ======== .. py:class:: LDAPBytesWarning - Raised when bytes/text mismatch in non-strict bytes mode. + This warning is deprecated. python-ldap no longer raises it. - See :ref:`bytes_mode` for details. + It used to be raised under Python 2 when bytes/text mismatch in non-strict + bytes mode. See :ref:`bytes_mode` for details. .. versionadded:: 3.0.0 + .. versionchanged:: 3.4.0 + + Deprecated. .. _ldap-objects: LDAPObject classes ================== -.. py:class:: LDAPObject +.. py:class:: ldap.ldapobject.LDAPObject Instances of :py:class:`LDAPObject` are returned by :py:func:`initialize()`. The connection is automatically unbound and closed when the LDAP object is deleted. - Internally :py:class:`LDAPObject` is set to - :py:class:`~ldap.ldapobject.SimpleLDAPObject` by default. + :py:class:`LDAPObject` is an alias of + :py:class:`~ldap.ldapobject.SimpleLDAPObject`, the default connection class. + If you wish to use a different class, instantiate it directly instead of + calling :func:`initialize()`. + + (It is also possible, but not recommended, to change the default by setting + ``ldap.ldapobject.LDAPObject`` to a different class.) .. autoclass:: ldap.ldapobject.SimpleLDAPObject @@ -699,7 +960,9 @@ and wait for and return with the server's result, or with and the value *value*. The synchronous forms returns ``True`` or ``False``. The asynchronous forms returns the message ID of the initiated request, and the result of the asynchronous compare can be obtained using - :py:meth:`result()`. + :py:meth:`result()`. The operation can fail with an exception, e.g. + :py:exc:`ldap.NO_SUCH_OBJECT` when *dn* does not exist or + :py:exc:`ldap.UNDEFINED_TYPE` for an invalid attribute. Note that the asynchronous technique yields the answer by raising the exception objects :py:exc:`ldap.COMPARE_TRUE` or @@ -709,10 +972,14 @@ and wait for and return with the server's result, or with The *dn* and *attr* arguments are text strings; see :ref:`bytes_mode`. - .. note:: - A design fault in the LDAP API prevents *value* - from containing *NULL* characters. +.. py:method:: LDAPObject.connect() -> None + + Opens a connection to the server if one is not established already. If that + fails, an instance of :py:exc:`ldap.LDAPError` is raised. + + Requires libldap 2.5+ and will fail with :py:exc:`NotImplementedError` + if that is not met. .. py:method:: LDAPObject.delete(dn) -> int @@ -808,7 +1075,7 @@ and wait for and return with the server's result, or with .. py:method:: LDAPObject.passwd(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> int -.. py:method:: LDAPObject.passwd_s(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> None +.. py:method:: LDAPObject.passwd_s(user, oldpw, newpw [, serverctrls=None [, clientctrls=None] [, extract_newpw=False]]]) -> (respoid, respvalue) Perform a ``LDAP Password Modify Extended Operation`` operation on the entry specified by *user*. @@ -819,6 +1086,13 @@ and wait for and return with the server's result, or with of the specified *user* which is sometimes used when a user changes his own password. + *respoid* is always :py:const:`None`. *respvalue* is also + :py:const:`None` unless *newpw* was :py:const:`None`. This requests that + the server generate a new random password. If *extract_newpw* is + :py:const:`True`, this password is a bytes object available through + ``respvalue.genPasswd``, otherwise *respvalue* is the raw ASN.1 response + (this is deprecated and only for backwards compatibility). + *serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`. The asynchronous version returns the initiated message id. @@ -828,6 +1102,7 @@ and wait for and return with the server's result, or with .. seealso:: :rfc:`3062` - LDAP Password Modify Extended Operation + :py:mod:`ldap.extop.passwd` @@ -1099,7 +1374,7 @@ and wait for and return with the server's result, or with This synchronous method implements the LDAP "Who Am I?" extended operation. - It is useful for finding out to find out which identity + It is useful for finding out which identity is assumed by the LDAP server after a SASL bind. .. seealso:: diff --git a/Doc/reference/ldapurl.rst b/Doc/reference/ldapurl.rst index 96b5ed24..eb2106b3 100644 --- a/Doc/reference/ldapurl.rst +++ b/Doc/reference/ldapurl.rst @@ -9,8 +9,7 @@ This module parses and generates LDAP URLs. It is implemented in pure Python and does not rely on any non-standard modules. Therefore it can be used stand- -alone without the rest of the python-ldap package. Compatibility note: This -module has been solely tested on Python 2.x and above. +alone without the rest of the python-ldap package. .. seealso:: @@ -66,6 +65,11 @@ A :py:class:`LDAPUrl` object represents a complete LDAP URL. .. autoclass:: ldapurl.LDAPUrl :members: + .. versionchanged:: 3.4.0 + + The urlscheme is now case insensitive and always converted to lower + case. ``LDAP://localhost`` is equivalent to ``ldap://localhost``. + LDAP URL extensions ^^^^^^^^^^^^^^^^^^^ diff --git a/Doc/reference/ldif.rst b/Doc/reference/ldif.rst index c508f7dc..87dcb70b 100644 --- a/Doc/reference/ldif.rst +++ b/Doc/reference/ldif.rst @@ -61,11 +61,11 @@ Example The following example demonstrates how to write LDIF output of an LDAP entry with :mod:`ldif` module. ->>> import sys,ldif ->>> entry={'objectClass':['top','person'],'cn':['Michael Stroeder'],'sn':['Stroeder']} +>>> import sys, ldif +>>> entry={'objectClass': [b'top', b'person'], 'cn': [b'Michael Stroeder'], 'sn': [b'Stroeder']} >>> dn='cn=Michael Stroeder,ou=Test' >>> ldif_writer=ldif.LDIFWriter(sys.stdout) ->>> ldif_writer.unparse(dn,entry) +>>> ldif_writer.unparse(dn, entry) dn: cn=Michael Stroeder,ou=Test cn: Michael Stroeder objectClass: top diff --git a/Doc/reference/slapdtest.rst b/Doc/reference/slapdtest.rst index bd54bb69..7517e662 100644 --- a/Doc/reference/slapdtest.rst +++ b/Doc/reference/slapdtest.rst @@ -14,6 +14,8 @@ This module is pure Python and does not rely on any non-standard modules. Therefore it can be used stand-alone without the rest of the python-ldap package. +Test fixtures for the popular `pytest` framework are developed in an external project, `pytest-ldap`_. + Functions ^^^^^^^^^ @@ -26,3 +28,5 @@ Classes .. autoclass:: slapdtest.SlapdTestCase :members: + +.. _pytest-ldap: https://pypi.org/project/pytest-ldap/ diff --git a/Doc/requirements.txt b/Doc/requirements.txt new file mode 100644 index 00000000..7102362c --- /dev/null +++ b/Doc/requirements.txt @@ -0,0 +1,2 @@ +pyasn1 +pyasn1_modules diff --git a/Doc/resources.rst b/Doc/resources.rst index 56cb1a1a..795f8b63 100644 --- a/Doc/resources.rst +++ b/Doc/resources.rst @@ -8,13 +8,13 @@ members. Therefore some information might be outdated or links might be broken. *Python LDAP Applications* articles by Matt Butcher --------------------------------------------------- -* `Part 1 - Installing and Configuring the Python-LDAP Library and Binding to an LDAP Directory `_ +* `Part 1 - Installing and Configuring the Python-LDAP Library and Binding to an LDAP Directory `_ This also covers SASL. -* `Part 2 - LDAP Operations `_ -* `Part 3 - More LDAP Operations and the LDAP URL Library `_ -* `Part 4 - LDAP Schema `_ +* `Part 2 - LDAP Operations `_ +* `Part 3 - More LDAP Operations and the LDAP URL Library `_ +* `Part 4 - LDAP Schema `_ Gee, someone waded through the badly documented mysteries of module :mod:`ldap.schema`. diff --git a/Doc/sample_workflow.rst b/Doc/sample_workflow.rst index 8a43553d..76017034 100644 --- a/Doc/sample_workflow.rst +++ b/Doc/sample_workflow.rst @@ -31,8 +31,6 @@ python-ldap won't affect the rest of your system:: $ python3 -m venv __venv__ -(For Python 2, install `virtualenv`_ and use it instead of ``python3 -m venv``.) - .. _git: https://git-scm.com/ .. _virtualenv: https://virtualenv.pypa.io/en/stable/ @@ -63,15 +61,15 @@ This will run tests on all supported versions of Python that you have installed, skipping the ones you don't. To run a subset of test environments, run for example:: - (__venv__)$ tox -e py27,py36 + (__venv__)$ tox -e py36,py39 In addition to ``pyXY`` environments, we have extra environments for checking things independent of the Python version: * ``doc`` checks syntax and spelling of the documentation * ``coverage-report`` generates a test coverage report for Python code. - It must be used last, e.g. ``tox -e py27,py36,coverage-report``. -* ``py2-nosasltls`` and ``py3-nosasltls`` check functionality without + It must be used last, e.g. ``tox -e py36,py39,coverage-report``. +* ``py3-nosasltls`` check functionality without SASL and TLS bindings compiled in. diff --git a/Doc/spelling_wordlist.txt b/Doc/spelling_wordlist.txt index 3ee0e858..4381ebee 100644 --- a/Doc/spelling_wordlist.txt +++ b/Doc/spelling_wordlist.txt @@ -25,6 +25,7 @@ changeNumber changesOnly changeType changeTypes +Christoph cidict clientctrls conf @@ -39,6 +40,7 @@ defresult dereferenced dereferencing desc +dev directoryOperation distinguished distributedOperation @@ -55,7 +57,10 @@ filterstr filterStr formatOID func +Gohlke +GPG Heimdal +Homebrew hostport hrefTarget hrefText @@ -96,14 +101,16 @@ oc oid oids OpenLDAP -Pagure postalAddress pre previousDN processResultsCount Proxied py +pyproject +pytest rdn +readthedocs reentrant refmodule refreshAndPersist @@ -125,8 +132,10 @@ serverctrls sessionSourceIp sessionSourceName sessionTrackingIdentifier +slapadd sizelimit slapd +startup stderr stdout str @@ -138,6 +147,8 @@ subtree syncrepl syntaxes timelimit +TLS +toml tracebacks tuple tuples @@ -145,6 +156,7 @@ UDP Umich unparsing unsigend +urandom uri urlPrefix urlscheme @@ -153,5 +165,6 @@ userPassword usr uuids Valgrind +Xcode whitespace workflow diff --git a/INSTALL b/INSTALL index b9b13d2d..224df4a4 100644 --- a/INSTALL +++ b/INSTALL @@ -1,8 +1,7 @@ Quick build instructions: edit setup.cfg (see Build/ for platform-specific examples) - python setup.py build - python setup.py install + pip install . Detailed instructions are in Doc/installing.rst, or online at: diff --git a/LICENCE.MIT b/LICENCE.MIT new file mode 100644 index 00000000..0c2021f6 --- /dev/null +++ b/LICENCE.MIT @@ -0,0 +1,55 @@ +The MIT License applies to contributions committed after July 1st, 2021, and +to all contributions by the following authors: + +* ​A. Karl Kornel +* Alex Willmer +* Aymeric Augustin +* Bernhard M. Wiedemann +* Bradley Baetz +* Christian Heimes +* Éloi Rivard +* Eyal Cherevatzki +* Florian Best +* Fred Thomsen +* Ivan A. Melnikov +* johnthagen +* Jonathon Reinhart +* Jon Dufresne +* Martin Basti +* Marti Raudsepp +* Miro Hrončok +* Paul Aurich +* Petr Viktorin +* Pieterjan De Potter +* Raphaël Barrois +* Robert Kuska +* Stanislav Láznička +* Tobias Bräutigam +* Tom van Dijk +* Wentao Han +* William Brown + + +------------------------------------------------------------------------------- + +MIT License + +Copyright (c) 2021 python-ldap contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Lib/ldap/__init__.py b/Lib/ldap/__init__.py index 068f9e67..b1797078 100644 --- a/Lib/ldap/__init__.py +++ b/Lib/ldap/__init__.py @@ -23,10 +23,17 @@ _trace_file = open(_trace_file, 'a') atexit.register(_trace_file.close) _trace_stack_limit = None +else: + # Any use of the _trace attributes should be guarded by `if __debug__`, + # so they should not be needed here. + # But, providing different API for debug mode is unnecessarily fragile. + _trace_level = 0 + _trace_file = sys.stderr + _trace_stack_limit = None import _ldap assert _ldap.__version__==__version__, \ - ImportError('ldap %s and _ldap %s version mismatch!' % (__version__,_ldap.__version__)) + ImportError(f'ldap {__version__} and _ldap {_ldap.__version__} version mismatch!') from _ldap import * # call into libldap to initialize it right now LIBLDAP_API_INFO = _ldap.get_option(_ldap.OPT_API_INFO) @@ -47,11 +54,10 @@ def release(self): try: # Check if Python installation was build with thread support - import thread + import threading except ImportError: LDAPLockBaseClass = DummyLock else: - import threading LDAPLockBaseClass = threading.Lock @@ -77,14 +83,14 @@ def acquire(self): if __debug__: global _trace_level if _trace_level>=self._min_trace_level: - _trace_file.write('***%s.acquire() %s %s\n' % (self.__class__.__name__,repr(self),self._desc)) + _trace_file.write('***{}.acquire() {} {}\n'.format(self.__class__.__name__,repr(self),self._desc)) return self._lock.acquire() def release(self): if __debug__: global _trace_level if _trace_level>=self._min_trace_level: - _trace_file.write('***%s.release() %s %s\n' % (self.__class__.__name__,repr(self),self._desc)) + _trace_file.write('***{}.release() {} {}\n'.format(self.__class__.__name__,repr(self),self._desc)) return self._lock.release() diff --git a/Lib/ldap/asyncsearch.py b/Lib/ldap/asyncsearch.py index 6514dd05..6c6929dd 100644 --- a/Lib/ldap/asyncsearch.py +++ b/Lib/ldap/asyncsearch.py @@ -30,7 +30,7 @@ def __init__(self,receivedResultType,expectedResultTypes): Exception.__init__(self) def __str__(self): - return 'Received wrong result type %s (expected one of %s).' % ( + return 'Received wrong result type {} (expected one of {}).'.format( self.receivedResultType, ', '.join(self.expectedResultTypes), ) diff --git a/Lib/ldap/cidict.py b/Lib/ldap/cidict.py index 4f7e0910..65041e0a 100644 --- a/Lib/ldap/cidict.py +++ b/Lib/ldap/cidict.py @@ -5,56 +5,78 @@ See https://www.python-ldap.org/ for details. """ +import warnings +from collections.abc import MutableMapping from ldap import __version__ -from ldap.compat import IterableUserDict +class cidict(MutableMapping): + """ + Case-insensitive but case-respecting dictionary. + """ + __slots__ = ('_keys', '_data') -class cidict(IterableUserDict): - """ - Case-insensitive but case-respecting dictionary. - """ + def __init__(self, default=None): + self._keys = {} + self._data = {} + if default: + self.update(default) + + # MutableMapping abstract methods + + def __getitem__(self, key): + return self._data[key.lower()] + + def __setitem__(self, key, value): + lower_key = key.lower() + self._keys[lower_key] = key + self._data[lower_key] = value + + def __delitem__(self, key): + lower_key = key.lower() + del self._keys[lower_key] + del self._data[lower_key] - def __init__(self,default=None): - self._keys = {} - IterableUserDict.__init__(self,{}) - self.update(default or {}) + def __iter__(self): + return iter(self._keys.values()) - def __getitem__(self,key): - return self.data[key.lower()] + def __len__(self): + return len(self._keys) - def __setitem__(self,key,value): - lower_key = key.lower() - self._keys[lower_key] = key - self.data[lower_key] = value + # Specializations for performance - def __delitem__(self,key): - lower_key = key.lower() - del self._keys[lower_key] - del self.data[lower_key] + def __contains__(self, key): + return key.lower() in self._keys - def update(self,dict): - for key, value in dict.items(): - self[key] = value + def clear(self): + self._keys.clear() + self._data.clear() - def has_key(self,key): - return key in self + def copy(self): + inst = self.__class__.__new__(self.__class__) + inst._data = self._data.copy() + inst._keys = self._keys.copy() + return inst - def __contains__(self,key): - return IterableUserDict.__contains__(self, key.lower()) + __copy__ = copy - def __iter__(self): - return iter(self.keys()) + # Backwards compatibility - def keys(self): - return self._keys.values() + def has_key(self, key): + """Compatibility with python-ldap 2.x""" + return key in self - def items(self): - result = [] - for k in self._keys.values(): - result.append((k,self[k])) - return result + @property + def data(self): + """Compatibility with older IterableUserDict-based implementation""" + warnings.warn( + 'ldap.cidict.cidict.data is an internal attribute; it may be ' + + 'removed at any time', + category=DeprecationWarning, + stacklevel=2, + ) + return self._data def strlist_minus(a,b): @@ -62,6 +84,11 @@ def strlist_minus(a,b): Return list of all items in a which are not in b (a - b). a,b are supposed to be lists of case-insensitive strings. """ + warnings.warn( + "strlist functions are deprecated and will be removed in 4.0", + category=DeprecationWarning, + stacklevel=2, + ) temp = cidict() for elt in b: temp[elt] = elt @@ -77,6 +104,11 @@ def strlist_intersection(a,b): """ Return intersection of two lists of case-insensitive strings a,b. """ + warnings.warn( + "strlist functions are deprecated and will be removed in 4.0", + category=DeprecationWarning, + stacklevel=2, + ) temp = cidict() for elt in a: temp[elt] = elt @@ -92,6 +124,11 @@ def strlist_union(a,b): """ Return union of two lists of case-insensitive strings a,b. """ + warnings.warn( + "strlist functions are deprecated and will be removed in 4.0", + category=DeprecationWarning, + stacklevel=2, + ) temp = cidict() for elt in a: temp[elt] = elt diff --git a/Lib/ldap/compat.py b/Lib/ldap/compat.py index cbfeef57..a287ce4e 100644 --- a/Lib/ldap/compat.py +++ b/Lib/ldap/compat.py @@ -1,113 +1,23 @@ """Compatibility wrappers for Py2/Py3.""" - -import sys -import os - -if sys.version_info[0] < 3: - from UserDict import UserDict, IterableUserDict - from urllib import quote - from urllib import quote_plus - from urllib import unquote as urllib_unquote - from urllib import urlopen - from urlparse import urlparse - - def unquote(uri): - """Specialized unquote that uses UTF-8 for parsing.""" - uri = uri.encode('ascii') - unquoted = urllib_unquote(uri) - return unquoted.decode('utf-8') - - # Old-style of re-raising an exception is SyntaxError in Python 3, - # so hide behind exec() so the Python 3 parser doesn't see it - exec('''def reraise(exc_type, exc_value, exc_traceback): - """Re-raise an exception given information from sys.exc_info() - - Note that unlike six.reraise, this does not support replacing the - traceback. All arguments must come from a single sys.exc_info() call. - """ - raise exc_type, exc_value, exc_traceback - ''') - -else: - from collections import UserDict - IterableUserDict = UserDict - from urllib.parse import quote, quote_plus, unquote, urlparse - from urllib.request import urlopen - - def reraise(exc_type, exc_value, exc_traceback): - """Re-raise an exception given information from sys.exc_info() - - Note that unlike six.reraise, this does not support replacing the - traceback. All arguments must come from a single sys.exc_info() call. - """ - # In Python 3, all exception info is contained in one object. - raise exc_value - -try: - from shutil import which -except ImportError: - # shutil.which() from Python 3.6 - # "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, - # 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; - # All Rights Reserved" - def which(cmd, mode=os.F_OK | os.X_OK, path=None): - """Given a command, mode, and a PATH string, return the path which - conforms to the given mode on the PATH, or None if there is no such - file. - - `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result - of os.environ.get("PATH"), or can be overridden with a custom search - path. - - """ - # Check that a given file can be accessed with the correct mode. - # Additionally check that `file` is not a directory, as on Windows - # directories pass the os.access check. - def _access_check(fn, mode): - return (os.path.exists(fn) and os.access(fn, mode) - and not os.path.isdir(fn)) - - # If we're given a path with a directory part, look it up directly rather - # than referring to PATH directories. This includes checking relative to the - # current directory, e.g. ./script - if os.path.dirname(cmd): - if _access_check(cmd, mode): - return cmd - return None - - if path is None: - path = os.environ.get("PATH", os.defpath) - if not path: - return None - path = path.split(os.pathsep) - - if sys.platform == "win32": - # The current directory takes precedence on Windows. - if not os.curdir in path: - path.insert(0, os.curdir) - - # PATHEXT is necessary to check on Windows. - pathext = os.environ.get("PATHEXT", "").split(os.pathsep) - # See if the given file matches any of the expected path extensions. - # This will allow us to short circuit when given "python.exe". - # If it does match, only test that one, otherwise we have to try - # others. - if any(cmd.lower().endswith(ext.lower()) for ext in pathext): - files = [cmd] - else: - files = [cmd + ext for ext in pathext] - else: - # On other platforms you don't have things like PATHEXT to tell you - # what file suffixes are executable, so just pass on cmd as-is. - files = [cmd] - - seen = set() - for dir in path: - normdir = os.path.normcase(dir) - if not normdir in seen: - seen.add(normdir) - for thefile in files: - name = os.path.join(dir, thefile) - if _access_check(name, mode): - return name - return None +import warnings + +warnings.warn( + "The ldap.compat module is deprecated and will be removed in the future", + DeprecationWarning, +) + +from collections import UserDict +IterableUserDict = UserDict +from urllib.parse import quote, quote_plus, unquote, urlparse +from urllib.request import urlopen +from collections.abc import MutableMapping +from shutil import which + +def reraise(exc_type, exc_value, exc_traceback): + """Re-raise an exception given information from sys.exc_info() + + Note that unlike six.reraise, this does not support replacing the + traceback. All arguments must come from a single sys.exc_info() call. + """ + # In Python 3, all exception info is contained in one object. + raise exc_value diff --git a/Lib/ldap/constants.py b/Lib/ldap/constants.py index 7a7982bb..0e7df6e7 100644 --- a/Lib/ldap/constants.py +++ b/Lib/ldap/constants.py @@ -13,16 +13,15 @@ # This module cannot import anything from ldap. # When building documentation, it is used to initialize ldap.__init__. -from __future__ import print_function -class Constant(object): +class Constant: """Base class for a definition of an OpenLDAP constant """ def __init__(self, name, optional=False, requirements=(), doc=None): self.name = name if optional: - self_requirement = 'defined(LDAP_{})'.format(self.name) + self_requirement = f'defined(LDAP_{self.name})' requirements = list(requirements) + [self_requirement] self.requirements = requirements self.doc = self.__doc__ = doc @@ -50,7 +49,7 @@ class TLSInt(Int): def __init__(self, *args, **kwargs): requrements = list(kwargs.get('requirements', ())) kwargs['requirements'] = ['HAVE_TLS'] + requrements - super(TLSInt, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) class Feature(Constant): @@ -70,7 +69,7 @@ class Feature(Constant): def __init__(self, name, c_feature, **kwargs): - super(Feature, self).__init__(name, **kwargs) + super().__init__(name, **kwargs) self.c_feature = c_feature @@ -245,6 +244,7 @@ class Str(Constant): Int('OPT_SIZELIMIT'), Int('OPT_TIMELIMIT'), Int('OPT_REFERRALS', optional=True), + Int('OPT_RESULT_CODE'), Int('OPT_ERROR_NUMBER'), Int('OPT_RESTART'), Int('OPT_PROTOCOL_VERSION'), @@ -262,6 +262,7 @@ class Str(Constant): Int('OPT_TIMEOUT'), Int('OPT_REFHOPLIMIT'), Int('OPT_NETWORK_TIMEOUT'), + Int('OPT_TCP_USER_TIMEOUT', optional=True), Int('OPT_URI'), Int('OPT_DEFBASE', optional=True), @@ -281,7 +282,6 @@ class Str(Constant): TLSInt('OPT_X_TLS_DEMAND'), TLSInt('OPT_X_TLS_ALLOW'), TLSInt('OPT_X_TLS_TRY'), - TLSInt('OPT_X_TLS_PEERCERT', optional=True), TLSInt('OPT_X_TLS_VERSION', optional=True), TLSInt('OPT_X_TLS_CIPHER', optional=True), @@ -300,6 +300,20 @@ class Str(Constant): TLSInt('OPT_X_TLS_PROTOCOL_MIN', optional=True), TLSInt('OPT_X_TLS_PACKAGE', optional=True), + # Added in OpenLDAP 2.4.52 + TLSInt('OPT_X_TLS_ECNAME', optional=True), + TLSInt('OPT_X_TLS_REQUIRE_SAN', optional=True), + + # Added in OpenLDAP 2.5 + TLSInt('OPT_X_TLS_PEERCERT', optional=True), + TLSInt('OPT_X_TLS_PROTOCOL_MAX', optional=True), + + TLSInt('OPT_X_TLS_PROTOCOL_SSL3', optional=True), + TLSInt('OPT_X_TLS_PROTOCOL_TLS1_0', optional=True), + TLSInt('OPT_X_TLS_PROTOCOL_TLS1_1', optional=True), + TLSInt('OPT_X_TLS_PROTOCOL_TLS1_2', optional=True), + TLSInt('OPT_X_TLS_PROTOCOL_TLS1_3', optional=True), + Int('OPT_X_SASL_MECH'), Int('OPT_X_SASL_REALM'), Int('OPT_X_SASL_AUTHCID'), @@ -340,11 +354,10 @@ class Str(Constant): # XXX - these should be errors Int('URL_ERR_BADSCOPE'), Int('URL_ERR_MEM'), - # Int('LIBLDAP_R'), - Feature('LIBLDAP_R', 'HAVE_LIBLDAP_R'), Feature('SASL_AVAIL', 'HAVE_SASL'), Feature('TLS_AVAIL', 'HAVE_TLS'), + Feature('INIT_FD_AVAIL', 'HAVE_LDAP_INIT_FD'), Str("CONTROL_MANAGEDSAIT"), Str("CONTROL_PROXY_AUTHZ"), @@ -392,7 +405,7 @@ def pop_requirement(): if requirement not in current_requirements: current_requirements.append(requirement) print() - print('#if {}'.format(requirement)) + print(f'#if {requirement}') print(definition.c_template.format(self=definition)) diff --git a/Lib/ldap/controls/__init__.py b/Lib/ldap/controls/__init__.py index 811b3be1..73557168 100644 --- a/Lib/ldap/controls/__init__.py +++ b/Lib/ldap/controls/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ controls.py - support classes for LDAP controls @@ -13,7 +12,7 @@ import _ldap assert _ldap.__version__==__version__, \ - ImportError('ldap %s and _ldap %s version mismatch!' % (__version__,_ldap.__version__)) + ImportError(f'ldap {__version__} and _ldap {_ldap.__version__} version mismatch!') import ldap diff --git a/Lib/ldap/controls/deref.py b/Lib/ldap/controls/deref.py index b9994eb6..e5b2a7ec 100644 --- a/Lib/ldap/controls/deref.py +++ b/Lib/ldap/controls/deref.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.deref - classes for (see https://tools.ietf.org/html/draft-masarati-ldap-deref) diff --git a/Lib/ldap/controls/libldap.py b/Lib/ldap/controls/libldap.py index f6ea42c4..9a102379 100644 --- a/Lib/ldap/controls/libldap.py +++ b/Lib/ldap/controls/libldap.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ controls.libldap - LDAP controls wrapper classes with en-/decoding done by OpenLDAP functions @@ -10,7 +9,7 @@ import _ldap assert _ldap.__version__==__version__, \ - ImportError('ldap %s and _ldap %s version mismatch!' % (__version__,_ldap.__version__)) + ImportError(f'ldap {__version__} and _ldap {_ldap.__version__} version mismatch!') import ldap diff --git a/Lib/ldap/controls/openldap.py b/Lib/ldap/controls/openldap.py index 5da2dd3f..26c76868 100644 --- a/Lib/ldap/controls/openldap.py +++ b/Lib/ldap/controls/openldap.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.openldap - classes for OpenLDAP-specific controls @@ -52,6 +51,7 @@ class SearchNoOpMixIn: """ def noop_search_st(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass=*)',timeout=-1): + msg_id = None try: msg_id = self.search_ext( base, @@ -67,9 +67,10 @@ def noop_search_st(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass=*) ldap.TIMELIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED, ldap.ADMINLIMIT_EXCEEDED - ) as e: - self.abandon(msg_id) - raise e + ): + if msg_id is not None: + self.abandon(msg_id) + raise else: noop_srch_ctrl = [ c diff --git a/Lib/ldap/controls/pagedresults.py b/Lib/ldap/controls/pagedresults.py index efdd0408..12ca573d 100644 --- a/Lib/ldap/controls/pagedresults.py +++ b/Lib/ldap/controls/pagedresults.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.paged - classes for Simple Paged control (see RFC 2696) diff --git a/Lib/ldap/controls/ppolicy.py b/Lib/ldap/controls/ppolicy.py index 67efe3ad..f3a8416d 100644 --- a/Lib/ldap/controls/ppolicy.py +++ b/Lib/ldap/controls/ppolicy.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.ppolicy - classes for Password Policy controls (see https://tools.ietf.org/html/draft-behera-ldap-password-policy) @@ -41,9 +40,10 @@ class PasswordPolicyError(univ.Enumerated): ('insufficientPasswordQuality',5), ('passwordTooShort',6), ('passwordTooYoung',7), - ('passwordInHistory',8) + ('passwordInHistory',8), + ('passwordTooLong',9), ) - subtypeSpec = univ.Enumerated.subtypeSpec + constraint.SingleValueConstraint(0,1,2,3,4,5,6,7,8) + subtypeSpec = univ.Enumerated.subtypeSpec + constraint.SingleValueConstraint(0,1,2,3,4,5,6,7,8,9) class PasswordPolicyResponseValue(univ.Sequence): @@ -63,17 +63,31 @@ class PasswordPolicyResponseValue(univ.Sequence): class PasswordPolicyControl(ValueLessRequestControl,ResponseControl): + """ + Indicates the errors and warnings about the password policy. + + Attributes + ---------- + + timeBeforeExpiration : int + The time before the password expires. + + graceAuthNsRemaining : int + The number of grace authentications remaining. + + error: int + The password and authentication errors. + """ controlType = '1.3.6.1.4.1.42.2.27.8.5.1' def __init__(self,criticality=False): self.criticality = criticality - - def decodeControlValue(self,encodedControlValue): - ppolicyValue,_ = decoder.decode(encodedControlValue,asn1Spec=PasswordPolicyResponseValue()) self.timeBeforeExpiration = None self.graceAuthNsRemaining = None self.error = None + def decodeControlValue(self,encodedControlValue): + ppolicyValue,_ = decoder.decode(encodedControlValue,asn1Spec=PasswordPolicyResponseValue()) warning = ppolicyValue.getComponentByName('warning') if warning.hasValue(): if 'timeBeforeExpiration' in warning: diff --git a/Lib/ldap/controls/psearch.py b/Lib/ldap/controls/psearch.py index 002a88e9..32900c8b 100644 --- a/Lib/ldap/controls/psearch.py +++ b/Lib/ldap/controls/psearch.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.psearch - classes for Persistent Search Control (see https://tools.ietf.org/html/draft-ietf-ldapext-psearch) diff --git a/Lib/ldap/controls/pwdpolicy.py b/Lib/ldap/controls/pwdpolicy.py index cf9c1978..54f1a700 100644 --- a/Lib/ldap/controls/pwdpolicy.py +++ b/Lib/ldap/controls/pwdpolicy.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.pwdpolicy - classes for Password Policy controls (see https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy) diff --git a/Lib/ldap/controls/readentry.py b/Lib/ldap/controls/readentry.py index 57cefef4..7b2a7e89 100644 --- a/Lib/ldap/controls/readentry.py +++ b/Lib/ldap/controls/readentry.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.readentry - classes for the Read Entry controls (see RFC 4527) @@ -43,7 +42,7 @@ def decodeControlValue(self,encodedControlValue): self.dn = str(decodedEntry[0]) self.entry = {} for attr in decodedEntry[1]: - self.entry[str(attr[0])] = [ str(attr_value) for attr_value in attr[1] ] + self.entry[str(attr[0])] = [ bytes(attr_value) for attr_value in attr[1] ] class PreReadControl(ReadEntryControl): diff --git a/Lib/ldap/controls/sessiontrack.py b/Lib/ldap/controls/sessiontrack.py index 9c8a057f..a1fb8b34 100644 --- a/Lib/ldap/controls/sessiontrack.py +++ b/Lib/ldap/controls/sessiontrack.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.sessiontrack - class for session tracking control (see draft-wahl-ldap-session) diff --git a/Lib/ldap/controls/simple.py b/Lib/ldap/controls/simple.py index d4130348..96837e2a 100644 --- a/Lib/ldap/controls/simple.py +++ b/Lib/ldap/controls/simple.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.simple - classes for some very simple LDAP controls @@ -8,6 +7,9 @@ import struct,ldap from ldap.controls import RequestControl,ResponseControl,LDAPControl,KNOWN_RESPONSE_CONTROLS +from pyasn1.type import univ +from pyasn1.codec.ber import encoder,decoder + class ValueLessRequestControl(RequestControl): """ @@ -58,8 +60,6 @@ class BooleanControl(LDAPControl): booleanValue Boolean (True/False or 1/0) which is the boolean controlValue. """ - boolean2ber = { 1:'\x01\x01\xFF', 0:'\x01\x01\x00' } - ber2boolean = { '\x01\x01\xFF':1, '\x01\x01\x00':0 } def __init__(self,controlType=None,criticality=False,booleanValue=False): self.controlType = controlType @@ -67,10 +67,11 @@ def __init__(self,controlType=None,criticality=False,booleanValue=False): self.booleanValue = booleanValue def encodeControlValue(self): - return self.boolean2ber[int(self.booleanValue)] + return encoder.encode(self.booleanValue,asn1Spec=univ.Boolean()) def decodeControlValue(self,encodedControlValue): - self.booleanValue = self.ber2boolean[encodedControlValue] + decodedValue,_ = decoder.decode(encodedControlValue,asn1Spec=univ.Boolean()) + self.booleanValue = bool(int(decodedValue)) class ManageDSAITControl(ValueLessRequestControl): diff --git a/Lib/ldap/controls/sss.py b/Lib/ldap/controls/sss.py index a5312d2f..e6ee3686 100644 --- a/Lib/ldap/controls/sss.py +++ b/Lib/ldap/controls/sss.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.sss - classes for Server Side Sorting (see RFC 2891) @@ -12,6 +11,8 @@ ] +import sys + import ldap from ldap.ldapobject import LDAPObject from ldap.controls import (RequestControl, ResponseControl, @@ -57,7 +58,7 @@ def __init__( ): RequestControl.__init__(self,self.controlType,criticality) self.ordering_rules = ordering_rules - if isinstance(ordering_rules, basestring): + if isinstance(ordering_rules, str): ordering_rules = [ordering_rules] for rule in ordering_rules: rule = rule.split(':') diff --git a/Lib/ldap/controls/vlv.py b/Lib/ldap/controls/vlv.py index 9fea2f03..5fc7ce88 100644 --- a/Lib/ldap/controls/vlv.py +++ b/Lib/ldap/controls/vlv.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.vlv - classes for Virtual List View (see draft-ietf-ldapext-ldapv3-vlv) diff --git a/Lib/ldap/dn.py b/Lib/ldap/dn.py index 00c7b064..64d7d0e9 100644 --- a/Lib/ldap/dn.py +++ b/Lib/ldap/dn.py @@ -3,13 +3,11 @@ See https://www.python-ldap.org/ for details. """ - -import sys from ldap.pkginfo import __version__ import _ldap assert _ldap.__version__==__version__, \ - ImportError('ldap %s and _ldap %s version mismatch!' % (__version__,_ldap.__version__)) + ImportError(f'ldap {__version__} and _ldap {_ldap.__version__} version mismatch!') import ldap.functions @@ -28,11 +26,12 @@ def escape_dn_chars(s): s = s.replace('>' ,'\\>') s = s.replace(';' ,'\\;') s = s.replace('=' ,'\\=') - s = s.replace('\000' ,'\\\000') - if s[0]=='#' or s[0]==' ': - s = ''.join(('\\',s)) + # RFC 4514 requires NULL (U+0000) to be escaped as hex pair "\00" + s = s.replace('\x00' ,'\\00') if s[-1]==' ': s = ''.join((s[:-1],'\\ ')) + if s[0]=='#' or s[0]==' ': + s = ''.join(('\\',s)) return s @@ -47,17 +46,20 @@ def str2dn(dn,flags=0): """ if not dn: return [] - if sys.version_info[0] < 3 and isinstance(dn, unicode): - dn = dn.encode('utf-8') return ldap.functions._ldap_function_call(None,_ldap.str2dn,dn,flags) -def dn2str(dn): +def dn2str(dn, flags=0): """ This function takes a decomposed DN as parameter and returns - a single string. It's the inverse to str2dn() but will always - return a DN in LDAPv3 format compliant to RFC 4514. + a single string. It's the inverse to str2dn() but will by default always + return a DN in LDAPv3 format compliant to RFC 4514 if not otherwise specified + via flags. + + See also the OpenLDAP man-page ldap_dn2str(3) """ + if flags: + return ldap.functions._ldap_function_call(None, _ldap.dn2str, dn, flags) return ','.join([ '+'.join([ '='.join((atype,escape_dn_chars(avalue or ''))) @@ -65,6 +67,7 @@ def dn2str(dn): for rdn in dn ]) + def explode_dn(dn, notypes=False, flags=0): """ explode_dn(dn [, notypes=False [, flags=0]]) -> list @@ -111,7 +114,7 @@ def explode_rdn(rdn, notypes=False, flags=0): def is_dn(s,flags=0): """ - Returns True is `s' can be parsed by ldap.dn.str2dn() like as a + Returns True if `s' can be parsed by ldap.dn.str2dn() as a distinguished host_name (DN), otherwise False is returned. """ try: @@ -120,3 +123,8 @@ def is_dn(s,flags=0): return False else: return True + + +def normalize(s, flags=0): + """Returns a normalized distinguished name (DN)""" + return dn2str(str2dn(s, flags), flags) diff --git a/Lib/ldap/extop/__init__.py b/Lib/ldap/extop/__init__.py index 874166d9..dc9aea2f 100644 --- a/Lib/ldap/extop/__init__.py +++ b/Lib/ldap/extop/__init__.py @@ -28,7 +28,7 @@ def __init__(self,requestName,requestValue): self.requestValue = requestValue def __repr__(self): - return '%s(%s,%s)' % (self.__class__.__name__,self.requestName,self.requestValue) + return f'{self.__class__.__name__}({self.requestName},{self.requestValue})' def encodedRequestValue(self): """ @@ -53,7 +53,7 @@ def __init__(self,responseName,encodedResponseValue): self.responseValue = self.decodeResponseValue(encodedResponseValue) def __repr__(self): - return '%s(%s,%s)' % (self.__class__.__name__,self.responseName,self.responseValue) + return f'{self.__class__.__name__}({self.responseName},{self.responseValue})' def decodeResponseValue(self,value): """ @@ -65,3 +65,4 @@ def decodeResponseValue(self,value): # Import sub-modules from ldap.extop.dds import * +from ldap.extop.passwd import PasswordModifyResponse diff --git a/Lib/ldap/extop/dds.py b/Lib/ldap/extop/dds.py index 4d156e83..a970d71d 100644 --- a/Lib/ldap/extop/dds.py +++ b/Lib/ldap/extop/dds.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.extop.dds - Classes for Dynamic Entries extended operations (see RFC 2589) @@ -36,6 +35,7 @@ class RefreshRequestValue(univ.Sequence): ) def __init__(self,requestName=None,entryName=None,requestTtl=None): + super().__init__(requestName or self.requestName, b'') self.entryName = entryName self.requestTtl = requestTtl or self.defaultRequestTtl diff --git a/Lib/ldap/extop/passwd.py b/Lib/ldap/extop/passwd.py new file mode 100644 index 00000000..13e9f252 --- /dev/null +++ b/Lib/ldap/extop/passwd.py @@ -0,0 +1,32 @@ +""" +ldap.extop.passwd - Classes for Password Modify extended operation +(see RFC 3062) + +See https://www.python-ldap.org/ for details. +""" + +from ldap.extop import ExtendedResponse + +# Imports from pyasn1 +from pyasn1.type import namedtype, univ, tag +from pyasn1.codec.der import decoder + + +class PasswordModifyResponse(ExtendedResponse): + responseName = None + + class PasswordModifyResponseValue(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType( + 'genPasswd', + univ.OctetString().subtype( + implicitTag=tag.Tag(tag.tagClassContext, + tag.tagFormatSimple, 0) + ) + ) + ) + + def decodeResponseValue(self, value): + respValue, _ = decoder.decode(value, asn1Spec=self.PasswordModifyResponseValue()) + self.genPasswd = bytes(respValue.getComponentByName('genPasswd')) + return self.genPasswd diff --git a/Lib/ldap/filter.py b/Lib/ldap/filter.py index 3dba7f74..5bd41b21 100644 --- a/Lib/ldap/filter.py +++ b/Lib/ldap/filter.py @@ -24,6 +24,8 @@ def escape_filter_chars(assertion_value,escape_mode=0): If 1 all NON-ASCII chars are escaped. If 2 all chars are escaped. """ + if not isinstance(assertion_value, str): + raise TypeError("assertion_value must be of type str.") if escape_mode: r = [] if escape_mode==1: @@ -71,7 +73,7 @@ def time_span_filter( if from_timestamp < 0: from_timestamp = until_timestamp + from_timestamp if from_timestamp > until_timestamp: - raise ValueError('from_timestamp %r must not be greater than until_timestamp %r' % ( + raise ValueError('from_timestamp {!r} must not be greater than until_timestamp {!r}'.format( from_timestamp, until_timestamp )) return ( diff --git a/Lib/ldap/functions.py b/Lib/ldap/functions.py index 529c4c8f..8658db40 100644 --- a/Lib/ldap/functions.py +++ b/Lib/ldap/functions.py @@ -27,9 +27,6 @@ # Tracing is only supported in debugging mode import traceback -# See _raise_byteswarning in ldapobject.py -_LDAP_WARN_SKIP_FRAME = True - def _ldap_function_call(lock,func,*args,**kwargs): """ @@ -44,7 +41,7 @@ def _ldap_function_call(lock,func,*args,**kwargs): lock.acquire() if __debug__: if ldap._trace_level>=1: - ldap._trace_file.write('*** %s.%s %s\n' % ( + ldap._trace_file.write('*** {}.{} {}\n'.format( '_ldap',func.__name__, pprint.pformat((args,kwargs)) )) @@ -67,7 +64,7 @@ def _ldap_function_call(lock,func,*args,**kwargs): def initialize( uri, trace_level=0, trace_file=sys.stdout, trace_stack_limit=None, - bytes_mode=None, **kwargs + bytes_mode=None, fileno=None, **kwargs ): """ Return LDAPObject instance by opening LDAP connection to @@ -84,12 +81,17 @@ def initialize( Default is to use stdout. bytes_mode Whether to enable :ref:`bytes_mode` for backwards compatibility under Py2. + fileno + If not None the socket file descriptor is used to connect to an + LDAP server. Additional keyword arguments (such as ``bytes_strictness``) are passed to ``LDAPObject``. """ return LDAPObject( - uri, trace_level, trace_file, trace_stack_limit, bytes_mode, **kwargs) + uri, trace_level, trace_file, trace_stack_limit, bytes_mode, + fileno=fileno, **kwargs + ) def get_option(option): diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index e4e6841a..057fe71a 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -3,9 +3,6 @@ See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - from os import strerror from ldap.pkginfo import __version__, __author__, __license__ @@ -27,44 +24,20 @@ from ldap.schema import SCHEMA_ATTRS from ldap.controls import LDAPControl,DecodeControlTuples,RequestControlTuples -from ldap.extop import ExtendedRequest,ExtendedResponse -from ldap.compat import reraise +from ldap.extop import ExtendedRequest,ExtendedResponse,PasswordModifyResponse from ldap import LDAPError -PY2 = sys.version_info[0] <= 2 -if PY2: - text_type = unicode -else: - text_type = str - - -# See SimpleLDAPObject._bytesify_input -_LDAP_WARN_SKIP_FRAME = True class LDAPBytesWarning(BytesWarning): - """python-ldap bytes mode warning - """ - -def _raise_byteswarning(message): - """Raise LDAPBytesWarning - """ + """Python 2 bytes mode warning""" - # Call stacks that raise the warning tend to be complicated, so - # getting a useful stacklevel is tricky. - # We walk stack frames, ignoring functions in uninteresting files, - # based on the _LDAP_WARN_SKIP_FRAME marker in globals(). - stacklevel = 2 - try: - getframe = sys._getframe - except AttributeError: - pass - else: - frame = sys._getframe(stacklevel) - while frame and frame.f_globals.get('_LDAP_WARN_SKIP_FRAME'): - stacklevel += 1 - frame = frame.f_back - warnings.warn(message, LDAPBytesWarning, stacklevel=stacklevel+1) + def __init__(self, *args, **kwargs): + warnings.warn( + "LDAPBytesWarning is deprecated and will be removed in the future", + DeprecationWarning, + ) + super().__init__(*args, **kwargs) class NO_UNIQUE_ENTRY(ldap.NO_SUCH_OBJECT): @@ -94,198 +67,38 @@ class SimpleLDAPObject: } def __init__( - self,uri, + self,uri=None, trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None, - bytes_strictness=None, + bytes_strictness=None, fileno=None ): self._trace_level = trace_level or ldap._trace_level self._trace_file = trace_file or ldap._trace_file self._trace_stack_limit = trace_stack_limit self._uri = uri self._ldap_object_lock = self._ldap_lock('opcall') - self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri) - self.timeout = -1 - self.protocol_version = ldap.VERSION3 - - # Bytes mode - # ---------- - - if PY2: - if bytes_mode is None: - bytes_mode = True - if bytes_strictness is None: - _raise_byteswarning( - "Under Python 2, python-ldap uses bytes by default. " - "This will be removed in Python 3 (no bytes for " - "DN/RDN/field names). " - "Please call initialize(..., bytes_mode=False) explicitly.") - bytes_strictness = 'warn' - else: - if bytes_strictness is None: - bytes_strictness = 'error' - else: - if bytes_mode: - raise ValueError("bytes_mode is *not* supported under Python 3.") - bytes_mode = False - bytes_strictness = 'error' - self.bytes_mode = bytes_mode - self.bytes_strictness = bytes_strictness - - def _bytesify_input(self, arg_name, value): - """Adapt a value following bytes_mode in Python 2. - - In Python 3, returns the original value unmodified. - - With bytes_mode ON, takes bytes or None and returns bytes or None. - With bytes_mode OFF, takes unicode or None and returns bytes or None. - - For the wrong argument type (unicode or bytes, respectively), - behavior depends on the bytes_strictness setting. - In all cases, bytes or None are returned (or an exception is raised). - """ - if not PY2: - return value - if value is None: - return value - - elif self.bytes_mode: - if isinstance(value, bytes): - return value - elif self.bytes_strictness == 'silent': - pass - elif self.bytes_strictness == 'warn': - _raise_byteswarning( - "Received non-bytes value for '{}' in bytes mode; " - "please choose an explicit " - "option for bytes_mode on your LDAP connection".format(arg_name)) - else: - raise TypeError( - "All provided fields *must* be bytes when bytes mode is on; " - "got type '{}' for '{}'.".format(type(value).__name__, arg_name) - ) - return value.encode('utf-8') - else: - if isinstance(value, unicode): - return value.encode('utf-8') - elif self.bytes_strictness == 'silent': - pass - elif self.bytes_strictness == 'warn': - _raise_byteswarning( - "Received non-text value for '{}' with bytes_mode off and " - "bytes_strictness='warn'".format(arg_name)) - else: - raise TypeError( - "All provided fields *must* be text when bytes mode is off; " - "got type '{}' for '{}'.".format(type(value).__name__, arg_name) - ) - return value - - def _bytesify_modlist(self, arg_name, modlist, with_opcode): - """Adapt a modlist according to bytes_mode. - - A modlist is a tuple of (op, attr, value), where: - - With bytes_mode ON, attr is checked to be bytes - - With bytes_mode OFF, attr is converted from unicode to bytes - - value is *always* bytes - """ - if not PY2: - return modlist - if with_opcode: - return tuple( - (op, self._bytesify_input(arg_name, attr), val) - for op, attr, val in modlist + if fileno is not None: + if not hasattr(_ldap, "initialize_fd"): + raise ValueError("libldap does not support initialize_fd") + if hasattr(fileno, "fileno"): + fileno = fileno.fileno() + self._l = ldap.functions._ldap_function_call( + ldap._ldap_module_lock, _ldap.initialize_fd, fileno, uri ) else: - return tuple( - (self._bytesify_input(arg_name, attr), val) - for attr, val in modlist - ) - - def _unbytesify_text_value(self, value): - """Adapt a 'known text, UTF-8 encoded' returned value following bytes_mode. - - With bytes_mode ON, takes bytes or None and returns bytes or None. - With bytes_mode OFF, takes bytes or None and returns unicode or None. - - This function should only be applied on field *values*; distinguished names - or field *names* are already natively handled in result4. - """ - if value is None: - return value - - # Preserve logic of assertions only under Python 2 - if PY2: - assert isinstance(value, bytes), "Expected bytes value, got text instead (%r)" % (value,) - - if self.bytes_mode: - return value - else: - return value.decode('utf-8') - - def _maybe_rebytesify_text(self, value): - """Re-encodes text to bytes if needed by bytes_mode. + self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri) + self.timeout = -1 + self.protocol_version = ldap.VERSION3 - Takes unicode (and checks for it), and returns: - - bytes under bytes_mode - - unicode otherwise. - """ - if not PY2: - return value + if bytes_mode: + raise ValueError("bytes_mode is *not* supported under Python 3.") - if value is None: - return value + @property + def bytes_mode(self): + return False - assert isinstance(value, text_type), "Should return text, got bytes instead (%r)" % (value,) - if not self.bytes_mode: - return value - else: - return value.encode('utf-8') - - def _bytesify_result_value(self, result_value): - """Applies bytes_mode to a result value. - - Such a value can either be: - - a dict mapping an attribute name to its list of values - (where attribute names are unicode and values bytes) - - a list of referals (which are unicode) - """ - if not PY2: - return result_value - if hasattr(result_value, 'items'): - # It's a attribute_name: [values] dict - return { - self._maybe_rebytesify_text(key): value - for (key, value) in result_value.items() - } - elif isinstance(result_value, bytes): - return result_value - else: - # It's a list of referals - # Example value: - # [u'ldap://DomainDnsZones.xxxx.root.local/DC=DomainDnsZones,DC=xxxx,DC=root,DC=local'] - return [self._maybe_rebytesify_text(referal) for referal in result_value] - - def _bytesify_results(self, results, with_ctrls=False): - """Converts a "results" object according to bytes_mode. - - Takes: - - a list of (dn, {field: [values]}) if with_ctrls is False - - a list of (dn, {field: [values]}, ctrls) if with_ctrls is True - - And, if bytes_mode is on, converts dn and fields to bytes. - """ - if not PY2: - return results - if with_ctrls: - return [ - (self._maybe_rebytesify_text(dn), self._bytesify_result_value(fields), ctrls) - for (dn, fields, ctrls) in results - ] - else: - return [ - (self._maybe_rebytesify_text(dn), self._bytesify_result_value(fields)) - for (dn, fields) in results - ] + @property + def bytes_strictness(self): + return 'error' def _ldap_lock(self,desc=''): if ldap.LIBLDAP_R: @@ -301,7 +114,7 @@ def _ldap_call(self,func,*args,**kwargs): self._ldap_object_lock.acquire() if __debug__: if self._trace_level>=1: - self._trace_file.write('*** %s %s - %s\n%s\n' % ( + self._trace_file.write('*** {} {} - {}\n{}\n'.format( repr(self), self._uri, '.'.join((self.__class__.__name__,func.__name__)), @@ -319,18 +132,14 @@ def _ldap_call(self,func,*args,**kwargs): finally: self._ldap_object_lock.release() except LDAPError as e: - exc_type,exc_value,exc_traceback = sys.exc_info() try: if 'info' not in e.args[0] and 'errno' in e.args[0]: e.args[0]['info'] = strerror(e.args[0]['errno']) except IndexError: pass if __debug__ and self._trace_level>=2: - self._trace_file.write('=> LDAPError - %s: %s\n' % (e.__class__.__name__,str(e))) - try: - reraise(exc_type, exc_value, exc_traceback) - finally: - exc_type = exc_value = exc_traceback = None + self._trace_file.write('=> LDAPError - {}: {}\n'.format(e.__class__.__name__,str(e))) + raise else: if __debug__ and self._trace_level>=2: if not diagnostic_message_success is None: @@ -350,7 +159,7 @@ def __getattr__(self,name): elif name in self.__dict__: return self.__dict__[name] else: - raise AttributeError('%s has no attribute %s' % ( + raise AttributeError('{} has no attribute {}'.format( self.__class__.__name__,repr(name) )) @@ -362,6 +171,13 @@ def fileno(self): """ return self.get_option(ldap.OPT_DESC) + def connect(self): + """ + connect() -> None + Establishes LDAP connection if needed. + """ + return self._ldap_call(self._l.connect) + def abandon_ext(self,msgid,serverctrls=None,clientctrls=None): """ abandon_ext(msgid[,serverctrls=None[,clientctrls=None]]) -> None @@ -406,9 +222,6 @@ def add_ext(self,dn,modlist,serverctrls=None,clientctrls=None): The parameter modlist is similar to the one passed to modify(), except that no operation integer need be included in the tuples. """ - if PY2: - dn = self._bytesify_input('dn', dn) - modlist = self._bytesify_modlist('modlist', modlist, with_opcode=False) return self._ldap_call(self._l.add_ext,dn,modlist,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def add_ext_s(self,dn,modlist,serverctrls=None,clientctrls=None): @@ -433,9 +246,6 @@ def simple_bind(self,who=None,cred=None,serverctrls=None,clientctrls=None): """ simple_bind([who='' [,cred='']]) -> int """ - if PY2: - who = self._bytesify_input('who', who) - cred = self._bytesify_input('cred', cred) return self._ldap_call(self._l.simple_bind,who,cred,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def simple_bind_s(self,who=None,cred=None,serverctrls=None,clientctrls=None): @@ -512,9 +322,6 @@ def compare_ext(self,dn,attr,value,serverctrls=None,clientctrls=None): A design bug in the library prevents value from containing nul characters. """ - if PY2: - dn = self._bytesify_input('dn', dn) - attr = self._bytesify_input('attr', attr) return self._ldap_call(self._l.compare_ext,dn,attr,value,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def compare_ext_s(self,dn,attr,value,serverctrls=None,clientctrls=None): @@ -526,7 +333,7 @@ def compare_ext_s(self,dn,attr,value,serverctrls=None,clientctrls=None): except ldap.COMPARE_FALSE: return False raise ldap.PROTOCOL_ERROR( - 'Compare operation returned wrong result: %r' % (ldap_res) + f'Compare operation returned wrong result: {ldap_res!r}' ) def compare(self,dn,attr,value): @@ -545,7 +352,6 @@ def delete_ext(self,dn,serverctrls=None,clientctrls=None): form returns the message id of the initiated request, and the result can be obtained from a subsequent call to result(). """ - dn = self._bytesify_input('dn', dn) return self._ldap_call(self._l.delete_ext,dn,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def delete_ext_s(self,dn,serverctrls=None,clientctrls=None): @@ -585,7 +391,7 @@ def extop_s(self,extreq,serverctrls=None,clientctrls=None,extop_resp_class=None) if extop_resp_class: respoid,respvalue = res if extop_resp_class.responseName!=respoid: - raise ldap.PROTOCOL_ERROR("Wrong OID in extended response! Expected %s, got %s" % (extop_resp_class.responseName,respoid)) + raise ldap.PROTOCOL_ERROR(f"Wrong OID in extended response! Expected {extop_resp_class.responseName}, got {respoid}") return extop_resp_class(extop_resp_class.responseName,respvalue) else: return res @@ -594,9 +400,6 @@ def modify_ext(self,dn,modlist,serverctrls=None,clientctrls=None): """ modify_ext(dn, modlist[,serverctrls=None[,clientctrls=None]]) -> int """ - if PY2: - dn = self._bytesify_input('dn', dn) - modlist = self._bytesify_modlist('modlist', modlist, with_opcode=True) return self._ldap_call(self._l.modify_ext,dn,modlist,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def modify_ext_s(self,dn,modlist,serverctrls=None,clientctrls=None): @@ -650,15 +453,18 @@ def modrdn_s(self,dn,newrdn,delold=1): return self.rename_s(dn,newrdn,None,delold) def passwd(self,user,oldpw,newpw,serverctrls=None,clientctrls=None): - if PY2: - user = self._bytesify_input('user', user) - oldpw = self._bytesify_input('oldpw', oldpw) - newpw = self._bytesify_input('newpw', newpw) return self._ldap_call(self._l.passwd,user,oldpw,newpw,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) - def passwd_s(self,user,oldpw,newpw,serverctrls=None,clientctrls=None): - msgid = self.passwd(user,oldpw,newpw,serverctrls,clientctrls) - return self.extop_result(msgid,all=1,timeout=self.timeout) + def passwd_s(self, user, oldpw, newpw, serverctrls=None, clientctrls=None, extract_newpw=False): + msgid = self.passwd(user, oldpw, newpw, serverctrls, clientctrls) + respoid, respvalue = self.extop_result(msgid, all=1, timeout=self.timeout) + + if respoid != PasswordModifyResponse.responseName: + raise ldap.PROTOCOL_ERROR("Unexpected OID %s in extended response!" % respoid) + if extract_newpw and respvalue: + respvalue = PasswordModifyResponse(PasswordModifyResponse.responseName, respvalue) + + return respoid, respvalue def rename(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls=None): """ @@ -675,10 +481,6 @@ def rename(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls This actually corresponds to the rename* routines in the LDAP-EXT C API library. """ - if PY2: - dn = self._bytesify_input('dn', dn) - newrdn = self._bytesify_input('newrdn', newrdn) - newsuperior = self._bytesify_input('newsuperior', newsuperior) return self._ldap_call(self._l.rename,dn,newrdn,newsuperior,delold,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def rename_s(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls=None): @@ -726,7 +528,7 @@ def result(self,msgid=ldap.RES_ANY,all=1,timeout=None): The method returns a tuple of the form (result_type, result_data). The result_type is one of the constants RES_*. - See search() for a description of the search result's + See search_ext() for a description of the search result's result_data, otherwise the result_data is normally meaningless. The result() method will block for timeout seconds, or @@ -767,8 +569,6 @@ def result4(self,msgid=ldap.RES_ANY,all=1,timeout=None,add_ctrls=0,add_intermedi if add_ctrls: resp_data = [ (t,r,DecodeControlTuples(c,resp_ctrl_classes)) for t,r,c in resp_data ] decoded_resp_ctrls = DecodeControlTuples(resp_ctrls,resp_ctrl_classes) - if resp_data is not None: - resp_data = self._bytesify_results(resp_data, with_ctrls=add_ctrls) return resp_type, resp_data, resp_msgid, decoded_resp_ctrls, resp_name, resp_value def search_ext(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): @@ -795,7 +595,7 @@ def search_ext(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverct values are stored in a list as dictionary value. The DN in dn is extracted using the underlying ldap_get_dn(), - which may raise an exception of the DN is malformed. + which may raise an exception if the DN is malformed. If attrsonly is non-zero, the values of attrs will be meaningless (they are not transmitted in the result). @@ -816,24 +616,8 @@ def search_ext(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverct The amount of search results retrieved can be limited with the sizelimit parameter if non-zero. """ - - if PY2: - base = self._bytesify_input('base', base) - if filterstr is None: - # workaround for default argument, - # see https://github.com/python-ldap/python-ldap/issues/147 - if self.bytes_mode: - filterstr = b'(objectClass=*)' - else: - filterstr = u'(objectClass=*)' - else: - filterstr = self._bytesify_input('filterstr', filterstr) - if attrlist is not None: - attrlist = tuple(self._bytesify_input('attrlist', a) - for a in attrlist) - else: - if filterstr is None: - filterstr = '(objectClass=*)' + if filterstr is None: + filterstr = '(objectClass=*)' return self._ldap_call( self._l.search_ext, base,scope,filterstr, @@ -930,12 +714,8 @@ def search_subschemasubentry_s(self,dn=None): Returns: None or text/bytes depending on bytes_mode. """ - if self.bytes_mode: - empty_dn = b'' - attrname = b'subschemaSubentry' - else: - empty_dn = u'' - attrname = u'subschemaSubentry' + empty_dn = '' + attrname = 'subschemaSubentry' if dn is None: dn = empty_dn try: @@ -958,9 +738,8 @@ def search_subschemasubentry_s(self,dn=None): # If dn was already root DSE we can return here return None else: - # With legacy bytes mode, return bytes; otherwise, since this is a DN, - # RFCs impose that the field value *can* be decoded to UTF-8. - return self._unbytesify_text_value(search_subschemasubentry_dn) + if search_subschemasubentry_dn is not None: + return search_subschemasubentry_dn.decode('utf-8') except IndexError: return None @@ -988,14 +767,9 @@ def read_subschemasubentry_s(self,subschemasubentry_dn,attrs=None): """ Returns the sub schema sub entry's data """ - if self.bytes_mode: - filterstr = b'(objectClass=subschema)' - if attrs is None: - attrs = [attr.encode('utf-8') for attr in SCHEMA_ATTRS] - else: - filterstr = u'(objectClass=subschema)' - if attrs is None: - attrs = SCHEMA_ATTRS + filterstr = '(objectClass=subschema)' + if attrs is None: + attrs = SCHEMA_ATTRS try: subschemasubentry = self.read_s( subschemasubentry_dn, @@ -1030,12 +804,8 @@ def read_rootdse_s(self, filterstr=None, attrlist=None): """ convenience wrapper around read_s() for reading rootDSE """ - if self.bytes_mode: - base = b'' - attrlist = attrlist or [b'*', b'+'] - else: - base = u'' - attrlist = attrlist or [u'*', u'+'] + base = '' + attrlist = attrlist or ['*', '+'] ldap_rootdse = self.read_s( base, filterstr=filterstr, @@ -1048,10 +818,7 @@ def get_naming_contexts(self): returns all attribute values of namingContexts in rootDSE if namingContexts is not present (not readable) then empty list is returned """ - if self.bytes_mode: - name = b'namingContexts' - else: - name = u'namingContexts' + name = 'namingContexts' return self.read_rootdse_s( attrlist=[name] ).get(name, []) @@ -1060,8 +827,7 @@ def get_naming_contexts(self): class ReconnectLDAPObject(SimpleLDAPObject): """ :py:class:`SimpleLDAPObject` subclass whose synchronous request methods - automatically reconnect and re-try in case of server failure - (:exc:`ldap.SERVER_DOWN`). + automatically reconnect and re-try in case of server failure. The first arguments are same as for the :py:func:`~ldap.initialize()` function. @@ -1073,6 +839,10 @@ class ReconnectLDAPObject(SimpleLDAPObject): * retry_delay: specifies the time in seconds between reconnect attempts. This class also implements the pickle protocol. + + .. versionadded:: 3.4.5 + The exceptions :py:exc:`ldap.SERVER_DOWN`, :py:exc:`ldap.UNAVAILABLE`, :py:exc:`ldap.CONNECT_ERROR` and + :py:exc:`ldap.TIMEOUT` (configurable via :py:attr:`_reconnect_exceptions`) now trigger a reconnect. """ __transient_attrs__ = { @@ -1082,11 +852,12 @@ class ReconnectLDAPObject(SimpleLDAPObject): '_reconnect_lock', '_last_bind', } + _reconnect_exceptions = (ldap.SERVER_DOWN, ldap.UNAVAILABLE, ldap.CONNECT_ERROR, ldap.TIMEOUT) def __init__( - self,uri, + self,uri=None, trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None, - bytes_strictness=None, retry_max=1, retry_delay=60.0 + bytes_strictness=None, retry_max=1, retry_delay=60.0, fileno=None ): """ Parameters like SimpleLDAPObject.__init__() with these @@ -1102,7 +873,8 @@ def __init__( self._last_bind = None SimpleLDAPObject.__init__(self, uri, trace_level, trace_file, trace_stack_limit, bytes_mode, - bytes_strictness=bytes_strictness) + bytes_strictness=bytes_strictness, + fileno=fileno) self._reconnect_lock = ldap.LDAPLock(desc='reconnect lock within %s' % (repr(self))) self._retry_max = retry_max self._retry_delay = retry_delay @@ -1116,7 +888,10 @@ def __getstate__(self): for k,v in self.__dict__.items() if k not in self.__transient_attrs__ } - state['_last_bind'] = self._last_bind[0].__name__, self._last_bind[1], self._last_bind[2] + if self._last_bind is None: + state['_last_bind'] = None + else: + state['_last_bind'] = self._last_bind[0].__name__, self._last_bind[1], self._last_bind[2] return state def __setstate__(self,d): @@ -1127,15 +902,16 @@ def __setstate__(self,d): else: d.setdefault('bytes_strictness', 'warn') self.__dict__.update(d) - self._last_bind = getattr(SimpleLDAPObject, self._last_bind[0]), self._last_bind[1], self._last_bind[2] + if self._last_bind is not None: + self._last_bind = getattr(SimpleLDAPObject, self._last_bind[0]), self._last_bind[1], self._last_bind[2] self._ldap_object_lock = self._ldap_lock() self._reconnect_lock = ldap.LDAPLock(desc='reconnect lock within %s' % (repr(self))) # XXX cannot pickle file, use default trace file self._trace_file = ldap._trace_file - self.reconnect(self._uri) + self.reconnect(self._uri,force=True) - def _store_last_bind(self,method,*args,**kwargs): - self._last_bind = (method,args,kwargs) + def _store_last_bind(self,_method,*args,**kwargs): + self._last_bind = (_method,args,kwargs) def _apply_last_bind(self): if self._last_bind!=None: @@ -1153,30 +929,39 @@ def _restore_options(self): def passwd_s(self,*args,**kwargs): return self._apply_method_s(SimpleLDAPObject.passwd_s,*args,**kwargs) - def reconnect(self,uri,retry_max=1,retry_delay=60.0): + def reconnect(self,uri,retry_max=1,retry_delay=60.0,force=True): # Drop and clean up old connection completely # Reconnect self._reconnect_lock.acquire() try: + if hasattr(self,'_l'): + if force: + SimpleLDAPObject.unbind_s(self) + else: + return reconnect_counter = retry_max while reconnect_counter: counter_text = '%d. (of %d)' % (retry_max-reconnect_counter+1,retry_max) if __debug__ and self._trace_level>=1: - self._trace_file.write('*** Trying %s reconnect to %s...\n' % ( + self._trace_file.write('*** Trying {} reconnect to {}...\n'.format( counter_text,uri )) try: - # Do the connect - self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri) - self._restore_options() - # StartTLS extended operation in case this was called before - if self._start_tls: - SimpleLDAPObject.start_tls_s(self) - # Repeat last simple or SASL bind - self._apply_last_bind() + try: + # Do the connect + self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri) + self._restore_options() + # StartTLS extended operation in case this was called before + if self._start_tls: + SimpleLDAPObject.start_tls_s(self) + # Repeat last simple or SASL bind + self._apply_last_bind() + except ldap.LDAPError: + SimpleLDAPObject.unbind_s(self) + raise except (ldap.SERVER_DOWN,ldap.TIMEOUT): if __debug__ and self._trace_level>=1: - self._trace_file.write('*** %s reconnect to %s failed\n' % ( + self._trace_file.write('*** {} reconnect to {} failed\n'.format( counter_text,uri )) reconnect_counter = reconnect_counter-1 @@ -1185,10 +970,9 @@ def reconnect(self,uri,retry_max=1,retry_delay=60.0): if __debug__ and self._trace_level>=1: self._trace_file.write('=> delay %s...\n' % (retry_delay)) time.sleep(retry_delay) - SimpleLDAPObject.unbind_s(self) else: if __debug__ and self._trace_level>=1: - self._trace_file.write('*** %s reconnect to %s successful => repeat last operation\n' % ( + self._trace_file.write('*** {} reconnect to {} successful => repeat last operation\n'.format( counter_text,uri )) self._reconnects_done = self._reconnects_done + 1 @@ -1198,14 +982,12 @@ def reconnect(self,uri,retry_max=1,retry_delay=60.0): return # reconnect() def _apply_method_s(self,func,*args,**kwargs): - if not hasattr(self,'_l'): - self.reconnect(self._uri,retry_max=self._retry_max,retry_delay=self._retry_delay) + self.reconnect(self._uri,retry_max=self._retry_max,retry_delay=self._retry_delay,force=False) try: return func(self,*args,**kwargs) - except ldap.SERVER_DOWN: - SimpleLDAPObject.unbind_s(self) + except self._reconnect_exceptions: # Try to reconnect - self.reconnect(self._uri,retry_max=self._retry_max,retry_delay=self._retry_delay) + self.reconnect(self._uri,retry_max=self._retry_max,retry_delay=self._retry_delay,force=True) # Re-try last operation return func(self,*args,**kwargs) diff --git a/Lib/ldap/logger.py b/Lib/ldap/logger.py index 4db961e3..ae66bd08 100644 --- a/Lib/ldap/logger.py +++ b/Lib/ldap/logger.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- """ Helper class for using logging as trace file object """ import logging -class logging_file_class(object): +class logging_file_class: def __init__(self, logging_level): self._logging_level = logging_level diff --git a/Lib/ldap/modlist.py b/Lib/ldap/modlist.py index 4acf4e9f..bf4e4819 100644 --- a/Lib/ldap/modlist.py +++ b/Lib/ldap/modlist.py @@ -50,7 +50,7 @@ def modifyModlist( case_ignore_attr_types = {v.lower() for v in case_ignore_attr_types or []} modlist = [] attrtype_lower_map = {} - for a in old_entry.keys(): + for a in old_entry: attrtype_lower_map[a.lower()]=a for attrtype, value in new_entry.items(): attrtype_lower = attrtype.lower() diff --git a/Lib/ldap/pkginfo.py b/Lib/ldap/pkginfo.py index d004c5d8..2ac6852d 100644 --- a/Lib/ldap/pkginfo.py +++ b/Lib/ldap/pkginfo.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- """ meta attributes for packaging which does not import any dependencies """ -__version__ = '3.1.0' -__author__ = u'python-ldap project' +__version__ = '3.4.5' +__author__ = 'python-ldap project' __license__ = 'Python style' diff --git a/Lib/ldap/schema/models.py b/Lib/ldap/schema/models.py index 7520b2b1..3d9322c0 100644 --- a/Lib/ldap/schema/models.py +++ b/Lib/ldap/schema/models.py @@ -7,7 +7,7 @@ import sys import ldap.cidict -from ldap.compat import IterableUserDict +from collections import UserDict from ldap.schema.tokenizer import split_tokens,extract_tokens @@ -47,7 +47,7 @@ class SchemaElement: } def __init__(self,schema_element_str=None): - if sys.version_info >= (3, 0) and isinstance(schema_element_str, bytes): + if isinstance(schema_element_str, bytes): schema_element_str = schema_element_str.decode('utf-8') if schema_element_str: l = split_tokens(schema_element_str) @@ -69,9 +69,9 @@ def key_attr(self,key,value,quoted=0): assert value is None or type(value)==str,TypeError("value has to be of str, was %r" % value) if value: if quoted: - return " %s '%s'" % (key,value.replace("'","\\'")) + return " {} '{}'".format(key,value.replace("'","\\'")) else: - return " %s %s" % (key,value) + return f" {key} {value}" else: return "" @@ -84,9 +84,9 @@ def key_list(self,key,values,sep=' ',quoted=0): else: quoted_values = values if len(values)==1: - return ' %s %s' % (key,quoted_values[0]) + return ' {} {}'.format(key,quoted_values[0]) else: - return ' %s ( %s )' % (key,sep.join(quoted_values)) + return ' {} ( {} )'.format(key,sep.join(quoted_values)) def __str__(self): result = [str(self.oid)] @@ -133,7 +133,7 @@ class ObjectClass(SchemaElement): implementations to indicate the source of the associated schema element """ - schema_attribute = u'objectClasses' + schema_attribute = 'objectClasses' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -240,7 +240,7 @@ class AttributeType(SchemaElement): implementations to indicate the source of the associated schema element """ - schema_attribute = u'attributeTypes' + schema_attribute = 'attributeTypes' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -334,7 +334,7 @@ class LDAPSyntax(SchemaElement): Integer flag (0 or 1) indicating whether the attribute type is marked as not human-readable (X-NOT-HUMAN-READABLE) """ - schema_attribute = u'ldapSyntaxes' + schema_attribute = 'ldapSyntaxes' token_defaults = { 'DESC':(None,), 'X-NOT-HUMAN-READABLE':(None,), @@ -383,7 +383,7 @@ class MatchingRule(SchemaElement): OID of the LDAP syntax this matching rule is usable with (string, or None if missing) """ - schema_attribute = u'matchingRules' + schema_attribute = 'matchingRules' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -429,7 +429,7 @@ class MatchingRuleUse(SchemaElement): NAMEs or OIDs of attribute types for which this matching rule is used (tuple of strings) """ - schema_attribute = u'matchingRuleUse' + schema_attribute = 'matchingRuleUse' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -489,7 +489,7 @@ class DITContentRule(SchemaElement): NAMEs or OIDs of attributes which may not be present in an entry of the object class. (tuple of strings) """ - schema_attribute = u'dITContentRules' + schema_attribute = 'dITContentRules' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -547,7 +547,7 @@ class DITStructureRule(SchemaElement): NAMEs or OIDs of allowed structural object classes of superior entries in the DIT (tuple of strings) """ - schema_attribute = u'dITStructureRules' + schema_attribute = 'dITStructureRules' token_defaults = { 'NAME':(()), @@ -610,7 +610,7 @@ class NameForm(SchemaElement): NAMEs or OIDs of additional attributes an RDN may contain (tuple of strings) """ - schema_attribute = u'nameForms' + schema_attribute = 'nameForms' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -640,7 +640,7 @@ def __str__(self): return '( %s )' % ''.join(result) -class Entry(IterableUserDict): +class Entry(UserDict): """ Schema-aware implementation of an LDAP entry class. @@ -653,7 +653,7 @@ def __init__(self,schema,dn,entry): self._attrtype2keytuple = {} self._s = schema self.dn = dn - IterableUserDict.IterableUserDict.__init__(self,{}) + super().__init__() self.update(entry) def _at2key(self,nameoroid): @@ -674,7 +674,7 @@ def _at2key(self,nameoroid): return t def update(self,dict): - for key, value in dict.values(): + for key, value in dict.items(): self[key] = value def __contains__(self,nameoroid): diff --git a/Lib/ldap/schema/subentry.py b/Lib/ldap/schema/subentry.py index 5ccbce05..3f73df71 100644 --- a/Lib/ldap/schema/subentry.py +++ b/Lib/ldap/schema/subentry.py @@ -5,10 +5,9 @@ """ import copy +from urllib.request import urlopen import ldap.cidict,ldap.schema - -from ldap.compat import urlopen from ldap.schema.models import * import ldapurl @@ -23,7 +22,7 @@ SCHEMA_CLASS_MAPPING[o.schema_attribute] = o SCHEMA_ATTR_MAPPING[o] = o.schema_attribute -SCHEMA_ATTRS = SCHEMA_CLASS_MAPPING.keys() +SCHEMA_ATTRS = list(SCHEMA_CLASS_MAPPING) class SubschemaError(ValueError): @@ -122,7 +121,7 @@ def __init__(self,sub_schema_sub_entry,check_uniqueness=1): self.sed[se_class][se_id] = se_instance if hasattr(se_instance,'names'): - for name in ldap.cidict.cidict({}.fromkeys(se_instance.names)).keys(): + for name in ldap.cidict.cidict({}.fromkeys(se_instance.names)): if check_uniqueness and name in self.name2oid[se_class]: self.non_unique_names[se_class][se_id] = None raise NameNotUnique(attr_value) @@ -130,7 +129,7 @@ def __init__(self,sub_schema_sub_entry,check_uniqueness=1): self.name2oid[se_class][name] = se_id # Turn dict into list maybe more handy for applications - self.non_unique_oids = self.non_unique_oids.keys() + self.non_unique_oids = list(self.non_unique_oids) return # subSchema.__init__() @@ -168,7 +167,7 @@ def listall(self,schema_element_class,schema_element_filters=None): except AttributeError: pass else: - result = avail_se.keys() + result = list(avail_se) return result @@ -192,7 +191,7 @@ def tree(self,schema_element_class,schema_element_filters=None): # This helps with falsely assigned OIDs. continue assert se_obj.__class__==schema_element_class, \ - "Schema element referenced by %s must be of class %s but was %s" % ( + "Schema element referenced by {} must be of class {} but was {}".format( se_oid,schema_element_class.__name__,se_obj.__class__ ) for s in se_obj.sup or ('_',): @@ -217,7 +216,7 @@ def getoid(self,se_class,nameoroid,raise_keyerror=0): result_oid = self.name2oid[se_class][nameoroid_stripped] except KeyError: if raise_keyerror: - raise KeyError('No registered %s-OID for nameoroid %s' % (se_class.__name__,repr(nameoroid_stripped))) + raise KeyError('No registered {}-OID for nameoroid {}'.format(se_class.__name__,repr(nameoroid_stripped))) else: result_oid = nameoroid_stripped return result_oid @@ -250,7 +249,7 @@ def get_obj(self,se_class,nameoroid,default=None,raise_keyerror=0): se_obj = self.sed[se_class][se_oid] except KeyError: if raise_keyerror: - raise KeyError('No ldap.schema.%s instance with nameoroid %s and se_oid %s' % ( + raise KeyError('No ldap.schema.{} instance with nameoroid {} and se_oid {}'.format( se_class.__name__,repr(nameoroid),repr(se_oid)) ) else: @@ -422,14 +421,14 @@ def attribute_types( # Remove all mandantory attribute types from # optional attribute type list - for a in list(r_may.keys()): + for a in list(r_may): if a in r_must: del r_may[a] # Apply attr_type_filter to results if attr_type_filter: for l in [r_must,r_may]: - for a in list(l.keys()): + for a in list(l): for afk,afv in attr_type_filter: try: schema_attr_type = self.sed[AttributeType][a] @@ -462,7 +461,7 @@ def urlfetch(uri,trace_level=0): l=ldap.initialize(ldap_url.initializeUrl(),trace_level) l.protocol_version = ldap.VERSION3 - l.simple_bind_s(ldap_url.who or u'', ldap_url.cred or u'') + l.simple_bind_s(ldap_url.who or '', ldap_url.cred or '') subschemasubentry_dn = l.search_subschemasubentry_s(ldap_url.dn) if subschemasubentry_dn is None: s_temp = None @@ -477,10 +476,10 @@ def urlfetch(uri,trace_level=0): l.unbind_s() del l else: - ldif_file = urlopen(uri) - ldif_parser = ldif.LDIFRecordList(ldif_file,max_entries=1) - ldif_parser.parse() - subschemasubentry_dn,s_temp = ldif_parser.all_records[0] + with urlopen(uri) as ldif_file: + ldif_parser = ldif.LDIFRecordList(ldif_file,max_entries=1) + ldif_parser.parse() + subschemasubentry_dn,s_temp = ldif_parser.all_records[0] # Work-around for mixed-cased attribute names subschemasubentry_entry = ldap.cidict.cidict() s_temp = s_temp or {} diff --git a/Lib/ldap/schema/tokenizer.py b/Lib/ldap/schema/tokenizer.py index 20958c09..623b86d5 100644 --- a/Lib/ldap/schema/tokenizer.py +++ b/Lib/ldap/schema/tokenizer.py @@ -13,12 +13,16 @@ r"|" # or r"([^'$()\s]+)" # string of length >= 1 without '$() or whitespace r"|" # or - r"('.*?'(?!\w))" # any string or empty string surrounded by single quotes - # except if right quote is succeeded by alphanumeric char + r"('(?:[^'\\]|\\.)*'(?!\w))" + # any string or empty string surrounded by unescaped + # single quotes except if right quote is succeeded by + # alphanumeric char r"|" # or r"([^\s]+?)", # residue, all non-whitespace strings ).findall +UNESCAPE_PATTERN = re.compile(r"\\(.)") + def split_tokens(s): """ @@ -30,7 +34,7 @@ def split_tokens(s): if unquoted: parts.append(unquoted) elif quoted: - parts.append(quoted[1:-1]) + parts.append(UNESCAPE_PATTERN.sub(r'\1', quoted[1:-1])) elif opar: parens += 1 parts.append(opar) diff --git a/Lib/ldap/syncrepl.py b/Lib/ldap/syncrepl.py index 0de5cec4..0e1b6a3f 100644 --- a/Lib/ldap/syncrepl.py +++ b/Lib/ldap/syncrepl.py @@ -1,10 +1,10 @@ -# -*- coding: utf-8 -*- """ ldap.syncrepl - for implementing syncrepl consumer (see RFC 4533) See https://www.python-ldap.org/ for project details. """ +from typing import AnyStr, Dict, List, Tuple, Union from uuid import UUID # Imports from pyasn1 @@ -13,8 +13,10 @@ from ldap.pkginfo import __version__, __author__, __license__ from ldap.controls import RequestControl, ResponseControl, KNOWN_RESPONSE_CONTROLS +from ldap import RES_SEARCH_RESULT, RES_SEARCH_ENTRY, RES_INTERMEDIATE __all__ = [ + 'OpenLDAPSyncreplCookie', 'SyncreplConsumer', ] @@ -314,34 +316,35 @@ def __init__(self, encodedMessage): self.refreshPresent = None self.syncIdSet = None - for attr in ['newcookie', 'refreshDelete', 'refreshPresent', 'syncIdSet']: - comp = d[0].getComponentByName(attr) + # Due to the way pyasn1 works, refreshDelete and refreshPresent are both + # valid in the components as they are fully populated defaults. We must + # get the component directly from the message, not by iteration. + attr = d[0].getName() + comp = d[0].getComponent() - if comp is not None and comp.hasValue(): - - if attr == 'newcookie': - self.newcookie = str(comp) - return + if comp is not None and comp.hasValue(): + if attr == 'newcookie': + self.newcookie = str(comp) + return - val = {} + val = {} - cookie = comp.getComponentByName('cookie') - if cookie.hasValue(): - val['cookie'] = str(cookie) + cookie = comp.getComponentByName('cookie') + if cookie.hasValue(): + val['cookie'] = str(cookie) - if attr.startswith('refresh'): - val['refreshDone'] = bool(comp.getComponentByName('refreshDone')) - elif attr == 'syncIdSet': - uuids = [] - ids = comp.getComponentByName('syncUUIDs') - for i in range(len(ids)): - uuid = UUID(bytes=bytes(ids.getComponentByPosition(i))) - uuids.append(str(uuid)) - val['syncUUIDs'] = uuids - val['refreshDeletes'] = bool(comp.getComponentByName('refreshDeletes')) + if attr.startswith('refresh'): + val['refreshDone'] = bool(comp.getComponentByName('refreshDone')) + elif attr == 'syncIdSet': + uuids = [] + ids = comp.getComponentByName('syncUUIDs') + for i in range(len(ids)): + uuid = UUID(bytes=bytes(ids.getComponentByPosition(i))) + uuids.append(str(uuid)) + val['syncUUIDs'] = uuids + val['refreshDeletes'] = bool(comp.getComponentByName('refreshDeletes')) - setattr(self, attr, val) - return + setattr(self, attr, val) class SyncreplConsumer: @@ -407,7 +410,7 @@ def syncrepl_poll(self, msgid=-1, timeout=None, all=0): all=0, ) - if type == 101: + if type == RES_SEARCH_RESULT: # search result. This marks the end of a refreshOnly session. # look for a SyncDone control, save the cookie, and if necessary # delete non-present entries. @@ -420,7 +423,7 @@ def syncrepl_poll(self, msgid=-1, timeout=None, all=0): return False - elif type == 100: + elif type == RES_SEARCH_ENTRY: # search entry with associated SyncState control for m in msg: dn, attrs, ctrls = m @@ -439,7 +442,7 @@ def syncrepl_poll(self, msgid=-1, timeout=None, all=0): self.syncrepl_set_cookie(c.cookie) break - elif type == 121: + elif type == RES_INTERMEDIATE: # Intermediate message. If it is a SyncInfoMessage, parse it for m in msg: rname, resp, ctrls = m @@ -534,3 +537,71 @@ def syncrepl_refreshdone(self): follows. """ pass + + +class OpenLDAPSyncreplCookie: + """ + OpenLDAPSyncreplCookie - allows a consumer to track a cookie across a + refreshAndPersist syncrepl session against a multi-provider OpenLDAP cluster + """ + + rid: int = 0 + sid: int = 0 + _csnset: Dict[int, str] + + def __init__(self, cookie: AnyStr = "") -> None: + self._csnset = {} + + if cookie: + self.update(cookie) + + def _parse_csn(self, csn: str) -> Tuple[str, str, str, str]: + time, order, sid, other = csn.split('#', 3) + return (time, order, sid, other) + + def _parse_cookie(self, cookie: AnyStr) -> Dict[str, Union[str, List[str]]]: + if isinstance(cookie, bytes): + cookie = cookie.decode() + + result = {} + parts = cookie.split(',') + for part in parts: + if part.startswith('rid='): + result['rid'] = part[4:] + elif part.startswith('sid='): + result['sid'] = part[4:] + elif part.startswith('csn='): + result['csn'] = part[4:].split(';') + elif part.startswith('delcsn='): + result['delcsn'] = part[7:] + else: + # Did not recognize this cookie part + pass + return result + + def update(self, cookie: AnyStr): + """ + Update the CSN set based on a cookie we just received, use in + syncrepl_set_cookie() to track the session state. + """ + components = self._parse_cookie(cookie) + for csn in components.get('csn', []): + _, _, sid, _ = self._parse_csn(csn) + if sid not in self._csnset or self._csnset[sid] < csn: + self._csnset[sid] = csn + + return self + + def unparse(self) -> str: + """ + Return the cookie as a string, use in syncrepl_get_cookie() or when + storing the state for later use. + """ + cookie = 'rid={:03},sid={:03x}'.format(self.rid or 0, self.sid or 0) + if self._csnset: + cookie += ',csn=' + cookie += ';'.join(csn for sid, csn in sorted(self._csnset.items())) + return cookie + + def __str__(self): + return self.unparse() diff --git a/Lib/ldapurl.py b/Lib/ldapurl.py index 7a25ecab..57900028 100644 --- a/Lib/ldapurl.py +++ b/Lib/ldapurl.py @@ -4,7 +4,7 @@ See https://www.python-ldap.org/ for details. """ -__version__ = '3.1.0' +__version__ = '3.4.5' __all__ = [ # constants @@ -16,7 +16,8 @@ 'LDAPUrlExtension','LDAPUrlExtensions','LDAPUrl' ] -from ldap.compat import UserDict, quote, unquote +from collections.abc import MutableMapping +from urllib.parse import quote, unquote LDAP_SCOPE_BASE = 0 LDAP_SCOPE_ONELEVEL = 1 @@ -47,21 +48,16 @@ def isLDAPUrl(s): + """Returns True if s is a LDAP URL, else False """ - Returns 1 if s is a LDAP URL, 0 else - """ - s_lower = s.lower() - return \ - s_lower.startswith('ldap://') or \ - s_lower.startswith('ldaps://') or \ - s_lower.startswith('ldapi://') + return s.lower().startswith(('ldap://', 'ldaps://', 'ldapi://')) def ldapUrlEscape(s): """Returns URL encoding of string s""" return quote(s).replace(',','%2C').replace('/','%2F') -class LDAPUrlExtension(object): +class LDAPUrlExtension: """ Class for parsing and unparsing LDAP URL extensions as described in RFC 4516. @@ -102,9 +98,9 @@ def _parse(self,extension): def unparse(self): if self.exvalue is None: - return '%s%s' % ('!'*(self.critical>0),self.extype) + return '{}{}'.format('!'*(self.critical>0),self.extype) else: - return '%s%s=%s' % ( + return '{}{}={}'.format( '!'*(self.critical>0), self.extype,quote(self.exvalue or '') ) @@ -113,7 +109,7 @@ def __str__(self): return self.unparse() def __repr__(self): - return '<%s.%s instance at %s: %s>' % ( + return '<{}.{} instance at {}: {}>'.format( self.__class__.__module__, self.__class__.__name__, hex(id(self)), @@ -130,61 +126,74 @@ def __ne__(self,other): return not self.__eq__(other) -class LDAPUrlExtensions(UserDict): - """ - Models a collection of LDAP URL extensions as - dictionary type - """ - - def __init__(self,default=None): - UserDict.__init__(self) - for k,v in (default or {}).items(): - self[k]=v - - def __setitem__(self,name,value): +class LDAPUrlExtensions(MutableMapping): """ - value - Either LDAPUrlExtension instance, (critical,exvalue) - or string'ed exvalue + Models a collection of LDAP URL extensions as + a mapping type """ - assert isinstance(value,LDAPUrlExtension) - assert name==value.extype - self.data[name] = value - - def values(self): - return [ - self[k] - for k in self.keys() - ] - - def __str__(self): - return ','.join(str(v) for v in self.values()) - - def __repr__(self): - return '<%s.%s instance at %s: %s>' % ( - self.__class__.__module__, - self.__class__.__name__, - hex(id(self)), - self.data - ) + __slots__ = ('_data', ) + + def __init__(self, default=None): + self._data = {} + if default is not None: + self.update(default) + + def __setitem__(self, name, value): + """Store an extension + + name + string + value + LDAPUrlExtension instance, whose extype nust match `name` + """ + if not isinstance(value, LDAPUrlExtension): + raise TypeError("value must be LDAPUrlExtension, not " + + type(value).__name__) + if name != value.extype: + raise ValueError( + "key {!r} does not match extension type {!r}".format( + name, value.extype)) + self._data[name] = value + + def __getitem__(self, name): + return self._data[name] + + def __delitem__(self, name): + del self._data[name] + + def __iter__(self): + return iter(self._data) + + def __len__(self): + return len(self._data) + + def __str__(self): + return ','.join(str(v) for v in self.values()) + + def __repr__(self): + return '<{}.{} instance at {}: {}>'.format( + self.__class__.__module__, + self.__class__.__name__, + hex(id(self)), + self._data + ) - def __eq__(self,other): - assert isinstance(other,self.__class__),TypeError( - "other has to be instance of %s" % (self.__class__) - ) - return self.data==other.data + def __eq__(self,other): + if not isinstance(other, self.__class__): + return NotImplemented + return self._data == other._data - def parse(self,extListStr): - for extension_str in extListStr.strip().split(','): - if extension_str: - e = LDAPUrlExtension(extension_str) - self[e.extype] = e + def parse(self,extListStr): + for extension_str in extListStr.strip().split(','): + if extension_str: + e = LDAPUrlExtension(extension_str) + self[e.extype] = e - def unparse(self): - return ','.join([ v.unparse() for v in self.values() ]) + def unparse(self): + return ','.join(v.unparse() for v in self.values()) -class LDAPUrl(object): +class LDAPUrl: """ Class for parsing and unparsing LDAP URLs as described in RFC 4516. @@ -221,7 +230,7 @@ def __init__( extensions=None, who=None,cred=None ): - self.urlscheme=urlscheme + self.urlscheme=urlscheme.lower() self.hostport=hostport self.dn=dn self.attrs=attrs @@ -256,9 +265,7 @@ def _parse(self,ldap_url): if not isLDAPUrl(ldap_url): raise ValueError('Value %s for ldap_url does not seem to be a LDAP URL.' % (repr(ldap_url))) scheme,rest = ldap_url.split('://',1) - self.urlscheme = scheme.strip() - if not self.urlscheme in ['ldap','ldaps','ldapi']: - raise ValueError('LDAP URL contains unsupported URL scheme %s.' % (self.urlscheme)) + self.urlscheme = scheme.lower() slash_pos = rest.find('/') qemark_pos = rest.find('?') if (slash_pos==-1) and (qemark_pos==-1): @@ -326,7 +333,7 @@ def initializeUrl(self): hostport = ldapUrlEscape(self.hostport) else: hostport = self.hostport - return '%s://%s' % (self.urlscheme,hostport) + return f'{self.urlscheme}://{hostport}' def unparse(self): """ @@ -347,7 +354,7 @@ def unparse(self): hostport = ldapUrlEscape(self.hostport) else: hostport = self.hostport - ldap_url = '%s://%s/%s?%s?%s?%s' % ( + ldap_url = '{}://{}/{}?{}?{}?{}'.format( self.urlscheme, hostport,dn,attrs_str,scope_str,filterstr ) @@ -366,24 +373,30 @@ def htmlHREF(self,urlPrefix='',hrefText=None,hrefTarget=None): hrefTarget string added as link target attribute """ - assert type(urlPrefix)==StringType, "urlPrefix must be StringType" + if not isinstance(urlPrefix, str): + raise TypeError("urlPrefix must be str, not " + + type(urlPrefix).__name__) if hrefText is None: - hrefText = self.unparse() - assert type(hrefText)==StringType, "hrefText must be StringType" + hrefText = self.unparse() + if not isinstance(hrefText, str): + raise TypeError("hrefText must be str, not " + + type(hrefText).__name__) if hrefTarget is None: - target = '' + target = '' else: - assert type(hrefTarget)==StringType, "hrefTarget must be StringType" - target = ' target="%s"' % hrefTarget - return '%s' % ( - target,urlPrefix,self.unparse(),hrefText + if not isinstance(hrefTarget, str): + raise TypeError("hrefTarget must be str, not " + + type(hrefTarget).__name__) + target = ' target="%s"' % hrefTarget + return '{}'.format( + target, urlPrefix, self.unparse(), hrefText ) def __str__(self): return self.unparse() def __repr__(self): - return '<%s.%s instance at %s: %s>' % ( + return '<{}.{} instance at {}: {}>'.format( self.__class__.__module__, self.__class__.__name__, hex(id(self)), @@ -400,7 +413,7 @@ def __getattr__(self,name): else: return None else: - raise AttributeError('%s has no attribute %s' % ( + raise AttributeError('{} has no attribute {}'.format( self.__class__.__name__,name )) return result # __getattr__() diff --git a/Lib/ldif.py b/Lib/ldif.py index 3f13ec68..356f95ea 100644 --- a/Lib/ldif.py +++ b/Lib/ldif.py @@ -3,10 +3,7 @@ See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - -__version__ = '3.1.0' +__version__ = '3.4.5' __all__ = [ # constants @@ -25,7 +22,8 @@ from io import StringIO import warnings -from ldap.compat import urlparse, urlopen +from urllib.parse import urlparse +from urllib.request import urlopen attrtype_pattern = r'[\w;.-]+(;[\w_-]+)*' attrvalue_pattern = r'(([^,]|\\,)+|".*?")' @@ -375,7 +373,8 @@ def _next_key_and_value(self): if self._process_url_schemes: u = urlparse(url) if u[0] in self._process_url_schemes: - attr_value = urlopen(url).read() + with urlopen(url) as fd: + attr_value = fd.read() else: # All values should be valid ascii; we support UTF-8 as a # non-official, backwards compatibility layer. diff --git a/Lib/slapdtest/__init__.py b/Lib/slapdtest/__init__.py index 56ba2c91..0fabc4c4 100644 --- a/Lib/slapdtest/__init__.py +++ b/Lib/slapdtest/__init__.py @@ -1,12 +1,12 @@ -# -*- coding: utf-8 -*- """ slapdtest - module for spawning test instances of OpenLDAP's slapd server See https://www.python-ldap.org/ for details. """ -__version__ = '3.1.0' +__version__ = '3.4.5' from slapdtest._slapdtest import SlapdObject, SlapdTestCase, SysLogHandler from slapdtest._slapdtest import requires_ldapi, requires_sasl, requires_tls +from slapdtest._slapdtest import requires_init_fd from slapdtest._slapdtest import skip_unless_ci diff --git a/Lib/slapdtest/_slapdtest.py b/Lib/slapdtest/_slapdtest.py index f1885caf..b60313b0 100644 --- a/Lib/slapdtest/_slapdtest.py +++ b/Lib/slapdtest/_slapdtest.py @@ -1,12 +1,8 @@ -# -*- coding: utf-8 -*- """ slapdtest - module for spawning test instances of OpenLDAP's slapd server See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - import os import socket import sys @@ -16,43 +12,48 @@ import atexit from logging.handlers import SysLogHandler import unittest +from shutil import which +from urllib.parse import quote_plus # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' import ldap -from ldap.compat import quote_plus, which HERE = os.path.abspath(os.path.dirname(__file__)) -# a template string for generating simple slapd.conf file -SLAPD_CONF_TEMPLATE = r""" -serverID %(serverid)s -moduleload back_%(database)s -%(include_directives)s -loglevel %(loglevel)s -allow bind_v2 - -authz-regexp - "gidnumber=%(root_gid)s\\+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth" - "%(rootdn)s" - -database %(database)s -directory "%(directory)s" -suffix "%(suffix)s" -rootdn "%(rootdn)s" -rootpw "%(rootpw)s" - -TLSCACertificateFile "%(cafile)s" -TLSCertificateFile "%(servercert)s" -TLSCertificateKeyFile "%(serverkey)s" -# ignore missing client cert but fail with invalid client cert -TLSVerifyClient try - -authz-regexp - "C=DE, O=python-ldap, OU=slapd-test, CN=([A-Za-z]+)" - "ldap://ou=people,dc=local???($1)" - +# a template string for generating simple slapd.d file +SLAPD_CONF_TEMPLATE = r"""dn: cn=config +objectClass: olcGlobal +cn: config +olcServerID: %(serverid)s +olcLogLevel: %(loglevel)s +olcAllows: bind_v2 +olcAuthzRegexp: {0}"gidnumber=%(root_gid)s\+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth" "%(rootdn)s" +olcAuthzRegexp: {1}"C=DE, O=python-ldap, OU=slapd-test, CN=([A-Za-z]+)" "ldap://ou=people,dc=local???($1)" +olcTLSCACertificateFile: %(cafile)s +olcTLSCertificateFile: %(servercert)s +olcTLSCertificateKeyFile: %(serverkey)s +olcTLSVerifyClient: try + +dn: cn=module,cn=config +objectClass: olcModuleList +cn: module +olcModuleLoad: back_%(database)s + +dn: olcDatabase=config,cn=config +objectClass: olcDatabaseConfig +olcDatabase: config +olcRootDN: %(rootdn)s + +dn: olcDatabase=%(database)s,cn=config +objectClass: olcDatabaseConfig +objectClass: olcMdbConfig +olcDatabase: %(database)s +olcSuffix: %(suffix)s +olcRootDN: %(rootdn)s +olcRootPW: %(rootpw)s +olcDbDirectory: %(directory)s """ LOCALHOST = '127.0.0.1' @@ -109,6 +110,14 @@ def requires_ldapi(): else: return identity +def requires_init_fd(): + if not ldap.INIT_FD_AVAIL: + return skip_unless_ci( + "test needs ldap.INIT_FD", feature='INIT_FD') + else: + return identity + + def _add_sbin(path): """Add /sbin and related directories to a command search path""" directories = path.split(os.pathsep) @@ -154,7 +163,7 @@ def combined_logger( return new_logger # end of combined_logger() -class SlapdObject(object): +class SlapdObject: """ Controller class for a slapd instance, OpenLDAP's server. @@ -169,6 +178,9 @@ class SlapdObject(object): manager, the slapd server is shut down and the temporary data store is removed. + :param openldap_schema_files: A list of schema names or schema paths to + load at startup. By default this only contains `core`. + .. versionchanged:: 3.1 Added context manager functionality @@ -179,12 +191,12 @@ class SlapdObject(object): root_cn = 'Manager' root_pw = 'password' slapd_loglevel = 'stats stats2' - local_host = '127.0.0.1' + local_host = LOCALHOST testrunsubdirs = ( - 'schema', + 'slapd.d', ) openldap_schema_files = ( - 'core.schema', + 'core.ldif', ) TMPDIR = os.environ.get('TMP', os.getcwd()) @@ -200,9 +212,6 @@ class SlapdObject(object): BIN_PATH = os.environ.get('BIN', os.environ.get('PATH', os.defpath)) SBIN_PATH = os.environ.get('SBIN', _add_sbin(BIN_PATH)) - # time in secs to wait before trying to access slapd via LDAP (again) - _start_sleep = 1.5 - # create loggers once, multiple calls mess up refleak tests _log = combined_logger('python-ldap-test') @@ -211,10 +220,9 @@ def __init__(self): self._port = self._avail_tcp_port() self.server_id = self._port % 4096 self.testrundir = os.path.join(self.TMPDIR, 'python-ldap-test-%d' % self._port) - self._schema_prefix = os.path.join(self.testrundir, 'schema') - self._slapd_conf = os.path.join(self.testrundir, 'slapd.conf') + self._slapd_conf = os.path.join(self.testrundir, 'slapd.d') self._db_directory = os.path.join(self.testrundir, "openldap-data") - self.ldap_uri = "ldap://%s:%d/" % (LOCALHOST, self._port) + self.ldap_uri = "ldap://%s:%d/" % (self.local_host, self._port) if HAVE_LDAPI: ldapi_path = os.path.join(self.testrundir, 'ldapi') self.ldapi_uri = "ldapi://%s" % quote_plus(ldapi_path) @@ -243,6 +251,14 @@ def __init__(self): def root_dn(self): return 'cn={self.root_cn},{self.suffix}'.format(self=self) + @property + def hostname(self): + return self.local_host + + @property + def port(self): + return self._port + def _find_commands(self): self.PATH_LDAPADD = self._find_command('ldapadd') self.PATH_LDAPDELETE = self._find_command('ldapdelete') @@ -252,7 +268,6 @@ def _find_commands(self): self.PATH_SLAPD = os.environ.get('SLAPD', None) if not self.PATH_SLAPD: self.PATH_SLAPD = self._find_command('slapd', in_sbin=True) - self.PATH_SLAPTEST = self._find_command('slaptest', in_sbin=True) def _find_command(self, cmd, in_sbin=False): if in_sbin: @@ -265,7 +280,7 @@ def _find_command(self, cmd, in_sbin=False): if command is None: raise ValueError( "Command '{}' not found. Set the {} environment variable to " - "override slapdtest's search path.".format(cmd, var_name) + "override slapdtest's search path: {}.".format(cmd, var_name, path) ) return command @@ -279,7 +294,6 @@ def setup_rundir(self): os.mkdir(self.testrundir) os.mkdir(self._db_directory) self._create_sub_dirs(self.testrunsubdirs) - self._ln_schema_files(self.openldap_schema_files, self.SCHEMADIR) def _cleanup_rundir(self): """ @@ -324,17 +338,8 @@ def gen_config(self): for generating specific static configuration files you have to override this method """ - include_directives = '\n'.join( - 'include "{schema_prefix}/{schema_file}"'.format( - schema_prefix=self._schema_prefix, - schema_file=schema_file, - ) - for schema_file in self.openldap_schema_files - ) config_dict = { 'serverid': hex(self.server_id), - 'schema_prefix':self._schema_prefix, - 'include_directives': include_directives, 'loglevel': self.slapd_loglevel, 'database': self.database, 'directory': self._db_directory, @@ -346,6 +351,7 @@ def gen_config(self): 'cafile': self.cafile, 'servercert': self.servercert, 'serverkey': self.serverkey, + 'slapd_path': self.SBIN_PATH, } return self.slapd_conf_template % config_dict @@ -358,37 +364,39 @@ def _create_sub_dirs(self, dir_names): self._log.debug('Create directory %s', dir_name) os.mkdir(dir_name) - def _ln_schema_files(self, file_names, source_dir): - """ - write symbolic links to original schema files - """ - for fname in file_names: - ln_source = os.path.join(source_dir, fname) - ln_target = os.path.join(self._schema_prefix, fname) - self._log.debug('Create symlink %s -> %s', ln_source, ln_target) - os.symlink(ln_source, ln_target) - def _write_config(self): - """Writes the slapd.conf file out, and returns the path to it.""" - self._log.debug('Writing config to %s', self._slapd_conf) - with open(self._slapd_conf, 'w') as config_file: - config_file.write(self.gen_config()) - self._log.info('Wrote config to %s', self._slapd_conf) + """Loads the slapd.d configuration.""" + self._log.debug("importing configuration: %s", self._slapd_conf) + + self.slapadd(self.gen_config(), ["-n0"]) + ldif_paths = [ + schema + if os.path.exists(schema) + else os.path.join(self.SCHEMADIR, schema) + for schema in self.openldap_schema_files + ] + for ldif_path in ldif_paths: + self.slapadd(None, ["-n0", "-l", ldif_path]) + + self._log.debug("import ok: %s", self._slapd_conf) def _test_config(self): self._log.debug('testing config %s', self._slapd_conf) popen_list = [ - self.PATH_SLAPTEST, - "-f", self._slapd_conf, - '-u', + self.PATH_SLAPD, + "-Ttest", + "-F", self._slapd_conf, + "-u", + "-v", + "-d", "config" ] - if self._log.isEnabledFor(logging.DEBUG): - popen_list.append('-v') - popen_list.extend(['-d', 'config']) - else: - popen_list.append('-Q') - proc = subprocess.Popen(popen_list) - if proc.wait() != 0: + p = subprocess.run( + popen_list, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + if p.returncode != 0: + self._log.error(p.stdout.decode("utf-8")) raise RuntimeError("configuration test failed") self._log.info("config ok: %s", self._slapd_conf) @@ -401,31 +409,37 @@ def _start_slapd(self): urls.append(self.ldapi_uri) slapd_args = [ self.PATH_SLAPD, - '-f', self._slapd_conf, - '-F', self.testrundir, + '-F', self._slapd_conf, '-h', ' '.join(urls), ] + stderr = None if self._log.isEnabledFor(logging.DEBUG): slapd_args.extend(['-d', '-1']) + stderr = os.open(os.path.join(self.testrundir, 'slapd.log'), os.O_WRONLY|os.O_CREAT) else: slapd_args.extend(['-d', '0']) self._log.info('starting slapd: %r', ' '.join(slapd_args)) - self._proc = subprocess.Popen(slapd_args) + self._proc = subprocess.Popen(slapd_args, stderr=stderr) + if stderr is not None: + os.close(stderr) + stderr = None # Waits until the LDAP server socket is open, or slapd crashed + deadline = time.monotonic() + 10 # no cover to avoid spurious coverage changes, see # https://github.com/python-ldap/python-ldap/issues/127 - for _ in range(10): # pragma: no cover + while True: # pragma: no cover if self._proc.poll() is not None: self._stopped() raise RuntimeError("slapd exited before opening port") - time.sleep(self._start_sleep) try: self._log.debug( "slapd connection check to %s", self.default_ldap_uri ) self.ldapwhoami() except RuntimeError: - pass + if time.monotonic() >= deadline: + break + time.sleep(0.2) else: return raise RuntimeError("slapd did not start properly") @@ -448,7 +462,7 @@ def start(self): self._proc.pid, self.ldap_uri, self.ldapi_uri ) - def stop(self): + def stop(self, cleanup=True): """ Stops the slapd server, and waits for it to terminate and cleans up """ @@ -456,23 +470,24 @@ def stop(self): self._log.debug('stopping slapd with pid %d', self._proc.pid) self._proc.terminate() self.wait() - self._cleanup_rundir() - if hasattr(atexit, 'unregister'): - # Python 3 - atexit.unregister(self.stop) - elif hasattr(atexit, '_exithandlers'): - # Python 2, can be None during process shutdown - try: - atexit._exithandlers.remove(self.stop) - except ValueError: - pass + if cleanup: + self._cleanup_rundir() + atexit.unregister(self.stop) def restart(self): """ Restarts the slapd server with same data """ - self._proc.terminate() + self.terminate() self.wait() + self.resume() + + def terminate(self): + """Terminate slapd server""" + self._proc.terminate() + + def resume(self): + """Start slapd server""" self._start_slapd() def wait(self): @@ -504,13 +519,20 @@ def _cli_auth_args(self): # no cover to avoid spurious coverage changes def _cli_popen(self, ldapcommand, extra_args=None, ldap_uri=None, - stdin_data=None): # pragma: no cover + stdin_data=None, tool=None): # pragma: no cover if ldap_uri is None: ldap_uri = self.default_ldap_uri - args = [ - ldapcommand, - '-H', ldap_uri, - ] + self._cli_auth_args() + (extra_args or []) + + if ldapcommand.split("/")[-1].startswith("ldap"): + args = [ldapcommand, '-H', ldap_uri] + self._cli_auth_args() + else: + if tool: + args = [ldapcommand, '-T', tool, '-F', self._slapd_conf] + else: + args = [ldapcommand, '-F', self._slapd_conf] + + args += (extra_args or []) + self._log.debug('Run command: %r', ' '.join(args)) proc = subprocess.Popen( args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, @@ -561,12 +583,23 @@ def ldapdelete(self, dn, recursive=False, extra_args=None): extra_args.append(dn) self._cli_popen(self.PATH_LDAPDELETE, extra_args=extra_args) + def slapadd(self, ldif, extra_args=None): + """ + Runs slapadd on this slapd instance, passing it the ldif content + """ + self._cli_popen( + self.PATH_SLAPD, + stdin_data=ldif.encode("utf-8") if ldif else None, + extra_args=extra_args, + tool='add' + ) + def __enter__(self): self.start() return self def __exit__(self, exc_type, exc_value, traceback): - self.stop() + self.stop(exc_type is None) class SlapdTestCase(unittest.TestCase): @@ -595,4 +628,4 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - cls.server.stop() + cls.server.stop(False) diff --git a/Lib/slapdtest/certs/ca.conf b/Lib/slapdtest/certs/ca.conf index 5046b0d6..d1d89e18 100644 --- a/Lib/slapdtest/certs/ca.conf +++ b/Lib/slapdtest/certs/ca.conf @@ -32,7 +32,7 @@ serial = $tmpdir/$ca.crt.srl crlnumber = $tmpdir/$ca.crl.srl database = $tmpdir/$ca.db unique_subject = no -default_days = 3652 +default_days = 365200 default_md = sha256 policy = match_pol email_in_dn = no @@ -40,7 +40,7 @@ preserve = no name_opt = $name_opt cert_opt = ca_default copy_extensions = none -default_crl_days = 3651 +default_crl_days = 365100 [match_pol] countryName = match diff --git a/Lib/slapdtest/certs/ca.pem b/Lib/slapdtest/certs/ca.pem index cf2ff33c..b52ffafb 100644 --- a/Lib/slapdtest/certs/ca.pem +++ b/Lib/slapdtest/certs/ca.pem @@ -5,31 +5,31 @@ Certificate: Signature Algorithm: sha256WithRSAEncryption Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA Validity - Not Before: Dec 2 11:57:47 2017 GMT - Not After : Sep 4 11:57:47 2027 GMT + Not Before: Apr 12 18:52:38 2019 GMT + Not After : Oct 17 18:52:38 2994 GMT Subject: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: - 00:af:1f:cf:0f:c5:95:66:2d:eb:85:cc:21:fc:0d: - 0f:44:d8:2f:a8:85:08:ef:60:67:57:fa:0b:c5:e4: - b3:fb:f1:6f:cb:30:7a:47:0d:a7:f1:b5:37:81:5f: - f6:39:28:e2:f9:4d:6c:2e:a6:5c:0e:3c:db:4d:c9: - 2a:64:ce:0d:15:30:c7:75:52:b8:74:c5:0b:00:4c: - 2f:94:1b:dd:fb:83:2c:58:02:73:b0:86:3a:6a:aa: - 55:f2:d5:49:99:17:a5:e2:44:ec:dd:62:5f:8d:ce: - 77:29:0b:8d:87:23:e2:4b:d6:1c:25:f3:06:a9:ee: - 33:6f:ac:ed:22:9e:35:ec:55:e7:1b:38:68:7e:46: - e3:c3:42:ac:06:0b:0a:7a:84:c9:3d:ef:3d:a5:6e: - e9:10:24:c3:28:fe:1f:4a:9a:23:8a:3c:db:0a:66: - 5d:07:f8:c5:17:68:53:e4:0e:37:33:c4:d2:ad:58: - 62:6b:8a:87:ab:73:eb:bc:2b:ac:07:69:84:8d:e3: - c4:a9:78:9b:6c:1e:03:63:df:b4:96:18:bd:3c:2e: - be:7f:2c:d5:a8:f8:12:b9:ab:27:52:b0:de:38:62: - 3c:54:a7:f3:aa:37:a3:11:12:b2:a7:6f:8d:96:10: - ce:01:cb:25:24:a6:51:18:93:69:9b:9e:5c:8a:ff: - fe:89 + 00:d7:30:73:20:44:7d:83:d4:c7:01:b8:ab:1e:7c: + 91:f4:38:ac:9c:41:43:64:0c:31:99:48:70:22:7d: + ae:1b:47:e7:2a:28:4d:f7:46:4e:b4:ba:ae:c0:9d: + d5:1f:4b:7a:79:2f:b9:dc:68:7f:79:84:88:50:51: + 3b:7d:dc:d5:57:17:66:45:c0:2c:20:13:f7:99:d6: + 9d:e2:12:7c:41:76:82:51:19:2c:b6:ff:46:cb:04: + 56:38:22:2a:c3:7a:b5:71:51:49:4e:62:68:a0:99: + 6f:de:f3:a2:0f:a2:aa:1b:72:a5:87:bc:42:5a:a7: + 22:8d:33:b4:88:a8:dc:5d:72:ca:dd:a0:9a:4e:db: + 7d:8b:10:de:c5:41:e9:e9:8d:fa:6c:dd:94:6e:b1: + 31:c2:6d:a1:69:6c:7a:3a:b2:76:65:c9:e5:95:38: + 62:40:81:c6:29:26:26:d1:d1:c1:f4:5e:fa:24:ef: + 13:da:24:13:6f:f5:5c:ba:b1:31:8f:30:94:71:7b: + c6:e5:da:b9:b5:64:39:39:09:c2:4a:80:64:58:1d: + 99:f5:65:3c:a7:26:08:95:26:35:7b:fa:e7:20:08: + ff:72:df:9b:8f:9f:da:8b:c3:a7:8b:fc:8c:c0:a5: + 31:87:1d:4c:14:f6:cf:90:5e:2e:6e:a6:db:27:08: + eb:df Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: critical @@ -37,44 +37,44 @@ Certificate: X509v3 Key Usage: critical Certificate Sign, CRL Sign X509v3 Subject Key Identifier: - 3B:1F:32:F4:FE:57:D1:6F:49:91:55:F2:24:F1:0A:66:3B:A5:EE:D4 + BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2 X509v3 Authority Key Identifier: - keyid:3B:1F:32:F4:FE:57:D1:6F:49:91:55:F2:24:F1:0A:66:3B:A5:EE:D4 + keyid:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2 Signature Algorithm: sha256WithRSAEncryption - 0a:e7:dc:38:ce:03:dd:a8:99:11:d0:24:be:ef:1a:18:9d:7c: - 95:75:4a:4a:29:44:23:28:fc:66:d5:81:ce:05:c2:c0:6b:71: - d6:8d:33:a9:53:a6:1c:f1:4e:50:ae:a3:b1:72:d6:69:53:ad: - a9:62:a9:45:27:68:17:35:41:97:ec:e9:65:91:62:12:ed:eb: - 45:3a:9b:cc:09:bc:e3:ad:22:6b:13:6b:b0:67:ef:ce:01:83: - 5e:6c:95:e2:b3:73:b9:69:9a:33:49:f9:5f:52:4e:39:94:c9: - db:93:6f:d8:ba:10:92:ce:fa:12:6b:bc:31:ff:c1:67:70:63: - 07:dc:53:7a:3a:a3:51:20:15:44:cf:1c:a9:cd:b7:30:1d:8e: - 55:93:8a:56:8c:3d:e9:8b:ae:0c:77:8d:5c:8b:fd:22:d8:4c: - 3e:e4:76:e8:d9:e8:c3:98:f4:98:ff:02:60:95:8e:3e:26:7a: - e2:fe:2c:0a:a4:52:8d:4c:3d:dd:4c:fd:2f:2c:db:83:4c:2b: - 25:24:37:78:9a:07:27:52:f9:1c:c0:65:65:cb:50:77:b4:2d: - fa:f4:af:bb:42:1c:43:65:c6:01:6e:f1:4b:fe:b8:4a:3c:29: - 8b:b6:84:1e:17:99:61:98:65:fe:f2:e9:ce:bb:ac:87:69:cb: - e6:13:42:bf + 06:20:1f:eb:42:6a:42:62:b1:ee:69:c8:cd:47:a6:2e:69:95: + 59:dc:49:09:69:40:93:25:a1:ec:6d:3a:dd:dc:e5:74:ab:33: + 9d:8f:cc:e3:bb:7a:3f:5b:51:58:74:f7:bd:6c:7c:3c:b6:5a: + 05:50:a8:8c:c3:fb:5b:75:2a:c2:6c:06:93:4c:a9:93:71:1c: + 51:e5:be:a1:24:93:e2:79:ca:ea:08:86:90:b9:70:e7:7a:40: + bf:f4:d6:71:f4:4d:c0:0f:e0:31:a0:23:46:77:30:72:a9:62: + 8a:2a:12:c4:dd:3d:86:ae:f7:6b:33:80:26:58:49:53:ff:cd: + 8a:c6:f6:11:2c:b3:ff:a5:8e:1c:f8:22:e2:1b:8e:04:33:fb: + 0d:da:31:86:12:9f:d1:03:86:9c:6a:78:5e:3c:5e:8a:52:aa: + 68:1f:ff:f9:17:75:b0:da:f2:99:3c:80:3c:96:2a:33:07:54: + 59:84:e7:92:34:0f:99:76:e3:d6:4d:4d:9c:fb:21:35:f9:cb: + a5:30:80:8b:9d:61:90:d3:d4:59:3a:2f:f2:f6:20:13:7e:26: + dc:50:b0:49:3e:19:fe:eb:7d:cf:b9:1a:5d:5c:3a:76:30:d9: + 0e:d7:df:de:ce:a9:c4:21:df:63:b9:d0:64:86:0b:28:9a:2e: + ab:51:73:e4 -----BEGIN CERTIFICATE----- -MIIDijCCAnKgAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU +MIIDjDCCAnSgAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV -BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwHhcNMTcxMjAyMTE1NzQ3WhcNMjcwOTA0 -MTE1NzQ3WjBWMQswCQYDVQQGEwJERTEUMBIGA1UECgwLcHl0aG9uLWxkYXAxEzAR -BgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNVBAMME1B5dGhvbiBMREFQIFRlc3QgQ0Ew -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvH88PxZVmLeuFzCH8DQ9E -2C+ohQjvYGdX+gvF5LP78W/LMHpHDafxtTeBX/Y5KOL5TWwuplwOPNtNySpkzg0V -MMd1Urh0xQsATC+UG937gyxYAnOwhjpqqlXy1UmZF6XiROzdYl+NzncpC42HI+JL -1hwl8wap7jNvrO0injXsVecbOGh+RuPDQqwGCwp6hMk97z2lbukQJMMo/h9KmiOK -PNsKZl0H+MUXaFPkDjczxNKtWGJrioerc+u8K6wHaYSN48SpeJtsHgNj37SWGL08 -Lr5/LNWo+BK5qydSsN44YjxUp/OqN6MRErKnb42WEM4ByyUkplEYk2mbnlyK//6J -AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud -DgQWBBQ7HzL0/lfRb0mRVfIk8QpmO6Xu1DAfBgNVHSMEGDAWgBQ7HzL0/lfRb0mR -VfIk8QpmO6Xu1DANBgkqhkiG9w0BAQsFAAOCAQEACufcOM4D3aiZEdAkvu8aGJ18 -lXVKSilEIyj8ZtWBzgXCwGtx1o0zqVOmHPFOUK6jsXLWaVOtqWKpRSdoFzVBl+zp -ZZFiEu3rRTqbzAm8460iaxNrsGfvzgGDXmyV4rNzuWmaM0n5X1JOOZTJ25Nv2LoQ -ks76Emu8Mf/BZ3BjB9xTejqjUSAVRM8cqc23MB2OVZOKVow96YuuDHeNXIv9IthM -PuR26Nnow5j0mP8CYJWOPiZ64v4sCqRSjUw93Uz9Lyzbg0wrJSQ3eJoHJ1L5HMBl -ZctQd7Qt+vSvu0IcQ2XGAW7xS/64Sjwpi7aEHheZYZhl/vLpzrush2nL5hNCvw== +BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwIBcNMTkwNDEyMTg1MjM4WhgPMjk5NDEw +MTcxODUyMzhaMFYxCzAJBgNVBAYTAkRFMRQwEgYDVQQKDAtweXRob24tbGRhcDET +MBEGA1UECwwKc2xhcGQtdGVzdDEcMBoGA1UEAwwTUHl0aG9uIExEQVAgVGVzdCBD +QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANcwcyBEfYPUxwG4qx58 +kfQ4rJxBQ2QMMZlIcCJ9rhtH5yooTfdGTrS6rsCd1R9Lenkvudxof3mEiFBRO33c +1VcXZkXALCAT95nWneISfEF2glEZLLb/RssEVjgiKsN6tXFRSU5iaKCZb97zog+i +qhtypYe8QlqnIo0ztIio3F1yyt2gmk7bfYsQ3sVB6emN+mzdlG6xMcJtoWlsejqy +dmXJ5ZU4YkCBxikmJtHRwfRe+iTvE9okE2/1XLqxMY8wlHF7xuXaubVkOTkJwkqA +ZFgdmfVlPKcmCJUmNXv65yAI/3Lfm4+f2ovDp4v8jMClMYcdTBT2z5BeLm6m2ycI +698CAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFL141UrxkJbF6OxmSSNHA18mc4ayMB8GA1UdIwQYMBaAFL141UrxkJbF +6OxmSSNHA18mc4ayMA0GCSqGSIb3DQEBCwUAA4IBAQAGIB/rQmpCYrHuacjNR6Yu +aZVZ3EkJaUCTJaHsbTrd3OV0qzOdj8zju3o/W1FYdPe9bHw8tloFUKiMw/tbdSrC +bAaTTKmTcRxR5b6hJJPiecrqCIaQuXDnekC/9NZx9E3AD+AxoCNGdzByqWKKKhLE +3T2GrvdrM4AmWElT/82KxvYRLLP/pY4c+CLiG44EM/sN2jGGEp/RA4acanhePF6K +UqpoH//5F3Ww2vKZPIA8liozB1RZhOeSNA+ZduPWTU2c+yE1+culMICLnWGQ09RZ +Oi/y9iATfibcULBJPhn+633PuRpdXDp2MNkO19/ezqnEId9judBkhgsomi6rUXPk -----END CERTIFICATE----- diff --git a/Lib/slapdtest/certs/client.key b/Lib/slapdtest/certs/client.key index 70600ba5..7213c0b4 100644 --- a/Lib/slapdtest/certs/client.key +++ b/Lib/slapdtest/certs/client.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDGxvbMEbahViK4 -P6aoWUkciIf1dEYTBuU8M1eShREUZ3Ytq/ee425pXRyxxDrAa8ygRjqs7tauwhgA -KNuPRGyw7hyZ1Ku4vQObwX9rzyHQ6Fj606U4HHbYfAVb0AF7OzLbhNH3isCRtNcm -EUSYG1Nkfn9zQkV4pz2KJM4ePt4GyGJV0NhRUdHdwgsWiuRt2EIRPwLXdkf/4svm -2EahAJax+SMaZYe/lg9w9SBxl6DTaF9lFpoPqzeW+KnXCmE8e+qSWea8EjkzIRXa -4KJ/EnUMP2fUL+Je2agGF2YfCId9YvLTQV8YDv7Im1Sp0sbIUDoWX14GDUbZ8cBr -wfOyI0TfAgMBAAECggEATWv1eGp1zcU05Lq1+OA938U1316YZJTM+HOu6jy1+FKL -7yIJ4nMG8Db6FCswDv5txwdTl0O3jn2+x2Eik1y9UPSNY0U4VU4Zd7MYJC+bJjk5 -XwjMU1yS1aMIm0gbK5pVJrdG6Lm8Y4QiQIt9Qhlyk7PJhGUNlf7ds06+kX0/ETiO -vx5SatExeKu5F+JRnGFdAN0106SF5vBum+UbrgOSnJmfwX5VoOXARD21ppxgMzAr -JyGBpgBgy++GpV15gXGuA7DVMIADdHw8hV4OuBLjpkUL+ntArjhpUi7TP7VU3WKR -uUmvLm9CX1l8O/xZMpt9N1+o71a//7asnz8AMtT6cQKBgQD4FgefUkVnXDA1xKDW -1JbArVQeHiLGlRdLakRUY/HdGj72YgAOLt3UsrON4VQXl0C6rks/8HKCFaMexBlF -OecJNWsEVgBEAfsQ+NvrApOQsTszc8Zqna0Kqe2vA0VNa+SAzdHzhBbFcaVkzXJb -JB7M0/OIt5IaqXg6Y5eX2eZF1QKBgQDNHkIoJ/2hYtlSgXpGaniM+0XemQJgJXig -edAQdGKKfqwmjSFjByDM01ZaidMu5fEkeGhMRE73IbwNw0pWsMXylD6bI6+sk7yQ -biM+fslFEEDbgSJe41Jy2eerh5am+dnrMWNhd7QZV1K6tmaqrIzkmIV21/EPXIPp -BNHO8GV14wKBgGOybrO/GzcTXChvcXeEDWU3AqPr1mvZhHgBJ56GX69MGdtnvL/2 -Y51Th0bQM7wbQ58B5im21j2itl/pzIH+Z/NSbURbz1WFOkEy0SYbbfPq1XCy6Rz1 -apHrgiIf/VzErBp7HBFxlrkYF7Bvw7IOzPXhg3AA3Y0rZ66HUWdr4NdVAoGBAJfC -E2Bydgy5feC1OypuC9MC9abDviY0kxLoDTCfa2jcX7IGKPWDiJkCo5lI7557Mfax -vzjuMR5XLzNfkdih4VKgq9FMjeU5SQHy+tB6LZ+Tbuj4md1qgs3GuskGAEh6Auko -GUc7sVwuZ18NJNiR4Ywf7F8JVajv4gi9MB3Tbr3RAoGARSnVu+6rYSQTyEqvbsaB -gIW7Ezea5q06GcQF072nk3tNSXuU/52YMlodAJ1UfFPbBAtaa7wEFN8oRG1IyKON -MGyf6RD8GoInJjaDihkdCsR28RkchwymG1UMPnPzqRxSAb7da5YuMR8PEioVbL68 -dxhsgNi1Wtc2nGqN96qufG0= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDjt5O6nRrnAWPm +T0JvRLBHMclll92IWF/O4GEdcJ5fbBxP3BxK0Dv+6aRcR7b2o0f6fk/bgNepXfv/ +MXDQcFlESbfmUNGshFmZr0sjPrYPD1R06TZs+/7RsMXnx1c79mFGEQ4wqzDOBHKQ +xeDhNJk+BcE0QABsqF8AA2XC2/dK14QCljKLC84k1zTFTnh8duN2eAalaPQFFOoj +4AnonUnswJ45zIx5V2BdG+oqO5dwo/cEukKgAEL8T2IJ9Cqlmh2sPbMqYC8cODq6 +YcugMznxrfHV5LNThfkvwMe26+vv68r65zalPDy0M+cUMTMyBVY4TL3fejrloY2t +YMhPJIclAgMBAAECggEAPXdd/u9NRbGQX6hhTFuEIZOEw1F80MLaCaNzU1kExskN +01icom0W5LX4UZhiAK0OTsUtlRhwHh1qWfXkd777uX0UkKycDC8laGByra7Nwb7n +ky8oK77Rh5RptyiNmXflxd3wsJ5k7BczPXTMQL3L53vyLMJh2vKPwhcorrJlS+Pi +JjINMaR4IrDlpMYlrn9NTjsGr+mj/pdmKfU/KVXeKzFcwKTjUnDJNSbGDIC0AxaJ +dGU0yIX9MPW+p5szcA9o22UWW4LsEFY4YABeCqbm9/UQt3jWVMjCy4AOgr/9HWSR +DvXI/Xtdl3CTCr8+qDnhBaUI27z+UelZfTBFKUb8AQKBgQD6SmtrTBgEfb6tuxJw +AAHRuUcWGjatZ7X+meHRC9B7UPxUrKl9tU5NC7Gz6YMt+vr4bNMwykI6Ndj+4tSJ +KqsAC86v19CH4usMBLZ68MeTRvtQGiPah71syYrxf0uvYOx/KzUUBX240Ls+lEbE +W33psMoNAezUPpJwKx7CMjcBgQKBgQDo6VaT59bKRc3DXJvqFjd7TPIex+ny6JK+ +8oOwyyFFBwkzfymoOxN4lxSrE6yf7uTemRRn+RIH3UGDottIDqzhjvtcV5uODeIN +8WzxTbl759qIxt+z7aF7SkwJLJAAZS3qqCXKtMBo7ln4xKaoRLT2RohqD1YXGrg8 +wmYcUZoPpQKBgQCm2QVSuZ8pH0oFNjfMQbT0wbYJnd/lKMXBu4M1f9Ky4gHT0GYM +Ttirs6f6byfrduvmv2TpmWscsti80SktZywnE7fssMlqTHKzyFB9FBV2sFLHyyUr +gGFeK9xbsKgbeVkuTPdNKXvtv/eSd/XU38jIB/opQadGtY+ZBqWyfxb8AQKBgBLc +SlmBzZ/llSr7xdhn4ihG69hYQfacpL13r/hSCqinUDRuWLY5ynLacR8FYdY1pyzr +Yn6k6bPfU93QA0fLgG5ngK1SntMbBrIwWa0UqS+Cb+zhhd3xIUF1m8CmbibKCrTU +1vKaPnaAzqJZclFv9uN2hLdp9IO8cyzgZRpn9TzNAoGAUfZF1983qknfBgD8Lgm3 +zzKYtc8q2Ukatfo4VCp66CEprbLcBq5mKx6JiBoMGqU8SI5XVG0F0aHH2n8gImcu +bO0vtEldDc1ylZ/H7xhHFWlMzmTlsbHdHVtetFfKLTpjq6duvgLA12lJNHNVu3OU +Z1bRWDeZIP70+jdYrmSoVi8= -----END PRIVATE KEY----- diff --git a/Lib/slapdtest/certs/client.pem b/Lib/slapdtest/certs/client.pem index 33b95a73..ca2989ca 100644 --- a/Lib/slapdtest/certs/client.pem +++ b/Lib/slapdtest/certs/client.pem @@ -5,31 +5,31 @@ Certificate: Signature Algorithm: sha256WithRSAEncryption Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA Validity - Not Before: Dec 2 11:57:48 2017 GMT - Not After : Dec 2 11:57:48 2027 GMT + Not Before: Apr 12 18:52:38 2019 GMT + Not After : Mar 1 18:52:38 3019 GMT Subject: C=DE, O=python-ldap, OU=slapd-test, CN=client Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: - 00:c6:c6:f6:cc:11:b6:a1:56:22:b8:3f:a6:a8:59: - 49:1c:88:87:f5:74:46:13:06:e5:3c:33:57:92:85: - 11:14:67:76:2d:ab:f7:9e:e3:6e:69:5d:1c:b1:c4: - 3a:c0:6b:cc:a0:46:3a:ac:ee:d6:ae:c2:18:00:28: - db:8f:44:6c:b0:ee:1c:99:d4:ab:b8:bd:03:9b:c1: - 7f:6b:cf:21:d0:e8:58:fa:d3:a5:38:1c:76:d8:7c: - 05:5b:d0:01:7b:3b:32:db:84:d1:f7:8a:c0:91:b4: - d7:26:11:44:98:1b:53:64:7e:7f:73:42:45:78:a7: - 3d:8a:24:ce:1e:3e:de:06:c8:62:55:d0:d8:51:51: - d1:dd:c2:0b:16:8a:e4:6d:d8:42:11:3f:02:d7:76: - 47:ff:e2:cb:e6:d8:46:a1:00:96:b1:f9:23:1a:65: - 87:bf:96:0f:70:f5:20:71:97:a0:d3:68:5f:65:16: - 9a:0f:ab:37:96:f8:a9:d7:0a:61:3c:7b:ea:92:59: - e6:bc:12:39:33:21:15:da:e0:a2:7f:12:75:0c:3f: - 67:d4:2f:e2:5e:d9:a8:06:17:66:1f:08:87:7d:62: - f2:d3:41:5f:18:0e:fe:c8:9b:54:a9:d2:c6:c8:50: - 3a:16:5f:5e:06:0d:46:d9:f1:c0:6b:c1:f3:b2:23: - 44:df + 00:e3:b7:93:ba:9d:1a:e7:01:63:e6:4f:42:6f:44: + b0:47:31:c9:65:97:dd:88:58:5f:ce:e0:61:1d:70: + 9e:5f:6c:1c:4f:dc:1c:4a:d0:3b:fe:e9:a4:5c:47: + b6:f6:a3:47:fa:7e:4f:db:80:d7:a9:5d:fb:ff:31: + 70:d0:70:59:44:49:b7:e6:50:d1:ac:84:59:99:af: + 4b:23:3e:b6:0f:0f:54:74:e9:36:6c:fb:fe:d1:b0: + c5:e7:c7:57:3b:f6:61:46:11:0e:30:ab:30:ce:04: + 72:90:c5:e0:e1:34:99:3e:05:c1:34:40:00:6c:a8: + 5f:00:03:65:c2:db:f7:4a:d7:84:02:96:32:8b:0b: + ce:24:d7:34:c5:4e:78:7c:76:e3:76:78:06:a5:68: + f4:05:14:ea:23:e0:09:e8:9d:49:ec:c0:9e:39:cc: + 8c:79:57:60:5d:1b:ea:2a:3b:97:70:a3:f7:04:ba: + 42:a0:00:42:fc:4f:62:09:f4:2a:a5:9a:1d:ac:3d: + b3:2a:60:2f:1c:38:3a:ba:61:cb:a0:33:39:f1:ad: + f1:d5:e4:b3:53:85:f9:2f:c0:c7:b6:eb:eb:ef:eb: + ca:fa:e7:36:a5:3c:3c:b4:33:e7:14:31:33:32:05: + 56:38:4c:bd:df:7a:3a:e5:a1:8d:ad:60:c8:4f:24: + 87:25 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: critical @@ -39,45 +39,45 @@ Certificate: X509v3 Extended Key Usage: critical TLS Web Client Authentication X509v3 Subject Key Identifier: - 67:63:38:F4:B4:BC:F3:6B:BC:74:0E:7C:27:C9:BB:C2:CC:58:AC:16 + 4F:E7:35:C7:C8:C1:01:C3:7C:53:86:B9:BF:AE:8B:D6:45:A2:78:20 X509v3 Authority Key Identifier: - keyid:3B:1F:32:F4:FE:57:D1:6F:49:91:55:F2:24:F1:0A:66:3B:A5:EE:D4 + keyid:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2 Signature Algorithm: sha256WithRSAEncryption - 76:24:42:6b:33:4f:d6:59:07:48:5b:04:9c:3c:d3:3f:63:80: - 75:4d:78:d7:d5:85:b1:77:81:31:a3:91:cb:c9:a3:8c:0e:00: - 28:08:74:71:6c:fc:83:8c:80:ec:1c:e8:ee:83:e0:7f:49:3b: - f3:42:33:5a:1f:68:0c:a5:41:42:ce:bf:77:29:07:f2:18:a7: - 81:17:d7:76:47:04:d9:8a:dd:e8:5a:26:26:ea:a4:76:70:e1: - f1:fa:e1:db:bc:f2:24:b2:37:a8:58:2f:e3:66:89:77:02:55: - 87:ef:3c:1f:66:ce:4e:86:b3:4c:57:43:86:7f:4c:ab:5a:33: - dd:ca:e3:2f:3b:af:b4:43:5a:53:8b:e0:12:da:e7:c0:13:76: - b2:68:d5:14:f8:1a:07:ce:8a:87:5c:91:bd:35:d7:83:c6:2a: - a4:e0:92:50:01:b9:c2:fa:69:06:5c:8a:80:ee:9c:24:f9:49: - 64:e3:59:c1:a6:69:29:ce:b7:89:20:a9:7c:d6:9f:df:2a:d1: - a4:98:2a:6d:7b:93:6a:52:e3:ae:de:1a:d8:f3:2e:cf:02:7e: - ba:9a:fa:f4:b3:b5:6e:9a:23:10:70:53:53:30:d5:8a:32:35: - 01:52:58:6d:9d:f5:8e:bb:b9:76:bd:41:16:88:26:f8:d3:ce: - 70:03:c8:59 + 1c:90:5f:cf:18:48:95:4d:9d:d3:8e:6d:d1:69:19:1e:7b:3f: + 1f:48:7c:c8:0d:2f:c4:53:0f:89:23:f4:be:ea:b4:7a:c6:dd: + cc:18:0f:e7:34:ea:2c:d4:07:0d:65:78:e8:20:40:3f:36:ef: + 2c:00:31:69:e6:20:48:65:be:57:03:0e:69:ff:b9:83:59:99: + 7d:4d:86:98:14:5b:8e:39:25:3a:a8:6d:51:dc:45:a5:0f:cd: + f3:7a:fd:55:af:5f:55:75:20:03:f5:4a:75:6a:79:2f:76:84: + f6:4e:3d:1d:59:45:9a:b1:6a:57:6f:16:76:76:f8:df:6e:96: + d5:25:27:34:4b:21:d8:c9:9a:36:55:45:a0:43:16:43:68:93: + 37:af:81:89:06:d1:56:1b:9e:0f:62:40:ad:3c:4c:f5:ef:6c: + a2:a4:7f:f2:fa:78:9c:0d:c0:19:f1:10:e8:d8:cf:03:67:3c: + 2d:4d:f3:5d:67:5c:41:a7:4f:d6:c5:0e:ff:2c:04:dd:23:bb: + 85:44:8e:25:ac:15:a3:82:fa:a4:4f:fa:1d:87:f0:58:dc:ae: + 53:05:b9:81:e8:cb:e5:0c:ac:a5:74:68:03:f9:22:a0:45:b6: + 62:58:e0:98:d9:8c:54:a4:22:03:7a:37:12:eb:7d:b1:ad:45: + 60:8e:7a:df -----BEGIN CERTIFICATE----- -MIIDkjCCAnqgAwIBAgIBAzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU +MIIDlDCCAnygAwIBAgIBAzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV -BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwHhcNMTcxMjAyMTE1NzQ4WhcNMjcxMjAy -MTE1NzQ4WjBJMQswCQYDVQQGEwJERTEUMBIGA1UECgwLcHl0aG9uLWxkYXAxEzAR -BgNVBAsMCnNsYXBkLXRlc3QxDzANBgNVBAMMBmNsaWVudDCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAMbG9swRtqFWIrg/pqhZSRyIh/V0RhMG5TwzV5KF -ERRndi2r957jbmldHLHEOsBrzKBGOqzu1q7CGAAo249EbLDuHJnUq7i9A5vBf2vP -IdDoWPrTpTgcdth8BVvQAXs7MtuE0feKwJG01yYRRJgbU2R+f3NCRXinPYokzh4+ -3gbIYlXQ2FFR0d3CCxaK5G3YQhE/Atd2R//iy+bYRqEAlrH5Ixplh7+WD3D1IHGX -oNNoX2UWmg+rN5b4qdcKYTx76pJZ5rwSOTMhFdrgon8SdQw/Z9Qv4l7ZqAYXZh8I -h31i8tNBXxgO/sibVKnSxshQOhZfXgYNRtnxwGvB87IjRN8CAwEAAaN4MHYwDAYD -VR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH -AwIwHQYDVR0OBBYEFGdjOPS0vPNrvHQOfCfJu8LMWKwWMB8GA1UdIwQYMBaAFDsf -MvT+V9FvSZFV8iTxCmY7pe7UMA0GCSqGSIb3DQEBCwUAA4IBAQB2JEJrM0/WWQdI -WwScPNM/Y4B1TXjX1YWxd4Exo5HLyaOMDgAoCHRxbPyDjIDsHOjug+B/STvzQjNa -H2gMpUFCzr93KQfyGKeBF9d2RwTZit3oWiYm6qR2cOHx+uHbvPIksjeoWC/jZol3 -AlWH7zwfZs5OhrNMV0OGf0yrWjPdyuMvO6+0Q1pTi+AS2ufAE3ayaNUU+BoHzoqH -XJG9NdeDxiqk4JJQAbnC+mkGXIqA7pwk+Ulk41nBpmkpzreJIKl81p/fKtGkmCpt -e5NqUuOu3hrY8y7PAn66mvr0s7VumiMQcFNTMNWKMjUBUlhtnfWOu7l2vUEWiCb4 -085wA8hZ +BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwIBcNMTkwNDEyMTg1MjM4WhgPMzAxOTAz +MDExODUyMzhaMEkxCzAJBgNVBAYTAkRFMRQwEgYDVQQKDAtweXRob24tbGRhcDET +MBEGA1UECwwKc2xhcGQtdGVzdDEPMA0GA1UEAwwGY2xpZW50MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA47eTup0a5wFj5k9Cb0SwRzHJZZfdiFhfzuBh +HXCeX2wcT9wcStA7/umkXEe29qNH+n5P24DXqV37/zFw0HBZREm35lDRrIRZma9L +Iz62Dw9UdOk2bPv+0bDF58dXO/ZhRhEOMKswzgRykMXg4TSZPgXBNEAAbKhfAANl +wtv3SteEApYyiwvOJNc0xU54fHbjdngGpWj0BRTqI+AJ6J1J7MCeOcyMeVdgXRvq +KjuXcKP3BLpCoABC/E9iCfQqpZodrD2zKmAvHDg6umHLoDM58a3x1eSzU4X5L8DH +tuvr7+vK+uc2pTw8tDPnFDEzMgVWOEy933o65aGNrWDITySHJQIDAQABo3gwdjAM +BgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEF +BQcDAjAdBgNVHQ4EFgQUT+c1x8jBAcN8U4a5v66L1kWieCAwHwYDVR0jBBgwFoAU +vXjVSvGQlsXo7GZJI0cDXyZzhrIwDQYJKoZIhvcNAQELBQADggEBAByQX88YSJVN +ndOObdFpGR57Px9IfMgNL8RTD4kj9L7qtHrG3cwYD+c06izUBw1leOggQD827ywA +MWnmIEhlvlcDDmn/uYNZmX1NhpgUW445JTqobVHcRaUPzfN6/VWvX1V1IAP1SnVq +eS92hPZOPR1ZRZqxaldvFnZ2+N9ultUlJzRLIdjJmjZVRaBDFkNokzevgYkG0VYb +ng9iQK08TPXvbKKkf/L6eJwNwBnxEOjYzwNnPC1N811nXEGnT9bFDv8sBN0ju4VE +jiWsFaOC+qRP+h2H8FjcrlMFuYHoy+UMrKV0aAP5IqBFtmJY4JjZjFSkIgN6NxLr +fbGtRWCOet8= -----END CERTIFICATE----- diff --git a/Lib/slapdtest/certs/gencerts.sh b/Lib/slapdtest/certs/gencerts.sh index 7a971a3a..8a99db58 100755 --- a/Lib/slapdtest/certs/gencerts.sh +++ b/Lib/slapdtest/certs/gencerts.sh @@ -29,7 +29,7 @@ openssl ca -selfsign \ -in $CATMPDIR/ca.csr \ -out $CAOUTDIR/ca.pem \ -extensions ca_ext \ - -days 3563 \ + -days 356300 \ -batch # server cert diff --git a/Lib/slapdtest/certs/server.key b/Lib/slapdtest/certs/server.key index a48ee567..a8916701 100644 --- a/Lib/slapdtest/certs/server.key +++ b/Lib/slapdtest/certs/server.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDAgcI7Pj89Aw4r -rb+N8j3t1ynJgXRhQNxbxQcQmUCi8AtpGKNXu+aM9u2HxZ677ALfhsEivtQA5QKz -Ll5G2G2IQa7uzgIco73OL/kMZIJt7sKfnvfACtSoOlD0IyOzVEEu0AVA7hMHb6Ul -I0mCNfdk2FWFbc1nUmIAEBhIODbFoNW+Rc6lz94NsUqArDayvWpAsXkUbubQikpi -KNX1OpOOC9GUbEhwI/G90ZnUg9STk/2oxsvwLlYdOhGhpyfl53tTu7eLMBriMxFl -UTvkY1GUgPj0fA/giWtCerGOyeKu1hFlbS4LjborsfrlyYPwfwTg3YL4hVnt9fF0 -rpjzY1mlAgMBAAECggEAJY6rSEeiqtKXxynEv3rNXkOmIWwiOn8e/sB32mMr2x4d -+8kUxR8hocrjGKQTjfJDtTxjHdZBIlOLrU2UkxnSdMzrxidm/hNsCngNjL9nOu9k -BSRMjakPSCrodFkOtAPyG6H2BG7uQ3siqxYxVzgUJhaWyMtdUZUfDYgWVLCy7udU -5ML/OTOi7virueMmshjXoyrDug9OpiEMKiLu3ndAaDk/26m05ePAXB6TjW8SFw1B -qn7cITSG0G5MZ9pOw0KwT9irY1SdppBHVWIg7dkYWRCni0BPCFewastU+GVKH5PJ -+dYSvafhkEGD1bBu484KN9yX1BcHV41ZKR8pGgMM2QKBgQD3/0R2vZsTxoO1CHNI -IT7nBnuPIOP45iTFm/SNRY7e4dhQBy6HM6JD3Sr6Iksm8jRoboz+tnAso6l6QHRS -842uqBiOHdnka2RslDmrEun1lJv1MWuPM8JN0o8pYjVG/IRtaAFnYSEk72UoNy2h -bHC4OGFNwMbAadVm7DK5OiMfXwKBgQDGuBRxz7jkVZoMbbaeIqmGZAIejWkJweDZ -AK+txM+6Sg+Li14t190N3Xf6tyyidKhUAEWaINzLjZB+luxNaDXtxqWzLYHCwQKA -qfrjWVeZOS1clLya7jwl1jJqBtBiGKHv9eRL21hgX/9gX3odxqFMvX3vm6L7F1q1 -5CNApW0ZewKBgGO8qNcsWBLy8oM7G8n1fOvCwqyEaMrwG/fRSeALCnN+1tUQnljH -nkm2yBMC+cB3Bja9xzylOKXrSDyfcWjvBJsqhX2aacggnKnCTxMLL0aR9sr8jipw -gYN03Bijo5Oh+MxbWL0v5fmJweATmOljyE1+dzui/QvjRGz5L0kpJXj3AoGBAIa4 -3+t1B4WN312TuB4no8Tf4mvyNQcPcS/Nfk0RxD8o3Lcfal8sHMq8ng3Ux6bv7frd -IFLo+qfpts+L5HJqNz2X0ljSfkmZ7udp1hTySigwEmfU0rU61H5WZGFrczU+O/Ni -Qj+HWrgj/Q/KSxEKy+oqAcpDOtB+Odpc6+V1Aa0nAoGBAItWHP9UjTNFqOfyjZhG -qaUiZd1S2KyRR0l/lVcn+rJ46Yg5i+lMGwHMF1xPyWH4ELz+QCUX3doOI4yB2ikg -XXFcc8/bqgaR4AfOvP98T86s7+f33kaAKZsgyAFB2cjo+fz8ArTz+GjPeHbiOPaR -Ra7+BVwl9GE0+bCdirq+99GO +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCsBk0ml3ERFJyg +I6ujIJYERVU4doTZZd4r4z/LOef0hyiYiIQAc9wetaoZpM+bl4Eherxy9SBaCBwR +zefbaYQz2f2hdEDb+sISOiTke1eiF2ugYNlS55Wk1KnCnORE9bjcSNLPsscoUSzE +2bnBSoUwdiVK18YOCZR6GTeC8eA3ekvlR+9g+FBOgQ9+StXPDdq+iIAGXZREJIua +munErtTOw85De4YFCnzGw3UeCITDD4wFmI2IWphRFwWPsSDwUJfATA8S+7Rm4vwr +Qj726gUDlicTzPXKhJjXjj6XL7xXHfpQwMPkBCrxesKceHMJ+mrRsuuqHciuixRi +g94mILElAgMBAAECggEADG5oJOHMye8zYl8xiBhSvvxDrFDkSNGTvJgvhAArQwCB +boRvBZlZzt5R7Ih8eEH6kvDLrYMJU3hCjwbSOojlhNm7+m7sQPleDPMmt1wyeQQ4 +Qt681cDmj4LOwcGUvWcEdObOVTQWMFOtaIxTYCSCe34OM9pj9Z+7mxc3a78O9PND +Ib/CwcTA1OyoupzkKirqkdLXwK3x2aT/1TMaPX94taHB51cxXc7AglL9QnuCkuaG +krqrexy3rGimzsP3OwQGEUjWKcZVSSPT8/k1pPE9hRgOqBy05BfkAzlebdvc3GO5 +AbZk0NX2sfVHl4dTEXs/hTBCTQ3XmaltumQ9MdL+AQKBgQDg2I5QxBA2UHb8vCtK +f31kfG6YQc4MkoslrrMrtJjZqDYaLZPS1ARPSfYRqcc+7GDreuLmw39f8ZECd+2W +BYUqzZv9g13R9DY99g0/sINnZGsESwfIdLNNlHvVx2UrD5ybCj4vLhuPsVV7XlWs +cpl+rcuBVpqy8UIXifQ/Z3xLvwKBgQDD3CLjuC0mcTO2sIWqEHqVkc8CY2NJA2Qh +C78fwpaCqJUUdWnS69QbRGWgkFJL+oO8lQVQ1bXhZLHyQmy7Z5d5olCH6AW4GRnf +hBAnKJ+QTm9B6QVWzjUuHuOeCukfiTQbha14pOS9ar3X2QFWjDnzCRrnAxJmoY3H +BJATLHhMGwKBgQDSxAy7xt4Pm+O9y8Gk5tcq771X+i9k96V54EZRzMuPFDAK3/h2 +o4marZD9Q7Hi2P+NHTc+67klvbKZpsPOYkRPOEdmH9M9cPe7oz8OGa9DpwzuDEsy +a7p8GZjvbyb1c3/wkWxzG3x4eNnReD9FFHOwHMfr6LvAy4iRuh57pM0NzwKBgDY3 +1DixnV4M7EHgb7/6O9T3vhRtKujlVWyIcen61etpe4tkTV0kB11c+70M9pstyBYG +MqiD4It6coAbvznJnXcAZcaZhivGVxE237nXVwR9kfLu7JlxD+uqhVwUrSAbvR75 +TGIfU2rUB6We3u30d349wQK+KPPcOQEk1DValBqNAoGBAKfXOXgFBkIVW79fOkup +aIZXdEmU3Up61Oo0KDbxsg4l73NnnvuEnNMBTx3nT3KCVIAcQL9MNpLX/Z0HjOn1 +aiWVtTNq2OFL0V0HueBhbkFiWp551jTS7LjndCYHpUB/B8/wXP0kxHUm8HrQrRvK +DhV3zcxsXts1INidXjzzOkPi -----END PRIVATE KEY----- diff --git a/Lib/slapdtest/certs/server.pem b/Lib/slapdtest/certs/server.pem index 7e750596..25ba06c0 100644 --- a/Lib/slapdtest/certs/server.pem +++ b/Lib/slapdtest/certs/server.pem @@ -5,31 +5,31 @@ Certificate: Signature Algorithm: sha256WithRSAEncryption Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA Validity - Not Before: Dec 2 11:57:48 2017 GMT - Not After : Dec 2 11:57:48 2027 GMT + Not Before: Apr 12 18:52:38 2019 GMT + Not After : Mar 1 18:52:38 3019 GMT Subject: C=DE, O=python-ldap, OU=slapd-test, CN=server cert for localhost Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: - 00:c0:81:c2:3b:3e:3f:3d:03:0e:2b:ad:bf:8d:f2: - 3d:ed:d7:29:c9:81:74:61:40:dc:5b:c5:07:10:99: - 40:a2:f0:0b:69:18:a3:57:bb:e6:8c:f6:ed:87:c5: - 9e:bb:ec:02:df:86:c1:22:be:d4:00:e5:02:b3:2e: - 5e:46:d8:6d:88:41:ae:ee:ce:02:1c:a3:bd:ce:2f: - f9:0c:64:82:6d:ee:c2:9f:9e:f7:c0:0a:d4:a8:3a: - 50:f4:23:23:b3:54:41:2e:d0:05:40:ee:13:07:6f: - a5:25:23:49:82:35:f7:64:d8:55:85:6d:cd:67:52: - 62:00:10:18:48:38:36:c5:a0:d5:be:45:ce:a5:cf: - de:0d:b1:4a:80:ac:36:b2:bd:6a:40:b1:79:14:6e: - e6:d0:8a:4a:62:28:d5:f5:3a:93:8e:0b:d1:94:6c: - 48:70:23:f1:bd:d1:99:d4:83:d4:93:93:fd:a8:c6: - cb:f0:2e:56:1d:3a:11:a1:a7:27:e5:e7:7b:53:bb: - b7:8b:30:1a:e2:33:11:65:51:3b:e4:63:51:94:80: - f8:f4:7c:0f:e0:89:6b:42:7a:b1:8e:c9:e2:ae:d6: - 11:65:6d:2e:0b:8d:ba:2b:b1:fa:e5:c9:83:f0:7f: - 04:e0:dd:82:f8:85:59:ed:f5:f1:74:ae:98:f3:63: - 59:a5 + 00:ac:06:4d:26:97:71:11:14:9c:a0:23:ab:a3:20: + 96:04:45:55:38:76:84:d9:65:de:2b:e3:3f:cb:39: + e7:f4:87:28:98:88:84:00:73:dc:1e:b5:aa:19:a4: + cf:9b:97:81:21:7a:bc:72:f5:20:5a:08:1c:11:cd: + e7:db:69:84:33:d9:fd:a1:74:40:db:fa:c2:12:3a: + 24:e4:7b:57:a2:17:6b:a0:60:d9:52:e7:95:a4:d4: + a9:c2:9c:e4:44:f5:b8:dc:48:d2:cf:b2:c7:28:51: + 2c:c4:d9:b9:c1:4a:85:30:76:25:4a:d7:c6:0e:09: + 94:7a:19:37:82:f1:e0:37:7a:4b:e5:47:ef:60:f8: + 50:4e:81:0f:7e:4a:d5:cf:0d:da:be:88:80:06:5d: + 94:44:24:8b:9a:9a:e9:c4:ae:d4:ce:c3:ce:43:7b: + 86:05:0a:7c:c6:c3:75:1e:08:84:c3:0f:8c:05:98: + 8d:88:5a:98:51:17:05:8f:b1:20:f0:50:97:c0:4c: + 0f:12:fb:b4:66:e2:fc:2b:42:3e:f6:ea:05:03:96: + 27:13:cc:f5:ca:84:98:d7:8e:3e:97:2f:bc:57:1d: + fa:50:c0:c3:e4:04:2a:f1:7a:c2:9c:78:73:09:fa: + 6a:d1:b2:eb:aa:1d:c8:ae:8b:14:62:83:de:26:20: + b1:25 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: critical @@ -39,48 +39,48 @@ Certificate: X509v3 Extended Key Usage: critical TLS Web Server Authentication X509v3 Subject Key Identifier: - 1B:78:45:40:0D:50:8A:8B:3B:C1:0A:F8:3F:7A:48:7B:A6:3C:28:09 + 08:D1:86:1B:82:0A:4F:71:31:E4:F5:31:23:CC:67:3B:FA:84:3B:A0 X509v3 Authority Key Identifier: - keyid:3B:1F:32:F4:FE:57:D1:6F:49:91:55:F2:24:F1:0A:66:3B:A5:EE:D4 + keyid:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2 X509v3 Subject Alternative Name: DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1 Signature Algorithm: sha256WithRSAEncryption - ad:08:3f:7d:b1:09:a1:a5:6c:c3:58:80:1d:e5:33:a5:bb:c0: - 33:39:95:aa:88:ee:c4:8e:38:3b:59:a7:0e:39:74:6c:fe:11: - 33:5e:fa:50:cb:20:4b:67:b7:c9:5e:96:a7:9e:d8:47:46:e1: - ab:fe:5d:8b:9a:2d:1a:1b:43:08:f9:93:0f:2a:e3:ce:83:4a: - 94:cd:02:f0:8e:25:f2:41:0d:55:10:f5:4c:5b:39:8b:77:5e: - ab:78:16:64:a1:48:d5:e1:f6:69:9a:0f:d8:30:a6:cc:92:4d: - 81:df:46:74:ab:cf:1d:b7:d4:01:b9:6d:d5:f4:14:b8:d5:54: - 84:79:11:42:69:55:7f:74:ce:01:96:2f:3f:51:23:b3:11:fb: - 72:dc:4c:b9:a3:89:ef:31:e4:c0:49:06:fa:8d:09:71:e1:c1: - 74:a9:ed:f8:96:87:67:16:b5:5d:16:5d:59:70:ff:1c:b5:a1: - 6c:d2:22:11:3a:0e:6f:76:9b:69:cb:f3:85:a7:79:ad:53:f5: - 34:e8:87:cc:dd:09:51:25:e0:28:ee:79:a0:a3:dc:0a:dd:f0: - 1b:e3:c9:5f:14:d3:95:f5:12:4d:23:95:45:2c:3c:32:94:ad: - ce:1e:a0:5f:e6:e8:28:c6:f9:c7:fb:57:06:ad:0b:eb:86:ca: - 0e:d2:a8:67 + 88:60:af:be:11:c4:aa:dc:9b:f1:e7:14:da:20:aa:6f:2f:06: + ae:38:b2:7c:ac:90:81:22:51:7e:cb:26:15:6e:fe:67:98:c1: + 0d:dc:aa:39:98:2b:d2:cc:3c:ff:1a:92:2f:56:0a:a9:6e:d8: + 9a:3d:c5:4d:6f:cc:91:2e:e3:4e:bf:22:ab:cb:92:1a:a0:8f: + 43:cd:82:bc:48:55:c4:95:cf:10:6b:6a:31:19:92:7d:e0:06: + 05:6f:0b:33:e7:2a:37:42:f9:ec:1b:29:99:e1:58:0c:01:a7: + c3:8b:58:71:21:9f:61:8c:a7:fb:b6:7e:32:8b:a9:4e:c7:1f: + f6:46:e8:dd:ac:a6:4c:53:f8:4d:93:e4:ec:73:ab:0b:be:98: + c5:78:c4:92:c0:4c:78:47:52:2f:93:07:67:20:a4:5a:7f:59: + 7e:4f:48:53:20:0d:37:bb:06:f8:44:42:64:b4:94:15:43:d1: + 4c:51:f3:97:1d:2d:cd:db:b9:bb:1a:69:10:89:7d:ae:1d:0d: + 94:78:45:29:cd:c4:42:67:67:96:05:bf:da:aa:23:65:7b:04: + ff:b7:ac:9d:ee:0b:e7:0f:c1:c5:0b:48:fe:0f:d6:3f:d8:b4: + 77:12:bb:f5:91:4f:43:e6:01:3f:a4:c0:ea:8c:c6:68:99:8e: + 49:e8:c4:8b -----BEGIN CERTIFICATE----- -MIID1TCCAr2gAwIBAgIBAjANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU +MIID1zCCAr+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV -BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwHhcNMTcxMjAyMTE1NzQ4WhcNMjcxMjAy -MTE1NzQ4WjBcMQswCQYDVQQGEwJERTEUMBIGA1UECgwLcHl0aG9uLWxkYXAxEzAR -BgNVBAsMCnNsYXBkLXRlc3QxIjAgBgNVBAMMGXNlcnZlciBjZXJ0IGZvciBsb2Nh -bGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAgcI7Pj89Aw4r -rb+N8j3t1ynJgXRhQNxbxQcQmUCi8AtpGKNXu+aM9u2HxZ677ALfhsEivtQA5QKz -Ll5G2G2IQa7uzgIco73OL/kMZIJt7sKfnvfACtSoOlD0IyOzVEEu0AVA7hMHb6Ul -I0mCNfdk2FWFbc1nUmIAEBhIODbFoNW+Rc6lz94NsUqArDayvWpAsXkUbubQikpi -KNX1OpOOC9GUbEhwI/G90ZnUg9STk/2oxsvwLlYdOhGhpyfl53tTu7eLMBriMxFl -UTvkY1GUgPj0fA/giWtCerGOyeKu1hFlbS4LjborsfrlyYPwfwTg3YL4hVnt9fF0 -rpjzY1mlAgMBAAGjgacwgaQwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAw -FgYDVR0lAQH/BAwwCgYIKwYBBQUHAwEwHQYDVR0OBBYEFBt4RUANUIqLO8EK+D96 -SHumPCgJMB8GA1UdIwQYMBaAFDsfMvT+V9FvSZFV8iTxCmY7pe7UMCwGA1UdEQQl -MCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0B -AQsFAAOCAQEArQg/fbEJoaVsw1iAHeUzpbvAMzmVqojuxI44O1mnDjl0bP4RM176 -UMsgS2e3yV6Wp57YR0bhq/5di5otGhtDCPmTDyrjzoNKlM0C8I4l8kENVRD1TFs5 -i3deq3gWZKFI1eH2aZoP2DCmzJJNgd9GdKvPHbfUAblt1fQUuNVUhHkRQmlVf3TO -AZYvP1EjsxH7ctxMuaOJ7zHkwEkG+o0JceHBdKnt+JaHZxa1XRZdWXD/HLWhbNIi -EToOb3abacvzhad5rVP1NOiHzN0JUSXgKO55oKPcCt3wG+PJXxTTlfUSTSOVRSw8 -MpStzh6gX+boKMb5x/tXBq0L64bKDtKoZw== +BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwIBcNMTkwNDEyMTg1MjM4WhgPMzAxOTAz +MDExODUyMzhaMFwxCzAJBgNVBAYTAkRFMRQwEgYDVQQKDAtweXRob24tbGRhcDET +MBEGA1UECwwKc2xhcGQtdGVzdDEiMCAGA1UEAwwZc2VydmVyIGNlcnQgZm9yIGxv +Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKwGTSaXcREU +nKAjq6MglgRFVTh2hNll3ivjP8s55/SHKJiIhABz3B61qhmkz5uXgSF6vHL1IFoI +HBHN59tphDPZ/aF0QNv6whI6JOR7V6IXa6Bg2VLnlaTUqcKc5ET1uNxI0s+yxyhR +LMTZucFKhTB2JUrXxg4JlHoZN4Lx4Dd6S+VH72D4UE6BD35K1c8N2r6IgAZdlEQk +i5qa6cSu1M7DzkN7hgUKfMbDdR4IhMMPjAWYjYhamFEXBY+xIPBQl8BMDxL7tGbi +/CtCPvbqBQOWJxPM9cqEmNeOPpcvvFcd+lDAw+QEKvF6wpx4cwn6atGy66odyK6L +FGKD3iYgsSUCAwEAAaOBpzCBpDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIF +oDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUCNGGG4IKT3Ex5PUx +I8xnO/qEO6AwHwYDVR0jBBgwFoAUvXjVSvGQlsXo7GZJI0cDXyZzhrIwLAYDVR0R +BCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3 +DQEBCwUAA4IBAQCIYK++EcSq3Jvx5xTaIKpvLwauOLJ8rJCBIlF+yyYVbv5nmMEN +3Ko5mCvSzDz/GpIvVgqpbtiaPcVNb8yRLuNOvyKry5IaoI9DzYK8SFXElc8Qa2ox +GZJ94AYFbwsz5yo3QvnsGymZ4VgMAafDi1hxIZ9hjKf7tn4yi6lOxx/2RujdrKZM +U/hNk+Tsc6sLvpjFeMSSwEx4R1IvkwdnIKRaf1l+T0hTIA03uwb4REJktJQVQ9FM +UfOXHS3N27m7GmkQiX2uHQ2UeEUpzcRCZ2eWBb/aqiNlewT/t6yd7gvnD8HFC0j+ +D9Y/2LR3Erv1kU9D5gE/pMDqjMZomY5J6MSL -----END CERTIFICATE----- diff --git a/MANIFEST.in b/MANIFEST.in index 687d2b0c..bedea8d6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include MANIFEST.in Makefile CHANGES INSTALL LICENCE README TODO -include tox.ini .coveragerc +include tox.ini include Modules/*.c Modules/*.h recursive-include Build *.cfg* recursive-include Lib *.py diff --git a/Makefile b/Makefile index 2d3293e6..da23b374 100644 --- a/Makefile +++ b/Makefile @@ -5,17 +5,21 @@ LCOV_REPORT_OPTIONS=--show-details -no-branch-coverage \ --title "python-ldap LCOV report" SCAN_REPORT=build/scan_report PYTHON_SUPP=/usr/share/doc/python3-devel/valgrind-python.supp -AUTOPEP8_OPTS=--aggressive + .NOTPARALLEL: .PHONY: all all: +Modules/constants_generated.h: Lib/ldap/constants.py + $(PYTHON) $^ > $@ + indent Modules/constants_generated.h + rm -f Modules/constants_generated.h~ + .PHONY: clean clean: rm -rf build dist *.egg-info .tox MANIFEST - rm -f .coverage .coverage.* find . \( -name '*.py[co]' -or -name '*.so*' -or -name '*.dylib' \) \ -delete find . -depth -name __pycache__ -exec rm -rf {} \; @@ -30,7 +34,7 @@ lcov-clean: if [ -d build ]; then find build -name '*.gc??' -delete; fi lcov-coverage: - WITH_GCOV=1 tox -e py27,py36 + WITH_GCOV=1 tox -e py36 $(LCOV_INFO): build lcov --capture --directory build --output-file $(LCOV_INFO) @@ -80,13 +84,16 @@ valgrind: build $(PYTHON_SUPP) fi # Code autoformatter -.PHONY: autoformat indent autopep8 -autoformat: indent autopep8 +.PHONY: autoformat indent black black-check +autoformat: indent black indent: - indent Modules/*.c Modules/*.h + indent Modules/*.c + indent -npsl Modules/pythonldap.h rm -f Modules/*.c~ Modules/*.h~ -autopep8: - $(PYTHON) -m autopep8 -r -i -j0 $(AUTOPEP8_OPTS) \ - Demo Lib Tests setup.py +black: + $(PYTHON) -m black $(CURDIR) + +black-check: + $(PYTHON) -m black $(CURDIR) --check diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c index bc26727e..f96a6068 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -1,16 +1,10 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" +#include "pythonldap.h" #include "patchlevel.h" #include #include -#include "constants.h" -#include "LDAPObject.h" -#include "ldapcontrol.h" -#include "message.h" -#include "berval.h" -#include "options.h" #ifdef HAVE_SASL #include @@ -126,6 +120,7 @@ Tuple_to_LDAPMod(PyObject *tup, int no_op) } lm = PyMem_NEW(LDAPMod, 1); + if (lm == NULL) goto nomem; @@ -236,6 +231,7 @@ List_to_LDAPMods(PyObject *list, int no_op) } lms = PyMem_NEW(LDAPMod *, len + 1); + if (lms == NULL) goto nomem; @@ -274,13 +270,8 @@ attrs_from_List(PyObject *attrlist, char ***attrsp) if (attrlist == Py_None) { /* None means a NULL attrlist */ -#if PY_MAJOR_VERSION == 2 - } - else if (PyBytes_Check(attrlist)) { -#else } else if (PyUnicode_Check(attrlist)) { -#endif /* caught by John Benninghoff */ LDAPerror_TypeError ("attrs_from_List(): expected *list* of strings, not a string", @@ -291,17 +282,16 @@ attrs_from_List(PyObject *attrlist, char ***attrsp) PyObject *item = NULL; Py_ssize_t i, len, strlen; -#if PY_MAJOR_VERSION >= 3 const char *str; -#else - char *str; -#endif seq = PySequence_Fast(attrlist, "expected list of strings or None"); if (seq == NULL) goto error; - len = PySequence_Length(attrlist); + len = PySequence_Size(seq); + if (len == -1) { + goto error; + } attrs = PyMem_NEW(char *, len + 1); @@ -313,29 +303,17 @@ attrs_from_List(PyObject *attrlist, char ***attrsp) item = PySequence_Fast_GET_ITEM(seq, i); if (item == NULL) goto error; -#if PY_MAJOR_VERSION == 2 - /* Encoded in Python to UTF-8 */ - if (!PyBytes_Check(item)) { - LDAPerror_TypeError - ("attrs_from_List(): expected bytes in list", item); - goto error; - } - if (PyBytes_AsStringAndSize(item, &str, &strlen) == -1) { - goto error; - } -#else if (!PyUnicode_Check(item)) { LDAPerror_TypeError ("attrs_from_List(): expected string in list", item); goto error; } str = PyUnicode_AsUTF8AndSize(item, &strlen); -#endif /* Make a copy. PyBytes_AsString* / PyUnicode_AsUTF8* return * internal values that must be treated like const char. Python * 3.7 actually returns a const char. */ - attrs[i] = (char *)PyMem_NEW(char *, strlen + 1); + attrs[i] = (char *)PyMem_NEW(char, strlen + 1); if (attrs[i] == NULL) goto nomem; @@ -415,7 +393,7 @@ l_ldap_unbind_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_unbind_ext"); + return LDAPerror(self->ldap); self->valid = 0; Py_INCREF(Py_None); @@ -461,7 +439,7 @@ l_ldap_abandon_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_abandon_ext"); + return LDAPerror(self->ldap); Py_INCREF(Py_None); return Py_None; @@ -517,9 +495,9 @@ l_ldap_add_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_add_ext"); + return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_simple_bind */ @@ -568,9 +546,9 @@ l_ldap_simple_bind(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_simple_bind"); + return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } #ifdef HAVE_SASL @@ -727,8 +705,8 @@ l_ldap_sasl_bind_s(LDAPObject *self, PyObject *args) servercred->bv_len); } else if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "l_ldap_sasl_bind_s"); - return PyInt_FromLong(ldaperror); + return LDAPerror(self->ldap); + return PyLong_FromLong(ldaperror); } static PyObject * @@ -755,15 +733,9 @@ l_ldap_sasl_interactive_bind_s(LDAPObject *self, PyObject *args) * unsigned int, we need to use the "I" flag if we're running Python 2.3+ and a * "i" otherwise. */ -#if (PY_MAJOR_VERSION == 2) && (PY_MINOR_VERSION < 3) - if (!PyArg_ParseTuple - (args, "sOOOi:sasl_interactive_bind_s", &who, &SASLObject, - &serverctrls, &clientctrls, &sasl_flags)) -#else if (!PyArg_ParseTuple (args, "sOOOI:sasl_interactive_bind_s", &who, &SASLObject, &serverctrls, &clientctrls, &sasl_flags)) -#endif return NULL; if (not_valid(self)) @@ -806,8 +778,8 @@ l_ldap_sasl_interactive_bind_s(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (msgid != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_sasl_interactive_bind_s"); - return PyInt_FromLong(msgid); + return LDAPerror(self->ldap); + return PyLong_FromLong(msgid); } #endif @@ -854,9 +826,9 @@ l_ldap_cancel(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_cancel"); + return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } #endif @@ -908,9 +880,9 @@ l_ldap_compare_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_compare_ext"); + return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_delete_ext */ @@ -954,9 +926,9 @@ l_ldap_delete_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_delete_ext"); + return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_modify_ext */ @@ -1011,9 +983,9 @@ l_ldap_modify_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_modify_ext"); + return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_rename */ @@ -1061,9 +1033,9 @@ l_ldap_rename(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_rename"); + return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_result4 */ @@ -1081,12 +1053,11 @@ l_ldap_result4(LDAPObject *self, PyObject *args) struct timeval *tvp; int res_type; LDAPMessage *msg = NULL; - PyObject *result_str, *retval, *pmsg, *pyctrls = 0; + PyObject *retval, *pmsg, *pyctrls = 0; int res_msgid = 0; char *retoid = 0; PyObject *valuestr = NULL; int result = LDAP_SUCCESS; - char **refs = NULL; LDAPControl **serverctrls = 0; if (!PyArg_ParseTuple @@ -1109,7 +1080,7 @@ l_ldap_result4(LDAPObject *self, PyObject *args) LDAP_END_ALLOW_THREADS(self); if (res_type < 0) /* LDAP or system error */ - return LDAPerror(self->ldap, "ldap_result4"); + return LDAPerror(self->ldap); if (res_type == 0) { /* Polls return (None, None, None, None); timeouts raise an exception */ @@ -1157,23 +1128,15 @@ l_ldap_result4(LDAPObject *self, PyObject *args) } LDAP_BEGIN_ALLOW_THREADS(self); - rc = ldap_parse_result(self->ldap, msg, &result, NULL, NULL, &refs, + rc = ldap_parse_result(self->ldap, msg, &result, NULL, NULL, NULL, &serverctrls, 0); LDAP_END_ALLOW_THREADS(self); } if (result != LDAP_SUCCESS) { /* result error */ - char *e, err[1024]; - - if (result == LDAP_REFERRAL && refs && refs[0]) { - snprintf(err, sizeof(err), "Referral:\n%s", refs[0]); - e = err; - } - else - e = "ldap_parse_result"; - ldap_msgfree(msg); + ldap_controls_free(serverctrls); Py_XDECREF(valuestr); - return LDAPerror(self->ldap, e); + return LDAPraise_for_message(self->ldap, msg); } if (!(pyctrls = LDAPControls_to_List(serverctrls))) { @@ -1182,36 +1145,29 @@ l_ldap_result4(LDAPObject *self, PyObject *args) LDAP_BEGIN_ALLOW_THREADS(self); ldap_set_option(self->ldap, LDAP_OPT_ERROR_NUMBER, &err); LDAP_END_ALLOW_THREADS(self); + ldap_controls_free(serverctrls); ldap_msgfree(msg); Py_XDECREF(valuestr); - return LDAPerror(self->ldap, "LDAPControls_to_List"); + return LDAPerror(self->ldap); } ldap_controls_free(serverctrls); pmsg = LDAPmessage_to_python(self->ldap, msg, add_ctrls, add_intermediates); - if (res_type == 0) { - result_str = Py_None; - Py_INCREF(Py_None); - } - else { - result_str = PyInt_FromLong(res_type); - } - if (pmsg == NULL) { retval = NULL; } else { /* s handles NULL, but O does not */ if (add_extop) { - retval = Py_BuildValue("(OOiOsO)", result_str, pmsg, res_msgid, + retval = Py_BuildValue("(iOiOsO)", res_type, pmsg, res_msgid, pyctrls, retoid, valuestr ? valuestr : Py_None); } else { retval = - Py_BuildValue("(OOiO)", result_str, pmsg, res_msgid, pyctrls); + Py_BuildValue("(iOiO)", res_type, pmsg, res_msgid, pyctrls); } if (pmsg != Py_None) { @@ -1220,7 +1176,6 @@ l_ldap_result4(LDAPObject *self, PyObject *args) } Py_XDECREF(valuestr); Py_XDECREF(pyctrls); - Py_DECREF(result_str); return retval; } @@ -1294,9 +1249,9 @@ l_ldap_search_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_search_ext"); + return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_whoami_s (available since OpenLDAP 2.1.13) */ @@ -1341,7 +1296,7 @@ l_ldap_whoami_s(LDAPObject *self, PyObject *args) if (ldaperror != LDAP_SUCCESS) { ber_bvfree(bvalue); - return LDAPerror(self->ldap, "ldap_whoami_s"); + return LDAPerror(self->ldap); } result = LDAPberval_to_unicode_object(bvalue); @@ -1368,7 +1323,7 @@ l_ldap_start_tls_s(LDAPObject *self, PyObject *args) LDAP_END_ALLOW_THREADS(self); if (ldaperror != LDAP_SUCCESS) { ldap_set_option(self->ldap, LDAP_OPT_ERROR_NUMBER, &ldaperror); - return LDAPerror(self->ldap, "ldap_start_tls_s"); + return LDAPerror(self->ldap); } Py_INCREF(Py_None); @@ -1380,14 +1335,16 @@ l_ldap_start_tls_s(LDAPObject *self, PyObject *args) /* ldap_set_option */ static PyObject * -l_ldap_set_option(PyObject *self, PyObject *args) +l_ldap_set_option(LDAPObject *self, PyObject *args) { PyObject *value; int option; if (!PyArg_ParseTuple(args, "iO:set_option", &option, &value)) return NULL; - if (!LDAP_set_option((LDAPObject *)self, option, value)) + if (not_valid(self)) + return NULL; + if (!LDAP_set_option(self, option, value)) return NULL; Py_INCREF(Py_None); return Py_None; @@ -1396,13 +1353,15 @@ l_ldap_set_option(PyObject *self, PyObject *args) /* ldap_get_option */ static PyObject * -l_ldap_get_option(PyObject *self, PyObject *args) +l_ldap_get_option(LDAPObject *self, PyObject *args) { int option; if (!PyArg_ParseTuple(args, "i:get_option", &option)) return NULL; - return LDAP_get_option((LDAPObject *)self, option); + if (not_valid(self)) + return NULL; + return LDAP_get_option(self, option); } /* ldap_passwd */ @@ -1460,9 +1419,9 @@ l_ldap_passwd(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_passwd"); + return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_extended_operation */ @@ -1511,9 +1470,41 @@ l_ldap_extended_operation(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_extended_operation"); + return LDAPerror(self->ldap); + + return PyLong_FromLong(msgid); +} + +/* ldap_connect */ - return PyInt_FromLong(msgid); +static PyObject * +l_ldap_connect(LDAPObject *self, PyObject Py_UNUSED(args)) +{ +#if LDAP_VENDOR_VERSION >= 20500 + int ldaperror; + + if (ldap_version_info.ldapai_vendor_version < 20500) +#endif + { + PyErr_SetString(PyExc_NotImplementedError, + "loaded libldap doesn't support this feature"); + return NULL; + } + +#if LDAP_VENDOR_VERSION >= 20500 + if (not_valid(self)) + return NULL; + + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = ldap_connect(self->ldap); + LDAP_END_ALLOW_THREADS(self); + + if ( ldaperror != LDAP_SUCCESS ) + return LDAPerror(self->ldap); + + Py_INCREF(Py_None); + return Py_None; +#endif } /* methods */ @@ -1545,6 +1536,7 @@ static PyMethodDef methods[] = { {"cancel", (PyCFunction)l_ldap_cancel, METH_VARARGS}, #endif {"extop", (PyCFunction)l_ldap_extended_operation, METH_VARARGS}, + {"connect", (PyCFunction)l_ldap_connect, METH_NOARGS}, {NULL, NULL} }; diff --git a/Modules/LDAPObject.h b/Modules/LDAPObject.h deleted file mode 100644 index a456bce0..00000000 --- a/Modules/LDAPObject.h +++ /dev/null @@ -1,50 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_LDAPObject -#define __h_LDAPObject - -#include "common.h" - -#include "lber.h" -#include "ldap.h" -#if LDAP_API_VERSION < 2040 -#error Current python-ldap requires OpenLDAP 2.4.x -#endif - -#if PYTHON_API_VERSION < 1007 -typedef PyObject *_threadstate; -#else -typedef PyThreadState *_threadstate; -#endif - -typedef struct { - PyObject_HEAD LDAP *ldap; - _threadstate _save; /* for thread saving on referrals */ - int valid; -} LDAPObject; - -extern PyTypeObject LDAP_Type; - -#define LDAPObject_Check(v) (Py_TYPE(v) == &LDAP_Type) - -extern LDAPObject *newLDAPObject(LDAP *); - -/* macros to allow thread saving in the context of an LDAP connection */ - -#define LDAP_BEGIN_ALLOW_THREADS( l ) \ - { \ - LDAPObject *lo = (l); \ - if (lo->_save != NULL) \ - Py_FatalError( "saving thread twice?" ); \ - lo->_save = PyEval_SaveThread(); \ - } - -#define LDAP_END_ALLOW_THREADS( l ) \ - { \ - LDAPObject *lo = (l); \ - _threadstate _save = lo->_save; \ - lo->_save = NULL; \ - PyEval_RestoreThread( _save ); \ - } - -#endif /* __h_LDAPObject */ diff --git a/Modules/berval.c b/Modules/berval.c index 7435ee0a..39cc98a8 100644 --- a/Modules/berval.c +++ b/Modules/berval.c @@ -1,7 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "berval.h" +#include "pythonldap.h" /* * Copies out the data from a berval, and returns it as a new Python object, @@ -17,7 +16,7 @@ LDAPberval_to_object(const struct berval *bv) { PyObject *ret = NULL; - if (!bv) { + if (!bv || !bv->bv_val) { ret = Py_None; Py_INCREF(ret); } diff --git a/Modules/berval.h b/Modules/berval.h deleted file mode 100644 index 2aa9c977..00000000 --- a/Modules/berval.h +++ /dev/null @@ -1,12 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_berval -#define __h_berval - -#include "common.h" -#include "lber.h" - -PyObject *LDAPberval_to_object(const struct berval *bv); -PyObject *LDAPberval_to_unicode_object(const struct berval *bv); - -#endif /* __h_berval_ */ diff --git a/Modules/common.c b/Modules/common.c index 9d7001c0..4cfee744 100644 --- a/Modules/common.c +++ b/Modules/common.c @@ -1,7 +1,7 @@ /* Miscellaneous common routines * See https://www.python-ldap.org/ for details. */ -#include "common.h" +#include "pythonldap.h" /* dynamically add the methods into the module dictionary d */ diff --git a/Modules/common.h b/Modules/common.h deleted file mode 100644 index affa5f93..00000000 --- a/Modules/common.h +++ /dev/null @@ -1,39 +0,0 @@ -/* common utility macros - * See https://www.python-ldap.org/ for details. */ - -#ifndef __h_common -#define __h_common - -#define PY_SSIZE_T_CLEAN - -#include "Python.h" - -#if defined(HAVE_CONFIG_H) -#include "config.h" -#endif - -#if defined(MS_WINDOWS) -#include -#else /* unix */ -#include -#include -#include -#endif - -#include -#define streq( a, b ) \ - ( (*(a)==*(b)) && 0==strcmp(a,b) ) - -extern PyObject *LDAPerror_TypeError(const char *, PyObject *); - -void LDAPadd_methods(PyObject *d, PyMethodDef *methods); - -#define PyNone_Check(o) ((o) == Py_None) - -/* Py2/3 compatibility */ -#if PY_VERSION_HEX >= 0x03000000 -/* In Python 3, alias PyInt to PyLong */ -#define PyInt_FromLong PyLong_FromLong -#endif - -#endif /* __h_common_ */ diff --git a/Modules/constants.c b/Modules/constants.c index f8da3736..64804e8d 100644 --- a/Modules/constants.c +++ b/Modules/constants.c @@ -1,10 +1,7 @@ /* constants defined for LDAP * See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "constants.h" -#include "lber.h" -#include "ldap.h" +#include "pythonldap.h" /* the base exception class */ @@ -32,7 +29,8 @@ static PyObject *errobjects[LDAP_ERROR_MAX - LDAP_ERROR_MIN + 1]; PyObject * LDAPerr(int errnum) { - if (errnum >= LDAP_ERROR_MIN && errnum <= LDAP_ERROR_MAX) { + if (errnum >= LDAP_ERROR_MIN && errnum <= LDAP_ERROR_MAX && + errobjects[errnum + LDAP_ERROR_OFFSET] != NULL) { PyErr_SetNone(errobjects[errnum + LDAP_ERROR_OFFSET]); } else { @@ -48,38 +46,82 @@ LDAPerr(int errnum) /* Convert an LDAP error into an informative python exception */ PyObject * -LDAPerror(LDAP *l, char *msg) +LDAPraise_for_message(LDAP *l, LDAPMessage *m) { if (l == NULL) { PyErr_SetFromErrno(LDAPexception_class); + ldap_msgfree(m); return NULL; } else { - int myerrno, errnum, opt_errnum; + int myerrno, errnum, opt_errnum, msgid = -1, msgtype = 0; PyObject *errobj; PyObject *info; PyObject *str; PyObject *pyerrno; - char *matched, *error; + PyObject *pyresult; + PyObject *pyctrls = NULL; + char *matched = NULL, *error = NULL, **refs = NULL; + LDAPControl **serverctrls = NULL; /* at first save errno for later use before it gets overwritten by another call */ myerrno = errno; - opt_errnum = ldap_get_option(l, LDAP_OPT_ERROR_NUMBER, &errnum); - if (opt_errnum != LDAP_OPT_SUCCESS) - errnum = opt_errnum; + if (m != NULL) { + msgid = ldap_msgid(m); + msgtype = ldap_msgtype(m); + ldap_parse_result(l, m, &errnum, &matched, &error, &refs, + &serverctrls, 1); + } - if (errnum == LDAP_NO_MEMORY) - return PyErr_NoMemory(); + if (msgtype <= 0) { + opt_errnum = ldap_get_option(l, LDAP_OPT_ERROR_NUMBER, &errnum); + if (opt_errnum != LDAP_OPT_SUCCESS) + errnum = opt_errnum; + + if (errnum == LDAP_NO_MEMORY) { + return PyErr_NoMemory(); + } + + ldap_get_option(l, LDAP_OPT_MATCHED_DN, &matched); + ldap_get_option(l, LDAP_OPT_ERROR_STRING, &error); + } - if (errnum >= LDAP_ERROR_MIN && errnum <= LDAP_ERROR_MAX) + if (errnum >= LDAP_ERROR_MIN && errnum <= LDAP_ERROR_MAX && + errobjects[errnum + LDAP_ERROR_OFFSET] != NULL) { errobj = errobjects[errnum + LDAP_ERROR_OFFSET]; - else + } + else { errobj = LDAPexception_class; + } info = PyDict_New(); - if (info == NULL) + if (info == NULL) { + ldap_memfree(matched); + ldap_memfree(error); + ldap_memvfree((void **)refs); + ldap_controls_free(serverctrls); return NULL; + } + + if (msgtype > 0) { + pyresult = PyLong_FromLong(msgtype); + if (pyresult) + PyDict_SetItemString(info, "msgtype", pyresult); + Py_XDECREF(pyresult); + } + + if (msgid >= 0) { + pyresult = PyLong_FromLong(msgid); + if (pyresult) + PyDict_SetItemString(info, "msgid", pyresult); + Py_XDECREF(pyresult); + } + + pyresult = PyLong_FromLong(errnum); + if (pyresult) + PyDict_SetItemString(info, "result", pyresult); + Py_XDECREF(pyresult); str = PyUnicode_FromString(ldap_err2string(errnum)); if (str) @@ -87,14 +129,27 @@ LDAPerror(LDAP *l, char *msg) Py_XDECREF(str); if (myerrno != 0) { - pyerrno = PyInt_FromLong(myerrno); + pyerrno = PyLong_FromLong(myerrno); if (pyerrno) PyDict_SetItemString(info, "errno", pyerrno); Py_XDECREF(pyerrno); } - if (ldap_get_option(l, LDAP_OPT_MATCHED_DN, &matched) >= 0 - && matched != NULL) { + if (!(pyctrls = LDAPControls_to_List(serverctrls))) { + int err = LDAP_NO_MEMORY; + + ldap_set_option(l, LDAP_OPT_ERROR_NUMBER, &err); + ldap_memfree(matched); + ldap_memfree(error); + ldap_memvfree((void **)refs); + ldap_controls_free(serverctrls); + return PyErr_NoMemory(); + } + ldap_controls_free(serverctrls); + PyDict_SetItemString(info, "ctrls", pyctrls); + Py_XDECREF(pyctrls); + + if (matched != NULL) { if (*matched != '\0') { str = PyUnicode_FromString(matched); if (str) @@ -104,33 +159,44 @@ LDAPerror(LDAP *l, char *msg) ldap_memfree(matched); } - if (errnum == LDAP_REFERRAL) { - str = PyUnicode_FromString(msg); + if (errnum == LDAP_REFERRAL && refs != NULL && refs[0] != NULL) { + /* Keep old behaviour, overshadow error message */ + char err[1024]; + + snprintf(err, sizeof(err), "Referral:\n%s", refs[0]); + str = PyUnicode_FromString(err); + PyDict_SetItemString(info, "info", str); + Py_XDECREF(str); + } + else if (error != NULL && *error != '\0') { + str = PyUnicode_FromString(error); if (str) PyDict_SetItemString(info, "info", str); Py_XDECREF(str); } - else if (ldap_get_option(l, LDAP_OPT_ERROR_STRING, &error) >= 0) { - if (error != NULL && *error != '\0') { - str = PyUnicode_FromString(error); - if (str) - PyDict_SetItemString(info, "info", str); - Py_XDECREF(str); - } - ldap_memfree(error); - } + PyErr_SetObject(errobj, info); Py_DECREF(info); + ldap_memvfree((void **)refs); + ldap_memfree(error); return NULL; } } +PyObject * +LDAPerror(LDAP *l) +{ + return LDAPraise_for_message(l, NULL); +} + /* initialise the module constants */ int LDAPinit_constants(PyObject *m) { - PyObject *exc; + PyObject *exc, *nobj; + struct ldap_apifeature_info info = { 1, "X_OPENLDAP_THREAD_SAFE", 0 }; + int thread_safe = 0; /* simple constants */ @@ -155,11 +221,31 @@ LDAPinit_constants(PyObject *m) return -1; Py_INCREF(LDAPexception_class); +#ifdef LDAP_API_FEATURE_X_OPENLDAP_THREAD_SAFE + if (ldap_get_option(NULL, LDAP_OPT_API_FEATURE_INFO, &info) == LDAP_SUCCESS) { + thread_safe = (info.ldapaif_version == 1); + } +#endif + if (PyModule_AddIntConstant(m, "LIBLDAP_R", thread_safe) != 0) + return -1; + + if (ldap_get_option(NULL, LDAP_OPT_API_INFO, &ldap_version_info) != LDAP_SUCCESS) { + PyErr_SetString(PyExc_ImportError, "unrecognised libldap version"); + return -1; + } + if (PyModule_AddIntConstant(m, "_VENDOR_VERSION_RUNTIME", + ldap_version_info.ldapai_vendor_version ) != 0) + return -1; + /* Generated constants -- see Lib/ldap/constants.py */ #define add_err(n) do { \ exc = PyErr_NewException("ldap." #n, LDAPexception_class, NULL); \ if (exc == NULL) return -1; \ + nobj = PyLong_FromLong(LDAP_##n); \ + if (nobj == NULL) return -1; \ + if (PyObject_SetAttrString(exc, "errnum", nobj) != 0) return -1; \ + Py_DECREF(nobj); \ errobjects[LDAP_##n+LDAP_ERROR_OFFSET] = exc; \ if (PyModule_AddObject(m, #n, exc) != 0) return -1; \ Py_INCREF(exc); \ diff --git a/Modules/constants.h b/Modules/constants.h deleted file mode 100644 index 8a390b5b..00000000 --- a/Modules/constants.h +++ /dev/null @@ -1,25 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_constants_ -#define __h_constants_ - -#include "common.h" -#include "lber.h" -#include "ldap.h" - -extern int LDAPinit_constants(PyObject *m); -extern PyObject *LDAPconstant(int); - -extern PyObject *LDAPexception_class; -extern PyObject *LDAPerror(LDAP *, char *msg); -PyObject *LDAPerr(int errnum); - -#ifndef LDAP_CONTROL_PAGE_OID -#define LDAP_CONTROL_PAGE_OID "1.2.840.113556.1.4.319" -#endif /* !LDAP_CONTROL_PAGE_OID */ - -#ifndef LDAP_CONTROL_VALUESRETURNFILTER -#define LDAP_CONTROL_VALUESRETURNFILTER "1.2.826.0.1.3344810.2.3" /* RFC 3876 */ -#endif /* !LDAP_CONTROL_VALUESRETURNFILTER */ - -#endif /* __h_constants_ */ diff --git a/Modules/constants_generated.h b/Modules/constants_generated.h index 455852ed..3e59f828 100644 --- a/Modules/constants_generated.h +++ b/Modules/constants_generated.h @@ -76,10 +76,12 @@ add_err(TOO_LATE); add_err(CANNOT_CANCEL); #endif + #if defined(LDAP_ASSERTION_FAILED) add_err(ASSERTION_FAILED); #endif + #if defined(LDAP_PROXIED_AUTHORIZATION_DENIED) add_err(PROXIED_AUTHORIZATION_DENIED); #endif @@ -171,6 +173,7 @@ add_int(OPT_TIMELIMIT); add_int(OPT_REFERRALS); #endif +add_int(OPT_RESULT_CODE); add_int(OPT_ERROR_NUMBER); add_int(OPT_RESTART); add_int(OPT_PROTOCOL_VERSION); @@ -186,12 +189,18 @@ add_int(OPT_DEBUG_LEVEL); add_int(OPT_TIMEOUT); add_int(OPT_REFHOPLIMIT); add_int(OPT_NETWORK_TIMEOUT); + +#if defined(LDAP_OPT_TCP_USER_TIMEOUT) +add_int(OPT_TCP_USER_TIMEOUT); +#endif + add_int(OPT_URI); #if defined(LDAP_OPT_DEFBASE) add_int(OPT_DEFBASE); #endif + #if HAVE_TLS #if defined(LDAP_OPT_X_TLS) @@ -213,26 +222,26 @@ add_int(OPT_X_TLS_DEMAND); add_int(OPT_X_TLS_ALLOW); add_int(OPT_X_TLS_TRY); -#if defined(LDAP_OPT_X_TLS_PEERCERT) -add_int(OPT_X_TLS_PEERCERT); -#endif - #if defined(LDAP_OPT_X_TLS_VERSION) add_int(OPT_X_TLS_VERSION); #endif + #if defined(LDAP_OPT_X_TLS_CIPHER) add_int(OPT_X_TLS_CIPHER); #endif + #if defined(LDAP_OPT_X_TLS_PEERCERT) add_int(OPT_X_TLS_PEERCERT); #endif + #if defined(LDAP_OPT_X_TLS_CRLCHECK) add_int(OPT_X_TLS_CRLCHECK); #endif + #if defined(LDAP_OPT_X_TLS_CRLFILE) add_int(OPT_X_TLS_CRLFILE); #endif @@ -245,14 +254,61 @@ add_int(OPT_X_TLS_CRL_ALL); add_int(OPT_X_TLS_NEWCTX); #endif + #if defined(LDAP_OPT_X_TLS_PROTOCOL_MIN) add_int(OPT_X_TLS_PROTOCOL_MIN); #endif + #if defined(LDAP_OPT_X_TLS_PACKAGE) add_int(OPT_X_TLS_PACKAGE); #endif + +#if defined(LDAP_OPT_X_TLS_ECNAME) +add_int(OPT_X_TLS_ECNAME); +#endif + + +#if defined(LDAP_OPT_X_TLS_REQUIRE_SAN) +add_int(OPT_X_TLS_REQUIRE_SAN); +#endif + + +#if defined(LDAP_OPT_X_TLS_PEERCERT) +add_int(OPT_X_TLS_PEERCERT); +#endif + + +#if defined(LDAP_OPT_X_TLS_PROTOCOL_MAX) +add_int(OPT_X_TLS_PROTOCOL_MAX); +#endif + + +#if defined(LDAP_OPT_X_TLS_PROTOCOL_SSL3) +add_int(OPT_X_TLS_PROTOCOL_SSL3); +#endif + + +#if defined(LDAP_OPT_X_TLS_PROTOCOL_TLS1_0) +add_int(OPT_X_TLS_PROTOCOL_TLS1_0); +#endif + + +#if defined(LDAP_OPT_X_TLS_PROTOCOL_TLS1_1) +add_int(OPT_X_TLS_PROTOCOL_TLS1_1); +#endif + + +#if defined(LDAP_OPT_X_TLS_PROTOCOL_TLS1_2) +add_int(OPT_X_TLS_PROTOCOL_TLS1_2); +#endif + + +#if defined(LDAP_OPT_X_TLS_PROTOCOL_TLS1_3) +add_int(OPT_X_TLS_PROTOCOL_TLS1_3); +#endif + #endif add_int(OPT_X_SASL_MECH); @@ -269,22 +325,27 @@ add_int(OPT_X_SASL_SSF_MAX); add_int(OPT_X_SASL_NOCANON); #endif + #if defined(LDAP_OPT_X_SASL_USERNAME) add_int(OPT_X_SASL_USERNAME); #endif + #if defined(LDAP_OPT_CONNECT_ASYNC) add_int(OPT_CONNECT_ASYNC); #endif + #if defined(LDAP_OPT_X_KEEPALIVE_IDLE) add_int(OPT_X_KEEPALIVE_IDLE); #endif + #if defined(LDAP_OPT_X_KEEPALIVE_PROBES) add_int(OPT_X_KEEPALIVE_PROBES); #endif + #if defined(LDAP_OPT_X_KEEPALIVE_INTERVAL) add_int(OPT_X_KEEPALIVE_INTERVAL); #endif @@ -309,28 +370,24 @@ add_int(OPT_SUCCESS); add_int(URL_ERR_BADSCOPE); add_int(URL_ERR_MEM); -#ifdef HAVE_LIBLDAP_R -if (PyModule_AddIntConstant(m, "LIBLDAP_R", 1) != 0) - return -1; +#ifdef HAVE_SASL +if (PyModule_AddIntConstant(m, "SASL_AVAIL", 1) != 0) return -1; #else -if (PyModule_AddIntConstant(m, "LIBLDAP_R", 0) != 0) - return -1; +if (PyModule_AddIntConstant(m, "SASL_AVAIL", 0) != 0) return -1; #endif -#ifdef HAVE_SASL -if (PyModule_AddIntConstant(m, "SASL_AVAIL", 1) != 0) - return -1; + +#ifdef HAVE_TLS +if (PyModule_AddIntConstant(m, "TLS_AVAIL", 1) != 0) return -1; #else -if (PyModule_AddIntConstant(m, "SASL_AVAIL", 0) != 0) - return -1; +if (PyModule_AddIntConstant(m, "TLS_AVAIL", 0) != 0) return -1; #endif -#ifdef HAVE_TLS -if (PyModule_AddIntConstant(m, "TLS_AVAIL", 1) != 0) - return -1; + +#ifdef HAVE_LDAP_INIT_FD +if (PyModule_AddIntConstant(m, "INIT_FD_AVAIL", 1) != 0) return -1; #else -if (PyModule_AddIntConstant(m, "TLS_AVAIL", 0) != 0) - return -1; +if (PyModule_AddIntConstant(m, "INIT_FD_AVAIL", 0) != 0) return -1; #endif add_string(CONTROL_MANAGEDSAIT); diff --git a/Modules/functions.c b/Modules/functions.c index 4731efb8..3f7f7eca 100644 --- a/Modules/functions.c +++ b/Modules/functions.c @@ -1,11 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "functions.h" -#include "LDAPObject.h" -#include "berval.h" -#include "constants.h" -#include "options.h" +#include "pythonldap.h" /* ldap_initialize */ @@ -15,15 +10,75 @@ l_ldap_initialize(PyObject *unused, PyObject *args) char *uri; LDAP *ld = NULL; int ret; + PyThreadState *save; - if (!PyArg_ParseTuple(args, "s:initialize", &uri)) + if (!PyArg_ParseTuple(args, "z:initialize", &uri)) return NULL; - Py_BEGIN_ALLOW_THREADS ret = ldap_initialize(&ld, uri); - Py_END_ALLOW_THREADS if (ret != LDAP_SUCCESS) - return LDAPerror(ld, "ldap_initialize"); + save = PyEval_SaveThread(); + ret = ldap_initialize(&ld, uri); + PyEval_RestoreThread(save); + + if (ret != LDAP_SUCCESS) + return LDAPerror(ld); + + return (PyObject *)newLDAPObject(ld); +} + +#ifdef HAVE_LDAP_INIT_FD +/* initialize_fd(fileno, url) */ + +static PyObject * +l_ldap_initialize_fd(PyObject *unused, PyObject *args) +{ + char *url; + LDAP *ld = NULL; + int ret; + int fd; + int proto = -1; + LDAPURLDesc *lud = NULL; + + PyThreadState *save; + + if (!PyArg_ParseTuple(args, "is:initialize_fd", &fd, &url)) + return NULL; + + /* Get LDAP protocol from scheme */ + ret = ldap_url_parse(url, &lud); + if (ret != LDAP_SUCCESS) + return LDAPerr(ret); + + if (strcmp(lud->lud_scheme, "ldap") == 0) { + proto = LDAP_PROTO_TCP; + } + else if (strcmp(lud->lud_scheme, "ldaps") == 0) { + proto = LDAP_PROTO_TCP; + } + else if (strcmp(lud->lud_scheme, "ldapi") == 0) { + proto = LDAP_PROTO_IPC; + } +#ifdef LDAP_CONNECTIONLESS + else if (strcmp(lud->lud_scheme, "cldap") == 0) { + proto = LDAP_PROTO_UDP; + } +#endif + else { + ldap_free_urldesc(lud); + PyErr_SetString(PyExc_ValueError, "unsupported URL scheme"); + return NULL; + } + ldap_free_urldesc(lud); + + save = PyEval_SaveThread(); + ret = ldap_init_fd((ber_socket_t) fd, proto, url, &ld); + PyEval_RestoreThread(save); + + if (ret != LDAP_SUCCESS) + return LDAPerror(ld); + return (PyObject *)newLDAPObject(ld); } +#endif /* ldap_str2dn */ @@ -75,9 +130,8 @@ l_ldap_str2dn(PyObject *unused, PyObject *args) tuple = Py_BuildValue("(O&O&i)", LDAPberval_to_unicode_object, &ava->la_attr, LDAPberval_to_unicode_object, &ava->la_value, - ava-> - la_flags & ~(LDAP_AVA_FREE_ATTR | - LDAP_AVA_FREE_VALUE)); + ava->la_flags & ~(LDAP_AVA_FREE_ATTR | + LDAP_AVA_FREE_VALUE)); if (!tuple) { Py_DECREF(rdnlist); goto failed; @@ -101,6 +155,222 @@ l_ldap_str2dn(PyObject *unused, PyObject *args) return result; } +/* ldap_dn2str */ + +static void +_free_dn_structure(LDAPDN dn) +{ + if (dn == NULL) + return; + + for (LDAPRDN *rdn = dn; *rdn != NULL; rdn++) { + for (LDAPAVA **avap = *rdn; *avap != NULL; avap++) { + LDAPAVA *ava = *avap; + + if (ava->la_attr.bv_val) { + free(ava->la_attr.bv_val); + } + if (ava->la_value.bv_val) { + free(ava->la_value.bv_val); + } + free(ava); + } + free(*rdn); + } + free(dn); +} + +/* + * Convert a Python list-of-list-of-(str, str, int) into an LDAPDN and + * call ldap_dn2bv to build a DN string. + * + * Python signature: dn2str(dn: list[list[tuple[str, str, int]]], flags: int) -> str + * Returns the DN string on success, or raises TypeError or RuntimeError on error. + */ +static PyObject * +l_ldap_dn2str(PyObject *self, PyObject *args) +{ + PyObject *dn_list = NULL; + int flags = 0; + LDAPDN dn = NULL; + LDAPAVA *ava; + LDAPAVA **rdn; + BerValue str = { 0, NULL }; + PyObject *py_rdn_seq = NULL, *py_ava_item = NULL; + PyObject *py_name = NULL, *py_value = NULL, *py_encoding = NULL; + PyObject *result = NULL; + Py_ssize_t nrdns = 0, navas = 0, name_len = 0, value_len = 0; + int i = 0, j = 0; + int ldap_err; + const char *name_utf8, *value_utf8; + + const char *type_error_message = "expected list[list[tuple[str, str, int]]]"; + + if (!PyArg_ParseTuple(args, "Oi:dn2str", &dn_list, &flags)) { + return NULL; + } + + if (!PySequence_Check(dn_list)) { + PyErr_SetString(PyExc_TypeError, type_error_message); + return NULL; + } + + nrdns = PySequence_Size(dn_list); + if (nrdns < 0) { + PyErr_SetString(PyExc_TypeError, type_error_message); + return NULL; + } + + /* Allocate array of LDAPRDN pointers (+1 for NULL terminator) */ + dn = (LDAPRDN *) calloc((size_t)nrdns + 1, sizeof(LDAPRDN)); + if (dn == NULL) { + PyErr_NoMemory(); + return NULL; + } + + for (i = 0; i < nrdns; i++) { + py_rdn_seq = PySequence_GetItem(dn_list, i); /* New reference */ + if (py_rdn_seq == NULL) { + goto error_cleanup; + } + if (!PySequence_Check(py_rdn_seq)) { + PyErr_SetString(PyExc_TypeError, type_error_message); + goto error_cleanup; + } + + navas = PySequence_Size(py_rdn_seq); + if (navas < 0) { + PyErr_SetString(PyExc_TypeError, type_error_message); + goto error_cleanup; + } + + /* Allocate array of LDAPAVA* pointers (+1 for NULL terminator) */ + rdn = (LDAPAVA **)calloc((size_t)navas + 1, sizeof(LDAPAVA *)); + if (rdn == NULL) { + PyErr_NoMemory(); + goto error_cleanup; + } + + for (j = 0; j < navas; j++) { + py_ava_item = PySequence_GetItem(py_rdn_seq, j); /* New reference */ + if (py_ava_item == NULL) { + goto error_cleanup; + } + /* Expect a 3‐tuple: (name: str, value: str, encoding: int) */ + if (!PyTuple_Check(py_ava_item) || PyTuple_Size(py_ava_item) != 3) { + PyErr_SetString(PyExc_TypeError, type_error_message); + goto error_cleanup; + } + + py_name = PyTuple_GetItem(py_ava_item, 0); /* Borrowed reference */ + py_value = PyTuple_GetItem(py_ava_item, 1); /* Borrowed reference */ + py_encoding = PyTuple_GetItem(py_ava_item, 2); /* Borrowed reference */ + + if (!PyUnicode_Check(py_name) || !PyUnicode_Check(py_value) || !PyLong_Check(py_encoding)) { + PyErr_SetString(PyExc_TypeError, type_error_message); + goto error_cleanup; + } + + name_len = 0; + value_len = 0; + name_utf8 = PyUnicode_AsUTF8AndSize(py_name, &name_len); + value_utf8 = PyUnicode_AsUTF8AndSize(py_value, &value_len); + if (name_utf8 == NULL || value_utf8 == NULL) { + goto error_cleanup; + } + + ava = (LDAPAVA *) calloc(1, sizeof(LDAPAVA)); + + if (ava == NULL) { + PyErr_NoMemory(); + goto error_cleanup; + } + + ava->la_attr.bv_val = (char *)malloc((size_t)name_len + 1); + if (ava->la_attr.bv_val == NULL) { + free(ava); + PyErr_NoMemory(); + goto error_cleanup; + } + memcpy(ava->la_attr.bv_val, name_utf8, (size_t)name_len); + ava->la_attr.bv_val[name_len] = '\0'; + ava->la_attr.bv_len = (ber_len_t) name_len; + + ava->la_value.bv_val = (char *)malloc((size_t)value_len + 1); + if (ava->la_value.bv_val == NULL) { + free(ava->la_attr.bv_val); + free(ava); + PyErr_NoMemory(); + goto error_cleanup; + } + memcpy(ava->la_value.bv_val, value_utf8, (size_t)value_len); + ava->la_value.bv_val[value_len] = '\0'; + ava->la_value.bv_len = (ber_len_t) value_len; + + ava->la_flags = (int)PyLong_AsLong(py_encoding); + if (PyErr_Occurred()) { + /* Encoding conversion failed */ + free(ava->la_attr.bv_val); + free(ava->la_value.bv_val); + free(ava); + goto error_cleanup; + } + + rdn[j] = ava; + Py_DECREF(py_ava_item); + py_ava_item = NULL; + } + + /* Null‐terminate the RDN */ + rdn[navas] = NULL; + + dn[i] = rdn; + Py_DECREF(py_rdn_seq); + py_rdn_seq = NULL; + } + + /* Null‐terminate the DN */ + dn[nrdns] = NULL; + + /* Call ldap_dn2bv to build a DN string */ + ldap_err = ldap_dn2bv(dn, &str, flags); + if (ldap_err != LDAP_SUCCESS) { + PyErr_SetString(PyExc_RuntimeError, ldap_err2string(ldap_err)); + goto error_cleanup; + } + + result = PyUnicode_FromString(str.bv_val); + if (result == NULL) { + goto error_cleanup; + } + + /* Free the memory allocated by ldap_dn2bv */ + ldap_memfree(str.bv_val); + str.bv_val = NULL; + + /* Free our local DN structure */ + _free_dn_structure(dn); + dn = NULL; + + return result; + + error_cleanup: + /* Free any partially built DN structure */ + _free_dn_structure(dn); + dn = NULL; + + /* If ldap_dn2bv allocated something, free it */ + if (str.bv_val) { + ldap_memfree(str.bv_val); + str.bv_val = NULL; + } + + /* Cleanup Python temporaries */ + Py_XDECREF(py_ava_item); + Py_XDECREF(py_rdn_seq); + return NULL; +} + /* ldap_set_option (global options) */ static PyObject * @@ -133,7 +403,11 @@ l_ldap_get_option(PyObject *self, PyObject *args) static PyMethodDef methods[] = { {"initialize", (PyCFunction)l_ldap_initialize, METH_VARARGS}, +#ifdef HAVE_LDAP_INIT_FD + {"initialize_fd", (PyCFunction)l_ldap_initialize_fd, METH_VARARGS}, +#endif {"str2dn", (PyCFunction)l_ldap_str2dn, METH_VARARGS}, + {"dn2str", (PyCFunction)l_ldap_dn2str, METH_VARARGS}, {"set_option", (PyCFunction)l_ldap_set_option, METH_VARARGS}, {"get_option", (PyCFunction)l_ldap_get_option, METH_VARARGS}, {NULL, NULL} diff --git a/Modules/functions.h b/Modules/functions.h deleted file mode 100644 index 2aef9740..00000000 --- a/Modules/functions.h +++ /dev/null @@ -1,9 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_functions_ -#define __h_functions_ - -#include "common.h" -extern void LDAPinit_functions(PyObject *); - -#endif /* __h_functions_ */ diff --git a/Modules/ldapcontrol.c b/Modules/ldapcontrol.c index f53e681a..4a37b614 100644 --- a/Modules/ldapcontrol.c +++ b/Modules/ldapcontrol.c @@ -1,12 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "LDAPObject.h" -#include "ldapcontrol.h" -#include "berval.h" -#include "constants.h" - -#include "lber.h" +#include "pythonldap.h" /* Prints to stdout the contents of an array of LDAPControl objects */ @@ -82,6 +76,7 @@ Tuple_to_LDAPControl(PyObject *tup) return NULL; lc = PyMem_NEW(LDAPControl, 1); + if (lc == NULL) { PyErr_NoMemory(); return NULL; @@ -91,6 +86,7 @@ Tuple_to_LDAPControl(PyObject *tup) len = strlen(oid); lc->ldctl_oid = PyMem_NEW(char, len + 1); + if (lc->ldctl_oid == NULL) { PyErr_NoMemory(); LDAPControl_DEL(lc); @@ -137,6 +133,7 @@ LDAPControls_from_object(PyObject *list, LDAPControl ***controls_ret) len = PySequence_Length(list); ldcs = PyMem_NEW(LDAPControl *, len + 1); + if (ldcs == NULL) { PyErr_NoMemory(); return 0; @@ -339,6 +336,7 @@ encode_assertion_control(PyObject *self, PyObject *args) char *assertion_filterstr; struct berval ctrl_val; LDAP *ld = NULL; + PyThreadState *save; if (!PyArg_ParseTuple(args, "s:encode_assertion_control", &assertion_filterstr)) { @@ -348,21 +346,27 @@ encode_assertion_control(PyObject *self, PyObject *args) /* XXX: ldap_create() is a nasty and slow hack. It's creating a full blown * LDAP object just to encode assertion controls. */ - Py_BEGIN_ALLOW_THREADS err = ldap_create(&ld); - Py_END_ALLOW_THREADS if (err != LDAP_SUCCESS) - return LDAPerror(ld, "ldap_create"); + save = PyEval_SaveThread(); + err = ldap_create(&ld); + PyEval_RestoreThread(save); + if (err != LDAP_SUCCESS) + return LDAPerror(ld); - err = - ldap_create_assertion_control_value(ld, assertion_filterstr, - &ctrl_val); + err = ldap_create_assertion_control_value(ld, assertion_filterstr, + &ctrl_val); if (err != LDAP_SUCCESS) { - LDAPerror(ld, "ldap_create_assertion_control_value"); - Py_BEGIN_ALLOW_THREADS ldap_unbind_ext(ld, NULL, NULL); - Py_END_ALLOW_THREADS return NULL; + LDAPerror(ld); + save = PyEval_SaveThread(); + ldap_unbind_ext(ld, NULL, NULL); + PyEval_RestoreThread(save); + return NULL; } - Py_BEGIN_ALLOW_THREADS ldap_unbind_ext(ld, NULL, NULL); - Py_END_ALLOW_THREADS res = LDAPberval_to_object(&ctrl_val); + save = PyEval_SaveThread(); + ldap_unbind_ext(ld, NULL, NULL); + PyEval_RestoreThread(save); + res = LDAPberval_to_object(&ctrl_val); + if (ctrl_val.bv_val != NULL) { ber_memfree(ctrl_val.bv_val); } diff --git a/Modules/ldapcontrol.h b/Modules/ldapcontrol.h deleted file mode 100644 index de694c07..00000000 --- a/Modules/ldapcontrol.h +++ /dev/null @@ -1,14 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_ldapcontrol -#define __h_ldapcontrol - -#include "common.h" -#include "ldap.h" - -void LDAPinit_control(PyObject *d); -void LDAPControl_List_DEL(LDAPControl **); -int LDAPControls_from_object(PyObject *, LDAPControl ***); -PyObject *LDAPControls_to_List(LDAPControl **ldcs); - -#endif /* __h_ldapcontrol */ diff --git a/Modules/ldapmodule.c b/Modules/ldapmodule.c index 8bd55ab4..d0735356 100644 --- a/Modules/ldapmodule.c +++ b/Modules/ldapmodule.c @@ -1,21 +1,14 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "constants.h" -#include "functions.h" -#include "ldapcontrol.h" - -#include "LDAPObject.h" - -#if PY_MAJOR_VERSION >= 3 -PyMODINIT_FUNC PyInit__ldap(void); -#else -PyMODINIT_FUNC init_ldap(void); -#endif +#include "pythonldap.h" #define _STR(x) #x #define STR(x) _STR(x) +LDAPAPIInfo ldap_version_info = { + .ldapai_info_version = LDAP_API_INFO_VERSION, +}; + static char version_str[] = STR(LDAPMODULE_VERSION); static char author_str[] = STR(LDAPMODULE_AUTHOR); static char license_str[] = STR(LDAPMODULE_LICENSE); @@ -33,27 +26,24 @@ static PyMethodDef methods[] = { {NULL, NULL} }; +static struct PyModuleDef ldap_moduledef = { + PyModuleDef_HEAD_INIT, + "_ldap", /* m_name */ + "", /* m_doc */ + -1, /* m_size */ + methods, /* m_methods */ +}; + /* module initialisation */ -/* Common initialization code */ -PyObject * -init_ldap_module(void) +PyMODINIT_FUNC +PyInit__ldap() { PyObject *m, *d; /* Create the module and add the functions */ -#if PY_MAJOR_VERSION >= 3 - static struct PyModuleDef ldap_moduledef = { - PyModuleDef_HEAD_INIT, - "_ldap", /* m_name */ - "", /* m_doc */ - -1, /* m_size */ - methods, /* m_methods */ - }; m = PyModule_Create(&ldap_moduledef); -#else - m = Py_InitModule("_ldap", methods); -#endif + /* Initialize LDAP class */ if (PyType_Ready(&LDAP_Type) < 0) { Py_DECREF(m); @@ -72,30 +62,9 @@ init_ldap_module(void) LDAPinit_functions(d); LDAPinit_control(d); - /* Marker for LDAPBytesWarning stack walking - * See _raise_byteswarning in ldapobject.py - */ - if (PyModule_AddIntConstant(m, "_LDAP_WARN_SKIP_FRAME", 1) != 0) { - return NULL; - } - /* Check for errors */ if (PyErr_Occurred()) Py_FatalError("can't initialize module _ldap"); return m; } - -#if PY_MAJOR_VERSION < 3 -PyMODINIT_FUNC -init_ldap() -{ - init_ldap_module(); -} -#else -PyMODINIT_FUNC -PyInit__ldap() -{ - return init_ldap_module(); -} -#endif diff --git a/Modules/message.c b/Modules/message.c index 2c05488a..f1403237 100644 --- a/Modules/message.c +++ b/Modules/message.c @@ -1,10 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "message.h" -#include "berval.h" -#include "ldapcontrol.h" -#include "constants.h" +#include "pythonldap.h" /* * Converts an LDAP message into a Python structure. @@ -53,7 +49,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, if (dn == NULL) { Py_DECREF(result); ldap_msgfree(m); - return LDAPerror(ld, "ldap_get_dn"); + return LDAPerror(ld); } attrdict = PyDict_New(); @@ -69,7 +65,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, Py_DECREF(result); ldap_msgfree(m); ldap_memfree(dn); - return LDAPerror(ld, "ldap_get_entry_controls"); + return LDAPerror(ld); } /* convert serverctrls to list of tuples */ @@ -81,7 +77,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, ldap_msgfree(m); ldap_memfree(dn); ldap_controls_free(serverctrls); - return LDAPerror(ld, "LDAPControls_to_List"); + return LDAPerror(ld); } ldap_controls_free(serverctrls); @@ -201,7 +197,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, Py_DECREF(reflist); Py_DECREF(result); ldap_msgfree(m); - return LDAPerror(ld, "ldap_parse_reference"); + return LDAPerror(ld); } /* convert serverctrls to list of tuples */ if (!(pyctrls = LDAPControls_to_List(serverctrls))) { @@ -212,7 +208,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, Py_DECREF(result); ldap_msgfree(m); ldap_controls_free(serverctrls); - return LDAPerror(ld, "LDAPControls_to_List"); + return LDAPerror(ld); } ldap_controls_free(serverctrls); if (refs) { @@ -255,7 +251,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, 0) != LDAP_SUCCESS) { Py_DECREF(result); ldap_msgfree(m); - return LDAPerror(ld, "ldap_parse_intermediate"); + return LDAPerror(ld); } /* convert serverctrls to list of tuples */ if (!(pyctrls = LDAPControls_to_List(serverctrls))) { @@ -267,26 +263,41 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, ldap_controls_free(serverctrls); ldap_memfree(retoid); ber_bvfree(retdata); - return LDAPerror(ld, "LDAPControls_to_List"); + return LDAPerror(ld); } ldap_controls_free(serverctrls); valuestr = LDAPberval_to_object(retdata); ber_bvfree(retdata); + if (valuestr == NULL) { + ldap_memfree(retoid); + Py_DECREF(result); + ldap_msgfree(m); + return NULL; + } + pyoid = PyUnicode_FromString(retoid); ldap_memfree(retoid); if (pyoid == NULL) { + Py_DECREF(valuestr); + Py_DECREF(result); + ldap_msgfree(m); + return NULL; + } + + valtuple = Py_BuildValue("(NNN)", pyoid, valuestr, pyctrls); + if (valtuple == NULL) { + Py_DECREF(result); + ldap_msgfree(m); + return NULL; + } + + if (PyList_Append(result, valtuple) == -1) { + Py_DECREF(valtuple); Py_DECREF(result); ldap_msgfree(m); return NULL; } - valtuple = Py_BuildValue("(OOO)", pyoid, - valuestr ? valuestr : Py_None, - pyctrls); - Py_DECREF(pyoid); - Py_DECREF(valuestr); - Py_XDECREF(pyctrls); - PyList_Append(result, valtuple); Py_DECREF(valtuple); } } diff --git a/Modules/message.h b/Modules/message.h deleted file mode 100644 index 2978ea56..00000000 --- a/Modules/message.h +++ /dev/null @@ -1,13 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_message -#define __h_message - -#include "common.h" -#include "lber.h" -#include "ldap.h" - -extern PyObject *LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, - int add_intermediates); - -#endif /* __h_message_ */ diff --git a/Modules/options.c b/Modules/options.c index 85560e62..4577b075 100644 --- a/Modules/options.c +++ b/Modules/options.c @@ -1,10 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "constants.h" -#include "LDAPObject.h" -#include "ldapcontrol.h" -#include "options.h" +#include "pythonldap.h" void set_timeval_from_double(struct timeval *tv, double d) @@ -40,9 +36,14 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) { int res; int intval; + unsigned int uintval; double doubleval; char *strval; struct timeval tv; +#if HAVE_SASL + /* unsigned long */ + ber_len_t blen; +#endif void *ptr; LDAP *ld; LDAPControl **controls = NULL; @@ -52,8 +53,12 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) switch (option) { case LDAP_OPT_API_INFO: case LDAP_OPT_API_FEATURE_INFO: + case LDAP_OPT_DESC: #ifdef HAVE_SASL case LDAP_OPT_X_SASL_SSF: +#endif +#ifdef LDAP_OPT_X_TLS_PEERCERT + case LDAP_OPT_X_TLS_PEERCERT: #endif /* Read-only options */ PyErr_SetString(PyExc_ValueError, "read-only option"); @@ -88,10 +93,12 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) #ifdef LDAP_OPT_X_TLS_PROTOCOL_MIN case LDAP_OPT_X_TLS_PROTOCOL_MIN: #endif +#ifdef LDAP_OPT_X_TLS_PROTOCOL_MAX + case LDAP_OPT_X_TLS_PROTOCOL_MAX: +#endif +#ifdef LDAP_OPT_X_TLS_REQUIRE_SAN + case LDAP_OPT_X_TLS_REQUIRE_SAN: #endif -#ifdef HAVE_SASL - case LDAP_OPT_X_SASL_SSF_MIN: - case LDAP_OPT_X_SASL_SSF_MAX: #endif #ifdef LDAP_OPT_X_KEEPALIVE_IDLE case LDAP_OPT_X_KEEPALIVE_IDLE: @@ -108,6 +115,26 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) return 0; ptr = &intval; break; + +#ifdef LDAP_OPT_TCP_USER_TIMEOUT + case LDAP_OPT_TCP_USER_TIMEOUT: +#endif + if (!PyArg_Parse(value, "I:set_option", &uintval)) + return 0; + ptr = &uintval; + break; + +#ifdef HAVE_SASL + case LDAP_OPT_X_SASL_SSF_MIN: + case LDAP_OPT_X_SASL_SSF_MAX: + case LDAP_OPT_X_SASL_SSF_EXTERNAL: + case LDAP_OPT_X_SASL_MAXBUFSIZE: + if (!PyArg_Parse(value, "k:set_option", &blen)) + return 0; + ptr = &blen; + break; +#endif + case LDAP_OPT_HOST_NAME: case LDAP_OPT_URI: #ifdef LDAP_OPT_DEFBASE @@ -126,15 +153,22 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) #ifdef LDAP_OPT_X_TLS_CRLFILE case LDAP_OPT_X_TLS_CRLFILE: #endif +#ifdef LDAP_OPT_X_TLS_ECNAME + case LDAP_OPT_X_TLS_ECNAME: +#endif #endif #ifdef HAVE_SASL case LDAP_OPT_X_SASL_SECPROPS: +#endif +#ifdef LDAP_OPT_SOCKET_BIND_ADDRESSES + case LDAP_OPT_SOCKET_BIND_ADDRESSES: #endif /* String valued options */ if (!PyArg_Parse(value, "s:set_option", &strval)) return 0; ptr = strval; break; + case LDAP_OPT_TIMEOUT: case LDAP_OPT_NETWORK_TIMEOUT: /* Float valued timeval options */ @@ -168,8 +202,8 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) } else { PyErr_Format(PyExc_ValueError, - "timeout must be >= 0 or -1/None for infinity, got %d", - option); + "timeout must be >= 0 or -1/None for infinity, got %S", + value); return 0; } break; @@ -185,11 +219,18 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) return 0; } - if (self) + if (self) { LDAP_BEGIN_ALLOW_THREADS(self); - res = ldap_set_option(ld, option, ptr); - if (self) + res = ldap_set_option(ld, option, ptr); LDAP_END_ALLOW_THREADS(self); + } + else { + PyThreadState *save; + + save = PyEval_SaveThread(); + res = ldap_set_option(NULL, option, ptr); + PyEval_RestoreThread(save); + } if ((option == LDAP_OPT_SERVER_CONTROLS) || (option == LDAP_OPT_CLIENT_CONTROLS)) @@ -203,29 +244,55 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) return 1; } +static int +LDAP_int_get_option(LDAPObject *self, int option, void *value) +{ + int res; + + if (self != NULL) { + LDAP_BEGIN_ALLOW_THREADS(self); + res = ldap_get_option(self->ldap, option, value); + LDAP_END_ALLOW_THREADS(self); + } + else { + PyThreadState *save; + + save = PyEval_SaveThread(); + res = ldap_get_option(NULL, option, value); + PyEval_RestoreThread(save); + } + return res; +} + PyObject * LDAP_get_option(LDAPObject *self, int option) { int res; int intval; + unsigned int uintval; struct timeval *tv; LDAPAPIInfo apiinfo; LDAPControl **lcs; char *strval; + struct berval berbytes; +#if HAVE_SASL + /* unsigned long */ + ber_len_t blen; +#endif PyObject *extensions, *v; Py_ssize_t i, num_extensions; - LDAP *ld; - - ld = self ? self->ldap : NULL; switch (option) { +#ifdef HAVE_SASL + case LDAP_OPT_X_SASL_SECPROPS: + case LDAP_OPT_X_SASL_SSF_EXTERNAL: + /* Write-only options */ + PyErr_SetString(PyExc_ValueError, "write-only option"); + return NULL; +#endif case LDAP_OPT_API_INFO: apiinfo.ldapai_info_version = LDAP_API_INFO_VERSION; - if (self) - LDAP_BEGIN_ALLOW_THREADS(self); - res = ldap_get_option(ld, option, &apiinfo); - if (self) - LDAP_END_ALLOW_THREADS(self); + res = LDAP_int_get_option(self, option, &apiinfo); if (res != LDAP_OPT_SUCCESS) return option_error(res, "ldap_get_option"); @@ -236,8 +303,8 @@ LDAP_get_option(LDAPObject *self, int option) extensions = PyTuple_New(num_extensions); for (i = 0; i < num_extensions; i++) PyTuple_SET_ITEM(extensions, i, - PyUnicode_FromString(apiinfo. - ldapai_extensions[i])); + PyUnicode_FromString(apiinfo.ldapai_extensions + [i])); /* return api info as a dictionary */ v = Py_BuildValue("{s:i, s:i, s:i, s:s, s:i, s:O}", @@ -257,9 +324,6 @@ LDAP_get_option(LDAPObject *self, int option) return v; -#ifdef HAVE_SASL - case LDAP_OPT_X_SASL_SSF: -#endif case LDAP_OPT_REFERRALS: case LDAP_OPT_RESTART: case LDAP_OPT_DEREF: @@ -278,10 +342,12 @@ LDAP_get_option(LDAPObject *self, int option) #ifdef LDAP_OPT_X_TLS_PROTOCOL_MIN case LDAP_OPT_X_TLS_PROTOCOL_MIN: #endif +#ifdef LDAP_OPT_X_TLS_PROTOCOL_MAX + case LDAP_OPT_X_TLS_PROTOCOL_MAX: +#endif +#ifdef LDAP_OPT_X_TLS_REQUIRE_SAN + case LDAP_OPT_X_TLS_REQUIRE_SAN: #endif -#ifdef HAVE_SASL - case LDAP_OPT_X_SASL_SSF_MIN: - case LDAP_OPT_X_SASL_SSF_MAX: #endif #ifdef LDAP_OPT_X_SASL_NOCANON case LDAP_OPT_X_SASL_NOCANON: @@ -299,14 +365,31 @@ LDAP_get_option(LDAPObject *self, int option) case LDAP_OPT_X_KEEPALIVE_INTERVAL: #endif /* Integer-valued options */ - if (self) - LDAP_BEGIN_ALLOW_THREADS(self); - res = ldap_get_option(ld, option, &intval); - if (self) - LDAP_END_ALLOW_THREADS(self); + res = LDAP_int_get_option(self, option, &intval); + if (res != LDAP_OPT_SUCCESS) + return option_error(res, "ldap_get_option"); + return PyLong_FromLong(intval); + +#ifdef LDAP_OPT_TCP_USER_TIMEOUT + case LDAP_OPT_TCP_USER_TIMEOUT: +#endif + /* unsigned int options */ + res = LDAP_int_get_option(self, option, &uintval); + if (res != LDAP_OPT_SUCCESS) + return option_error(res, "ldap_get_option"); + return PyLong_FromUnsignedLong(uintval); + +#ifdef HAVE_SASL + case LDAP_OPT_X_SASL_SSF: + case LDAP_OPT_X_SASL_SSF_MIN: + case LDAP_OPT_X_SASL_SSF_MAX: + case LDAP_OPT_X_SASL_MAXBUFSIZE: + /* ber_len_t options (unsigned long)*/ + res = LDAP_int_get_option(self, option, &blen); if (res != LDAP_OPT_SUCCESS) return option_error(res, "ldap_get_option"); - return PyInt_FromLong(intval); + return PyLong_FromUnsignedLong(blen); +#endif case LDAP_OPT_HOST_NAME: case LDAP_OPT_URI: @@ -335,9 +418,11 @@ LDAP_get_option(LDAPObject *self, int option) #ifdef LDAP_OPT_X_TLS_PACKAGE case LDAP_OPT_X_TLS_PACKAGE: #endif +#ifdef LDAP_OPT_X_TLS_ECNAME + case LDAP_OPT_X_TLS_ECNAME: +#endif #endif #ifdef HAVE_SASL - case LDAP_OPT_X_SASL_SECPROPS: case LDAP_OPT_X_SASL_MECH: case LDAP_OPT_X_SASL_REALM: case LDAP_OPT_X_SASL_AUTHCID: @@ -345,13 +430,12 @@ LDAP_get_option(LDAPObject *self, int option) #ifdef LDAP_OPT_X_SASL_USERNAME case LDAP_OPT_X_SASL_USERNAME: #endif +#endif +#ifdef LDAP_OPT_SOCKET_BIND_ADDRESSES + case LDAP_OPT_SOCKET_BIND_ADDRESSES: #endif /* String-valued options */ - if (self) - LDAP_BEGIN_ALLOW_THREADS(self); - res = ldap_get_option(ld, option, &strval); - if (self) - LDAP_END_ALLOW_THREADS(self); + res = LDAP_int_get_option(self, option, &strval); if (res != LDAP_OPT_SUCCESS) return option_error(res, "ldap_get_option"); if (strval == NULL) { @@ -362,14 +446,23 @@ LDAP_get_option(LDAPObject *self, int option) ldap_memfree(strval); return v; +#ifdef HAVE_TLS +#ifdef LDAP_OPT_X_TLS_PEERCERT + case LDAP_OPT_X_TLS_PEERCERT: +#endif +#endif + /* Options dealing with raw data */ + res = LDAP_int_get_option(self, option, &berbytes); + if (res != LDAP_OPT_SUCCESS) + return option_error(res, "ldap_get_option"); + v = LDAPberval_to_object(&berbytes); + ldap_memfree(berbytes.bv_val); + return v; + case LDAP_OPT_TIMEOUT: case LDAP_OPT_NETWORK_TIMEOUT: /* Double-valued timeval options */ - if (self) - LDAP_BEGIN_ALLOW_THREADS(self); - res = ldap_get_option(ld, option, &tv); - if (self) - LDAP_END_ALLOW_THREADS(self); + res = LDAP_int_get_option(self, option, &tv); if (res != LDAP_OPT_SUCCESS) return option_error(res, "ldap_get_option"); if (tv == NULL) { @@ -384,12 +477,7 @@ LDAP_get_option(LDAPObject *self, int option) case LDAP_OPT_SERVER_CONTROLS: case LDAP_OPT_CLIENT_CONTROLS: - if (self) - LDAP_BEGIN_ALLOW_THREADS(self); - res = ldap_get_option(ld, option, &lcs); - if (self) - LDAP_END_ALLOW_THREADS(self); - + res = LDAP_int_get_option(self, option, &lcs); if (res != LDAP_OPT_SUCCESS) return option_error(res, "ldap_get_option"); diff --git a/Modules/options.h b/Modules/options.h deleted file mode 100644 index fd6a5ce2..00000000 --- a/Modules/options.h +++ /dev/null @@ -1,7 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -int LDAP_optionval_by_name(const char *name); -int LDAP_set_option(LDAPObject *self, int option, PyObject *value); -PyObject *LDAP_get_option(LDAPObject *self, int option); - -void set_timeval_from_double(struct timeval *tv, double d); diff --git a/Modules/pythonldap.h b/Modules/pythonldap.h new file mode 100644 index 00000000..35ed0d92 --- /dev/null +++ b/Modules/pythonldap.h @@ -0,0 +1,133 @@ +/* common utility macros + * See https://www.python-ldap.org/ for details. */ + +#ifndef pythonldap_h +#define pythonldap_h + +/* *** common *** */ +#define PY_SSIZE_T_CLEAN + +#include "Python.h" + +#if defined(HAVE_CONFIG_H) +#include "config.h" +#endif + +#include +#include +#include + +#if LDAP_VENDOR_VERSION < 20400 +#error Current python-ldap requires OpenLDAP 2.4.x +#endif + +#if LDAP_VENDOR_VERSION >= 20448 + /* openldap.h with ldap_init_fd() was introduced in 2.4.48 + * see https://bugs.openldap.org/show_bug.cgi?id=8671 + */ +#define HAVE_LDAP_INIT_FD 1 +#include +#elif (defined(__APPLE__) && (LDAP_VENDOR_VERSION == 20428)) +/* macOS system libldap 2.4.28 does not have ldap_init_fd symbol */ +#undef HAVE_LDAP_INIT_FD +#else + /* ldap_init_fd() has been around for a very long time + * SSSD has been defining the function for a while, so it's probably OK. + */ +#define HAVE_LDAP_INIT_FD 1 +#define LDAP_PROTO_TCP 1 +#define LDAP_PROTO_UDP 2 +#define LDAP_PROTO_IPC 3 +LDAP_F(int) ldap_init_fd(ber_socket_t fd, int proto, LDAP_CONST char *url, + LDAP **ldp); +#endif + +#if defined(MS_WINDOWS) +#include +#else /* unix */ +#include +#include +#include +#endif + +#define PYLDAP_FUNC(rtype) rtype +#define PYLDAP_DATA(rtype) extern rtype + +PYLDAP_FUNC(PyObject *) LDAPerror_TypeError(const char *, PyObject *); + +PYLDAP_FUNC(void) LDAPadd_methods(PyObject *d, PyMethodDef *methods); + +#define PyNone_Check(o) ((o) == Py_None) + +/* *** berval *** */ +PYLDAP_FUNC(PyObject *) LDAPberval_to_object(const struct berval *bv); +PYLDAP_FUNC(PyObject *) LDAPberval_to_unicode_object(const struct berval *bv); + +/* *** constants *** */ +PYLDAP_FUNC(int) LDAPinit_constants(PyObject *m); + +PYLDAP_DATA(PyObject *) LDAPexception_class; +PYLDAP_FUNC(PyObject *) LDAPerror(LDAP *); +PYLDAP_FUNC(PyObject *) LDAPraise_for_message(LDAP *, LDAPMessage *m); +PYLDAP_FUNC(PyObject *) LDAPerr(int errnum); + +PYLDAP_DATA(LDAPAPIInfo) ldap_version_info; + +#ifndef LDAP_CONTROL_PAGE_OID +#define LDAP_CONTROL_PAGE_OID "1.2.840.113556.1.4.319" +#endif /* !LDAP_CONTROL_PAGE_OID */ + +#ifndef LDAP_CONTROL_VALUESRETURNFILTER +#define LDAP_CONTROL_VALUESRETURNFILTER "1.2.826.0.1.3344810.2.3" /* RFC 3876 */ +#endif /* !LDAP_CONTROL_VALUESRETURNFILTER */ + +/* *** functions *** */ +PYLDAP_FUNC(void) LDAPinit_functions(PyObject *); + +/* *** ldapcontrol *** */ +PYLDAP_FUNC(void) LDAPinit_control(PyObject *d); +PYLDAP_FUNC(void) LDAPControl_List_DEL(LDAPControl **); +PYLDAP_FUNC(int) LDAPControls_from_object(PyObject *, LDAPControl ***); +PYLDAP_FUNC(PyObject *) LDAPControls_to_List(LDAPControl **ldcs); + +/* *** ldapobject *** */ +typedef struct { + PyObject_HEAD LDAP *ldap; + PyThreadState *_save; /* for thread saving on referrals */ + int valid; +} LDAPObject; + +PYLDAP_DATA(PyTypeObject) LDAP_Type; +PYLDAP_FUNC(LDAPObject *) newLDAPObject(LDAP *); + +/* macros to allow thread saving in the context of an LDAP connection */ + +#define LDAP_BEGIN_ALLOW_THREADS( l ) \ + { \ + LDAPObject *lo = (l); \ + if (lo->_save != NULL) \ + Py_FatalError( "saving thread twice?" ); \ + lo->_save = PyEval_SaveThread(); \ + } + +#define LDAP_END_ALLOW_THREADS( l ) \ + { \ + LDAPObject *lo = (l); \ + PyThreadState *_save = lo->_save; \ + lo->_save = NULL; \ + PyEval_RestoreThread( _save ); \ + } + +/* *** messages *** */ +PYLDAP_FUNC(PyObject *) +LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, + int add_intermediates); + +/* *** options *** */ +PYLDAP_FUNC(int) LDAP_optionval_by_name(const char *name); +PYLDAP_FUNC(int) LDAP_set_option(LDAPObject *self, int option, + PyObject *value); +PYLDAP_FUNC(PyObject *) LDAP_get_option(LDAPObject *self, int option); +PYLDAP_FUNC(void) set_timeval_from_double(struct timeval *tv, double d); + +#endif /* pythonldap_h */ diff --git a/README b/README index 81db9bb3..8045555d 100644 --- a/README +++ b/README @@ -127,3 +127,27 @@ their contributions and input into this package. Thanks! We may have missed someone: please mail us if we have omitted your name. + +Licence +======= + +The python-ldap project comes with a LICENCE file. + +We are aware that its text is unclear, but it cannot be changed: +all authors of python-ldap would need to approve the licence change, +but a complete list of all the authors is not available. +(Note that the Git repository of the project is incomplete. +Furthermore, commits imported from CVS lack authorship information; users +"stroeder" or "leonard" are commiters (reviewers), but sometimes not +authors of the committed code.) + +The current maintainers assume that the license is the sentence that refers +to "Python-style license" and assume this means a highly permissive open source +license that only requires preservation of the text of the LICENCE file +(including the disclaimer paragraph). + +------------------------------------------------------------------------------- + +All contributions committed since July 1st, 2021, as well as some past +contributions, are licensed under the MIT license. +The MIT licence and more details are listed in the file LICENCE.MIT. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..752b1394 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +Security updates are applied only to the latest release. + +## Reporting a Vulnerability + +If you have discovered a security vulnerability in this project, please report it privately. **Do not disclose it as a public issue.** This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released. + +Please disclose it at our [security advisory](https://github.com/python-ldap/python-ldap/security/advisories/new). + +This project is maintained by a team of volunteers on a reasonable-effort basis. As such, vulnerabilities will be disclosed in a best effort base. diff --git a/Tests/__init__.py b/Tests/__init__.py index d6545732..1a6a8836 100644 --- a/Tests/__init__.py +++ b/Tests/__init__.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap See https://www.python-ldap.org/ for details. """ -from __future__ import absolute_import from . import t_bind from . import t_cext @@ -23,3 +21,4 @@ from . import t_untested_mods from . import t_ldap_controls_libldap from . import t_ldap_options +from . import t_ldap_syncrepl diff --git a/Tests/t_bind.py b/Tests/t_bind.py index 3e2b67f8..ba90c4cd 100644 --- a/Tests/t_bind.py +++ b/Tests/t_bind.py @@ -1,14 +1,3 @@ -from __future__ import unicode_literals - -import sys - -if sys.version_info[0] <= 2: - PY2 = True - text_type = unicode -else: - PY2 = False - text_type = str - import os import unittest @@ -44,19 +33,6 @@ def test_unicode_bind(self): l = self._get_ldapobject(False) l.simple_bind("CN=user", self.unicode_val) - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_unicode_bind_bytesmode(self): - l = self._get_ldapobject(True) - with self.assertRaises(TypeError): - l.simple_bind_s(self.dn_unicode, self.unicode_val_bytes) - - with self.assertRaises(TypeError): - l.simple_bind_s(self.dn_bytes, self.unicode_val) - - # Works when encoded to UTF-8 - with self.assertRaises(ldap.INVALID_CREDENTIALS): - l.simple_bind_s(self.dn_bytes, self.unicode_val_bytes) - def test_unicode_bind_no_bytesmode(self): l = self._get_ldapobject(False) with self.assertRaises(TypeError): diff --git a/Tests/t_cext.py b/Tests/t_cext.py index 96c3b2c8..846127f8 100644 --- a/Tests/t_cext.py +++ b/Tests/t_cext.py @@ -1,14 +1,12 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap's C wrapper module _ldap See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - +import contextlib import errno import os +import socket import unittest # Switch off processing .ldaprc or ldap.conf before importing _ldap @@ -16,7 +14,7 @@ # import the plain C wrapper module import _ldap -from slapdtest import SlapdTestCase, requires_tls +from slapdtest import SlapdTestCase, requires_tls, requires_init_fd class TestLdapCExtension(SlapdTestCase): @@ -29,7 +27,7 @@ class TestLdapCExtension(SlapdTestCase): @classmethod def setUpClass(cls): - super(TestLdapCExtension, cls).setUpClass() + super().setUpClass() # add two initial objects after server was started and is still empty suffix_dc = cls.server.suffix.split(',')[0][3:] cls.server._log.debug( @@ -53,14 +51,14 @@ def setUpClass(cls): ) def setUp(self): - super(TestLdapCExtension, self).setUp() + super().setUp() self._writesuffix = None def tearDown(self): # cleanup test subtree if self._writesuffix is not None: self.server.ldapdelete(self._writesuffix, recursive=True) - super(TestLdapCExtension, self).tearDown() + super().tearDown() @property def writesuffix(self): @@ -92,14 +90,35 @@ def _open_conn(self, bind=True): """ l = _ldap.initialize(self.server.ldap_uri) if bind: - # Perform a simple bind - l.set_option(_ldap.OPT_PROTOCOL_VERSION, _ldap.VERSION3) - m = l.simple_bind(self.server.root_dn, self.server.root_pw) - result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ONE, self.timeout) - self.assertEqual(result, _ldap.RES_BIND) - self.assertEqual(type(msgid), type(0)) + self._bind_conn(l) return l + @contextlib.contextmanager + def _open_conn_fd(self, bind=True): + sock = socket.create_connection( + (self.server.hostname, self.server.port) + ) + try: + l = _ldap.initialize_fd(sock.fileno(), self.server.ldap_uri) + if bind: + self._bind_conn(l) + yield sock, l + finally: + try: + sock.close() + except OSError: + # already closed + pass + + def _bind_conn(self, l): + # Perform a simple bind + l.set_option(_ldap.OPT_PROTOCOL_VERSION, _ldap.VERSION3) + m = l.simple_bind(self.server.root_dn, self.server.root_pw) + result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ONE, self.timeout) + self.assertEqual(result, _ldap.RES_BIND) + self.assertEqual(type(msgid), type(0)) + + # Test for the existence of a whole bunch of constants # that the C module is supposed to export def test_constants(self): @@ -215,15 +234,42 @@ def test_test_flags(self): if 'TLS' in disabled: self.assertFalse(_ldap.TLS_AVAIL) else: - self.assertFalse(_ldap.TLS_AVAIL) + self.assertTrue(_ldap.TLS_AVAIL) if 'SASL' in disabled: self.assertFalse(_ldap.SASL_AVAIL) else: - self.assertFalse(_ldap.SASL_AVAIL) + self.assertTrue(_ldap.SASL_AVAIL) def test_simple_bind(self): l = self._open_conn() + def test_simple_bind_fileno(self): + with self._open_conn_fd() as (sock, l): + self.assertEqual(l.whoami_s(), "dn:" + self.server.root_dn) + + @requires_init_fd() + def test_simple_bind_fileno_invalid(self): + with open(os.devnull) as f: + l = _ldap.initialize_fd(f.fileno(), self.server.ldap_uri) + with self.assertRaises(_ldap.SERVER_DOWN): + self._bind_conn(l) + + @requires_init_fd() + def test_simple_bind_fileno_closed(self): + with self._open_conn_fd() as (sock, l): + self.assertEqual(l.whoami_s(), "dn:" + self.server.root_dn) + sock.close() + with self.assertRaises(_ldap.SERVER_DOWN): + l.whoami_s() + + @requires_init_fd() + def test_simple_bind_fileno_rebind(self): + with self._open_conn_fd() as (sock, l): + self.assertEqual(l.whoami_s(), "dn:" + self.server.root_dn) + l.unbind_ext() + with self.assertRaises(_ldap.LDAPError): + self._bind_conn(l) + def test_simple_anonymous_bind(self): l = self._open_conn(bind=False) m = l.simple_bind("", "") @@ -234,6 +280,29 @@ def test_simple_anonymous_bind(self): self.assertEqual(pmsg, []) self.assertEqual(ctrls, []) + @unittest.skipUnless( + _ldap.VENDOR_VERSION >= 20500 and \ + _ldap._VENDOR_VERSION_RUNTIME >= 20500, + reason="Test requires libldap 2.5+" + ) + def test_connect(self): + l = self._open_conn(bind=False) + invalid_fileno = l.get_option(_ldap.OPT_DESC) + l.connect() + fileno = l.get_option(_ldap.OPT_DESC) + self.assertNotEqual(invalid_fileno, fileno) + + self._bind_conn(l) + + @unittest.skipUnless( + _ldap._VENDOR_VERSION_RUNTIME < 20500, + reason="Test requires linking to libldap < 2.5" + ) + def test_connect_notimpl(self): + l = self._open_conn(bind=False) + with self.assertRaises(NotImplementedError): + l.connect() + def test_anon_rootdse_search(self): l = self._open_conn(bind=False) # see if we can get the rootdse with anon search (without prior bind) @@ -241,7 +310,7 @@ def test_anon_rootdse_search(self): '', _ldap.SCOPE_BASE, '(objectClass=*)', - [str('objectClass'), str('namingContexts')], + ['objectClass', 'namingContexts'], ) self.assertEqual(type(m), type(0)) result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) @@ -381,30 +450,36 @@ def test_compare(self): self.assertEqual(type(m), type(0)) result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) self.assertEqual(result, _ldap.RES_ADD) + # try a false compare m = l.compare_ext(dn, "userPassword", "bad_string") - try: + with self.assertRaises(_ldap.COMPARE_FALSE) as e: r = l.result4(m, _ldap.MSG_ALL, self.timeout) - except _ldap.COMPARE_FALSE: - pass - else: - self.fail("expected COMPARE_FALSE, got %r" % r) + + self.assertEqual(e.exception.args[0]['msgid'], m) + self.assertEqual(e.exception.args[0]['msgtype'], _ldap.RES_COMPARE) + self.assertEqual(e.exception.args[0]['result'], 5) + self.assertFalse(e.exception.args[0]['ctrls']) + # try a true compare m = l.compare_ext(dn, "userPassword", "the_password") - try: + with self.assertRaises(_ldap.COMPARE_TRUE) as e: r = l.result4(m, _ldap.MSG_ALL, self.timeout) - except _ldap.COMPARE_TRUE: - pass - else: - self.fail("expected COMPARE_TRUE, got %r" % r) + + self.assertEqual(e.exception.args[0]['msgid'], m) + self.assertEqual(e.exception.args[0]['msgtype'], _ldap.RES_COMPARE) + self.assertEqual(e.exception.args[0]['result'], 6) + self.assertFalse(e.exception.args[0]['ctrls']) + # try a compare on bad attribute m = l.compare_ext(dn, "badAttribute", "ignoreme") - try: + with self.assertRaises(_ldap.error) as e: r = l.result4(m, _ldap.MSG_ALL, self.timeout) - except _ldap.error: - pass - else: - self.fail("expected LDAPError, got %r" % r) + + self.assertEqual(e.exception.args[0]['msgid'], m) + self.assertEqual(e.exception.args[0]['msgtype'], _ldap.RES_COMPARE) + self.assertEqual(e.exception.args[0]['result'], 17) + self.assertFalse(e.exception.args[0]['ctrls']) def test_delete_no_such_object(self): """ @@ -880,6 +955,29 @@ def test_tls_packages(self): package = _ldap.get_option(_ldap.OPT_X_TLS_PACKAGE) self.assertIn(package, {"GnuTLS", "MozNSS", "OpenSSL"}) + @unittest.skipUnless( + hasattr(_ldap, "OPT_X_TLS_REQUIRE_SAN"), + reason="Test requires OPT_X_TLS_REQUIRE_SAN" + ) + def test_require_san(self): + l = self._open_conn(bind=False) + value = l.get_option(_ldap.OPT_X_TLS_REQUIRE_SAN) + self.assertIn( + value, + { + _ldap.OPT_X_TLS_NEVER, + _ldap.OPT_X_TLS_ALLOW, + _ldap.OPT_X_TLS_TRY, + _ldap.OPT_X_TLS_DEMAND, + _ldap.OPT_X_TLS_HARD, + } + ) + l.set_option(_ldap.OPT_X_TLS_REQUIRE_SAN, _ldap.OPT_X_TLS_TRY) + self.assertEqual( + l.get_option(_ldap.OPT_X_TLS_REQUIRE_SAN), + _ldap.OPT_X_TLS_TRY + ) + if __name__ == '__main__': unittest.main() diff --git a/Tests/t_cidict.py b/Tests/t_cidict.py index fa5a39b8..97146ec8 100644 --- a/Tests/t_cidict.py +++ b/Tests/t_cidict.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap's module ldap.cidict @@ -7,6 +6,7 @@ import os import unittest +import warnings # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' @@ -48,6 +48,42 @@ def test_cidict(self): self.assertEqual(cix.has_key("abcdef"), False) self.assertEqual(cix.has_key("AbCDef"), False) + def test_strlist_deprecated(self): + strlist_funcs = [ + ldap.cidict.strlist_intersection, + ldap.cidict.strlist_minus, + ldap.cidict.strlist_union + ] + for strlist_func in strlist_funcs: + with warnings.catch_warnings(record=True) as w: + warnings.resetwarnings() + warnings.simplefilter("always", DeprecationWarning) + strlist_func(["a"], ["b"]) + self.assertEqual(len(w), 1) + + def test_cidict_data(self): + """test the deprecated data atrtribute""" + d = ldap.cidict.cidict({'A': 1, 'B': 2}) + with warnings.catch_warnings(record=True) as w: + warnings.resetwarnings() + warnings.simplefilter('always', DeprecationWarning) + data = d.data + assert data == {'a': 1, 'b': 2} + self.assertEqual(len(w), 1) + + def test_copy(self): + cix1 = ldap.cidict.cidict( + {"a": 1, "B": 2} + ) + cix2 = cix1.copy() + self.assertEqual(cix1, cix2) + cix1["c"] = 3 + self.assertNotIn("c", cix2) + cix2["C"] = 4 + self.assertNotEqual(cix1, cix2) + self.assertEqual(list(cix1.keys()), ["a", "B", "c"]) + self.assertEqual(list(cix2.keys()), ["a", "B", "C"]) + if __name__ == '__main__': unittest.main() diff --git a/Tests/t_edit.py b/Tests/t_edit.py index a5b3f657..5d8b3f06 100644 --- a/Tests/t_edit.py +++ b/Tests/t_edit.py @@ -1,14 +1,3 @@ -from __future__ import unicode_literals - -import sys - -if sys.version_info[0] <= 2: - PY2 = True - text_type = unicode -else: - PY2 = False - text_type = str - import os import unittest @@ -24,7 +13,7 @@ class EditionTests(SlapdTestCase): @classmethod def setUpClass(cls): - super(EditionTests, cls).setUpClass() + super().setUpClass() base = cls.server.suffix suffix_dc = base.split(',')[0][3:] diff --git a/Tests/t_ldap_controls_readentry.py b/Tests/t_ldap_controls_readentry.py new file mode 100644 index 00000000..313a7905 --- /dev/null +++ b/Tests/t_ldap_controls_readentry.py @@ -0,0 +1,28 @@ +import os +import unittest + +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' + +from ldap.controls import readentry # noqa: E402 + + +PRC_ENC = b'db\x04)uid=Administrator,cn=users,l=school,l=dev0503\x04\tentryUUID1&\x04$5d96cc2c-8e13-103a-8ca5-2f74868e0e44' +PRC_DEC = b'0\x0b\x04\tentryUUID' + + +class TestLibldapControls(unittest.TestCase): + + def test_pagedresults_encode(self): + pr = readentry.PostReadControl(True, ['entryUUID']) + self.assertEqual(pr.encodeControlValue(), PRC_DEC) + + def test_readentry_decode(self): + pr = readentry.PostReadControl(True, ['entryUUID']) + pr.decodeControlValue(PRC_ENC) + self.assertIsInstance(pr.dn, str) + self.assertEqual(pr.entry, {'entryUUID': [b'5d96cc2c-8e13-103a-8ca5-2f74868e0e44']}) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/t_ldap_controls_sss.py b/Tests/t_ldap_controls_sss.py new file mode 100644 index 00000000..b510fcbe --- /dev/null +++ b/Tests/t_ldap_controls_sss.py @@ -0,0 +1,17 @@ +import os +import unittest + +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' + +from ldap.controls import sss + + +class TestControlsPPolicy(unittest.TestCase): + def test_create_sss_request_control(self): + control = sss.SSSRequestControl(ordering_rules=['-uidNumber']) + self.assertEqual(control.ordering_rules, ['-uidNumber']) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/t_ldap_dn.py b/Tests/t_ldap_dn.py index 4b4dd319..c4d9cb6c 100644 --- a/Tests/t_ldap_dn.py +++ b/Tests/t_ldap_dn.py @@ -1,12 +1,8 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap's module ldap.dn See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - # from Python's standard lib import os import unittest @@ -44,18 +40,24 @@ def test_escape_dn_chars(self): test function escape_dn_chars() """ self.assertEqual(ldap.dn.escape_dn_chars('foobar'), 'foobar') - self.assertEqual(ldap.dn.escape_dn_chars('foo,bar'), 'foo\\,bar') - self.assertEqual(ldap.dn.escape_dn_chars('foo=bar'), 'foo\\=bar') + self.assertEqual(ldap.dn.escape_dn_chars('foo,bar'), r'foo\,bar') + self.assertEqual(ldap.dn.escape_dn_chars('foo=bar'), r'foo\=bar') self.assertEqual(ldap.dn.escape_dn_chars('foo#bar'), 'foo#bar') - self.assertEqual(ldap.dn.escape_dn_chars('#foobar'), '\\#foobar') + self.assertEqual(ldap.dn.escape_dn_chars('#foobar'), r'\#foobar') self.assertEqual(ldap.dn.escape_dn_chars('foo bar'), 'foo bar') - self.assertEqual(ldap.dn.escape_dn_chars(' foobar'), '\\ foobar') + self.assertEqual(ldap.dn.escape_dn_chars(' foobar'), r'\ foobar') + self.assertEqual(ldap.dn.escape_dn_chars(' '), r'\ ') + self.assertEqual(ldap.dn.escape_dn_chars(' '), r'\ \ ') + self.assertEqual(ldap.dn.escape_dn_chars('foobar '), r'foobar\ ') + self.assertEqual(ldap.dn.escape_dn_chars('f+o>o,bo\,b\= (3, 5, 0): - # Python 3.4.x does not include 'search_ext()' in message - self.assertEqual( - "search_ext() argument 1 must be str, not bytes", - text_type(e.exception) - ) + # Python 3.4.x does not include 'search_ext()' in message + self.assertEqual( + "search_ext() argument 1 must be str, not bytes", + str(e.exception) + ) with self.assertRaises(TypeError) as e: l.search_s( base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*'] ) - if PY2: - self.assertIn( - u"got type 'str' for 'filterstr'", text_type(e.exception) - ) - elif sys.version_info >= (3, 5, 0): - self.assertEqual( - "search_ext() argument 3 must be str, not bytes", - text_type(e.exception) - ) + self.assertEqual( + "search_ext() argument 3 must be str, not bytes", + str(e.exception) + ) with self.assertRaises(TypeError) as e: l.search_s( base, ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*'] ) - if PY2: - self.assertIn( - u"got type 'str' for 'attrlist'", text_type(e.exception) - ) - elif sys.version_info >= (3, 5, 0): - self.assertEqual( - ('attrs_from_List(): expected string in list', b'*'), - e.exception.args - ) + self.assertEqual( + ('attrs_from_List(): expected string in list', b'*'), + e.exception.args + ) def test_search_keys_are_text(self): base = self.server.suffix @@ -157,143 +170,12 @@ def test_search_keys_are_text(self): result.sort() dn, fields = result[0] self.assertEqual(dn, 'cn=Foo1,%s' % base) - self.assertEqual(type(dn), text_type) - for key, values in fields.items(): - self.assertEqual(type(key), text_type) - for value in values: - self.assertEqual(type(value), bytes) - - def _get_bytes_ldapobject(self, explicit=True, **kwargs): - if explicit: - kwargs.setdefault('bytes_mode', True) - else: - kwargs = {} - return self._open_ldap_conn( - who=self.server.root_dn.encode('utf-8'), - cred=self.server.root_pw.encode('utf-8'), - **kwargs - ) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_search_requires_bytes(self): - l = self._get_bytes_ldapobject() - base = self.server.suffix - - with self.assertRaises(TypeError): - l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*']) - with self.assertRaises(TypeError): - l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*']) - with self.assertRaises(TypeError): - l.search_s(base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', [b'*']) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_search_results_have_bytes(self): - l = self._get_bytes_ldapobject() - base = self.server.suffix - result = l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, b'(cn=Foo*)', [b'*']) - result.sort() - dn, fields = result[0] - self.assertEqual(dn, b'cn=Foo1,%s' % base) - self.assertEqual(type(dn), bytes) + self.assertEqual(type(dn), str) for key, values in fields.items(): - self.assertEqual(type(key), bytes) + self.assertEqual(type(key), str) for value in values: self.assertEqual(type(value), bytes) - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_search_defaults(self): - l = self._get_bytes_ldapobject() - base = 'cn=Foo1,' + self.server.suffix - kwargs = dict( - base=base.encode('utf-8'), - scope=ldap.SCOPE_SUBTREE, - # filterstr=b'(objectClass=*)' - ) - expected = [ - ( - base, - {'cn': [b'Foo1'], 'objectClass': [b'organizationalRole']} - ), - ] - - result = l.search_s(**kwargs) - self.assertEqual(result, expected) - result = l.search_st(**kwargs) - self.assertEqual(result, expected) - result = l.search_ext_s(**kwargs) - self.assertEqual(result, expected) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_unset_bytesmode_search_warns_bytes(self): - l = self._get_bytes_ldapobject(explicit=False) - base = self.server.suffix - - l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*']) - l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*']) - l.search_s(base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', [b'*']) - - def _search_wrong_type(self, bytes_mode, strictness): - if bytes_mode: - l = self._get_bytes_ldapobject(bytes_strictness=strictness) - else: - l = self._open_ldap_conn(bytes_mode=False, - bytes_strictness=strictness) - base = 'cn=Foo1,' + self.server.suffix - if not bytes_mode: - base = base.encode('utf-8') - result = l.search_s(base, scope=ldap.SCOPE_SUBTREE) - return result[0][-1]['cn'] - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_silent(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - self._search_wrong_type(bytes_mode=True, strictness='silent') - self.assertEqual(w, []) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_warn(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - self._search_wrong_type(bytes_mode=True, strictness='warn') - self.assertEqual(len(w), 1) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_error(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - with self.assertRaises(TypeError): - self._search_wrong_type(bytes_mode=True, strictness='error') - self.assertEqual(w, []) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_textmode_silent(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - self._search_wrong_type(bytes_mode=True, strictness='silent') - self.assertEqual(w, []) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_textmode_warn(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - self._search_wrong_type(bytes_mode=True, strictness='warn') - self.assertEqual(len(w), 1) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_textmode_error(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - with self.assertRaises(TypeError): - self._search_wrong_type(bytes_mode=True, strictness='error') - self.assertEqual(w, []) - def test_search_accepts_unicode_dn(self): base = self.server.suffix l = self._ldap_conn @@ -315,7 +197,7 @@ def test_attrlist_accepts_unicode(self): result.sort() for dn, attrs in result: - self.assertIsInstance(dn, text_type) + self.assertIsInstance(dn, str) self.assertEqual(attrs, {}) def test001_search_subtree(self): @@ -418,37 +300,18 @@ def test_find_unique_entry(self): def test_search_subschema(self): l = self._ldap_conn dn = l.search_subschemasubentry_s() - self.assertIsInstance(dn, text_type) + self.assertIsInstance(dn, str) self.assertEqual(dn, "cn=Subschema") subschema = l.read_subschemasubentry_s(dn) self.assertIsInstance(subschema, dict) self.assertEqual( sorted(subschema), [ - u'attributeTypes', - u'ldapSyntaxes', - u'matchingRuleUse', - u'matchingRules', - u'objectClasses' - ] - ) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_search_subschema_have_bytes(self): - l = self._get_bytes_ldapobject() - dn = l.search_subschemasubentry_s() - self.assertIsInstance(dn, bytes) - self.assertEqual(dn, b"cn=Subschema") - subschema = l.read_subschemasubentry_s(dn) - self.assertIsInstance(subschema, dict) - self.assertEqual( - sorted(subschema), - [ - b'attributeTypes', - b'ldapSyntaxes', - b'matchingRuleUse', - b'matchingRules', - b'objectClasses' + 'attributeTypes', + 'ldapSyntaxes', + 'matchingRuleUse', + 'matchingRules', + 'objectClasses' ] ) @@ -465,7 +328,7 @@ def test004_enotconn(self): info = ldap_err.args[0]['info'] expected_info = os.strerror(errno.ENOTCONN) if info != expected_info: - self.fail("expected info=%r, got %d" % (expected_info, info)) + self.fail(f"expected info={expected_info!r}, got {info!r}") else: self.fail("expected SERVER_DOWN, got %r" % r) @@ -482,7 +345,7 @@ def test005_invalid_credentials(self): @requires_sasl() @requires_ldapi() - def test006_sasl_extenal_bind_s(self): + def test006_sasl_external_bind_s(self): l = self.ldap_object_class(self.server.ldapi_uri) l.sasl_external_bind_s() self.assertEqual(l.whoami_s(), 'dn:'+self.server.root_dn.lower()) @@ -491,6 +354,30 @@ def test006_sasl_extenal_bind_s(self): l.sasl_external_bind_s(authz_id=authz_id) self.assertEqual(l.whoami_s(), authz_id.lower()) + @requires_sasl() + @requires_ldapi() + def test006_sasl_options(self): + l = self.ldap_object_class(self.server.ldapi_uri) + + minssf = l.get_option(ldap.OPT_X_SASL_SSF_MIN) + self.assertGreaterEqual(minssf, 0) + self.assertLessEqual(minssf, 256) + maxssf = l.get_option(ldap.OPT_X_SASL_SSF_MAX) + self.assertGreaterEqual(maxssf, 0) + # libldap sets SSF_MAX to INT_MAX + self.assertLessEqual(maxssf, 2**31 - 1) + + l.set_option(ldap.OPT_X_SASL_SSF_MIN, 56) + l.set_option(ldap.OPT_X_SASL_SSF_MAX, 256) + self.assertEqual(l.get_option(ldap.OPT_X_SASL_SSF_MIN), 56) + self.assertEqual(l.get_option(ldap.OPT_X_SASL_SSF_MAX), 256) + + l.sasl_external_bind_s() + with self.assertRaisesRegex(ValueError, "write-only option"): + l.get_option(ldap.OPT_X_SASL_SSF_EXTERNAL) + l.set_option(ldap.OPT_X_SASL_SSF_EXTERNAL, 256) + self.assertEqual(l.whoami_s(), 'dn:' + self.server.root_dn.lower()) + def test007_timeout(self): l = self.ldap_object_class(self.server.ldap_uri) m = l.search_ext(self.server.suffix, ldap.SCOPE_SUBTREE, '(objectClass=*)') @@ -507,50 +394,14 @@ def assertIsSubclass(self, cls, other): def test_simple_bind_noarg(self): l = self.ldap_object_class(self.server.ldap_uri) l.simple_bind_s() - self.assertEqual(l.whoami_s(), u'') + self.assertEqual(l.whoami_s(), '') l = self.ldap_object_class(self.server.ldap_uri) l.simple_bind_s(None, None) - self.assertEqual(l.whoami_s(), u'') - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_ldapbyteswarning(self): - self.assertIsSubclass(ldap.LDAPBytesWarning, BytesWarning) - self.assertIsSubclass(ldap.LDAPBytesWarning, Warning) - self.assertIsInstance(self.server.suffix, text_type) - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - conn = self._get_bytes_ldapobject(explicit=False) - result = conn.search_s( - self.server.suffix, - ldap.SCOPE_SUBTREE, - b'(cn=Foo*)', - attrlist=[b'*'], - ) - self.assertEqual(len(result), 4) - - # ReconnectLDAP only emits one warning - self.assertGreaterEqual(len(w), 1, w) - msg = w[-1] - self.assertIs(msg.category, ldap.LDAPBytesWarning) - self.assertEqual( - text_type(msg.message), - "Received non-bytes value for 'base' in bytes " - "mode; please choose an explicit option for bytes_mode on your " - "LDAP connection" - ) - - @contextlib.contextmanager - def catch_byteswarnings(self, *args, **kwargs): - with warnings.catch_warnings(record=True) as w: - conn = self._get_bytes_ldapobject(*args, **kwargs) - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - yield conn, w + self.assertEqual(l.whoami_s(), '') def _check_byteswarning(self, warning, expected_message): self.assertIs(warning.category, ldap.LDAPBytesWarning) - self.assertIn(expected_message, text_type(warning.message)) + self.assertIn(expected_message, str(warning.message)) def _normalize(filename): # Python 2 likes to report the ".pyc" file in warnings, @@ -567,44 +418,6 @@ def _normalize(filename): linecache.getline(warning.filename, warning.lineno) ) - def _test_byteswarning_level_search(self, methodname): - with self.catch_byteswarnings(explicit=False) as (conn, w): - method = getattr(conn, methodname) - result = method( - self.server.suffix.encode('utf-8'), - ldap.SCOPE_SUBTREE, - '(cn=Foo*)', - attrlist=['*'], # CORRECT LINE - ) - self.assertEqual(len(result), 4) - - self.assertEqual(len(w), 2, w) - - self._check_byteswarning( - w[0], u"Received non-bytes value for 'filterstr'") - - self._check_byteswarning( - w[1], u"Received non-bytes value for 'attrlist'") - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_byteswarning_level_search(self): - self._test_byteswarning_level_search('search_s') - self._test_byteswarning_level_search('search_st') - self._test_byteswarning_level_search('search_ext_s') - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_byteswarning_initialize(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - bytes_uri = self.server.ldap_uri.decode('utf-8') - self.ldap_object_class(bytes_uri) # CORRECT LINE - - self.assertEqual(len(w), 1, w) - - self._check_byteswarning( - w[0], u"Under Python 2, python-ldap uses bytes by default.") - @requires_tls() def test_multiple_starttls(self): # Test for openldap does not re-register nss shutdown callbacks @@ -619,35 +432,52 @@ def test_multiple_starttls(self): l.simple_bind_s(self.server.root_dn, self.server.root_pw) self.assertEqual(l.whoami_s(), 'dn:' + self.server.root_dn) + @requires_tls() + @unittest.skipUnless( + hasattr(ldap, "OPT_X_TLS_PEERCERT"), + reason="Requires OPT_X_TLS_PEERCERT" + ) + def test_get_tls_peercert(self): + l = self.ldap_object_class(self.server.ldap_uri) + peercert = l.get_option(ldap.OPT_X_TLS_PEERCERT) + self.assertEqual(peercert, None) + with self.assertRaises(ValueError): + l.set_option(ldap.OPT_X_TLS_PEERCERT, b"") + + l.set_option(ldap.OPT_X_TLS_CACERTFILE, self.server.cafile) + l.set_option(ldap.OPT_X_TLS_NEWCTX, 0) + l.start_tls_s() + + peercert = l.get_option(ldap.OPT_X_TLS_PEERCERT) + self.assertTrue(peercert) + self.assertIsInstance(peercert, bytes) + + with open(self.server.servercert, "rb") as f: + server_cert = f.read() + pem_body = PEM_CERT_RE.search(server_cert).group(1) + server_der = base64.b64decode(pem_body) + + self.assertEqual(server_der, peercert) + def test_dse(self): dse = self._ldap_conn.read_rootdse_s() self.assertIsInstance(dse, dict) - self.assertEqual(dse[u'supportedLDAPVersion'], [b'3']) + self.assertEqual(dse['supportedLDAPVersion'], [b'3']) keys = set(dse) # SASL info may be missing in restricted build environments - keys.discard(u'supportedSASLMechanisms') + keys.discard('supportedSASLMechanisms') self.assertEqual( keys, - {u'configContext', u'entryDN', u'namingContexts', u'objectClass', - u'structuralObjectClass', u'subschemaSubentry', - u'supportedControl', u'supportedExtension', u'supportedFeatures', - u'supportedLDAPVersion'} + {'configContext', 'entryDN', 'namingContexts', 'objectClass', + 'structuralObjectClass', 'subschemaSubentry', + 'supportedControl', 'supportedExtension', 'supportedFeatures', + 'supportedLDAPVersion'} ) self.assertEqual( self._ldap_conn.get_naming_contexts(), [self.server.suffix.encode('utf-8')] ) - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_dse_bytes(self): - l = self._get_bytes_ldapobject() - dse = l.read_rootdse_s() - self.assertIsInstance(dse, dict) - self.assertEqual(dse[u'supportedLDAPVersion'], [b'3']) - self.assertEqual( - l.get_naming_contexts(), - [self.server.suffix.encode('utf-8')] - ) def test_compare_s_true(self): base = self.server.suffix @@ -661,6 +491,117 @@ def test_compare_s_false(self): result = l.compare_s('cn=Foo1,%s' % base, 'cn', b'Foo2') self.assertIs(result, False) + def test_compare_s_notfound(self): + base = self.server.suffix + l = self._ldap_conn + with self.assertRaises(ldap.NO_SUCH_OBJECT): + result = l.compare_s('cn=invalid,%s' % base, 'cn', b'Foo2') + + def test_compare_s_invalidattr(self): + base = self.server.suffix + l = self._ldap_conn + with self.assertRaises(ldap.UNDEFINED_TYPE): + result = l.compare_s('cn=Foo1,%s' % base, 'invalidattr', b'invalid') + + def test_compare_true_exception_contains_message_id(self): + base = self.server.suffix + l = self._ldap_conn + msgid = l.compare('cn=Foo1,%s' % base, 'cn', b'Foo1') + with self.assertRaises(ldap.COMPARE_TRUE) as cm: + l.result() + self.assertEqual(cm.exception.args[0]["msgid"], msgid) + + def test_async_search_no_such_object_exception_contains_message_id(self): + msgid = self._ldap_conn.search("CN=XXX", ldap.SCOPE_SUBTREE) + with self.assertRaises(ldap.NO_SUCH_OBJECT) as cm: + self._ldap_conn.result() + self.assertEqual(cm.exception.args[0]["msgid"], msgid) + + def test_passwd_s(self): + l = self._ldap_conn + + # first, create a user to change password on + dn = "cn=PasswordTest," + self.server.suffix + result, pmsg, msgid, ctrls = l.add_ext_s( + dn, + [ + ('objectClass', b'person'), + ('sn', b'PasswordTest'), + ('cn', b'PasswordTest'), + ('userPassword', b'initial'), + ] + ) + self.assertEqual(result, ldap.RES_ADD) + self.assertIsInstance(msgid, int) + self.assertEqual(pmsg, []) + self.assertEqual(ctrls, []) + + # try changing password with a wrong old-pw + with self.assertRaises(ldap.UNWILLING_TO_PERFORM): + l.passwd_s(dn, "bogus", "ignored") + + # have the server generate a new random pw + respoid, respvalue = l.passwd_s(dn, "initial", None, extract_newpw=True) + self.assertEqual(respoid, None) + + password = respvalue.genPasswd + self.assertIsInstance(password, bytes) + + # try changing password back + respoid, respvalue = l.passwd_s(dn, password, "initial") + self.assertEqual(respoid, None) + self.assertEqual(respvalue, None) + + l.delete_s(dn) + + def test_slapadd(self): + with self.assertRaises(ldap.INVALID_DN_SYNTAX): + self._ldap_conn.add_s("myAttribute=foobar,ou=Container,%s" % self.server.suffix, [ + ("objectClass", b'myClass'), + ("myAttribute", b'foobar'), + ]) + + self.server.slapadd(SCHEMA_TEMPLATE, ["-n0"]) + self.server.restart() + self.reset_connection() + + self._ldap_conn.add_s("myAttribute=foobar,ou=Container,%s" % self.server.suffix, [ + ("objectClass", b'myClass'), + ("myAttribute", b'foobar'), + ]) + + def test_valid_attrlist_parameter_types(self): + """Tests the case when a valid parameter type is passed to search_ext + + Any iterable which only contains strings should not raise any errors. + """ + + l = self._ldap_conn + + valid_attrlist_parameters = [{"a": "2"}, ["a", "b"], {}, set(), set(["a", "b"])] + + for attrlist in valid_attrlist_parameters: + out = l.search_ext( + "%s" % self.server.suffix, ldap.SCOPE_SUBTREE, attrlist=attrlist + ) + + def test_invalid_attrlist_parameter_types(self): + """Tests the case when an invalid parameter type is passed to search_ext + + Any object type that is either not a interable or does contain something + that isn't a string should raise a TypeError. The exception is the string type itself. + """ + + invalid_attrlist_parameters = [{1: 2}, 0, object(), "string"] + + l = self._ldap_conn + + for attrlist in invalid_attrlist_parameters: + with self.assertRaises(TypeError): + l.search_ext( + "%s" % self.server.suffix, ldap.SCOPE_SUBTREE, attrlist=attrlist + ) + class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject): """ @@ -695,35 +636,140 @@ def test103_reconnect_get_state(self): self.assertEqual( l1.__getstate__(), { - str('_last_bind'): ( + '_last_bind': ( 'simple_bind_s', (bind_dn, 'user1_pw'), {} ), - str('_options'): [(17, 3)], - str('_reconnects_done'): 0, - str('_retry_delay'): 60.0, - str('_retry_max'): 1, - str('_start_tls'): 0, - str('_trace_level'): ldap._trace_level, - str('_trace_stack_limit'): 5, - str('_uri'): self.server.ldap_uri, - str('bytes_mode'): l1.bytes_mode, - str('bytes_strictness'): l1.bytes_strictness, - str('timeout'): -1, + '_options': [(17, 3)], + '_reconnects_done': 0, + '_retry_delay': 60.0, + '_retry_max': 1, + '_start_tls': 0, + '_trace_level': ldap._trace_level, + '_trace_stack_limit': 5, + '_uri': self.server.ldap_uri, + 'timeout': -1, }, ) def test104_reconnect_restore(self): - l1 = self.ldap_object_class(self.server.ldap_uri) + l0 = self.ldap_object_class(self.server.ldap_uri) + + l0_state = pickle.dumps(l0) + del l0 + l1 = pickle.loads(l0_state) + self.assertEqual(l1.whoami_s(), '') + bind_dn = 'cn=user1,'+self.server.suffix l1.simple_bind_s(bind_dn, 'user1_pw') + self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn) l1_state = pickle.dumps(l1) del l1 + l2 = pickle.loads(l1_state) self.assertEqual(l2.whoami_s(), 'dn:'+bind_dn) + def test105_reconnect_restore(self): + l1 = self.ldap_object_class(self.server.ldap_uri, retry_max=2, retry_delay=1) + bind_dn = 'cn=user1,'+self.server.suffix + l1.simple_bind_s(bind_dn, 'user1_pw') + self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn) + self.server.terminate() + self.server.wait() + try: + l1.whoami_s() + except ldap.SERVER_DOWN: + pass + else: + self.assertEqual(True, False) + finally: + self.server.resume() + self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn) + + def test106_reconnect_restore(self): + """ + The idea of this test is to stop the LDAP server, make a search and ignore the `SERVER_DOWN` exception which happens after the reconnect timeout + and then re-use the same connection when the LDAP server is available again. + After starting the server the LDAP connection can be re-used again as it will reconnect on the next operation. + Prior to fixing PR !267 the connection was reestablished but no `bind()` was done resulting in a anonymous search which caused `INSUFFICIENT_ACCESS` when anonymous seach is disallowed. + """ + lo = self.ldap_object_class(self.server.ldap_uri, retry_max=2, retry_delay=1) + bind_dn = 'cn=user1,' + self.server.suffix + lo.simple_bind_s(bind_dn, 'user1_pw') + + dn = lo.whoami_s()[3:] + + self.server.terminate() + self.server.wait() + + # do a search, wait for the timeout, ignore SERVER_DOWN + with self.assertRaises(ldap.SERVER_DOWN): + lo.search_s(dn, ldap.SCOPE_BASE, '(objectClass=*)') + + self.server.resume() + + # try to use the connection again + lo.search_s(dn, ldap.SCOPE_BASE, '(objectClass=*)') + + def test107_reconnect_restore(self): + """ + The idea of this test is to restart the LDAP-Server while there are ongoing searches. + This causes :class:`ldap.UNAVAILABLE` to be raised (with |OpenLDAP|) for a short time. + To increase the chance of triggering this bug we are starting multiple threads + with a large number of retry attempts in a short amount of time. + """ + excs = [] + thread_count = 10 + run_time = 10.0 + start_barrier = threading.Barrier(thread_count + 1) # +1 for the main thread + + def _reconnect_search_thread(): + lo = self.ldap_object_class(self.server.ldap_uri) + bind_dn = 'cn=user1,' + self.server.suffix + lo.simple_bind_s(bind_dn, 'user1_pw') + lo._retry_max = 10E4 + lo._retry_delay = 0.001 + lo.search_ext_s(self.server.suffix, ldap.SCOPE_SUBTREE, "cn=user1", attrlist=["cn"]) + start_barrier.wait() + end_time = time.time() + run_time + while time.time() < end_time: + lo.search_ext_s(self.server.suffix, ldap.SCOPE_SUBTREE, filterstr="cn=user1", attrlist=["cn"]) + + def reconnect_search_thread(): + try: + _reconnect_search_thread() + except Exception as exc: + excs.append((str(exc), traceback.format_exc())) + + threads = [threading.Thread(target=reconnect_search_thread) for _ in range(thread_count)] + for t in threads: + t.start() + + start_barrier.wait() # wait until all threads are ready to start + self.server.restart() # restart after all threads have started their search loop + + for t in threads: + t.join() + + for exc, tb in excs[:5]: + print('Exception occurred', exc, tb) + self.assertEqual(excs, []) + + +@requires_init_fd() +class Test03_SimpleLDAPObjectWithFileno(Test00_SimpleLDAPObject): + def _open_ldap_conn(self, who=None, cred=None, **kwargs): + sock = socket.create_connection( + (self.server.hostname, self.server.port) + ) + result = super()._open_ldap_conn( + who=who, cred=cred, fileno=sock.fileno(), **kwargs + ) + sock.detach() + return result + if __name__ == '__main__': unittest.main() diff --git a/Tests/t_ldapurl.py b/Tests/t_ldapurl.py index 1408efcf..f9c72098 100644 --- a/Tests/t_ldapurl.py +++ b/Tests/t_ldapurl.py @@ -1,20 +1,15 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap's module ldapurl See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - import os import unittest +from urllib.parse import quote # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' -from ldap.compat import quote - import ldapurl from ldapurl import LDAPUrl @@ -38,18 +33,23 @@ class TestIsLDAPUrl(unittest.TestCase): 'ldap://host.com:6666/o=University%20of%20Michigan,':1, 'ldap://ldap.itd.umich.edu/c=GB?objectClass?one':1, 'ldap://ldap.question.com/o=Question%3f,c=US?mail':1, - 'ldap://ldap.netscape.com/o=Babsco,c=US??(int=%5c00%5c00%5c00%5c04)':1, + 'ldap://ldap.netscape.com/o=Babsco,c=US???(int=%5c00%5c00%5c00%5c04)':1, 'ldap:///??sub??bindname=cn=Manager%2co=Foo':1, 'ldap:///??sub??!bindname=cn=Manager%2co=Foo':1, # More examples from various sources 'ldap://ldap.nameflow.net:1389/c%3dDE':1, 'ldap://root.openldap.org/dc=openldap,dc=org':1, - 'ldap://root.openldap.org/dc=openldap,dc=org':1, + 'ldaps://root.openldap.org/dc=openldap,dc=org':1, 'ldap://x500.mh.se/o=Mitthogskolan,c=se????1.2.752.58.10.2=T.61':1, 'ldp://root.openldap.org/dc=openldap,dc=org':0, 'ldap://localhost:1389/ou%3DUnstructured%20testing%20tree%2Cdc%3Dstroeder%2Cdc%3Dcom??one':1, 'ldaps://ldap.example.com/c%3dDE':1, 'ldapi:///dc=stroeder,dc=de????x-saslmech=EXTERNAL':1, + 'LDAP://localhost': True, + 'LDAPS://localhost': True, + 'LDAPI://%2Frun%2Fldap.sock': True, + ' ldap://space.example': False, + 'ldap ://space.example': False, } def test_isLDAPUrl(self): @@ -61,6 +61,11 @@ def test_isLDAPUrl(self): ldap_url, result, expected, ) ) + if expected: + LDAPUrl(ldapUrl=ldap_url) + else: + with self.assertRaises(ValueError): + LDAPUrl(ldapUrl=ldap_url) class TestParseLDAPUrl(unittest.TestCase): @@ -149,6 +154,22 @@ class TestParseLDAPUrl(unittest.TestCase): dn='dc=stroeder,dc=com', ), ), + ( + 'LDAPS://localhost:12345/dc=stroeder,dc=com', + LDAPUrl( + urlscheme='ldaps', + hostport='localhost:12345', + dn='dc=stroeder,dc=com', + ), + ), + ( + 'ldaps://localhost:12345/dc=stroeder,dc=com', + LDAPUrl( + urlscheme='LDAPS', + hostport='localhost:12345', + dn='dc=stroeder,dc=com', + ), + ), ( 'ldapi://%2ftmp%2fopenldap2-1389/dc=stroeder,dc=com', LDAPUrl( @@ -164,7 +185,7 @@ def test_ldapurl(self): ldap_url_obj = LDAPUrl(ldapUrl=ldap_url_str) self.assertEqual( ldap_url_obj, test_ldap_url_obj, - 'Attributes of LDAPUrl(%s) are:\n%s\ninstead of:\n%s' % ( + 'Attributes of LDAPUrl({}) are:\n{}\ninstead of:\n{}'.format( repr(ldap_url_str), repr(ldap_url_obj), repr(test_ldap_url_obj), @@ -174,7 +195,7 @@ def test_ldapurl(self): unparsed_ldap_url_obj = LDAPUrl(ldapUrl=unparsed_ldap_url_str) self.assertEqual( unparsed_ldap_url_obj, test_ldap_url_obj, - 'Attributes of LDAPUrl(%s) are:\n%s\ninstead of:\n%s' % ( + 'Attributes of LDAPUrl({}) are:\n{}\ninstead of:\n{}'.format( repr(unparsed_ldap_url_str), repr(unparsed_ldap_url_obj), repr(test_ldap_url_obj), diff --git a/Tests/t_ldif.py b/Tests/t_ldif.py index 048b3f40..2f9ed679 100644 --- a/Tests/t_ldif.py +++ b/Tests/t_ldif.py @@ -1,12 +1,8 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap's module ldif See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - import os import textwrap import unittest diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..77783f8b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,107 @@ +[build-system] +requires = [ + "setuptools", +] +build-backend = "setuptools.build_meta" + +[project] +name = "python-ldap" +license.text = "python-ldap" # Replace with 'license' once Python 3.8 is dropped +dynamic = ["version"] +description = "Python modules for implementing LDAP clients" +authors = [ + {name = "python-ldap project", email = "python-ldap@python.org"}, +] +readme = "README" +requires-python = ">=3.6" +keywords = ["ldap", "directory", "authentication"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Programming Language :: C", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Database", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP", + "License :: OSI Approved :: Python Software Foundation License", +] +dependencies = [ + "pyasn1 >= 0.3.7", + "pyasn1_modules >= 0.1.5", +] + +[project.urls] +Homepage = "https://www.python-ldap.org/" +Documentation = "https://python-ldap.readthedocs.io/" +Repository = "https://github.com/python-ldap/python-ldap" +Download = "https://pypi.org/project/python-ldap/" +Changelog = "https://github.com/python-ldap/python-ldap/blob/main/CHANGES" + + + +[tool.setuptools] +zip-safe = false +include-package-data = true +license-files = ["LICENCE", "LICENCE.MIT"] +# Explicitly list all Python modules +py-modules = ["ldapurl", "ldif"] + +[tool.setuptools.dynamic] +version = {attr = "ldap.pkginfo.__version__"} + +[tool.setuptools.packages.find] +where = ["Lib"] + +[tool.setuptools.package-dir] +"" = "Lib" + +[tool.isort] +line_length = 88 +known_first_party = ["ldap", "_ldap", "ldapurl", "ldif", "slapdtest"] +sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] + +[tool.coverage.run] +branch = true +source = [ + "ldap", + "ldif", + "ldapurl", + "slapdtest", +] + +[tool.coverage.paths] +source = [ + "Lib/", + ".tox/*/lib/python*/site-packages/", +] + +[tool.coverage.report] +ignore_errors = false +precision = 1 +exclude_lines = [ + "pragma: no cover", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "if PY2", + "if not PY2", +] + +[tool.coverage.html] +directory = "build/htmlcov" +title = "python-ldap coverage report" diff --git a/setup.cfg b/setup.cfg index c6546068..fdb32fbc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,7 @@ +# Package metadata (for Setuptools 30.3 and later) +[metadata] +license_file = LICENCE + # Example for setup.cfg # You have to edit this file to reflect your system configuation @@ -12,14 +16,16 @@ # These defines needs OpenLDAP built with # ./configure --with-cyrus-sasl --with-tls -defines = HAVE_SASL HAVE_TLS HAVE_LIBLDAP_R +defines = HAVE_SASL HAVE_TLS extra_compile_args = extra_objects = +# Uncomment this if your libldap is not thread-safe and you need libldap_r +# instead # Example for full-featured build: # Support for StartTLS/LDAPS, SASL bind and reentrant libldap_r. -libs = ldap_r lber +#libs = ldap_r lber # Installation options [install] @@ -29,7 +35,7 @@ optimize = 1 # Linux distributors/packagers should adjust these settings [bdist_rpm] provides = python-ldap -requires = python libldap-2_4 +requires = python libldap-2 vendor = python-ldap project packager = python-ldap team distribution_name = openSUSE 11.x diff --git a/setup.py b/setup.py index e66ecbd6..f2e816be 100644 --- a/setup.py +++ b/setup.py @@ -1,21 +1,20 @@ """ -setup.py - Setup package with the help Python's DistUtils +setup.py - C extension module configuration for python-ldap See https://www.python-ldap.org/ for details. +This file handles only the C extension modules (_ldap) configuration, +while pyproject.toml handles all project metadata, dependencies, and other settings. """ import sys,os from setuptools import setup, Extension -if sys.version_info[0] == 2 and sys.version_info[1] < 7: - raise RuntimeError('This software requires Python 2.7 or 3.x.') -if sys.version_info[0] >= 3 and sys.version_info < (3, 4): - raise RuntimeError('The C API from Python 3.4+ is required.') +if sys.version_info < (3, 6): + raise RuntimeError( + 'The C API from Python 3.6+ is required, found %s' % sys.version_info + ) -if sys.version_info[0] >= 3: - from configparser import ConfigParser -else: - from ConfigParser import ConfigParser +from configparser import ConfigParser sys.path.insert(0, os.path.join(os.getcwd(), 'Lib/ldap')) import pkginfo @@ -57,53 +56,8 @@ class OpenLDAP2: LDAP_CLASS.extra_link_args.append('-pg') LDAP_CLASS.libs.append('gcov') -#-- Let distutils/setuptools do the rest -name = 'python-ldap' - +#-- C extension modules configuration only setup( - #-- Package description - name = name, - license=pkginfo.__license__, - version=pkginfo.__version__, - description = 'Python modules for implementing LDAP clients', - long_description = """python-ldap: - python-ldap provides an object-oriented API to access LDAP directory servers - from Python programs. Mainly it wraps the OpenLDAP 2.x libs for that purpose. - Additionally the package contains modules for other LDAP-related stuff - (e.g. processing LDIF, LDAPURLs, LDAPv3 schema, LDAPv3 extended operations - and controls, etc.). - """, - author = 'python-ldap project', - author_email = 'python-ldap@python.org', - url = 'https://www.python-ldap.org/', - download_url = 'https://pypi.org/project/python-ldap/', - classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'Operating System :: OS Independent', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Programming Language :: C', - - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - # Note: when updating Python versions, also change .travis.yml and tox.ini - - 'Topic :: Database', - 'Topic :: Internet', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP', - 'License :: OSI Approved :: Python Software Foundation License', - ], - #-- C extension modules ext_modules = [ Extension( '_ldap', @@ -119,15 +73,8 @@ class OpenLDAP2: 'Modules/berval.c', ], depends = [ - 'Modules/LDAPObject.h', - 'Modules/berval.h', - 'Modules/common.h', + 'Modules/pythonldap.h', 'Modules/constants_generated.h', - 'Modules/constants.h', - 'Modules/functions.h', - 'Modules/ldapcontrol.h', - 'Modules/message.h', - 'Modules/options.h', ], libraries = LDAP_CLASS.libs, include_dirs = ['Modules'] + LDAP_CLASS.include_dirs, @@ -137,7 +84,6 @@ class OpenLDAP2: extra_objects = LDAP_CLASS.extra_objects, runtime_library_dirs = (not sys.platform.startswith("win"))*LDAP_CLASS.library_dirs, define_macros = LDAP_CLASS.defines + \ - ('ldap_r' in LDAP_CLASS.libs or 'oldap_r' in LDAP_CLASS.libs)*[('HAVE_LIBLDAP_R',None)] + \ ('sasl' in LDAP_CLASS.libs or 'sasl2' in LDAP_CLASS.libs or 'libsasl' in LDAP_CLASS.libs)*[('HAVE_SASL',None)] + \ ('ssl' in LDAP_CLASS.libs and 'crypto' in LDAP_CLASS.libs)*[('HAVE_TLS',None)] + \ [ @@ -147,27 +93,4 @@ class OpenLDAP2: ] ), ], - #-- Python "stand alone" modules - py_modules = [ - 'ldapurl', - 'ldif', - - ], - packages = [ - 'ldap', - 'ldap.controls', - 'ldap.extop', - 'ldap.schema', - 'slapdtest', - ], - package_dir = {'': 'Lib',}, - data_files = LDAP_CLASS.extra_files, - include_package_data=True, - install_requires=[ - 'pyasn1 >= 0.3.7', - 'pyasn1_modules >= 0.1.5', - ], - zip_safe=False, - python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', - test_suite = 'Tests', ) diff --git a/tox.ini b/tox.ini index 1434ba01..0741ef29 100644 --- a/tox.ini +++ b/tox.ini @@ -4,57 +4,53 @@ # and then run "tox" from this directory. [tox] -# Note: when updating Python versions, also change setup.py and .travis.yml -envlist = py27,py34,py35,py36,py37,py38,{py2,py3}-nosasltls,doc,py3-trace,coverage-report +# Note: when updating Python versions, also change setup.py and .github/worlflows/* +envlist = py{39,310,311,312},py3-nosasltls,doc,py3-trace,pypy3.9 minver = 1.8 +[gh-actions] +python = + 3.9: py39, py3-trace, doc, py3-nosasltls + 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 + pypy3.9: pypy3.9 + pypy3.10: pypy3.10 + [testenv] -deps = coverage +deps = setuptools passenv = WITH_GCOV # - Enable BytesWarning # - Turn all warnings into exceptions. -# - 'ignore:the imp module is deprecated' is required to ignore import of 'imp' -# in distutils. Python < 3.6 use PendingDeprecationWarning; Python >= 3.6 use -# DeprecationWarning. +setenv = + CFLAGS=-Wno-int-in-bool-context -Werror -Werror=declaration-after-statement -std=c99 commands = {envpython} -bb -Werror \ - "-Wignore:the imp module is deprecated:DeprecationWarning" \ - "-Wignore:the imp module is deprecated:PendingDeprecationWarning" \ - "-Wignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working:DeprecationWarning" \ - -m coverage run --parallel setup.py \ - clean --all \ - test - -[testenv:py27] -# No warnings with Python 2.7 -passenv = {[testenv]passenv} -commands = {envpython} \ - -m coverage run --parallel setup.py test + -m unittest discover -v -s Tests -p 't_*' {posargs} -[testenv:py34] -# No warnings with Python 3.4 -passenv = {[testenv]passenv} -commands = {envpython} \ - -m coverage run --parallel setup.py test - -[testenv:py2-nosasltls] -basepython = python2 -deps = {[testenv]deps} -passenv = {[testenv]passenv} +[testenv:py312] +# Python 3.12 headers are incompatible with declaration-after-statement setenv = - CI_DISABLED=LDAPI:SASL:TLS -# rebuild without SASL and TLS, run without LDAPI -commands = {envpython} \ - -m coverage run --parallel setup.py \ - clean --all \ - build_ext -UHAVE_SASL,HAVE_TLS \ - test + CFLAGS=-Wno-int-in-bool-context -Werror -std=c99 + [testenv:py3-nosasltls] basepython = python3 -deps = {[testenv]deps} +# don't install, install dependencies manually +skip_install = true +deps = + {[testenv]deps} + pyasn1 + pyasn1_modules passenv = {[testenv]passenv} -setenv = {[testenv:py2-nosasltls]setenv} -commands = {[testenv:py2-nosasltls]commands} +setenv = + CI_DISABLED=LDAPI:SASL:TLS +# build and install without SASL and TLS, run without LDAPI +commands = + {envpython} setup.py clean --all + {envpython} setup.py build_ext -UHAVE_SASL,HAVE_TLS + {envpython} setup.py install --single-version-externally-managed --root=/ + {[testenv]commands} [testenv:py3-trace] basepython = python3 @@ -65,24 +61,35 @@ setenv = PYTHON_LDAP_TRACE_FILE={envtmpdir}/trace.log commands = {[testenv]commands} -[testenv:pypy] -# PyPy doesn't have working setup.py test -deps = pytest -commands = {envpython} -m pytest - -[testenv:pypy35] -# PyPy-5.9.0 -basepython = pypy3.5 -deps = {[testenv:pypy]deps} -commands = {[testenv:pypy]commands} +[testenv:c90] +setenv = + CFLAGS=-Wno-int-in-bool-context -Werror -Werror=declaration-after-statement -std=c90 +commands = {envpython} -Werror -c "import ldap" # we just test compilation here -[testenv:coverage-report] -deps = coverage -skip_install = true +[testenv:macos] +# Travis CI macOS image does not have slapd +# SDK libldap does not support ldap_init_fd +basepython = python3 +deps = {[testenv]deps} +passenv = {[testenv]passenv} +setenv = + CI_DISABLED=INIT_FD commands = - {envpython} -m coverage combine - {envpython} -m coverage report --show-missing - {envpython} -m coverage html + {envpython} -m unittest -v {posargs} \ + Tests/t_cidict.py \ + Tests/t_ldap_dn.py \ + Tests/t_ldap_filter.py \ + Tests/t_ldap_functions.py \ + Tests/t_ldap_modlist.py \ + Tests/t_ldap_schema_tokenizer.py \ + Tests/t_ldapurl.py \ + Tests/t_ldif.py \ + Tests/t_untested_mods.py + +[testenv:pypy3] +basepython = pypy3 +deps = pytest +commands = {envpython} -m pytest {posargs} [testenv:doc] basepython = python3 @@ -91,6 +98,7 @@ deps = markdown sphinx sphinxcontrib-spelling + setuptools commands = {envpython} setup.py check --restructuredtext --metadata --strict {envpython} -m markdown README -f {envtmpdir}/README.html