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/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..41306bec --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,23 @@ +If you found a bug in python-ldap, or would request a new feature, +this is the place to let us know. + +Please describe the issue and your environment here. + +--- + +Issue description: + + + + + + +Steps to reproduce: + + + +Operating system: + +Python version: + +python-ldap version: 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 03b366de..75a13538 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,10 @@ - # Auto-generated .*.swp *.pyc __pycache__/ .tox -.coverage* -!.coveragerc +/.cache +/.pytest_cache # shared libs installed by 'setup.py test' /Lib/*.so* @@ -17,3 +16,9 @@ __pycache__/ build/ dist/ PKG-INFO + +# generated in the sample workflow +/__venv__/ + +# test dirs +python-ldap-test-* diff --git a/.indent.pro b/.indent.pro new file mode 100644 index 00000000..4e8adc1b --- /dev/null +++ b/.indent.pro @@ -0,0 +1,31 @@ +--blank-lines-after-declarations +--blank-lines-after-procedures +--braces-after-func-def-line +--braces-on-if-line +--braces-on-struct-decl-line +--break-after-boolean-operator +--comment-indentation25 +--comment-line-length79 +--continue-at-parentheses +--dont-cuddle-do-while +--dont-cuddle-else +--indent-level4 +--line-length79 +--no-space-after-casts +--no-space-after-function-call-names +--no-space-after-parentheses +--no-tabs +--procnames-start-lines +--space-after-for +--space-after-if +--space-after-while +--swallow-optional-blank-lines +-T PyCFunction +-T PyObject +-T PyMethodDef +-T LDAP +-T LDAPMod +-T LDAPMessage +-T LDAPControl +-T LDAPObject +-T sasl_interact_t 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 14c0965d..00000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: python - -python: -- '2.7' -- '3.3' -- '3.4' -- '3.5' -- '3.6' -# Note: when updating Python versions, also change setup.py and tox.ini - -sudo: false - -cache: pip - -addons: - apt: - packages: - - ldap-utils - - slapd - -env: - - WITH_GCOV=1 - -install: - - pip install "pip>=7.1.0" - - pip install tox-travis tox codecov coverage - -script: tox - -after_success: - # gather Python coverage - - python -m coverage combine - # send Python and GCOV coverage - - codecov diff --git a/Build/build-openbsd/Makefile b/Build/build-openbsd/Makefile index ce91fafe..40ad81cb 100644 --- a/Build/build-openbsd/Makefile +++ b/Build/build-openbsd/Makefile @@ -10,7 +10,7 @@ HOMEPAGE= https://www.python-ldap.org/ FAKE= Yes CONFIGURE_STYLE= gnu SEPARATE_BUILD= Yes -EXTRACT_ONLY= +EXTRACT_ONLY= CONFIGURE_ARGS+= --with-ldap=${LOCALBASE} diff --git a/Build/build-openbsd/pkg/DESCR b/Build/build-openbsd/pkg/DESCR index d3cf0ccf..9e58e627 100644 --- a/Build/build-openbsd/pkg/DESCR +++ b/Build/build-openbsd/pkg/DESCR @@ -1,2 +1,2 @@ -This Python library provides access to the LDAP (Lightweight Directory Access +This Python library provides access to the LDAP (Lightweight Directory Access Protocol) RFC1823 C interface. diff --git a/Build/setup.cfg.mingw b/Build/setup.cfg.mingw index 16ab57fc..ffbd0d8a 100644 --- a/Build/setup.cfg.mingw +++ b/Build/setup.cfg.mingw @@ -15,8 +15,8 @@ defines = WIN32 library_dirs = C:/msys/1.0/home/mcicogni/openldap-mingw-build-4/openldap-2.2.18/libraries/libldap_r/.libs C:/msys/1.0/home/mcicogni/openldap-mingw-build-4/openldap-2.2.18/libraries/liblber/.libs C:\msys\1.0\home\mcicogni\openldap-mingw-build-4\openssl-0.9.7e include_dirs = C:/msys/1.0/home/mcicogni/openldap-mingw-build-4/openldap-2.2.18/include -extra_compile_args = -extra_objects = +extra_compile_args = +extra_objects = libs = ldap_r lber ssl crypto ws2_32 gdi32 diff --git a/Build/setup.cfg.suse-linux b/Build/setup.cfg.suse-linux index 3ea064d2..0a48ef2d 100644 --- a/Build/setup.cfg.suse-linux +++ b/Build/setup.cfg.suse-linux @@ -8,7 +8,7 @@ library_dirs = /usr/lib/sasl2 include_dirs = /usr/include/sasl -extra_compile_args = +extra_compile_args = extra_objects = # Example for full-featured SuSE build: diff --git a/CHANGES b/CHANGES index 113eb275..05fcf4b6 100644 --- a/CHANGES +++ b/CHANGES @@ -1,21 +1,437 @@ +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 + +This release brings two minor API changes: +- Long-deprecated functions `ldap.open()` and `ldap.init()` are removed +- `LDAPObject.compare_s()` and `compare_ext_s` return bool instead of 0 or 1 + +All changes since 3.0.0: + +Lib/ +* Remove long deprecated functions ldap.open() and ldap.init() +* LDAPObject.compare_s() and LDAPObject.compare_ext_s() now return a bool + instead of 1 or 0. +* Make iteration over cidict yield same values as keys() +* Fail if pyasn1 is not installed +* Fix parsing of PPolicyControl ASN.1 structure +* Use items() when appropriate in dict iteration +* Add support for tracing LDAP calls. Tracing can now be enabled with + the env var PYTHON_LDAP_TRACE_LEVEL and redirected to a file with + PYTHON_LDAP_TRACE_FILE. + (This is mainly intended for debugging and internal testing; the + configuration or output may change in future versions.) + +Modules/ +* Fix ref counting bug in LDAPmessage_to_python + +Doc/ +* Remove warning about unreleased version +* Doc: Replace Mac OS X -> macOS + +Tests/ +* Add tests and coverage for tracing +* Disable warnings-as-errors for Python 3.4 +* Fix assertTrue to assertEqual +* Mark several test values as bytes + +Lib/slapdtest/ +* Fix error message for missing commands +* Make SlapdObject a context manager +* Disable SASL external when missing SASL support +* Make SlapdObject.root_dn a property +* In SlapdObject, build include directives dynamically +* Move import statements to top level + +Code style: +* Add Makefile rules for automatic formatting of C and Python code +* Reformat and indent all C files +* Trim white space throughout the project + +Infrastructure: +* Add py3-trace tox environment to Travis CI config +* Add new Pytest cache directory to gitignore + +General: +* Update all pypi.python.org URLs to pypi.org + + ---------------------------------------------------------------- -Released 3.0.0 xxxx-xx-xx +Released 3.0.0 2018-03-12 + +Notable changes since 2.4.45 (please see detailed logs below): +* Python 3 support and bytes_mode + see: https://python-ldap.readthedocs.io/en/latest/bytes_mode.html +* The module `ldap.async` is renamed to `ldap.asyncsearch` +* New dependencies: pyasn1, pyasn1_modules +* Dropped support for Python 2.6 and 3.3 + + +Changes since 3.0.0b4: + +Lib/ +* Add bytes_strictness to allow configuring behavior on bytes/text mismatch + +Modules/ +* Add argument name to bytes mode TypeError +* Use correct integer types for BER encode/decode (fix for big endian machines) + +Test/ +* Set $LDAPNOINIT in all tests +* Add test for secure TLS default +* Ignore SASL methods in DSE test (fix for restricted environments) +* Remove filterstr workaround from syncrepl test +* Explicitly set TLS_REQUIRE_CERT option to TLS_HARD in test_tls_ext_noca + +Doc/ +* Link to bytes mode from text-string arguments in the ldap module + +Infrastructure: +* Include lber in list of libraries in setup.cfg + +---------------------------------------------------------------- +Released 3.0.0b4 2018-01-10 + +Changes since 3.0.0b3: + +Removed support for Python 3.3, which reached its end-of-life 2017-09-29. + +Lib/ +* Make default argument values work under bytes_mode +* Update use of map() to use list/set comprehensions instead + +Test/ +* Refactor syncrepl tests to run with bytes_mode + +Doc/ +* Document all_records attribute of LDIFRecordList + + +---------------------------------------------------------------- +Released 3.0.0b3 2017-12-20 + +Changes since 3.0.0b2: + +The functions `ldap.open()`, `ldap.init()`, `ldif.CreateLDIF()` +and `ldif.ParseLDIF()`, which were deprecated for over a decade, +are scheduled for removal in python-ldap 3.1. + +Infrastructure: +* Require setuptools to build +* Start running automatic tests on PyPy + +Lib/ +* When raising LDAPBytesWarning, give helpful code locations +* Use modern Python idioms in several places +* Avoid reimplementing UserDict.get() in cidict and models.Entry + +Doc/ +* Use https links + +Test/ +* Add reproducer for openldap's NSS shutdown/restart issue +* Make testing on non-Linux platforms easier + + +---------------------------------------------------------------- +Released 3.0.0b2 2017-12-11 + +Changes since 3.0.0b1: + +The module `ldap.async` is renamed to `ldap.asyncsearch`, due to +`async` becoming a keyword in Python 3.7. +The old module name is deprecated, but will be available as long +as Python 3.6 is supported. + +Lib/ +* Use custom ldap.LDAPBytesWarning class +* Rename ldap.async to ldap.asyncsearch + +Modules/ +* Support None for set_option(OPT_TIMEOUT) and OPT_NETWORK_TIMEOUT +* Fix error reporting of LDAPObject.set_option() +* Change memory handling in attrs_from_List() + +Test/ +* Remove workaround for OpenLDAP NSS issue + +Demo/ +* Use uniform shebang in all demos + +Doc/ +* Provide build deps for Alpine and CentOS +* Move sample workflow out of the main Contributing guide + +Infrastructure: +* Add valgrind target to check for memory leaks +* Minimal configuration for pytest + + +---------------------------------------------------------------- +Released 3.0.0b1 2017-12-04 Changes since 2.4.45: +(this list includes changes from 2.5.x) -Mandatory prerequisites: -- Python 2.7.x or 3.3+ -- pyasn1 0.3.7+ and pyasn1_modules 0.1.5+ +New dependencies (automatically installed when using pip): +* pyasn1 0.3.7+ +* pyasn1_modules 0.1.5+ -Python 3 support is merged from the pyldap fork (https://github.com/pyldap) +Python 3 support and bytes_mode: +* merged from the pyldap fork (https://github.com/pyldap) +* please see documentation on bytes_mode and text/bytes handling: + https://python-ldap.readthedocs.io/en/latest/bytes_mode.html + +Removed support for Python 2.6. Infrastructure: -- Add .gitignore -- Re-format README to ReStructured Text -- Setup for automatic testing using Travis CI +* Move to Git +* Don't define search path for includes and libs in the default setup.cfg +* Include sasl/sasl.h from the standard path +* Re-format README to ReStructured Text +* Setup for automatic testing using Travis CI +* Add coverage reporting for Python and C +* Add install requires into setup.py +* Remove distclean.sh in favor of make clean +* Use `package`, `depends`, `install_requires` in setup.py +* Add make target for scan-build (static analysis using clang) +* Add make target and suppression file for Valgrind (memory checker) Modules/ -(thanks to Michael Ströder) +* Remove unused LDAPberval helper functions +* Fix type conversion in page control +* Fix multiple ref leaks in error-handling code +* Fix reference leak in result4 +* Fix several compiler warnings +* Fix memory leak in whoami +* Fix internal error handling of LDAPControl_to_List() +* Fix two memory leaks and release GIL in encode_assertion_control +* Allow set_option() to set timeouts to infinity +and, thanks to Michael Ströder: * removed unused code schema.c * moved code from version.c to ldapmodule.c * removed obsolete back-ward compability constants from common.h @@ -24,7 +440,9 @@ Modules/ * assume C extension API for Python 2.7+ Lib/ -(thanks to Michael Ströder) +* Avoid eval() for getting module-level variables to fix running under pytest +* Compability changes for pyasn1 0.3 or newer +and, thanks to Michael Ströder: * ldap.__version__, ldap.__author__ and ldap.__license__ now imported from new sub-module ldap.pkginfo also to setup.py * Added safety assertion when importing _ldap: @@ -32,8 +450,6 @@ Lib/ * removed stand-alone module dsml * slapdtest.SlapdObject.restart() just restarts slapd without cleaning any data -* Compability changes for pyasn1 0.3.x or newer - (thanks to Ilya Etingof and Christian Heimes) * The methods SSSResponseControl.decodeControlValue() and VLVResponseControl.decodeControlValue() now follow the coding convention to use camel-cased ASN.1 name as class attribute name. @@ -52,24 +468,35 @@ Lib/ * module ldif now uses functions b64encode() and b64decode() * fixed pickling and restoring of ReconnectLDAPObject -Lib/slapdtest.py +Lib/slapdtest * Automatically try some common locations for SCHEMADIR * Ensure server is stopped when the process exits +* Check for LDAP schema and slapd binaries +* slapdtest is now a package and includes testing certificates Tests/ -(thanks to Michael Ströder) +* Expand cidict membership test +* Add test suite for binds +* Add test suite for edits +* Add a smoke-check for listall() and attribute_types() +* Add test case for SASL EXTERNAL auth +* Add tests for start_tls +* In CI, treat compiler warnings as fatal errors +* Added tests for ldap.syncrepl +and, thanks to Michael Ströder: * added explicit reconnect tests for ReconnectLDAPObject * scripts do not directly call SlapdTestCase.setUpClass() anymore * added LDIF test with folded, base64-encoded attribute * added more tests for sub-module ldap.dn -* added tests for ldap.syncrepl (thanks to Karl Kornel) -Tests/ -(thanks to pyldap contributors): -* Expand cidict membership test -* Add test suite for binds -* Add test suite for edits -* Add a smoke-check for listall() and attribute_types() +Doc/ +* Build documentation without the compiled C extension +* Merge contents from python-ldap.org +* Move reference documentation in its own section +* Document return value of {modify,add,delete}_ext_s() as a tuple +* Add tests for documentation (build & spelling) +* Link to documentation of old versions +* Add a contributing guide ---------------------------------------------------------------- Released 2.4.45 2017-10-09 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000..7fd68fc9 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,10 @@ +Thank you for your interest in python-ldap! + +If you wish to help, detailed instructions are in `Doc/contributing.rst`_, +and in `online documentation`_. + +.. _Doc/contributing.rst: Doc/contributing.rst +.. _online documentation: https://python-ldap.readthedocs.io/en/latest/contributing.html + + +Open-source veretans should find no surprises there. diff --git a/Demo/Lib/ldap/async/deltree.py b/Demo/Lib/ldap/async/deltree.py index 68d3643e..9db52c84 100644 --- a/Demo/Lib/ldap/async/deltree.py +++ b/Demo/Lib/ldap/async/deltree.py @@ -1,11 +1,9 @@ -from __future__ import print_function - import ldap,ldap.async class DeleteLeafs(ldap.async.AsyncSearchHandler): """ Class for deleting entries which are results of a search. - + DNs of Non-leaf entries are collected in DeleteLeafs.nonLeafEntries. """ _entryResultTypes = ldap.async._entryResultTypes 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/Lib/ldif/ldifcopy.py b/Demo/Lib/ldif/ldifcopy.py index 62cb3919..3bbe3f30 100644 --- a/Demo/Lib/ldif/ldifcopy.py +++ b/Demo/Lib/ldif/ldifcopy.py @@ -20,4 +20,3 @@ process_url_schemes=['file','ftp','http'] ) ldif_collector.parse() - 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 59c594ff..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 @@ -61,4 +60,3 @@ def print_result(search_result): res = ld.search_ext_s(base, scope, filter, attrlist = ['mail'], serverctrls = [mv]) print("Matched values control: %s" % control_filter) print_result(res) - diff --git a/Demo/options.py b/Demo/options.py index 8b4e2159..7a8ee9db 100644 --- a/Demo/options.py +++ b/Demo/options.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import ldap host="localhost:1390" @@ -24,7 +22,3 @@ #print("time limit:",l.get_option(ldap.OPT_TIMELIMIT)) print("Binding...") l.simple_bind_s("","") - - - - 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/README b/Demo/pyasn1/README index b5fc28f7..2ca95a6a 100644 --- a/Demo/pyasn1/README +++ b/Demo/pyasn1/README @@ -2,5 +2,5 @@ The sample modules/scripts herein require modules pyasn1 and pyasn1-modules. https://github.com/etingof/pyasn1 -https://pypi.python.org/pypi/pyasn1 -https://pypi.python.org/pypi/pyasn1-modules +https://pypi.org/project/pyasn1/ +https://pypi.org/project/pyasn1-modules/ 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 91909a3a..491172c0 100644 --- a/Demo/pyasn1/sessiontrack.py +++ b/Demo/pyasn1/sessiontrack.py @@ -1,5 +1,4 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- +#!/usr/bin/env python """ 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' @@ -58,4 +56,3 @@ ldap_url.attrs or ['*'], serverctrls=[st_ctrl] ) - 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 e4c62e8b..754b237a 100644 --- a/Demo/pyasn1/syncrepl.py +++ b/Demo/pyasn1/syncrepl.py @@ -1,5 +1,4 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- +#!/usr/bin/env python """ 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 @@ -45,9 +43,9 @@ def __init__(self, db_path, *args, **kwargs): if db_path: self.__data = shelve.open(db_path, 'c') else: - self.__data = dict() + self.__data = {} # We need this for later internal use - self.__presentUUIDs = dict() + self.__presentUUIDs = {} def close_db(self): # Close the data store properly to avoid corruption @@ -64,7 +62,7 @@ def syncrepl_entry(self, dn, attributes, uuid): logger.debug('dn=%r attributes=%r uuid=%r', dn, attributes, uuid) # First we determine the type of change we have here # (and store away the previous data for later if needed) - previous_attributes = dict() + previous_attributes = {} if uuid in self.__data: change_type = 'modify' previous_attributes = self.__data[uuid] @@ -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): @@ -93,14 +91,14 @@ def syncrepl_present(self,uuids,refreshDeletes=False): # If we have not been given any UUID values, # then we have recieved all the present controls... if uuids is None: - # We only do things if refreshDeletes is false as the syncrepl - # extension will call syncrepl_delete instead when it detects a + # We only do things if refreshDeletes is false as the syncrepl + # extension will call syncrepl_delete instead when it detects a # delete notice if refreshDeletes is 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 d6796baf..9fc14e41 100644 --- a/Demo/resiter.py +++ b/Demo/resiter.py @@ -2,9 +2,8 @@ Demo for using ldap.resiter.ResultProcessor written by Michael Stroeder -See http://www.python-ldap.org for details. +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 4d350f02..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 @@ -47,7 +46,7 @@ attr_type_filter = [ ('no_user_mod',[0]), ('usage',range(2)), - ] + ] ) except KeyError as e: print('***KeyError',str(e)) diff --git a/Demo/schema_tree.py b/Demo/schema_tree.py index 79c8a837..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]: @@ -98,4 +97,3 @@ def HTMLSchemaTree(schema,se_class,se_tree,se_oid,level): print('\n*** Attribute types tree ***\n') PrintSchemaTree(schema,ldap.schema.AttributeType,at_tree,'_',0) - diff --git a/Demo/simple.py b/Demo/simple.py index 6004c9ef..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", [""]) @@ -97,11 +96,10 @@ # res = l.search_s( - "ou=CSEE, o=UQ, c=AU", - _ldap.SCOPE_SUBTREE, + "ou=CSEE, o=UQ, c=AU", + _ldap.SCOPE_SUBTREE, "objectclass=*", ) print(res) l.unbind() - diff --git a/Demo/simplebrowse.py b/Demo/simplebrowse.py index aa88f67e..fd4563ae 100644 --- a/Demo/simplebrowse.py +++ b/Demo/simplebrowse.py @@ -1,9 +1,8 @@ -#! python +#!/usr/bin/env python # # simple LDAP server browsing example # -from __future__ import print_function import ldap from traceback import print_exc @@ -50,7 +49,7 @@ # We're not interested in attributes at this stage, so # we specify [] as the list of attribute names to retreive. # - for name,attrs in l.search_s(dn, ldap.SCOPE_ONELEVEL, + for name,attrs in l.search_s(dn, ldap.SCOPE_ONELEVEL, "objectclass=*", []): #-- shorten resulting dns for output brevity if name.startswith(dn+", "): @@ -100,7 +99,7 @@ print(" %-24s" % name) for k,vals in attrs.items(): for v in vals: - if len(v) > 200: + if len(v) > 200: v = `v[:200]` + \ ("... (%d bytes)" % len(v)) else: @@ -125,4 +124,3 @@ except: print_exc() - diff --git a/Doc/.gitignore b/Doc/.gitignore new file mode 100644 index 00000000..5e134217 --- /dev/null +++ b/Doc/.gitignore @@ -0,0 +1,2 @@ +/_build/ +/.build/ diff --git a/Doc/Makefile b/Doc/Makefile index 5c8c6f12..9ce697a5 100644 --- a/Doc/Makefile +++ b/Doc/Makefile @@ -1,68 +1,68 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d .build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html web htmlhelp latex changes linkcheck - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " web to make files usable by Sphinx.web" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " changes to make an overview over all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - -clean: - -rm -rf .build/* - -html: - mkdir -p .build/html .build/doctrees - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html - @echo - @echo "Build finished. The HTML pages are in .build/html." - -web: - mkdir -p .build/web .build/doctrees - $(SPHINXBUILD) -b web $(ALLSPHINXOPTS) .build/web - @echo - @echo "Build finished; now you can run" - @echo " python -m sphinx.web .build/web" - @echo "to start the server." - -htmlhelp: - mkdir -p .build/htmlhelp .build/doctrees - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in .build/htmlhelp." - -latex: - mkdir -p .build/latex .build/doctrees - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex - @echo - @echo "Build finished; the LaTeX files are in .build/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." - -changes: - mkdir -p .build/changes .build/doctrees - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes - @echo - @echo "The overview file is in .build/changes." - -linkcheck: - mkdir -p .build/linkcheck .build/doctrees - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in .build/linkcheck/output.txt." +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d .build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html web htmlhelp latex changes linkcheck + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " web to make files usable by Sphinx.web" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview over all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + +clean: + -rm -rf .build/* + +html: + mkdir -p .build/html .build/doctrees + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html + @echo + @echo "Build finished. The HTML pages are in .build/html." + +web: + mkdir -p .build/web .build/doctrees + $(SPHINXBUILD) -b web $(ALLSPHINXOPTS) .build/web + @echo + @echo "Build finished; now you can run" + @echo " python -m sphinx.web .build/web" + @echo "to start the server." + +htmlhelp: + mkdir -p .build/htmlhelp .build/doctrees + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in .build/htmlhelp." + +latex: + mkdir -p .build/latex .build/doctrees + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex + @echo + @echo "Build finished; the LaTeX files are in .build/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + mkdir -p .build/changes .build/doctrees + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes + @echo + @echo "The overview file is in .build/changes." + +linkcheck: + mkdir -p .build/linkcheck .build/doctrees + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in .build/linkcheck/output.txt." diff --git a/Doc/bytes_mode.rst b/Doc/bytes_mode.rst new file mode 100644 index 00000000..3a984bf0 --- /dev/null +++ b/Doc/bytes_mode.rst @@ -0,0 +1,41 @@ +.. _text-bytes: +.. _bytes_mode: + +Bytes/text management +===================== + +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). + +Attribute *values*, on the other hand, **MAY** +contain any type of data, including text. +To know what type of data is represented, python-ldap would need access to the +schema, which is not always available (nor always correct). +Thus, attribute values are *always* treated as ``bytes``. +Encoding/decoding to other formats – text, images, etc. – is left to the caller. + + +Historical note +--------------- + +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. + +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. + +In python-ldap 2.x (for Python 2), bytes were used for all fields, +including those guaranteed to be text. + +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. + +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 9c322de3..e79cfb34 100644 --- a/Doc/conf.py +++ b/Doc/conf.py @@ -1,133 +1,156 @@ -# -*- coding: utf-8 -*- -# -# python-ldap documentation build configuration file, created by -# sphinx-quickstart on Sat Mar 29 15:08:17 2008. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# The contents of this file are pickled, so don't put values in the namespace -# that aren't pickleable (module imports are okay, they're removed automatically). -# -# All configuration values have a default value; values that are commented out -# serve to show the default value. - -import sys - -# If your extensions are in another directory, add it here. -#sys.path.append('some/directory') - -# General configuration -# --------------------- - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['.templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General substitutions. -project = 'python-ldap' -copyright = '2008-2017, python-ldap project team' - -# The default replacements for |version| and |release|, also used in various -# other places throughout the built documents. -# -# The short X.Y version. -version = '2.5' -# The full version, including alpha/beta/rc tags. -release = '2.5.2.0' - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -today_fmt = '%B %d, %Y' - -# List of documents that shouldn't be included in the build. -#unused_docs = [] - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - - -# Options for HTML output -# ----------------------- - -# The style sheet to use for HTML and HTML Help pages. A file of that name -# must exist either in Sphinx' static/ path, or in one of the custom paths -# given in html_static_path. -html_style = 'pyramid.css' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['/usr/lib/python2.7/site-packages/sphinx/themes/pyramid/static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Content template for the index page. -#html_index = '' - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -html_use_modindex = True - -# If true, the reST sources are included in the HTML build as _sources/. -#html_copy_source = True - -# Output file base name for HTML help builder. -htmlhelp_basename = 'python-ldap-doc' - - -# Options for LaTeX output -# ------------------------ - -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, document class [howto/manual]). -latex_documents = [('index', 'python-ldap.tex', 'python-ldap Documentation', - 'python-ldap project', 'manual')] - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -latex_use_modindex = True +# +# python-ldap documentation build configuration file, created by +# sphinx-quickstart on Sat Mar 29 15:08:17 2008. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# The contents of this file are pickled, so don't put values in the namespace +# that aren't pickleable (module imports are okay, they're removed automatically). +# +# All configuration values have a default value; values that are commented out +# serve to show the default value. + +import datetime +import sys +import os + +# If your extensions are in another directory, add it here. +_doc_dir = os.path.dirname(__file__) +sys.path.append(_doc_dir) +sys.path.insert(0, os.path.join(_doc_dir, '../Lib/')) +sys.path.insert(0, os.path.join(_doc_dir, '../Lib/ldap')) + +# Import fake `_ldap` module +import fake_ldap_module_for_documentation + +# Now ldap can be used normally +from ldap import __version__ + +# General configuration +# --------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon', +] + +try: + import sphinxcontrib.spelling +except ImportError: + pass +else: + extensions.append('sphinxcontrib.spelling') + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['.templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The root toctree document. +root_doc = 'index' + +# General substitutions. +project = 'python-ldap' +copyright = 'python-ldap project team' + +# The default replacements for |version| and |release|, also used in various +# other places throughout the built documents. +# +# The short X.Y version. +version = '.'.join(__version__.split('.')[:2]) +# The full version, including alpha/beta/rc tags. +release = __version__ + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# Options for HTML output +# ----------------------- + +# The style sheet to use for HTML and HTML Help pages. A file of that name +# must exist either in Sphinx' static/ path, or in one of the custom paths +# given in html_static_path. +#html_style = 'pyramid.css' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +#html_static_path = ['/usr/lib/python2.7/site-packages/sphinx/themes/pyramid/static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Content template for the index page. +#html_index = '' + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +html_use_modindex = True + +# If true, the reST sources are included in the HTML build as _sources/. +#html_copy_source = True + +# Output file base name for HTML help builder. +htmlhelp_basename = 'python-ldap-doc' + + +# Options for LaTeX output +# ------------------------ + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, document class [howto/manual]). +latex_documents = [('index', 'python-ldap.tex', 'python-ldap Documentation', + 'python-ldap project', 'manual')] + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +latex_use_modindex = True + +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} diff --git a/Doc/contributing.rst b/Doc/contributing.rst new file mode 100644 index 00000000..de63a2e3 --- /dev/null +++ b/Doc/contributing.rst @@ -0,0 +1,230 @@ +.. highlight:: console + +Contributing to python-ldap +*************************** + +Thank you for your interest in python-ldap! +If you'd like to contribute (be it code, documentation, maintenance effort, +or anything else), this guide is for you. + + +.. toctree:: + :hidden: + + sample_workflow.rst + + +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 no obligation to provide assistance. + +So, keep it friendly, respectful, and supportive! + + +Mailing list +------------ + +Discussion about the use and future of python-ldap occurs in +the ``python-ldap@python.org`` mailing list. + +It's also the channel to use if documentation (including this guide) is not +clear to you. +Do try searching around before you ask on the list, though! + +You can `subscribe or unsubscribe`_ to this list or browse the `list archive`_. + +.. _subscribe or unsubscribe: https://mail.python.org/mailman/listinfo/python-ldap +.. _list archive: https://mail.python.org/pipermail/python-ldap/ + + +Issues +------ + +Please report bugs, missing features and other issues to `the bug tracker`_ +at GitHub. You will need a GitHub account for that. + +If you prefer not to open a GitHub account, you're always welcome to use the +mailing list. + + +Security Contact +---------------- + +If you found a security issue that should not be discussed publicly, +please e-mail the maintainer at ``pviktori@redhat.com``. +If required, write to coordinate a more secure channel. + +All other communication should be public. + + +Contributing code +================= + +If you're used to open-source Python development with Git, here's the gist: + +* ``git clone https://github.com/python-ldap/python-ldap`` +* Use GitHub for `the bug tracker`_ and pull requests. +* Run tests with `tox`_; ignore Python interpreters you don't have locally. + +.. _the bug tracker: https://github.com/python-ldap/python-ldap/issues +.. _tox: https://tox.readthedocs.io/en/latest/ + +* 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`_. + +.. _Read the Docs: https://python-ldap.readthedocs.io/ + +If you're new to some aspect of the project, you're welcome to use (or adapt) +our :ref:`sample workflow `. + + +.. _additional tests: + +Additional tests and scripts +============================ + +We use several specialized tools for debugging and maintenance. + +Make targets +------------ + +Make targets currently use the ``python3`` executable. +Specify a different one using, for example:: + + make PYTHON=/usr/local/bin/python + +Notable targets are: + +``make autoformat`` + Automatically re-formats C and Python code to conform to Python style + guides (`PEP 7`_ and `PEP 8`_). + Note that no backups are made – please commit any other changes before + using this target. + + 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/ + +``make lcov lcov-open`` + Generate and view test coverage for C code. + Requires LCOV_. + +``make scan-build`` + Run static analysis. Requires ``clang``. + +``make valgrind`` + Run Valgrind_ to check for memory leaks. Requires ``valgrind`` and + a Python suppression file, which you can specify as ``PYTHON_SUPP``, e.g.:: + + make valgrind PYTHON_SUPP=/your/path/to/valgrind-python.supp + + The suppression file is ``Misc/valgrind-python.supp`` in the Python + source distribution, and it's frequently packaged together with + Python development headers. + +.. _LCOV: https://github.com/linux-test-project/lcov +.. _Valgrind: http://valgrind.org/ + + +Reference leak tests +-------------------- + +Reference leak tests require a *pydebug* build of CPython and `pytest`_ with +`pytest-leaks`_ plugin. A *pydebug* build has a global reference counter, which +keeps track of all reference increments and decrements. The leak plugin runs +each test multiple times and checks if the reference count increases. + +.. _pytest: https://docs.pytest.org/en/latest/ +.. _pytest-leaks: https://pypi.org/project/pytest-leaks/ + +Download and compile the *pydebug* build:: + + $ curl -O https://www.python.org/ftp/python/3.6.3/Python-3.6.3.tar.xz + $ tar xJf Python-3.6.3.tar.xz + $ cd Python-3.6.3 + $ ./configure --with-pydebug + $ make + +Create a virtual environment with the *pydebug* build:: + + $ ./python -m venv /tmp/refleak + $ /tmp/refleak/bin/pip install pytest pytest-leaks + +Run reference leak tests:: + + $ cd path/to/python-ldap + $ /tmp/refleak/bin/pip install --upgrade . + $ /tmp/refleak/bin/pytest -v -R: + +Run ``/tmp/refleak/bin/pip install --upgrade .`` every time a file outside +of ``Tests/`` is modified. + + +.. _committer instructions: + +Instructions for core committers +================================ + +If you have the authority (and responsibility) of merging changes from others, +remember: + +* All code changes need to be reviewed by someone other than the author. + +* Tests must always pass. New features without tests shall *not* pass review. + +* Make sure commit messages don't use GitHub-specific link syntax. + Use the full URL, e.g. ``https://github.com/python-ldap/python-ldap/issues/50`` + instead of ``#20``. + + * Exception: it's fine to use the short form in the summary line of a merge + commit, if the full URL appears later. + * It's OK to use shortcuts in GitHub *discussions*, where they are not + hashed into immutable history. + +* Make a merge commit if the contribution contains several well-isolated + separate commits with good descriptions. Use *squash-and-merge* (or + *fast-forward* from a command line) for all other cases. + +* It's OK to push small changes into a pull request. If you do this, document + what you have done (so the contributor can learn for the future), and get + their :abbr:`ACK (confirmation)` before merging. + +* When squashing, do edit commit messages to add references to the pull request + and relevant discussions/issues, and to conform to Git best practices. + + * Consider making the summary line suitable for the CHANGES document, + and starting it with a prefix like ``Lib:`` or ``Tests:``. + +If you have good reason to break the “rules”, go ahead and break them, +but mention why. + + +Instructions for release managers +================================= + +If you are tasked with releasing python-ldap, remember to: + +* Bump all instances of the version number. +* 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 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/fake_ldap_module_for_documentation.py b/Doc/fake_ldap_module_for_documentation.py new file mode 100644 index 00000000..30807819 --- /dev/null +++ b/Doc/fake_ldap_module_for_documentation.py @@ -0,0 +1,30 @@ +""" +A module that mocks `_ldap` for the purposes of generating documentation + +This module provides placeholders for the contents of `_ldap`, making it +possible to generate documentation even _ldap is not compiled. +It should also make the documentation independent of which features are +available in the system OpenLDAP library. + +The overly long module name will show up in AttributeError messages, +hinting that this is not the actual _ldap. + +See https://www.python-ldap.org/ for details. +""" + +import sys + +# Cause `import _ldap` to import this module instead of the actual `_ldap`. +sys.modules['_ldap'] = sys.modules[__name__] + +from constants import CONSTANTS +from pkginfo import __version__ + +for constant in CONSTANTS: + globals()[constant.name] = constant + +def get_option(num): + pass + +class LDAPError: + pass diff --git a/Doc/faq.rst b/Doc/faq.rst new file mode 100644 index 00000000..39a6743c --- /dev/null +++ b/Doc/faq.rst @@ -0,0 +1,194 @@ +python-ldap FAQ +=============== + +Project +------- + +**Q**: Is python-ldap yet another abandon-ware project? + + **A1**: “Jump on in.” + + **A2**: “Jump into the C ;-)” + + **A3**: see file CHANGES in source distribution + or `repository`_. + +.. _repository: https://github.com/python-ldap/python-ldap/blob/main/CHANGES + + +Usage +----- + +**Q**: Does it work with Python 3? + + **A0**: Yes, from 3.0 on. + + **A1**. For earlier versions, there's `pyldap`_, an independent fork + now merged into python-ldap. + +.. _pyldap: https://pypi.org/project/pyldap/ + + +**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. + + +**Q**: My code imports module ``_ldap``. +That used to work, but after an upgrade it does not work anymore. Why? + + **A**: Despite some outdated programming examples, the extension module + ``_ldap`` **MUST NOT** be imported directly, unless you really know what + you're doing (e.g. for internal regression testing). + + Import ``ldap`` instead, which is a Python wrapper around ``_ldap`` + 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 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. + Client-chasing of referrals is a broken concept, since LDAPv3 does not specify + which credentials to use when chasing the referral. Windows clients are supposed + to simply use their Windows credentials, but this does not work in general when + chasing referrals received from and pointing to arbitrary LDAP servers. + + Therefore, per default, ``libldap`` automatically chases the referrals + internally with an *anonymous* access which fails with MS AD. + + So, the best thing to do is to switch this behaviour off:: + + 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 + mean to be using a synchronous call + (see detailed explanation in :ref:`sending-ldap-requests`). + +**Q**: Can I use LDAPv2 via python-ldap? + + **A**: Yes, by explicitly setting the class attribute + :attr:`~ldap.LDAPObject.protocol_version`. + + You should not do that nowadays since + `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 +---------- + +**Q**: Does it work with Windows 32? + + **A**: Yes. You can find links to unofficial pre-compiled packages + for Windows on the :ref:`installing` page. + + +**Q**: Can python-ldap be built against OpenLDAP 2.3 libs or older? + + **A**: No. + The needed minimal version of OpenLDAP is documented in :ref:`build prerequisites`. + Patched builds of python-ldap linked to older libs are not supported by the + python-ldap project. + + +**Q**: During build there are warning messages displayed +telling Lib/ldap.py and Lib/ldap/schema.py are not found:: + + warning: build_py: file Lib/ldap.py (for module ldap) not found + warning: build_py: file Lib/ldap/schema.py (for module ldap.schema) not found + +.. + + **A**: ``ldap`` and ``ldap.schema`` are both module packages + (directories containing various sub-modules). + The messages above are falsely produced by DistUtils. + Don't worry about it. + +.. _install-macosx: + +**Q**: What's the correct way to install on macOS? + + **A**:: + + xcode-select --install + pip install python-ldap \ + --global-option=build_ext \ + --global-option="-I$(xcrun --show-sdk-path)/usr/include/sasl" + + +**Q**: While importing module ``ldap``, some shared lib files are not found. +The error message looks similar to this:: + + ImportError: ld.so.1: /usr/local/bin/python: fatal: liblber.so.2: open failed: No such file or directory + +.. + + **A1**: You need to make sure that the path to ``liblber.so.2`` and + ``libldap.so.2`` is in your ``LD_LIBRARY_PATH`` environment variable. + + **A2**: Alternatively, if you're on Linux, you can add the path to + ``liblber.so.2`` and ``libldap.so.2`` to ``/etc/ld.so.conf`` + and invoke the command ``ldconfig`` afterwards. + + + +Historic +-------- + +**Q**: Can python-ldap 2.x be built against Netscape, Mozilla or Novell libs? + + **A**: Nope. + + +**Q**: My binary version of python-ldap was build with LDAP libs 3.3. +But the python-ldap docs say LDAP libs 2.x are needed. I'm confused! + + Short answer: + See answer above and the :ref:`installing` page for + a more recent version. + + Long answer: + E.g. some Win32 DLLs floating around for download are based on + the old Umich LDAP code which is not maintained anymore for + *many* years! Last Umich 3.3 release was 1997 if I remember correctly. + + The OpenLDAP project took over the Umich code and started releasing + OpenLDAP 1.x series mainly fixing bugs and doing some improvements + to the database backend. Still, only LDAPv2 was supported at server + and client side. (Many commercial vendors also derived their products + from the Umich code.) + + OpenLDAP 2.x is a full-fledged LDAPv3 implementation. It has + its roots in Umich code but has many more features/improvements. + + +**Q**: While importing module ``ldap``, there are undefined references reported. +The error message looks similar to this:: + + ImportError: /usr/local/lib/libldap.so.2: undefined symbol: res_query + +.. + + **A**: Especially on older Linux systems, you might have to explicitly link + against ``libresolv``. + + Tweak ``setup.cfg`` to contain this line:: + + libs = lber ldap resolv diff --git a/Doc/index.rst b/Doc/index.rst index 1f9bf6aa..fcf90059 100644 --- a/Doc/index.rst +++ b/Doc/index.rst @@ -1,80 +1,83 @@ -########################## -python-ldap Documentation -########################## - -.. topic:: Abstract - - This document describes the package python-ldap with its various modules. - - Depending on what you want to do this manual assumes basic to expert - knowledge about the Python language and the LDAP standard (LDAPv3). - - -******** -Contents -******** - -.. toctree:: - :maxdepth: 3 - - installing.rst - ldap.rst - ldap-async.rst - ldap-controls.rst - ldap-dn.rst - ldap-extop.rst - ldap-filter.rst - ldap-modlist.rst - ldap-resiter.rst - ldap-schema.rst - ldap-syncrepl.rst - ldap-sasl.rst - ldif.rst - ldapurl.rst - slapdtest.rst - - - -********************* -Bytes/text management -********************* - -The LDAP protocol states that some fields (distinguised names, relative distinguished names, -attribute names, queries) be encoded in UTF-8; some other (mostly attribute *values*) **MAY** -contain any type of data, and thus be treated as bytes. - -In Python 2, ``python-ldap`` used bytes for all fields, including those guaranteed to be text. -In order to support Python 3, this distinction is made explicit. This is done -through the ``bytes_mode`` flag to ``ldap.initialize()``. - -When porting from ``python-ldap`` 2.x, users are advised to update their code to set ``bytes_mode=False`` -on calls to these methods. -Under Python 2, ``python-pyldap`` aggressively checks the type of provided arguments, and will raise a ``TypeError`` -for any invalid parameter. -However, if the ``bytes_mode`` kwarg isn't provided, ``pyldap`` will only -raise warnings. - -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('login', 'secret_password') - >>> results = con.search_s('ou=people,dc=example,dc=org', ldap.SCOPE_SUBTREE, "(cn=Raphaël)") - >>> results - [ - ("cn=Raphaël,ou=people,dc=example,dc=org", { - 'cn': [b'Rapha\xc3\xabl'], - 'sn': [b'Barrois'], - }), - ] - - -****************** -Indices and tables -****************** - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +python-ldap +=========== + + +What is python-ldap? +-------------------- + +python-ldap provides an object-oriented API to access `LDAP`_ +directory servers from `Python`_ programs. + +For LDAP operations the module wraps `OpenLDAP`_'s +client library, *libldap*. + +Additionally, the package contains modules for other LDAP-related stuff: + + * `LDIF`_ parsing and generation + * LDAP URLs + * LDAPv3 subschema + +.. _LDAP: https://en.wikipedia.org/wiki/Ldap +.. _Python: https://www.python.org/ +.. _OpenLDAP: https://www.openldap.org/ +.. _LDIF: https://en.wikipedia.org/wiki/LDIF + + +Get it! +------- + +:ref:`Installation instructions ` are available for +several platforms. + +Source code can be obtained using Git:: + + git clone https://github.com/python-ldap/python-ldap + + +Mailing list +------------ + +Discussion about the use and future of python-ldap occurs in +the ``python-ldap@python.org`` mailing list. + +You can `subscribe or unsubscribe`_ to this list or browse the `list archive`_. + +.. _subscribe or unsubscribe: https://mail.python.org/mailman/listinfo/python-ldap +.. _list archive: https://mail.python.org/pipermail/python-ldap/ + + +Documentation +------------- + +The documentation for python-ldap 3.x is hosted at `Read the Docs`_. + +You can switch between versions of the library, or download PDF or HTML +versions for offline use, using the sidebar on the right. + +Documentation for some older versions is available for download at the +`GitHub release page`_. + +.. _Read the Docs: https://python-ldap.readthedocs.io/en/latest/ +.. _GitHub release page: https://github.com/python-ldap/python-ldap/releases + + +Contents +-------- + +.. toctree:: + :maxdepth: 2 + + installing.rst + bytes_mode.rst + reference/index.rst + resources.rst + contributing.rst + faq.rst + + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/Doc/installing.rst b/Doc/installing.rst index bcc41be3..1c7ec8c3 100644 --- a/Doc/installing.rst +++ b/Doc/installing.rst @@ -1,19 +1,123 @@ -*********************** -Building and installing -*********************** +.. highlight:: console -python-ldap is built and installed using the Python DistUtils installed -along with your Python installation: +.. _installing: -:: +Installing python-ldap +###################### + + +Installing from PyPI +==================== + +The preferred point for downloading the “official” source distribution +is the `PyPI repository`_ which supports installing via `pip`_. +For example:: + + $ python -m pip install python-ldap + +.. _PyPI repository: https://pypi.org/project/python-ldap/ +.. _pip: https://pip.pypa.io/en/stable/ + +For installing from PyPI, you will need the same :ref:`build prerequisites` +as when installing from source. + +We do not currently provide pre-built packages (wheels). + + +Furthermore, python-ldap requires the modules `pyasn1`_ and `pyasn1-modules`_. +``pip`` will install these automatically. + +.. _pyasn1: https://pypi.org/project/pyasn1/ +.. _pyasn1-modules: https://pypi.org/project/pyasn1-modules/ + + +Pre-built Binaries +================== + +Because distributions seem to be all over the place, this page +tries to list all the current ones we know of. + +Note that the python-ldap team is not responsible for the binary packages +except the sources you can grab from the PyPI page. Also note that binary +packages are most times not up to date. If you experience troubles +with a binary package, it would be nice if you try to build a recent version +of python-ldap before submitting a bug report to make sure you did not +hit a problem already fixed in recent releases. + +`openSUSE Linux `_ +--------------------------------------------- + +Ships with python-ldap and there's an additional +`download repository `_ +which contains builds of latest releases +(see also `OBS package `_). + +`Debian Linux `_ +---------------------------------------- + +Have a look into the +`Debian Package Tracker `_ +to get up to date information which versions are available. + + +Windows +------- + +Unofficial binary builds for Windows are provided by Christoph Gohlke, available at +`python-ldap-build `_. + + +`FreeBSD `_ +------------------------------------- + +The CVS repository of FreeBSD contains the package +`py-ldap `_ + +macOS +----- + +You can install directly with pip. First install Xcode command line tools:: + + $ xcode-select --install + +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 - python setup.py build - python setup.py install +If using Homebrew:: -If you have more than one Python interpreter installed locally you should + $ brew install openldap + $ pip install python-ldap + + +.. _install-source: + +Installing from Source +====================== + + +python-ldap is built and installed using modern Python packaging standards +with pyproject.toml configuration. From a source repository:: + + $ pip 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. -See further instructions for using DistUtils here: https://docs.python.org/2.7/install/index.html +Further instructions can be found in :ref:`Setuptools documentation +`. + + +.. _build prerequisites: Build prerequisites =================== @@ -21,34 +125,75 @@ Build prerequisites The following software packages are required to be installed on the local system when building python-ldap: -- Python version 2.3 or later including its development files: https://www.python.org/ -- OpenLDAP client libs version 2.4.11 or later: https://www.openldap.org/ - It is not possible and not supported to build with prior versions. -- OpenSSL (optional): https://www.openssl.org/ -- cyrus-sasl (optional): https://www.cyrusimap.org/sasl/ -- Kerberos libs, MIT or heimdal (optional) +- `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. +- `OpenSSL`_ (optional) +- `Cyrus SASL`_ (optional) +- Kerberos libraries, MIT or Heimdal (optional) + +.. _Python: https://www.python.org/ +.. _OpenLDAP: https://www.openldap.org/ +.. _OpenSSL: https://www.openssl.org/ +.. _Cyrus SASL: https://www.cyrusimap.org/sasl/ + + +Alpine +------ + +Packages for building:: + + # apk add build-base openldap-dev python3-dev -Installation prerequisites -========================== +CentOS +------ -Furthermore it is required that modules -:py:mod:`pyasn1` and :py:mod:`pyasn1_modules` -are installed. +Packages for building:: - https://github.com/etingof/pyasn1 + # yum groupinstall "Development tools" + # yum install openldap-devel python-devel - https://pypi.python.org/pypi/pyasn1 +Debian +------ - https://pypi.python.org/pypi/pyasn1-modules +Packages for building:: -setup.cfg -========= + # apt-get install build-essential ldap-utils \ + libldap2-dev libsasl2-dev -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 the -documentation of Python's DistUtils. +Packages for building and testing:: + + # 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 \ + python3-devel python3-tox \ + lcov clang-analyzer valgrind + +.. note:: + + ``openldap-2.4.45-2`` (Fedora 26), ``openldap-2.4.45-4`` (Fedora 27) or + newer are required. + + +``setup.cfg`` +============= + +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 `. .. data:: library_dirs @@ -68,40 +213,48 @@ documentation of Python's DistUtils. .. data:: extra_objects + + .. _libs-used-label: -Libs used ---------- +Libraries used +--------------- -.. data:noindex: ldap -.. data:noindex: ldap_r +.. data:: ldap + :noindex: +.. data:: ldap_r + :noindex: - The LDAP protocol library of OpenLDAP. ldap_r is the reentrant version + The LDAP protocol library of OpenLDAP. ``ldap_r`` is the reentrant version and should be preferred. -.. data:noindex: lber +.. data:: lber + :noindex: The BER encoder/decoder library of OpenLDAP. -.. data:noindex: sasl2 +.. data:: sasl2 + :noindex: - The Cyrus-SASL library if needed and present during build + The Cyrus-SASL library (optional) -.. data:noindex: ssl +.. data:: ssl + :noindex: - The SSL/TLS library of OpenSSL if needed and present during build + The SSL/TLS library of OpenSSL (optional) -.. data:noindex: crypto +.. data:: crypto + :noindex: - The basic cryptographic library of OpenSSL if needed and present during build + The basic cryptographic library of OpenSSL (optional) Example -============= +------- The following example is for a full-featured build (including SSL and SASL support) of python-ldap with OpenLDAP installed in a different prefix directory -(here /opt/openldap-2.4) and SASL header files found in /usr/include/sasl. -Debugging symbols are preserved with compile option -g. +(here ``/opt/openldap-2.4``) and SASL header files found in /usr/include/sasl. +Debugging symbols are preserved with compile option ``-g``. :: @@ -113,4 +266,3 @@ Debugging symbols are preserved with compile option -g. extra_objects = libs = ldap_r lber sasl2 ssl crypto - diff --git a/Doc/reference/index.rst b/Doc/reference/index.rst new file mode 100644 index 00000000..fa45b84d --- /dev/null +++ b/Doc/reference/index.rst @@ -0,0 +1,26 @@ +python-ldap Reference Documentation +=================================== + +This document describes the package python-ldap with its various modules. + +Depending on what you want to do this manual assumes basic to expert +knowledge about the Python language and the LDAP standard (LDAPv3). + + +.. toctree:: + :maxdepth: 3 + + ldap.rst + ldap-async.rst + ldap-controls.rst + ldap-dn.rst + ldap-extop.rst + ldap-filter.rst + ldap-modlist.rst + ldap-resiter.rst + ldap-schema.rst + ldap-syncrepl.rst + ldap-sasl.rst + ldif.rst + ldapurl.rst + slapdtest.rst diff --git a/Doc/ldap-async.rst b/Doc/reference/ldap-async.rst similarity index 53% rename from Doc/ldap-async.rst rename to Doc/reference/ldap-async.rst index 59a34f43..d7a18405 100644 --- a/Doc/ldap-async.rst +++ b/Doc/reference/ldap-async.rst @@ -1,49 +1,60 @@ -************************************************************** -:py:mod:`ldap.async` Stream-processing of large search results -************************************************************** +******************************************************************** +:py:mod:`ldap.asyncsearch` Stream-processing of large search results +******************************************************************** -.. py:module:: ldap.async +.. py:module:: ldap.asyncsearch :synopsis: Framework for stream-processing of large search results. With newer Python versions one might want to consider using :py:mod:`ldap.resiter` instead. +.. versionchanged:: 3.0 + In Python 3.7 ``async`` is a reserved keyword. The module + :py:mod:`ldap.async` has been renamed to :py:mod:`ldap.asyncsearch`. The + old name :py:mod:`ldap.async` is still available for backwards + compatibility. + +.. deprecated:: 3.0 + The old name :py:mod:`ldap.async` is deprecated, but will not be removed + until Python 3.6 reaches end-of-life. + + Classes ======= -.. autoclass:: ldap.async.AsyncSearchHandler +.. autoclass:: ldap.asyncsearch.AsyncSearchHandler :members: -.. autoclass:: ldap.async.List +.. autoclass:: ldap.asyncsearch.List :members: -.. autoclass:: ldap.async.Dict +.. autoclass:: ldap.asyncsearch.Dict :members: -.. autoclass:: ldap.async.IndexedDict +.. autoclass:: ldap.asyncsearch.IndexedDict :members: -.. autoclass:: ldap.async.LDIFWriter +.. autoclass:: ldap.asyncsearch.LDIFWriter :members: -.. _ldap.async-example: +.. _ldap.asyncsearch-example: Examples ======== -.. _ldap.async-example.List: +.. _ldap.asyncsearch-example.List: -Using ldap.async.List -^^^^^^^^^^^^^^^^^^^^^ +Using ldap.asyncsearch.List +^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This example demonstrates how to use class ldap.async.List for +This example demonstrates how to use class ldap.asyncsearch.List for retrieving partial search results even though the exception :exc:`ldap.SIZELIMIT_EXCEEDED` was raised because a server side limit was hit. :: - import sys,ldap,ldap.async + import sys,ldap,ldap.asyncsearch - s = ldap.async.List( + s = ldap.asyncsearch.List( ldap.initialize('ldap://localhost'), ) @@ -67,17 +78,17 @@ retrieving partial search results even though the exception ) ) -.. _ldap.async-example.LDIFWriter: +.. _ldap.asyncsearch-example.LDIFWriter: -Using ldap.async.LDIFWriter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Using ldap.asyncsearch.LDIFWriter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This example demonstrates how to use class ldap.async.LDIFWriter +This example demonstrates how to use class ldap.asyncsearch.LDIFWriter for writing search results as LDIF to stdout. :: - import sys,ldap,ldap.async + import sys,ldap,ldap.asyncsearch - s = ldap.async.LDIFWriter( + s = ldap.asyncsearch.LDIFWriter( ldap.initialize('ldap://localhost:1390'), sys.stdout ) @@ -101,4 +112,3 @@ for writing search results as LDIF to stdout. :: s.endResultBreak-s.beginResultsDropped ) ) - diff --git a/Doc/ldap-controls.rst b/Doc/reference/ldap-controls.rst similarity index 91% rename from Doc/ldap-controls.rst rename to Doc/reference/ldap-controls.rst index 1520b241..2206e101 100644 --- a/Doc/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/ldap-dn.rst b/Doc/reference/ldap-dn.rst similarity index 99% rename from Doc/ldap-dn.rst rename to Doc/reference/ldap-dn.rst index c22a64c4..3e0bbb5f 100644 --- a/Doc/ldap-dn.rst +++ b/Doc/reference/ldap-dn.rst @@ -107,4 +107,3 @@ Splitting a LDAPv3 DN with a multi-valued RDN into its AVA parts: >>> ldap.dn.str2dn('cn=John Doe+mail=john.doe@example.com,dc=example,dc=com') [[('cn', 'John Doe', 1), ('mail', 'john.doe@example.com', 1)], [('dc', 'example', 1)], [('dc', 'com', 1)]] - diff --git a/Doc/ldap-extop.rst b/Doc/reference/ldap-extop.rst similarity index 97% rename from Doc/ldap-extop.rst rename to Doc/reference/ldap-extop.rst index 607f3f00..ad70e4e7 100644 --- a/Doc/ldap-extop.rst +++ b/Doc/reference/ldap-extop.rst @@ -39,3 +39,4 @@ 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/ldap-filter.rst b/Doc/reference/ldap-filter.rst similarity index 99% rename from Doc/ldap-filter.rst rename to Doc/reference/ldap-filter.rst index 577befbc..b08d5e9b 100644 --- a/Doc/ldap-filter.rst +++ b/Doc/reference/ldap-filter.rst @@ -35,4 +35,3 @@ The :mod:`ldap.filter` module defines the following functions: whole filter string. .. % -> string - diff --git a/Doc/ldap-modlist.rst b/Doc/reference/ldap-modlist.rst similarity index 100% rename from Doc/ldap-modlist.rst rename to Doc/reference/ldap-modlist.rst diff --git a/Doc/ldap-resiter.rst b/Doc/reference/ldap-resiter.rst similarity index 99% rename from Doc/ldap-resiter.rst rename to Doc/reference/ldap-resiter.rst index 0e72e922..66ce6cab 100644 --- a/Doc/ldap-resiter.rst +++ b/Doc/reference/ldap-resiter.rst @@ -20,7 +20,7 @@ derived classes which has these methods: Examples -======== +-------- .. _ldap.resiter.ResultProcessor-example: diff --git a/Doc/ldap-sasl.rst b/Doc/reference/ldap-sasl.rst similarity index 99% rename from Doc/ldap-sasl.rst rename to Doc/reference/ldap-sasl.rst index 96f13aa5..9a8c96aa 100644 --- a/Doc/ldap-sasl.rst +++ b/Doc/reference/ldap-sasl.rst @@ -35,7 +35,7 @@ Classes .. autoclass:: ldap.sasl.sasl :members: - + This class is used with :py:meth:`ldap.LDAPObject.sasl_interactive_bind_s()`. @@ -82,4 +82,3 @@ and sends a SASL external bind request. ldap_conn.sasl_non_interactive_bind_s('EXTERNAL') # Find out the SASL Authorization Identity print ldap_conn.whoami_s() - diff --git a/Doc/ldap-schema.rst b/Doc/reference/ldap-schema.rst similarity index 100% rename from Doc/ldap-schema.rst rename to Doc/reference/ldap-schema.rst diff --git a/Doc/ldap-syncrepl.rst b/Doc/reference/ldap-syncrepl.rst similarity index 90% rename from Doc/ldap-syncrepl.rst rename to Doc/reference/ldap-syncrepl.rst index d3717dfb..046b15a9 100644 --- a/Doc/ldap-syncrepl.rst +++ b/Doc/reference/ldap-syncrepl.rst @@ -21,3 +21,5 @@ This module defines the following classes: .. autoclass:: ldap.syncrepl.SyncreplConsumer :members: +.. autoclass:: ldap.syncrepl.OpenLDAPSyncreplCookie + :members: diff --git a/Doc/ldap.rst b/Doc/reference/ldap.rst similarity index 72% rename from Doc/ldap.rst rename to Doc/reference/ldap.rst index d526370f..397b7663 100644 --- a/Doc/ldap.rst +++ b/Doc/reference/ldap.rst @@ -29,18 +29,31 @@ Functions This module defines the following functions: -.. py:function:: initialize(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=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. - - Note that internally the OpenLDAP funtion + 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) `_ is called which just initializes the LDAP connection struct in the C API - nothing else. Therefore the first call to an operation method (bind, @@ -48,7 +61,7 @@ This module defines the following functions: that nothing is sent on the wire. The error handling in the calling application has to correctly handle this behaviour. - The optional arguments are for generating debug log information: + Three optional arguments are for generating debug log information: *trace_level* specifies the amount of information being logged, *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. @@ -59,18 +72,26 @@ This module defines the following functions: :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. + 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 -.. py:function:: open(host [, port=PORT]) -> LDAPObject object + .. versionadded:: 3.3 - Opens a new connection with an LDAP server, and return an LDAP object (see - :ref:`ldap-objects`) used to perform operations on that server. *host* is a - string containing solely the host name. *port* is an integer specifying the - port where the LDAP server is listening (default is 389). + The *fileno* argument was added. - Note: Using this function is deprecated. + .. deprecated:: 3.4 + + *bytes_mode* and *bytes_strictness* arguments are deprecated. .. py:function:: get_option(option) -> int|string @@ -83,6 +104,16 @@ 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. + .. _ldap-constants: @@ -111,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: @@ -122,9 +159,9 @@ Options :manpage:`ldap.conf(5)` and :manpage:`ldap_get_option(3)` -For use with functions :py:func:set_option() and :py:func:get_option() -and methods :py:method:LDAPObject.set_option() and :py:method:LDAPObject.get_option() the -following option identifiers are defined as constants: +For use with functions :py:func:`set_option` and :py:func:`get_option` and +methods :py:meth:`LDAPObject.set_option` and :py:meth:`LDAPObject.get_option` +the following option identifiers are defined as constants: .. py:data:: OPT_API_FEATURE_INFO @@ -141,7 +178,7 @@ following option identifiers are defined as constants: .. py:data:: OPT_DEREF - Specifies how alias derefencing is done within the underlying LDAP C lib. + Specifies how alias dereferencing is done within the underlying LDAP C lib. .. py:data:: OPT_ERROR_STRING @@ -153,6 +190,9 @@ following option identifiers are defined as constants: .. py:data:: OPT_NETWORK_TIMEOUT + .. versionchanged:: 3.0 + A timeout of ``-1`` or ``None`` resets timeout to infinity. + .. py:data:: OPT_PROTOCOL_VERSION Sets the LDAP protocol version used for a connection. This is mapped to @@ -177,6 +217,9 @@ following option identifiers are defined as constants: .. py:data:: OPT_TIMEOUT + .. versionchanged:: 3.0 + A timeout of ``-1`` or ``None`` resets timeout to infinity. + .. py:data:: OPT_URI .. _ldap-sasl-options: @@ -184,6 +227,9 @@ 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 @@ -192,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 @@ -211,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: @@ -296,21 +546,32 @@ The module defines the following exceptions: .. py:exception:: LDAPError - This is the base class of all execeptions raised by the module :py:mod:`ldap`. + This is the base class of all exceptions raised by the module :py:mod:`ldap`. Unlike the C interface, errors are not returned as result codes, but 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 @@ -344,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 @@ -413,7 +674,7 @@ The module defines the following exceptions: .. py:exception:: IS_LEAF - The object specified is a leaf of the diretcory tree. + The object specified is a leaf of the directory tree. Sets the :py:const:`matched` field of the exception dictionary value. .. py:exception:: LOCAL_ERROR @@ -448,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 @@ -555,40 +812,46 @@ The above exceptions are raised when a result code from an underlying API call does not indicate success. -.. _ldap-objects: +.. _ldap-warnings: -LDAPObject classes -================== +Warnings +======== -.. py:class:: LDAPObject +.. py:class:: LDAPBytesWarning - Instances of :py:class:`LDAPObject` are returned by :py:func:`initialize()` - and :py:func:`open()` (deprecated). The connection is automatically unbound - and closed when the LDAP object is deleted. + This warning is deprecated. python-ldap no longer raises it. - Internally :py:class:`LDAPObject` is set to :py:class:`SimpleLDAPObject` - by default. + It used to be raised under Python 2 when bytes/text mismatch in non-strict + bytes mode. See :ref:`bytes_mode` for details. -.. py:class:: SimpleLDAPObject(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=5]]]) + .. versionadded:: 3.0.0 - This basic class wraps all methods of the underlying C API object. + .. versionchanged:: 3.4.0 - The arguments are same like for function :py:func:`initialize()`. + Deprecated. -.. py:class:: ReconnectLDAPObject(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=5] [, retry_max=1 [, retry_delay=60.0]]]]) +.. _ldap-objects: - This class is derived from :py:class:`SimpleLDAPObject` and used for automatic - reconnects when using the synchronous request methods (see below). This class - also implements the pickle protocol. +LDAPObject classes +================== + +.. 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. - The first arguments are same like for function :py:func:`initialize()`. + :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()`. - For automatic reconnects it has additional arguments: + (It is also possible, but not recommended, to change the default by setting + ``ldap.ldapobject.LDAPObject`` to a different class.) - *retry_max* specifies the number of reconnect attempts before - re-raising the :py:exc:`ldap.SERVER_DOWN` exception. +.. autoclass:: ldap.ldapobject.SimpleLDAPObject - *retry_delay* specifies the time in seconds between reconnect attempts. +.. autoclass:: ldap.ldapobject.ReconnectLDAPObject .. _ldap-controls: @@ -614,6 +877,8 @@ with names ending in :py:const:`_ext` or :py:const:`_ext_s`: request. +.. _sending-ldap-requests: + Sending LDAP requests --------------------- @@ -660,6 +925,9 @@ and wait for and return with the server's result, or with *serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`. + The *dn* argument, and mod_type (second item) of *modlist* are text strings; + see :ref:`bytes_mode`. + .. py:method:: LDAPObject.bind(who, cred, method) -> int @@ -682,18 +950,19 @@ and wait for and return with the server's result, or with .. py:method:: LDAPObject.compare(dn, attr, value) -> int -.. py:method:: LDAPObject.compare_s(dn, attr, value) -> tuple +.. py:method:: LDAPObject.compare_s(dn, attr, value) -> bool .. py:method:: LDAPObject.compare_ext(dn, attr, value [, serverctrls=None [, clientctrls=None]]) -> int -.. py:method:: LDAPObject.compare_ext_s(dn, attr, value [, serverctrls=None [, clientctrls=None]]) -> tuple +.. py:method:: LDAPObject.compare_ext_s(dn, attr, value [, serverctrls=None [, clientctrls=None]]) -> bool - Perform an LDAP comparison between the attribute named *attr* of - entry *dn*, and the value *value*. The synchronous forms - returns :py:const:`0` for false, or :py:const:`1` for true. - 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()`. + Perform an LDAP comparison between the attribute named *attr* of entry *dn*, + 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()`. 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 @@ -701,10 +970,16 @@ and wait for and return with the server's result, or with *serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`. - .. note:: + The *dn* and *attr* arguments are text strings; see :ref:`bytes_mode`. + + +.. py:method:: LDAPObject.connect() -> None - A design fault in the LDAP API prevents *value* - from containing *NULL* characters. + 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 @@ -721,6 +996,8 @@ and wait for and return with the server's result, or with *serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`. + The *dn* argument is text string; see :ref:`bytes_mode`. + .. py:method:: LDAPObject.extop(extreq[,serverctrls=None[,clientctrls=None]]]) -> int @@ -774,6 +1051,9 @@ and wait for and return with the server's result, or with You might want to look into sub-module :py:mod:`ldap.modlist` for generating *modlist*. + The *dn* argument, and mod_type (second item) of *modlist* are text strings; + see :ref:`bytes_mode`. + .. py:method:: LDAPObject.modrdn(dn, newrdn [, delold=1]) -> int @@ -790,10 +1070,12 @@ and wait for and return with the server's result, or with This operation is emulated by :py:meth:`rename()` and :py:meth:`rename_s()` methods since the modrdn2* routines in the C library are deprecated. + The *dn* and *newrdn* arguments are text strings; see :ref:`bytes_mode`. + .. 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*. @@ -804,13 +1086,23 @@ 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. + The *user*, *oldpw* and *newpw* arguments are text strings; see :ref:`bytes_mode`. + .. seealso:: :rfc:`3062` - LDAP Password Modify Extended Operation + :py:mod:`ldap.extop.passwd` @@ -829,12 +1121,14 @@ and wait for and return with the server's result, or with *serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`. + The *dn* and *newdn* arguments are text strings; see :ref:`bytes_mode`. + .. py:method:: LDAPObject.result([msgid=RES_ANY [, all=1 [, timeout=None]]]) -> 2-tuple This method is used to wait for and return the result of an operation previously initiated by one of the LDAP *asynchronous* operations - (eg :py:meth:`search()`, :py:meth:`modify()`, etc.) + (e.g. :py:meth:`search()`, :py:meth:`modify()`, etc.) The *msgid* parameter is the integer identifier returned by that method. The identifier is guaranteed to be unique across an LDAP session, @@ -933,7 +1227,7 @@ and wait for and return with the server's result, or with .. py:method:: LDAPObject.sasl_interactive_bind_s(who, auth[, serverctrls=None [, clientctrls=None [, sasl_flags=ldap.SASL_QUIET]]]) -> None This call is used to bind to the directory with a SASL bind request. - + *auth* is an :py:class:`ldap.sasl.sasl()` instance. *serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`. @@ -964,9 +1258,9 @@ and wait for and return with the server's result, or with *serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`. -.. py:method:: LDAPObject.simple_bind([who='' [, cred='' [, serverctrls=None [, clientctrls=None]]]]) -> int +.. py:method:: LDAPObject.simple_bind([who=None [, cred=None [, serverctrls=None [, clientctrls=None]]]]) -> int -.. py:method:: LDAPObject.simple_bind_s([who='' [, cred='' [, serverctrls=None [, clientctrls=None]]]]) -> None +.. py:method:: LDAPObject.simple_bind_s([who=None [, cred=None [, serverctrls=None [, clientctrls=None]]]]) -> None After an LDAP object is created, and before any other operations can be attempted over the connection, a bind operation must be performed. @@ -979,6 +1273,12 @@ and wait for and return with the server's result, or with *serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`. + The *who* and *cred* arguments are text strings; see :ref:`bytes_mode`. + + .. versionchanged:: 3.0 + + :meth:`~LDAPObject.simple_bind` and :meth:`~LDAPObject.simple_bind_s` + now accept ``None`` for *who* and *cred*, too. .. py:method:: LDAPObject.search(base, scope [,filterstr='(objectClass=*)' [, attrlist=None [, attrsonly=0]]]) ->int @@ -1032,6 +1332,12 @@ and wait for and return with the server's result, or with or :py:meth:`search_ext_s()` (client-side search limit). If non-zero not more than *sizelimit* results are returned by the server. + The *base* and *filterstr* arguments, and *attrlist* contents, + are text strings; see :ref:`bytes_mode`. + + .. versionchanged:: 3.0 + + ``filterstr=None`` is equivalent to ``filterstr='(objectClass=*)'``. .. py:method:: LDAPObject.start_tls_s() -> None @@ -1068,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:: @@ -1114,6 +1420,8 @@ These attributes are mutable unless described as read-only. This option is mapped to option constant :py:const:`OPT_NETWORK_TIMEOUT` and used in the underlying OpenLDAP client lib. + .. versionchanged:: 3.0.0 + A timeout of ``-1`` or ``None`` resets timeout to infinity. .. py:attribute:: LDAPObject.protocol_version -> int @@ -1171,4 +1479,3 @@ subtree search. >>> for dn,entry in r: >>> print('Processing',repr(dn)) >>> handle_ldap_entry(entry) - diff --git a/Doc/ldapurl.rst b/Doc/reference/ldapurl.rst similarity index 90% rename from Doc/ldapurl.rst rename to Doc/reference/ldapurl.rst index c13061cd..eb2106b3 100644 --- a/Doc/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. Compability 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 ^^^^^^^^^^^^^^^^^^^ @@ -91,7 +95,7 @@ Example ^^^^^^^ Important security advice: -For security reasons you shouldn't specify passwords in LDAP URLs +For security reasons you should not specify passwords in LDAP URLs unless you really know what you are doing. The following example demonstrates how to parse a LDAP URL @@ -124,4 +128,3 @@ with \module{ldapurl} module. >>> ldap_url = ldapurl.LDAPUrl(hostport='localhost:1389',dn='dc=stroeder,dc=com',attrs=['cn','mail'],who='cn=Michael,dc=stroeder,dc=com',cred='secret') >>> ldap_url.unparse() 'ldap://localhost:1389/dc=stroeder,dc=com?cn,mail?base?(objectclass=*)?bindname=cn=Michael%2Cdc=stroeder%2Cdc=com,X-BINDPW=secret' - diff --git a/Doc/ldif.rst b/Doc/reference/ldif.rst similarity index 76% rename from Doc/ldif.rst rename to Doc/reference/ldif.rst index 1a9eabc3..87dcb70b 100644 --- a/Doc/ldif.rst +++ b/Doc/reference/ldif.rst @@ -22,8 +22,20 @@ Functions .. autofunction:: ldif.CreateLDIF + .. deprecated:: 3.0 + + ``ldif.CreateLDIF()`` is deprecated. It will be removed in version 3.1. + Use :meth:`ldif.LDIFWriter.unparse` with a file or ``io.StringIO`` + instead. + .. autofunction:: ldif.ParseLDIF + .. deprecated:: 3.0 + + ``ldif.ParseLDIF()`` is deprecated. It will be removed in version 3.1. + Use the ``all_records`` attribute of the returned value of + ``ldif.LDIFRecordList.parse()`` instead. + Classes ^^^^^^^ @@ -49,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 @@ -82,4 +94,3 @@ with :mod:`ldif` module, skip some entries and write the result to stdout. :: parser = MyLDIF(open("input.ldif", 'rb'), sys.stdout) parser.parse() - diff --git a/Doc/slapdtest.rst b/Doc/reference/slapdtest.rst similarity index 83% rename from Doc/slapdtest.rst rename to Doc/reference/slapdtest.rst index bd54bb69..7517e662 100644 --- a/Doc/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 new file mode 100644 index 00000000..795f8b63 --- /dev/null +++ b/Doc/resources.rst @@ -0,0 +1,55 @@ +Third-party documentation +========================= + +The following documents referenced are not written by python-ldap project +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 `_ + + This also covers SASL. + +* `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`. + + +`LDAP Programming in Python `_ +------------------------------------------------------------------------- + +Another article for getting started with python-ldap. + + + +`RFC 1823 `_ +------------------------------------------------- + +The LDAP Application Program Interface, mainly for LDAPv2. + + + +`LDAPEXT draft `_ +---------------------------------------------------------------------------- + +The Internet draft of the discontinued IETF working group LDAPEXT is of +interest here since the OpenLDAP 2 libs implement this (expired) draft. + + +`OpenLDAP `_ +--------------------------------------- + +It's worth to have a look at the +`manual pages `_ and the +`Developer's FAQ `_. + + +`VSLDAP `_ +---------------------------------------------------------------------------------------- + +VSLDAP Interoperability Test Suite. diff --git a/Doc/sample_workflow.rst b/Doc/sample_workflow.rst new file mode 100644 index 00000000..76017034 --- /dev/null +++ b/Doc/sample_workflow.rst @@ -0,0 +1,81 @@ +.. _sample workflow: + +Sample workflow for python-ldap development +------------------------------------------- + +This document will guide you through the process of contributing a change +to python-ldap. + +We assume that, as a user of python-ldap, you're not new to software +development in general, so these instructions are terse. +If you need additional detail, please do ask on the mailing list. + +.. note:: + + The following instructions are for Linux. + If you can translate them to another system, please contribute your + translation! + + +Install `Git`_, `tox`_ and the :ref:`build prerequisites`. + +.. _tox: https://tox.readthedocs.io/en/latest/ + +Clone the repository:: + + $ git clone https://github.com/python-ldap/python-ldap + $ cd python-ldap + +Create a :mod:`virtual environment ` to ensure you in-development +python-ldap won't affect the rest of your system:: + + $ python3 -m venv __venv__ + +.. _git: https://git-scm.com/ +.. _virtualenv: https://virtualenv.pypa.io/en/stable/ + +Activate the virtual environment:: + + $ source __venv__/bin/activate + +Install python-ldap to it in `editable mode`_:: + + (__venv__)$ python -m pip install -e . + +This way, importing a Python module from python-ldap will directly +use the code from your source tree. +If you change C code, you will still need to recompile +(using the ``pip install`` command again). + +.. _editable mode: https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs + +Change the code as desired. + + +To run tests, install and run `tox`_:: + + (__venv__)$ python -m pip install tox + (__venv__)$ tox --skip-missing-interpreters + +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 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 py36,py39,coverage-report``. +* ``py3-nosasltls`` check functionality without + SASL and TLS bindings compiled in. + + +When your change is ready, commit to Git, and submit a pull request on GitHub. +You can take a look at the :ref:`committer instructions` to see what we are looking +for in a pull request. + +If you don't want to open a GitHub account, please send patches as attachments +to the python-ldap mailing list. diff --git a/Doc/spelling_wordlist.txt b/Doc/spelling_wordlist.txt new file mode 100644 index 00000000..4381ebee --- /dev/null +++ b/Doc/spelling_wordlist.txt @@ -0,0 +1,170 @@ +args +async +asyncsearch +attr +attrlist +attrList +attrs +attrsonly +attrsOnly +attrtype +authzId +automagically +backend +behaviour +BER +bindname +boolean +booleanValue +Bytestrings +cancelled +canonicalization +cb +cfg +changeNumber +changesOnly +changeType +changeTypes +Christoph +cidict +clientctrls +conf +controlType +controlValue +committers +CPython +criticality +cryptographic +Cyrus +defresult +dereferenced +dereferencing +desc +dev +directoryOperation +distinguished +distributedOperation +dit +dn +DN +dSAOperation +encodedControlValue +encodedResponseValue +extype +exvalue +favour +filterstr +filterStr +formatOID +func +Gohlke +GPG +Heimdal +Homebrew +hostport +hrefTarget +hrefText +ignoreResultsNumber +integerValue +Interoperability +isn +Keepalive +Kerberos +keyerror +knownLDAPControls +kwarg +ldap +ldapadd +ldapControls +ldapControlTuples +ldapdelete +ldapi +LDAPObject +ldaps +ldapurl +ldapwhoami +ldif +LDIFWriter +libldap +libs +Libs +macOS +modlist +modrdn +msgid +multi +nameoroid +nots +Novell +objectClass +oc +oid +oids +OpenLDAP +postalAddress +pre +previousDN +processResultsCount +Proxied +py +pyproject +pytest +rdn +readthedocs +reentrant +refmodule +refreshAndPersist +refreshDeletes +refreshOnly +requestName +requestValue +resiter +respvalue +ResultProcessor +returnECs +ruleid +rundir +sasl +searchRoot +searchScope +sed +serverctrls +sessionSourceIp +sessionSourceName +sessionTrackingIdentifier +slapadd +sizelimit +slapd +startup +stderr +stdout +str +Subclasses +subentry +subschema +substr +subtree +syncrepl +syntaxes +timelimit +TLS +toml +tracebacks +tuple +tuples +UDP +Umich +unparsing +unsigend +urandom +uri +urlPrefix +urlscheme +userApplications +userPassword +usr +uuids +Valgrind +Xcode +whitespace +workflow diff --git a/INSTALL b/INSTALL index 98dd40cc..224df4a4 100644 --- a/INSTALL +++ b/INSTALL @@ -1,26 +1,8 @@ ------------------------------- -Installing python-ldap ------------------------------- - -Prerequisites: - - Required: - - - Python 2.3 or newer (see http://www.python.org) - - - OpenLDAP 2.4.11+ client libs (see http://www.openldap.org) - It is not possible and not supported - by the python-ldap project to build with prior versions. - - Optional dependencies of OpenLDAP libs: +Quick build instructions: - - Cyrus SASL 2.1.x or newer (see http://asg.web.cmu.edu/sasl/sasl-library.html) + edit setup.cfg (see Build/ for platform-specific examples) + pip install . - - OpenSSL 0.9.7 or newer (see http://www.openssl.org) - - - MIT Kerberos or heimdal libs +Detailed instructions are in Doc/installing.rst, or online at: -Quick build instructions: - edit setup.cfg (see Build/ for platform-specific examples) - python setup.py build - python setup.py install + https://python-ldap.readthedocs.io/en/latest/installing.html 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 b22a6f43..b1797078 100644 --- a/Lib/ldap/__init__.py +++ b/Lib/ldap/__init__.py @@ -8,18 +8,32 @@ from ldap.pkginfo import __version__, __author__, __license__ +import os import sys if __debug__: # Tracing is only supported in debugging mode + import atexit import traceback + _trace_level = int(os.environ.get("PYTHON_LDAP_TRACE_LEVEL", 0)) + _trace_file = os.environ.get("PYTHON_LDAP_TRACE_FILE") + if _trace_file is None: + _trace_file = sys.stderr + else: + _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) @@ -40,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 @@ -70,23 +83,23 @@ 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() # Create module-wide lock for serializing all calls into underlying LDAP lib _ldap_module_lock = LDAPLock(desc='Module wide') -from ldap.functions import open,initialize,init,get_option,set_option,escape_str,strf_secs,strp_secs +from ldap.functions import initialize,get_option,set_option,escape_str,strf_secs,strp_secs -from ldap.ldapobject import NO_UNIQUE_ENTRY +from ldap.ldapobject import NO_UNIQUE_ENTRY, LDAPBytesWarning from ldap.dn import explode_dn,explode_rdn,str2dn,dn2str del str2dn @@ -94,5 +107,5 @@ def release(self): # More constants -# For compability of 2.3 and 2.4 OpenLDAP API +# For compatibility of 2.3 and 2.4 OpenLDAP API OPT_DIAGNOSTIC_MESSAGE = OPT_ERROR_STRING diff --git a/Lib/ldap/async.py b/Lib/ldap/async.py index 0dd4940c..1d4505bc 100644 --- a/Lib/ldap/async.py +++ b/Lib/ldap/async.py @@ -1,283 +1,15 @@ """ -ldap.async - handle async LDAP operations +ldap.asyncsearch - handle async LDAP search operations See https://www.python-ldap.org/ for details. """ +import warnings -import ldap +from ldap.asyncsearch import * +from ldap.asyncsearch import __version__ -from ldap import __version__ - -SEARCH_RESULT_TYPES = set([ - ldap.RES_SEARCH_ENTRY, - ldap.RES_SEARCH_RESULT, - ldap.RES_SEARCH_REFERENCE, -]) - -ENTRY_RESULT_TYPES = set([ - ldap.RES_SEARCH_ENTRY, - ldap.RES_SEARCH_RESULT, -]) - - -class WrongResultType(Exception): - - def __init__(self,receivedResultType,expectedResultTypes): - self.receivedResultType = receivedResultType - self.expectedResultTypes = expectedResultTypes - Exception.__init__(self) - - def __str__(self): - return 'Received wrong result type %s (expected one of %s).' % ( - self.receivedResultType, - ', '.join(self.expectedResultTypes), - ) - - -class AsyncSearchHandler: - """ - Class for stream-processsing LDAP search results - - Arguments: - - l - LDAPObject instance - """ - - def __init__(self,l): - self._l = l - self._msgId = None - self._afterFirstResult = 1 - - def startSearch( - self, - searchRoot, - searchScope, - filterStr, - attrList=None, - attrsOnly=0, - timeout=-1, - sizelimit=0, - serverctrls=None, - clientctrls=None - ): - """ - searchRoot - See parameter base of method LDAPObject.search() - searchScope - See parameter scope of method LDAPObject.search() - filterStr - See parameter filter of method LDAPObject.search() - attrList=None - See parameter attrlist of method LDAPObject.search() - attrsOnly - See parameter attrsonly of method LDAPObject.search() - timeout - Maximum time the server shall use for search operation - sizelimit - Maximum number of entries a server should return - (request client-side limit) - serverctrls - list of server-side LDAP controls - clientctrls - list of client-side LDAP controls - """ - self._msgId = self._l.search_ext( - searchRoot,searchScope,filterStr, - attrList,attrsOnly,serverctrls,clientctrls,timeout,sizelimit - ) - self._afterFirstResult = 1 - return # startSearch() - - def preProcessing(self): - """ - Do anything you want after starting search but - before receiving and processing results - """ - - def afterFirstResult(self): - """ - Do anything you want right after successfully receiving but before - processing first result - """ - - def postProcessing(self): - """ - Do anything you want after receiving and processing all results - """ - - def processResults(self,ignoreResultsNumber=0,processResultsCount=0,timeout=-1): - """ - ignoreResultsNumber - Don't process the first ignoreResultsNumber results. - processResultsCount - If non-zero this parameters indicates the number of results - processed is limited to processResultsCount. - timeout - See parameter timeout of ldap.LDAPObject.result() - """ - self.preProcessing() - result_counter = 0 - end_result_counter = ignoreResultsNumber+processResultsCount - go_ahead = 1 - partial = 0 - self.beginResultsDropped = 0 - self.endResultBreak = result_counter - try: - result_type,result_list = None,None - while go_ahead: - while result_type is None and not result_list: - result_type,result_list,result_msgid,result_serverctrls = self._l.result3(self._msgId,0,timeout) - if self._afterFirstResult: - self.afterFirstResult() - self._afterFirstResult = 0 - if not result_list: - break - if result_type not in SEARCH_RESULT_TYPES: - raise WrongResultType(result_type,SEARCH_RESULT_TYPES) - # Loop over list of search results - for result_item in result_list: - if result_counter might cause + # backward compatibility problems + TLSInt('OPT_X_TLS_CRLCHECK', optional=True), + + TLSInt('OPT_X_TLS_CRLFILE', optional=True), + + TLSInt('OPT_X_TLS_CRL_NONE'), + TLSInt('OPT_X_TLS_CRL_PEER'), + TLSInt('OPT_X_TLS_CRL_ALL'), + TLSInt('OPT_X_TLS_NEWCTX', optional=True), + 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'), + Int('OPT_X_SASL_AUTHZID'), + Int('OPT_X_SASL_SSF'), + Int('OPT_X_SASL_SSF_EXTERNAL'), + Int('OPT_X_SASL_SECPROPS'), + Int('OPT_X_SASL_SSF_MIN'), + Int('OPT_X_SASL_SSF_MAX'), + Int('OPT_X_SASL_NOCANON', optional=True), + Int('OPT_X_SASL_USERNAME', optional=True), + Int('OPT_CONNECT_ASYNC', optional=True), + Int('OPT_X_KEEPALIVE_IDLE', optional=True), + Int('OPT_X_KEEPALIVE_PROBES', optional=True), + Int('OPT_X_KEEPALIVE_INTERVAL', optional=True), + + Int('DN_FORMAT_LDAP'), + Int('DN_FORMAT_LDAPV3'), + Int('DN_FORMAT_LDAPV2'), + Int('DN_FORMAT_DCE'), + Int('DN_FORMAT_UFN'), + Int('DN_FORMAT_AD_CANONICAL'), + # Int('DN_FORMAT_LBER'), # for testing only + Int('DN_FORMAT_MASK'), + Int('DN_PRETTY'), + Int('DN_SKIP'), + Int('DN_P_NOLEADTRAILSPACES'), + Int('DN_P_NOSPACEAFTERRDN'), + Int('DN_PEDANTIC'), + + Int('AVA_NULL'), + Int('AVA_STRING'), + Int('AVA_BINARY'), + Int('AVA_NONPRINTABLE'), + + Int('OPT_SUCCESS'), + + # XXX - these should be errors + Int('URL_ERR_BADSCOPE'), + Int('URL_ERR_MEM'), + + 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"), + Str("CONTROL_SUBENTRIES"), + Str("CONTROL_VALUESRETURNFILTER"), + Str("CONTROL_ASSERT"), + Str("CONTROL_PRE_READ"), + Str("CONTROL_POST_READ"), + Str("CONTROL_SORTREQUEST"), + Str("CONTROL_SORTRESPONSE"), + Str("CONTROL_PAGEDRESULTS"), + Str("CONTROL_SYNC"), + Str("CONTROL_SYNC_STATE"), + Str("CONTROL_SYNC_DONE"), + Str("SYNC_INFO"), + Str("CONTROL_PASSWORDPOLICYREQUEST"), + Str("CONTROL_PASSWORDPOLICYRESPONSE"), + Str("CONTROL_RELAX"), +) + + +def print_header(): # pragma: no cover + """Print the C header file to standard output""" + + print('/*') + print(' * Generated with:') + print(' * python Lib/ldap/constants.py > Modules/constants_generated.h') + print(' *') + print(' * Please do any modifications there, then re-generate this file') + print(' */') + print('') + + current_requirements = [] + + def pop_requirement(): + popped = current_requirements.pop() + print('#endif') + print() + + for definition in CONSTANTS: + while not set(current_requirements).issubset(definition.requirements): + pop_requirement() + + for requirement in definition.requirements: + if requirement not in current_requirements: + current_requirements.append(requirement) + print() + print(f'#if {requirement}') + + print(definition.c_template.format(self=definition)) + + while current_requirements: + pop_requirement() + + +if __name__ == '__main__': + print_header() diff --git a/Lib/ldap/controls/__init__.py b/Lib/ldap/controls/__init__.py index 932fa536..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,14 +12,11 @@ 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 -try: - from pyasn1.error import PyAsn1Error -except ImportError: - PyAsn1Error = None +from pyasn1.error import PyAsn1Error __all__ = [ @@ -96,7 +92,7 @@ class attributes class LDAPControl(RequestControl,ResponseControl): """ Base class for combined request/response controls mainly - for backward-compability to python-ldap 2.3.x + for backward-compatibility to python-ldap 2.3.x """ def __init__(self,controlType=None,criticality=False,controlValue=None,encodedControlValue=None): diff --git a/Lib/ldap/controls/deref.py b/Lib/ldap/controls/deref.py index d1b5ce9e..e5b2a7ec 100644 --- a/Lib/ldap/controls/deref.py +++ b/Lib/ldap/controls/deref.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- """ -ldap.controls.deref - classes for +ldap.controls.deref - classes for (see https://tools.ietf.org/html/draft-masarati-ldap-deref) See https://www.python-ldap.org/ for project details. @@ -26,7 +25,7 @@ # Request types #--------------------------------------------------------------------------- -# For compability with ASN.1 declaration in I-D +# For compatibility with ASN.1 declaration in I-D AttributeList = AttributeDescriptionList class DerefSpec(univ.Sequence): @@ -107,10 +106,10 @@ def decodeControlValue(self,encodedControlValue): self.derefRes = {} for deref_res in decodedValue: deref_attr,deref_val,deref_vals = deref_res[0],deref_res[1],deref_res[2] - partial_attrs_dict = dict([ - (str(tv[0]),map(str,tv[1])) + partial_attrs_dict = { + str(tv[0]): [str(v) for v in tv[1]] for tv in deref_vals or [] - ]) + } try: self.derefRes[str(deref_attr)].append((str(deref_val),partial_attrs_dict)) except KeyError: 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 7108c632..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 @@ -23,7 +22,7 @@ class SearchNoOpControl(ValueLessRequestControl,ResponseControl): No-op control attached to search operations implementing sort of a count operation - see http://www.openldap.org/its/index.cgi?findid=6598 + see https://www.openldap.org/its/index.cgi?findid=6598 """ controlType = '1.3.6.1.4.1.4203.666.5.18' @@ -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 aa761f34..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) @@ -11,13 +10,13 @@ ] # Imports from python-ldap 2.4+ -import ldap.controls -from ldap.controls import RequestControl,ResponseControl,ValueLessRequestControl,KNOWN_RESPONSE_CONTROLS +from ldap.controls import ( + ResponseControl, ValueLessRequestControl, KNOWN_RESPONSE_CONTROLS +) # Imports from pyasn1 from pyasn1.type import tag,namedtype,namedval,univ,constraint -from pyasn1.codec.ber import encoder,decoder -from pyasn1_modules.rfc2251 import LDAPDN +from pyasn1.codec.der import decoder class PasswordPolicyWarning(univ.Choice): @@ -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,32 +63,43 @@ 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 + self.timeBeforeExpiration = None + self.graceAuthNsRemaining = None + self.error = None def decodeControlValue(self,encodedControlValue): ppolicyValue,_ = decoder.decode(encodedControlValue,asn1Spec=PasswordPolicyResponseValue()) warning = ppolicyValue.getComponentByName('warning') - if not warning.hasValue(): - self.timeBeforeExpiration,self.graceAuthNsRemaining = None,None - else: - timeBeforeExpiration = warning.getComponentByName('timeBeforeExpiration') - if timeBeforeExpiration.hasValue(): - self.timeBeforeExpiration = int(timeBeforeExpiration) - else: - self.timeBeforeExpiration = None - graceAuthNsRemaining = warning.getComponentByName('graceAuthNsRemaining') - if graceAuthNsRemaining.hasValue(): - self.graceAuthNsRemaining = int(graceAuthNsRemaining) - else: - self.graceAuthNsRemaining = None + if warning.hasValue(): + if 'timeBeforeExpiration' in warning: + self.timeBeforeExpiration = int( + warning.getComponentByName('timeBeforeExpiration')) + if 'graceAuthNsRemaining' in warning: + self.graceAuthNsRemaining = int( + warning.getComponentByName('graceAuthNsRemaining')) + error = ppolicyValue.getComponentByName('error') if error.hasValue(): self.error = int(error) - else: - self.error = None KNOWN_RESPONSE_CONTROLS[PasswordPolicyControl.controlType] = PasswordPolicyControl diff --git a/Lib/ldap/controls/psearch.py b/Lib/ldap/controls/psearch.py index 91a5c241..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) @@ -32,7 +31,7 @@ 'modify':4, 'modDN':8, } -CHANGE_TYPES_STR = dict([(v,k) for k,v in CHANGE_TYPES_INT.items()]) +CHANGE_TYPES_STR = {v: k for k,v in CHANGE_TYPES_INT.items()} class PersistentSearchControl(RequestControl): @@ -40,13 +39,13 @@ class PersistentSearchControl(RequestControl): Implements the request control for persistent search. changeTypes - List of strings specifiying the types of changes returned by the server. + List of strings specifying the types of changes returned by the server. Setting to None requests all changes. changesOnly Boolean which indicates whether only changes are returned by the server. returnECs Boolean which indicates whether the server should return an - Entry Change Notication response control + Entry Change Notification response control """ class PersistentSearchControlValue(univ.Sequence): 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 5d4955d1..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(':') @@ -126,7 +127,7 @@ def decodeControlValue(self, encoded): self.attributeType = attribute_type else: self.attributeType = None - # backward compability class attributes + # backward compatibility class attributes self.result = self.sortResult self.attribute_type_error = self.attributeType diff --git a/Lib/ldap/controls/vlv.py b/Lib/ldap/controls/vlv.py index 74d107b3..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) @@ -134,7 +133,7 @@ def decodeControlValue(self,encoded): self.contextID = str(context_id) else: self.contextID = None - # backward compability class attributes + # backward compatibility class attributes self.target_position = self.targetPosition self.content_count = self.contentCount self.result = self.virtualListViewResult 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 d18c6829..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): """ @@ -63,10 +63,6 @@ def decodeResponseValue(self,value): return value -# Optionally import sub-modules which need pyasn1 et al -try: - import pyasn1,pyasn1_modules.rfc2251 -except ImportError: - pass -else: - from ldap.extop.dds import * +# 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 f5d92787..5bd41b21 100644 --- a/Lib/ldap/filter.py +++ b/Lib/ldap/filter.py @@ -3,7 +3,7 @@ See https://www.python-ldap.org/ for details. -Compability: +Compatibility: - Tested with Python 2.0+ """ @@ -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: @@ -54,7 +56,7 @@ def filter_format(filter_template,assertion_values): List or tuple of assertion values. Length must match count of %s in filter_template. """ - return filter_template % (tuple(map(escape_filter_chars,assertion_values))) + return filter_template % tuple(escape_filter_chars(v) for v in assertion_values) def time_span_filter( @@ -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 b8870378..8658db40 100644 --- a/Lib/ldap/functions.py +++ b/Lib/ldap/functions.py @@ -41,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)) )) @@ -62,7 +62,10 @@ def _ldap_function_call(lock,func,*args,**kwargs): return result -def initialize(uri,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None, bytes_mode=None): +def initialize( + uri, trace_level=0, trace_file=sys.stdout, trace_stack_limit=None, + bytes_mode=None, fileno=None, **kwargs +): """ Return LDAPObject instance by opening LDAP connection to LDAP host specified by LDAP URL @@ -77,34 +80,18 @@ def initialize(uri,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None, b File object where to write the trace output to. Default is to use stdout. bytes_mode - Whether to enable "bytes_mode" for backwards compatibility under Py2. - """ - return LDAPObject(uri,trace_level,trace_file,trace_stack_limit,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. -def open(host,port=389,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None,bytes_mode=None): + Additional keyword arguments (such as ``bytes_strictness``) are + passed to ``LDAPObject``. """ - Return LDAPObject instance by opening LDAP connection to - specified LDAP host - - Parameters: - host - LDAP host and port, e.g. localhost - port - integer specifying the port number to use, e.g. 389 - trace_level - If non-zero a trace output of LDAP calls is generated. - trace_file - File object where to write the trace output to. - Default is to use stdout. - bytes_mode - Whether to enable "bytes_mode" for backwards compatibility under Py2. - """ - import warnings - warnings.warn('ldap.open() is deprecated! Use ldap.initialize() instead.', DeprecationWarning,2) - return initialize('ldap://%s:%d' % (host,port),trace_level,trace_file,trace_stack_limit,bytes_mode) - -init = open + return LDAPObject( + uri, trace_level, trace_file, trace_stack_limit, bytes_mode, + fileno=fileno, **kwargs + ) def get_option(option): @@ -130,8 +117,7 @@ def escape_str(escape_func,s,*args): Applies escape_func() to all items of `args' and returns a string based on format string `s'. """ - escape_args = map(escape_func,args) - return s % tuple(escape_args) + return s % tuple(escape_func(v) for v in args) def strf_secs(secs): diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index 36f98cfb..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__ @@ -14,6 +11,7 @@ 'LDAPObject', 'SimpleLDAPObject', 'ReconnectLDAPObject', + 'LDAPBytesWarning' ] @@ -26,16 +24,21 @@ 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 = bool(sys.version_info[0] <= 2) -if PY2: - text_type = unicode -else: - text_type = str + +class LDAPBytesWarning(BytesWarning): + """Python 2 bytes mode warning""" + + 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): """ @@ -46,7 +49,9 @@ class NO_UNIQUE_ENTRY(ldap.NO_SUCH_OBJECT): class SimpleLDAPObject: """ - Drop-in wrapper class around _ldap.LDAPObject + This basic class wraps all methods of the underlying C API object. + + The arguments are same as for the :func:`~ldap.initialize()` function. """ CLASSATTR_OPTION_MAPPING = { @@ -62,197 +67,38 @@ class SimpleLDAPObject: } def __init__( - self,uri, - trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None + self,uri=None, + trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None, + bytes_strictness=None, fileno=None ): - self._trace_level = trace_level - self._trace_file = trace_file or sys.stdout + 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 - # ---------- - - # By default, raise a TypeError when receiving invalid args - self.bytes_mode_hardfail = True - if bytes_mode is None and PY2: - warnings.warn( - "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.", - BytesWarning, - stacklevel=2, + 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 ) - bytes_mode = True - # Disable hard failure when running in backwards compatibility mode. - self.bytes_mode_hardfail = False - elif bytes_mode and not PY2: - raise ValueError("bytes_mode is *not* supported under Python 3.") - # On by default on Py2, off on Py3. - self.bytes_mode = bytes_mode - - def _bytesify_input(self, 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. - - This function should be applied on all text inputs (distinguished names - and attribute names in modlists) to convert them to the bytes expected - by the C bindings. - """ - if not PY2: - return value - - if value is None: - return value - elif self.bytes_mode: - if isinstance(value, bytes): - return value - else: - if self.bytes_mode_hardfail: - raise TypeError("All provided fields *must* be bytes when bytes mode is on; got %r" % (value,)) - else: - warnings.warn( - "Received non-bytes value %r with default (disabled) bytes mode; please choose an explicit " - "option for bytes_mode on your LDAP connection" % (value,), - BytesWarning, - stacklevel=6, - ) - return value.encode('utf-8') else: - if not isinstance(value, text_type): - raise TypeError("All provided fields *must* be text when bytes mode is off; got %r" % (value,)) - assert not isinstance(value, bytes) - return value.encode('utf-8') - - def _bytesify_inputs(self, *values): - """Adapt values following bytes_mode. - - Applies _bytesify_input on each arg. - - Usage: - >>> a, b, c = self._bytesify_inputs(a, b, c) - """ - if not PY2: - return values - return ( - self._bytesify_input(value) - for value in values - ) - - def _bytesify_modlist(self, 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(attr), val) - for op, attr, val in modlist - ) - else: - return tuple( - (self._bytesify_input(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 dict( - (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: @@ -268,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__)), @@ -286,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: @@ -317,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) )) @@ -329,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 @@ -373,8 +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. """ - dn = self._bytesify_input(dn) - modlist = self._bytesify_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): @@ -395,14 +242,13 @@ def add(self,dn,modlist): def add_s(self,dn,modlist): return self.add_ext_s(dn,modlist,None,None) - def simple_bind(self,who='',cred='',serverctrls=None,clientctrls=None): + def simple_bind(self,who=None,cred=None,serverctrls=None,clientctrls=None): """ simple_bind([who='' [,cred='']]) -> int """ - who, cred = self._bytesify_inputs(who, cred) return self._ldap_call(self._l.simple_bind,who,cred,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) - def simple_bind_s(self,who='',cred='',serverctrls=None,clientctrls=None): + def simple_bind_s(self,who=None,cred=None,serverctrls=None,clientctrls=None): """ simple_bind_s([who='' [,cred='']]) -> 4-tuple """ @@ -461,14 +307,14 @@ def sasl_bind_s(self,dn,mechanism,cred,serverctrls=None,clientctrls=None): def compare_ext(self,dn,attr,value,serverctrls=None,clientctrls=None): """ compare_ext(dn, attr, value [,serverctrls=None[,clientctrls=None]]) -> int - compare_ext_s(dn, attr, value [,serverctrls=None[,clientctrls=None]]) -> int + compare_ext_s(dn, attr, value [,serverctrls=None[,clientctrls=None]]) -> bool compare(dn, attr, value) -> int - compare_s(dn, attr, value) -> int - Perform an LDAP comparison between the attribute named attr of - entry dn, and the value value. The synchronous form returns 0 - for false, or 1 for true. The asynchronous form returns the - message id of the initiates request, and the result of the - asynchronous compare can be obtained using result(). + compare_s(dn, attr, value) -> bool + Perform an LDAP comparison between the attribute named attr of entry + dn, and the value value. The synchronous form returns True or False. + The asynchronous form returns the message id of the initiates request, + and the result of the asynchronous compare can be obtained using + result(). Note that this latter technique yields the answer by raising the exception objects COMPARE_TRUE or COMPARE_FALSE. @@ -476,7 +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. """ - dn, attr = self._bytesify_inputs(dn, 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): @@ -484,11 +329,11 @@ def compare_ext_s(self,dn,attr,value,serverctrls=None,clientctrls=None): try: ldap_res = self.result3(msgid,all=1,timeout=self.timeout) except ldap.COMPARE_TRUE: - return 1 + return True except ldap.COMPARE_FALSE: - return 0 + 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): @@ -507,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) return self._ldap_call(self._l.delete_ext,dn,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def delete_ext_s(self,dn,serverctrls=None,clientctrls=None): @@ -547,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 @@ -556,8 +400,6 @@ def modify_ext(self,dn,modlist,serverctrls=None,clientctrls=None): """ modify_ext(dn, modlist[,serverctrls=None[,clientctrls=None]]) -> int """ - dn = self._bytesify_input(dn) - modlist = self._bytesify_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): @@ -611,12 +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): - user, oldpw, newpw = self._bytesify_inputs(user, oldpw, 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): """ @@ -633,7 +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. """ - dn, newrdn, newsuperior = self._bytesify_inputs(dn, newrdn, 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): @@ -647,7 +494,7 @@ def result(self,msgid=ldap.RES_ANY,all=1,timeout=None): This method is used to wait for and return the result of an operation previously initiated by one of the LDAP asynchronous - operation routines (eg search(), modify(), etc.) They all + operation routines (e.g. search(), modify(), etc.) They all returned an invocation identifier (a message id) upon successful initiation of their operation. This id is guaranteed to be unique across an LDAP session, and can be used to request the @@ -681,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 @@ -722,11 +569,9 @@ 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='(objectClass=*)',attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): + def search_ext(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): """ search(base, scope [,filterstr='(objectClass=*)' [,attrlist=None [,attrsonly=0]]]) -> int search_s(base, scope [,filterstr='(objectClass=*)' [,attrlist=None [,attrsonly=0]]]) @@ -750,7 +595,7 @@ def search_ext(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrson 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). @@ -771,9 +616,8 @@ def search_ext(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrson The amount of search results retrieved can be limited with the sizelimit parameter if non-zero. """ - base, filterstr = self._bytesify_inputs(base, filterstr) - if attrlist is not None: - attrlist = tuple(self._bytesify_inputs(*attrlist)) + if filterstr is None: + filterstr = '(objectClass=*)' return self._ldap_call( self._l.search_ext, base,scope,filterstr, @@ -783,17 +627,17 @@ def search_ext(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrson timeout,sizelimit, ) - def search_ext_s(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): + def search_ext_s(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): msgid = self.search_ext(base,scope,filterstr,attrlist,attrsonly,serverctrls,clientctrls,timeout,sizelimit) return self.result(msgid,all=1,timeout=timeout)[1] - def search(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0): + def search(self,base,scope,filterstr=None,attrlist=None,attrsonly=0): return self.search_ext(base,scope,filterstr,attrlist,attrsonly,None,None) - def search_s(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0): + def search_s(self,base,scope,filterstr=None,attrlist=None,attrsonly=0): return self.search_ext_s(base,scope,filterstr,attrlist,attrsonly,None,None,timeout=self.timeout) - def search_st(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0,timeout=-1): + def search_st(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,timeout=-1): return self.search_ext_s(base,scope,filterstr,attrlist,attrsonly,None,None,timeout) def start_tls_s(self): @@ -860,7 +704,7 @@ def set_option(self,option,invalue): invalue = RequestControlTuples(invalue) return self._ldap_call(self._l.set_option,option,invalue) - def search_subschemasubentry_s(self,dn=''): + def search_subschemasubentry_s(self,dn=None): """ Returns the distinguished name of the sub schema sub entry for a part of a DIT specified by dn. @@ -870,9 +714,13 @@ def search_subschemasubentry_s(self,dn=''): Returns: None or text/bytes depending on bytes_mode. """ + empty_dn = '' + attrname = 'subschemaSubentry' + if dn is None: + dn = empty_dn try: r = self.search_s( - dn,ldap.SCOPE_BASE,'(objectClass=*)',['subschemaSubentry'] + dn,ldap.SCOPE_BASE,None,[attrname] ) except (ldap.NO_SUCH_OBJECT,ldap.NO_SUCH_ATTRIBUTE,ldap.INSUFFICIENT_ACCESS): r = [] @@ -881,18 +729,17 @@ def search_subschemasubentry_s(self,dn=''): try: if r: e = ldap.cidict.cidict(r[0][1]) - search_subschemasubentry_dn = e.get('subschemaSubentry',[None])[0] + search_subschemasubentry_dn = e.get(attrname,[None])[0] if search_subschemasubentry_dn is None: if dn: # Try to find sub schema sub entry in root DSE - return self.search_subschemasubentry_s(dn='') + return self.search_subschemasubentry_s(dn=empty_dn) else: # 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 @@ -905,7 +752,7 @@ def read_s(self,dn,filterstr=None,attrlist=None,serverctrls=None,clientctrls=Non r = self.search_ext_s( dn, ldap.SCOPE_BASE, - filterstr or '(objectClass=*)', + filterstr, attrlist=attrlist, serverctrls=serverctrls, clientctrls=clientctrls, @@ -920,18 +767,21 @@ def read_subschemasubentry_s(self,subschemasubentry_dn,attrs=None): """ Returns the sub schema sub entry's data """ + filterstr = '(objectClass=subschema)' + if attrs is None: + attrs = SCHEMA_ATTRS try: subschemasubentry = self.read_s( subschemasubentry_dn, - filterstr='(objectClass=subschema)', - attrlist=attrs or SCHEMA_ATTRS + filterstr=filterstr, + attrlist=attrs ) except ldap.NO_SUCH_OBJECT: return None else: return subschemasubentry - def find_unique_entry(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass=*)',attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1): + def find_unique_entry(self,base,scope=ldap.SCOPE_SUBTREE,filterstr=None,attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1): """ Returns a unique entry, raises exception if not unique """ @@ -939,7 +789,7 @@ def find_unique_entry(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass base, scope, filterstr, - attrlist=attrlist or ['*'], + attrlist=attrlist, attrsonly=attrsonly, serverctrls=serverctrls, clientctrls=clientctrls, @@ -950,14 +800,16 @@ def find_unique_entry(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass raise NO_UNIQUE_ENTRY('No or non-unique search result for %s' % (repr(filterstr))) return r[0] - def read_rootdse_s(self, filterstr='(objectClass=*)', attrlist=None): + def read_rootdse_s(self, filterstr=None, attrlist=None): """ convenience wrapper around read_s() for reading rootDSE """ + base = '' + attrlist = attrlist or ['*', '+'] ldap_rootdse = self.read_s( - '', + base, filterstr=filterstr, - attrlist=attrlist or ['*', '+'], + attrlist=attrlist, ) return ldap_rootdse # read_rootdse_s() @@ -966,36 +818,46 @@ 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 """ + name = 'namingContexts' return self.read_rootdse_s( - attrlist=['namingContexts'] - ).get('namingContexts', []) + attrlist=[name] + ).get(name, []) class ReconnectLDAPObject(SimpleLDAPObject): """ - In case of server failure (ldap.SERVER_DOWN) the implementations - of all synchronous operation methods (search_s() etc.) are doing - an automatic reconnect and rebind and will retry the very same - operation. - - This is very handy for broken LDAP server implementations - (e.g. in Lotus Domino) which drop connections very often making - it impossible to have a long-lasting control flow in the - application. + :py:class:`SimpleLDAPObject` subclass whose synchronous request methods + automatically reconnect and re-try in case of server failure. + + The first arguments are same as for the :py:func:`~ldap.initialize()` + function. + For automatic reconnects it has additional arguments: + + * retry_max: specifies the number of reconnect attempts before + re-raising the :py:exc:`ldap.SERVER_DOWN` exception. + + * 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__ = set([ + __transient_attrs__ = { '_l', '_ldap_object_lock', '_trace_file', '_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, - 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 @@ -1009,7 +871,10 @@ def __init__( self._uri = uri self._options = [] self._last_bind = None - SimpleLDAPObject.__init__(self,uri,trace_level,trace_file,trace_stack_limit,bytes_mode) + SimpleLDAPObject.__init__(self, uri, trace_level, trace_file, + trace_stack_limit, bytes_mode, + 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 @@ -1018,25 +883,35 @@ def __init__( def __getstate__(self): """return data representation for pickled object""" - state = dict([ - (k,v) + state = { + k: v 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): """set up the object from pickled data""" + hardfail = d.get('bytes_mode_hardfail') + if hardfail: + d.setdefault('bytes_strictness', 'error') + 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))) - self._trace_file = sys.stdout - self.reconnect(self._uri) + # XXX cannot pickle file, use default trace file + self._trace_file = ldap._trace_file + 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: @@ -1044,7 +919,7 @@ def _apply_last_bind(self): func(self,*args,**kwargs) else: # Send explicit anon simple bind request to provoke ldap.SERVER_DOWN in method reconnect() - SimpleLDAPObject.simple_bind_s(self,'','') + SimpleLDAPObject.simple_bind_s(self, None, None) def _restore_options(self): """Restore all recorded options""" @@ -1054,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 @@ -1086,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 @@ -1099,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 a853500d..bf4e4819 100644 --- a/Lib/ldap/modlist.py +++ b/Lib/ldap/modlist.py @@ -11,16 +11,16 @@ def addModlist(entry,ignore_attr_types=None): """Build modify list for call of method LDAPObject.add()""" - ignore_attr_types = set(map(str.lower,ignore_attr_types or [])) + ignore_attr_types = {v.lower() for v in ignore_attr_types or []} modlist = [] - for attrtype in entry.keys(): + for attrtype, value in entry.items(): if attrtype.lower() in ignore_attr_types: # This attribute type is ignored continue # Eliminate empty attr value strings in list - attrvaluelist = [item for item in entry[attrtype] if item is not None] + attrvaluelist = [item for item in value if item is not None] if attrvaluelist: - modlist.append((attrtype,entry[attrtype])) + modlist.append((attrtype, value)) return modlist # addModlist() @@ -46,19 +46,19 @@ def modifyModlist( List of attribute type names for which comparison will be made case-insensitive """ - ignore_attr_types = set(map(str.lower,ignore_attr_types or [])) - case_ignore_attr_types = set(map(str.lower,case_ignore_attr_types or [])) + ignore_attr_types = {v.lower() for v in ignore_attr_types or []} + case_ignore_attr_types = {v.lower() for v in case_ignore_attr_types or []} modlist = [] attrtype_lower_map = {} - for a in old_entry.keys(): - attrtype_lower_map[str.lower(a)]=a - for attrtype in new_entry.keys(): - attrtype_lower = str.lower(attrtype) + for a in old_entry: + attrtype_lower_map[a.lower()]=a + for attrtype, value in new_entry.items(): + attrtype_lower = attrtype.lower() if attrtype_lower in ignore_attr_types: # This attribute type is ignored continue # Filter away null-strings - new_value = [item for item in new_entry[attrtype] if item is not None] + new_value = [item for item in value if item is not None] if attrtype_lower in attrtype_lower_map: old_value = old_entry.get(attrtype_lower_map[attrtype_lower],[]) old_value = [item for item in old_value if item is not None] @@ -73,9 +73,8 @@ def modifyModlist( replace_attr_value = len(old_value)!=len(new_value) if not replace_attr_value: if attrtype_lower in case_ignore_attr_types: - norm_func = str.lower - old_value_set = set(map(str.lower,old_value)) - new_value_set = set(map(str.lower,new_value)) + old_value_set = {v.lower() for v in old_value} + new_value_set = {v.lower() for v in new_value} else: old_value_set = set(old_value) new_value_set = set(new_value) @@ -89,10 +88,10 @@ def modifyModlist( if not ignore_oldexistent: # Remove all attributes of old_entry which are not present # in new_entry at all - for a in attrtype_lower_map.keys(): + for a, val in attrtype_lower_map.items(): if a in ignore_attr_types: # This attribute type is ignored continue - attrtype = attrtype_lower_map[a] + attrtype = val modlist.append((ldap.MOD_DELETE,attrtype,None)) return modlist # modifyModlist() diff --git a/Lib/ldap/pkginfo.py b/Lib/ldap/pkginfo.py index da891b3d..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__ = '2.5.2' -__author__ = u'python-ldap project' +__version__ = '3.4.5' +__author__ = 'python-ldap project' __license__ = 'Python style' diff --git a/Lib/ldap/sasl.py b/Lib/ldap/sasl.py index fa6f4f5c..cc0a2ead 100644 --- a/Lib/ldap/sasl.py +++ b/Lib/ldap/sasl.py @@ -54,8 +54,8 @@ def callback(self, cb_id, challenge, prompt, defresult): """ The callback method will be called by the sasl_bind_s() method several times. Each time it will provide the id, which - tells us what kind of information is requested (the CB_ ... - constants above). The challenge might be a short (english) text + tells us what kind of information is requested (the CB_* + constants above). The challenge might be a short (English) text or some binary string, from which the return value is calculated. The prompt argument is always a human-readable description string; The defresult is a default value provided by the sasl library diff --git a/Lib/ldap/schema/models.py b/Lib/ldap/schema/models.py index c0391b4c..3d9322c0 100644 --- a/Lib/ldap/schema/models.py +++ b/Lib/ldap/schema/models.py @@ -7,11 +7,11 @@ import sys import ldap.cidict -from ldap.compat import IterableUserDict +from collections import UserDict from ldap.schema.tokenizer import split_tokens,extract_tokens -NOT_HUMAN_READABLE_LDAP_SYNTAXES = set([ +NOT_HUMAN_READABLE_LDAP_SYNTAXES = { '1.3.6.1.4.1.1466.115.121.1.4', # Audio '1.3.6.1.4.1.1466.115.121.1.5', # Binary '1.3.6.1.4.1.1466.115.121.1.8', # Certificate @@ -21,7 +21,7 @@ '1.3.6.1.4.1.1466.115.121.1.28', # JPEG '1.3.6.1.4.1.1466.115.121.1.40', # Octet String '1.3.6.1.4.1.1466.115.121.1.49', # Supported Algorithm -]) +} class SchemaElement: @@ -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)] @@ -106,26 +106,32 @@ class ObjectClass(SchemaElement): oid OID assigned to the object class names - This list of strings contains all NAMEs of the object class + All NAMEs of the object class (tuple of strings) desc - This string contains description text (DESC) of the object class + Description text (DESC) of the object class (string, or None if missing) obsolete Integer flag (0 or 1) indicating whether the object class is marked as OBSOLETE in the schema must - This list of strings contains NAMEs or OIDs of all attributes - an entry of the object class must have + NAMEs or OIDs of all attributes an entry of the object class must have + (tuple of strings) may - This list of strings contains NAMEs or OIDs of additional attributes - an entry of the object class may have + NAMEs or OIDs of additional attributes an entry of the object class may + have (tuple of strings) kind Kind of an object class: 0 = STRUCTURAL, 1 = ABSTRACT, 2 = AUXILIARY sup - This list of strings contains NAMEs or OIDs of object classes - this object class is derived from + NAMEs or OIDs of object classes this object class is derived from + (tuple of strings) + x_origin + Value of the X-ORIGIN extension flag (tuple of strings) + + Although it's not official, X-ORIGIN is used in several LDAP server + implementations to indicate the source of the associated schema + element """ schema_attribute = 'objectClasses' token_defaults = { @@ -137,7 +143,8 @@ class ObjectClass(SchemaElement): 'AUXILIARY':None, 'ABSTRACT':None, 'MUST':(()), - 'MAY':() + 'MAY':(), + 'X-ORIGIN':() } def _set_attrs(self,l,d): @@ -146,6 +153,7 @@ def _set_attrs(self,l,d): self.desc = d['DESC'][0] self.must = d['MUST'] self.may = d['MAY'] + self.x_origin = d['X-ORIGIN'] # Default is STRUCTURAL, see RFC2552 or draft-ietf-ldapbis-syntaxes self.kind = 0 if d['ABSTRACT']!=None: @@ -168,6 +176,7 @@ def __str__(self): result.append({0:' STRUCTURAL',1:' ABSTRACT',2:' AUXILIARY'}[self.kind]) result.append(self.key_list('MUST',self.must,sep=' $ ')) result.append(self.key_list('MAY',self.may,sep=' $ ')) + result.append(self.key_list('X-ORIGIN',self.x_origin,quoted=1)) return '( %s )' % ''.join(result) @@ -190,11 +199,11 @@ class AttributeType(SchemaElement): Class attributes: oid - OID assigned to the attribute type + OID assigned to the attribute type (string) names - This list of strings contains all NAMEs of the attribute type + All NAMEs of the attribute type (tuple of strings) desc - This string contains description text (DESC) of the attribute type + Description text (DESC) of the attribute type (string, or None if missing) obsolete Integer flag (0 or 1) indicating whether the attribute type is marked as OBSOLETE in the schema @@ -202,19 +211,19 @@ class AttributeType(SchemaElement): Integer flag (0 or 1) indicating whether the attribute must have only one value syntax - String contains OID of the LDAP syntax assigned to the attribute type + OID of the LDAP syntax assigned to the attribute type no_user_mod Integer flag (0 or 1) indicating whether the attribute is modifiable by a client application equality - String contains NAME or OID of the matching rule used for - checking whether attribute values are equal + NAME or OID of the matching rule used for checking whether attribute values + are equal (string, or None if missing) substr - String contains NAME or OID of the matching rule used for - checking whether an attribute value contains another value + NAME or OID of the matching rule used for checking whether an attribute + value contains another value (string, or None if missing) ordering - String contains NAME or OID of the matching rule used for - checking whether attribute values are lesser-equal than + NAME or OID of the matching rule used for checking whether attribute values + are lesser-equal than (string, or None if missing) usage USAGE of an attribute type: 0 = userApplications @@ -222,8 +231,14 @@ class AttributeType(SchemaElement): 2 = distributedOperation, 3 = dSAOperation sup - This list of strings contains NAMEs or OIDs of attribute types - this attribute type is derived from + NAMEs or OIDs of attribute types this attribute type is derived from + (tuple of strings) + x_origin + Value of the X-ORIGIN extension flag (tuple of strings). + + Although it's not official, X-ORIGIN is used in several LDAP server + implementations to indicate the source of the associated schema + element """ schema_attribute = 'attributeTypes' token_defaults = { @@ -239,7 +254,7 @@ class AttributeType(SchemaElement): 'COLLECTIVE':None, 'NO-USER-MODIFICATION':None, 'USAGE':('userApplications',), - 'X-ORIGIN':(None,), + 'X-ORIGIN':(), 'X-ORDERED':(None,), } @@ -251,7 +266,7 @@ def _set_attrs(self,l,d): self.equality = d['EQUALITY'][0] self.ordering = d['ORDERING'][0] self.substr = d['SUBSTR'][0] - self.x_origin = d['X-ORIGIN'][0] + self.x_origin = d['X-ORIGIN'] self.x_ordered = d['X-ORDERED'][0] try: syntax = d['SYNTAX'][0] @@ -302,7 +317,7 @@ def __str__(self): 3:" USAGE dSAOperation", }[self.usage] ) - result.append(self.key_attr('X-ORIGIN',self.x_origin,quoted=1)) + result.append(self.key_list('X-ORIGIN',self.x_origin,quoted=1)) result.append(self.key_attr('X-ORDERED',self.x_ordered,quoted=1)) return '( %s )' % ''.join(result) @@ -314,7 +329,7 @@ class LDAPSyntax(SchemaElement): oid OID assigned to the LDAP syntax desc - This string contains description text (DESC) of the LDAP syntax + Description text (DESC) of the LDAP syntax (string, or None if missing) not_human_readable Integer flag (0 or 1) indicating whether the attribute type is marked as not human-readable (X-NOT-HUMAN-READABLE) @@ -358,14 +373,15 @@ class MatchingRule(SchemaElement): oid OID assigned to the matching rule names - This list of strings contains all NAMEs of the matching rule + All NAMEs of the matching rule (tuple of strings) desc - This string contains description text (DESC) of the matching rule + Description text (DESC) of the matching rule obsolete Integer flag (0 or 1) indicating whether the matching rule is marked as OBSOLETE in the schema syntax - String contains OID of the LDAP syntax this matching rule is usable with + OID of the LDAP syntax this matching rule is usable with + (string, or None if missing) """ schema_attribute = 'matchingRules' token_defaults = { @@ -403,15 +419,15 @@ class MatchingRuleUse(SchemaElement): oid OID of the accompanying matching rule names - This list of strings contains all NAMEs of the matching rule + All NAMEs of the matching rule (tuple of strings) desc - This string contains description text (DESC) of the matching rule + Description text (DESC) of the matching rule (string, or None if missing) obsolete Integer flag (0 or 1) indicating whether the matching rule is marked as OBSOLETE in the schema applies - This list of strings contains NAMEs or OIDs of attribute types - for which this matching rule is used + NAMEs or OIDs of attribute types for which this matching rule is used + (tuple of strings) """ schema_attribute = 'matchingRuleUse' token_defaults = { @@ -449,26 +465,29 @@ class DITContentRule(SchemaElement): oid OID of the accompanying structural object class names - This list of strings contains all NAMEs of the DIT content rule + All NAMEs of the DIT content rule (tuple of strings) desc - This string contains description text (DESC) of the DIT content rule + Description text (DESC) of the DIT content rule + (string, or None if missing) obsolete Integer flag (0 or 1) indicating whether the DIT content rule is marked as OBSOLETE in the schema aux - This list of strings contains NAMEs or OIDs of all auxiliary - object classes usable in an entry of the object class + NAMEs or OIDs of all auxiliary object classes usable in an entry of the + object class (tuple of strings) must - This list of strings contains NAMEs or OIDs of all attributes - an entry of the object class must have which may extend the - list of required attributes of the object classes of an entry + NAMEs or OIDs of all attributes an entry of the object class must + have, which may extend the list of required attributes of the object + classes of an entry. + (tuple of strings) may - This list of strings contains NAMEs or OIDs of additional attributes - an entry of the object class may have which may extend the - list of optional attributes of the object classes of an entry + NAMEs or OIDs of additional attributes an entry of the object class may + have. which may extend the list of optional attributes of the object + classes of an entry. + (tuple of strings) nots - This list of strings contains NAMEs or OIDs of attributes which - may not be present in an entry of the object class + NAMEs or OIDs of attributes which may not be present in an entry of the + object class. (tuple of strings) """ schema_attribute = 'dITContentRules' token_defaults = { @@ -515,17 +534,18 @@ class DITStructureRule(SchemaElement): ruleid rule ID of the DIT structure rule (only locally unique) names - This list of strings contains all NAMEs of the DIT structure rule + All NAMEs of the DIT structure rule (tuple of strings) desc - This string contains description text (DESC) of the DIT structure rule + Description text (DESC) of the DIT structure rule + (string, or None if missing) obsolete Integer flag (0 or 1) indicating whether the DIT content rule is marked as OBSOLETE in the schema form - List of strings with NAMEs or OIDs of associated name forms + NAMEs or OIDs of associated name forms (tuple of strings) sup - List of strings with NAMEs or OIDs of allowed structural object classes - of superior entries in the DIT + NAMEs or OIDs of allowed structural object classes + of superior entries in the DIT (tuple of strings) """ schema_attribute = 'dITStructureRules' @@ -573,23 +593,22 @@ class NameForm(SchemaElement): oid OID of the name form names - This list of strings contains all NAMEs of the name form + All NAMEs of the name form (tuple of strings) desc - This string contains description text (DESC) of the name form + Description text (DESC) of the name form (string, or None if missing) obsolete Integer flag (0 or 1) indicating whether the name form is marked as OBSOLETE in the schema form - List of strings with NAMEs or OIDs of associated name forms + NAMEs or OIDs of associated name forms (tuple of strings) oc - String with NAME or OID of structural object classes this name form - is usable with + NAME or OID of structural object classes this name form + is usable with (string) must - This list of strings contains NAMEs or OIDs of all attributes - an RDN must contain + NAMEs or OIDs of all attributes an RDN must contain (tuple of strings) may - This list of strings contains NAMEs or OIDs of additional attributes - an RDN may contain + NAMEs or OIDs of additional attributes an RDN may contain + (tuple of strings) """ schema_attribute = 'nameForms' token_defaults = { @@ -621,7 +640,7 @@ def __str__(self): return '( %s )' % ''.join(result) -class Entry(IterableUserDict): +class Entry(UserDict): """ Schema-aware implementation of an LDAP entry class. @@ -634,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): @@ -655,8 +674,8 @@ def _at2key(self,nameoroid): return t def update(self,dict): - for key in dict.keys(): - self[key] = dict[key] + for key, value in dict.items(): + self[key] = value def __contains__(self,nameoroid): return self._at2key(nameoroid) in self.data @@ -679,12 +698,6 @@ def has_key(self,nameoroid): k = self._at2key(nameoroid) return k in self.data - def get(self,nameoroid,failobj): - try: - return self[nameoroid] - except KeyError: - return failobj - def keys(self): return self._keytuple2attrtype.values() diff --git a/Lib/ldap/schema/subentry.py b/Lib/ldap/schema/subentry.py index 2a42b4c0..3f73df71 100644 --- a/Lib/ldap/schema/subentry.py +++ b/Lib/ldap/schema/subentry.py @@ -4,20 +4,25 @@ See https://www.python-ldap.org/ for details. """ -import ldap.cidict,ldap.schema +import copy +from urllib.request import urlopen +import ldap.cidict,ldap.schema from ldap.schema.models import * +import ldapurl +import ldif + + SCHEMA_CLASS_MAPPING = ldap.cidict.cidict() SCHEMA_ATTR_MAPPING = {} -for _name in dir(): - o = eval(_name) +for o in list(vars().values()): if hasattr(o,'schema_attribute'): 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): @@ -116,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) @@ -124,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__() @@ -137,8 +142,8 @@ def ldap_entry(self): entry = {} # Collect the schema elements and store them in # entry's attributes - for se_class in self.sed.keys(): - for se in self.sed[se_class].values(): + for se_class, elements in self.sed.items(): + for se in elements.values(): se_str = str(se) try: entry[SCHEMA_ATTR_MAPPING[se_class]].append(se_str) @@ -154,8 +159,7 @@ def listall(self,schema_element_class,schema_element_filters=None): avail_se = self.sed[schema_element_class] if schema_element_filters: result = [] - for se_key in avail_se.keys(): - se = avail_se[se_key] + for se_key, se in avail_se.items(): for fk,fv in schema_element_filters: try: if getattr(se,fk) in fv: @@ -163,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 @@ -187,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 ('_',): @@ -212,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 @@ -245,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: @@ -258,7 +262,6 @@ def get_inheritedobj(self,se_class,nameoroid,inherited=None): Get a schema element by name or OID with all class attributes set including inherited class attributes """ - import copy inherited = inherited or [] se = copy.copy(self.sed[se_class].get(self.getoid(se_class,nameoroid))) if se and hasattr(se,'sup'): @@ -418,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] @@ -453,12 +456,10 @@ def urlfetch(uri,trace_level=0): is loaded with urllib. """ uri = uri.strip() - if uri.startswith('ldap:') or uri.startswith('ldaps:') or uri.startswith('ldapi:'): - import ldapurl + if uri.startswith(('ldap:', 'ldaps:', 'ldapi:')): ldap_url = ldapurl.LDAPUrl(uri) - # This is an internal function; don't enable bytes_mode. - l=ldap.initialize(ldap_url.initializeUrl(),trace_level,bytes_mode=False) + l=ldap.initialize(ldap_url.initializeUrl(),trace_level) l.protocol_version = ldap.VERSION3 l.simple_bind_s(ldap_url.who or '', ldap_url.cred or '') subschemasubentry_dn = l.search_subschemasubentry_s(ldap_url.dn) @@ -475,12 +476,10 @@ def urlfetch(uri,trace_level=0): l.unbind_s() del l else: - import ldif - from ldap.compat import urlopen - 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 8442726a..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 = dict() + 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 @@ -484,7 +487,7 @@ def syncrepl_set_cookie(self, cookie): def syncrepl_get_cookie(self): """ - Called by syncrepl_search() to retreive the cookie stored by syncrepl_set_cookie() + Called by syncrepl_search() to retrieve the cookie stored by syncrepl_set_cookie() """ pass @@ -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 366e177d..57900028 100644 --- a/Lib/ldapurl.py +++ b/Lib/ldapurl.py @@ -4,7 +4,7 @@ See https://www.python-ldap.org/ for details. """ -__version__ = '2.5.2' +__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(map(str,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): @@ -313,9 +320,9 @@ def applyDefaults(self,defaults): Dictionary containing a mapping from class attributes to default values """ - for k in defaults.keys(): + for k, value in defaults.items(): if getattr(self,k) is None: - setattr(self,k,defaults[k]) + setattr(self, k, value) def initializeUrl(self): """ @@ -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__() @@ -429,4 +442,3 @@ def __delattr__(self,name): pass else: del self.__dict__[name] - diff --git a/Lib/ldif.py b/Lib/ldif.py index 82c4e3f8..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__ = '2.5.2' +__version__ = '3.4.5' __all__ = [ # constants @@ -23,8 +20,10 @@ import re from base64 import b64encode, b64decode 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'(([^,]|\\,)+|".*?")' @@ -69,7 +68,7 @@ def list_dict(l): """ return a dictionary with all items of l being the keys of the dictionary """ - return dict([(i,None) for i in l]) + return {i: None for i in l} class LDIFWriter: @@ -149,8 +148,8 @@ def _unparseEntryRecord(self,entry): entry dictionary holding an entry """ - for attr_type in sorted(entry.keys()): - for attr_value in entry[attr_type]: + for attr_type, values in sorted(entry.items()): + for attr_value in values: self._unparseAttrTypeandValue(attr_type,attr_value) def _unparseChangeRecord(self,modlist): @@ -209,7 +208,7 @@ def unparse(self,dn,record): def CreateLDIF(dn,record,base64_attrs=None,cols=76): """ Create LDIF single formatted record including trailing empty line. - This is a compability function. Use is deprecated! + This is a compatibility function. dn string-representation of distinguished name @@ -222,6 +221,12 @@ def CreateLDIF(dn,record,base64_attrs=None,cols=76): Specifies how many columns a line may have before it's folded into many lines. """ + warnings.warn( + 'ldif.CreateLDIF() is deprecated. Use LDIFWriter.unparse() instead. It ' + 'will be removed in python-ldap 3.1', + category=DeprecationWarning, + stacklevel=2, + ) f = StringIO() ldif_writer = LDIFWriter(f,base64_attrs,cols,'\n') ldif_writer.unparse(dn,record) @@ -368,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. @@ -395,7 +401,7 @@ def _consume_empty_lines(self): def parse_entry_records(self): """ - Continously read and parse LDIF entry records + Continuously read and parse LDIF entry records """ # Local symbol for better performance next_key_and_value = self._next_key_and_value @@ -449,7 +455,7 @@ def parse_entry_records(self): def parse(self): """ - Invokes LDIFParser.parse_entry_records() for backward compability + Invokes LDIFParser.parse_entry_records() for backward compatibility """ return self.parse_entry_records() # parse() @@ -570,8 +576,10 @@ def parse_change_records(self): class LDIFRecordList(LDIFParser): """ - Collect all records of LDIF input into a single list. - of 2-tuples (dn,entry). It can be a memory hog! + Collect all records of a LDIF file. It can be a memory hog! + + Records are stored in :attr:`.all_records` as a single list + of 2-tuples (dn, entry), after calling :meth:`.parse`. """ def __init__( @@ -579,20 +587,15 @@ def __init__( input_file, ignored_attr_types=None,max_entries=0,process_url_schemes=None ): - """ - See LDIFParser.__init__() - - Additional Parameters: - all_records - List instance for storing parsed records - """ LDIFParser.__init__(self,input_file,ignored_attr_types,max_entries,process_url_schemes) + + #: List storing parsed records. self.all_records = [] self.all_modify_changes = [] def handle(self,dn,entry): """ - Append single record to dictionary of all records. + Append a single record to the list of all records (:attr:`.all_records`). """ self.all_records.append((dn,entry)) @@ -633,8 +636,14 @@ def handle(self,dn,entry): def ParseLDIF(f,ignore_attrs=None,maxentries=0): """ Parse LDIF records read from file. - This is a compability function. Use is deprecated! + This is a compatibility function. """ + warnings.warn( + 'ldif.ParseLDIF() is deprecated. Use LDIFRecordList.parse() instead. It ' + 'will be removed in python-ldap 3.1', + category=DeprecationWarning, + stacklevel=2, + ) ldif_parser = LDIFRecordList( f,ignored_attr_types=ignore_attrs,max_entries=maxentries,process_url_schemes=0 ) diff --git a/Lib/slapdtest/__init__.py b/Lib/slapdtest/__init__.py new file mode 100644 index 00000000..0fabc4c4 --- /dev/null +++ b/Lib/slapdtest/__init__.py @@ -0,0 +1,12 @@ +""" +slapdtest - module for spawning test instances of OpenLDAP's slapd server + +See https://www.python-ldap.org/ for details. +""" + +__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.py b/Lib/slapdtest/_slapdtest.py similarity index 50% rename from Lib/slapdtest.py rename to Lib/slapdtest/_slapdtest.py index 4955420a..b60313b0 100644 --- a/Lib/slapdtest.py +++ b/Lib/slapdtest/_slapdtest.py @@ -1,46 +1,132 @@ -# -*- 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 - -__version__ = '2.5.2' - import os import socket +import sys import time import subprocess import logging import atexit from logging.handlers import SysLogHandler import unittest - -from ldap.compat import quote_plus - -# a template string for generating simple slapd.conf file -SLAPD_CONF_TEMPLATE = r""" -serverID %(serverid)s -moduleload back_%(database)s -include "%(schema_prefix)s/core.schema" -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" +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 + +HERE = os.path.abspath(os.path.dirname(__file__)) + +# 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' +CI_DISABLED = set(os.environ.get('CI_DISABLED', '').split(':')) +if 'LDAPI' in CI_DISABLED: + HAVE_LDAPI = False +else: + HAVE_LDAPI = hasattr(socket, 'AF_UNIX') + + +def identity(test_item): + """Identity decorator + + """ + return test_item + + +def skip_unless_ci(reason, feature=None): + """Skip test unless test case is executed on CI like Travis CI + """ + if not os.environ.get('CI', False): + return unittest.skip(reason) + elif feature in CI_DISABLED: + return unittest.skip(reason) + else: + # Don't skip on Travis + return identity + + +def requires_tls(): + """Decorator for TLS tests + + Tests are not skipped on CI (e.g. Travis CI) + """ + if not ldap.TLS_AVAIL: + return skip_unless_ci("test needs ldap.TLS_AVAIL", feature='TLS') + else: + return identity + + +def requires_sasl(): + if not ldap.SASL_AVAIL: + return skip_unless_ci( + "test needs ldap.SASL_AVAIL", feature='SASL') + else: + return identity + + +def requires_ldapi(): + if not HAVE_LDAPI: + return skip_unless_ci( + "test needs ldapi support (AF_UNIX)", feature='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) + if sys.platform != 'win32': + for sbin in '/usr/local/sbin', '/sbin', '/usr/sbin': + if sbin not in directories: + directories.append(sbin) + return os.pathsep.join(directories) + def combined_logger( log_name, log_level=logging.WARN, @@ -59,7 +145,7 @@ def combined_logger( pass # for writing to syslog new_logger = logging.getLogger(log_name) - if sys_log_format: + if sys_log_format and os.path.exists('/dev/log'): my_syslog_formatter = logging.Formatter( fmt=' '.join((log_name, sys_log_format))) my_syslog_handler = logging.handlers.SysLogHandler( @@ -77,37 +163,43 @@ def combined_logger( return new_logger # end of combined_logger() -class SlapdObject(object): +class SlapdObject: """ Controller class for a slapd instance, OpenLDAP's server. This class creates a temporary data store for slapd, runs it listening on a private Unix domain socket and TCP port, - and initialises it with a top-level entry and the root user. + and initializes it with a top-level entry and the root user. When a reference to an instance of this class is lost, the slapd server is shut down. + + An instance can be used as a context manager. When exiting the context + 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 """ slapd_conf_template = SLAPD_CONF_TEMPLATE database = 'mdb' suffix = 'dc=slapd-test,dc=python-ldap,dc=org' root_cn = 'Manager' - root_dn = 'cn=%s,%s' % (root_cn, suffix) root_pw = 'password' slapd_loglevel = 'stats stats2' - # use SASL/EXTERNAL via LDAPI when invoking OpenLDAP CLI tools - cli_sasl_external = True - 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()) - SBINDIR = os.environ.get('SBIN', '/usr/sbin') - BINDIR = os.environ.get('BIN', '/usr/bin') if 'SCHEMA' in os.environ: SCHEMADIR = os.environ['SCHEMA'] elif os.path.isdir("/etc/openldap/schema"): @@ -116,39 +208,82 @@ class SlapdObject(object): SCHEMADIR = "/etc/ldap/schema" else: SCHEMADIR = None - PATH_LDAPADD = os.path.join(BINDIR, 'ldapadd') - PATH_LDAPMODIFY = os.path.join(BINDIR, 'ldapmodify') - PATH_LDAPWHOAMI = os.path.join(BINDIR, 'ldapwhoami') - PATH_SLAPD = os.environ.get('SLAPD', os.path.join(SBINDIR, 'slapd')) - PATH_SLAPTEST = os.path.join(SBINDIR, 'slaptest') - # time in secs to wait before trying to access slapd via LDAP (again) - _start_sleep = 1.5 + BIN_PATH = os.environ.get('BIN', os.environ.get('PATH', os.defpath)) + SBIN_PATH = os.environ.get('SBIN', _add_sbin(BIN_PATH)) + + # create loggers once, multiple calls mess up refleak tests + _log = combined_logger('python-ldap-test') def __init__(self): - self._log = combined_logger('python-ldap-test') self._proc = None 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) - ldapi_path = os.path.join(self.testrundir, 'ldapi') - self.ldapi_uri = "ldapi://%s" % quote_plus(ldapi_path) - - def _check_requirements(self): - binaries = [ - self.PATH_LDAPADD, self.PATH_LDAPMODIFY, self.PATH_LDAPWHOAMI, - self.PATH_SLAPD, self.PATH_SLAPTEST - ] - for binary in binaries: - if not os.path.isfile(binary): - raise ValueError('Binary {} is missing.'.format(binary)) + 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) + self.default_ldap_uri = self.ldapi_uri + # use SASL/EXTERNAL via LDAPI when invoking OpenLDAP CLI tools + self.cli_sasl_external = ldap.SASL_AVAIL + else: + self.ldapi_uri = None + self.default_ldap_uri = self.ldap_uri + # Use simple bind via LDAP uri + self.cli_sasl_external = False + + self._find_commands() + if self.SCHEMADIR is None: raise ValueError('SCHEMADIR is None, ldap schemas are missing.') + # TLS certs + self.cafile = os.path.join(HERE, 'certs/ca.pem') + self.servercert = os.path.join(HERE, 'certs/server.pem') + self.serverkey = os.path.join(HERE, 'certs/server.key') + self.clientcert = os.path.join(HERE, 'certs/client.pem') + self.clientkey = os.path.join(HERE, 'certs/client.key') + + @property + 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') + self.PATH_LDAPMODIFY = self._find_command('ldapmodify') + self.PATH_LDAPWHOAMI = self._find_command('ldapwhoami') + + self.PATH_SLAPD = os.environ.get('SLAPD', None) + if not self.PATH_SLAPD: + self.PATH_SLAPD = self._find_command('slapd', in_sbin=True) + + def _find_command(self, cmd, in_sbin=False): + if in_sbin: + path = self.SBIN_PATH + var_name = 'SBIN' + else: + path = self.BIN_PATH + var_name = 'BIN' + command = which(cmd, path=path) + if command is None: + raise ValueError( + "Command '{}' not found. Set the {} environment variable to " + "override slapdtest's search path: {}.".format(cmd, var_name, path) + ) + return command + def setup_rundir(self): """ creates rundir structure @@ -159,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): """ @@ -189,9 +323,11 @@ def _avail_tcp_port(self): find an available port for TCP connection """ sock = socket.socket() - sock.bind((self.local_host, 0)) - port = sock.getsockname()[1] - sock.close() + try: + sock.bind((self.local_host, 0)) + port = sock.getsockname()[1] + finally: + sock.close() self._log.info('Found available port %d', port) return port @@ -204,7 +340,6 @@ def gen_config(self): """ config_dict = { 'serverid': hex(self.server_id), - 'schema_prefix':self._schema_prefix, 'loglevel': self.slapd_loglevel, 'database': self.database, 'directory': self._db_directory, @@ -213,6 +348,10 @@ def gen_config(self): 'rootpw': self.root_pw, 'root_uid': os.getuid(), 'root_gid': os.getgid(), + 'cafile': self.cafile, + 'servercert': self.servercert, + 'serverkey': self.serverkey, + 'slapd_path': self.SBIN_PATH, } return self.slapd_conf_template % config_dict @@ -225,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) @@ -263,30 +404,45 @@ def _start_slapd(self): """ Spawns/forks the slapd process """ + urls = [self.ldap_uri] + if self.ldapi_uri: + urls.append(self.ldapi_uri) slapd_args = [ self.PATH_SLAPD, - '-f', self._slapd_conf, - '-h', '%s' % ' '.join((self.ldap_uri, self.ldapi_uri)), + '-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 - while 1: + deadline = time.monotonic() + 10 + # no cover to avoid spurious coverage changes, see + # https://github.com/python-ldap/python-ldap/issues/127 + 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.ldapi_uri) + 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") def start(self): """ @@ -294,7 +450,6 @@ def start(self): """ if self._proc is None: - self._check_requirements() # prepare directory structure atexit.register(self.stop) self._cleanup_rundir() @@ -307,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 """ @@ -315,14 +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 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): @@ -352,13 +517,27 @@ def _cli_auth_args(self): ] return authc_args - def _cli_popen(self, ldapcommand, extra_args=None, ldap_uri=None, stdin_data=None): - args = [ - ldapcommand, - '-H', ldap_uri or self.ldapi_uri, - ] + self._cli_auth_args() + (extra_args or []) + # no cover to avoid spurious coverage changes + def _cli_popen(self, ldapcommand, extra_args=None, ldap_uri=None, + stdin_data=None, tool=None): # pragma: no cover + if ldap_uri is None: + ldap_uri = self.default_ldap_uri + + 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) + proc = subprocess.Popen( + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) self._log.debug('stdin_data=%r', stdin_data) stdout_data, stderr_data = proc.communicate(stdin_data) if stdout_data is not None: @@ -366,7 +545,11 @@ def _cli_popen(self, ldapcommand, extra_args=None, ldap_uri=None, stdin_data=Non if stderr_data is not None: self._log.debug('stderr_data=%r', stderr_data) if proc.wait() != 0: - raise RuntimeError('ldapadd process failed') + raise RuntimeError( + '{!r} process failed:\n{!r}\n{!r}'.format( + args, stdout_data, stderr_data + ) + ) return stdout_data, stderr_data def ldapwhoami(self, extra_args=None): @@ -389,6 +572,35 @@ def ldapmodify(self, ldif, extra_args=None): self._cli_popen(self.PATH_LDAPMODIFY, extra_args=extra_args, stdin_data=ldif.encode('utf-8')) + def ldapdelete(self, dn, recursive=False, extra_args=None): + """ + Runs ldapdelete on this slapd instance, deleting 'dn' + """ + if extra_args is None: + extra_args = [] + if recursive: + extra_args.append('-r') + 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(exc_type is None) + class SlapdTestCase(unittest.TestCase): """ @@ -413,8 +625,7 @@ def _open_ldap_conn(self, who=None, cred=None, **kwargs): def setUpClass(cls): cls.server = cls.server_class() cls.server.start() - cls.server = cls.server @classmethod def tearDownClass(cls): - cls.server.stop() + cls.server.stop(False) diff --git a/Lib/slapdtest/certs/README b/Lib/slapdtest/certs/README new file mode 100644 index 00000000..4be616ae --- /dev/null +++ b/Lib/slapdtest/certs/README @@ -0,0 +1,24 @@ +python-ldap test certificates +============================= + +Certificates and keys +--------------------- + +* ``ca.pem``: internal root CA certificate +* ``server.pem``: TLS server certificate for slapd, signed by root CA. The + server cert is valid for DNS Name ``localhost`` and IPs ``127.0.0.1`` and + ``:1``. +* ``server.key``: private key for ``server.pem``, no password protection +* ``client.pem``: certificate for TLS client cert authentication, signed by + root CA. +* ``client.key``: private key for ``client.pem``, no password protection + +Configuration and scripts +------------------------- + +* ``ca.conf`` contains the CA definition as well as extensions for the + client and server certificates. +* ``client.conf`` and ``server.conf`` hold the subject and base configuration + for server and client certs. +* ``gencerts.sh`` creates new CA, client and server certificates. +* ``gennssdb.sh`` can be used to create a NSSDB for all certs and keys. diff --git a/Lib/slapdtest/certs/ca.conf b/Lib/slapdtest/certs/ca.conf new file mode 100644 index 00000000..d1d89e18 --- /dev/null +++ b/Lib/slapdtest/certs/ca.conf @@ -0,0 +1,77 @@ +# Written by Christian Heimes + +[default] +ca = "ca" +tmpdir = $ENV::CATMPDIR +outdir = $ENV::CAOUTDIR +name_opt = multiline,-esc_msb,utf8 + +[req] +default_bits = 2048 +encrypt_key = no +default_md = sha256 +utf8 = yes +string_mask = utf8only +prompt = no +distinguished_name = ca_dn + +[ca_dn] +countryName = "DE" +organizationName = "python-ldap" +organizationalUnitName = "slapd-test" +commonName = "Python LDAP Test CA" + +[ca] +default_ca = python_ldap_ca + +[python_ldap_ca] +certificate = $outdir/$ca.pem +private_key = $outdir/$ca.key +new_certs_dir = $tmpdir +serial = $tmpdir/$ca.crt.srl +crlnumber = $tmpdir/$ca.crl.srl +database = $tmpdir/$ca.db +unique_subject = no +default_days = 365200 +default_md = sha256 +policy = match_pol +email_in_dn = no +preserve = no +name_opt = $name_opt +cert_opt = ca_default +copy_extensions = none +default_crl_days = 365100 + +[match_pol] +countryName = match +stateOrProvinceName = optional +localityName = optional +organizationName = match +organizationalUnitName = match +commonName = supplied + +[ca_ext] +basicConstraints = critical,CA:true +keyUsage = critical,keyCertSign,cRLSign +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always + +[server_san] +DNS.1 = localhost +IP.1 = 127.0.0.1 +IP.2 = ::1 + +[server_ext] +basicConstraints = critical,CA:false +keyUsage = critical,digitalSignature,keyEncipherment +extendedKeyUsage = critical,serverAuth +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always +subjectAltName = @server_san + +[client_ext] +basicConstraints = critical,CA:false +keyUsage = critical,digitalSignature +extendedKeyUsage = critical,clientAuth +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always diff --git a/Lib/slapdtest/certs/ca.pem b/Lib/slapdtest/certs/ca.pem new file mode 100644 index 00000000..b52ffafb --- /dev/null +++ b/Lib/slapdtest/certs/ca.pem @@ -0,0 +1,80 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA + Validity + 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: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 + CA:TRUE + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + X509v3 Subject Key Identifier: + 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:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2 + + Signature Algorithm: sha256WithRSAEncryption + 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----- +MIIDjDCCAnSgAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU +MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV +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.conf b/Lib/slapdtest/certs/client.conf new file mode 100644 index 00000000..774dc3a4 --- /dev/null +++ b/Lib/slapdtest/certs/client.conf @@ -0,0 +1,16 @@ +# Written by Christian Heimes + +[req] +default_bits = 2048 +encrypt_key = no +default_md = sha256 +utf8 = yes +string_mask = utf8only +prompt = no +distinguished_name = client_dn + +[client_dn] +countryName = "DE" +organizationName = "python-ldap" +organizationalUnitName = "slapd-test" +commonName = "client" diff --git a/Lib/slapdtest/certs/client.key b/Lib/slapdtest/certs/client.key new file mode 100644 index 00000000..7213c0b4 --- /dev/null +++ b/Lib/slapdtest/certs/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +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 new file mode 100644 index 00000000..ca2989ca --- /dev/null +++ b/Lib/slapdtest/certs/client.pem @@ -0,0 +1,83 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 3 (0x3) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA + Validity + 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: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 + CA:FALSE + X509v3 Key Usage: critical + Digital Signature + X509v3 Extended Key Usage: critical + TLS Web Client Authentication + X509v3 Subject Key Identifier: + 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:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2 + + Signature Algorithm: sha256WithRSAEncryption + 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----- +MIIDlDCCAnygAwIBAgIBAzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU +MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV +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 new file mode 100755 index 00000000..8a99db58 --- /dev/null +++ b/Lib/slapdtest/certs/gencerts.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# Written by Christian Heimes +set -e + +export CAOUTDIR=. +export CATMPDIR=tmp + +rm -rf $CATMPDIR +rm -rf ca.pem ca.key server.pem server.key client.pem client.key +rm -rf cert9.db key4.db pkcs11.tx + +mkdir -p $CAOUTDIR +mkdir -p $CATMPDIR + +touch $CATMPDIR/ca.db +touch $CATMPDIR/ca.db.attr +echo '01' > $CATMPDIR/ca.crt.srl +echo '01' > $CATMPDIR/ca.crl.srl + +# root CA +openssl req -new \ + -config ca.conf \ + -out $CATMPDIR/ca.csr \ + -keyout $CAOUTDIR/ca.key \ + -batch + +openssl ca -selfsign \ + -config ca.conf \ + -in $CATMPDIR/ca.csr \ + -out $CAOUTDIR/ca.pem \ + -extensions ca_ext \ + -days 356300 \ + -batch + +# server cert +openssl req -new \ + -config server.conf \ + -out $CATMPDIR/server.csr \ + -keyout $CAOUTDIR/server.key \ + -batch + +openssl ca \ + -config ca.conf \ + -in $CATMPDIR/server.csr \ + -out $CAOUTDIR/server.pem \ + -policy match_pol \ + -extensions server_ext \ + -batch + +# client cert +openssl req -new \ + -config client.conf \ + -out $CATMPDIR/client.csr \ + -keyout $CAOUTDIR/client.key \ + -batch + +openssl ca \ + -config ca.conf \ + -in $CATMPDIR/client.csr \ + -out $CAOUTDIR/client.pem \ + -policy match_pol \ + -extensions client_ext \ + -batch + +# cleanup +rm -rf $CATMPDIR ca.key + +echo DONE diff --git a/Lib/slapdtest/certs/gennssdb.sh b/Lib/slapdtest/certs/gennssdb.sh new file mode 100755 index 00000000..aeeb3331 --- /dev/null +++ b/Lib/slapdtest/certs/gennssdb.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Written by Christian Heimes +set -e + +CATMPDIR=tmp +PASSFILE=${CATMPDIR}/passwd.txt +NSSDB=sql:${CAOUTDIR} + +mkdir -p $CATMPDIR + +# Create PKCS#12 files for NSSDB import +echo "dummy" > $PASSFILE +openssl pkcs12 -name "servercert" -in server.pem -inkey server.key \ + -caname "testca" -CAfile ca.pem \ + -password "file:${PASSFILE}" -export -out server.p12 +openssl pkcs12 -name "clientcert" -in client.pem -inkey client.key \ + -caname "testca" -CAfile ca.pem \ + -password "file:${PASSFILE}" -export -out client.p12 + +# Create NSS DB +certutil -d $NSSDB -N --empty-password +certutil -d $NSSDB -A -n "testca" -t CT,, -a -i ca.pem +pk12util -d $NSSDB -i server.p12 -w ${PASSFILE} +pk12util -d $NSSDB -i client.p12 -w ${PASSFILE} +certutil -d $NSSDB -L + +# cleanup +rm -rf $CATMPDIR server.p12 client.p12 \ No newline at end of file diff --git a/Lib/slapdtest/certs/server.conf b/Lib/slapdtest/certs/server.conf new file mode 100644 index 00000000..94f4427a --- /dev/null +++ b/Lib/slapdtest/certs/server.conf @@ -0,0 +1,16 @@ +# Written by Christian Heimes + +[req] +default_bits = 2048 +encrypt_key = no +default_md = sha256 +utf8 = yes +string_mask = utf8only +prompt = no +distinguished_name = server_dn + +[server_dn] +countryName = "DE" +organizationName = "python-ldap" +organizationalUnitName = "slapd-test" +commonName = "server cert for localhost" diff --git a/Lib/slapdtest/certs/server.key b/Lib/slapdtest/certs/server.key new file mode 100644 index 00000000..a8916701 --- /dev/null +++ b/Lib/slapdtest/certs/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +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 new file mode 100644 index 00000000..25ba06c0 --- /dev/null +++ b/Lib/slapdtest/certs/server.pem @@ -0,0 +1,86 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 2 (0x2) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA + Validity + 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: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 + CA:FALSE + X509v3 Key Usage: critical + Digital Signature, Key Encipherment + X509v3 Extended Key Usage: critical + TLS Web Server Authentication + X509v3 Subject Key Identifier: + 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: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 + 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----- +MIID1zCCAr+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU +MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV +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 469ebdf0..bedea8d6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,10 @@ -include MANIFEST.in Makefile CHANGES INSTALL LICENCE README TODO PKG-INFO -include tox.ini .coveragerc -include Modules/*.c Modules/*.h Modules/LICENSE +include MANIFEST.in Makefile CHANGES INSTALL LICENCE README TODO +include tox.ini +include Modules/*.c Modules/*.h recursive-include Build *.cfg* recursive-include Lib *.py recursive-include Demo *.py recursive-include Tests *.py *.ldif +recursive-include Lib/slapdtest *.pem *.key *.conf *.sh README +recursive-include Doc *.rst *.py spelling_wordlist.txt Makefile +prune Doc/.build diff --git a/Makefile b/Makefile index 770620a9..da23b374 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,32 @@ +PYTHON=python3 LCOV_INFO=build/lcov.info LCOV_REPORT=build/lcov_report 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 + .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 $(VENV) .tox MANIFEST - rm -f .coverage .coverage.* - find . -name '*.py[co]' -or -name '*.so*' -or -name '*.dylib' -delete + rm -rf build dist *.egg-info .tox MANIFEST + find . \( -name '*.py[co]' -or -name '*.so*' -or -name '*.dylib' \) \ + -delete find . -depth -name __pycache__ -exec rm -rf {} \; +build: + mkdir -p build + # LCOV report (measuring test coverage for C code) .PHONY: lcov-clean lcov-coverage lcov-report lcov-open lcov lcov-clean: @@ -22,9 +34,9 @@ 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): +$(LCOV_INFO): build lcov --capture --directory build --output-file $(LCOV_INFO) $(LCOV_REPORT): $(LCOV_INFO) @@ -39,3 +51,49 @@ lcov-open: $(LCOV_REPORT) lcov: lcov-clean $(MAKE) lcov-coverage $(MAKE) lcov-report + +# clang-analyzer for static C code analysis +.PHONY: scan-build +scan-build: + scan-build -o $(SCAN_REPORT) --html-title="python-ldap scan report" \ + -analyze-headers --view \ + $(PYTHON) setup.py clean --all build + +# valgrind memory checker +.PHONY: valgrind +$(PYTHON_SUPP): + @ >&2 echo "valgrind-python.supp not found" + @ >&2 echo "install Python development files and run:" + @ >&2 echo " $(MAKE) valgrind PYTHON_SUPP=/your/path/to/valgrind-python.supp" + exit 1; + +valgrind: build $(PYTHON_SUPP) + valgrind \ + --leak-check=full \ + --track-fds=yes \ + --suppressions=$(PYTHON_SUPP) \ + --suppressions=Misc/python-ldap.supp \ + --gen-suppressions=all \ + --log-file=build/valgrind.log \ + $(PYTHON) setup.py test + + @grep -A7 "blocks are definitely lost" build/valgrind.log; \ + if [ $$? == 0 ]; then \ + echo "Found definitive leak, see build/valgrind.log"; \ + exit 1; \ + fi + +# Code autoformatter +.PHONY: autoformat indent black black-check +autoformat: indent black + +indent: + indent Modules/*.c + indent -npsl Modules/pythonldap.h + rm -f Modules/*.c~ Modules/*.h~ + +black: + $(PYTHON) -m black $(CURDIR) + +black-check: + $(PYTHON) -m black $(CURDIR) --check diff --git a/Misc/python-ldap.supp b/Misc/python-ldap.supp new file mode 100644 index 00000000..b9954a9a --- /dev/null +++ b/Misc/python-ldap.supp @@ -0,0 +1,52 @@ +# Valgrind suppression file for Python 3.6. + +{ + Ignore libldap memory leak, https://github.com/python-ldap/python-ldap/issues/82 + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:ber_memalloc_x + fun:ber_flatten + fun:ldap_cancel + fun:l_ldap_cancel + ... +} + +{ + Known leak in SASL interaction, https://github.com/python-ldap/python-ldap/issues/81 + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:strdup + fun:interaction + fun:py_ldap_sasl_interaction + fun:ldap_int_sasl_bind + fun:ldap_sasl_interactive_bind + fun:ldap_sasl_interactive_bind_s + fun:l_ldap_sasl_interactive_bind_s + ... +} + +{ + NSS backend leaks one string during first initialization + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:PL_strdup + ... + fun:ldap_set_option + ... +} + +{ + Ignore possible leaks in exception initialization + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:PyObject_Malloc + ... + fun:PyErr_NewException + fun:LDAPinit_constants + fun:init_ldap_module + ... +} diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c index ce0ff52a..f96a6068 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -1,35 +1,25 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" +#include "pythonldap.h" #include "patchlevel.h" #include #include -#include "errors.h" -#include "constants.h" -#include "LDAPObject.h" -#include "ldapcontrol.h" -#include "message.h" -#include "berval.h" -#include "options.h" #ifdef HAVE_SASL -#ifdef __APPLE__ #include -#else -#include -#endif #endif -static void free_attrs(char***, PyObject*); +static void free_attrs(char ***); /* constructor */ -LDAPObject* -newLDAPObject( LDAP* l ) +LDAPObject * +newLDAPObject(LDAP *l) { - LDAPObject* self = (LDAPObject*) PyObject_NEW(LDAPObject, &LDAP_Type); - if (self == NULL) + LDAPObject *self = (LDAPObject *)PyObject_NEW(LDAPObject, &LDAP_Type); + + if (self == NULL) return NULL; self->ldap = l; self->_save = NULL; @@ -40,13 +30,13 @@ newLDAPObject( LDAP* l ) /* destructor */ static void -dealloc( LDAPObject* self ) +dealloc(LDAPObject *self) { if (self->ldap) { if (self->valid) { - LDAP_BEGIN_ALLOW_THREADS( self ); - ldap_unbind_ext( self->ldap, NULL, NULL ); - LDAP_END_ALLOW_THREADS( self ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldap_unbind_ext(self->ldap, NULL, NULL); + LDAP_END_ALLOW_THREADS(self); self->valid = 0; } self->ldap = NULL; @@ -58,17 +48,19 @@ dealloc( LDAPObject* self ) * utility functions */ -/* - * check to see if the LDAPObject is valid, +/* + * check to see if the LDAPObject is valid, * ie has been opened, and not closed. An exception is set if not valid. */ static int -not_valid( LDAPObject* l ) { +not_valid(LDAPObject *l) +{ if (l->valid) { return 0; - } else { - PyErr_SetString( LDAPexception_class, "LDAP connection invalid" ); + } + else { + PyErr_SetString(LDAPexception_class, "LDAP connection invalid"); return 1; } } @@ -76,7 +68,7 @@ not_valid( LDAPObject* l ) { /* free a LDAPMod (complete or partially) allocated in Tuple_to_LDAPMod() */ static void -LDAPMod_DEL( LDAPMod* lm ) +LDAPMod_DEL(LDAPMod *lm) { Py_ssize_t i; @@ -91,8 +83,8 @@ LDAPMod_DEL( LDAPMod* lm ) PyMem_DEL(lm); } -/* - * convert a tuple of the form (int,str,[str,...]) +/* + * convert a tuple of the form (int,str,[str,...]) * or (str, [str,...]) if no_op is true, into an LDAPMod structure. * See ldap_modify(3) for details. * @@ -103,8 +95,8 @@ LDAPMod_DEL( LDAPMod* lm ) /* XXX - there is no way to pass complex-structured BER objects in here! */ -static LDAPMod* -Tuple_to_LDAPMod( PyObject* tup, int no_op ) +static LDAPMod * +Tuple_to_LDAPMod(PyObject *tup, int no_op) { int op; char *type; @@ -113,21 +105,22 @@ Tuple_to_LDAPMod( PyObject* tup, int no_op ) Py_ssize_t i, len, nstrs; if (!PyTuple_Check(tup)) { - PyErr_SetObject(PyExc_TypeError, Py_BuildValue("sO", - "expected a tuple", tup)); + LDAPerror_TypeError("Tuple_to_LDAPMod(): expected a tuple", tup); return NULL; } if (no_op) { - if (!PyArg_ParseTuple( tup, "sO", &type, &list )) - return NULL; + if (!PyArg_ParseTuple(tup, "sO:Tuple_to_LDAPMod", &type, &list)) + return NULL; op = 0; - } else { - if (!PyArg_ParseTuple( tup, "isO", &op, &type, &list )) - return NULL; + } + else { + if (!PyArg_ParseTuple(tup, "isO:Tuple_to_LDAPMod", &op, &type, &list)) + return NULL; } lm = PyMem_NEW(LDAPMod, 1); + if (lm == NULL) goto nomem; @@ -136,45 +129,52 @@ Tuple_to_LDAPMod( PyObject* tup, int no_op ) len = strlen(type); lm->mod_type = PyMem_NEW(char, len + 1); + if (lm->mod_type == NULL) goto nomem; memcpy(lm->mod_type, type, len + 1); if (list == Py_None) { /* None indicates a NULL mod_bvals */ - } else if (PyBytes_Check(list)) { + } + else if (PyBytes_Check(list)) { /* Single string is a singleton list */ lm->mod_bvalues = PyMem_NEW(struct berval *, 2); + if (lm->mod_bvalues == NULL) goto nomem; lm->mod_bvalues[0] = PyMem_NEW(struct berval, 1); + if (lm->mod_bvalues[0] == NULL) goto nomem; lm->mod_bvalues[1] = NULL; lm->mod_bvalues[0]->bv_len = PyBytes_Size(list); lm->mod_bvalues[0]->bv_val = PyBytes_AsString(list); - } else if (PySequence_Check(list)) { + } + else if (PySequence_Check(list)) { nstrs = PySequence_Length(list); lm->mod_bvalues = PyMem_NEW(struct berval *, nstrs + 1); + if (lm->mod_bvalues == NULL) goto nomem; for (i = 0; i < nstrs; i++) { - lm->mod_bvalues[i] = PyMem_NEW(struct berval, 1); - if (lm->mod_bvalues[i] == NULL) - goto nomem; - lm->mod_bvalues[i+1] = NULL; - item = PySequence_GetItem(list, i); - if (item == NULL) - goto error; - if (!PyBytes_Check(item)) { - PyErr_SetObject( PyExc_TypeError, Py_BuildValue( "sO", - "expected a byte string in the list", item)); - Py_DECREF(item); - goto error; - } - lm->mod_bvalues[i]->bv_len = PyBytes_Size(item); - lm->mod_bvalues[i]->bv_val = PyBytes_AsString(item); - Py_DECREF(item); + lm->mod_bvalues[i] = PyMem_NEW(struct berval, 1); + + if (lm->mod_bvalues[i] == NULL) + goto nomem; + lm->mod_bvalues[i + 1] = NULL; + item = PySequence_GetItem(list, i); + if (item == NULL) + goto error; + if (!PyBytes_Check(item)) { + LDAPerror_TypeError + ("Tuple_to_LDAPMod(): expected a byte string in the list", + item); + goto error; + } + lm->mod_bvalues[i]->bv_len = PyBytes_Size(item); + lm->mod_bvalues[i]->bv_val = PyBytes_AsString(item); + Py_DECREF(item); } if (nstrs == 0) lm->mod_bvalues[0] = NULL; @@ -182,10 +182,10 @@ Tuple_to_LDAPMod( PyObject* tup, int no_op ) return lm; -nomem: + nomem: PyErr_NoMemory(); -error: - if (lm) + error: + if (lm) LDAPMod_DEL(lm); return NULL; @@ -194,47 +194,51 @@ Tuple_to_LDAPMod( PyObject* tup, int no_op ) /* free the structure allocated in List_to_LDAPMods() */ static void -LDAPMods_DEL( LDAPMod** lms ) { - LDAPMod** lmp; - for ( lmp = lms; *lmp; lmp++ ) - LDAPMod_DEL( *lmp ); +LDAPMods_DEL(LDAPMod **lms) +{ + LDAPMod **lmp; + + for (lmp = lms; *lmp; lmp++) + LDAPMod_DEL(*lmp); PyMem_DEL(lms); } -/* - * convert a list of tuples into a LDAPMod*[] array structure +/* + * convert a list of tuples into a LDAPMod*[] array structure * NOTE: list of tuples must live longer than the LDAPMods */ -static LDAPMod** -List_to_LDAPMods( PyObject *list, int no_op ) { +static LDAPMod ** +List_to_LDAPMods(PyObject *list, int no_op) +{ Py_ssize_t i, len; - LDAPMod** lms; + LDAPMod **lms; PyObject *item; if (!PySequence_Check(list)) { - PyErr_SetObject( PyExc_TypeError, Py_BuildValue("sO", - "expected list of tuples", list )); + LDAPerror_TypeError("List_to_LDAPMods(): expected list of tuples", + list); return NULL; } len = PySequence_Length(list); if (len < 0) { - PyErr_SetObject( PyExc_TypeError, Py_BuildValue("sO", - "expected list of tuples", list )); + LDAPerror_TypeError("List_to_LDAPMods(): expected list of tuples", + list); return NULL; } lms = PyMem_NEW(LDAPMod *, len + 1); - if (lms == NULL) + + if (lms == NULL) goto nomem; for (i = 0; i < len; i++) { lms[i] = NULL; item = PySequence_GetItem(list, i); - if (item == NULL) + if (item == NULL) goto error; lms[i] = Tuple_to_LDAPMod(item, no_op); Py_DECREF(item); @@ -244,9 +248,9 @@ List_to_LDAPMods( PyObject *list, int no_op ) { lms[len] = NULL; return lms; -nomem: + nomem: PyErr_NoMemory(); -error: + error: if (lms) LDAPMods_DEL(lms); return NULL; @@ -258,82 +262,94 @@ List_to_LDAPMods( PyObject *list, int no_op ) { */ int -attrs_from_List( PyObject *attrlist, char***attrsp, PyObject** seq) { +attrs_from_List(PyObject *attrlist, char ***attrsp) +{ char **attrs = NULL; - Py_ssize_t i, len; - PyObject *item; - - *seq = NULL; + PyObject *seq = NULL; 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 + } + else if (PyUnicode_Check(attrlist)) { /* caught by John Benninghoff */ - PyErr_SetObject( PyExc_TypeError, Py_BuildValue("sO", - "expected *list* of strings, not a string", attrlist )); + LDAPerror_TypeError + ("attrs_from_List(): expected *list* of strings, not a string", + attrlist); goto error; - } else { - *seq = PySequence_Fast(attrlist, "expected list of strings or None"); - if (*seq == NULL) + } + else { + PyObject *item = NULL; + Py_ssize_t i, len, strlen; + + const char *str; + + 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); + if (attrs == NULL) - goto nomem; + goto nomem; for (i = 0; i < len; i++) { attrs[i] = NULL; - item = PySequence_Fast_GET_ITEM(*seq, i); + item = PySequence_Fast_GET_ITEM(seq, i); if (item == NULL) goto error; -#if PY_MAJOR_VERSION == 2 - /* Encoded by Python to UTF-8 */ - if (!PyBytes_Check(item)) { -#else if (!PyUnicode_Check(item)) { -#endif - PyErr_SetObject(PyExc_TypeError, Py_BuildValue("sO", - "expected string in list", item)); + LDAPerror_TypeError + ("attrs_from_List(): expected string in list", item); goto error; } -#if PY_MAJOR_VERSION == 2 - attrs[i] = PyBytes_AsString(item); -#else - attrs[i] = PyUnicode_AsUTF8(item); -#endif + str = PyUnicode_AsUTF8AndSize(item, &strlen); + /* 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); + + if (attrs[i] == NULL) + goto nomem; + memcpy(attrs[i], str, strlen + 1); } attrs[len] = NULL; + Py_DECREF(seq); } *attrsp = attrs; return 1; -nomem: + nomem: PyErr_NoMemory(); -error: - free_attrs(&attrs, *seq); + error: + Py_XDECREF(seq); + free_attrs(&attrs); return 0; } /* free memory allocated from above routine */ static void -free_attrs( char*** attrsp, PyObject* seq ) { +free_attrs(char ***attrsp) +{ char **attrs = *attrsp; + char **p; - if (attrs != NULL) { - PyMem_DEL(attrs); - *attrsp = NULL; - } + if (attrs == NULL) + return; - Py_XDECREF(seq); + *attrsp = NULL; + for (p = attrs; *p != NULL; p++) { + PyMem_DEL(*p); + } + PyMem_DEL(attrs); } /*------------------------------------------------------------ @@ -342,18 +358,20 @@ free_attrs( char*** attrsp, PyObject* seq ) { /* ldap_unbind_ext */ -static PyObject* -l_ldap_unbind_ext( LDAPObject* self, PyObject* args ) +static PyObject * +l_ldap_unbind_ext(LDAPObject *self, PyObject *args) { PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; int ldaperror; - if (!PyArg_ParseTuple( args, "|OO", &serverctrls, &clientctrls)) return NULL; - if (not_valid(self)) return NULL; + if (!PyArg_ParseTuple(args, "|OO:unbind_ext", &serverctrls, &clientctrls)) + return NULL; + if (not_valid(self)) + return NULL; if (!PyNone_Check(serverctrls)) { if (!LDAPControls_from_object(serverctrls, &server_ldcs)) @@ -361,19 +379,21 @@ l_ldap_unbind_ext( LDAPObject* self, PyObject* args ) } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_unbind_ext( self->ldap, server_ldcs, client_ldcs ); - LDAP_END_ALLOW_THREADS( self ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = ldap_unbind_ext(self->ldap, server_ldcs, client_ldcs); + LDAP_END_ALLOW_THREADS(self); - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_unbind_ext" ); + if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); self->valid = 0; Py_INCREF(Py_None); @@ -382,19 +402,22 @@ l_ldap_unbind_ext( LDAPObject* self, PyObject* args ) /* ldap_abandon_ext */ -static PyObject* -l_ldap_abandon_ext( LDAPObject* self, PyObject* args ) +static PyObject * +l_ldap_abandon_ext(LDAPObject *self, PyObject *args) { int msgid; PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; int ldaperror; - if (!PyArg_ParseTuple( args, "i|OO", &msgid, &serverctrls, &clientctrls)) return NULL; - if (not_valid(self)) return NULL; + if (!PyArg_ParseTuple + (args, "i|OO:abandon_ext", &msgid, &serverctrls, &clientctrls)) + return NULL; + if (not_valid(self)) + return NULL; if (!PyNone_Check(serverctrls)) { if (!LDAPControls_from_object(serverctrls, &server_ldcs)) @@ -402,19 +425,21 @@ l_ldap_abandon_ext( LDAPObject* self, PyObject* args ) } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_abandon_ext( self->ldap, msgid, server_ldcs, client_ldcs ); - LDAP_END_ALLOW_THREADS( self ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = ldap_abandon_ext(self->ldap, msgid, server_ldcs, client_ldcs); + LDAP_END_ALLOW_THREADS(self); - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_abandon_ext" ); + if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); Py_INCREF(Py_None); return Py_None; @@ -423,53 +448,62 @@ l_ldap_abandon_ext( LDAPObject* self, PyObject* args ) /* ldap_add_ext */ static PyObject * -l_ldap_add_ext( LDAPObject* self, PyObject *args ) +l_ldap_add_ext(LDAPObject *self, PyObject *args) { char *dn; PyObject *modlist; PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; int msgid; int ldaperror; LDAPMod **mods; - if (!PyArg_ParseTuple( args, "sO|OO", &dn, &modlist, &serverctrls, &clientctrls )) return NULL; - if (not_valid(self)) return NULL; + if (!PyArg_ParseTuple + (args, "sO|OO:add_ext", &dn, &modlist, &serverctrls, &clientctrls)) + return NULL; + if (not_valid(self)) + return NULL; - mods = List_to_LDAPMods( modlist, 1 ); + mods = List_to_LDAPMods(modlist, 1); if (mods == NULL) return NULL; if (!PyNone_Check(serverctrls)) { - if (!LDAPControls_from_object(serverctrls, &server_ldcs)) + if (!LDAPControls_from_object(serverctrls, &server_ldcs)) { + LDAPMods_DEL(mods); return NULL; + } } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPMods_DEL(mods); + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_add_ext( self->ldap, dn, mods, server_ldcs, client_ldcs, &msgid); - LDAP_END_ALLOW_THREADS( self ); - LDAPMods_DEL( mods ); - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = + ldap_add_ext(self->ldap, dn, mods, server_ldcs, client_ldcs, &msgid); + LDAP_END_ALLOW_THREADS(self); + LDAPMods_DEL(mods); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_add_ext" ); + if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } /* ldap_simple_bind */ -static PyObject* -l_ldap_simple_bind( LDAPObject* self, PyObject* args ) +static PyObject * +l_ldap_simple_bind(LDAPObject *self, PyObject *args) { char *who; int msgid; @@ -477,14 +511,18 @@ l_ldap_simple_bind( LDAPObject* self, PyObject* args ) Py_ssize_t cred_len; PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; struct berval cred; - if (!PyArg_ParseTuple( args, "ss#|OO", &who, &cred.bv_val, &cred_len, &serverctrls, &clientctrls )) return NULL; + if (!PyArg_ParseTuple + (args, "zz#|OO:simple_bind", &who, &cred.bv_val, &cred_len, + &serverctrls, &clientctrls)) + return NULL; cred.bv_len = (ber_len_t) cred_len; - if (not_valid(self)) return NULL; + if (not_valid(self)) + return NULL; if (!PyNone_Check(serverctrls)) { if (!LDAPControls_from_object(serverctrls, &server_ldcs)) @@ -492,24 +530,27 @@ l_ldap_simple_bind( LDAPObject* self, PyObject* args ) } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_sasl_bind( self->ldap, who, LDAP_SASL_SIMPLE, &cred, server_ldcs, client_ldcs, &msgid); - LDAP_END_ALLOW_THREADS( self ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = + ldap_sasl_bind(self->ldap, who, LDAP_SASL_SIMPLE, &cred, server_ldcs, + client_ldcs, &msgid); + LDAP_END_ALLOW_THREADS(self); - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_simple_bind" ); + if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); - return PyInt_FromLong( msgid ); + return PyLong_FromLong(msgid); } - #ifdef HAVE_SASL /* The following functions implement SASL binds. A new method sasl_interactive_bind_s(bind_dn, sasl_mechanism) has been introduced. @@ -526,7 +567,7 @@ l_ldap_simple_bind( LDAPObject* self, PyObject* args ) auth modules ("mechanisms"), or try ldapsearch -b "" -s base -LLL -x supportedSASLMechanisms - + (perhaps with an additional -h and -p argument for ldap host and port). The latter will show you which SASL mechanisms are known to the LDAP server. If you do not want to set up Kerberos, you @@ -546,48 +587,43 @@ l_ldap_simple_bind( LDAPObject* self, PyObject* args ) argument specifies, which information should be passed back to the SASL lib (see SASL_CB_xxx in sasl.h) */ -static int interaction ( unsigned flags, - sasl_interact_t *interact, - PyObject* SASLObject ) +static int +interaction(unsigned flags, sasl_interact_t *interact, PyObject *SASLObject) { /* const char *dflt = interact->defresult; */ - PyObject *result; - char *c_result; - result = PyObject_CallMethod(SASLObject, - "callback", - "isss", - interact->id, /* see sasl.h */ - interact->challenge, - interact->prompt, - interact->defresult); - - if (result == NULL) - /*searching for a better error code */ - return LDAP_OPERATIONS_ERROR; - c_result = PyBytes_AsString(result); /*xxx Error checking?? */ - - /* according to the sasl docs, we should malloc() the returned - string only for calls where interact->id == SASL_CB_PASS, so we - probably leak a few bytes per ldap bind. However, if I restrict - the strdup() to this case, I get segfaults. Should probably be - fixed sometimes. - */ - interact->result = strdup( c_result ); - if (interact->result == NULL) - return LDAP_OPERATIONS_ERROR; - interact->len = strlen(c_result); - /* We _should_ overwrite the python string buffer for security - reasons, however we may not (api/stringObjects.html). Any ideas? - */ - - Py_DECREF(result); /*not needed any longer */ - result = NULL; - - return LDAP_SUCCESS; -} + PyObject *result; + char *c_result; + + result = PyObject_CallMethod(SASLObject, "callback", "isss", interact->id, /* see sasl.h */ + interact->challenge, + interact->prompt, interact->defresult); + + if (result == NULL) + /*searching for a better error code */ + return LDAP_OPERATIONS_ERROR; + c_result = PyBytes_AsString(result); /*xxx Error checking?? */ + + /* according to the sasl docs, we should malloc() the returned + string only for calls where interact->id == SASL_CB_PASS, so we + probably leak a few bytes per ldap bind. However, if I restrict + the strdup() to this case, I get segfaults. Should probably be + fixed sometimes. + */ + interact->result = strdup(c_result); + if (interact->result == NULL) + return LDAP_OPERATIONS_ERROR; + interact->len = strlen(c_result); + /* We _should_ overwrite the python string buffer for security + reasons, however we may not (api/stringObjects.html). Any ideas? + */ + + Py_DECREF(result); /*not needed any longer */ + result = NULL; + return LDAP_SUCCESS; +} -/* +/* This function will be called by ldap_sasl_interactive_bind(). The "*in" is an array of sasl_interact_t's (see sasl.h for a reference). The last interact in the array has an interact->id of @@ -595,26 +631,27 @@ static int interaction ( unsigned flags, */ -int py_ldap_sasl_interaction( LDAP *ld, - unsigned flags, - void *defaults, - void *in ) +int +py_ldap_sasl_interaction(LDAP *ld, unsigned flags, void *defaults, void *in) { - /* These are just typecasts */ - sasl_interact_t *interact = (sasl_interact_t *) in; - PyObject *SASLObject = (PyObject *) defaults; - /* Loop over the array of sasl_interact_t structs */ - while( interact->id != SASL_CB_LIST_END ) { - int rc = 0; - rc = interaction( flags, interact, SASLObject ); - if( rc ) return rc; - interact++; - } - return LDAP_SUCCESS; + /* These are just typecasts */ + sasl_interact_t *interact = (sasl_interact_t *)in; + PyObject *SASLObject = (PyObject *)defaults; + + /* Loop over the array of sasl_interact_t structs */ + while (interact->id != SASL_CB_LIST_END) { + int rc = 0; + + rc = interaction(flags, interact, SASLObject); + if (rc) + return rc; + interact++; + } + return LDAP_SUCCESS; } -static PyObject* -l_ldap_sasl_bind_s( LDAPObject* self, PyObject* args ) +static PyObject * +l_ldap_sasl_bind_s(LDAPObject *self, PyObject *args) { const char *dn; const char *mechanism; @@ -623,16 +660,19 @@ l_ldap_sasl_bind_s( LDAPObject* self, PyObject* args ) PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; struct berval *servercred; int ldaperror; - if (!PyArg_ParseTuple(args, "zzz#OO", &dn, &mechanism, &cred.bv_val, &cred_len, &serverctrls, &clientctrls )) + if (!PyArg_ParseTuple + (args, "zzz#OO:sasl_bind_s", &dn, &mechanism, &cred.bv_val, &cred_len, + &serverctrls, &clientctrls)) return NULL; - if (not_valid(self)) return NULL; + if (not_valid(self)) + return NULL; cred.bv_len = cred_len; @@ -641,63 +681,65 @@ l_ldap_sasl_bind_s( LDAPObject* self, PyObject* args ) return NULL; } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); + LDAP_BEGIN_ALLOW_THREADS(self); ldaperror = ldap_sasl_bind_s(self->ldap, dn, mechanism, cred.bv_val ? &cred : NULL, - (LDAPControl**) server_ldcs, - (LDAPControl**) client_ldcs, - &servercred); - LDAP_END_ALLOW_THREADS( self ); + (LDAPControl **)server_ldcs, + (LDAPControl **)client_ldcs, &servercred); + LDAP_END_ALLOW_THREADS(self); - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); if (ldaperror == LDAP_SASL_BIND_IN_PROGRESS) { if (servercred && servercred->bv_val && *servercred->bv_val) - return PyBytes_FromStringAndSize( servercred->bv_val, servercred->bv_len ); - } else if (ldaperror != LDAP_SUCCESS) - return LDAPerror( self->ldap, "l_ldap_sasl_bind_s" ); - return PyInt_FromLong( ldaperror ); + return PyBytes_FromStringAndSize(servercred->bv_val, + servercred->bv_len); + } + else if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); + return PyLong_FromLong(ldaperror); } -static PyObject* -l_ldap_sasl_interactive_bind_s( LDAPObject* self, PyObject* args ) +static PyObject * +l_ldap_sasl_interactive_bind_s(LDAPObject *self, PyObject *args) { char *c_mechanism; char *who; PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; - PyObject *SASLObject = NULL; + PyObject *SASLObject = NULL; PyObject *mechanism = NULL; int msgid; static unsigned sasl_flags = LDAP_SASL_QUIET; - /* + /* * In Python 2.3+, a "I" format argument indicates that we're either converting * the Python object into a long or an unsigned int. In versions prior to that, * it will always convert to a long. Since the sasl_flags variable is an * unsigned int, we need to use the "I" flag if we're running Python 2.3+ and a - * "i" otherwise. + * "i" otherwise. */ -#if (PY_MAJOR_VERSION == 2) && (PY_MINOR_VERSION < 3) - if (!PyArg_ParseTuple(args, "sOOOi", &who, &SASLObject, &serverctrls, &clientctrls, &sasl_flags )) -#else - if (!PyArg_ParseTuple(args, "sOOOI", &who, &SASLObject, &serverctrls, &clientctrls, &sasl_flags )) -#endif - return NULL; + if (!PyArg_ParseTuple + (args, "sOOOI:sasl_interactive_bind_s", &who, &SASLObject, + &serverctrls, &clientctrls, &sasl_flags)) + return NULL; - if (not_valid(self)) return NULL; + if (not_valid(self)) + return NULL; if (!PyNone_Check(serverctrls)) { if (!LDAPControls_from_object(serverctrls, &server_ldcs)) @@ -705,13 +747,16 @@ l_ldap_sasl_interactive_bind_s( LDAPObject* self, PyObject* args ) } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPControl_List_DEL(server_ldcs); return NULL; + } } /* now we extract the sasl mechanism from the SASL Object */ mechanism = PyObject_GetAttrString(SASLObject, "mech"); - if (mechanism == NULL) return NULL; + if (mechanism == NULL) + return NULL; c_mechanism = PyBytes_AsString(mechanism); Py_DECREF(mechanism); mechanism = NULL; @@ -721,43 +766,44 @@ l_ldap_sasl_interactive_bind_s( LDAPObject* self, PyObject* args ) Python object SASLObject, but passing it through some static variable would destroy thread safety, IMHO. */ - msgid = ldap_sasl_interactive_bind_s(self->ldap, - who, - c_mechanism, - (LDAPControl**) server_ldcs, - (LDAPControl**) client_ldcs, - sasl_flags, - py_ldap_sasl_interaction, - SASLObject); - - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + msgid = ldap_sasl_interactive_bind_s(self->ldap, + who, + c_mechanism, + (LDAPControl **)server_ldcs, + (LDAPControl **)client_ldcs, + sasl_flags, + py_ldap_sasl_interaction, SASLObject); + + LDAPControl_List_DEL(server_ldcs); + 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 - #ifdef LDAP_API_FEATURE_CANCEL /* ldap_cancel */ -static PyObject* -l_ldap_cancel( LDAPObject* self, PyObject* args ) +static PyObject * +l_ldap_cancel(LDAPObject *self, PyObject *args) { int msgid; int cancelid; PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; int ldaperror; - if (!PyArg_ParseTuple( args, "i|OO", &cancelid, &serverctrls, &clientctrls)) return NULL; - if (not_valid(self)) return NULL; + if (!PyArg_ParseTuple + (args, "i|OO:cancel", &cancelid, &serverctrls, &clientctrls)) + return NULL; + if (not_valid(self)) + return NULL; if (!PyNone_Check(serverctrls)) { if (!LDAPControls_from_object(serverctrls, &server_ldcs)) @@ -765,21 +811,24 @@ l_ldap_cancel( LDAPObject* self, PyObject* args ) } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_cancel( self->ldap, cancelid, server_ldcs, client_ldcs, &msgid ); - LDAP_END_ALLOW_THREADS( self ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = + ldap_cancel(self->ldap, cancelid, server_ldcs, client_ldcs, &msgid); + LDAP_END_ALLOW_THREADS(self); - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_cancel" ); + if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); - return PyInt_FromLong( msgid ); + return PyLong_FromLong(msgid); } #endif @@ -787,23 +836,27 @@ l_ldap_cancel( LDAPObject* self, PyObject* args ) /* ldap_compare_ext */ static PyObject * -l_ldap_compare_ext( LDAPObject* self, PyObject *args ) +l_ldap_compare_ext(LDAPObject *self, PyObject *args) { char *dn, *attr; PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; int msgid; int ldaperror; Py_ssize_t value_len; struct berval value; - if (!PyArg_ParseTuple( args, "sss#|OO", &dn, &attr, &value.bv_val, &value_len, &serverctrls, &clientctrls )) return NULL; + if (!PyArg_ParseTuple + (args, "sss#|OO:compare_ext", &dn, &attr, &value.bv_val, &value_len, + &serverctrls, &clientctrls)) + return NULL; value.bv_len = (ber_len_t) value_len; - if (not_valid(self)) return NULL; + if (not_valid(self)) + return NULL; if (!PyNone_Check(serverctrls)) { if (!LDAPControls_from_object(serverctrls, &server_ldcs)) @@ -811,40 +864,46 @@ l_ldap_compare_ext( LDAPObject* self, PyObject *args ) } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_compare_ext( self->ldap, dn, attr, &value, server_ldcs, client_ldcs, &msgid ); - LDAP_END_ALLOW_THREADS( self ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = + ldap_compare_ext(self->ldap, dn, attr, &value, server_ldcs, + client_ldcs, &msgid); + LDAP_END_ALLOW_THREADS(self); - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_compare_ext" ); + if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); - return PyInt_FromLong( msgid ); + return PyLong_FromLong(msgid); } - /* ldap_delete_ext */ static PyObject * -l_ldap_delete_ext( LDAPObject* self, PyObject *args ) +l_ldap_delete_ext(LDAPObject *self, PyObject *args) { char *dn; PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; int msgid; int ldaperror; - if (!PyArg_ParseTuple( args, "s|OO", &dn, &serverctrls, &clientctrls )) return NULL; - if (not_valid(self)) return NULL; + if (!PyArg_ParseTuple + (args, "s|OO:delete_ext", &dn, &serverctrls, &clientctrls)) + return NULL; + if (not_valid(self)) + return NULL; if (!PyNone_Check(serverctrls)) { if (!LDAPControls_from_object(serverctrls, &server_ldcs)) @@ -852,91 +911,105 @@ l_ldap_delete_ext( LDAPObject* self, PyObject *args ) } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_delete_ext( self->ldap, dn, server_ldcs, client_ldcs, &msgid ); - LDAP_END_ALLOW_THREADS( self ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = + ldap_delete_ext(self->ldap, dn, server_ldcs, client_ldcs, &msgid); + LDAP_END_ALLOW_THREADS(self); - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_delete_ext" ); + if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); - return PyInt_FromLong(msgid); + return PyLong_FromLong(msgid); } - /* ldap_modify_ext */ static PyObject * -l_ldap_modify_ext( LDAPObject* self, PyObject *args ) +l_ldap_modify_ext(LDAPObject *self, PyObject *args) { char *dn; PyObject *modlist; PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; int msgid; int ldaperror; LDAPMod **mods; - if (!PyArg_ParseTuple( args, "sO|OO", &dn, &modlist, &serverctrls, &clientctrls )) return NULL; - if (not_valid(self)) return NULL; + if (!PyArg_ParseTuple + (args, "sO|OO:modify_ext", &dn, &modlist, &serverctrls, &clientctrls)) + return NULL; + if (not_valid(self)) + return NULL; - mods = List_to_LDAPMods( modlist, 0 ); + mods = List_to_LDAPMods(modlist, 0); if (mods == NULL) return NULL; if (!PyNone_Check(serverctrls)) { - if (!LDAPControls_from_object(serverctrls, &server_ldcs)) + if (!LDAPControls_from_object(serverctrls, &server_ldcs)) { + LDAPMods_DEL(mods); return NULL; + } } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPMods_DEL(mods); + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_modify_ext( self->ldap, dn, mods, server_ldcs, client_ldcs, &msgid ); - LDAP_END_ALLOW_THREADS( self ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = + ldap_modify_ext(self->ldap, dn, mods, server_ldcs, client_ldcs, + &msgid); + LDAP_END_ALLOW_THREADS(self); - LDAPMods_DEL( mods ); - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAPMods_DEL(mods); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_modify_ext" ); + if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); - return PyInt_FromLong( msgid ); + return PyLong_FromLong(msgid); } - /* ldap_rename */ static PyObject * -l_ldap_rename( LDAPObject* self, PyObject *args ) +l_ldap_rename(LDAPObject *self, PyObject *args) { char *dn, *newrdn; char *newSuperior = NULL; int delold = 1; PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; int msgid; int ldaperror; - if (!PyArg_ParseTuple( args, "ss|ziOO", &dn, &newrdn, &newSuperior, &delold, &serverctrls, &clientctrls )) + if (!PyArg_ParseTuple + (args, "ss|ziOO:rename", &dn, &newrdn, &newSuperior, &delold, + &serverctrls, &clientctrls)) + return NULL; + if (not_valid(self)) return NULL; - if (not_valid(self)) return NULL; if (!PyNone_Check(serverctrls)) { if (!LDAPControls_from_object(serverctrls, &server_ldcs)) @@ -944,28 +1017,31 @@ l_ldap_rename( LDAPObject* self, PyObject *args ) } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_rename( self->ldap, dn, newrdn, newSuperior, delold, server_ldcs, client_ldcs, &msgid ); - LDAP_END_ALLOW_THREADS( self ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = + ldap_rename(self->ldap, dn, newrdn, newSuperior, delold, server_ldcs, + client_ldcs, &msgid); + LDAP_END_ALLOW_THREADS(self); - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_rename" ); + if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); - return PyInt_FromLong( msgid ); + return PyLong_FromLong(msgid); } - /* ldap_result4 */ static PyObject * -l_ldap_result4( LDAPObject* self, PyObject *args ) +l_ldap_result4(LDAPObject *self, PyObject *args) { int msgid = LDAP_RES_ANY; int all = 1; @@ -974,39 +1050,48 @@ l_ldap_result4( LDAPObject* self, PyObject *args ) int add_intermediates = 0; int add_extop = 0; struct timeval tv; - struct timeval* tvp; + 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 = 0; + PyObject *valuestr = NULL; + int result = LDAP_SUCCESS; + LDAPControl **serverctrls = 0; - if (!PyArg_ParseTuple( args, "|iidiii", &msgid, &all, &timeout, &add_ctrls, &add_intermediates, &add_extop )) + if (!PyArg_ParseTuple + (args, "|iidiii:result4", &msgid, &all, &timeout, &add_ctrls, + &add_intermediates, &add_extop)) + return NULL; + if (not_valid(self)) return NULL; - if (not_valid(self)) return NULL; - + if (timeout >= 0) { tvp = &tv; - set_timeval_from_double( tvp, timeout ); - } else { + set_timeval_from_double(tvp, timeout); + } + else { tvp = NULL; } - LDAP_BEGIN_ALLOW_THREADS( self ); - res_type = ldap_result( self->ldap, msgid, all, tvp, &msg ); - LDAP_END_ALLOW_THREADS( self ); + LDAP_BEGIN_ALLOW_THREADS(self); + res_type = ldap_result(self->ldap, msgid, all, tvp, &msg); + 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 */ if (timeout == 0) { if (add_extop) { - return Py_BuildValue("(OOOOOO)", Py_None, Py_None, Py_None, Py_None, Py_None, Py_None); - } else { - return Py_BuildValue("(OOOO)", Py_None, Py_None, Py_None, Py_None); + return Py_BuildValue("(OOOOOO)", Py_None, Py_None, Py_None, + Py_None, Py_None, Py_None); + } + else { + return Py_BuildValue("(OOOO)", Py_None, Py_None, Py_None, + Py_None); } } else @@ -1016,88 +1101,88 @@ l_ldap_result4( LDAPObject* self, PyObject *args ) if (msg) res_msgid = ldap_msgid(msg); - int result = LDAP_SUCCESS; - char **refs = NULL; - LDAPControl **serverctrls = 0; if (res_type == LDAP_RES_SEARCH_ENTRY) { /* LDAPmessage_to_python will parse entries and read the controls for each entry */ - } else if (res_type == LDAP_RES_SEARCH_REFERENCE) { + } + else if (res_type == LDAP_RES_SEARCH_REFERENCE) { /* LDAPmessage_to_python will parse refs and read the controls for each res */ - } else if (res_type == LDAP_RES_INTERMEDIATE) { + } + else if (res_type == LDAP_RES_INTERMEDIATE) { /* LDAPmessage_to_python will parse intermediates and controls */ - } else { + } + else { int rc; + if (res_type == LDAP_RES_EXTENDED) { struct berval *retdata = 0; - LDAP_BEGIN_ALLOW_THREADS( self ); - rc = ldap_parse_extended_result( self->ldap, msg, &retoid, &retdata, 0 ); - LDAP_END_ALLOW_THREADS( self ); + + LDAP_BEGIN_ALLOW_THREADS(self); + rc = ldap_parse_extended_result(self->ldap, msg, &retoid, &retdata, + 0); + LDAP_END_ALLOW_THREADS(self); /* handle error rc!=0 here? */ if (rc == LDAP_SUCCESS) { valuestr = LDAPberval_to_object(retdata); } - ber_bvfree( retdata ); + ber_bvfree(retdata); } - - LDAP_BEGIN_ALLOW_THREADS( self ); - rc = ldap_parse_result( self->ldap, msg, &result, NULL, NULL, &refs, - &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); - return LDAPerror( self->ldap, e ); + + LDAP_BEGIN_ALLOW_THREADS(self); + rc = ldap_parse_result(self->ldap, msg, &result, NULL, NULL, NULL, + &serverctrls, 0); + LDAP_END_ALLOW_THREADS(self); + } + + if (result != LDAP_SUCCESS) { /* result error */ + ldap_controls_free(serverctrls); + Py_XDECREF(valuestr); + return LDAPraise_for_message(self->ldap, msg); } if (!(pyctrls = LDAPControls_to_List(serverctrls))) { int err = LDAP_NO_MEMORY; - LDAP_BEGIN_ALLOW_THREADS( self ); + + LDAP_BEGIN_ALLOW_THREADS(self); ldap_set_option(self->ldap, LDAP_OPT_ERROR_NUMBER, &err); - LDAP_END_ALLOW_THREADS( self ); + LDAP_END_ALLOW_THREADS(self); + ldap_controls_free(serverctrls); ldap_msgfree(msg); - return LDAPerror(self->ldap, "LDAPControls_to_List"); + Py_XDECREF(valuestr); + return LDAPerror(self->ldap); } ldap_controls_free(serverctrls); - pmsg = LDAPmessage_to_python( self->ldap, msg, add_ctrls, add_intermediates ); - - result_str = LDAPconstant( res_type ); + pmsg = + LDAPmessage_to_python(self->ldap, msg, add_ctrls, add_intermediates); if (pmsg == NULL) { - retval = NULL; - } else { + retval = NULL; + } + else { /* s handles NULL, but O does not */ if (add_extop) { - retval = Py_BuildValue("(OOiOsO)", result_str, pmsg, res_msgid, - pyctrls, retoid, valuestr ? valuestr : Py_None); - } else { - retval = Py_BuildValue("(OOiO)", result_str, pmsg, res_msgid, pyctrls); + retval = Py_BuildValue("(iOiOsO)", res_type, pmsg, res_msgid, + pyctrls, retoid, + valuestr ? valuestr : Py_None); + } + else { + retval = + Py_BuildValue("(iOiO)", res_type, pmsg, res_msgid, pyctrls); } if (pmsg != Py_None) { - Py_DECREF(pmsg); + Py_DECREF(pmsg); } } - if (valuestr) { - Py_DECREF(valuestr); - } + Py_XDECREF(valuestr); Py_XDECREF(pyctrls); - Py_DECREF(result_str); return retval; } - /* ldap_search_ext */ -static PyObject* -l_ldap_search_ext( LDAPObject* self, PyObject* args ) +static PyObject * +l_ldap_search_ext(LDAPObject *self, PyObject *args) { char *base; int scope; @@ -1108,69 +1193,76 @@ l_ldap_search_ext( LDAPObject* self, PyObject* args ) PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - PyObject *attrs_seq = NULL; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; double timeout = -1.0; struct timeval tv; - struct timeval* tvp; + struct timeval *tvp; int sizelimit = 0; int msgid; int ldaperror; - if (!PyArg_ParseTuple( args, "sis|OiOOdi", - &base, &scope, &filter, &attrlist, &attrsonly, - &serverctrls, &clientctrls, &timeout, &sizelimit )) return NULL; - if (not_valid(self)) return NULL; + if (!PyArg_ParseTuple(args, "sis|OiOOdi:search_ext", + &base, &scope, &filter, &attrlist, &attrsonly, + &serverctrls, &clientctrls, &timeout, &sizelimit)) + return NULL; + if (not_valid(self)) + return NULL; - if (!attrs_from_List( attrlist, &attrs, &attrs_seq )) - return NULL; + if (!attrs_from_List(attrlist, &attrs)) + return NULL; if (timeout >= 0) { tvp = &tv; - set_timeval_from_double( tvp, timeout ); - } else { + set_timeval_from_double(tvp, timeout); + } + else { tvp = NULL; } if (!PyNone_Check(serverctrls)) { - if (!LDAPControls_from_object(serverctrls, &server_ldcs)) + if (!LDAPControls_from_object(serverctrls, &server_ldcs)) { + free_attrs(&attrs); return NULL; + } } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + free_attrs(&attrs); + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_search_ext( self->ldap, base, scope, filter, attrs, attrsonly, - server_ldcs, client_ldcs, tvp, sizelimit, &msgid ); - LDAP_END_ALLOW_THREADS( self ); - - free_attrs( &attrs, attrs_seq); - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = + ldap_search_ext(self->ldap, base, scope, filter, attrs, attrsonly, + server_ldcs, client_ldcs, tvp, sizelimit, &msgid); + LDAP_END_ALLOW_THREADS(self); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_search_ext" ); + free_attrs(&attrs); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); - return PyInt_FromLong( msgid ); -} + if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); + return PyLong_FromLong(msgid); +} /* ldap_whoami_s (available since OpenLDAP 2.1.13) */ -static PyObject* -l_ldap_whoami_s( LDAPObject* self, PyObject* args ) +static PyObject * +l_ldap_whoami_s(LDAPObject *self, PyObject *args) { PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; struct berval *bvalue = NULL; @@ -1178,8 +1270,10 @@ l_ldap_whoami_s( LDAPObject* self, PyObject* args ) int ldaperror; - if (!PyArg_ParseTuple( args, "|OO", &serverctrls, &clientctrls)) return NULL; - if (not_valid(self)) return NULL; + if (!PyArg_ParseTuple(args, "|OO:whoami_s", &serverctrls, &clientctrls)) + return NULL; + if (not_valid(self)) + return NULL; if (!PyNone_Check(serverctrls)) { if (!LDAPControls_from_object(serverctrls, &server_ldcs)) @@ -1187,21 +1281,26 @@ l_ldap_whoami_s( LDAPObject* self, PyObject* args ) } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_whoami_s( self->ldap, &bvalue, server_ldcs, client_ldcs ); - LDAP_END_ALLOW_THREADS( self ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = ldap_whoami_s(self->ldap, &bvalue, server_ldcs, client_ldcs); + LDAP_END_ALLOW_THREADS(self); - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_whoami_s" ); + if (ldaperror != LDAP_SUCCESS) { + ber_bvfree(bvalue); + return LDAPerror(self->ldap); + } result = LDAPberval_to_unicode_object(bvalue); + ber_bvfree(bvalue); return result; } @@ -1209,20 +1308,22 @@ l_ldap_whoami_s( LDAPObject* self, PyObject* args ) #ifdef HAVE_TLS /* ldap_start_tls_s */ -static PyObject* -l_ldap_start_tls_s( LDAPObject* self, PyObject* args ) +static PyObject * +l_ldap_start_tls_s(LDAPObject *self, PyObject *args) { int ldaperror; - if (!PyArg_ParseTuple( args, "" )) return NULL; - if (not_valid(self)) return NULL; + if (!PyArg_ParseTuple(args, ":start_tls_s")) + return NULL; + if (not_valid(self)) + return NULL; - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_start_tls_s( self->ldap, NULL, NULL ); - LDAP_END_ALLOW_THREADS( self ); - if ( ldaperror != LDAP_SUCCESS ){ + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = ldap_start_tls_s(self->ldap, NULL, NULL); + 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); @@ -1233,38 +1334,40 @@ l_ldap_start_tls_s( LDAPObject* self, PyObject* args ) /* ldap_set_option */ -static PyObject* -l_ldap_set_option(PyObject* self, PyObject *args) +static PyObject * +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) == -1) + if (not_valid(self)) + return NULL; + if (!LDAP_set_option(self, option, value)) return NULL; Py_INCREF(Py_None); return Py_None; } - /* ldap_get_option */ -static PyObject* -l_ldap_get_option(PyObject* self, PyObject *args) +static PyObject * +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 */ static PyObject * -l_ldap_passwd( LDAPObject* self, PyObject *args ) +l_ldap_passwd(LDAPObject *self, PyObject *args) { struct berval user; Py_ssize_t user_len; @@ -1274,20 +1377,23 @@ l_ldap_passwd( LDAPObject* self, PyObject *args ) Py_ssize_t newpw_len; PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; int msgid; int ldaperror; - if (!PyArg_ParseTuple( args, "z#z#z#|OO", &user.bv_val, &user_len, &oldpw.bv_val, &oldpw_len, &newpw.bv_val, &newpw_len, &serverctrls, &clientctrls )) + if (!PyArg_ParseTuple + (args, "z#z#z#|OO:passwd", &user.bv_val, &user_len, &oldpw.bv_val, + &oldpw_len, &newpw.bv_val, &newpw_len, &serverctrls, &clientctrls)) return NULL; user.bv_len = (ber_len_t) user_len; oldpw.bv_len = (ber_len_t) oldpw_len; newpw.bv_len = (ber_len_t) newpw_len; - - if (not_valid(self)) return NULL; + + if (not_valid(self)) + return NULL; if (!PyNone_Check(serverctrls)) { if (!LDAPControls_from_object(serverctrls, &server_ldcs)) @@ -1295,49 +1401,51 @@ l_ldap_passwd( LDAPObject* self, PyObject *args ) } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_passwd( self->ldap, - user.bv_val != NULL ? &user : NULL, - oldpw.bv_val != NULL ? &oldpw : NULL, - newpw.bv_val != NULL ? &newpw : NULL, - server_ldcs, - client_ldcs, - &msgid ); - LDAP_END_ALLOW_THREADS( self ); - - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = ldap_passwd(self->ldap, + user.bv_val != NULL ? &user : NULL, + oldpw.bv_val != NULL ? &oldpw : NULL, + newpw.bv_val != NULL ? &newpw : NULL, + server_ldcs, client_ldcs, &msgid); + LDAP_END_ALLOW_THREADS(self); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_passwd" ); + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); - return PyInt_FromLong( msgid ); -} + if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); + return PyLong_FromLong(msgid); +} /* ldap_extended_operation */ static PyObject * -l_ldap_extended_operation( LDAPObject* self, PyObject *args ) +l_ldap_extended_operation(LDAPObject *self, PyObject *args) { char *reqoid = NULL; - struct berval reqvalue = {0, NULL}; + struct berval reqvalue = { 0, NULL }; PyObject *serverctrls = Py_None; PyObject *clientctrls = Py_None; - LDAPControl** server_ldcs = NULL; - LDAPControl** client_ldcs = NULL; + LDAPControl **server_ldcs = NULL; + LDAPControl **client_ldcs = NULL; int msgid; int ldaperror; - if (!PyArg_ParseTuple( args, "sz#|OO", &reqoid, &reqvalue.bv_val, &reqvalue.bv_len, &serverctrls, &clientctrls )) + if (!PyArg_ParseTuple + (args, "sz#|OO:extended_operation", &reqoid, &reqvalue.bv_val, + &reqvalue.bv_len, &serverctrls, &clientctrls)) return NULL; - if (not_valid(self)) return NULL; + if (not_valid(self)) + return NULL; if (!PyNone_Check(serverctrls)) { if (!LDAPControls_from_object(serverctrls, &server_ldcs)) @@ -1345,95 +1453,125 @@ l_ldap_extended_operation( LDAPObject* self, PyObject *args ) } if (!PyNone_Check(clientctrls)) { - if (!LDAPControls_from_object(clientctrls, &client_ldcs)) + if (!LDAPControls_from_object(clientctrls, &client_ldcs)) { + LDAPControl_List_DEL(server_ldcs); return NULL; + } } - LDAP_BEGIN_ALLOW_THREADS( self ); - ldaperror = ldap_extended_operation( self->ldap, reqoid, - reqvalue.bv_val != NULL ? &reqvalue : NULL, - server_ldcs, - client_ldcs, - &msgid ); - LDAP_END_ALLOW_THREADS( self ); - - LDAPControl_List_DEL( server_ldcs ); - LDAPControl_List_DEL( client_ldcs ); + LDAP_BEGIN_ALLOW_THREADS(self); + ldaperror = ldap_extended_operation(self->ldap, reqoid, + reqvalue.bv_val != + NULL ? &reqvalue : NULL, server_ldcs, + client_ldcs, &msgid); + LDAP_END_ALLOW_THREADS(self); + + LDAPControl_List_DEL(server_ldcs); + LDAPControl_List_DEL(client_ldcs); + + if (ldaperror != LDAP_SUCCESS) + return LDAPerror(self->ldap); - if ( ldaperror!=LDAP_SUCCESS ) - return LDAPerror( self->ldap, "ldap_extended_operation" ); + return PyLong_FromLong(msgid); +} + +/* ldap_connect */ + +static PyObject * +l_ldap_connect(LDAPObject *self, PyObject Py_UNUSED(args)) +{ +#if LDAP_VENDOR_VERSION >= 20500 + int ldaperror; - return PyInt_FromLong( msgid ); + 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 */ static PyMethodDef methods[] = { - {"unbind_ext", (PyCFunction)l_ldap_unbind_ext, METH_VARARGS }, - {"abandon_ext", (PyCFunction)l_ldap_abandon_ext, METH_VARARGS }, - {"add_ext", (PyCFunction)l_ldap_add_ext, METH_VARARGS }, - {"simple_bind", (PyCFunction)l_ldap_simple_bind, METH_VARARGS }, + {"unbind_ext", (PyCFunction)l_ldap_unbind_ext, METH_VARARGS}, + {"abandon_ext", (PyCFunction)l_ldap_abandon_ext, METH_VARARGS}, + {"add_ext", (PyCFunction)l_ldap_add_ext, METH_VARARGS}, + {"simple_bind", (PyCFunction)l_ldap_simple_bind, METH_VARARGS}, #ifdef HAVE_SASL - {"sasl_interactive_bind_s", (PyCFunction)l_ldap_sasl_interactive_bind_s, METH_VARARGS }, - {"sasl_bind_s", (PyCFunction)l_ldap_sasl_bind_s, METH_VARARGS }, + {"sasl_interactive_bind_s", (PyCFunction)l_ldap_sasl_interactive_bind_s, + METH_VARARGS}, + {"sasl_bind_s", (PyCFunction)l_ldap_sasl_bind_s, METH_VARARGS}, #endif - {"compare_ext", (PyCFunction)l_ldap_compare_ext, METH_VARARGS }, - {"delete_ext", (PyCFunction)l_ldap_delete_ext, METH_VARARGS }, - {"modify_ext", (PyCFunction)l_ldap_modify_ext, METH_VARARGS }, - {"rename", (PyCFunction)l_ldap_rename, METH_VARARGS }, - {"result4", (PyCFunction)l_ldap_result4, METH_VARARGS }, - {"search_ext", (PyCFunction)l_ldap_search_ext, METH_VARARGS }, + {"compare_ext", (PyCFunction)l_ldap_compare_ext, METH_VARARGS}, + {"delete_ext", (PyCFunction)l_ldap_delete_ext, METH_VARARGS}, + {"modify_ext", (PyCFunction)l_ldap_modify_ext, METH_VARARGS}, + {"rename", (PyCFunction)l_ldap_rename, METH_VARARGS}, + {"result4", (PyCFunction)l_ldap_result4, METH_VARARGS}, + {"search_ext", (PyCFunction)l_ldap_search_ext, METH_VARARGS}, #ifdef HAVE_TLS - {"start_tls_s", (PyCFunction)l_ldap_start_tls_s, METH_VARARGS }, + {"start_tls_s", (PyCFunction)l_ldap_start_tls_s, METH_VARARGS}, #endif - {"whoami_s", (PyCFunction)l_ldap_whoami_s, METH_VARARGS }, - {"passwd", (PyCFunction)l_ldap_passwd, METH_VARARGS }, - {"set_option", (PyCFunction)l_ldap_set_option, METH_VARARGS }, - {"get_option", (PyCFunction)l_ldap_get_option, METH_VARARGS }, + {"whoami_s", (PyCFunction)l_ldap_whoami_s, METH_VARARGS}, + {"passwd", (PyCFunction)l_ldap_passwd, METH_VARARGS}, + {"set_option", (PyCFunction)l_ldap_set_option, METH_VARARGS}, + {"get_option", (PyCFunction)l_ldap_get_option, METH_VARARGS}, #ifdef LDAP_API_FEATURE_CANCEL - {"cancel", (PyCFunction)l_ldap_cancel, METH_VARARGS }, + {"cancel", (PyCFunction)l_ldap_cancel, METH_VARARGS}, #endif - {"extop", (PyCFunction)l_ldap_extended_operation, METH_VARARGS }, - { NULL, NULL } + {"extop", (PyCFunction)l_ldap_extended_operation, METH_VARARGS}, + {"connect", (PyCFunction)l_ldap_connect, METH_NOARGS}, + {NULL, NULL} }; /* type entry */ PyTypeObject LDAP_Type = { -#if defined(MS_WINDOWS) || defined(__CYGWIN__) - /* see http://www.python.org/doc/FAQ.html#3.24 */ - PyVarObject_HEAD_INIT(NULL, 0) -#else /* ! MS_WINDOWS */ - PyVarObject_HEAD_INIT(&PyType_Type, 0) -#endif /* MS_WINDOWS */ - "LDAP", /*tp_name*/ - sizeof(LDAPObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - 0, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0) + "LDAP", /*tp_name */ + sizeof(LDAPObject), /*tp_basicsize */ + 0, /*tp_itemsize */ + /* methods */ + (destructor) dealloc, /*tp_dealloc */ + 0, /*tp_print */ + 0, /*tp_getattr */ + 0, /*tp_setattr */ + 0, /*tp_compare */ + 0, /*tp_repr */ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash */ + 0, /*tp_call */ + 0, /*tp_str */ + 0, /*tp_getattro */ + 0, /*tp_setattro */ + 0, /*tp_as_buffer */ + 0, /*tp_flags */ + 0, /*tp_doc */ + 0, /*tp_traverse */ + 0, /*tp_clear */ + 0, /*tp_richcompare */ + 0, /*tp_weaklistoffset */ + 0, /*tp_iter */ + 0, /*tp_iternext */ + methods, /*tp_methods */ + 0, /*tp_members */ + 0, /*tp_getset */ }; diff --git a/Modules/LDAPObject.h b/Modules/LDAPObject.h deleted file mode 100644 index 8cd6fc3e..00000000 --- a/Modules/LDAPObject.h +++ /dev/null @@ -1,51 +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 9702b8ce..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 7d3ac8b3..4cfee744 100644 --- a/Modules/common.c +++ b/Modules/common.c @@ -1,18 +1,33 @@ /* 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 */ void -LDAPadd_methods( PyObject* d, PyMethodDef* methods ) +LDAPadd_methods(PyObject *d, PyMethodDef *methods) { PyMethodDef *meth; - for( meth = methods; meth->ml_meth; meth++ ) { - PyObject *f = PyCFunction_New( meth, NULL ); - PyDict_SetItemString( d, meth->ml_name, f ); + for (meth = methods; meth->ml_meth; meth++) { + PyObject *f = PyCFunction_New(meth, NULL); + + PyDict_SetItemString(d, meth->ml_name, f); Py_DECREF(f); } } + +/* Raise TypeError with custom message and object */ +PyObject * +LDAPerror_TypeError(const char *msg, PyObject *obj) +{ + PyObject *args = Py_BuildValue("sO", msg, obj); + + if (args == NULL) { + return NULL; + } + PyErr_SetObject(PyExc_TypeError, args); + Py_DECREF(args); + return NULL; +} diff --git a/Modules/common.h b/Modules/common.h deleted file mode 100644 index 0eea1d92..00000000 --- a/Modules/common.h +++ /dev/null @@ -1,37 +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) ) - -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 7ed9e418..64804e8d 100644 --- a/Modules/constants.c +++ b/Modules/constants.c @@ -1,378 +1,265 @@ /* constants defined for LDAP * See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "constants.h" -#include "lber.h" -#include "ldap.h" - -static PyObject* reverse; -static PyObject* forward; - -/* convert an result integer into a Python string */ - -PyObject* -LDAPconstant( int val ) { - PyObject *i = PyInt_FromLong( val ); - PyObject *s = PyObject_GetItem( reverse, i ); - if (s == NULL) { - PyErr_Clear(); - return i; - } - Py_DECREF(i); - return s; -} +#include "pythonldap.h" -/* initialise the module constants */ +/* the base exception class */ -void -LDAPinit_constants( PyObject* d ) -{ - PyObject *zero, *author,*obj; - - reverse = PyDict_New(); - forward = PyDict_New(); - - PyDict_SetItemString( d, "_reverse", reverse ); - PyDict_SetItemString( d, "_forward", forward ); - -#define add_int(d, name) \ - { \ - PyObject *i = PyInt_FromLong(LDAP_##name); \ - PyDict_SetItemString( d, #name, i ); \ - Py_DECREF(i); \ - } - - /* simple constants */ - - add_int(d,API_VERSION); - add_int(d,VENDOR_VERSION); - - add_int(d,PORT); - add_int(d,VERSION1); - add_int(d,VERSION2); - add_int(d,VERSION3); - add_int(d,VERSION_MIN); - add_int(d,VERSION); - add_int(d,VERSION_MAX); - add_int(d,TAG_MESSAGE); - add_int(d,TAG_MSGID); - - add_int(d,REQ_BIND); - add_int(d,REQ_UNBIND); - add_int(d,REQ_SEARCH); - add_int(d,REQ_MODIFY); - add_int(d,REQ_ADD); - add_int(d,REQ_DELETE); - add_int(d,REQ_MODRDN); - add_int(d,REQ_COMPARE); - add_int(d,REQ_ABANDON); - - add_int(d,TAG_LDAPDN); - add_int(d,TAG_LDAPCRED); - add_int(d,TAG_CONTROLS); - add_int(d,TAG_REFERRAL); - - add_int(d,REQ_EXTENDED); -#if LDAP_API_VERSION >= 2004 - add_int(d,TAG_NEWSUPERIOR); - add_int(d,TAG_EXOP_REQ_OID); - add_int(d,TAG_EXOP_REQ_VALUE); - add_int(d,TAG_EXOP_RES_OID); - add_int(d,TAG_EXOP_RES_VALUE); -#ifdef HAVE_SASL - add_int(d,TAG_SASL_RES_CREDS); -#endif -#endif +PyObject *LDAPexception_class; - add_int(d,SASL_AUTOMATIC); - add_int(d,SASL_INTERACTIVE); - add_int(d,SASL_QUIET); - - /* reversibles */ - - zero = PyInt_FromLong( 0 ); - PyDict_SetItem( reverse, zero, Py_None ); - Py_DECREF( zero ); - - add_int(d,RES_BIND); - add_int(d,RES_SEARCH_ENTRY); - add_int(d,RES_SEARCH_RESULT); - add_int(d,RES_MODIFY); - add_int(d,RES_ADD); - add_int(d,RES_DELETE); - add_int(d,RES_MODRDN); - add_int(d,RES_COMPARE); - add_int(d,RES_ANY); - - add_int(d,RES_SEARCH_REFERENCE); - add_int(d,RES_EXTENDED); - add_int(d,RES_UNSOLICITED); - - add_int(d,RES_INTERMEDIATE); - - /* non-reversibles */ - - add_int(d,AUTH_NONE); - add_int(d,AUTH_SIMPLE); - add_int(d,SCOPE_BASE); - add_int(d,SCOPE_ONELEVEL); - add_int(d,SCOPE_SUBTREE); -#ifdef LDAP_SCOPE_SUBORDINATE - add_int(d,SCOPE_SUBORDINATE); -#endif - add_int(d,MOD_ADD); - add_int(d,MOD_DELETE); - add_int(d,MOD_REPLACE); - add_int(d,MOD_INCREMENT); - add_int(d,MOD_BVALUES); - - add_int(d,MSG_ONE); - add_int(d,MSG_ALL); - add_int(d,MSG_RECEIVED); - - /* (errors.c contains the error constants) */ - - add_int(d,DEREF_NEVER); - add_int(d,DEREF_SEARCHING); - add_int(d,DEREF_FINDING); - add_int(d,DEREF_ALWAYS); - add_int(d,NO_LIMIT); - - add_int(d,OPT_API_INFO); - add_int(d,OPT_DEREF); - add_int(d,OPT_SIZELIMIT); - add_int(d,OPT_TIMELIMIT); -#ifdef LDAP_OPT_REFERRALS - add_int(d,OPT_REFERRALS); -#endif - add_int(d,OPT_ERROR_NUMBER); - add_int(d,OPT_RESTART); - add_int(d,OPT_PROTOCOL_VERSION); - add_int(d,OPT_SERVER_CONTROLS); - add_int(d,OPT_CLIENT_CONTROLS); - add_int(d,OPT_API_FEATURE_INFO); - add_int(d,OPT_HOST_NAME); - - add_int(d,OPT_DESC); - add_int(d,OPT_DIAGNOSTIC_MESSAGE); - - add_int(d,OPT_ERROR_STRING); - add_int(d,OPT_MATCHED_DN); - add_int(d,OPT_DEBUG_LEVEL); - add_int(d,OPT_TIMEOUT); - add_int(d,OPT_REFHOPLIMIT); - add_int(d,OPT_NETWORK_TIMEOUT); - add_int(d,OPT_URI); -#ifdef LDAP_OPT_DEFBASE - add_int(d,OPT_DEFBASE); -#endif -#ifdef HAVE_TLS - add_int(d,OPT_X_TLS); -#ifdef LDAP_OPT_X_TLS_NEWCTX - add_int(d,OPT_X_TLS_CTX); -#endif - add_int(d,OPT_X_TLS_CACERTFILE); - add_int(d,OPT_X_TLS_CACERTDIR); - add_int(d,OPT_X_TLS_CERTFILE); - add_int(d,OPT_X_TLS_KEYFILE); - add_int(d,OPT_X_TLS_REQUIRE_CERT); - add_int(d,OPT_X_TLS_CIPHER_SUITE); - add_int(d,OPT_X_TLS_RANDOM_FILE); - add_int(d,OPT_X_TLS_DHFILE); - add_int(d,OPT_X_TLS_NEVER); - add_int(d,OPT_X_TLS_HARD); - add_int(d,OPT_X_TLS_DEMAND); - add_int(d,OPT_X_TLS_ALLOW); - add_int(d,OPT_X_TLS_TRY); -#ifdef LDAP_OPT_X_TLS_PEERCERT - add_int(d,OPT_X_TLS_PEERCERT); -#endif -#ifdef LDAP_OPT_X_TLS_VERSION - add_int(d,OPT_X_TLS_VERSION); -#endif -#ifdef LDAP_OPT_X_TLS_CIPHER - add_int(d,OPT_X_TLS_CIPHER); -#endif -#ifdef LDAP_OPT_X_TLS_PEERCERT - add_int(d,OPT_X_TLS_PEERCERT); -#endif -#ifdef LDAP_OPT_X_TLS_CRLCHECK - /* only available if OpenSSL supports it => might cause backward compability problems */ - add_int(d,OPT_X_TLS_CRLCHECK); -#ifdef LDAP_OPT_X_TLS_CRLFILE - add_int(d,OPT_X_TLS_CRLFILE); -#endif - add_int(d,OPT_X_TLS_CRL_NONE); - add_int(d,OPT_X_TLS_CRL_PEER); - add_int(d,OPT_X_TLS_CRL_ALL); -#endif -#ifdef LDAP_OPT_X_TLS_NEWCTX - add_int(d,OPT_X_TLS_NEWCTX); -#endif -#ifdef LDAP_OPT_X_TLS_PROTOCOL_MIN - add_int(d,OPT_X_TLS_PROTOCOL_MIN); -#endif -#ifdef LDAP_OPT_X_TLS_PACKAGE - add_int(d,OPT_X_TLS_PACKAGE); -#endif -#endif - add_int(d,OPT_X_SASL_MECH); - add_int(d,OPT_X_SASL_REALM); - add_int(d,OPT_X_SASL_AUTHCID); - add_int(d,OPT_X_SASL_AUTHZID); - add_int(d,OPT_X_SASL_SSF); - add_int(d,OPT_X_SASL_SSF_EXTERNAL); - add_int(d,OPT_X_SASL_SECPROPS); - add_int(d,OPT_X_SASL_SSF_MIN); - add_int(d,OPT_X_SASL_SSF_MAX); -#ifdef LDAP_OPT_X_SASL_NOCANON - add_int(d,OPT_X_SASL_NOCANON); -#endif -#ifdef LDAP_OPT_X_SASL_USERNAME - add_int(d,OPT_X_SASL_USERNAME); -#endif -#ifdef LDAP_OPT_CONNECT_ASYNC - add_int(d,OPT_CONNECT_ASYNC); -#endif -#ifdef LDAP_OPT_X_KEEPALIVE_IDLE - add_int(d,OPT_X_KEEPALIVE_IDLE); -#endif -#ifdef LDAP_OPT_X_KEEPALIVE_PROBES - add_int(d,OPT_X_KEEPALIVE_PROBES); -#endif -#ifdef LDAP_OPT_X_KEEPALIVE_INTERVAL - add_int(d,OPT_X_KEEPALIVE_INTERVAL); -#endif +/* list of exception classes */ - add_int(d,DN_FORMAT_LDAP); - add_int(d,DN_FORMAT_LDAPV3); - add_int(d,DN_FORMAT_LDAPV2); - add_int(d,DN_FORMAT_DCE); - add_int(d,DN_FORMAT_UFN); - add_int(d,DN_FORMAT_AD_CANONICAL); - /* add_int(d,DN_FORMAT_LBER); */ /* "for testing only" */ - add_int(d,DN_FORMAT_MASK); - add_int(d,DN_PRETTY); - add_int(d,DN_SKIP); - add_int(d,DN_P_NOLEADTRAILSPACES); - add_int(d,DN_P_NOSPACEAFTERRDN); - add_int(d,DN_PEDANTIC); - - add_int(d,AVA_NULL); - add_int(d,AVA_STRING); - add_int(d,AVA_BINARY); - add_int(d,AVA_NONPRINTABLE); - - /*add_int(d,OPT_ON);*/ - obj = PyInt_FromLong(1); - PyDict_SetItemString( d, "OPT_ON", obj ); - Py_DECREF(obj); - /*add_int(d,OPT_OFF);*/ - obj = PyInt_FromLong(0); - PyDict_SetItemString( d, "OPT_OFF", obj ); - Py_DECREF(obj); - - add_int(d,OPT_SUCCESS); - - /* XXX - these belong in errors.c */ - - add_int(d,URL_ERR_BADSCOPE); - add_int(d,URL_ERR_MEM); - - /* add_int(d,LIBLDAP_R); */ -#ifdef HAVE_LIBLDAP_R - obj = PyInt_FromLong(1); -#else - obj = PyInt_FromLong(0); -#endif - PyDict_SetItemString( d, "LIBLDAP_R", obj ); - Py_DECREF(obj); +#define LDAP_ERROR_MIN LDAP_REFERRAL_LIMIT_EXCEEDED - /* add_int(d,SASL); */ -#ifdef HAVE_SASL - obj = PyInt_FromLong(1); +#ifdef LDAP_PROXIED_AUTHORIZATION_DENIED +#define LDAP_ERROR_MAX LDAP_PROXIED_AUTHORIZATION_DENIED #else - obj = PyInt_FromLong(0); -#endif - PyDict_SetItemString( d, "SASL_AVAIL", obj ); - Py_DECREF(obj); - - /* add_int(d,TLS); */ -#ifdef HAVE_TLS - obj = PyInt_FromLong(1); +#ifdef LDAP_ASSERTION_FAILED +#define LDAP_ERROR_MAX LDAP_ASSERTION_FAILED #else - obj = PyInt_FromLong(0); +#define LDAP_ERROR_MAX LDAP_OTHER +#endif #endif - PyDict_SetItemString( d, "TLS_AVAIL", obj ); - Py_DECREF(obj); - - obj = PyUnicode_FromString(LDAP_CONTROL_MANAGEDSAIT); - PyDict_SetItemString( d, "CONTROL_MANAGEDSAIT", obj ); - Py_DECREF(obj); - - obj = PyUnicode_FromString(LDAP_CONTROL_PROXY_AUTHZ); - PyDict_SetItemString( d, "CONTROL_PROXY_AUTHZ", obj ); - Py_DECREF(obj); - obj = PyUnicode_FromString(LDAP_CONTROL_SUBENTRIES); - PyDict_SetItemString( d, "CONTROL_SUBENTRIES", obj ); - Py_DECREF(obj); +#define LDAP_ERROR_OFFSET -LDAP_ERROR_MIN - obj = PyUnicode_FromString(LDAP_CONTROL_VALUESRETURNFILTER); - PyDict_SetItemString( d, "CONTROL_VALUESRETURNFILTER", obj ); - Py_DECREF(obj); +static PyObject *errobjects[LDAP_ERROR_MAX - LDAP_ERROR_MIN + 1]; - obj = PyUnicode_FromString(LDAP_CONTROL_ASSERT); - PyDict_SetItemString( d, "CONTROL_ASSERT", obj ); - Py_DECREF(obj); +/* Convert a bare LDAP error number into an exception */ +PyObject * +LDAPerr(int errnum) +{ + if (errnum >= LDAP_ERROR_MIN && errnum <= LDAP_ERROR_MAX && + errobjects[errnum + LDAP_ERROR_OFFSET] != NULL) { + PyErr_SetNone(errobjects[errnum + LDAP_ERROR_OFFSET]); + } + else { + PyObject *args = Py_BuildValue("{s:i}", "errnum", errnum); - obj = PyUnicode_FromString(LDAP_CONTROL_PRE_READ); - PyDict_SetItemString( d, "CONTROL_PRE_READ", obj ); - Py_DECREF(obj); + if (args == NULL) + return NULL; + PyErr_SetObject(LDAPexception_class, args); + Py_DECREF(args); + } + return NULL; +} - obj = PyUnicode_FromString(LDAP_CONTROL_POST_READ); - PyDict_SetItemString( d, "CONTROL_POST_READ", obj ); - Py_DECREF(obj); +/* Convert an LDAP error into an informative python exception */ +PyObject * +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, msgid = -1, msgtype = 0; + PyObject *errobj; + PyObject *info; + PyObject *str; + PyObject *pyerrno; + 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; + + if (m != NULL) { + msgid = ldap_msgid(m); + msgtype = ldap_msgtype(m); + ldap_parse_result(l, m, &errnum, &matched, &error, &refs, + &serverctrls, 1); + } + + 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 && + errobjects[errnum + LDAP_ERROR_OFFSET] != NULL) { + errobj = errobjects[errnum + LDAP_ERROR_OFFSET]; + } + else { + errobj = LDAPexception_class; + } + + info = PyDict_New(); + 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) + PyDict_SetItemString(info, "desc", str); + Py_XDECREF(str); + + if (myerrno != 0) { + pyerrno = PyLong_FromLong(myerrno); + if (pyerrno) + PyDict_SetItemString(info, "errno", pyerrno); + Py_XDECREF(pyerrno); + } + + 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) + PyDict_SetItemString(info, "matched", str); + Py_XDECREF(str); + } + ldap_memfree(matched); + } + + 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); + } + + PyErr_SetObject(errobj, info); + Py_DECREF(info); + ldap_memvfree((void **)refs); + ldap_memfree(error); + return NULL; + } +} - obj = PyUnicode_FromString(LDAP_CONTROL_SORTREQUEST); - PyDict_SetItemString( d, "CONTROL_SORTREQUEST", obj ); - Py_DECREF(obj); +PyObject * +LDAPerror(LDAP *l) +{ + return LDAPraise_for_message(l, NULL); +} - obj = PyUnicode_FromString(LDAP_CONTROL_SORTRESPONSE); - PyDict_SetItemString( d, "CONTROL_SORTRESPONSE", obj ); - Py_DECREF(obj); +/* initialise the module constants */ - obj = PyUnicode_FromString(LDAP_CONTROL_PAGEDRESULTS); - PyDict_SetItemString( d, "CONTROL_PAGEDRESULTS", obj ); - Py_DECREF(obj); +int +LDAPinit_constants(PyObject *m) +{ + PyObject *exc, *nobj; + struct ldap_apifeature_info info = { 1, "X_OPENLDAP_THREAD_SAFE", 0 }; + int thread_safe = 0; - obj = PyUnicode_FromString(LDAP_CONTROL_SYNC); - PyDict_SetItemString( d, "CONTROL_SYNC", obj ); - Py_DECREF(obj); + /* simple constants */ - obj = PyUnicode_FromString(LDAP_CONTROL_SYNC_STATE); - PyDict_SetItemString( d, "CONTROL_SYNC_STATE", obj ); - Py_DECREF(obj); + if (PyModule_AddIntConstant(m, "OPT_ON", 1) != 0) + return -1; + if (PyModule_AddIntConstant(m, "OPT_OFF", 0) != 0) + return -1; - obj = PyUnicode_FromString(LDAP_CONTROL_SYNC_DONE); - PyDict_SetItemString( d, "CONTROL_SYNC_DONE", obj ); - Py_DECREF(obj); + /* exceptions */ - obj = PyUnicode_FromString(LDAP_SYNC_INFO); - PyDict_SetItemString( d, "SYNC_INFO", obj ); - Py_DECREF(obj); + LDAPexception_class = PyErr_NewException("ldap.LDAPError", NULL, NULL); + if (LDAPexception_class == NULL) { + return -1; + } - obj = PyUnicode_FromString(LDAP_CONTROL_PASSWORDPOLICYREQUEST); - PyDict_SetItemString( d, "CONTROL_PASSWORDPOLICYREQUEST", obj ); - Py_DECREF(obj); + if (PyModule_AddObject(m, "LDAPError", LDAPexception_class) != 0) + return -1; + Py_INCREF(LDAPexception_class); - obj = PyUnicode_FromString(LDAP_CONTROL_PASSWORDPOLICYRESPONSE); - PyDict_SetItemString( d, "CONTROL_PASSWORDPOLICYRESPONSE", obj ); - Py_DECREF(obj); + /* XXX - backward compatibility with pre-1.8 */ + if (PyModule_AddObject(m, "error", LDAPexception_class) != 0) + return -1; + Py_INCREF(LDAPexception_class); - obj = PyUnicode_FromString(LDAP_CONTROL_RELAX); - PyDict_SetItemString( d, "CONTROL_RELAX", obj ); - Py_DECREF(obj); +#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); \ +} while (0) + +#define add_int(n) do { \ + if (PyModule_AddIntConstant(m, #n, LDAP_##n) != 0) return -1; \ +} while (0) + +#define add_string(n) do { \ + if (PyModule_AddStringConstant(m, #n, LDAP_##n) != 0) return -1; \ +} while (0) + +#include "constants_generated.h" + + return 0; } diff --git a/Modules/constants.h b/Modules/constants.h deleted file mode 100644 index eb766124..00000000 --- a/Modules/constants.h +++ /dev/null @@ -1,18 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_constants_ -#define __h_constants_ - -#include "common.h" -extern void LDAPinit_constants( PyObject* d ); -extern PyObject* LDAPconstant( int ); - -#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 new file mode 100644 index 00000000..3e59f828 --- /dev/null +++ b/Modules/constants_generated.h @@ -0,0 +1,409 @@ +/* + * Generated with: + * python Lib/ldap/constants.py > Modules/constants_generated.h + * + * Please do any modifications there, then re-generate this file + */ + +add_err(ADMINLIMIT_EXCEEDED); +add_err(AFFECTS_MULTIPLE_DSAS); +add_err(ALIAS_DEREF_PROBLEM); +add_err(ALIAS_PROBLEM); +add_err(ALREADY_EXISTS); +add_err(AUTH_METHOD_NOT_SUPPORTED); +add_err(AUTH_UNKNOWN); +add_err(BUSY); +add_err(CLIENT_LOOP); +add_err(COMPARE_FALSE); +add_err(COMPARE_TRUE); +add_err(CONFIDENTIALITY_REQUIRED); +add_err(CONNECT_ERROR); +add_err(CONSTRAINT_VIOLATION); +add_err(CONTROL_NOT_FOUND); +add_err(DECODING_ERROR); +add_err(ENCODING_ERROR); +add_err(FILTER_ERROR); +add_err(INAPPROPRIATE_AUTH); +add_err(INAPPROPRIATE_MATCHING); +add_err(INSUFFICIENT_ACCESS); +add_err(INVALID_CREDENTIALS); +add_err(INVALID_DN_SYNTAX); +add_err(INVALID_SYNTAX); +add_err(IS_LEAF); +add_err(LOCAL_ERROR); +add_err(LOOP_DETECT); +add_err(MORE_RESULTS_TO_RETURN); +add_err(NAMING_VIOLATION); +add_err(NO_MEMORY); +add_err(NO_OBJECT_CLASS_MODS); +add_err(NO_OBJECT_CLASS_MODS); +add_err(NO_RESULTS_RETURNED); +add_err(NO_SUCH_ATTRIBUTE); +add_err(NO_SUCH_OBJECT); +add_err(NOT_ALLOWED_ON_NONLEAF); +add_err(NOT_ALLOWED_ON_RDN); +add_err(NOT_SUPPORTED); +add_err(OBJECT_CLASS_VIOLATION); +add_err(OPERATIONS_ERROR); +add_err(OTHER); +add_err(PARAM_ERROR); +add_err(PARTIAL_RESULTS); +add_err(PROTOCOL_ERROR); +add_err(REFERRAL); +add_err(REFERRAL_LIMIT_EXCEEDED); +add_err(RESULTS_TOO_LARGE); +add_err(SASL_BIND_IN_PROGRESS); +add_err(SERVER_DOWN); +add_err(SIZELIMIT_EXCEEDED); +add_err(STRONG_AUTH_NOT_SUPPORTED); +add_err(STRONG_AUTH_REQUIRED); +add_err(SUCCESS); +add_err(TIMELIMIT_EXCEEDED); +add_err(TIMEOUT); +add_err(TYPE_OR_VALUE_EXISTS); +add_err(UNAVAILABLE); +add_err(UNAVAILABLE_CRITICAL_EXTENSION); +add_err(UNDEFINED_TYPE); +add_err(UNWILLING_TO_PERFORM); +add_err(USER_CANCELLED); +add_err(VLV_ERROR); +add_err(X_PROXY_AUTHZ_FAILURE); + +#if defined(LDAP_API_FEATURE_CANCEL) +add_err(CANCELLED); +add_err(NO_SUCH_OPERATION); +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 + +add_int(API_VERSION); +add_int(VENDOR_VERSION); +add_int(PORT); +add_int(VERSION1); +add_int(VERSION2); +add_int(VERSION3); +add_int(VERSION_MIN); +add_int(VERSION); +add_int(VERSION_MAX); +add_int(TAG_MESSAGE); +add_int(TAG_MSGID); +add_int(REQ_BIND); +add_int(REQ_UNBIND); +add_int(REQ_SEARCH); +add_int(REQ_MODIFY); +add_int(REQ_ADD); +add_int(REQ_DELETE); +add_int(REQ_MODRDN); +add_int(REQ_COMPARE); +add_int(REQ_ABANDON); +add_int(TAG_LDAPDN); +add_int(TAG_LDAPCRED); +add_int(TAG_CONTROLS); +add_int(TAG_REFERRAL); +add_int(REQ_EXTENDED); + +#if LDAP_API_VERSION >= 2004 +add_int(TAG_NEWSUPERIOR); +add_int(TAG_EXOP_REQ_OID); +add_int(TAG_EXOP_REQ_VALUE); +add_int(TAG_EXOP_RES_OID); +add_int(TAG_EXOP_RES_VALUE); + +#if defined(HAVE_SASL) +add_int(TAG_SASL_RES_CREDS); +#endif + +#endif + +add_int(SASL_AUTOMATIC); +add_int(SASL_INTERACTIVE); +add_int(SASL_QUIET); +add_int(RES_BIND); +add_int(RES_SEARCH_ENTRY); +add_int(RES_SEARCH_RESULT); +add_int(RES_MODIFY); +add_int(RES_ADD); +add_int(RES_DELETE); +add_int(RES_MODRDN); +add_int(RES_COMPARE); +add_int(RES_ANY); +add_int(RES_SEARCH_REFERENCE); +add_int(RES_EXTENDED); +add_int(RES_UNSOLICITED); +add_int(RES_INTERMEDIATE); +add_int(AUTH_NONE); +add_int(AUTH_SIMPLE); +add_int(SCOPE_BASE); +add_int(SCOPE_ONELEVEL); +add_int(SCOPE_SUBTREE); + +#if defined(LDAP_SCOPE_SUBORDINATE) +add_int(SCOPE_SUBORDINATE); +#endif + +add_int(MOD_ADD); +add_int(MOD_DELETE); +add_int(MOD_REPLACE); +add_int(MOD_INCREMENT); +add_int(MOD_BVALUES); +add_int(MSG_ONE); +add_int(MSG_ALL); +add_int(MSG_RECEIVED); +add_int(DEREF_NEVER); +add_int(DEREF_SEARCHING); +add_int(DEREF_FINDING); +add_int(DEREF_ALWAYS); +add_int(NO_LIMIT); +add_int(OPT_API_INFO); +add_int(OPT_DEREF); +add_int(OPT_SIZELIMIT); +add_int(OPT_TIMELIMIT); + +#if defined(LDAP_OPT_REFERRALS) +add_int(OPT_REFERRALS); +#endif + +add_int(OPT_RESULT_CODE); +add_int(OPT_ERROR_NUMBER); +add_int(OPT_RESTART); +add_int(OPT_PROTOCOL_VERSION); +add_int(OPT_SERVER_CONTROLS); +add_int(OPT_CLIENT_CONTROLS); +add_int(OPT_API_FEATURE_INFO); +add_int(OPT_HOST_NAME); +add_int(OPT_DESC); +add_int(OPT_DIAGNOSTIC_MESSAGE); +add_int(OPT_ERROR_STRING); +add_int(OPT_MATCHED_DN); +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) +add_int(OPT_X_TLS); +#endif + +add_int(OPT_X_TLS_CTX); +add_int(OPT_X_TLS_CACERTFILE); +add_int(OPT_X_TLS_CACERTDIR); +add_int(OPT_X_TLS_CERTFILE); +add_int(OPT_X_TLS_KEYFILE); +add_int(OPT_X_TLS_REQUIRE_CERT); +add_int(OPT_X_TLS_CIPHER_SUITE); +add_int(OPT_X_TLS_RANDOM_FILE); +add_int(OPT_X_TLS_DHFILE); +add_int(OPT_X_TLS_NEVER); +add_int(OPT_X_TLS_HARD); +add_int(OPT_X_TLS_DEMAND); +add_int(OPT_X_TLS_ALLOW); +add_int(OPT_X_TLS_TRY); + +#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 + +add_int(OPT_X_TLS_CRL_NONE); +add_int(OPT_X_TLS_CRL_PEER); +add_int(OPT_X_TLS_CRL_ALL); + +#if defined(LDAP_OPT_X_TLS_NEWCTX) +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); +add_int(OPT_X_SASL_REALM); +add_int(OPT_X_SASL_AUTHCID); +add_int(OPT_X_SASL_AUTHZID); +add_int(OPT_X_SASL_SSF); +add_int(OPT_X_SASL_SSF_EXTERNAL); +add_int(OPT_X_SASL_SECPROPS); +add_int(OPT_X_SASL_SSF_MIN); +add_int(OPT_X_SASL_SSF_MAX); + +#if defined(LDAP_OPT_X_SASL_NOCANON) +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 + +add_int(DN_FORMAT_LDAP); +add_int(DN_FORMAT_LDAPV3); +add_int(DN_FORMAT_LDAPV2); +add_int(DN_FORMAT_DCE); +add_int(DN_FORMAT_UFN); +add_int(DN_FORMAT_AD_CANONICAL); +add_int(DN_FORMAT_MASK); +add_int(DN_PRETTY); +add_int(DN_SKIP); +add_int(DN_P_NOLEADTRAILSPACES); +add_int(DN_P_NOSPACEAFTERRDN); +add_int(DN_PEDANTIC); +add_int(AVA_NULL); +add_int(AVA_STRING); +add_int(AVA_BINARY); +add_int(AVA_NONPRINTABLE); +add_int(OPT_SUCCESS); +add_int(URL_ERR_BADSCOPE); +add_int(URL_ERR_MEM); + +#ifdef HAVE_SASL +if (PyModule_AddIntConstant(m, "SASL_AVAIL", 1) != 0) return -1; +#else +if (PyModule_AddIntConstant(m, "SASL_AVAIL", 0) != 0) return -1; +#endif + + +#ifdef HAVE_TLS +if (PyModule_AddIntConstant(m, "TLS_AVAIL", 1) != 0) return -1; +#else +if (PyModule_AddIntConstant(m, "TLS_AVAIL", 0) != 0) return -1; +#endif + + +#ifdef HAVE_LDAP_INIT_FD +if (PyModule_AddIntConstant(m, "INIT_FD_AVAIL", 1) != 0) return -1; +#else +if (PyModule_AddIntConstant(m, "INIT_FD_AVAIL", 0) != 0) return -1; +#endif + +add_string(CONTROL_MANAGEDSAIT); +add_string(CONTROL_PROXY_AUTHZ); +add_string(CONTROL_SUBENTRIES); +add_string(CONTROL_VALUESRETURNFILTER); +add_string(CONTROL_ASSERT); +add_string(CONTROL_PRE_READ); +add_string(CONTROL_POST_READ); +add_string(CONTROL_SORTREQUEST); +add_string(CONTROL_SORTRESPONSE); +add_string(CONTROL_PAGEDRESULTS); +add_string(CONTROL_SYNC); +add_string(CONTROL_SYNC_STATE); +add_string(CONTROL_SYNC_DONE); +add_string(SYNC_INFO); +add_string(CONTROL_PASSWORDPOLICYREQUEST); +add_string(CONTROL_PASSWORDPOLICYRESPONSE); +add_string(CONTROL_RELAX); diff --git a/Modules/errors.c b/Modules/errors.c deleted file mode 100644 index e5cb0ee8..00000000 --- a/Modules/errors.c +++ /dev/null @@ -1,233 +0,0 @@ -/* - * errors that arise from ldap use - * Most errors become their own exception - * See https://www.python-ldap.org/ for details. */ - -#include "common.h" -#include "errors.h" -#include -#include - -/* the base exception class */ - -PyObject* -LDAPexception_class; - -/* list of error objects */ - -#define LDAP_ERROR_MIN LDAP_REFERRAL_LIMIT_EXCEEDED - -#ifdef LDAP_PROXIED_AUTHORIZATION_DENIED - #define LDAP_ERROR_MAX LDAP_PROXIED_AUTHORIZATION_DENIED -#else - #ifdef LDAP_ASSERTION_FAILED - #define LDAP_ERROR_MAX LDAP_ASSERTION_FAILED - #else - #define LDAP_ERROR_MAX LDAP_OTHER - #endif -#endif - -#define LDAP_ERROR_OFFSET -LDAP_ERROR_MIN - -static PyObject* errobjects[ LDAP_ERROR_MAX-LDAP_ERROR_MIN+1 ]; - - -/* Convert a bare LDAP error number into an exception */ -PyObject* -LDAPerr(int errnum) -{ - if (errnum >= LDAP_ERROR_MIN && errnum <= LDAP_ERROR_MAX) - PyErr_SetNone(errobjects[errnum+LDAP_ERROR_OFFSET]); - else - PyErr_SetObject(LDAPexception_class, - Py_BuildValue("{s:i}", "errnum", errnum)); - return NULL; -} - -/* Convert an LDAP error into an informative python exception */ -PyObject* -LDAPerror( LDAP *l, char *msg ) -{ - if (l == NULL) { - PyErr_SetFromErrno( LDAPexception_class ); - return NULL; - } - else { - int myerrno, errnum, opt_errnum; - PyObject *errobj; - PyObject *info; - PyObject *str; - PyObject *pyerrno; - - /* at first save errno for later use before it gets overwritten by another call */ - myerrno = errno; - - char *matched, *error; - - 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(); - - if (errnum >= LDAP_ERROR_MIN && errnum <= LDAP_ERROR_MAX) - errobj = errobjects[errnum+LDAP_ERROR_OFFSET]; - else - errobj = LDAPexception_class; - - info = PyDict_New(); - if (info == NULL) - return NULL; - - str = PyUnicode_FromString(ldap_err2string(errnum)); - if (str) - PyDict_SetItemString( info, "desc", str ); - Py_XDECREF(str); - - if (myerrno != 0) { - pyerrno = PyInt_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 (*matched != '\0') { - str = PyUnicode_FromString(matched); - if (str) - PyDict_SetItemString( info, "matched", str ); - Py_XDECREF(str); - } - ldap_memfree(matched); - } - - if (errnum == LDAP_REFERRAL) { - str = PyUnicode_FromString(msg); - 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); - return NULL; - } -} - - -/* initialisation */ - -void -LDAPinit_errors( PyObject*d ) { - - /* create the base exception class */ - LDAPexception_class = PyErr_NewException("ldap.LDAPError", - NULL, - NULL); - PyDict_SetItemString( d, "LDAPError", LDAPexception_class ); - - /* XXX - backward compatibility with pre-1.8 */ - PyDict_SetItemString( d, "error", LDAPexception_class ); - - /* create each LDAP error object */ - -# define seterrobj2(n,o) \ - PyDict_SetItemString( d, #n, (errobjects[LDAP_##n+LDAP_ERROR_OFFSET] = o) ) - - -# define seterrobj(n) { \ - PyObject *e = PyErr_NewException("ldap." #n, \ - LDAPexception_class, NULL); \ - seterrobj2(n, e); \ - Py_INCREF(e); \ - } - - seterrobj(ADMINLIMIT_EXCEEDED); - seterrobj(AFFECTS_MULTIPLE_DSAS); - seterrobj(ALIAS_DEREF_PROBLEM); - seterrobj(ALIAS_PROBLEM); - seterrobj(ALREADY_EXISTS); - seterrobj(AUTH_METHOD_NOT_SUPPORTED); - seterrobj(AUTH_UNKNOWN); - seterrobj(BUSY); - seterrobj(CLIENT_LOOP); - seterrobj(COMPARE_FALSE); - seterrobj(COMPARE_TRUE); - seterrobj(CONFIDENTIALITY_REQUIRED); - seterrobj(CONNECT_ERROR); - seterrobj(CONSTRAINT_VIOLATION); - seterrobj(CONTROL_NOT_FOUND); - seterrobj(DECODING_ERROR); - seterrobj(ENCODING_ERROR); - seterrobj(FILTER_ERROR); - seterrobj(INAPPROPRIATE_AUTH); - seterrobj(INAPPROPRIATE_MATCHING); - seterrobj(INSUFFICIENT_ACCESS); - seterrobj(INVALID_CREDENTIALS); - seterrobj(INVALID_DN_SYNTAX); - seterrobj(INVALID_SYNTAX); - seterrobj(IS_LEAF); - seterrobj(LOCAL_ERROR); - seterrobj(LOOP_DETECT); - seterrobj(MORE_RESULTS_TO_RETURN); - seterrobj(NAMING_VIOLATION); - seterrobj(NO_MEMORY); - seterrobj(NO_OBJECT_CLASS_MODS); - seterrobj(NO_OBJECT_CLASS_MODS); - seterrobj(NO_RESULTS_RETURNED); - seterrobj(NO_SUCH_ATTRIBUTE); - seterrobj(NO_SUCH_OBJECT); - seterrobj(NOT_ALLOWED_ON_NONLEAF); - seterrobj(NOT_ALLOWED_ON_RDN); - seterrobj(NOT_SUPPORTED); - seterrobj(OBJECT_CLASS_VIOLATION); - seterrobj(OPERATIONS_ERROR); - seterrobj(OTHER); - seterrobj(PARAM_ERROR); - seterrobj(PARTIAL_RESULTS); - seterrobj(PROTOCOL_ERROR); - seterrobj(REFERRAL); - seterrobj(REFERRAL_LIMIT_EXCEEDED); - seterrobj(RESULTS_TOO_LARGE); - seterrobj(SASL_BIND_IN_PROGRESS); - seterrobj(SERVER_DOWN); - seterrobj(SIZELIMIT_EXCEEDED); - seterrobj(STRONG_AUTH_NOT_SUPPORTED); - seterrobj(STRONG_AUTH_REQUIRED); - seterrobj(SUCCESS); - seterrobj(TIMELIMIT_EXCEEDED); - seterrobj(TIMEOUT); - seterrobj(TYPE_OR_VALUE_EXISTS); - seterrobj(UNAVAILABLE); - seterrobj(UNAVAILABLE_CRITICAL_EXTENSION); - seterrobj(UNDEFINED_TYPE); - seterrobj(UNWILLING_TO_PERFORM); - seterrobj(USER_CANCELLED); - seterrobj(VLV_ERROR); - seterrobj(X_PROXY_AUTHZ_FAILURE); - -#ifdef LDAP_API_FEATURE_CANCEL - seterrobj(CANCELLED); - seterrobj(NO_SUCH_OPERATION); - seterrobj(TOO_LATE); - seterrobj(CANNOT_CANCEL); -#endif - -#ifdef LDAP_ASSERTION_FAILED - seterrobj(ASSERTION_FAILED); -#endif - -#ifdef LDAP_PROXIED_AUTHORIZATION_DENIED - seterrobj(PROXIED_AUTHORIZATION_DENIED); -#endif - -} diff --git a/Modules/errors.h b/Modules/errors.h deleted file mode 100644 index 32aebffe..00000000 --- a/Modules/errors.h +++ /dev/null @@ -1,15 +0,0 @@ -/* See https://www.python-ldap.org/ for details. */ - -#ifndef __h_errors_ -#define __h_errors_ - -#include "common.h" -#include "lber.h" -#include "ldap.h" - -extern PyObject* LDAPexception_class; -extern PyObject* LDAPerror( LDAP*, char*msg ); -extern void LDAPinit_errors( PyObject* ); -PyObject* LDAPerr(int errnum); - -#endif /* __h_errors */ diff --git a/Modules/functions.c b/Modules/functions.c index ffb6765c..3f7f7eca 100644 --- a/Modules/functions.c +++ b/Modules/functions.c @@ -1,37 +1,89 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "functions.h" -#include "LDAPObject.h" -#include "berval.h" -#include "errors.h" -#include "options.h" +#include "pythonldap.h" /* ldap_initialize */ -static PyObject* -l_ldap_initialize(PyObject* unused, PyObject *args) +static PyObject * +l_ldap_initialize(PyObject *unused, PyObject *args) { char *uri; LDAP *ld = NULL; int ret; + PyThreadState *save; - if (!PyArg_ParseTuple(args, "s", &uri)) - return NULL; + if (!PyArg_ParseTuple(args, "z:initialize", &uri)) + return NULL; - Py_BEGIN_ALLOW_THREADS + save = PyEval_SaveThread(); ret = ldap_initialize(&ld, uri); - Py_END_ALLOW_THREADS + PyEval_RestoreThread(save); + if (ret != LDAP_SUCCESS) - return LDAPerror(ld, "ldap_initialize"); - return (PyObject*)newLDAPObject(ld); + 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 */ -static PyObject* -l_ldap_str2dn( PyObject* unused, PyObject *args ) +static PyObject * +l_ldap_str2dn(PyObject *unused, PyObject *args) { struct berval str; LDAPDN dn; @@ -46,105 +98,325 @@ l_ldap_str2dn( PyObject* unused, PyObject *args ) * ((('a','b',1),('c','d',1)),(('e','f',1),)) * The integers are a bit combination of the AVA_* flags */ - if (!PyArg_ParseTuple( args, "z#|i:str2dn", - &str.bv_val, &str_len, &flags )) - return NULL; + if (!PyArg_ParseTuple(args, "z#|i:str2dn", &str.bv_val, &str_len, &flags)) + return NULL; str.bv_len = (ber_len_t) str_len; res = ldap_bv2dn(&str, &dn, flags); if (res != LDAP_SUCCESS) - return LDAPerr(res); + return LDAPerr(res); tmp = PyList_New(0); if (!tmp) - goto failed; + goto failed; for (i = 0; dn[i]; i++) { - LDAPRDN rdn; - PyObject *rdnlist; - - rdn = dn[i]; - rdnlist = PyList_New(0); - if (!rdnlist) - goto failed; - if (PyList_Append(tmp, rdnlist) == -1) { - Py_DECREF(rdnlist); - goto failed; - } - - for (j = 0; rdn[j]; j++) { - LDAPAVA *ava = rdn[j]; - PyObject *tuple; - - 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)); - if (!tuple) { - Py_DECREF(rdnlist); - goto failed; - } - - if (PyList_Append(rdnlist, tuple) == -1) { - Py_DECREF(tuple); - goto failed; - } - Py_DECREF(tuple); - } - Py_DECREF(rdnlist); + LDAPRDN rdn; + PyObject *rdnlist; + + rdn = dn[i]; + rdnlist = PyList_New(0); + if (!rdnlist) + goto failed; + if (PyList_Append(tmp, rdnlist) == -1) { + Py_DECREF(rdnlist); + goto failed; + } + + for (j = 0; rdn[j]; j++) { + LDAPAVA *ava = rdn[j]; + PyObject *tuple; + + 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)); + if (!tuple) { + Py_DECREF(rdnlist); + goto failed; + } + + if (PyList_Append(rdnlist, tuple) == -1) { + Py_DECREF(tuple); + goto failed; + } + Py_DECREF(tuple); + } + Py_DECREF(rdnlist); } result = tmp; tmp = NULL; -failed: + failed: Py_XDECREF(tmp); ldap_dnfree(dn); 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* -l_ldap_set_option(PyObject* self, PyObject *args) +static PyObject * +l_ldap_set_option(PyObject *self, PyObject *args) { PyObject *value; int option; if (!PyArg_ParseTuple(args, "iO:set_option", &option, &value)) - return NULL; + return NULL; if (!LDAP_set_option(NULL, option, value)) - return NULL; + return NULL; Py_INCREF(Py_None); return Py_None; } /* ldap_get_option (global options) */ -static PyObject* -l_ldap_get_option(PyObject* self, PyObject *args) +static PyObject * +l_ldap_get_option(PyObject *self, PyObject *args) { int option; if (!PyArg_ParseTuple(args, "i:get_option", &option)) - return NULL; + return NULL; return LDAP_get_option(NULL, option); } - /* methods */ static PyMethodDef methods[] = { - { "initialize", (PyCFunction)l_ldap_initialize, METH_VARARGS }, - { "str2dn", (PyCFunction)l_ldap_str2dn, METH_VARARGS }, - { "set_option", (PyCFunction)l_ldap_set_option, METH_VARARGS }, - { "get_option", (PyCFunction)l_ldap_get_option, METH_VARARGS }, - { NULL, NULL } + {"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} }; /* initialisation */ void -LDAPinit_functions( PyObject* d ) { - LDAPadd_methods( d, methods ); +LDAPinit_functions(PyObject *d) +{ + LDAPadd_methods(d, methods); } diff --git a/Modules/functions.h b/Modules/functions.h deleted file mode 100644 index 854a9403..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 6ff27c55..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 "errors.h" - -#include "lber.h" +#include "pythonldap.h" /* Prints to stdout the contents of an array of LDAPControl objects */ @@ -27,13 +21,13 @@ LDAPControl_DumpList( LDAPControl** lcs ) { } */ /* Free a single LDAPControl object created by Tuple_to_LDAPControl */ - + static void -LDAPControl_DEL( LDAPControl* lc ) +LDAPControl_DEL(LDAPControl *lc) { if (lc == NULL) return; - + if (lc->ldctl_oid) PyMem_DEL(lc->ldctl_oid); PyMem_DEL(lc); @@ -42,16 +36,17 @@ LDAPControl_DEL( LDAPControl* lc ) /* Free an array of LDAPControl objects created by LDAPControls_from_object */ void -LDAPControl_List_DEL( LDAPControl** lcs ) +LDAPControl_List_DEL(LDAPControl **lcs) { - LDAPControl** lcp; + LDAPControl **lcp; + if (lcs == NULL) return; - for ( lcp = lcs; *lcp; lcp++ ) - LDAPControl_DEL( *lcp ); + for (lcp = lcs; *lcp; lcp++) + LDAPControl_DEL(*lcp); - PyMem_DEL( lcs ); + PyMem_DEL(lcs); } /* Takes a tuple of the form: @@ -61,8 +56,8 @@ LDAPControl_List_DEL( LDAPControl** lcs ) * The Value string should represent an ASN.1 encoded structure. */ -static LDAPControl* -Tuple_to_LDAPControl( PyObject* tup ) +static LDAPControl * +Tuple_to_LDAPControl(PyObject *tup) { char *oid; char iscritical; @@ -72,15 +67,16 @@ Tuple_to_LDAPControl( PyObject* tup ) Py_ssize_t len; if (!PyTuple_Check(tup)) { - PyErr_SetObject(PyExc_TypeError, Py_BuildValue("sO", - "expected a tuple", tup)); - return NULL; + LDAPerror_TypeError("Tuple_to_LDAPControl(): expected a tuple", tup); + return NULL; } - if (!PyArg_ParseTuple( tup, "sbO", &oid, &iscritical, &bytes )) + if (!PyArg_ParseTuple + (tup, "sbO:Tuple_to_LDAPControl", &oid, &iscritical, &bytes)) return NULL; - + lc = PyMem_NEW(LDAPControl, 1); + if (lc == NULL) { PyErr_NoMemory(); return NULL; @@ -90,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); @@ -107,12 +104,11 @@ Tuple_to_LDAPControl( PyObject* tup ) berbytes.bv_val = PyBytes_AsString(bytes); } else { - PyErr_SetObject(PyExc_TypeError, Py_BuildValue("sO", - "expected bytes", bytes)); + LDAPerror_TypeError("Tuple_to_LDAPControl(): expected bytes", bytes); LDAPControl_DEL(lc); return NULL; } - + lc->ldctl_value = berbytes; return lc; @@ -122,42 +118,43 @@ Tuple_to_LDAPControl( PyObject* tup ) * function) into an array of LDAPControl objects. */ int -LDAPControls_from_object(PyObject* list, LDAPControl ***controls_ret) +LDAPControls_from_object(PyObject *list, LDAPControl ***controls_ret) { Py_ssize_t len, i; - LDAPControl** ldcs; - LDAPControl* ldc; - PyObject* item; - + LDAPControl **ldcs; + LDAPControl *ldc; + PyObject *item; + if (!PySequence_Check(list)) { - PyErr_SetObject(PyExc_TypeError, Py_BuildValue("sO", - "expected a list", list)); - return 0; + LDAPerror_TypeError("LDAPControls_from_object(): expected a list", + list); + return 0; } len = PySequence_Length(list); - ldcs = PyMem_NEW(LDAPControl*, len + 1); + ldcs = PyMem_NEW(LDAPControl *, len + 1); + if (ldcs == NULL) { PyErr_NoMemory(); return 0; } for (i = 0; i < len; i++) { - item = PySequence_GetItem(list, i); - if (item == NULL) { - PyMem_DEL(ldcs); - return 0; - } - - ldc = Tuple_to_LDAPControl(item); - if (ldc == NULL) { - Py_DECREF(item); - PyMem_DEL(ldcs); - return 0; - } - - ldcs[i] = ldc; - Py_DECREF(item); + item = PySequence_GetItem(list, i); + if (item == NULL) { + PyMem_DEL(ldcs); + return 0; + } + + ldc = Tuple_to_LDAPControl(item); + if (ldc == NULL) { + Py_DECREF(item); + PyMem_DEL(ldcs); + return 0; + } + + ldcs[i] = ldc; + Py_DECREF(item); } ldcs[len] = NULL; @@ -165,7 +162,7 @@ LDAPControls_from_object(PyObject* list, LDAPControl ***controls_ret) return 1; } -PyObject* +PyObject * LDAPControls_to_List(LDAPControl **ldcs) { PyObject *res = 0, *pyctrl; @@ -173,80 +170,79 @@ LDAPControls_to_List(LDAPControl **ldcs) Py_ssize_t num_ctrls = 0, i; if (tmp) - while (*tmp++) num_ctrls++; + while (*tmp++) + num_ctrls++; - if (!(res = PyList_New(num_ctrls))) - goto endlbl; + if ((res = PyList_New(num_ctrls)) == NULL) { + return NULL; + } for (i = 0; i < num_ctrls; i++) { - if (!(pyctrl = Py_BuildValue("sbO&", ldcs[i]->ldctl_oid, - ldcs[i]->ldctl_iscritical, - LDAPberval_to_object, - &ldcs[i]->ldctl_value))) { - goto endlbl; + pyctrl = Py_BuildValue("sbO&", + ldcs[i]->ldctl_oid, + ldcs[i]->ldctl_iscritical, + LDAPberval_to_object, &ldcs[i]->ldctl_value); + if (pyctrl == NULL) { + Py_DECREF(res); + return NULL; } PyList_SET_ITEM(res, i, pyctrl); } - Py_INCREF(res); - - endlbl: - Py_XDECREF(res); return res; } - - /* --------------- en-/decoders ------------- */ /* Matched Values, aka, Values Return Filter */ -static PyObject* +static PyObject * encode_rfc3876(PyObject *self, PyObject *args) { - PyObject *res = 0; - int err; - BerElement *vrber = 0; - char *vrFilter; - struct berval *ctrl_val; - - if (!PyArg_ParseTuple(args, "s:encode_valuesreturnfilter_control", &vrFilter)) { - goto endlbl; - } - - if (!(vrber = ber_alloc_t(LBER_USE_DER))) { - LDAPerr(LDAP_NO_MEMORY); - goto endlbl; - } - - err = ldap_put_vrFilter(vrber, vrFilter); - if (err == -1) { - LDAPerr(LDAP_FILTER_ERROR); - goto endlbl; - } - - err = ber_flatten(vrber, &ctrl_val); - if (err == -1) { - LDAPerr(LDAP_NO_MEMORY); - goto endlbl; - } - - res = LDAPberval_to_object(ctrl_val); - ber_bvfree(ctrl_val); - -endlbl: - if (vrber) - ber_free(vrber, 1); - - return res; + PyObject *res = 0; + int err; + BerElement *vrber = 0; + char *vrFilter; + struct berval *ctrl_val; + + if (!PyArg_ParseTuple + (args, "s:encode_valuesreturnfilter_control", &vrFilter)) { + goto endlbl; + } + + if (!(vrber = ber_alloc_t(LBER_USE_DER))) { + LDAPerr(LDAP_NO_MEMORY); + goto endlbl; + } + + err = ldap_put_vrFilter(vrber, vrFilter); + if (err == -1) { + LDAPerr(LDAP_FILTER_ERROR); + goto endlbl; + } + + err = ber_flatten(vrber, &ctrl_val); + if (err == -1) { + LDAPerr(LDAP_NO_MEMORY); + goto endlbl; + } + + res = LDAPberval_to_object(ctrl_val); + ber_bvfree(ctrl_val); + + endlbl: + if (vrber) + ber_free(vrber, 1); + + return res; } -static PyObject* +static PyObject * encode_rfc2696(PyObject *self, PyObject *args) { PyObject *res = 0; BerElement *ber = 0; struct berval cookie, *ctrl_val; Py_ssize_t cookie_len; - unsigned long size; + int size = 0; /* ber_int_t is int */ ber_tag_t tag; if (!PyArg_ParseTuple(args, "is#:encode_page_control", &size, @@ -289,14 +285,13 @@ encode_rfc2696(PyObject *self, PyObject *args) res = LDAPberval_to_object(ctrl_val); ber_bvfree(ctrl_val); - endlbl: + endlbl: if (ber) ber_free(ber, 1); return res; } - -static PyObject* +static PyObject * decode_rfc2696(PyObject *self, PyObject *args) { PyObject *res = 0; @@ -304,7 +299,7 @@ decode_rfc2696(PyObject *self, PyObject *args) struct berval ldctl_value; ber_tag_t tag; struct berval *cookiep; - unsigned long count = 0; + int count = 0; /* ber_int_t is int */ Py_ssize_t ldctl_value_len; if (!PyArg_ParseTuple(args, "s#:decode_page_control", @@ -324,16 +319,16 @@ decode_rfc2696(PyObject *self, PyObject *args) goto endlbl; } - res = Py_BuildValue("(kO&)", count, LDAPberval_to_object, cookiep); + res = Py_BuildValue("(iO&)", count, LDAPberval_to_object, cookiep); ber_bvfree(cookiep); - endlbl: + endlbl: if (ber) ber_free(ber, 1); return res; } -static PyObject* +static PyObject * encode_assertion_control(PyObject *self, PyObject *args) { int err; @@ -341,33 +336,51 @@ 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)) { goto endlbl; } + /* XXX: ldap_create() is a nasty and slow hack. It's creating a full blown + * LDAP object just to encode assertion controls. + */ + save = PyEval_SaveThread(); err = ldap_create(&ld); + PyEval_RestoreThread(save); if (err != LDAP_SUCCESS) - return LDAPerror(ld, "ldap_create"); + return LDAPerror(ld); - err = ldap_create_assertion_control_value(ld,assertion_filterstr,&ctrl_val); - if (err != LDAP_SUCCESS) - return LDAPerror(ld, "ldap_create_assertion_control_value"); + err = ldap_create_assertion_control_value(ld, assertion_filterstr, + &ctrl_val); + if (err != LDAP_SUCCESS) { + LDAPerror(ld); + save = PyEval_SaveThread(); + ldap_unbind_ext(ld, NULL, NULL); + PyEval_RestoreThread(save); + return NULL; + } + save = PyEval_SaveThread(); + ldap_unbind_ext(ld, NULL, NULL); + PyEval_RestoreThread(save); res = LDAPberval_to_object(&ctrl_val); - endlbl: + if (ctrl_val.bv_val != NULL) { + ber_memfree(ctrl_val.bv_val); + } + endlbl: return res; } static PyMethodDef methods[] = { - {"encode_page_control", encode_rfc2696, METH_VARARGS }, - {"decode_page_control", decode_rfc2696, METH_VARARGS }, - {"encode_valuesreturnfilter_control", encode_rfc3876, METH_VARARGS }, - {"encode_assertion_control", encode_assertion_control, METH_VARARGS }, - { NULL, NULL } + {"encode_page_control", encode_rfc2696, METH_VARARGS}, + {"decode_page_control", decode_rfc2696, METH_VARARGS}, + {"encode_valuesreturnfilter_control", encode_rfc3876, METH_VARARGS}, + {"encode_assertion_control", encode_assertion_control, METH_VARARGS}, + {NULL, NULL} }; void @@ -375,5 +388,3 @@ LDAPinit_control(PyObject *d) { LDAPadd_methods(d, methods); } - - diff --git a/Modules/ldapcontrol.h b/Modules/ldapcontrol.h deleted file mode 100644 index 1c09d954..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 46c9c8a9..d0735356 100644 --- a/Modules/ldapmodule.c +++ b/Modules/ldapmodule.c @@ -1,91 +1,70 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "constants.h" -#include "errors.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); static void -init_pkginfo( PyObject* m ) +init_pkginfo(PyObject *m) { - PyModule_AddStringConstant(m, "__version__", version_str); - PyModule_AddStringConstant(m, "__author__", author_str); - PyModule_AddStringConstant(m, "__license__", license_str); + PyModule_AddStringConstant(m, "__version__", version_str); + PyModule_AddStringConstant(m, "__author__", author_str); + PyModule_AddStringConstant(m, "__license__", license_str); } /* dummy module methods */ -static PyMethodDef methods[] = { - { NULL, NULL } +static PyMethodDef methods[] = { + {NULL, NULL} }; -/* module initialisation */ +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() +PyMODINIT_FUNC +PyInit__ldap() { - PyObject *m, *d; - -#if defined(MS_WINDOWS) || defined(__CYGWIN__) - LDAP_Type.ob_type = &PyType_Type; -#endif - - /* 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 - - PyType_Ready(&LDAP_Type); - - /* Add some symbolic constants to the module */ - d = PyModule_GetDict(m); - - init_pkginfo(m); - - LDAPinit_constants(d); - LDAPinit_errors(d); - LDAPinit_functions(d); - LDAPinit_control(d); - - /* Check for errors */ - if (PyErr_Occurred()) - Py_FatalError("can't initialize module _ldap"); - - return m; -} + PyObject *m, *d; + /* Create the module and add the functions */ + m = PyModule_Create(&ldap_moduledef); -#if PY_MAJOR_VERSION < 3 -PyMODINIT_FUNC init_ldap() { - init_ldap_module(); -} -#else -PyMODINIT_FUNC PyInit__ldap() { - return init_ldap_module(); + /* Initialize LDAP class */ + if (PyType_Ready(&LDAP_Type) < 0) { + Py_DECREF(m); + return NULL; + } + + /* Add some symbolic constants to the module */ + d = PyModule_GetDict(m); + + init_pkginfo(m); + + if (LDAPinit_constants(m) == -1) { + return NULL; + } + + LDAPinit_functions(d); + LDAPinit_control(d); + + /* Check for errors */ + if (PyErr_Occurred()) + Py_FatalError("can't initialize module _ldap"); + + return m; } -#endif diff --git a/Modules/message.c b/Modules/message.c index 1a289dbd..f1403237 100644 --- a/Modules/message.c +++ b/Modules/message.c @@ -1,11 +1,6 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "message.h" -#include "berval.h" -#include "errors.h" -#include "ldapcontrol.h" -#include "constants.h" +#include "pythonldap.h" /* * Converts an LDAP message into a Python structure. @@ -22,260 +17,291 @@ * be returned */ PyObject * -LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, int add_intermediates) +LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, + int add_intermediates) { /* we convert an LDAP message into a python structure. * It is always a list of dictionaries. * We always free m. */ - PyObject *result, *pyctrls = 0; - LDAPMessage* entry; - LDAPControl **serverctrls = 0; - int rc; + PyObject *result, *pyctrls = 0; + LDAPMessage *entry; + LDAPControl **serverctrls = 0; + int rc; - result = PyList_New(0); - if (result == NULL) { - ldap_msgfree( m ); - return NULL; - } + result = PyList_New(0); + if (result == NULL) { + ldap_msgfree(m); + return NULL; + } - for(entry = ldap_first_entry(ld,m); - entry != NULL; - entry = ldap_next_entry(ld,entry)) - { - char *dn; - char *attr; - BerElement *ber = NULL; - PyObject* entrytuple; - PyObject* attrdict; - PyObject* pydn; + for (entry = ldap_first_entry(ld, m); + entry != NULL; entry = ldap_next_entry(ld, entry)) { + char *dn; + char *attr; + BerElement *ber = NULL; + PyObject *entrytuple; + PyObject *attrdict; + PyObject *pydn; - dn = ldap_get_dn( ld, entry ); - if (dn == NULL) { - Py_DECREF(result); - ldap_msgfree( m ); - return LDAPerror( ld, "ldap_get_dn" ); - } + dn = ldap_get_dn(ld, entry); + if (dn == NULL) { + Py_DECREF(result); + ldap_msgfree(m); + return LDAPerror(ld); + } - attrdict = PyDict_New(); - if (attrdict == NULL) { - Py_DECREF(result); - ldap_msgfree( m ); - ldap_memfree(dn); - return NULL; - } + attrdict = PyDict_New(); + if (attrdict == NULL) { + Py_DECREF(result); + ldap_msgfree(m); + ldap_memfree(dn); + return NULL; + } - rc = ldap_get_entry_controls( ld, entry, &serverctrls ); - if (rc) { - Py_DECREF(result); - ldap_msgfree( m ); - ldap_memfree(dn); - return LDAPerror( ld, "ldap_get_entry_controls" ); - } + rc = ldap_get_entry_controls(ld, entry, &serverctrls); + if (rc) { + Py_DECREF(result); + ldap_msgfree(m); + ldap_memfree(dn); + return LDAPerror(ld); + } - /* convert serverctrls to list of tuples */ - if ( ! ( pyctrls = LDAPControls_to_List( serverctrls ) ) ) { - int err = LDAP_NO_MEMORY; - ldap_set_option( ld, LDAP_OPT_ERROR_NUMBER, &err ); - Py_DECREF(result); - ldap_msgfree( m ); - ldap_memfree(dn); - ldap_controls_free(serverctrls); - return LDAPerror( ld, "LDAPControls_to_List" ); - } - ldap_controls_free(serverctrls); + /* convert serverctrls to list of tuples */ + if (!(pyctrls = LDAPControls_to_List(serverctrls))) { + int err = LDAP_NO_MEMORY; - /* Fill attrdict with lists */ - for( attr = ldap_first_attribute( ld, entry, &ber ); - attr != NULL; - attr = ldap_next_attribute( ld, entry, ber ) - ) { - PyObject* valuelist; - PyObject* pyattr; - pyattr = PyUnicode_FromString(attr); + ldap_set_option(ld, LDAP_OPT_ERROR_NUMBER, &err); + Py_DECREF(result); + ldap_msgfree(m); + ldap_memfree(dn); + ldap_controls_free(serverctrls); + return LDAPerror(ld); + } + ldap_controls_free(serverctrls); - struct berval ** bvals = - ldap_get_values_len( ld, entry, attr ); + /* Fill attrdict with lists */ + for (attr = ldap_first_attribute(ld, entry, &ber); + attr != NULL; attr = ldap_next_attribute(ld, entry, ber) + ) { + PyObject *valuelist; + PyObject *pyattr; + struct berval **bvals; - /* Find which list to append to */ - if ( PyDict_Contains( attrdict, pyattr ) ) { - valuelist = PyDict_GetItem( attrdict, pyattr ); - } else { - valuelist = PyList_New(0); - if (valuelist != NULL && PyDict_SetItem(attrdict, - pyattr, valuelist) == -1) { - Py_DECREF(valuelist); - valuelist = NULL; /* catch error later */ - } - } + pyattr = PyUnicode_FromString(attr); - if (valuelist == NULL) { - Py_DECREF(pyattr); - Py_DECREF(attrdict); - Py_DECREF(result); - if (ber != NULL) - ber_free(ber, 0); - ldap_msgfree( m ); - ldap_memfree(attr); - ldap_memfree(dn); - Py_XDECREF(pyctrls); - return NULL; - } + bvals = ldap_get_values_len(ld, entry, attr); - if (bvals != NULL) { - Py_ssize_t i; - for (i=0; bvals[i]; i++) { - PyObject *valuestr; + /* Find which list to append to */ + if (PyDict_Contains(attrdict, pyattr)) { + /* Multiple attribute entries with same name. This code path + * is rarely used and cannot be exhausted with OpenLDAP + * tests. 389-DS sometimes triggeres it, see + * https://github.com/python-ldap/python-ldap/issues/218 + */ + valuelist = PyDict_GetItem(attrdict, pyattr); + /* Turn borrowed reference into owned reference */ + if (valuelist != NULL) + Py_INCREF(valuelist); + } + else { + valuelist = PyList_New(0); + if (valuelist != NULL && PyDict_SetItem(attrdict, + pyattr, + valuelist) == -1) { + Py_DECREF(valuelist); + valuelist = NULL; /* catch error later */ + } + } - valuestr = LDAPberval_to_object(bvals[i]); - if (PyList_Append( valuelist, valuestr ) == -1) { - Py_DECREF(pyattr); - Py_DECREF(attrdict); - Py_DECREF(result); - Py_DECREF(valuestr); - Py_DECREF(valuelist); - if (ber != NULL) - ber_free(ber, 0); - ldap_msgfree( m ); - ldap_memfree(attr); - ldap_memfree(dn); - Py_XDECREF(pyctrls); - return NULL; - } - Py_DECREF(valuestr); - } - ldap_value_free_len(bvals); - } - Py_DECREF(pyattr); - Py_DECREF( valuelist ); - ldap_memfree(attr); - } + if (valuelist == NULL) { + Py_DECREF(pyattr); + Py_DECREF(attrdict); + Py_DECREF(result); + if (ber != NULL) + ber_free(ber, 0); + ldap_msgfree(m); + ldap_memfree(attr); + ldap_memfree(dn); + Py_XDECREF(pyctrls); + return NULL; + } - pydn = PyUnicode_FromString(dn); - if (pydn == NULL) { - Py_DECREF(result); - ldap_msgfree( m ); - ldap_memfree(dn); - return NULL; - } + if (bvals != NULL) { + Py_ssize_t i; - if (add_ctrls) { - entrytuple = Py_BuildValue("(OOO)", pydn, attrdict, pyctrls); - } else { - entrytuple = Py_BuildValue("(OO)", pydn, attrdict); - } - Py_DECREF(pydn); - ldap_memfree(dn); - Py_DECREF(attrdict); - Py_XDECREF(pyctrls); - PyList_Append(result, entrytuple); - Py_DECREF(entrytuple); - if (ber != NULL) - ber_free(ber, 0); - } - for(entry = ldap_first_reference(ld,m); - entry != NULL; - entry = ldap_next_reference(ld,entry)) - { - char **refs = NULL; - PyObject* entrytuple; - PyObject* reflist = PyList_New(0); + for (i = 0; bvals[i]; i++) { + PyObject *valuestr; - if (reflist == NULL) { - Py_DECREF(result); - ldap_msgfree( m ); - return NULL; - } - if (ldap_parse_reference(ld, entry, &refs, &serverctrls, 0) != LDAP_SUCCESS) { - Py_DECREF(reflist); - Py_DECREF(result); - ldap_msgfree( m ); - return LDAPerror( ld, "ldap_parse_reference" ); - } - /* convert serverctrls to list of tuples */ - if ( ! ( pyctrls = LDAPControls_to_List( serverctrls ) ) ) { - int err = LDAP_NO_MEMORY; - ldap_set_option( ld, LDAP_OPT_ERROR_NUMBER, &err ); - Py_DECREF(reflist); - Py_DECREF(result); - ldap_msgfree( m ); - ldap_controls_free(serverctrls); - return LDAPerror( ld, "LDAPControls_to_List" ); - } - ldap_controls_free(serverctrls); - if (refs) { - Py_ssize_t i; - for (i=0; refs[i] != NULL; i++) { - /* A referal is a distinguishedName => unicode */ - PyObject *refstr = PyUnicode_FromString(refs[i]); - PyList_Append(reflist, refstr); - Py_DECREF(refstr); - } - ber_memvfree( (void **) refs ); - } - if (add_ctrls) { - entrytuple = Py_BuildValue("(sOO)", NULL, reflist, pyctrls); - } else { - entrytuple = Py_BuildValue("(sO)", NULL, reflist); - } - Py_DECREF(reflist); - Py_XDECREF(pyctrls); - PyList_Append(result, entrytuple); - Py_DECREF(entrytuple); - } - if (add_intermediates) { - for(entry = ldap_first_message(ld,m); - entry != NULL; - entry = ldap_next_message(ld,entry)) - { - /* list of tuples */ - /* each tuple is OID, Berval, controllist */ - if ( LDAP_RES_INTERMEDIATE == ldap_msgtype( entry ) ) { - PyObject* valtuple; - PyObject *valuestr; - char *retoid = 0; - PyObject *pyoid; - struct berval *retdata = 0; + valuestr = LDAPberval_to_object(bvals[i]); + if (PyList_Append(valuelist, valuestr) == -1) { + Py_DECREF(pyattr); + Py_DECREF(attrdict); + Py_DECREF(result); + Py_DECREF(valuestr); + Py_DECREF(valuelist); + if (ber != NULL) + ber_free(ber, 0); + ldap_msgfree(m); + ldap_memfree(attr); + ldap_memfree(dn); + Py_XDECREF(pyctrls); + return NULL; + } + Py_DECREF(valuestr); + } + ldap_value_free_len(bvals); + } + Py_DECREF(pyattr); + Py_DECREF(valuelist); + ldap_memfree(attr); + } - if (ldap_parse_intermediate( ld, entry, &retoid, &retdata, &serverctrls, 0 ) != LDAP_SUCCESS) { - Py_DECREF(result); - ldap_msgfree( m ); - return LDAPerror( ld, "ldap_parse_intermediate" ); - } - /* convert serverctrls to list of tuples */ - if ( ! ( pyctrls = LDAPControls_to_List( serverctrls ) ) ) { - int err = LDAP_NO_MEMORY; - ldap_set_option( ld, LDAP_OPT_ERROR_NUMBER, &err ); - Py_DECREF(result); - ldap_msgfree( m ); - ldap_controls_free(serverctrls); - ldap_memfree( retoid ); - ber_bvfree( retdata ); - return LDAPerror( ld, "LDAPControls_to_List" ); - } - ldap_controls_free(serverctrls); + pydn = PyUnicode_FromString(dn); + if (pydn == NULL) { + Py_DECREF(result); + ldap_msgfree(m); + ldap_memfree(dn); + return NULL; + } - valuestr = LDAPberval_to_object(retdata); - ber_bvfree( retdata ); - pyoid = PyUnicode_FromString(retoid); - ldap_memfree( retoid ); - if (pyoid == NULL) { - 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); - } - } - } - ldap_msgfree( m ); - return result; + if (add_ctrls) { + entrytuple = Py_BuildValue("(OOO)", pydn, attrdict, pyctrls); + } + else { + entrytuple = Py_BuildValue("(OO)", pydn, attrdict); + } + Py_DECREF(pydn); + ldap_memfree(dn); + Py_DECREF(attrdict); + Py_XDECREF(pyctrls); + PyList_Append(result, entrytuple); + Py_DECREF(entrytuple); + if (ber != NULL) + ber_free(ber, 0); + } + for (entry = ldap_first_reference(ld, m); + entry != NULL; entry = ldap_next_reference(ld, entry)) { + char **refs = NULL; + PyObject *entrytuple; + PyObject *reflist = PyList_New(0); + + if (reflist == NULL) { + Py_DECREF(result); + ldap_msgfree(m); + return NULL; + } + if (ldap_parse_reference(ld, entry, &refs, &serverctrls, 0) != + LDAP_SUCCESS) { + Py_DECREF(reflist); + Py_DECREF(result); + ldap_msgfree(m); + return LDAPerror(ld); + } + /* convert serverctrls to list of tuples */ + if (!(pyctrls = LDAPControls_to_List(serverctrls))) { + int err = LDAP_NO_MEMORY; + + ldap_set_option(ld, LDAP_OPT_ERROR_NUMBER, &err); + Py_DECREF(reflist); + Py_DECREF(result); + ldap_msgfree(m); + ldap_controls_free(serverctrls); + return LDAPerror(ld); + } + ldap_controls_free(serverctrls); + if (refs) { + Py_ssize_t i; + + for (i = 0; refs[i] != NULL; i++) { + /* A referal is a distinguishedName => unicode */ + PyObject *refstr = PyUnicode_FromString(refs[i]); + + PyList_Append(reflist, refstr); + Py_DECREF(refstr); + } + ber_memvfree((void **)refs); + } + if (add_ctrls) { + entrytuple = Py_BuildValue("(sOO)", NULL, reflist, pyctrls); + } + else { + entrytuple = Py_BuildValue("(sO)", NULL, reflist); + } + Py_DECREF(reflist); + Py_XDECREF(pyctrls); + PyList_Append(result, entrytuple); + Py_DECREF(entrytuple); + } + if (add_intermediates) { + for (entry = ldap_first_message(ld, m); + entry != NULL; entry = ldap_next_message(ld, entry)) { + /* list of tuples */ + /* each tuple is OID, Berval, controllist */ + if (LDAP_RES_INTERMEDIATE == ldap_msgtype(entry)) { + PyObject *valtuple; + PyObject *valuestr; + char *retoid = 0; + PyObject *pyoid; + struct berval *retdata = 0; + + if (ldap_parse_intermediate + (ld, entry, &retoid, &retdata, &serverctrls, + 0) != LDAP_SUCCESS) { + Py_DECREF(result); + ldap_msgfree(m); + return LDAPerror(ld); + } + /* convert serverctrls to list of tuples */ + if (!(pyctrls = LDAPControls_to_List(serverctrls))) { + int err = LDAP_NO_MEMORY; + + ldap_set_option(ld, LDAP_OPT_ERROR_NUMBER, &err); + Py_DECREF(result); + ldap_msgfree(m); + ldap_controls_free(serverctrls); + ldap_memfree(retoid); + ber_bvfree(retdata); + 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; + } + Py_DECREF(valtuple); + } + } + } + ldap_msgfree(m); + return result; } diff --git a/Modules/message.h b/Modules/message.h deleted file mode 100644 index c3522ac3..00000000 --- a/Modules/message.h +++ /dev/null @@ -1,12 +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 7cf996bf..4577b075 100644 --- a/Modules/options.c +++ b/Modules/options.c @@ -1,15 +1,12 @@ /* See https://www.python-ldap.org/ for details. */ -#include "common.h" -#include "errors.h" -#include "LDAPObject.h" -#include "ldapcontrol.h" -#include "options.h" +#include "pythonldap.h" void -set_timeval_from_double( struct timeval *tv, double d ) { - tv->tv_usec = (long) ( fmod(d, 1.0) * 1000000.0 ); - tv->tv_sec = (long) floor(d); +set_timeval_from_double(struct timeval *tv, double d) +{ + tv->tv_usec = (long)(fmod(d, 1.0) * 1000000.0); + tv->tv_sec = (long)floor(d); } /** @@ -23,7 +20,7 @@ option_error(int res, const char *fn) PyErr_SetString(PyExc_ValueError, "option error"); else if (res == LDAP_PARAM_ERROR) PyErr_SetString(PyExc_ValueError, "parameter error"); - else if (res == LDAP_NO_MEMORY) + else if (res == LDAP_NO_MEMORY) PyErr_NoMemory(); else PyErr_Format(PyExc_SystemError, "error %d from %s", res, fn); @@ -39,24 +36,33 @@ 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; ld = self ? self->ldap : NULL; - switch(option) { + 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 - /* Read-only options */ - PyErr_SetString(PyExc_ValueError, "read-only option"); - return 0; +#ifdef LDAP_OPT_X_TLS_PEERCERT + case LDAP_OPT_X_TLS_PEERCERT: +#endif + /* Read-only options */ + PyErr_SetString(PyExc_ValueError, "read-only option"); + return 0; case LDAP_OPT_REFERRALS: case LDAP_OPT_RESTART: #ifdef LDAP_OPT_X_SASL_NOCANON @@ -65,9 +71,9 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) #ifdef LDAP_OPT_CONNECT_ASYNC case LDAP_OPT_CONNECT_ASYNC: #endif - /* Truth-value options */ - ptr = PyObject_IsTrue(value) ? LDAP_OPT_ON : LDAP_OPT_OFF; - break; + /* Truth-value options */ + ptr = PyObject_IsTrue(value) ? LDAP_OPT_ON : LDAP_OPT_OFF; + break; case LDAP_OPT_DEREF: case LDAP_OPT_SIZELIMIT: @@ -87,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: @@ -102,11 +110,31 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) case LDAP_OPT_X_KEEPALIVE_INTERVAL: #endif - /* integer value options */ - if (!PyArg_Parse(value, "i:set_option", &intval)) - return 0; - ptr = &intval; - break; + /* integer value options */ + if (!PyArg_Parse(value, "i:set_option", &intval)) + 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 @@ -125,45 +153,89 @@ 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 - /* String valued options */ - if (!PyArg_Parse(value, "s:set_option", &strval)) - return 0; - ptr = strval; - break; +#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 */ - if (!PyArg_Parse(value, "d:set_option", &doubleval)) - return 0; - if (doubleval >= 0) { - set_timeval_from_double( &tv, doubleval ); - ptr = &tv; - } else { - ptr = NULL; + /* Float valued timeval options */ + if (value == Py_None) { + /* None is mapped to infinity timeout */ + doubleval = -1; + } + else { + /* 'd' handles int/long */ + if (!PyArg_Parse(value, "d:set_option", &doubleval)) { + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + /* TypeError: mention either float or None is expected */ + PyErr_Clear(); + PyErr_Format(PyExc_TypeError, + "A float or None is expected for timeout, got %.100s", + Py_TYPE(value)->tp_name); + } + return 0; } - break; + } + + if (doubleval >= 0) { + set_timeval_from_double(&tv, doubleval); + ptr = &tv; + } + else if (doubleval == -1) { + /* -1 is infinity timeout */ + tv.tv_sec = -1; + tv.tv_usec = 0; + ptr = &tv; + } + else { + PyErr_Format(PyExc_ValueError, + "timeout must be >= 0 or -1/None for infinity, got %S", + value); + return 0; + } + break; + case LDAP_OPT_SERVER_CONTROLS: case LDAP_OPT_CLIENT_CONTROLS: - if (!LDAPControls_from_object(value, &controls)) - return 0; - ptr = controls; - break; + if (!LDAPControls_from_object(value, &controls)) + return 0; + ptr = controls; + break; default: - PyErr_Format(PyExc_ValueError, "unknown option %d", option); - return 0; + PyErr_Format(PyExc_ValueError, "unknown option %d", option); + return 0; + } + + if (self) { + LDAP_BEGIN_ALLOW_THREADS(self); + res = ldap_set_option(ld, option, ptr); + LDAP_END_ALLOW_THREADS(self); } - - if (self) LDAP_BEGIN_ALLOW_THREADS(self); - res = ldap_set_option(ld, option, ptr); - if (self) LDAP_END_ALLOW_THREADS(self); + else { + PyThreadState *save; - if ((option == LDAP_OPT_SERVER_CONTROLS) || (option == LDAP_OPT_CLIENT_CONTROLS)) + save = PyEval_SaveThread(); + res = ldap_set_option(NULL, option, ptr); + PyEval_RestoreThread(save); + } + + if ((option == LDAP_OPT_SERVER_CONTROLS) || + (option == LDAP_OPT_CLIENT_CONTROLS)) LDAPControl_List_DEL(controls); - + if (res != LDAP_OPT_SUCCESS) { option_error(res, "ldap_set_option"); return 0; @@ -172,61 +244,86 @@ 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; - LDAPControl *lc; char *strval; - PyObject *extensions, *v, *tup; - Py_ssize_t i, num_extensions, num_controls; - LDAP *ld; - - ld = self ? self->ldap : NULL; - - switch(option) { - 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); - if (res != LDAP_OPT_SUCCESS) - return option_error(res, "ldap_get_option"); - - /* put the extensions into tuple form */ - num_extensions = 0; - while (apiinfo.ldapai_extensions[num_extensions]) - num_extensions++; - extensions = PyTuple_New(num_extensions); - for (i = 0; i < num_extensions; i++) - PyTuple_SET_ITEM(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}", - "info_version", apiinfo.ldapai_info_version, - "api_version", apiinfo.ldapai_api_version, - "protocol_version", apiinfo.ldapai_protocol_version, - "vendor_name", apiinfo.ldapai_vendor_name, - "vendor_version", apiinfo.ldapai_vendor_version, - "extensions", extensions); - - if (apiinfo.ldapai_vendor_name) - ldap_memfree(apiinfo.ldapai_vendor_name); - for (i = 0; i < num_extensions; i++) - ldap_memfree(apiinfo.ldapai_extensions[i]); - ldap_memfree(apiinfo.ldapai_extensions); - Py_DECREF(extensions); - - return v; + struct berval berbytes; +#if HAVE_SASL + /* unsigned long */ + ber_len_t blen; +#endif + PyObject *extensions, *v; + Py_ssize_t i, num_extensions; + switch (option) { #ifdef HAVE_SASL - case LDAP_OPT_X_SASL_SSF: + 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; + res = LDAP_int_get_option(self, option, &apiinfo); + if (res != LDAP_OPT_SUCCESS) + return option_error(res, "ldap_get_option"); + + /* put the extensions into tuple form */ + num_extensions = 0; + while (apiinfo.ldapai_extensions[num_extensions]) + num_extensions++; + extensions = PyTuple_New(num_extensions); + for (i = 0; i < num_extensions; i++) + PyTuple_SET_ITEM(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}", + "info_version", apiinfo.ldapai_info_version, + "api_version", apiinfo.ldapai_api_version, + "protocol_version", apiinfo.ldapai_protocol_version, + "vendor_name", apiinfo.ldapai_vendor_name, + "vendor_version", apiinfo.ldapai_vendor_version, + "extensions", extensions); + + if (apiinfo.ldapai_vendor_name) + ldap_memfree(apiinfo.ldapai_vendor_name); + for (i = 0; i < num_extensions; i++) + ldap_memfree(apiinfo.ldapai_extensions[i]); + ldap_memfree(apiinfo.ldapai_extensions); + Py_DECREF(extensions); + + return v; + case LDAP_OPT_REFERRALS: case LDAP_OPT_RESTART: case LDAP_OPT_DEREF: @@ -245,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: @@ -265,13 +364,32 @@ LDAP_get_option(LDAPObject *self, int option) #ifdef LDAP_OPT_X_KEEPALIVE_INTERVAL 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); - if (res != LDAP_OPT_SUCCESS) - return option_error(res, "ldap_get_option"); - return PyInt_FromLong(intval); + /* Integer-valued options */ + 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 PyLong_FromUnsignedLong(blen); +#endif case LDAP_OPT_HOST_NAME: case LDAP_OPT_URI: @@ -300,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: @@ -311,72 +431,62 @@ LDAP_get_option(LDAPObject *self, int option) case LDAP_OPT_X_SASL_USERNAME: #endif #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); - if (res != LDAP_OPT_SUCCESS) - return option_error(res, "ldap_get_option"); - if (strval == NULL) { - Py_INCREF(Py_None); - return Py_None; - } - v = PyUnicode_FromString(strval); - ldap_memfree(strval); - return v; +#ifdef LDAP_OPT_SOCKET_BIND_ADDRESSES + case LDAP_OPT_SOCKET_BIND_ADDRESSES: +#endif + /* String-valued options */ + res = LDAP_int_get_option(self, option, &strval); + if (res != LDAP_OPT_SUCCESS) + return option_error(res, "ldap_get_option"); + if (strval == NULL) { + Py_INCREF(Py_None); + return Py_None; + } + v = PyUnicode_FromString(strval); + 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); - if (res != LDAP_OPT_SUCCESS) - return option_error(res, "ldap_get_option"); - if (tv == NULL) { - Py_INCREF(Py_None); - return Py_None; - } - v = PyFloat_FromDouble( - (double) tv->tv_sec + ( (double) tv->tv_usec / 1000000.0 ) + /* Double-valued timeval options */ + res = LDAP_int_get_option(self, option, &tv); + if (res != LDAP_OPT_SUCCESS) + return option_error(res, "ldap_get_option"); + if (tv == NULL) { + Py_INCREF(Py_None); + return Py_None; + } + v = PyFloat_FromDouble((double)tv->tv_sec + + ((double)tv->tv_usec / 1000000.0) ); - ldap_memfree(tv); - return v; + ldap_memfree(tv); + return v; 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); - - if (res != LDAP_OPT_SUCCESS) - return option_error(res, "ldap_get_option"); - - if (lcs == NULL) - return PyList_New(0); - - /* Get the number of controls */ - num_controls = 0; - while (lcs[num_controls]) - num_controls++; - - /* We'll build a list of controls, with each control a tuple */ - v = PyList_New(num_controls); - for (i = 0; i < num_controls; i++) { - lc = lcs[i]; - tup = Py_BuildValue("(sbs)", - lc->ldctl_oid, - lc->ldctl_iscritical, - lc->ldctl_value.bv_val); - PyList_SET_ITEM(v, i, tup); - } - - ldap_controls_free(lcs); + res = LDAP_int_get_option(self, option, &lcs); + if (res != LDAP_OPT_SUCCESS) + return option_error(res, "ldap_get_option"); + + v = LDAPControls_to_List(lcs); + ldap_controls_free(lcs); + return v; - return v; - default: - PyErr_Format(PyExc_ValueError, "unknown option %d", option); - return NULL; + PyErr_Format(PyExc_ValueError, "unknown option %d", option); + return NULL; } } diff --git a/Modules/options.h b/Modules/options.h deleted file mode 100644 index 570fdc15..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 2a88bf27..8045555d 100644 --- a/README +++ b/README @@ -14,7 +14,7 @@ stuff (e.g. processing LDIF, LDAPURLs, LDAPv3 sub-schema, etc.). Not included: Direct BER support -See INSTALL for version compability +See INSTALL for version compatibility See TODO for planned features. Contributors welcome. @@ -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 8ceb63b6..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 @@ -22,3 +20,5 @@ from . import t_ldap_schema_subentry 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/ldif/subschema-ipa.demo1.freeipa.org.ldif b/Tests/data/subschema-ipa.demo1.freeipa.org.ldif similarity index 100% rename from Tests/ldif/subschema-ipa.demo1.freeipa.org.ldif rename to Tests/data/subschema-ipa.demo1.freeipa.org.ldif diff --git a/Tests/ldif/subschema-openldap-all.ldif b/Tests/data/subschema-openldap-all.ldif similarity index 100% rename from Tests/ldif/subschema-openldap-all.ldif rename to Tests/data/subschema-openldap-all.ldif diff --git a/Tests/t_bind.py b/Tests/t_bind.py index 290deb6d..ba90c4cd 100644 --- a/Tests/t_bind.py +++ b/Tests/t_bind.py @@ -1,34 +1,19 @@ -from __future__ import unicode_literals +import os +import unittest -import sys +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' -if sys.version_info[0] <= 2: - PY2 = True - text_type = unicode -else: - PY2 = False - text_type = str - -import ldap, unittest -from slapdtest import SlapdObject +import ldap from ldap.ldapobject import LDAPObject +from slapdtest import SlapdTestCase -class TestBinds(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.server = SlapdObject() - cls.server.start() - - cls.unicode_val = "abc\U0001f498def" - cls.unicode_val_bytes = cls.unicode_val.encode('utf-8') - cls.dn_unicode = "CN=" + cls.unicode_val - cls.dn_bytes = cls.dn_unicode.encode('utf-8') - - @classmethod - def tearDownClass(cls): - cls.server.stop() +class TestBinds(SlapdTestCase): + unicode_val = "abc\U0001f498def" + unicode_val_bytes = unicode_val.encode('utf-8') + dn_unicode = "CN=" + unicode_val + dn_bytes = dn_unicode.encode('utf-8') def _get_ldapobject(self, bytes_mode=None): l = LDAPObject(self.server.ldap_uri, bytes_mode=bytes_mode) @@ -48,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 d4740d0c..846127f8 100644 --- a/Tests/t_cext.py +++ b/Tests/t_cext.py @@ -1,21 +1,20 @@ -# -*- 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 -from slapdtest import SlapdTestCase # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' # import the plain C wrapper module import _ldap +from slapdtest import SlapdTestCase, requires_tls, requires_init_fd class TestLdapCExtension(SlapdTestCase): @@ -28,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( @@ -51,25 +50,74 @@ def setUpClass(cls): ]) ) + def setUp(self): + 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().tearDown() + + @property + def writesuffix(self): + """Initialize writesuffix on demand + + Creates a clean subtree for tests that write to slapd. ldapdelete + is not able to delete a Root DSE, therefore we need a temporary + work space. + + :return: DN + """ + if self._writesuffix is not None: + return self._writesuffix + self._writesuffix = 'ou=write tests,%s' % self.server.suffix + # Add writeable subtree + self.server.ldapadd( + "\n".join([ + 'dn: ' + self._writesuffix, + 'objectClass: organizationalUnit', + 'ou:' + self._writesuffix.split(',')[0][3:], + '' + ]) + ) + return self._writesuffix + def _open_conn(self, bind=True): """ Starts a server, and returns a LDAPObject bound to it """ 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 - def assertNotNone(self, expr, msg=None): - self.assertFalse(expr is None, msg or repr(expr)) + @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)) - def assertNone(self, expr, msg=None): - self.assertFalse(expr is not None, msg or repr(expr)) # Test for the existence of a whole bunch of constants # that the C module is supposed to export @@ -94,102 +142,167 @@ def test_constants(self): self.assertEqual(_ldap.RES_SEARCH_REFERENCE, 0x73) # v3 self.assertEqual(_ldap.RES_EXTENDED, 0x78) # v3 #self.assertEqual(_ldap.RES_INTERMEDIATE, 0x79) # v3 - self.assertNotNone(_ldap.RES_ANY) - self.assertNotNone(_ldap.RES_UNSOLICITED) + self.assertIsNotNone(_ldap.RES_ANY) + self.assertIsNotNone(_ldap.RES_UNSOLICITED) - self.assertNotNone(_ldap.AUTH_NONE) - self.assertNotNone(_ldap.AUTH_SIMPLE) + self.assertIsNotNone(_ldap.AUTH_NONE) + self.assertIsNotNone(_ldap.AUTH_SIMPLE) - self.assertNotNone(_ldap.SCOPE_BASE) - self.assertNotNone(_ldap.SCOPE_ONELEVEL) - self.assertNotNone(_ldap.SCOPE_SUBTREE) + self.assertIsNotNone(_ldap.SCOPE_BASE) + self.assertIsNotNone(_ldap.SCOPE_ONELEVEL) + self.assertIsNotNone(_ldap.SCOPE_SUBTREE) - self.assertNotNone(_ldap.MOD_ADD) - self.assertNotNone(_ldap.MOD_DELETE) - self.assertNotNone(_ldap.MOD_REPLACE) - self.assertNotNone(_ldap.MOD_INCREMENT) - self.assertNotNone(_ldap.MOD_BVALUES) + self.assertIsNotNone(_ldap.MOD_ADD) + self.assertIsNotNone(_ldap.MOD_DELETE) + self.assertIsNotNone(_ldap.MOD_REPLACE) + self.assertIsNotNone(_ldap.MOD_INCREMENT) + self.assertIsNotNone(_ldap.MOD_BVALUES) # for result4() - self.assertNotNone(_ldap.MSG_ONE) - self.assertNotNone(_ldap.MSG_ALL) - self.assertNotNone(_ldap.MSG_RECEIVED) + self.assertIsNotNone(_ldap.MSG_ONE) + self.assertIsNotNone(_ldap.MSG_ALL) + self.assertIsNotNone(_ldap.MSG_RECEIVED) # for OPT_DEFEF - self.assertNotNone(_ldap.DEREF_NEVER) - self.assertNotNone(_ldap.DEREF_SEARCHING) - self.assertNotNone(_ldap.DEREF_FINDING) - self.assertNotNone(_ldap.DEREF_ALWAYS) + self.assertIsNotNone(_ldap.DEREF_NEVER) + self.assertIsNotNone(_ldap.DEREF_SEARCHING) + self.assertIsNotNone(_ldap.DEREF_FINDING) + self.assertIsNotNone(_ldap.DEREF_ALWAYS) # for OPT_SIZELIMIT, OPT_TIMELIMIT - self.assertNotNone(_ldap.NO_LIMIT) + self.assertIsNotNone(_ldap.NO_LIMIT) # standard options - self.assertNotNone(_ldap.OPT_API_INFO) - self.assertNotNone(_ldap.OPT_DEREF) - self.assertNotNone(_ldap.OPT_SIZELIMIT) - self.assertNotNone(_ldap.OPT_TIMELIMIT) - self.assertNotNone(_ldap.OPT_REFERRALS) - self.assertNotNone(_ldap.OPT_RESTART) - self.assertNotNone(_ldap.OPT_PROTOCOL_VERSION) - self.assertNotNone(_ldap.OPT_SERVER_CONTROLS) - self.assertNotNone(_ldap.OPT_CLIENT_CONTROLS) - self.assertNotNone(_ldap.OPT_API_FEATURE_INFO) - self.assertNotNone(_ldap.OPT_HOST_NAME) - self.assertNotNone(_ldap.OPT_ERROR_NUMBER) # = OPT_RESULT_CODE - self.assertNotNone(_ldap.OPT_ERROR_STRING) # = OPT_DIAGNOSITIC_MESSAGE - self.assertNotNone(_ldap.OPT_MATCHED_DN) + self.assertIsNotNone(_ldap.OPT_API_INFO) + self.assertIsNotNone(_ldap.OPT_DEREF) + self.assertIsNotNone(_ldap.OPT_SIZELIMIT) + self.assertIsNotNone(_ldap.OPT_TIMELIMIT) + self.assertIsNotNone(_ldap.OPT_REFERRALS) + self.assertIsNotNone(_ldap.OPT_RESTART) + self.assertIsNotNone(_ldap.OPT_PROTOCOL_VERSION) + self.assertIsNotNone(_ldap.OPT_SERVER_CONTROLS) + self.assertIsNotNone(_ldap.OPT_CLIENT_CONTROLS) + self.assertIsNotNone(_ldap.OPT_API_FEATURE_INFO) + self.assertIsNotNone(_ldap.OPT_HOST_NAME) + self.assertIsNotNone(_ldap.OPT_ERROR_NUMBER) # = OPT_RESULT_CODE + self.assertIsNotNone(_ldap.OPT_ERROR_STRING) # = OPT_DIAGNOSITIC_MESSAGE + self.assertIsNotNone(_ldap.OPT_MATCHED_DN) # OpenLDAP specific - self.assertNotNone(_ldap.OPT_DEBUG_LEVEL) - self.assertNotNone(_ldap.OPT_TIMEOUT) - self.assertNotNone(_ldap.OPT_REFHOPLIMIT) - self.assertNotNone(_ldap.OPT_NETWORK_TIMEOUT) - self.assertNotNone(_ldap.OPT_URI) - #self.assertNotNone(_ldap.OPT_REFERRAL_URLS) - #self.assertNotNone(_ldap.OPT_SOCKBUF) - #self.assertNotNone(_ldap.OPT_DEFBASE) - #self.assertNotNone(_ldap.OPT_CONNECT_ASYNC) + self.assertIsNotNone(_ldap.OPT_DEBUG_LEVEL) + self.assertIsNotNone(_ldap.OPT_TIMEOUT) + self.assertIsNotNone(_ldap.OPT_REFHOPLIMIT) + self.assertIsNotNone(_ldap.OPT_NETWORK_TIMEOUT) + self.assertIsNotNone(_ldap.OPT_URI) + #self.assertIsNotNone(_ldap.OPT_REFERRAL_URLS) + #self.assertIsNotNone(_ldap.OPT_SOCKBUF) + #self.assertIsNotNone(_ldap.OPT_DEFBASE) + #self.assertIsNotNone(_ldap.OPT_CONNECT_ASYNC) # str2dn() - self.assertNotNone(_ldap.DN_FORMAT_LDAP) - self.assertNotNone(_ldap.DN_FORMAT_LDAPV3) - self.assertNotNone(_ldap.DN_FORMAT_LDAPV2) - self.assertNotNone(_ldap.DN_FORMAT_DCE) - self.assertNotNone(_ldap.DN_FORMAT_UFN) - self.assertNotNone(_ldap.DN_FORMAT_AD_CANONICAL) - self.assertNotNone(_ldap.DN_FORMAT_MASK) - self.assertNotNone(_ldap.DN_PRETTY) - self.assertNotNone(_ldap.DN_SKIP) - self.assertNotNone(_ldap.DN_P_NOLEADTRAILSPACES) - self.assertNotNone(_ldap.DN_P_NOSPACEAFTERRDN) - self.assertNotNone(_ldap.DN_PEDANTIC) - self.assertNotNone(_ldap.AVA_NULL) - self.assertNotNone(_ldap.AVA_STRING) - self.assertNotNone(_ldap.AVA_BINARY) - self.assertNotNone(_ldap.AVA_NONPRINTABLE) + self.assertIsNotNone(_ldap.DN_FORMAT_LDAP) + self.assertIsNotNone(_ldap.DN_FORMAT_LDAPV3) + self.assertIsNotNone(_ldap.DN_FORMAT_LDAPV2) + self.assertIsNotNone(_ldap.DN_FORMAT_DCE) + self.assertIsNotNone(_ldap.DN_FORMAT_UFN) + self.assertIsNotNone(_ldap.DN_FORMAT_AD_CANONICAL) + self.assertIsNotNone(_ldap.DN_FORMAT_MASK) + self.assertIsNotNone(_ldap.DN_PRETTY) + self.assertIsNotNone(_ldap.DN_SKIP) + self.assertIsNotNone(_ldap.DN_P_NOLEADTRAILSPACES) + self.assertIsNotNone(_ldap.DN_P_NOSPACEAFTERRDN) + self.assertIsNotNone(_ldap.DN_PEDANTIC) + self.assertIsNotNone(_ldap.AVA_NULL) + self.assertIsNotNone(_ldap.AVA_STRING) + self.assertIsNotNone(_ldap.AVA_BINARY) + self.assertIsNotNone(_ldap.AVA_NONPRINTABLE) # these two constants are pointless? XXX self.assertEqual(_ldap.OPT_ON, 1) self.assertEqual(_ldap.OPT_OFF, 0) # these constants useless after ldap_url_parse() was dropped XXX - self.assertNotNone(_ldap.URL_ERR_BADSCOPE) - self.assertNotNone(_ldap.URL_ERR_MEM) + self.assertIsNotNone(_ldap.URL_ERR_BADSCOPE) + self.assertIsNotNone(_ldap.URL_ERR_MEM) + + def test_test_flags(self): + # test flag, see slapdtest and tox.ini + disabled = os.environ.get('CI_DISABLED') + if not disabled: + self.skipTest("No CI_DISABLED env var") + disabled = set(disabled.split(':')) + if 'TLS' in disabled: + self.assertFalse(_ldap.TLS_AVAIL) + else: + self.assertTrue(_ldap.TLS_AVAIL) + if 'SASL' in disabled: + self.assertFalse(_ldap.SASL_AVAIL) + else: + 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("", "") self.assertEqual(type(m), type(0)) result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) - self.assertTrue(result, _ldap.RES_BIND) + self.assertEqual(result, _ldap.RES_BIND) self.assertEqual(msgid, m) 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) @@ -197,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) @@ -214,7 +327,7 @@ def test_anon_rootdse_search(self): def test_unbind(self): l = self._open_conn() m = l.unbind_ext() - self.assertNone(m) + self.assertIsNone(m) # Second attempt to unbind should yield an exception try: l.unbind_ext() @@ -252,7 +365,7 @@ def test_abandon(self): l = self._open_conn() m = l.search_ext(self.server.suffix, _ldap.SCOPE_SUBTREE, '(objectClass=*)') ret = l.abandon_ext(m) - self.assertNone(ret) + self.assertIsNone(ret) try: r = l.result4(m, _ldap.MSG_ALL, 0.3) # (timeout /could/ be longer) except _ldap.TIMEOUT as e: @@ -272,13 +385,20 @@ def test_search_ext_all(self): self.assertEqual(msgid, m) self.assertEqual(ctrls, []) + def test_invalid_search_filter(self): + l = self._open_conn() + with self.assertRaises(_ldap.FILTER_ERROR): + l.search_ext( + self.server.suffix, _ldap.SCOPE_SUBTREE, 'bogus filter expr' + ) + def test_add(self): """ test add operation """ l = self._open_conn() m = l.add_ext( - "cn=Foo," + self.server.suffix, + "cn=Foo," + self.writesuffix, [ ('objectClass', b'organizationalRole'), ('cn', b'Foo'), @@ -292,7 +412,7 @@ def test_add(self): self.assertEqual(msgid, m) self.assertEqual(ctrls, []) # search for it back - m = l.search_ext(self.server.suffix, _ldap.SCOPE_SUBTREE, '(cn=Foo)') + m = l.search_ext(self.writesuffix, _ldap.SCOPE_SUBTREE, '(cn=Foo)') result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) # Expect to get the objects self.assertEqual(result, _ldap.RES_SEARCH_RESULT) @@ -302,7 +422,7 @@ def test_add(self): self.assertEqual( pmsg[0], ( - 'cn=Foo,'+self.server.suffix, + 'cn=Foo,'+self.writesuffix, { 'objectClass': [b'organizationalRole'], 'cn': [b'Foo'], @@ -317,7 +437,7 @@ def test_compare(self): """ l = self._open_conn() # first, add an object with a field we can compare on - dn = "cn=CompareTest," + self.server.suffix + dn = "cn=CompareTest," + self.writesuffix m = l.add_ext( dn, [ @@ -330,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): """ @@ -371,7 +497,7 @@ def test_delete_no_such_object(self): def test_delete(self): l = self._open_conn() # first, add an object we will delete - dn = "cn=Deleteme,"+self.server.suffix + dn = "cn=Deleteme,"+self.writesuffix m = l.add_ext( dn, [ @@ -395,7 +521,7 @@ def test_modify_no_such_object(self): # try deleting an object that doesn't exist m = l.modify_ext( - "cn=DoesNotExist,"+self.server.suffix, + "cn=DoesNotExist,"+self.writesuffix, [ (_ldap.MOD_ADD, 'description', [b'blah']), ] @@ -432,7 +558,7 @@ def test_modify(self): """ l = self._open_conn() # first, add an object we will delete - dn = "cn=AddToMe,"+self.server.suffix + dn = "cn=AddToMe,"+self.writesuffix m = l.add_ext( dn, [ @@ -458,7 +584,7 @@ def test_modify(self): self.assertEqual(msgid, m) self.assertEqual(ctrls, []) # search for it back - m = l.search_ext(self.server.suffix, _ldap.SCOPE_SUBTREE, '(cn=AddToMe)') + m = l.search_ext(self.writesuffix, _ldap.SCOPE_SUBTREE, '(cn=AddToMe)') result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) # Expect to get the objects self.assertEqual(result, _ldap.RES_SEARCH_RESULT) @@ -472,7 +598,7 @@ def test_modify(self): def test_rename(self): l = self._open_conn() - dn = "cn=RenameMe,"+self.server.suffix + dn = "cn=RenameMe,"+self.writesuffix m = l.add_ext( dn, [ @@ -493,7 +619,7 @@ def test_rename(self): self.assertEqual(ctrls, []) # make sure the old one is gone - m = l.search_ext(self.server.suffix, _ldap.SCOPE_SUBTREE, '(cn=RenameMe)') + m = l.search_ext(self.writesuffix, _ldap.SCOPE_SUBTREE, '(cn=RenameMe)') result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) self.assertEqual(result, _ldap.RES_SEARCH_RESULT) self.assertEqual(len(pmsg), 0) # expect no results @@ -501,8 +627,8 @@ def test_rename(self): self.assertEqual(ctrls, []) # check that the new one looks right - dn2 = "cn=IAmRenamed,"+self.server.suffix - m = l.search_ext(self.server.suffix, _ldap.SCOPE_SUBTREE, '(cn=IAmRenamed)') + dn2 = "cn=IAmRenamed,"+self.writesuffix + m = l.search_ext(self.writesuffix, _ldap.SCOPE_SUBTREE, '(cn=IAmRenamed)') result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) self.assertEqual(result, _ldap.RES_SEARCH_RESULT) self.assertEqual(msgid, m) @@ -512,7 +638,7 @@ def test_rename(self): self.assertEqual(pmsg[0][1]['cn'], [b'IAmRenamed']) # create the container - containerDn = "ou=RenameContainer,"+self.server.suffix + containerDn = "ou=RenameContainer,"+self.writesuffix m = l.add_ext( containerDn, [ @@ -535,18 +661,18 @@ def test_rename(self): self.assertEqual(ctrls, []) # make sure dn2 is gone - m = l.search_ext(self.server.suffix, _ldap.SCOPE_SUBTREE, '(cn=IAmRenamed)') + m = l.search_ext(self.writesuffix, _ldap.SCOPE_SUBTREE, '(cn=IAmRenamed)') result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) self.assertEqual(result, _ldap.RES_SEARCH_RESULT) self.assertEqual(len(pmsg), 0) # expect no results self.assertEqual(msgid, m) self.assertEqual(ctrls, []) - m = l.search_ext(self.server.suffix, _ldap.SCOPE_SUBTREE, '(objectClass=*)') + m = l.search_ext(self.writesuffix, _ldap.SCOPE_SUBTREE, '(objectClass=*)') result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) # make sure dn3 is there - m = l.search_ext(self.server.suffix, _ldap.SCOPE_SUBTREE, '(cn=IAmRenamedAgain)') + m = l.search_ext(self.writesuffix, _ldap.SCOPE_SUBTREE, '(cn=IAmRenamedAgain)') result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) self.assertEqual(result, _ldap.RES_SEARCH_RESULT) self.assertEqual(msgid, m) @@ -573,7 +699,7 @@ def test_whoami_anonymous(self): # Anonymous bind m = l.simple_bind("", "") result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) - self.assertTrue(result, _ldap.RES_BIND) + self.assertEqual(result, _ldap.RES_BIND) # check with Who Am I? extended operation r = l.whoami_s() self.assertEqual("", r) @@ -588,7 +714,7 @@ def test_whoami_after_unbind(self): def test_passwd(self): l = self._open_conn() # first, create a user to change password on - dn = "cn=PasswordTest," + self.server.suffix + dn = "cn=PasswordTest," + self.writesuffix m = l.add_ext( dn, [ @@ -676,26 +802,21 @@ def test_sasl(self): return # TODO - def test_tls(self): - l = self._open_conn() - if not self._require_attr(l, 'start_tls_s'): # HAVE_TLS - return - # TODO - def test_cancel(self): l = self._open_conn() if not self._require_attr(l, 'cancel'): # FEATURE_CANCEL return - def test_errno107(self): + def test_enotconn(self): l = _ldap.initialize('ldap://127.0.0.1:42') try: m = l.simple_bind("", "") r = l.result4(m, _ldap.MSG_ALL, self.timeout) except _ldap.SERVER_DOWN as ldap_err: - errno = ldap_err.args[0]['errno'] - if errno != 107: - self.fail("expected errno=107, got %d" % errno) + errno_val = ldap_err.args[0]['errno'] + if errno_val != errno.ENOTCONN: + self.fail("expected errno=%d, got %d" + % (errno.ENOTCONN, errno_val)) else: self.fail("expected SERVER_DOWN, got %r" % r) @@ -726,6 +847,137 @@ def test_invalid_credentials(self): else: self.fail("expected INVALID_CREDENTIALS, got %r" % r) + # TODO: test_extop + + def assertInvalidControls(self, func, *args, **kwargs): + post = kwargs.pop('post', ()) + self.assertFalse(kwargs) + # last two args are serverctrls, clientctrls + with self.assertRaises(TypeError) as e: + func(*(args + (object, None) + post)) + self.assertEqual( + e.exception.args, + ('LDAPControls_from_object(): expected a list', object) + ) + with self.assertRaises(TypeError) as e: + func(*(args + (None, object) + post)) + self.assertEqual( + e.exception.args, + ('LDAPControls_from_object(): expected a list', object) + ) + + def test_invalid_controls(self): + l = self._open_conn() + self.assertInvalidControls(l.simple_bind, "", "") + self.assertInvalidControls(l.whoami_s) + self.assertInvalidControls(l.passwd, 'dn', 'initial', 'changed') + self.assertInvalidControls(l.add_ext, 'dn', [('cn', b'cn')]) + self.assertInvalidControls( + l.modify_ext, 'dn', [(_ldap.MOD_ADD, 'attr', [b'value'])]) + self.assertInvalidControls(l.compare_ext, 'dn', 'val1', 'val2') + self.assertInvalidControls( + l.rename, 'dn', 'newdn', 'container', False) + self.assertInvalidControls( + l.search_ext, 'dn', _ldap.SCOPE_SUBTREE, '(objectClass=*)', + None, 1) + self.assertInvalidControls(l.delete_ext, 'dn') + m = l.search_ext( + self.server.suffix, _ldap.SCOPE_SUBTREE, '(objectClass=*)') + self.assertInvalidControls(l.abandon_ext, m) + self.assertInvalidControls(l.cancel, 0) + self.assertInvalidControls(l.extop, 'oid', 'value') + if hasattr(l, 'sasl_bind_s'): + self.assertInvalidControls(l.sasl_bind_s, 'dn', 'MECH', 'CRED') + if hasattr(l, 'sasl_interactive_bind_s'): + self.assertInvalidControls( + l.sasl_interactive_bind_s, 'who', 'SASLObject', post=(1,)) + self.assertInvalidControls(l.unbind_ext) + + @requires_tls() + def test_tls_ext(self): + l = self._open_conn(bind=False) + # StartTLS needs LDAPv3 + l.set_option(_ldap.OPT_PROTOCOL_VERSION, _ldap.VERSION3) + l.set_option(_ldap.OPT_X_TLS_CACERTFILE, self.server.cafile) + # re-create TLS context + l.set_option(_ldap.OPT_X_TLS_NEWCTX, 0) + l.start_tls_s() + + @requires_tls() + def test_tls_require_cert(self): + # libldap defaults to secure cert validation + # see libraries/libldap/init.c + # gopts->ldo_tls_require_cert = LDAP_OPT_X_TLS_DEMAND; + + self.assertEqual( + _ldap.get_option(_ldap.OPT_X_TLS_REQUIRE_CERT), + _ldap.OPT_X_TLS_DEMAND + ) + l = self._open_conn(bind=False) + self.assertEqual( + l.get_option(_ldap.OPT_X_TLS_REQUIRE_CERT), + _ldap.OPT_X_TLS_DEMAND + ) + + @requires_tls() + def test_tls_ext_noca(self): + l = self._open_conn(bind=False) + l.set_option(_ldap.OPT_PROTOCOL_VERSION, _ldap.VERSION3) + # fails because libldap defaults to secure cert validation but + # the test CA is not installed as trust anchor. + with self.assertRaises(_ldap.CONNECT_ERROR) as e: + l.start_tls_s() + # known resaons: + # Ubuntu on Travis: '(unknown error code)' + # OpenSSL 1.1: error:1416F086:SSL routines:\ + # tls_process_server_certificate:certificate verify failed + # NSS: TLS error -8172:Peer's certificate issuer has \ + # been marked as not trusted by the user. + msg = str(e.exception) + candidates = ('certificate', 'tls', '(unknown error code)') + if not any(s in msg.lower() for s in candidates): + self.fail(msg) + + @requires_tls() + def test_tls_ext_clientcert(self): + l = self._open_conn(bind=False) + l.set_option(_ldap.OPT_PROTOCOL_VERSION, _ldap.VERSION3) + l.set_option(_ldap.OPT_X_TLS_CACERTFILE, self.server.cafile) + l.set_option(_ldap.OPT_X_TLS_CERTFILE, self.server.clientcert) + l.set_option(_ldap.OPT_X_TLS_KEYFILE, self.server.clientkey) + l.set_option(_ldap.OPT_X_TLS_REQUIRE_CERT, _ldap.OPT_X_TLS_HARD) + l.set_option(_ldap.OPT_X_TLS_NEWCTX, 0) + l.start_tls_s() + + @requires_tls() + def test_tls_packages(self): + # libldap has tls_g.c, tls_m.c, and tls_o.c with ldap_int_tls_impl + 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 00d07266..97146ec8 100644 --- a/Tests/t_cidict.py +++ b/Tests/t_cidict.py @@ -1,15 +1,17 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap's module ldap.cidict See https://www.python-ldap.org/ for details. """ -# from Python's standard lib +import os import unittest +import warnings -# from python-ldap -import ldap, ldap.cidict +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' +import ldap +import ldap.cidict class TestCidict(unittest.TestCase): @@ -28,12 +30,14 @@ def test_cidict(self): cix = ldap.cidict.cidict(data) self.assertEqual(cix["ABCDEF"], 123) self.assertEqual(cix.get("ABCDEF", None), 123) - self.assertEqual(cix.get("not existent", None), None) + self.assertIsNone(cix.get("not existent", None)) cix["xYZ"] = 987 self.assertEqual(cix["XyZ"], 987) self.assertEqual(cix.get("xyz", None), 987) cix_keys = sorted(cix.keys()) self.assertEqual(cix_keys, ['AbCDeF','xYZ']) + cix_keys = sorted(cix) + self.assertEqual(cix_keys, ['AbCDeF','xYZ']) cix_items = sorted(cix.items()) self.assertEqual(cix_items, [('AbCDeF',123), ('xYZ',987)]) del cix["abcdEF"] @@ -44,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 9aee43e9..5d8b3f06 100644 --- a/Tests/t_edit.py +++ b/Tests/t_edit.py @@ -1,73 +1,67 @@ -from __future__ import unicode_literals +import os +import unittest -import sys - -if sys.version_info[0] <= 2: - PY2 = True - text_type = unicode -else: - PY2 = False - text_type = str - -import ldap, unittest -from slapdtest import SlapdObject +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' +import ldap from ldap.ldapobject import LDAPObject +from slapdtest import SlapdTestCase -server = None +class EditionTests(SlapdTestCase): -class EditionTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + base = cls.server.suffix + suffix_dc = base.split(',')[0][3:] - def setUp(self): - global server - if server is None: - server = SlapdObject() - server.start() - base = server.suffix - suffix_dc = base.split(',')[0][3:] + # insert some Foo* objects via ldapadd + cls.server.ldapadd("\n".join([ + 'dn: '+cls.server.suffix, + 'objectClass: dcObject', + 'objectClass: organization', + 'dc: '+suffix_dc, + 'o: '+suffix_dc, + '', + 'dn: '+cls.server.root_dn, + 'objectClass: applicationProcess', + 'cn: '+cls.server.root_cn, + '', + "dn: cn=Foo1,"+base, + "objectClass: organizationalRole", + "cn: Foo1", + "", + "dn: cn=Foo2,"+base, + "objectClass: organizationalRole", + "cn: Foo2", + "", + "dn: cn=Foo3,"+base, + "objectClass: organizationalRole", + "cn: Foo3", + "", + "dn: ou=Container,"+base, + "objectClass: organizationalUnit", + "ou: Container", + "", + "dn: cn=Foo4,ou=Container,"+base, + "objectClass: organizationalRole", + "cn: Foo4", + "", + ])+"\n") - # insert some Foo* objects via ldapadd - server.ldapadd("\n".join([ - 'dn: '+server.suffix, - 'objectClass: dcObject', - 'objectClass: organization', - 'dc: '+suffix_dc, - 'o: '+suffix_dc, - '', - 'dn: '+server.root_dn, - 'objectClass: applicationProcess', - 'cn: '+server.root_cn, - '', - "dn: cn=Foo1,"+base, - "objectClass: organizationalRole", - "cn: Foo1", - "", - "dn: cn=Foo2,"+base, - "objectClass: organizationalRole", - "cn: Foo2", - "", - "dn: cn=Foo3,"+base, - "objectClass: organizationalRole", - "cn: Foo3", - "", - "dn: ou=Container,"+base, - "objectClass: organizationalUnit", - "ou: Container", - "", - "dn: cn=Foo4,ou=Container,"+base, - "objectClass: organizationalRole", - "cn: Foo4", - "", - ])+"\n") + def setUp(self): + self.ldap = LDAPObject(self.server.ldap_uri, bytes_mode=False) + self.ldap.protocol_version = 3 + self.ldap.set_option(ldap.OPT_REFERRALS, 0) + self.ldap.simple_bind_s( + self.server.root_dn, + self.server.root_pw + ) - l = LDAPObject(server.ldap_uri, bytes_mode=False) - l.protocol_version = 3 - l.set_option(ldap.OPT_REFERRALS,0) - l.simple_bind_s(server.root_dn, - server.root_pw) - self.ldap = l - self.server = server + def tearDown(self): + self.ldap.unbind() def test_add_object(self): base = self.server.suffix @@ -83,6 +77,12 @@ def test_add_object(self): ("cn=Added,ou=Container," + base, {'cn': [b'Added'], 'objectClass': [b'organizationalRole']}), ]) + # Delete object + self.ldap.delete_s(dn) + result = self.ldap.search_s( + base, ldap.SCOPE_SUBTREE, '(cn=Added)', ['*'] + ) + self.assertEqual(result, []) if __name__ == '__main__': diff --git a/Tests/t_ldap_asyncsearch.py b/Tests/t_ldap_asyncsearch.py new file mode 100644 index 00000000..1b89050d --- /dev/null +++ b/Tests/t_ldap_asyncsearch.py @@ -0,0 +1,24 @@ +import importlib +import os +import unittest +import warnings + +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' + +import ldap.asyncsearch + + +class TestLdapAsyncSearch(unittest.TestCase): + def test_deprecated(self): + with warnings.catch_warnings(record=True) as w: + warnings.resetwarnings() + warnings.simplefilter('once', DeprecationWarning) + old = importlib.import_module('ldap.async') + self.assertEqual(len(w), 1) + diff = set(dir(ldap.asyncsearch)).difference(dir(old)) + self.assertEqual(diff, set()) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/t_ldap_controls_libldap.py b/Tests/t_ldap_controls_libldap.py index 229935ef..d3978613 100644 --- a/Tests/t_ldap_controls_libldap.py +++ b/Tests/t_ldap_controls_libldap.py @@ -4,7 +4,6 @@ # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' -import ldap from ldap.controls import pagedresults from ldap.controls import libldap diff --git a/Tests/t_ldap_controls_ppolicy.py b/Tests/t_ldap_controls_ppolicy.py new file mode 100644 index 00000000..8644e563 --- /dev/null +++ b/Tests/t_ldap_controls_ppolicy.py @@ -0,0 +1,33 @@ +import os +import unittest + +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' + +from ldap.controls import ppolicy + + +PP_GRACEAUTH = b'0\x84\x00\x00\x00\t\xa0\x84\x00\x00\x00\x03\x81\x01\x02' +PP_TIMEBEFORE = b'0\x84\x00\x00\x00\t\xa0\x84\x00\x00\x00\x03\x80\x012' + + +class TestControlsPPolicy(unittest.TestCase): + def assertPPolicy(self, pp, timeBeforeExpiration=None, + graceAuthNsRemaining=None, error=None): + self.assertEqual(pp.timeBeforeExpiration, timeBeforeExpiration) + self.assertEqual(pp.graceAuthNsRemaining, graceAuthNsRemaining) + self.assertEqual(pp.error, error) + + def test_ppolicy_graceauth(self): + pp = ppolicy.PasswordPolicyControl() + pp.decodeControlValue(PP_GRACEAUTH) + self.assertPPolicy(pp, graceAuthNsRemaining=2) + + def test_ppolicy_timebefore(self): + pp = ppolicy.PasswordPolicyControl() + pp.decodeControlValue(PP_TIMEBEFORE) + self.assertPPolicy(pp, timeBeforeExpiration=50) + + +if __name__ == '__main__': + unittest.main() 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 459c1dcf..c4d9cb6c 100644 --- a/Tests/t_ldap_dn.py +++ b/Tests/t_ldap_dn.py @@ -1,16 +1,14 @@ -# -*- 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 -# from python-ldap +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' import ldap.dn @@ -42,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\ 2 entries returned + self._ldap_conn.find_unique_entry( + self.server.suffix, + ldap.SCOPE_ONELEVEL, + '(cn=Foo*)', + ['*'], + ) + with self.assertRaises(ldap.NO_UNIQUE_ENTRY): + # 0 entries returned + self._ldap_conn.find_unique_entry( + self.server.suffix, + ldap.SCOPE_ONELEVEL, + '(cn=Bar*)', + ['*'], + ) + 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), + [ + 'attributeTypes', + 'ldapSyntaxes', + 'matchingRuleUse', + 'matchingRules', + 'objectClasses' + ] + ) - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_search_subschema_have_bytes(self): - l = self._get_bytes_ldapobject(explicit=False) - dn = l.search_subschemasubentry_s() - self.assertIsInstance(dn, bytes) - self.assertEqual(dn, b"cn=Subschema") - - def test004_errno107(self): + def test004_enotconn(self): l = self.ldap_object_class('ldap://127.0.0.1:42') try: m = l.simple_bind_s("", "") r = l.result4(m, ldap.MSG_ALL, self.timeout) except ldap.SERVER_DOWN as ldap_err: - errno = ldap_err.args[0]['errno'] - if errno != 107: - self.fail("expected errno=107, got %d" % errno) + errno_val = ldap_err.args[0]['errno'] + if errno_val != errno.ENOTCONN: + self.fail("expected errno=%d, got %d" + % (errno.ENOTCONN, errno_val)) info = ldap_err.args[0]['info'] - if info != os.strerror(107): - self.fail("expected info=%r, got %d" % (os.strerror(107), info)) + expected_info = os.strerror(errno.ENOTCONN) + if info != expected_info: + self.fail(f"expected info={expected_info!r}, got {info!r}") else: self.fail("expected SERVER_DOWN, got %r" % r) @@ -298,7 +343,9 @@ def test005_invalid_credentials(self): else: self.fail("expected INVALID_CREDENTIALS, got %r" % r) - def test006_sasl_extenal_bind_s(self): + @requires_sasl() + @requires_ldapi() + 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()) @@ -307,13 +354,254 @@ 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=*)') l.abandon(m) with self.assertRaises(ldap.TIMEOUT): result = l.result(m, timeout=0.001) - + + def assertIsSubclass(self, cls, other): + self.assertTrue( + issubclass(cls, other), + cls.__mro__ + ) + + def test_simple_bind_noarg(self): + l = self.ldap_object_class(self.server.ldap_uri) + l.simple_bind_s() + 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(), '') + + def _check_byteswarning(self, warning, expected_message): + self.assertIs(warning.category, ldap.LDAPBytesWarning) + self.assertIn(expected_message, str(warning.message)) + + def _normalize(filename): + # Python 2 likes to report the ".pyc" file in warnings, + # tracebacks or __file__. + # Use the corresponding ".py" in that case. + if filename.endswith('.pyc'): + return filename[:-1] + return filename + + # Assert warning points to a line marked CORRECT LINE in this file + self.assertEquals(_normalize(warning.filename), _normalize(__file__)) + self.assertIn( + 'CORRECT LINE', + linecache.getline(warning.filename, warning.lineno) + ) + + @requires_tls() + def test_multiple_starttls(self): + # Test for openldap does not re-register nss shutdown callbacks + # after nss_Shutdown is called + # https://github.com/python-ldap/python-ldap/issues/60 + # https://bugzilla.redhat.com/show_bug.cgi?id=1520990 + for _ in range(10): + l = self.ldap_object_class(self.server.ldap_uri) + l.set_option(ldap.OPT_X_TLS_CACERTFILE, self.server.cafile) + l.set_option(ldap.OPT_X_TLS_NEWCTX, 0) + l.start_tls_s() + 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['supportedLDAPVersion'], [b'3']) + keys = set(dse) + # SASL info may be missing in restricted build environments + keys.discard('supportedSASLMechanisms') + self.assertEqual( + keys, + {'configContext', 'entryDN', 'namingContexts', 'objectClass', + 'structuralObjectClass', 'subschemaSubentry', + 'supportedControl', 'supportedExtension', 'supportedFeatures', + 'supportedLDAPVersion'} + ) + self.assertEqual( + self._ldap_conn.get_naming_contexts(), + [self.server.suffix.encode('utf-8')] + ) + + + def test_compare_s_true(self): + base = self.server.suffix + l = self._ldap_conn + result = l.compare_s('cn=Foo1,%s' % base, 'cn', b'Foo1') + self.assertIs(result, True) + + def test_compare_s_false(self): + base = self.server.suffix + l = self._ldap_conn + 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): """ @@ -322,6 +610,8 @@ class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject): ldap_object_class = ReconnectLDAPObject + @requires_sasl() + @requires_ldapi() def test101_reconnect_sasl_external(self): l = self.ldap_object_class(self.server.ldapi_uri) l.sasl_external_bind_s() @@ -331,7 +621,7 @@ def test101_reconnect_sasl_external(self): self.assertEqual(l.whoami_s(), authz_id) def test102_reconnect_simple_bind(self): - l = self.ldap_object_class(self.server.ldapi_uri) + l = self.ldap_object_class(self.server.ldap_uri) bind_dn = 'cn=user1,'+self.server.suffix l.simple_bind_s(bind_dn, 'user1_pw') self.assertEqual(l.whoami_s(), 'dn:'+bind_dn) @@ -339,42 +629,147 @@ def test102_reconnect_simple_bind(self): self.assertEqual(l.whoami_s(), 'dn:'+bind_dn) def test103_reconnect_get_state(self): - l1 = self.ldap_object_class(self.server.ldapi_uri) + l1 = self.ldap_object_class(self.server.ldap_uri) bind_dn = 'cn=user1,'+self.server.suffix l1.simple_bind_s(bind_dn, 'user1_pw') self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn) 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'): 0, - str('_trace_stack_limit'): 5, - str('_uri'): self.server.ldapi_uri, - str('bytes_mode'): l1.bytes_mode, - str('bytes_mode_hardfail'): l1.bytes_mode_hardfail, - 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.ldapi_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 2be03f63..f9c72098 100644 --- a/Tests/t_ldapurl.py +++ b/Tests/t_ldapurl.py @@ -1,14 +1,14 @@ -# -*- 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 ldap.compat import quote +from urllib.parse import quote + +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' import ldapurl from ldapurl import LDAPUrl @@ -33,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): @@ -56,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): @@ -144,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( @@ -159,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), @@ -169,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), @@ -178,10 +204,6 @@ def test_ldapurl(self): class TestLDAPUrl(unittest.TestCase): - - def assertNone(self, expr, msg=None): - self.assertFalse(expr is not None, msg or ("%r" % expr)) - def test_combo(self): u = MyLDAPUrl( "ldap://127.0.0.1:1234/dc=example,dc=com" @@ -218,15 +240,15 @@ def test_parse_empty_dn(self): def test_parse_default_attrs(self): u = LDAPUrl("ldap://") - self.assertNone(u.attrs) + self.assertIsNone(u.attrs) def test_parse_default_scope(self): u = LDAPUrl("ldap://") - self.assertNone(u.scope) # RFC4516 s3 + self.assertIsNone(u.scope) # RFC4516 s3 def test_parse_default_filter(self): u = LDAPUrl("ldap://") - self.assertNone(u.filterstr) # RFC4516 s3 + self.assertIsNone(u.filterstr) # RFC4516 s3 def test_parse_default_extensions(self): u = LDAPUrl("ldap://") @@ -280,9 +302,9 @@ def test_parse_dn(self): def test_parse_attrs(self): u = LDAPUrl("ldap:///?") - self.assertEqual(u.attrs, None) + self.assertIsNone(u.attrs) u = LDAPUrl("ldap:///??") - self.assertEqual(u.attrs, None) + self.assertIsNone(u.attrs) u = LDAPUrl("ldap:///?*?") self.assertEqual(u.attrs, ['*']) u = LDAPUrl("ldap:///?*,*?") @@ -298,9 +320,9 @@ def test_parse_attrs(self): def test_parse_scope_default(self): u = LDAPUrl("ldap:///??") - self.assertNone(u.scope) # on opposite to RFC4516 s3 for referral chasing + self.assertIsNone(u.scope) # on opposite to RFC4516 s3 for referral chasing u = LDAPUrl("ldap:///???") - self.assertNone(u.scope) # on opposite to RFC4516 s3 for referral chasing + self.assertIsNone(u.scope) # on opposite to RFC4516 s3 for referral chasing def test_parse_scope(self): u = LDAPUrl("ldap:///??sub") @@ -350,8 +372,8 @@ def test_parse_filter(self): def test_parse_extensions(self): u = LDAPUrl("ldap:///????") - self.assertNone(u.extensions) - self.assertNone(u.who) + self.assertIsNone(u.extensions) + self.assertIsNone(u.who) u = LDAPUrl("ldap:///????bindname=cn=root") self.assertEqual(len(u.extensions), 1) self.assertEqual(u.who, "cn=root") @@ -375,7 +397,7 @@ def test_parse_extensions_5questions(self): def test_parse_extensions_novalue(self): u = LDAPUrl("ldap:///????bindname") self.assertEqual(len(u.extensions), 1) - self.assertNone(u.who) + self.assertIsNone(u.who) @unittest.expectedFailure def test_bad_urls(self): diff --git a/Tests/t_ldif.py b/Tests/t_ldif.py index adf0d262..2f9ed679 100644 --- a/Tests/t_ldif.py +++ b/Tests/t_ldif.py @@ -1,22 +1,20 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap's module ldif See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - -# from Python's standard lib -import unittest +import os import textwrap +import unittest try: from StringIO import StringIO except ImportError: from io import StringIO -# from python-ldap +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' + import ldif @@ -465,7 +463,7 @@ def test_weird_empty_lines(self): """ # comment before version - + version: 1 @@ -584,7 +582,7 @@ def test_weird_empty_lines(self): """ # comment before version - + version: 1 diff --git a/Tests/t_slapdobject.py b/Tests/t_slapdobject.py new file mode 100644 index 00000000..e1cc971b --- /dev/null +++ b/Tests/t_slapdobject.py @@ -0,0 +1,18 @@ +import unittest + +import slapdtest + + +class TestSlapdObject(unittest.TestCase): + def test_context_manager(self): + with slapdtest.SlapdObject() as server: + self.assertIsNotNone(server._proc) + self.assertIsNone(server._proc) + + def test_context_manager_after_start(self): + server = slapdtest.SlapdObject() + server.start() + self.assertIsNotNone(server._proc) + with server: + self.assertIsNotNone(server._proc) + self.assertIsNone(server._proc) diff --git a/Tests/t_untested_mods.py b/Tests/t_untested_mods.py index 7595a788..5e726a6a 100644 --- a/Tests/t_untested_mods.py +++ b/Tests/t_untested_mods.py @@ -1,5 +1,10 @@ # modules without any tests -import ldap.async + +import os + +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' + import ldap.controls.deref import ldap.controls.openldap import ldap.controls.ppolicy @@ -9,6 +14,7 @@ import ldap.controls.sessiontrack import ldap.controls.sss import ldap.controls.vlv +import ldap.constants import ldap.logger import ldap.resiter import ldap.syncrepl 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 59816b72..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 @@ -5,20 +9,23 @@ # for wrapping OpenLDAP 2 libs [_ldap] -# Define extra include and library dirs if needed -library_dirs = /usr/lib /usr/lib64 /usr/local/lib /usr/local/lib64 -include_dirs = /usr/include /usr/include/sasl /usr/local/include /usr/local/include/sasl +# Define extra include and library dirs if needed. distutils adds non +# standard library_dirs as rpath. +# library_dirs = /usr/lib /usr/lib64 /usr/local/lib /usr/local/lib64 +# include_dirs = /usr/include /usr/local/include # 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 = +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 +#libs = ldap_r lber # Installation options [install] @@ -28,9 +35,17 @@ 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 release = 1 doc_files = CHANGES README INSTALL TODO Demo/ + +# pytest, https://docs.pytest.org/en/latest/customize.html +[tool:pytest] +testpaths = Tests +python_files = t_*.py +filterwarnings = + error + ignore::ldap.LDAPBytesWarning diff --git a/setup.py b/setup.py index e47d2158..f2e816be 100644 --- a/setup.py +++ b/setup.py @@ -1,27 +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 -has_setuptools = False -try: - from setuptools import setup, Extension - has_setuptools = True -except ImportError: - from distutils.core 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, 3): - raise RuntimeError('The C API from Python 3.3+ 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 @@ -63,71 +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' - -# Python 2.3.6+ and setuptools are needed to build eggs, so -# let's handle setuptools' additional keyword arguments to -# setup() in a fashion that doesn't break compatibility to -# distutils. This still allows 'normal' builds where either -# Python > 2.3.5 or setuptools (or both ;o) are not available. -kwargs = dict() -if has_setuptools: - kwargs = dict( - include_package_data = True, - install_requires = [ - 'setuptools', - 'pyasn1 >= 0.3.7', - 'pyasn1_modules >= 0.1.5', - ], - zip_safe = False, - python_requires = '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*', - ) - +#-- 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.python.org/pypi/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.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - # 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', @@ -136,13 +66,16 @@ class OpenLDAP2: 'Modules/ldapcontrol.c', 'Modules/common.c', 'Modules/constants.c', - 'Modules/errors.c', 'Modules/functions.c', 'Modules/ldapmodule.c', 'Modules/message.c', 'Modules/options.c', 'Modules/berval.c', ], + depends = [ + 'Modules/pythonldap.h', + 'Modules/constants_generated.h', + ], libraries = LDAP_CLASS.libs, include_dirs = ['Modules'] + LDAP_CLASS.include_dirs, library_dirs = LDAP_CLASS.library_dirs, @@ -151,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)] + \ [ @@ -161,46 +93,4 @@ class OpenLDAP2: ] ), ], - #-- Python "stand alone" modules - py_modules = [ - 'ldapurl', - 'ldif', - 'ldap', - 'slapdtest', - 'ldap.async', - 'ldap.compat', - 'ldap.controls', - 'ldap.controls.deref', - 'ldap.controls.libldap', - 'ldap.controls.openldap', - 'ldap.controls.ppolicy', - 'ldap.controls.psearch', - 'ldap.controls.pwdpolicy', - 'ldap.controls.readentry', - 'ldap.controls.sessiontrack', - 'ldap.controls.simple', - 'ldap.controls.sss', - 'ldap.controls.vlv', - 'ldap.cidict', - 'ldap.dn', - 'ldap.extop', - 'ldap.extop.dds', - 'ldap.filter', - 'ldap.functions', - 'ldap.ldapobject', - 'ldap.logger', - 'ldap.modlist', - 'ldap.pkginfo', - 'ldap.resiter', - 'ldap.sasl', - 'ldap.schema', - 'ldap.schema.models', - 'ldap.schema.subentry', - 'ldap.schema.tokenizer', - 'ldap.syncrepl', - ], - package_dir = {'': 'Lib',}, - data_files = LDAP_CLASS.extra_files, - test_suite = 'Tests', - **kwargs ) diff --git a/tox.ini b/tox.ini index b2d435c6..0741ef29 100644 --- a/tox.ini +++ b/tox.ini @@ -4,18 +4,105 @@ # and then run "tox" from this directory. [tox] -# Note: when updating Python versions, also change setup.py and .travis.yml -envlist = py27,py33,py34,py35,py36,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 -commands = {envpython} -m coverage run --parallel setup.py test +deps = setuptools passenv = WITH_GCOV +# - Enable BytesWarning +# - Turn all warnings into exceptions. +setenv = + CFLAGS=-Wno-int-in-bool-context -Werror -Werror=declaration-after-statement -std=c99 +commands = {envpython} -bb -Werror \ + -m unittest discover -v -s Tests -p 't_*' {posargs} + +[testenv:py312] +# Python 3.12 headers are incompatible with declaration-after-statement +setenv = + CFLAGS=-Wno-int-in-bool-context -Werror -std=c99 + -[testenv:coverage-report] -deps = coverage +[testenv:py3-nosasltls] +basepython = python3 +# don't install, install dependencies manually skip_install = true +deps = + {[testenv]deps} + pyasn1 + pyasn1_modules +passenv = {[testenv]passenv} +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 +deps = {[testenv]deps} +passenv = {[testenv]passenv} +setenv = + PYTHON_LDAP_TRACE_LEVEL=9 + PYTHON_LDAP_TRACE_FILE={envtmpdir}/trace.log +commands = {[testenv]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: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 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 +deps = + docutils + markdown + sphinx + sphinxcontrib-spelling + setuptools commands = - {envpython} -m coverage combine - {envpython} -m coverage report --show-missing - {envpython} -m coverage html + {envpython} setup.py check --restructuredtext --metadata --strict + {envpython} -m markdown README -f {envtmpdir}/README.html + {envpython} -m sphinx -v -W -b html -d {envtmpdir}/doctrees \ + {toxinidir}/Doc {envtmpdir}/html + {envpython} -m sphinx -v -W -b spelling -d {envtmpdir}/doctrees \ + {toxinidir}/Doc {envtmpdir}/spelling