diff --git a/.coveragerc b/.coveragerc index fc432b9e..738d86fa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,9 +1,10 @@ [run] branch = True source = - Lib/ -omit = - Lib/slapdtest.py + ldap + ldif + ldapurl + slapdtest [paths] source = diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..0bc2ae0e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +--- +name: CI + +on: [push, pull_request] + +jobs: + distros: + name: "Ubuntu with Python ${{ matrix.python-version }}" + runs-on: "ubuntu-latest" + strategy: + fail-fast: false + matrix: + python-version: [3.6, 3.7, 3.8, 3.9, pypy3] + steps: + - name: Checkout + uses: "actions/checkout@v2" + - name: Install apt dependencies + run: | + set -ex + sudo apt update + sudo apt install -y ldap-utils slapd enchant 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@v2 + with: + python-version: ${{ matrix.python-version }} + - 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..c0dbb45c --- /dev/null +++ b/.github/workflows/tox-fedora.yml @@ -0,0 +1,34 @@ +on: [push, pull_request] + +name: Tox on Fedora + +jobs: + tox_test: + name: Tox env "${{matrix.tox_env}}" on Fedora + steps: + - uses: actions/checkout@v2 + - name: Run Tox tests + uses: fedora-python/tox-github-action@master + with: + tox_env: ${{ matrix.tox_env }} + dnf_install: > + @c-development openldap-devel python3-devel + openldap-servers openldap-clients lcov clang-analyzer valgrind + enchant + strategy: + matrix: + tox_env: + - py36 + - py37 + - py38 + - py39 + - py310 + - c90-py36 + - c90-py37 + - py3-nosasltls + - py3-trace + - pypy3 + - doc + + # Use GitHub's Linux Docker host + runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 5f6d0425..bab21878 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ PKG-INFO # generated in the sample workflow /__venv__/ + +# test dirs +python-ldap-test-* diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 81466393..00000000 --- a/.travis.yml +++ /dev/null @@ -1,92 +0,0 @@ -language: python - -sudo: false - -cache: pip - -addons: - apt: - packages: - - ldap-utils - - slapd - - enchant - -# Note: when updating Python versions, also change setup.py and tox.ini -matrix: - include: - - python: 2.7 - env: - - TOXENV=py27 - - WITH_GCOV=1 - - python: 3.4 - env: - - TOXENV=py34 - - WITH_GCOV=1 - - python: 3.5 - env: - - TOXENV=py35 - - WITH_GCOV=1 - - python: 3.6 - env: - - TOXENV=py36 - - WITH_GCOV=1 - - python: pypy - env: - - TOXENV=pypy - - python: 3.7 - env: - - TOXENV=py37 - - WITH_GCOV=1 - dist: xenial - sudo: true - - python: 3.8-dev - env: - - TOXENV=py38 - - CFLAGS_std="-std=c99" - - WITH_GCOV=1 - dist: xenial - sudo: true - - python: 2.7 - env: - - TOXENV=py2-nosasltls - - WITH_GCOV=1 - - python: 3.6 - env: - - TOXENV=py3-nosasltls - - WITH_GCOV=1 - - python: 3.6 - env: - - TOXENV=py3-trace - - python: 3.6 - env: TOXENV=doc - allow_failures: - - env: - - TOXENV=py38 - - CFLAGS_std="-std=c99" - - WITH_GCOV=1 - - env: - - TOXENV=pypy - -env: - global: - # -Wno-int-in-bool-context: don't complain about PyMem_MALLOC() - # -Werror: turn all warnings into fatal errors - # -Werror=declaration-after-statement: strict ISO C90 - - CFLAGS_warnings="-Wno-int-in-bool-context -Werror -Werror=declaration-after-statement" - # Keep C90 compatibility where possible. - # (Python 3.8+ headers use C99 features, so this needs to be overridable.) - - CFLAGS_std="-std=c90" - # pass CFLAGS, CI (for Travis CI) and WITH_GCOV to tox tasks - - TOX_TESTENV_PASSENV="CFLAGS CI WITH_GCOV" - -install: - - pip install "pip>=7.1.0" - - pip install tox-travis tox codecov coverage - -script: CFLAGS="$CFLAGS_warnings $CFLAGS_std" tox - -after_success: - # gather Python coverage - - python -m coverage combine - # send Python and GCOV coverage - - codecov diff --git a/CHANGES b/CHANGES index 6e370160..c358fa9e 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,104 @@ +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 diff --git a/Demo/Lib/ldap/async/deltree.py b/Demo/Lib/ldap/async/deltree.py index d3fc6203..9db52c84 100644 --- a/Demo/Lib/ldap/async/deltree.py +++ b/Demo/Lib/ldap/async/deltree.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import ldap,ldap.async class DeleteLeafs(ldap.async.AsyncSearchHandler): diff --git a/Demo/Lib/ldapurl/urlsearch.py b/Demo/Lib/ldapurl/urlsearch.py index b293aa20..c58a1954 100644 --- a/Demo/Lib/ldapurl/urlsearch.py +++ b/Demo/Lib/ldapurl/urlsearch.py @@ -3,7 +3,6 @@ No output of LDAP data is produced except trace output. """ -from __future__ import print_function import sys,getpass,ldap,ldapurl try: diff --git a/Demo/initialize.py b/Demo/initialize.py index 952b3f4b..ab78cdb8 100644 --- a/Demo/initialize.py +++ b/Demo/initialize.py @@ -7,7 +7,6 @@ ldaps://localhost:1391 (LDAP over SSL) ldapi://%2ftmp%2fopenldap2 (domain socket /tmp/openldap2) """ -from __future__ import print_function import sys,os,ldap diff --git a/Demo/ldapcontrols.py b/Demo/ldapcontrols.py index a5ba8d34..eec86b4c 100644 --- a/Demo/ldapcontrols.py +++ b/Demo/ldapcontrols.py @@ -1,4 +1,3 @@ -from __future__ import print_function import ldap,ldapurl,pprint from ldap.controls import LDAPControl,BooleanControl diff --git a/Demo/ldapurl_search.py b/Demo/ldapurl_search.py index 07ffbca5..614ba1af 100644 --- a/Demo/ldapurl_search.py +++ b/Demo/ldapurl_search.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import sys,pprint,ldap from ldap.ldapobject import LDAPObject diff --git a/Demo/matchedvalues.py b/Demo/matchedvalues.py index 7a6e7b6b..bbc2a1bc 100644 --- a/Demo/matchedvalues.py +++ b/Demo/matchedvalues.py @@ -27,7 +27,6 @@ # Matched values control: (mail=*@example.org) # dn: uid=jsmith,ou=People,dc=example,dc=com # mail: jsmith@example.org -from __future__ import print_function import ldap from ldap.controls import MatchedValuesControl @@ -37,7 +36,7 @@ def print_result(search_result): print("dn: %s" % search_result[n][0]) for attr in search_result[n][1].keys(): for i in range(len(search_result[n][1][attr])): - print("%s: %s" % (attr, search_result[n][1][attr][i])) + print("{}: {}".format(attr, search_result[n][1][attr][i])) print diff --git a/Demo/options.py b/Demo/options.py index 28a04374..7a8ee9db 100644 --- a/Demo/options.py +++ b/Demo/options.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import ldap host="localhost:1390" diff --git a/Demo/page_control.py b/Demo/page_control.py index 8238ede3..b92cbf86 100644 --- a/Demo/page_control.py +++ b/Demo/page_control.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function - url = "ldap://localhost:1390" base = "dc=stroeder,dc=de" search_flt = r'(objectClass=*)' diff --git a/Demo/paged_search_ext_s.py b/Demo/paged_search_ext_s.py index d0f82918..3a1a4acd 100644 --- a/Demo/paged_search_ext_s.py +++ b/Demo/paged_search_ext_s.py @@ -1,4 +1,3 @@ -from __future__ import print_function url = "ldap://localhost:1390/" base = "dc=stroeder,dc=de" search_flt = r'(objectClass=*)' diff --git a/Demo/passwd_ext_op.py b/Demo/passwd_ext_op.py index cc5d22cd..6c695482 100644 --- a/Demo/passwd_ext_op.py +++ b/Demo/passwd_ext_op.py @@ -1,7 +1,6 @@ """ Example showing the use of the password extended operation. """ -from __future__ import print_function import sys,ldap,ldapurl,getpass diff --git a/Demo/pyasn1/dds.py b/Demo/pyasn1/dds.py index c803a1de..19270603 100644 --- a/Demo/pyasn1/dds.py +++ b/Demo/pyasn1/dds.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Demo script for Dynamic Entries (see RFC 2589) @@ -8,7 +7,6 @@ pyasn1-modules python-ldap 2.4+ """ -from __future__ import print_function from ldap.extop.dds import RefreshRequest,RefreshResponse diff --git a/Demo/pyasn1/derefcontrol.py b/Demo/pyasn1/derefcontrol.py index 0e7153de..9565a9e8 100644 --- a/Demo/pyasn1/derefcontrol.py +++ b/Demo/pyasn1/derefcontrol.py @@ -3,7 +3,6 @@ This sample script demonstrates the use of the dereference control (see https://tools.ietf.org/html/draft-masarati-ldap-deref) """ -from __future__ import print_function import pprint,ldap,ldap.modlist,ldap.resiter diff --git a/Demo/pyasn1/noopsearch.py b/Demo/pyasn1/noopsearch.py index 2045f50c..a239c0e7 100644 --- a/Demo/pyasn1/noopsearch.py +++ b/Demo/pyasn1/noopsearch.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Demo script for counting searching with OpenLDAP's no-op control @@ -9,7 +8,6 @@ pyasn1-modules python-ldap 2.4+ """ -from __future__ import print_function import sys,ldap,ldapurl,getpass diff --git a/Demo/pyasn1/ppolicy.py b/Demo/pyasn1/ppolicy.py index cf6b2ac9..c143bf16 100644 --- a/Demo/pyasn1/ppolicy.py +++ b/Demo/pyasn1/ppolicy.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Demo script for Password Policy Controls (see https://tools.ietf.org/html/draft-behera-ldap-password-policy) @@ -9,7 +8,6 @@ pyasn1-modules python-ldap 2.4+ """ -from __future__ import print_function import sys,ldap,ldapurl,getpass diff --git a/Demo/pyasn1/psearch.py b/Demo/pyasn1/psearch.py index 3bd59e6d..2703a253 100644 --- a/Demo/pyasn1/psearch.py +++ b/Demo/pyasn1/psearch.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Demo script for Persistent Search Control (see https://tools.ietf.org/html/draft-ietf-ldapext-psearch) @@ -10,7 +9,6 @@ pyasn1-modules python-ldap 2.4+ """ -from __future__ import print_function import sys,ldap,ldapurl,getpass diff --git a/Demo/pyasn1/readentrycontrol.py b/Demo/pyasn1/readentrycontrol.py index 10faa2b2..b3ea6e81 100644 --- a/Demo/pyasn1/readentrycontrol.py +++ b/Demo/pyasn1/readentrycontrol.py @@ -4,7 +4,6 @@ Originally contributed by Andreas Hasenack """ -from __future__ import print_function import pprint,ldap,ldap.modlist @@ -39,8 +38,8 @@ serverctrls = [pr] ) _,_,_,resp_ctrls = l.result3(msg_id) -print("resp_ctrls[0].dn:",resp_ctrls[0].dn) -print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) +print("resp_ctrls[0].dn:", resp_ctrls[0].dn) +print("resp_ctrls[0].entry:", pprint.pformat(resp_ctrls[0].entry)) print("""#--------------------------------------------------------------------------- # Modify entry @@ -56,7 +55,7 @@ ) _,_,_,resp_ctrls = l.result3(msg_id) print("resp_ctrls[0].dn:",resp_ctrls[0].dn) -print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) +print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry)) pr = PostReadControl(criticality=True,attrList=['uidNumber','gidNumber','entryCSN']) @@ -67,7 +66,7 @@ ) _,_,_,resp_ctrls = l.result3(msg_id) print("resp_ctrls[0].dn:",resp_ctrls[0].dn) -print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) +print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry)) print("""#--------------------------------------------------------------------------- # Rename entry @@ -83,7 +82,7 @@ ) _,_,_,resp_ctrls = l.result3(msg_id) print("resp_ctrls[0].dn:",resp_ctrls[0].dn) -print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) +print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry)) pr = PreReadControl(criticality=True,attrList=['uid']) msg_id = l.rename( @@ -94,7 +93,7 @@ ) _,_,_,resp_ctrls = l.result3(msg_id) print("resp_ctrls[0].dn:",resp_ctrls[0].dn) -print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) +print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry)) print("""#--------------------------------------------------------------------------- # Delete entry @@ -108,4 +107,4 @@ ) _,_,_,resp_ctrls = l.result3(msg_id) print("resp_ctrls[0].dn:",resp_ctrls[0].dn) -print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry)) +print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry)) diff --git a/Demo/pyasn1/sessiontrack.py b/Demo/pyasn1/sessiontrack.py index 58a34e82..491172c0 100644 --- a/Demo/pyasn1/sessiontrack.py +++ b/Demo/pyasn1/sessiontrack.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ demo_track_ldap_session.py @@ -8,7 +7,6 @@ https://tools.ietf.org/html/draft-wahl-ldap-session-03 """ -from __future__ import print_function __version__ = '0.1' diff --git a/Demo/pyasn1/sss_highest_number.py b/Demo/pyasn1/sss_highest_number.py index 5f5bdc50..020dcdb3 100644 --- a/Demo/pyasn1/sss_highest_number.py +++ b/Demo/pyasn1/sss_highest_number.py @@ -38,9 +38,9 @@ class MyLDAPObject(LDAPObject,ResultProcessor): except ldap.SIZELIMIT_EXCEEDED: pass # print result - print 'Highest value of %s' % (id_attr) + print('Highest value of %s' % (id_attr)) if ldap_result: dn,entry = ldap_result[0] - print '->',entry[id_attr] + print('->',entry[id_attr]) else: - print 'not found' + print('not found') diff --git a/Demo/pyasn1/syncrepl.py b/Demo/pyasn1/syncrepl.py index 7103bc64..f1f24e19 100644 --- a/Demo/pyasn1/syncrepl.py +++ b/Demo/pyasn1/syncrepl.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ This script implements a syncrepl consumer which syncs data from an OpenLDAP server to a local (shelve) database. @@ -8,7 +7,6 @@ The bound user needs read access to the attributes entryDN and entryCSN. """ -from __future__ import print_function # Import modules from Python standard lib import logging diff --git a/Demo/rename.py b/Demo/rename.py index 91d7528f..edb78a80 100644 --- a/Demo/rename.py +++ b/Demo/rename.py @@ -1,4 +1,3 @@ -from __future__ import print_function import ldap from getpass import getpass diff --git a/Demo/resiter.py b/Demo/resiter.py index 96e9c90d..9fc14e41 100644 --- a/Demo/resiter.py +++ b/Demo/resiter.py @@ -4,7 +4,6 @@ See https://www.python-ldap.org for details. """ -from __future__ import print_function import ldap,ldap.resiter diff --git a/Demo/sasl_bind.py b/Demo/sasl_bind.py index 667221c1..8453d087 100644 --- a/Demo/sasl_bind.py +++ b/Demo/sasl_bind.py @@ -1,6 +1,5 @@ # For documentation, see comments in Module/LDAPObject.c and the # ldap.sasl module documentation. -from __future__ import print_function import ldap,ldap.sasl diff --git a/Demo/schema.py b/Demo/schema.py index 03f90a83..bdad5e0c 100644 --- a/Demo/schema.py +++ b/Demo/schema.py @@ -1,4 +1,3 @@ -from __future__ import print_function import sys,ldap,ldap.schema schema_attrs = ldap.schema.SCHEMA_ATTRS diff --git a/Demo/schema_tree.py b/Demo/schema_tree.py index 648bb86f..2b182e98 100644 --- a/Demo/schema_tree.py +++ b/Demo/schema_tree.py @@ -4,7 +4,6 @@ Usage: schema_oc_tree.py [--html] [LDAP URL] """ -from __future__ import print_function import sys,getopt,ldap,ldap.schema @@ -15,9 +14,9 @@ def PrintSchemaTree(schema,se_class,se_tree,se_oid,level): """ASCII text output for console""" se_obj = schema.get_obj(se_class,se_oid) if se_obj!=None: - print('| '*(level-1)+'+---'*(level>0), \) - ', '.join(se_obj.names), \ - '(%s)' % se_obj.oid + print('| '*(level-1)+'+---'*(level>0), + ', '.join(se_obj.names), + '(%s)' % se_obj.oid) for sub_se_oid in se_tree[se_oid]: print('| '*(level+1)) PrintSchemaTree(schema,se_class,se_tree,sub_se_oid,level+1) @@ -28,10 +27,10 @@ def HTMLSchemaTree(schema,se_class,se_tree,se_oid,level): se_obj = schema.get_obj(se_class,se_oid) if se_obj!=None: print(""" -
%s (%s)
+
{} ({})
- %s - """ % (', '.join(se_obj.names),se_obj.oid,se_obj.desc)) + {} + """.format(', '.join(se_obj.names),se_obj.oid,se_obj.desc)) if se_tree[se_oid]: print('
') for sub_se_oid in se_tree[se_oid]: diff --git a/Demo/simple.py b/Demo/simple.py index 5fa04250..c82659c3 100644 --- a/Demo/simple.py +++ b/Demo/simple.py @@ -1,4 +1,3 @@ -from __future__ import print_function import sys,getpass import ldap @@ -86,7 +85,7 @@ ("labeleduri", ["labeleduri"]), ("manager", ["cn=Jaga Indulska"]), ("reports", ["reports"]), - ("jpegPhoto", [open("/www/leonard/leonard.jpg","r").read()]), + ("jpegPhoto", [open("/www/leonard/leonard.jpg").read()]), ("uid", ["leonard"]), ("userPassword", [""]) diff --git a/Demo/simplebrowse.py b/Demo/simplebrowse.py index 9d138003..fd4563ae 100644 --- a/Demo/simplebrowse.py +++ b/Demo/simplebrowse.py @@ -3,7 +3,6 @@ # # simple LDAP server browsing example # -from __future__ import print_function import ldap from traceback import print_exc diff --git a/Doc/bytes_mode.rst b/Doc/bytes_mode.rst index dcd3dcb2..3a984bf0 100644 --- a/Doc/bytes_mode.rst +++ b/Doc/bytes_mode.rst @@ -1,34 +1,12 @@ .. _text-bytes: +.. _bytes_mode: Bytes/text management ===================== -Python 3 introduces a hard distinction between *text* (``str``) – sequences of -characters (formally, *Unicode codepoints*) – and ``bytes`` – sequences of -8-bit values used to encode *any* kind of data for storage or transmission. - -Python 2 has the same distinction between ``str`` (bytes) and -``unicode`` (text). -However, values can be implicitly converted between these types as needed, -e.g. when comparing or writing to disk or the network. -The implicit encoding and decoding can be a source of subtle bugs when not -designed and tested adequately. - -In python-ldap 2.x (for Python 2), bytes were used for all fields, -including those guaranteed to be text. - -From version 3.0, python-ldap uses text where appropriate. -On Python 2, the :ref:`bytes mode ` setting influences how text is -handled. - - -What's text, and what's bytes ------------------------------ - The LDAP protocol states that some fields (distinguished names, relative distinguished names, attribute names, queries) be encoded in UTF-8. -In python-ldap, these are represented as text (``str`` on Python 3, -``unicode`` on Python 2). +In python-ldap, these are represented as text (``str`` on Python 3). Attribute *values*, on the other hand, **MAY** contain any type of data, including text. @@ -38,102 +16,26 @@ Thus, attribute values are *always* treated as ``bytes``. Encoding/decoding to other formats – text, images, etc. – is left to the caller. -.. _bytes_mode: - -The bytes mode --------------- - -In Python 3, text values are represented as ``str``, the Unicode text type. - -In Python 2, the behavior of python-ldap 3.0 is influenced by a ``bytes_mode`` -argument to :func:`ldap.initialize`: - -``bytes_mode=True`` (backwards compatible): - Text values are represented as bytes (``str``) encoded using UTF-8. - -``bytes_mode=False`` (future compatible): - Text values are represented as ``unicode``. - -If not given explicitly, python-ldap will default to ``bytes_mode=True``, -but if an ``unicode`` value supplied to it, if will warn and use that value. - -Backwards-compatible behavior is not scheduled for removal until Python 2 -itself reaches end of life. - - -Errors, warnings, and automatic encoding ----------------------------------------- - -While the type of values *returned* from python-ldap is always given by -``bytes_mode``, for Python 2 the behavior for “wrong-type” values *passed in* -can be controlled by the ``bytes_strictness`` argument to -:func:`ldap.initialize`: +Historical note +--------------- -``bytes_strictness='error'`` (default if ``bytes_mode`` is specified): - A ``TypeError`` is raised. - -``bytes_strictness='warn'`` (default when ``bytes_mode`` is not given explicitly): - A warning is raised, and the value is encoded/decoded - using the UTF-8 encoding. - - The warnings are of type :class:`~ldap.LDAPBytesWarning`, which - is a subclass of :class:`BytesWarning` designed to be easily - :ref:`filtered out ` if needed. - -``bytes_strictness='silent'``: - The value is automatically encoded/decoded using the UTF-8 encoding. - -On Python 3, ``bytes_strictness`` is ignored and a ``TypeError`` is always -raised. - -When setting ``bytes_strictness``, an explicit value for ``bytes_mode`` needs -to be given as well. - - -Porting recommendations ------------------------ - -Since end of life of Python 2 is coming in a few years, projects are strongly -urged to make their code compatible with Python 3. General instructions for -this are provided :ref:`in Python documentation ` and in the -`Conservative porting guide`_. - -.. _Conservative porting guide: https://portingguide.readthedocs.io/en/latest/ - - -When porting from python-ldap 2.x, users are advised to update their code -to set ``bytes_mode=False``, and fix any resulting failures. - -The typical usage is as follows. -Note that only the result's *values* are of the ``bytes`` type: - -.. code-block:: pycon - - >>> import ldap - >>> con = ldap.initialize('ldap://localhost:389', bytes_mode=False) - >>> con.simple_bind_s(u'login', u'secret_password') - >>> results = con.search_s(u'ou=people,dc=example,dc=org', ldap.SCOPE_SUBTREE, u"(cn=Raphaël)") - >>> results - [ - ("cn=Raphaël,ou=people,dc=example,dc=org", { - 'cn': [b'Rapha\xc3\xabl'], - 'sn': [b'Barrois'], - }), - ] - - -.. _filter-bytes-warning: - -Filtering warnings ------------------- +Python 3 introduced a hard distinction between *text* (``str``) – sequences of +characters (formally, *Unicode codepoints*) – and ``bytes`` – sequences of +8-bit values used to encode *any* kind of data for storage or transmission. -The bytes mode warnings can be filtered out and ignored with a -simple filter. +Python 2 had the same distinction between ``str`` (bytes) and +``unicode`` (text). +However, values could be implicitly converted between these types as needed, +e.g. when comparing or writing to disk or the network. +The implicit encoding and decoding can be a source of subtle bugs when not +designed and tested adequately. -.. code-block:: python +In python-ldap 2.x (for Python 2), bytes were used for all fields, +including those guaranteed to be text. - import warnings - import ldap +From version 3.0 to 3.3, python-ldap uses text where appropriate. +On Python 2, special ``bytes_mode`` and ``bytes_strictness`` settings +influenced how text was handled. - if hasattr(ldap, 'LDAPBytesWarning'): - warnings.simplefilter('ignore', ldap.LDAPBytesWarning) +From version 3.3 on, only Python 3 is supported. The “bytes mode” settings +are deprecated and do nothing. diff --git a/Doc/conf.py b/Doc/conf.py index e1fb9ee2..b883736e 100644 --- a/Doc/conf.py +++ b/Doc/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # python-ldap documentation build configuration file, created by # sphinx-quickstart on Sat Mar 29 15:08:17 2008. @@ -11,6 +10,7 @@ # All configuration values have a default value; values that are commented out # serve to show the default value. +import datetime import sys import os @@ -34,6 +34,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon', ] try: @@ -54,7 +55,7 @@ # General substitutions. project = 'python-ldap' -copyright = '2008-2017, python-ldap project team' +copyright = 'python-ldap project team' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. diff --git a/Doc/contributing.rst b/Doc/contributing.rst index e6481fec..1fc1365b 100644 --- a/Doc/contributing.rst +++ b/Doc/contributing.rst @@ -108,7 +108,7 @@ Notable targets are: Note that no backups are made – please commit any other changes before using this target. - Requires the ``indent`` program and the ``autopep8`` Python module. + Requires the ``indent`` program and the ``black`` Python module. .. _PEP 7: https://www.python.org/dev/peps/pep-0007/ .. _PEP 8: https://www.python.org/dev/peps/pep-0008/ @@ -221,10 +221,13 @@ If you are tasked with releasing python-ldap, remember to: * Merge all that (using pull requests). * Run ``python setup.py sdist``, and smoke-test the resulting package (install in a clean virtual environment, import ``ldap``). -* Create Git tag ``python-ldap-{version}``, and push it to GitHub and Pagure. +* Create GPG-signed Git tag: ``git tag -s python-ldap-{version}``. + Push it to GitHub and Pagure. * Release the ``sdist`` on PyPI. * Announce the release on the mailing list. Mention the Git hash. * Add the release's log from ``CHANGES`` on the `GitHub release page`_. +* Check that python-ldap.org shows the latest version; if not, adjust + things at readthedocs.org .. _GitHub release page: https://github.com/python-ldap/python-ldap/releases diff --git a/Doc/faq.rst b/Doc/faq.rst index c2e7e153..492b95f2 100644 --- a/Doc/faq.rst +++ b/Doc/faq.rst @@ -29,7 +29,7 @@ Usage .. _pyldap: https://pypi.org/project/pyldap/ -**Q**: Does it work with Python 2.6? (1.5|2.0|2.1|2.2|2.3|2.4|2.5)? +**Q**: Does it work with Python 2.7? (1.5|2.0|2.1|2.2|2.3|2.4|2.5|2.6|2.7)? **A**: No. Old versions of python-ldap are still available from PyPI, though. @@ -45,10 +45,10 @@ That used to work, but after an upgrade it does not work anymore. Why? providing the full functionality. **Q**: My script bound to MS Active Directory but a a search operation results -in the exception :exc:`ldap.OPERATIONS_ERROR` with the diagnostic messages text -“In order to perform this operation a successful bind must be -completed on the connection.” -What's happening here? +in the exception :exc:`ldap.OPERATIONS_ERROR` with the diagnostic message text +*“In order to perform this operation a successful bind must be completed on the +connection.”* Alternatively, a Samba 4 AD returns the diagnostic message +*"Operation unavailable without authentication"*. What's happening here? **A**: When searching from the domain level, MS AD returns referrals (search continuations) for some objects to indicate to the client where to look for these objects. @@ -65,6 +65,10 @@ What's happening here? l = ldap.initialize('ldap://foobar') l.set_option(ldap.OPT_REFERRALS,0) + Note that setting the above option does NOT prevent search continuations + from being returned, rather only that ``libldap`` won't attempt to resolve + referrals. + **Q**: Why am I seeing a ``ldap.SUCCESS`` traceback as output? **A**: Most likely, you are using one of the non-synchronous calls, and probably diff --git a/Doc/installing.rst b/Doc/installing.rst index 90187a93..e4518c11 100644 --- a/Doc/installing.rst +++ b/Doc/installing.rst @@ -111,7 +111,7 @@ Build prerequisites The following software packages are required to be installed on the local system when building python-ldap: -- `Python`_ version 2.7, or 3.4 or later including its development files +- `Python`_ including its development files - C compiler corresponding to your Python version (on Linux, it is usually ``gcc``) - `OpenLDAP`_ client libs version 2.4.11 or later; it is not possible and not supported to build with prior versions. @@ -130,7 +130,7 @@ Alpine Packages for building:: - # apk add build-base openldap-dev python2-dev python3-dev + # apk add build-base openldap-dev python3-dev CentOS ------ @@ -145,17 +145,21 @@ Debian Packages for building and testing:: - # apt-get install build-essential python3-dev python2.7-dev \ - libldap2-dev libsasl2-dev slapd ldap-utils python-tox \ + # apt-get install build-essential python3-dev \ + libldap2-dev libsasl2-dev slapd ldap-utils tox \ lcov valgrind +.. note:: + + On older releases ``tox`` was called ``python-tox``. + Fedora ------ Packages for building and testing:: # dnf install "@C Development Tools and Libraries" openldap-devel \ - python2-devel python3-devel python3-tox \ + python3-devel python3-tox \ lcov clang-analyzer valgrind .. note:: @@ -164,10 +168,10 @@ Packages for building and testing:: newer are required. -setup.cfg -========= +``setup.cfg`` +============= -The file setup.cfg allows to set some build and installation parameters for +The file ``setup.cfg`` allows to set some build and installation parameters for reflecting the local installation of required software packages. Only section ``[_ldap]`` is described here. More information about other sections can be found in :ref:`Setuptools documentation `. diff --git a/Doc/reference/ldap-controls.rst b/Doc/reference/ldap-controls.rst index 1520b241..37d7c1bc 100644 --- a/Doc/reference/ldap-controls.rst +++ b/Doc/reference/ldap-controls.rst @@ -197,6 +197,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 +209,16 @@ search. .. autoclass:: ldap.controls.readentry.PostReadControl :members: + + +:py:mod:`ldap.controls.ppolicy` Password Policy Control +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. seealso:: + `draft-behera-ldap-password-policy `_ + +.. py:module:: ldap.controls.ppolicy + :synopsis: passworld policies + +.. autoclass:: ldap.controls.ppolicy.PasswordPolicyControl + :members: diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index b13aa6f0..57485c7a 100644 --- a/Doc/reference/ldap.rst +++ b/Doc/reference/ldap.rst @@ -29,10 +29,10 @@ Functions This module defines the following functions: -.. py:function:: initialize(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=None, [bytes_mode=None, [bytes_strictness=None]]]]]) -> LDAPObject object +.. py:function:: initialize(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=None, [fileno=None]]]]) -> LDAPObject object Initializes a new connection object for accessing the given LDAP server, - and return an LDAP object (see :ref:`ldap-objects`) used to perform operations + and return an :class:`~ldap.ldapobject.LDAPObject` used to perform operations on that server. The *uri* parameter may be a comma- or whitespace-separated list of URIs @@ -40,6 +40,18 @@ This module defines the following functions: when using multiple URIs you cannot determine to which URI your client gets connected. + 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 @@ -53,22 +65,33 @@ This module defines the following functions: *trace_file* specifies a file-like object as target of the debug log and *trace_stack_limit* specifies the stack limit of tracebacks in debug log. - The *bytes_mode* and *bytes_strictness* arguments specify text/bytes - behavior under Python 2. - See :ref:`text-bytes` for a complete documentation. - Possible values for *trace_level* are :py:const:`0` for no logging, :py:const:`1` for only logging the method calls with arguments, :py:const:`2` for logging the method calls with arguments and the complete results and :py:const:`9` for also logging the traceback of method calls. - Additional keyword arguments are passed to :class:`LDAPObject`. + This function is a thin wrapper around instantiating + :class:`~ldap.ldapobject.LDAPObject`. + Any additional keyword arguments are passed to ``LDAPObject``. + It is also fine to instantiate a ``LDAPObject`` (or a subclass) directly. + + The function additionally takes *bytes_mode* and *bytes_strictness* keyword + arguments, which are deprecated and ignored. See :ref:`bytes_mode` for + details. .. seealso:: :rfc:`4516` - Lightweight Directory Access Protocol (LDAP): Uniform Resource Locator + .. versionadded:: 3.3 + + The *fileno* argument was added. + + .. deprecated:: 3.4 + + *bytes_mode* and *bytes_strictness* arguments are deprecated. + .. py:function:: get_option(option) -> int|string @@ -80,6 +103,12 @@ This module defines the following functions: This function sets the value of the global option specified by *option* to *invalue*. + .. note:: + + Most global settings do not affect existing :py:class:`LDAPObject` + connections. Applications should call :py:func:`set_option()` before + they establish connections with :py:func:`initialize`. + .. versionchanged:: 3.1 The deprecated functions ``ldap.init()`` and ``ldap.open()`` were removed. @@ -112,6 +141,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: @@ -218,33 +253,187 @@ 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 OpenSSL 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` .. py:data:: OPT_X_TLS_DEMAND + Value for :py:const:`OPT_X_TLS_REQUIRE_CERT` + .. py:data:: OPT_X_TLS_HARD -.. py:data:: OPT_X_TLS_KEYFILE + Value for :py:const:`OPT_X_TLS_REQUIRE_CERT` .. py:data:: OPT_X_TLS_NEVER + Value for :py:const:`OPT_X_TLS_REQUIRE_CERT` + +.. py:data:: OPT_X_TLS_TRY + + .. 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 (not supported) + +.. py:data:: OPT_X_TLS_PROTOCOL_MIN + + get/set minimum protocol version (wire protocol version as int) + + * ``0x303`` for TLS 1.2 + * ``0x304`` for TLS 1.3 + +.. py:data:: OPT_X_TLS_VERSION + + Get negotiated TLS protocol version as string + .. py:data:: OPT_X_TLS_RANDOM_FILE -.. py:data:: OPT_X_TLS_REQUIRE_CERT + get/set path to /dev/urandom (**DO NOT USE**) -.. py:data:: OPT_X_TLS_TRY +.. py:data:: OPT_X_TLS + + .. deprecated:: 3.3.0 + The option is deprecated in OpenLDAP and should no longer be used. It + will be removed in the future. + +.. note:: + + OpenLDAP supports several TLS/SSL libraries. OpenSSL is the most common + backend. Some options may not be available when libldap uses NSS, GnuTLS, + or Apple's Secure Transport backend. .. _ldap-keepalive-options: @@ -308,16 +497,27 @@ The module defines the following exceptions: are instead turned into exceptions, raised as soon an the error condition is detected. - The exceptions are accompanied by a dictionary possibly - containing an string value for the key :py:const:`desc` - (giving an English description of the error class) - and/or a string value for the key :py:const:`info` - (giving a string containing more information that the server may have sent). - - A third possible field of this dictionary is :py:const:`matched` and - is set to a truncated form of the name provided or alias dereferenced - for the lowest entry (object or alias) that was matched. - + The exceptions are accompanied by a dictionary with additional information. + All fields are optional and more fields may be added in the future. + Currently, ``python-ldap`` may set the following fields: + + * ``'result'``: a numeric code of the error class. + * ``'desc'``: string giving a description of the error class, as provided + by calling OpenLDAP's ``ldap_err2string`` on the ``result``. + * ``'info'``: string containing more information that the server may + have sent. The value is server-specific: for example, the OpenLDAP server + may send different info messages than Active Directory or 389-DS. + * ``'matched'``: truncated form of the name provided or alias. + dereferenced for the lowest entry (object or alias) that was matched. + * ``'msgid'``: ID of the matching asynchronous request. + This can be used in asynchronous code where :py:meth:`result()` raises the + result of an operation as an exception. For example, this is the case for + :py:meth:`~LDAPObject.compare()`, always raises the boolean result as an + exception (:py:exc:`COMPARE_TRUE` or :py:exc:`COMPARE_FALSE`). + * ``'ctrls'``: list of :py:class:`ldap.controls.LDAPControl` instances + attached to the error. + * ``'errno'``: the C ``errno``, usually set by system calls or ``libc`` + rather than the LDAP libraries. .. py:exception:: ADMINLIMIT_EXCEEDED @@ -351,14 +551,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 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 asynchronous operations, because + :py:meth:`~LDAPObject.compare_s()` returns a boolean result.) .. py:exception:: CONFIDENTIALITY_REQUIRED @@ -455,10 +655,6 @@ The module defines the following exceptions: .. py:exception:: NO_MEMORY -.. py:exception:: NO_OBJECT_CLASS_MODS - - Object class modifications are not allowed. - .. py:exception:: NO_RESULTS_RETURNED .. py:exception:: NO_SUCH_ATTRIBUTE @@ -562,31 +758,42 @@ The above exceptions are raised when a result code from an underlying API call does not indicate success. +.. _ldap-warnings: + Warnings ======== .. py:class:: LDAPBytesWarning - Raised when bytes/text mismatch in non-strict bytes mode. + This warning is deprecated. python-ldap no longer raises it. - See :ref:`bytes_mode` for details. + It used to be raised under Python 2 when bytes/text mismatch in non-strict + bytes mode. See :ref:`bytes_mode` for details. .. versionadded:: 3.0.0 + .. versionchanged:: 3.4.0 + + Deprecated. .. _ldap-objects: LDAPObject classes ================== -.. py:class:: LDAPObject +.. py:class:: ldap.ldapobject.LDAPObject Instances of :py:class:`LDAPObject` are returned by :py:func:`initialize()`. The connection is automatically unbound and closed when the LDAP object is deleted. - Internally :py:class:`LDAPObject` is set to - :py:class:`~ldap.ldapobject.SimpleLDAPObject` by default. + :py:class:`LDAPObject` is an alias of + :py:class:`~ldap.ldapobject.SimpleLDAPObject`, the default connection class. + If you wish to use a different class, instantiate it directly instead of + calling :func:`initialize()`. + + (It is also possible, but not recommended, to change the default by setting + ``ldap.ldapobject.LDAPObject`` to a different class.) .. autoclass:: ldap.ldapobject.SimpleLDAPObject @@ -699,7 +906,9 @@ and wait for and return with the server's result, or with and the value *value*. The synchronous forms returns ``True`` or ``False``. The asynchronous forms returns the message ID of the initiated request, and the result of the asynchronous compare can be obtained using - :py:meth:`result()`. + :py:meth:`result()`. The operation can fail with an exception, e.g. + :py:exc:`ldap.NO_SUCH_OBJECT` when *dn* does not exist or + :py:exc:`ldap.UNDEFINED_TYPE` for an invalid attribute. Note that the asynchronous technique yields the answer by raising the exception objects :py:exc:`ldap.COMPARE_TRUE` or @@ -808,7 +1017,7 @@ and wait for and return with the server's result, or with .. py:method:: LDAPObject.passwd(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> int -.. py:method:: LDAPObject.passwd_s(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> None +.. py:method:: LDAPObject.passwd_s(user, oldpw, newpw [, serverctrls=None [, clientctrls=None] [, extract_newpw=False]]]) -> (respoid, respvalue) Perform a ``LDAP Password Modify Extended Operation`` operation on the entry specified by *user*. @@ -819,6 +1028,13 @@ and wait for and return with the server's result, or with of the specified *user* which is sometimes used when a user changes his own password. + *respoid* is always :py:const:`None`. *respvalue* is also + :py:const:`None` unless *newpw* was :py:const:`None`. This requests that + the server generate a new random password. If *extract_newpw* is + :py:const:`True`, this password is a bytes object available through + ``respvalue.genPasswd``, otherwise *respvalue* is the raw ASN.1 response + (this is deprecated and only for backwards compatibility). + *serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`. The asynchronous version returns the initiated message id. @@ -828,6 +1044,7 @@ and wait for and return with the server's result, or with .. seealso:: :rfc:`3062` - LDAP Password Modify Extended Operation + :py:mod:`ldap.extop.passwd` diff --git a/Doc/reference/ldapurl.rst b/Doc/reference/ldapurl.rst index 96b5ed24..eb2106b3 100644 --- a/Doc/reference/ldapurl.rst +++ b/Doc/reference/ldapurl.rst @@ -9,8 +9,7 @@ This module parses and generates LDAP URLs. It is implemented in pure Python and does not rely on any non-standard modules. Therefore it can be used stand- -alone without the rest of the python-ldap package. Compatibility note: This -module has been solely tested on Python 2.x and above. +alone without the rest of the python-ldap package. .. seealso:: @@ -66,6 +65,11 @@ A :py:class:`LDAPUrl` object represents a complete LDAP URL. .. autoclass:: ldapurl.LDAPUrl :members: + .. versionchanged:: 3.4.0 + + The urlscheme is now case insensitive and always converted to lower + case. ``LDAP://localhost`` is equivalent to ``ldap://localhost``. + LDAP URL extensions ^^^^^^^^^^^^^^^^^^^ diff --git a/Doc/reference/ldif.rst b/Doc/reference/ldif.rst index c508f7dc..87dcb70b 100644 --- a/Doc/reference/ldif.rst +++ b/Doc/reference/ldif.rst @@ -61,11 +61,11 @@ Example The following example demonstrates how to write LDIF output of an LDAP entry with :mod:`ldif` module. ->>> import sys,ldif ->>> entry={'objectClass':['top','person'],'cn':['Michael Stroeder'],'sn':['Stroeder']} +>>> import sys, ldif +>>> entry={'objectClass': [b'top', b'person'], 'cn': [b'Michael Stroeder'], 'sn': [b'Stroeder']} >>> dn='cn=Michael Stroeder,ou=Test' >>> ldif_writer=ldif.LDIFWriter(sys.stdout) ->>> ldif_writer.unparse(dn,entry) +>>> ldif_writer.unparse(dn, entry) dn: cn=Michael Stroeder,ou=Test cn: Michael Stroeder objectClass: top diff --git a/Doc/reference/slapdtest.rst b/Doc/reference/slapdtest.rst index bd54bb69..7517e662 100644 --- a/Doc/reference/slapdtest.rst +++ b/Doc/reference/slapdtest.rst @@ -14,6 +14,8 @@ This module is pure Python and does not rely on any non-standard modules. Therefore it can be used stand-alone without the rest of the python-ldap package. +Test fixtures for the popular `pytest` framework are developed in an external project, `pytest-ldap`_. + Functions ^^^^^^^^^ @@ -26,3 +28,5 @@ Classes .. autoclass:: slapdtest.SlapdTestCase :members: + +.. _pytest-ldap: https://pypi.org/project/pytest-ldap/ diff --git a/Doc/sample_workflow.rst b/Doc/sample_workflow.rst index 8a43553d..76017034 100644 --- a/Doc/sample_workflow.rst +++ b/Doc/sample_workflow.rst @@ -31,8 +31,6 @@ python-ldap won't affect the rest of your system:: $ python3 -m venv __venv__ -(For Python 2, install `virtualenv`_ and use it instead of ``python3 -m venv``.) - .. _git: https://git-scm.com/ .. _virtualenv: https://virtualenv.pypa.io/en/stable/ @@ -63,15 +61,15 @@ This will run tests on all supported versions of Python that you have installed, skipping the ones you don't. To run a subset of test environments, run for example:: - (__venv__)$ tox -e py27,py36 + (__venv__)$ tox -e py36,py39 In addition to ``pyXY`` environments, we have extra environments for checking things independent of the Python version: * ``doc`` checks syntax and spelling of the documentation * ``coverage-report`` generates a test coverage report for Python code. - It must be used last, e.g. ``tox -e py27,py36,coverage-report``. -* ``py2-nosasltls`` and ``py3-nosasltls`` check functionality without + It must be used last, e.g. ``tox -e py36,py39,coverage-report``. +* ``py3-nosasltls`` check functionality without SASL and TLS bindings compiled in. diff --git a/Doc/spelling_wordlist.txt b/Doc/spelling_wordlist.txt index 3ee0e858..c24ab486 100644 --- a/Doc/spelling_wordlist.txt +++ b/Doc/spelling_wordlist.txt @@ -39,6 +39,7 @@ defresult dereferenced dereferencing desc +dev directoryOperation distinguished distributedOperation @@ -55,6 +56,7 @@ filterstr filterStr formatOID func +GPG Heimdal hostport hrefTarget @@ -103,7 +105,9 @@ previousDN processResultsCount Proxied py +pytest rdn +readthedocs reentrant refmodule refreshAndPersist @@ -125,8 +129,10 @@ serverctrls sessionSourceIp sessionSourceName sessionTrackingIdentifier +slapadd sizelimit slapd +startup stderr stdout str @@ -145,6 +151,7 @@ UDP Umich unparsing unsigend +urandom uri urlPrefix urlscheme 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 951f9575..b1797078 100644 --- a/Lib/ldap/__init__.py +++ b/Lib/ldap/__init__.py @@ -33,7 +33,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!') from _ldap import * # call into libldap to initialize it right now LIBLDAP_API_INFO = _ldap.get_option(_ldap.OPT_API_INFO) @@ -54,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 @@ -84,14 +83,14 @@ def acquire(self): if __debug__: global _trace_level if _trace_level>=self._min_trace_level: - _trace_file.write('***%s.acquire() %s %s\n' % (self.__class__.__name__,repr(self),self._desc)) + _trace_file.write('***{}.acquire() {} {}\n'.format(self.__class__.__name__,repr(self),self._desc)) return self._lock.acquire() def release(self): if __debug__: global _trace_level if _trace_level>=self._min_trace_level: - _trace_file.write('***%s.release() %s %s\n' % (self.__class__.__name__,repr(self),self._desc)) + _trace_file.write('***{}.release() {} {}\n'.format(self.__class__.__name__,repr(self),self._desc)) return self._lock.release() diff --git a/Lib/ldap/asyncsearch.py b/Lib/ldap/asyncsearch.py index 6514dd05..6c6929dd 100644 --- a/Lib/ldap/asyncsearch.py +++ b/Lib/ldap/asyncsearch.py @@ -30,7 +30,7 @@ def __init__(self,receivedResultType,expectedResultTypes): Exception.__init__(self) def __str__(self): - return 'Received wrong result type %s (expected one of %s).' % ( + return 'Received wrong result type {} (expected one of {}).'.format( self.receivedResultType, ', '.join(self.expectedResultTypes), ) diff --git a/Lib/ldap/cidict.py b/Lib/ldap/cidict.py index 4f7e0910..f846fd29 100644 --- a/Lib/ldap/cidict.py +++ b/Lib/ldap/cidict.py @@ -5,56 +5,78 @@ See https://www.python-ldap.org/ for details. """ +import warnings +from collections.abc import MutableMapping from ldap import __version__ -from ldap.compat import IterableUserDict +class cidict(MutableMapping): + """ + Case-insensitive but case-respecting dictionary. + """ + __slots__ = ('_keys', '_data') -class cidict(IterableUserDict): - """ - Case-insensitive but case-respecting dictionary. - """ + def __init__(self, default=None): + self._keys = {} + self._data = {} + if default: + self.update(default) + + # MutableMapping abstract methods + + def __getitem__(self, key): + return self._data[key.lower()] + + def __setitem__(self, key, value): + lower_key = key.lower() + self._keys[lower_key] = key + self._data[lower_key] = value + + def __delitem__(self, key): + lower_key = key.lower() + del self._keys[lower_key] + del self._data[lower_key] - def __init__(self,default=None): - self._keys = {} - IterableUserDict.__init__(self,{}) - self.update(default or {}) + def __iter__(self): + return iter(self._keys.values()) - def __getitem__(self,key): - return self.data[key.lower()] + def __len__(self): + return len(self._keys) - def __setitem__(self,key,value): - lower_key = key.lower() - self._keys[lower_key] = key - self.data[lower_key] = value + # Specializations for performance - def __delitem__(self,key): - lower_key = key.lower() - del self._keys[lower_key] - del self.data[lower_key] + def __contains__(self, key): + return key.lower() in self._keys - def update(self,dict): - for key, value in dict.items(): - self[key] = value + def clear(self): + self._keys.clear() + self._data.clear() - def has_key(self,key): - return key in self + def copy(self): + inst = self.__class__.__new__(self.__class__) + inst._data = self._data.copy() + inst._keys = self._keys.copy() + return inst - def __contains__(self,key): - return IterableUserDict.__contains__(self, key.lower()) + __copy__ = copy - def __iter__(self): - return iter(self.keys()) + # Backwards compatibility - def keys(self): - return self._keys.values() + def has_key(self, key): + """Compatibility with python-ldap 2.x""" + return key in self - def items(self): - result = [] - for k in self._keys.values(): - result.append((k,self[k])) - return result + @property + def data(self): + """Compatibility with older IterableUserDict-based implementation""" + warnings.warn( + 'ldap.cidict.cidict.data is an internal attribute; it may be ' + + 'removed at any time', + category=DeprecationWarning, + stacklevel=2, + ) + return self._data def strlist_minus(a,b): @@ -62,6 +84,11 @@ def strlist_minus(a,b): Return list of all items in a which are not in b (a - b). a,b are supposed to be lists of case-insensitive strings. """ + warnings.warn( + "strlist functions are deprecated and will be removed in 3.5", + category=DeprecationWarning, + stacklevel=2, + ) temp = cidict() for elt in b: temp[elt] = elt @@ -77,6 +104,11 @@ def strlist_intersection(a,b): """ Return intersection of two lists of case-insensitive strings a,b. """ + warnings.warn( + "strlist functions are deprecated and will be removed in 3.5", + category=DeprecationWarning, + stacklevel=2, + ) temp = cidict() for elt in a: temp[elt] = elt @@ -92,6 +124,11 @@ def strlist_union(a,b): """ Return union of two lists of case-insensitive strings a,b. """ + warnings.warn( + "strlist functions are deprecated and will be removed in 3.5", + category=DeprecationWarning, + stacklevel=2, + ) temp = cidict() for elt in a: temp[elt] = elt diff --git a/Lib/ldap/compat.py b/Lib/ldap/compat.py index cbfeef57..a287ce4e 100644 --- a/Lib/ldap/compat.py +++ b/Lib/ldap/compat.py @@ -1,113 +1,23 @@ """Compatibility wrappers for Py2/Py3.""" - -import sys -import os - -if sys.version_info[0] < 3: - from UserDict import UserDict, IterableUserDict - from urllib import quote - from urllib import quote_plus - from urllib import unquote as urllib_unquote - from urllib import urlopen - from urlparse import urlparse - - def unquote(uri): - """Specialized unquote that uses UTF-8 for parsing.""" - uri = uri.encode('ascii') - unquoted = urllib_unquote(uri) - return unquoted.decode('utf-8') - - # Old-style of re-raising an exception is SyntaxError in Python 3, - # so hide behind exec() so the Python 3 parser doesn't see it - exec('''def reraise(exc_type, exc_value, exc_traceback): - """Re-raise an exception given information from sys.exc_info() - - Note that unlike six.reraise, this does not support replacing the - traceback. All arguments must come from a single sys.exc_info() call. - """ - raise exc_type, exc_value, exc_traceback - ''') - -else: - from collections import UserDict - IterableUserDict = UserDict - from urllib.parse import quote, quote_plus, unquote, urlparse - from urllib.request import urlopen - - def reraise(exc_type, exc_value, exc_traceback): - """Re-raise an exception given information from sys.exc_info() - - Note that unlike six.reraise, this does not support replacing the - traceback. All arguments must come from a single sys.exc_info() call. - """ - # In Python 3, all exception info is contained in one object. - raise exc_value - -try: - from shutil import which -except ImportError: - # shutil.which() from Python 3.6 - # "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, - # 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; - # All Rights Reserved" - def which(cmd, mode=os.F_OK | os.X_OK, path=None): - """Given a command, mode, and a PATH string, return the path which - conforms to the given mode on the PATH, or None if there is no such - file. - - `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result - of os.environ.get("PATH"), or can be overridden with a custom search - path. - - """ - # Check that a given file can be accessed with the correct mode. - # Additionally check that `file` is not a directory, as on Windows - # directories pass the os.access check. - def _access_check(fn, mode): - return (os.path.exists(fn) and os.access(fn, mode) - and not os.path.isdir(fn)) - - # If we're given a path with a directory part, look it up directly rather - # than referring to PATH directories. This includes checking relative to the - # current directory, e.g. ./script - if os.path.dirname(cmd): - if _access_check(cmd, mode): - return cmd - return None - - if path is None: - path = os.environ.get("PATH", os.defpath) - if not path: - return None - path = path.split(os.pathsep) - - if sys.platform == "win32": - # The current directory takes precedence on Windows. - if not os.curdir in path: - path.insert(0, os.curdir) - - # PATHEXT is necessary to check on Windows. - pathext = os.environ.get("PATHEXT", "").split(os.pathsep) - # See if the given file matches any of the expected path extensions. - # This will allow us to short circuit when given "python.exe". - # If it does match, only test that one, otherwise we have to try - # others. - if any(cmd.lower().endswith(ext.lower()) for ext in pathext): - files = [cmd] - else: - files = [cmd + ext for ext in pathext] - else: - # On other platforms you don't have things like PATHEXT to tell you - # what file suffixes are executable, so just pass on cmd as-is. - files = [cmd] - - seen = set() - for dir in path: - normdir = os.path.normcase(dir) - if not normdir in seen: - seen.add(normdir) - for thefile in files: - name = os.path.join(dir, thefile) - if _access_check(name, mode): - return name - return None +import warnings + +warnings.warn( + "The ldap.compat module is deprecated and will be removed in the future", + DeprecationWarning, +) + +from collections import UserDict +IterableUserDict = UserDict +from urllib.parse import quote, quote_plus, unquote, urlparse +from urllib.request import urlopen +from collections.abc import MutableMapping +from shutil import which + +def reraise(exc_type, exc_value, exc_traceback): + """Re-raise an exception given information from sys.exc_info() + + Note that unlike six.reraise, this does not support replacing the + traceback. All arguments must come from a single sys.exc_info() call. + """ + # In Python 3, all exception info is contained in one object. + raise exc_value diff --git a/Lib/ldap/constants.py b/Lib/ldap/constants.py index 7a7982bb..1c1d76a7 100644 --- a/Lib/ldap/constants.py +++ b/Lib/ldap/constants.py @@ -13,16 +13,15 @@ # This module cannot import anything from ldap. # When building documentation, it is used to initialize ldap.__init__. -from __future__ import print_function -class Constant(object): +class Constant: """Base class for a definition of an OpenLDAP constant """ def __init__(self, name, optional=False, requirements=(), doc=None): self.name = name if optional: - self_requirement = 'defined(LDAP_{})'.format(self.name) + self_requirement = f'defined(LDAP_{self.name})' requirements = list(requirements) + [self_requirement] self.requirements = requirements self.doc = self.__doc__ = doc @@ -50,7 +49,7 @@ class TLSInt(Int): def __init__(self, *args, **kwargs): requrements = list(kwargs.get('requirements', ())) kwargs['requirements'] = ['HAVE_TLS'] + requrements - super(TLSInt, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) class Feature(Constant): @@ -70,7 +69,7 @@ class Feature(Constant): def __init__(self, name, c_feature, **kwargs): - super(Feature, self).__init__(name, **kwargs) + super().__init__(name, **kwargs) self.c_feature = c_feature @@ -281,7 +280,6 @@ class Str(Constant): TLSInt('OPT_X_TLS_DEMAND'), TLSInt('OPT_X_TLS_ALLOW'), TLSInt('OPT_X_TLS_TRY'), - TLSInt('OPT_X_TLS_PEERCERT', optional=True), TLSInt('OPT_X_TLS_VERSION', optional=True), TLSInt('OPT_X_TLS_CIPHER', optional=True), @@ -300,6 +298,9 @@ class Str(Constant): TLSInt('OPT_X_TLS_PROTOCOL_MIN', optional=True), TLSInt('OPT_X_TLS_PACKAGE', optional=True), + # Added in OpenLDAP 2.4.52 + TLSInt('OPT_X_TLS_REQUIRE_SAN', optional=True), + Int('OPT_X_SASL_MECH'), Int('OPT_X_SASL_REALM'), Int('OPT_X_SASL_AUTHCID'), @@ -345,6 +346,7 @@ class Str(Constant): Feature('LIBLDAP_R', 'HAVE_LIBLDAP_R'), Feature('SASL_AVAIL', 'HAVE_SASL'), Feature('TLS_AVAIL', 'HAVE_TLS'), + Feature('INIT_FD_AVAIL', 'HAVE_LDAP_INIT_FD'), Str("CONTROL_MANAGEDSAIT"), Str("CONTROL_PROXY_AUTHZ"), @@ -392,7 +394,7 @@ def pop_requirement(): if requirement not in current_requirements: current_requirements.append(requirement) print() - print('#if {}'.format(requirement)) + print(f'#if {requirement}') print(definition.c_template.format(self=definition)) diff --git a/Lib/ldap/controls/__init__.py b/Lib/ldap/controls/__init__.py index 811b3be1..73557168 100644 --- a/Lib/ldap/controls/__init__.py +++ b/Lib/ldap/controls/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ controls.py - support classes for LDAP controls @@ -13,7 +12,7 @@ import _ldap assert _ldap.__version__==__version__, \ - ImportError('ldap %s and _ldap %s version mismatch!' % (__version__,_ldap.__version__)) + ImportError(f'ldap {__version__} and _ldap {_ldap.__version__} version mismatch!') import ldap diff --git a/Lib/ldap/controls/deref.py b/Lib/ldap/controls/deref.py index b9994eb6..e5b2a7ec 100644 --- a/Lib/ldap/controls/deref.py +++ b/Lib/ldap/controls/deref.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.deref - classes for (see https://tools.ietf.org/html/draft-masarati-ldap-deref) diff --git a/Lib/ldap/controls/libldap.py b/Lib/ldap/controls/libldap.py index f6ea42c4..9a102379 100644 --- a/Lib/ldap/controls/libldap.py +++ b/Lib/ldap/controls/libldap.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ controls.libldap - LDAP controls wrapper classes with en-/decoding done by OpenLDAP functions @@ -10,7 +9,7 @@ import _ldap assert _ldap.__version__==__version__, \ - ImportError('ldap %s and _ldap %s version mismatch!' % (__version__,_ldap.__version__)) + ImportError(f'ldap {__version__} and _ldap {_ldap.__version__} version mismatch!') import ldap diff --git a/Lib/ldap/controls/openldap.py b/Lib/ldap/controls/openldap.py index 5da2dd3f..24040ed7 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 diff --git a/Lib/ldap/controls/pagedresults.py b/Lib/ldap/controls/pagedresults.py index efdd0408..12ca573d 100644 --- a/Lib/ldap/controls/pagedresults.py +++ b/Lib/ldap/controls/pagedresults.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.paged - classes for Simple Paged control (see RFC 2696) diff --git a/Lib/ldap/controls/ppolicy.py b/Lib/ldap/controls/ppolicy.py index 67efe3ad..da7586f0 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) @@ -63,17 +62,31 @@ class PasswordPolicyResponseValue(univ.Sequence): class PasswordPolicyControl(ValueLessRequestControl,ResponseControl): + """ + Indicates the errors and warnings about the password policy. + + Attributes + ---------- + + timeBeforeExpiration : int + The time before the password expires. + + graceAuthNsRemaining : int + The number of grace authentications remaining. + + error: int + The password and authentication errors. + """ controlType = '1.3.6.1.4.1.42.2.27.8.5.1' def __init__(self,criticality=False): self.criticality = criticality - - def decodeControlValue(self,encodedControlValue): - ppolicyValue,_ = decoder.decode(encodedControlValue,asn1Spec=PasswordPolicyResponseValue()) self.timeBeforeExpiration = None self.graceAuthNsRemaining = None self.error = None + def decodeControlValue(self,encodedControlValue): + ppolicyValue,_ = decoder.decode(encodedControlValue,asn1Spec=PasswordPolicyResponseValue()) warning = ppolicyValue.getComponentByName('warning') if warning.hasValue(): if 'timeBeforeExpiration' in warning: diff --git a/Lib/ldap/controls/psearch.py b/Lib/ldap/controls/psearch.py index 002a88e9..32900c8b 100644 --- a/Lib/ldap/controls/psearch.py +++ b/Lib/ldap/controls/psearch.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.psearch - classes for Persistent Search Control (see https://tools.ietf.org/html/draft-ietf-ldapext-psearch) diff --git a/Lib/ldap/controls/pwdpolicy.py b/Lib/ldap/controls/pwdpolicy.py index cf9c1978..54f1a700 100644 --- a/Lib/ldap/controls/pwdpolicy.py +++ b/Lib/ldap/controls/pwdpolicy.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.pwdpolicy - classes for Password Policy controls (see https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy) diff --git a/Lib/ldap/controls/readentry.py b/Lib/ldap/controls/readentry.py index 57cefef4..7b2a7e89 100644 --- a/Lib/ldap/controls/readentry.py +++ b/Lib/ldap/controls/readentry.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.readentry - classes for the Read Entry controls (see RFC 4527) @@ -43,7 +42,7 @@ def decodeControlValue(self,encodedControlValue): self.dn = str(decodedEntry[0]) self.entry = {} for attr in decodedEntry[1]: - self.entry[str(attr[0])] = [ str(attr_value) for attr_value in attr[1] ] + self.entry[str(attr[0])] = [ bytes(attr_value) for attr_value in attr[1] ] class PreReadControl(ReadEntryControl): diff --git a/Lib/ldap/controls/sessiontrack.py b/Lib/ldap/controls/sessiontrack.py index 9c8a057f..a1fb8b34 100644 --- a/Lib/ldap/controls/sessiontrack.py +++ b/Lib/ldap/controls/sessiontrack.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.sessiontrack - class for session tracking control (see draft-wahl-ldap-session) diff --git a/Lib/ldap/controls/simple.py b/Lib/ldap/controls/simple.py index d4130348..05f6760d 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 diff --git a/Lib/ldap/controls/sss.py b/Lib/ldap/controls/sss.py index 5cdfbdac..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) @@ -22,10 +21,6 @@ from pyasn1.type import univ, namedtype, tag, namedval, constraint from pyasn1.codec.ber import encoder, decoder -PY2 = sys.version_info[0] <= 2 -if not PY2: - basestring = str - # SortKeyList ::= SEQUENCE OF SEQUENCE { # attributeType AttributeDescription, @@ -63,7 +58,7 @@ def __init__( ): RequestControl.__init__(self,self.controlType,criticality) self.ordering_rules = ordering_rules - if isinstance(ordering_rules, basestring): + if isinstance(ordering_rules, str): ordering_rules = [ordering_rules] for rule in ordering_rules: rule = rule.split(':') diff --git a/Lib/ldap/controls/vlv.py b/Lib/ldap/controls/vlv.py index 9fea2f03..5fc7ce88 100644 --- a/Lib/ldap/controls/vlv.py +++ b/Lib/ldap/controls/vlv.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.controls.vlv - classes for Virtual List View (see draft-ietf-ldapext-ldapv3-vlv) diff --git a/Lib/ldap/dn.py b/Lib/ldap/dn.py index 00c7b064..a9d96846 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 @@ -29,10 +27,10 @@ def escape_dn_chars(s): s = s.replace(';' ,'\\;') s = s.replace('=' ,'\\=') s = s.replace('\000' ,'\\\000') - if s[0]=='#' or s[0]==' ': - s = ''.join(('\\',s)) if s[-1]==' ': s = ''.join((s[:-1],'\\ ')) + if s[0]=='#' or s[0]==' ': + s = ''.join(('\\',s)) return s @@ -47,8 +45,6 @@ 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) @@ -111,7 +107,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: diff --git a/Lib/ldap/extop/__init__.py b/Lib/ldap/extop/__init__.py index 874166d9..dc9aea2f 100644 --- a/Lib/ldap/extop/__init__.py +++ b/Lib/ldap/extop/__init__.py @@ -28,7 +28,7 @@ def __init__(self,requestName,requestValue): self.requestValue = requestValue def __repr__(self): - return '%s(%s,%s)' % (self.__class__.__name__,self.requestName,self.requestValue) + return f'{self.__class__.__name__}({self.requestName},{self.requestValue})' def encodedRequestValue(self): """ @@ -53,7 +53,7 @@ def __init__(self,responseName,encodedResponseValue): self.responseValue = self.decodeResponseValue(encodedResponseValue) def __repr__(self): - return '%s(%s,%s)' % (self.__class__.__name__,self.responseName,self.responseValue) + return f'{self.__class__.__name__}({self.responseName},{self.responseValue})' def decodeResponseValue(self,value): """ @@ -65,3 +65,4 @@ def decodeResponseValue(self,value): # Import sub-modules from ldap.extop.dds import * +from ldap.extop.passwd import PasswordModifyResponse diff --git a/Lib/ldap/extop/dds.py b/Lib/ldap/extop/dds.py index 4d156e83..7fab0813 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) diff --git a/Lib/ldap/extop/passwd.py b/Lib/ldap/extop/passwd.py new file mode 100644 index 00000000..13e9f252 --- /dev/null +++ b/Lib/ldap/extop/passwd.py @@ -0,0 +1,32 @@ +""" +ldap.extop.passwd - Classes for Password Modify extended operation +(see RFC 3062) + +See https://www.python-ldap.org/ for details. +""" + +from ldap.extop import ExtendedResponse + +# Imports from pyasn1 +from pyasn1.type import namedtype, univ, tag +from pyasn1.codec.der import decoder + + +class PasswordModifyResponse(ExtendedResponse): + responseName = None + + class PasswordModifyResponseValue(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType( + 'genPasswd', + univ.OctetString().subtype( + implicitTag=tag.Tag(tag.tagClassContext, + tag.tagFormatSimple, 0) + ) + ) + ) + + def decodeResponseValue(self, value): + respValue, _ = decoder.decode(value, asn1Spec=self.PasswordModifyResponseValue()) + self.genPasswd = bytes(respValue.getComponentByName('genPasswd')) + return self.genPasswd diff --git a/Lib/ldap/filter.py b/Lib/ldap/filter.py index 3dba7f74..782737aa 100644 --- a/Lib/ldap/filter.py +++ b/Lib/ldap/filter.py @@ -71,7 +71,7 @@ def time_span_filter( if from_timestamp < 0: from_timestamp = until_timestamp + from_timestamp if from_timestamp > until_timestamp: - raise ValueError('from_timestamp %r must not be greater than until_timestamp %r' % ( + raise ValueError('from_timestamp {!r} must not be greater than until_timestamp {!r}'.format( from_timestamp, until_timestamp )) return ( diff --git a/Lib/ldap/functions.py b/Lib/ldap/functions.py index 529c4c8f..8658db40 100644 --- a/Lib/ldap/functions.py +++ b/Lib/ldap/functions.py @@ -27,9 +27,6 @@ # Tracing is only supported in debugging mode import traceback -# See _raise_byteswarning in ldapobject.py -_LDAP_WARN_SKIP_FRAME = True - def _ldap_function_call(lock,func,*args,**kwargs): """ @@ -44,7 +41,7 @@ def _ldap_function_call(lock,func,*args,**kwargs): lock.acquire() if __debug__: if ldap._trace_level>=1: - ldap._trace_file.write('*** %s.%s %s\n' % ( + ldap._trace_file.write('*** {}.{} {}\n'.format( '_ldap',func.__name__, pprint.pformat((args,kwargs)) )) @@ -67,7 +64,7 @@ def _ldap_function_call(lock,func,*args,**kwargs): def initialize( uri, trace_level=0, trace_file=sys.stdout, trace_stack_limit=None, - bytes_mode=None, **kwargs + bytes_mode=None, fileno=None, **kwargs ): """ Return LDAPObject instance by opening LDAP connection to @@ -84,12 +81,17 @@ def initialize( Default is to use stdout. bytes_mode Whether to enable :ref:`bytes_mode` for backwards compatibility under Py2. + fileno + If not None the socket file descriptor is used to connect to an + LDAP server. Additional keyword arguments (such as ``bytes_strictness``) are passed to ``LDAPObject``. """ return LDAPObject( - uri, trace_level, trace_file, trace_stack_limit, bytes_mode, **kwargs) + uri, trace_level, trace_file, trace_stack_limit, bytes_mode, + fileno=fileno, **kwargs + ) def get_option(option): diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index e4e6841a..40091ad7 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -3,9 +3,6 @@ See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - from os import strerror from ldap.pkginfo import __version__, __author__, __license__ @@ -27,44 +24,20 @@ from ldap.schema import SCHEMA_ATTRS from ldap.controls import LDAPControl,DecodeControlTuples,RequestControlTuples -from ldap.extop import ExtendedRequest,ExtendedResponse -from ldap.compat import reraise +from ldap.extop import ExtendedRequest,ExtendedResponse,PasswordModifyResponse from ldap import LDAPError -PY2 = sys.version_info[0] <= 2 -if PY2: - text_type = unicode -else: - text_type = str - - -# See SimpleLDAPObject._bytesify_input -_LDAP_WARN_SKIP_FRAME = True class LDAPBytesWarning(BytesWarning): - """python-ldap bytes mode warning - """ - -def _raise_byteswarning(message): - """Raise LDAPBytesWarning - """ + """Python 2 bytes mode warning""" - # Call stacks that raise the warning tend to be complicated, so - # getting a useful stacklevel is tricky. - # We walk stack frames, ignoring functions in uninteresting files, - # based on the _LDAP_WARN_SKIP_FRAME marker in globals(). - stacklevel = 2 - try: - getframe = sys._getframe - except AttributeError: - pass - else: - frame = sys._getframe(stacklevel) - while frame and frame.f_globals.get('_LDAP_WARN_SKIP_FRAME'): - stacklevel += 1 - frame = frame.f_back - warnings.warn(message, LDAPBytesWarning, stacklevel=stacklevel+1) + def __init__(self, *args, **kwargs): + warnings.warn( + "LDAPBytesWarning is deprecated and will be removed in the future", + DeprecationWarning, + ) + super().__init__(*args, **kwargs) class NO_UNIQUE_ENTRY(ldap.NO_SUCH_OBJECT): @@ -96,196 +69,36 @@ class SimpleLDAPObject: def __init__( self,uri, trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None, - bytes_strictness=None, + bytes_strictness=None, fileno=None ): self._trace_level = trace_level or ldap._trace_level self._trace_file = trace_file or ldap._trace_file self._trace_stack_limit = trace_stack_limit self._uri = uri self._ldap_object_lock = self._ldap_lock('opcall') - self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri) - self.timeout = -1 - self.protocol_version = ldap.VERSION3 - - # Bytes mode - # ---------- - - if PY2: - if bytes_mode is None: - bytes_mode = True - if bytes_strictness is None: - _raise_byteswarning( - "Under Python 2, python-ldap uses bytes by default. " - "This will be removed in Python 3 (no bytes for " - "DN/RDN/field names). " - "Please call initialize(..., bytes_mode=False) explicitly.") - bytes_strictness = 'warn' - else: - if bytes_strictness is None: - bytes_strictness = 'error' - else: - if bytes_mode: - raise ValueError("bytes_mode is *not* supported under Python 3.") - bytes_mode = False - bytes_strictness = 'error' - self.bytes_mode = bytes_mode - self.bytes_strictness = bytes_strictness - - def _bytesify_input(self, arg_name, value): - """Adapt a value following bytes_mode in Python 2. - - In Python 3, returns the original value unmodified. - - With bytes_mode ON, takes bytes or None and returns bytes or None. - With bytes_mode OFF, takes unicode or None and returns bytes or None. - - For the wrong argument type (unicode or bytes, respectively), - behavior depends on the bytes_strictness setting. - In all cases, bytes or None are returned (or an exception is raised). - """ - if not PY2: - return value - if value is None: - return value - - elif self.bytes_mode: - if isinstance(value, bytes): - return value - elif self.bytes_strictness == 'silent': - pass - elif self.bytes_strictness == 'warn': - _raise_byteswarning( - "Received non-bytes value for '{}' in bytes mode; " - "please choose an explicit " - "option for bytes_mode on your LDAP connection".format(arg_name)) - else: - raise TypeError( - "All provided fields *must* be bytes when bytes mode is on; " - "got type '{}' for '{}'.".format(type(value).__name__, arg_name) - ) - return value.encode('utf-8') - else: - if isinstance(value, unicode): - return value.encode('utf-8') - elif self.bytes_strictness == 'silent': - pass - elif self.bytes_strictness == 'warn': - _raise_byteswarning( - "Received non-text value for '{}' with bytes_mode off and " - "bytes_strictness='warn'".format(arg_name)) - else: - raise TypeError( - "All provided fields *must* be text when bytes mode is off; " - "got type '{}' for '{}'.".format(type(value).__name__, arg_name) - ) - return value - - def _bytesify_modlist(self, arg_name, modlist, with_opcode): - """Adapt a modlist according to bytes_mode. - - A modlist is a tuple of (op, attr, value), where: - - With bytes_mode ON, attr is checked to be bytes - - With bytes_mode OFF, attr is converted from unicode to bytes - - value is *always* bytes - """ - if not PY2: - return modlist - if with_opcode: - return tuple( - (op, self._bytesify_input(arg_name, attr), val) - for op, attr, val in modlist + if fileno is not None: + if not hasattr(_ldap, "initialize_fd"): + raise ValueError("libldap does not support initialize_fd") + if hasattr(fileno, "fileno"): + fileno = fileno.fileno() + self._l = ldap.functions._ldap_function_call( + ldap._ldap_module_lock, _ldap.initialize_fd, fileno, uri ) else: - return tuple( - (self._bytesify_input(arg_name, attr), val) - for attr, val in modlist - ) - - def _unbytesify_text_value(self, value): - """Adapt a 'known text, UTF-8 encoded' returned value following bytes_mode. - - With bytes_mode ON, takes bytes or None and returns bytes or None. - With bytes_mode OFF, takes bytes or None and returns unicode or None. - - This function should only be applied on field *values*; distinguished names - or field *names* are already natively handled in result4. - """ - if value is None: - return value - - # Preserve logic of assertions only under Python 2 - if PY2: - assert isinstance(value, bytes), "Expected bytes value, got text instead (%r)" % (value,) - - if self.bytes_mode: - return value - else: - return value.decode('utf-8') - - def _maybe_rebytesify_text(self, value): - """Re-encodes text to bytes if needed by bytes_mode. + self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri) + self.timeout = -1 + self.protocol_version = ldap.VERSION3 - Takes unicode (and checks for it), and returns: - - bytes under bytes_mode - - unicode otherwise. - """ - if not PY2: - return value + if bytes_mode: + raise ValueError("bytes_mode is *not* supported under Python 3.") - if value is None: - return value + @property + def bytes_mode(self): + return False - assert isinstance(value, text_type), "Should return text, got bytes instead (%r)" % (value,) - if not self.bytes_mode: - return value - else: - return value.encode('utf-8') - - def _bytesify_result_value(self, result_value): - """Applies bytes_mode to a result value. - - Such a value can either be: - - a dict mapping an attribute name to its list of values - (where attribute names are unicode and values bytes) - - a list of referals (which are unicode) - """ - if not PY2: - return result_value - if hasattr(result_value, 'items'): - # It's a attribute_name: [values] dict - return { - self._maybe_rebytesify_text(key): value - for (key, value) in result_value.items() - } - elif isinstance(result_value, bytes): - return result_value - else: - # It's a list of referals - # Example value: - # [u'ldap://DomainDnsZones.xxxx.root.local/DC=DomainDnsZones,DC=xxxx,DC=root,DC=local'] - return [self._maybe_rebytesify_text(referal) for referal in result_value] - - def _bytesify_results(self, results, with_ctrls=False): - """Converts a "results" object according to bytes_mode. - - Takes: - - a list of (dn, {field: [values]}) if with_ctrls is False - - a list of (dn, {field: [values]}, ctrls) if with_ctrls is True - - And, if bytes_mode is on, converts dn and fields to bytes. - """ - if not PY2: - return results - if with_ctrls: - return [ - (self._maybe_rebytesify_text(dn), self._bytesify_result_value(fields), ctrls) - for (dn, fields, ctrls) in results - ] - else: - return [ - (self._maybe_rebytesify_text(dn), self._bytesify_result_value(fields)) - for (dn, fields) in results - ] + @property + def bytes_strictness(self): + return 'error' def _ldap_lock(self,desc=''): if ldap.LIBLDAP_R: @@ -301,7 +114,7 @@ def _ldap_call(self,func,*args,**kwargs): self._ldap_object_lock.acquire() if __debug__: if self._trace_level>=1: - self._trace_file.write('*** %s %s - %s\n%s\n' % ( + self._trace_file.write('*** {} {} - {}\n{}\n'.format( repr(self), self._uri, '.'.join((self.__class__.__name__,func.__name__)), @@ -319,18 +132,14 @@ def _ldap_call(self,func,*args,**kwargs): finally: self._ldap_object_lock.release() except LDAPError as e: - exc_type,exc_value,exc_traceback = sys.exc_info() try: if 'info' not in e.args[0] and 'errno' in e.args[0]: e.args[0]['info'] = strerror(e.args[0]['errno']) except IndexError: pass if __debug__ and self._trace_level>=2: - self._trace_file.write('=> LDAPError - %s: %s\n' % (e.__class__.__name__,str(e))) - try: - reraise(exc_type, exc_value, exc_traceback) - finally: - exc_type = exc_value = exc_traceback = None + self._trace_file.write('=> LDAPError - {}: {}\n'.format(e.__class__.__name__,str(e))) + raise else: if __debug__ and self._trace_level>=2: if not diagnostic_message_success is None: @@ -350,7 +159,7 @@ def __getattr__(self,name): elif name in self.__dict__: return self.__dict__[name] else: - raise AttributeError('%s has no attribute %s' % ( + raise AttributeError('{} has no attribute {}'.format( self.__class__.__name__,repr(name) )) @@ -406,9 +215,6 @@ def add_ext(self,dn,modlist,serverctrls=None,clientctrls=None): The parameter modlist is similar to the one passed to modify(), except that no operation integer need be included in the tuples. """ - if PY2: - dn = self._bytesify_input('dn', dn) - modlist = self._bytesify_modlist('modlist', modlist, with_opcode=False) return self._ldap_call(self._l.add_ext,dn,modlist,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def add_ext_s(self,dn,modlist,serverctrls=None,clientctrls=None): @@ -433,9 +239,6 @@ def simple_bind(self,who=None,cred=None,serverctrls=None,clientctrls=None): """ simple_bind([who='' [,cred='']]) -> int """ - if PY2: - who = self._bytesify_input('who', who) - cred = self._bytesify_input('cred', cred) return self._ldap_call(self._l.simple_bind,who,cred,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def simple_bind_s(self,who=None,cred=None,serverctrls=None,clientctrls=None): @@ -512,9 +315,6 @@ def compare_ext(self,dn,attr,value,serverctrls=None,clientctrls=None): A design bug in the library prevents value from containing nul characters. """ - if PY2: - dn = self._bytesify_input('dn', dn) - attr = self._bytesify_input('attr', attr) return self._ldap_call(self._l.compare_ext,dn,attr,value,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def compare_ext_s(self,dn,attr,value,serverctrls=None,clientctrls=None): @@ -526,7 +326,7 @@ def compare_ext_s(self,dn,attr,value,serverctrls=None,clientctrls=None): except ldap.COMPARE_FALSE: return False raise ldap.PROTOCOL_ERROR( - 'Compare operation returned wrong result: %r' % (ldap_res) + f'Compare operation returned wrong result: {ldap_res!r}' ) def compare(self,dn,attr,value): @@ -545,7 +345,6 @@ def delete_ext(self,dn,serverctrls=None,clientctrls=None): form returns the message id of the initiated request, and the result can be obtained from a subsequent call to result(). """ - dn = self._bytesify_input('dn', dn) return self._ldap_call(self._l.delete_ext,dn,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def delete_ext_s(self,dn,serverctrls=None,clientctrls=None): @@ -585,7 +384,7 @@ def extop_s(self,extreq,serverctrls=None,clientctrls=None,extop_resp_class=None) if extop_resp_class: respoid,respvalue = res if extop_resp_class.responseName!=respoid: - raise ldap.PROTOCOL_ERROR("Wrong OID in extended response! Expected %s, got %s" % (extop_resp_class.responseName,respoid)) + raise ldap.PROTOCOL_ERROR(f"Wrong OID in extended response! Expected {extop_resp_class.responseName}, got {respoid}") return extop_resp_class(extop_resp_class.responseName,respvalue) else: return res @@ -594,9 +393,6 @@ def modify_ext(self,dn,modlist,serverctrls=None,clientctrls=None): """ modify_ext(dn, modlist[,serverctrls=None[,clientctrls=None]]) -> int """ - if PY2: - dn = self._bytesify_input('dn', dn) - modlist = self._bytesify_modlist('modlist', modlist, with_opcode=True) return self._ldap_call(self._l.modify_ext,dn,modlist,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def modify_ext_s(self,dn,modlist,serverctrls=None,clientctrls=None): @@ -650,15 +446,18 @@ def modrdn_s(self,dn,newrdn,delold=1): return self.rename_s(dn,newrdn,None,delold) def passwd(self,user,oldpw,newpw,serverctrls=None,clientctrls=None): - if PY2: - user = self._bytesify_input('user', user) - oldpw = self._bytesify_input('oldpw', oldpw) - newpw = self._bytesify_input('newpw', newpw) return self._ldap_call(self._l.passwd,user,oldpw,newpw,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) - def passwd_s(self,user,oldpw,newpw,serverctrls=None,clientctrls=None): - msgid = self.passwd(user,oldpw,newpw,serverctrls,clientctrls) - return self.extop_result(msgid,all=1,timeout=self.timeout) + def passwd_s(self, user, oldpw, newpw, serverctrls=None, clientctrls=None, extract_newpw=False): + msgid = self.passwd(user, oldpw, newpw, serverctrls, clientctrls) + respoid, respvalue = self.extop_result(msgid, all=1, timeout=self.timeout) + + if respoid != PasswordModifyResponse.responseName: + raise ldap.PROTOCOL_ERROR("Unexpected OID %s in extended response!" % respoid) + if extract_newpw and respvalue: + respvalue = PasswordModifyResponse(PasswordModifyResponse.responseName, respvalue) + + return respoid, respvalue def rename(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls=None): """ @@ -675,10 +474,6 @@ def rename(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls This actually corresponds to the rename* routines in the LDAP-EXT C API library. """ - if PY2: - dn = self._bytesify_input('dn', dn) - newrdn = self._bytesify_input('newrdn', newrdn) - newsuperior = self._bytesify_input('newsuperior', newsuperior) return self._ldap_call(self._l.rename,dn,newrdn,newsuperior,delold,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def rename_s(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls=None): @@ -767,8 +562,6 @@ def result4(self,msgid=ldap.RES_ANY,all=1,timeout=None,add_ctrls=0,add_intermedi if add_ctrls: resp_data = [ (t,r,DecodeControlTuples(c,resp_ctrl_classes)) for t,r,c in resp_data ] decoded_resp_ctrls = DecodeControlTuples(resp_ctrls,resp_ctrl_classes) - if resp_data is not None: - resp_data = self._bytesify_results(resp_data, with_ctrls=add_ctrls) return resp_type, resp_data, resp_msgid, decoded_resp_ctrls, resp_name, resp_value def search_ext(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): @@ -816,24 +609,8 @@ def search_ext(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverct The amount of search results retrieved can be limited with the sizelimit parameter if non-zero. """ - - if PY2: - base = self._bytesify_input('base', base) - if filterstr is None: - # workaround for default argument, - # see https://github.com/python-ldap/python-ldap/issues/147 - if self.bytes_mode: - filterstr = b'(objectClass=*)' - else: - filterstr = u'(objectClass=*)' - else: - filterstr = self._bytesify_input('filterstr', filterstr) - if attrlist is not None: - attrlist = tuple(self._bytesify_input('attrlist', a) - for a in attrlist) - else: - if filterstr is None: - filterstr = '(objectClass=*)' + if filterstr is None: + filterstr = '(objectClass=*)' return self._ldap_call( self._l.search_ext, base,scope,filterstr, @@ -930,12 +707,8 @@ def search_subschemasubentry_s(self,dn=None): Returns: None or text/bytes depending on bytes_mode. """ - if self.bytes_mode: - empty_dn = b'' - attrname = b'subschemaSubentry' - else: - empty_dn = u'' - attrname = u'subschemaSubentry' + empty_dn = '' + attrname = 'subschemaSubentry' if dn is None: dn = empty_dn try: @@ -958,9 +731,8 @@ def search_subschemasubentry_s(self,dn=None): # If dn was already root DSE we can return here return None else: - # With legacy bytes mode, return bytes; otherwise, since this is a DN, - # RFCs impose that the field value *can* be decoded to UTF-8. - return self._unbytesify_text_value(search_subschemasubentry_dn) + if search_subschemasubentry_dn is not None: + return search_subschemasubentry_dn.decode('utf-8') except IndexError: return None @@ -988,14 +760,9 @@ def read_subschemasubentry_s(self,subschemasubentry_dn,attrs=None): """ Returns the sub schema sub entry's data """ - if self.bytes_mode: - filterstr = b'(objectClass=subschema)' - if attrs is None: - attrs = [attr.encode('utf-8') for attr in SCHEMA_ATTRS] - else: - filterstr = u'(objectClass=subschema)' - if attrs is None: - attrs = SCHEMA_ATTRS + filterstr = '(objectClass=subschema)' + if attrs is None: + attrs = SCHEMA_ATTRS try: subschemasubentry = self.read_s( subschemasubentry_dn, @@ -1030,12 +797,8 @@ def read_rootdse_s(self, filterstr=None, attrlist=None): """ convenience wrapper around read_s() for reading rootDSE """ - if self.bytes_mode: - base = b'' - attrlist = attrlist or [b'*', b'+'] - else: - base = u'' - attrlist = attrlist or [u'*', u'+'] + base = '' + attrlist = attrlist or ['*', '+'] ldap_rootdse = self.read_s( base, filterstr=filterstr, @@ -1048,10 +811,7 @@ def get_naming_contexts(self): returns all attribute values of namingContexts in rootDSE if namingContexts is not present (not readable) then empty list is returned """ - if self.bytes_mode: - name = b'namingContexts' - else: - name = u'namingContexts' + name = 'namingContexts' return self.read_rootdse_s( attrlist=[name] ).get(name, []) @@ -1086,7 +846,7 @@ class ReconnectLDAPObject(SimpleLDAPObject): def __init__( self,uri, trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None, - bytes_strictness=None, retry_max=1, retry_delay=60.0 + bytes_strictness=None, retry_max=1, retry_delay=60.0, fileno=None ): """ Parameters like SimpleLDAPObject.__init__() with these @@ -1102,7 +862,8 @@ def __init__( self._last_bind = None SimpleLDAPObject.__init__(self, uri, trace_level, trace_file, trace_stack_limit, bytes_mode, - bytes_strictness=bytes_strictness) + bytes_strictness=bytes_strictness, + fileno=fileno) self._reconnect_lock = ldap.LDAPLock(desc='reconnect lock within %s' % (repr(self))) self._retry_max = retry_max self._retry_delay = retry_delay @@ -1162,21 +923,25 @@ def reconnect(self,uri,retry_max=1,retry_delay=60.0): while reconnect_counter: counter_text = '%d. (of %d)' % (retry_max-reconnect_counter+1,retry_max) if __debug__ and self._trace_level>=1: - self._trace_file.write('*** Trying %s reconnect to %s...\n' % ( + self._trace_file.write('*** Trying {} reconnect to {}...\n'.format( counter_text,uri )) try: - # Do the connect - self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri) - self._restore_options() - # StartTLS extended operation in case this was called before - if self._start_tls: - SimpleLDAPObject.start_tls_s(self) - # Repeat last simple or SASL bind - self._apply_last_bind() + try: + # Do the connect + self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri) + self._restore_options() + # StartTLS extended operation in case this was called before + if self._start_tls: + SimpleLDAPObject.start_tls_s(self) + # Repeat last simple or SASL bind + self._apply_last_bind() + except ldap.LDAPError: + SimpleLDAPObject.unbind_s(self) + raise except (ldap.SERVER_DOWN,ldap.TIMEOUT): if __debug__ and self._trace_level>=1: - self._trace_file.write('*** %s reconnect to %s failed\n' % ( + self._trace_file.write('*** {} reconnect to {} failed\n'.format( counter_text,uri )) reconnect_counter = reconnect_counter-1 @@ -1185,10 +950,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 diff --git a/Lib/ldap/logger.py b/Lib/ldap/logger.py index 4db961e3..ae66bd08 100644 --- a/Lib/ldap/logger.py +++ b/Lib/ldap/logger.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- """ Helper class for using logging as trace file object """ import logging -class logging_file_class(object): +class logging_file_class: def __init__(self, logging_level): self._logging_level = logging_level diff --git a/Lib/ldap/modlist.py b/Lib/ldap/modlist.py index 4acf4e9f..bf4e4819 100644 --- a/Lib/ldap/modlist.py +++ b/Lib/ldap/modlist.py @@ -50,7 +50,7 @@ def modifyModlist( case_ignore_attr_types = {v.lower() for v in case_ignore_attr_types or []} modlist = [] attrtype_lower_map = {} - for a in old_entry.keys(): + for a in old_entry: attrtype_lower_map[a.lower()]=a for attrtype, value in new_entry.items(): attrtype_lower = attrtype.lower() diff --git a/Lib/ldap/pkginfo.py b/Lib/ldap/pkginfo.py index df29b60c..ef958a13 100644 --- a/Lib/ldap/pkginfo.py +++ b/Lib/ldap/pkginfo.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- """ meta attributes for packaging which does not import any dependencies """ -__version__ = '3.2.0' -__author__ = u'python-ldap project' +__version__ = '3.4.0' +__author__ = 'python-ldap project' __license__ = 'Python style' diff --git a/Lib/ldap/schema/models.py b/Lib/ldap/schema/models.py index 7520b2b1..d73420c5 100644 --- a/Lib/ldap/schema/models.py +++ b/Lib/ldap/schema/models.py @@ -7,7 +7,7 @@ import sys import ldap.cidict -from ldap.compat import IterableUserDict +from collections import UserDict as IterableUserDict from ldap.schema.tokenizer import split_tokens,extract_tokens @@ -47,7 +47,7 @@ class SchemaElement: } def __init__(self,schema_element_str=None): - if sys.version_info >= (3, 0) and isinstance(schema_element_str, bytes): + if isinstance(schema_element_str, bytes): schema_element_str = schema_element_str.decode('utf-8') if schema_element_str: l = split_tokens(schema_element_str) @@ -69,9 +69,9 @@ def key_attr(self,key,value,quoted=0): assert value is None or type(value)==str,TypeError("value has to be of str, was %r" % value) if value: if quoted: - return " %s '%s'" % (key,value.replace("'","\\'")) + return " {} '{}'".format(key,value.replace("'","\\'")) else: - return " %s %s" % (key,value) + return f" {key} {value}" else: return "" @@ -84,9 +84,9 @@ def key_list(self,key,values,sep=' ',quoted=0): else: quoted_values = values if len(values)==1: - return ' %s %s' % (key,quoted_values[0]) + return ' {} {}'.format(key,quoted_values[0]) else: - return ' %s ( %s )' % (key,sep.join(quoted_values)) + return ' {} ( {} )'.format(key,sep.join(quoted_values)) def __str__(self): result = [str(self.oid)] @@ -133,7 +133,7 @@ class ObjectClass(SchemaElement): implementations to indicate the source of the associated schema element """ - schema_attribute = u'objectClasses' + schema_attribute = 'objectClasses' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -240,7 +240,7 @@ class AttributeType(SchemaElement): implementations to indicate the source of the associated schema element """ - schema_attribute = u'attributeTypes' + schema_attribute = 'attributeTypes' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -334,7 +334,7 @@ class LDAPSyntax(SchemaElement): Integer flag (0 or 1) indicating whether the attribute type is marked as not human-readable (X-NOT-HUMAN-READABLE) """ - schema_attribute = u'ldapSyntaxes' + schema_attribute = 'ldapSyntaxes' token_defaults = { 'DESC':(None,), 'X-NOT-HUMAN-READABLE':(None,), @@ -383,7 +383,7 @@ class MatchingRule(SchemaElement): OID of the LDAP syntax this matching rule is usable with (string, or None if missing) """ - schema_attribute = u'matchingRules' + schema_attribute = 'matchingRules' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -429,7 +429,7 @@ class MatchingRuleUse(SchemaElement): NAMEs or OIDs of attribute types for which this matching rule is used (tuple of strings) """ - schema_attribute = u'matchingRuleUse' + schema_attribute = 'matchingRuleUse' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -489,7 +489,7 @@ class DITContentRule(SchemaElement): NAMEs or OIDs of attributes which may not be present in an entry of the object class. (tuple of strings) """ - schema_attribute = u'dITContentRules' + schema_attribute = 'dITContentRules' token_defaults = { 'NAME':(()), 'DESC':(None,), @@ -547,7 +547,7 @@ class DITStructureRule(SchemaElement): NAMEs or OIDs of allowed structural object classes of superior entries in the DIT (tuple of strings) """ - schema_attribute = u'dITStructureRules' + schema_attribute = 'dITStructureRules' token_defaults = { 'NAME':(()), @@ -610,7 +610,7 @@ class NameForm(SchemaElement): NAMEs or OIDs of additional attributes an RDN may contain (tuple of strings) """ - schema_attribute = u'nameForms' + schema_attribute = 'nameForms' token_defaults = { 'NAME':(()), 'DESC':(None,), diff --git a/Lib/ldap/schema/subentry.py b/Lib/ldap/schema/subentry.py index 5ccbce05..b83d819b 100644 --- a/Lib/ldap/schema/subentry.py +++ b/Lib/ldap/schema/subentry.py @@ -5,10 +5,9 @@ """ import copy +from urllib.request import urlopen import ldap.cidict,ldap.schema - -from ldap.compat import urlopen from ldap.schema.models import * import ldapurl @@ -23,7 +22,7 @@ SCHEMA_CLASS_MAPPING[o.schema_attribute] = o SCHEMA_ATTR_MAPPING[o] = o.schema_attribute -SCHEMA_ATTRS = SCHEMA_CLASS_MAPPING.keys() +SCHEMA_ATTRS = list(SCHEMA_CLASS_MAPPING) class SubschemaError(ValueError): @@ -122,7 +121,7 @@ def __init__(self,sub_schema_sub_entry,check_uniqueness=1): self.sed[se_class][se_id] = se_instance if hasattr(se_instance,'names'): - for name in ldap.cidict.cidict({}.fromkeys(se_instance.names)).keys(): + for name in ldap.cidict.cidict({}.fromkeys(se_instance.names)): if check_uniqueness and name in self.name2oid[se_class]: self.non_unique_names[se_class][se_id] = None raise NameNotUnique(attr_value) @@ -130,7 +129,7 @@ def __init__(self,sub_schema_sub_entry,check_uniqueness=1): self.name2oid[se_class][name] = se_id # Turn dict into list maybe more handy for applications - self.non_unique_oids = self.non_unique_oids.keys() + self.non_unique_oids = list(self.non_unique_oids) return # subSchema.__init__() @@ -168,7 +167,7 @@ def listall(self,schema_element_class,schema_element_filters=None): except AttributeError: pass else: - result = avail_se.keys() + result = list(avail_se) return result @@ -192,7 +191,7 @@ def tree(self,schema_element_class,schema_element_filters=None): # This helps with falsely assigned OIDs. continue assert se_obj.__class__==schema_element_class, \ - "Schema element referenced by %s must be of class %s but was %s" % ( + "Schema element referenced by {} must be of class {} but was {}".format( se_oid,schema_element_class.__name__,se_obj.__class__ ) for s in se_obj.sup or ('_',): @@ -217,7 +216,7 @@ def getoid(self,se_class,nameoroid,raise_keyerror=0): result_oid = self.name2oid[se_class][nameoroid_stripped] except KeyError: if raise_keyerror: - raise KeyError('No registered %s-OID for nameoroid %s' % (se_class.__name__,repr(nameoroid_stripped))) + raise KeyError('No registered {}-OID for nameoroid {}'.format(se_class.__name__,repr(nameoroid_stripped))) else: result_oid = nameoroid_stripped return result_oid @@ -250,7 +249,7 @@ def get_obj(self,se_class,nameoroid,default=None,raise_keyerror=0): se_obj = self.sed[se_class][se_oid] except KeyError: if raise_keyerror: - raise KeyError('No ldap.schema.%s instance with nameoroid %s and se_oid %s' % ( + raise KeyError('No ldap.schema.{} instance with nameoroid {} and se_oid {}'.format( se_class.__name__,repr(nameoroid),repr(se_oid)) ) else: @@ -422,14 +421,14 @@ def attribute_types( # Remove all mandantory attribute types from # optional attribute type list - for a in list(r_may.keys()): + for a in list(r_may): if a in r_must: del r_may[a] # Apply attr_type_filter to results if attr_type_filter: for l in [r_must,r_may]: - for a in list(l.keys()): + for a in list(l): for afk,afv in attr_type_filter: try: schema_attr_type = self.sed[AttributeType][a] @@ -462,7 +461,7 @@ def urlfetch(uri,trace_level=0): l=ldap.initialize(ldap_url.initializeUrl(),trace_level) l.protocol_version = ldap.VERSION3 - l.simple_bind_s(ldap_url.who or u'', ldap_url.cred or u'') + l.simple_bind_s(ldap_url.who or '', ldap_url.cred or '') subschemasubentry_dn = l.search_subschemasubentry_s(ldap_url.dn) if subschemasubentry_dn is None: s_temp = None diff --git a/Lib/ldap/schema/tokenizer.py b/Lib/ldap/schema/tokenizer.py index 20958c09..623b86d5 100644 --- a/Lib/ldap/schema/tokenizer.py +++ b/Lib/ldap/schema/tokenizer.py @@ -13,12 +13,16 @@ r"|" # or r"([^'$()\s]+)" # string of length >= 1 without '$() or whitespace r"|" # or - r"('.*?'(?!\w))" # any string or empty string surrounded by single quotes - # except if right quote is succeeded by alphanumeric char + r"('(?:[^'\\]|\\.)*'(?!\w))" + # any string or empty string surrounded by unescaped + # single quotes except if right quote is succeeded by + # alphanumeric char r"|" # or r"([^\s]+?)", # residue, all non-whitespace strings ).findall +UNESCAPE_PATTERN = re.compile(r"\\(.)") + def split_tokens(s): """ @@ -30,7 +34,7 @@ def split_tokens(s): if unquoted: parts.append(unquoted) elif quoted: - parts.append(quoted[1:-1]) + parts.append(UNESCAPE_PATTERN.sub(r'\1', quoted[1:-1])) elif opar: parens += 1 parts.append(opar) diff --git a/Lib/ldap/syncrepl.py b/Lib/ldap/syncrepl.py index 0de5cec4..1708b468 100644 --- a/Lib/ldap/syncrepl.py +++ b/Lib/ldap/syncrepl.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ ldap.syncrepl - for implementing syncrepl consumer (see RFC 4533) @@ -314,34 +313,35 @@ def __init__(self, encodedMessage): self.refreshPresent = None self.syncIdSet = None - for attr in ['newcookie', 'refreshDelete', 'refreshPresent', 'syncIdSet']: - comp = d[0].getComponentByName(attr) + # Due to the way pyasn1 works, refreshDelete and refreshPresent are both + # valid in the components as they are fully populated defaults. We must + # get the component directly from the message, not by iteration. + attr = d[0].getName() + comp = d[0].getComponent() - if comp is not None and comp.hasValue(): - - if attr == 'newcookie': - self.newcookie = str(comp) - return + if comp is not None and comp.hasValue(): + if attr == 'newcookie': + self.newcookie = str(comp) + return - val = {} + val = {} - cookie = comp.getComponentByName('cookie') - if cookie.hasValue(): - val['cookie'] = str(cookie) + cookie = comp.getComponentByName('cookie') + if cookie.hasValue(): + val['cookie'] = str(cookie) - if attr.startswith('refresh'): - val['refreshDone'] = bool(comp.getComponentByName('refreshDone')) - elif attr == 'syncIdSet': - uuids = [] - ids = comp.getComponentByName('syncUUIDs') - for i in range(len(ids)): - uuid = UUID(bytes=bytes(ids.getComponentByPosition(i))) - uuids.append(str(uuid)) - val['syncUUIDs'] = uuids - val['refreshDeletes'] = bool(comp.getComponentByName('refreshDeletes')) + if attr.startswith('refresh'): + val['refreshDone'] = bool(comp.getComponentByName('refreshDone')) + elif attr == 'syncIdSet': + uuids = [] + ids = comp.getComponentByName('syncUUIDs') + for i in range(len(ids)): + uuid = UUID(bytes=bytes(ids.getComponentByPosition(i))) + uuids.append(str(uuid)) + val['syncUUIDs'] = uuids + val['refreshDeletes'] = bool(comp.getComponentByName('refreshDeletes')) - setattr(self, attr, val) - return + setattr(self, attr, val) class SyncreplConsumer: diff --git a/Lib/ldapurl.py b/Lib/ldapurl.py index 6de06459..cce4e806 100644 --- a/Lib/ldapurl.py +++ b/Lib/ldapurl.py @@ -4,7 +4,7 @@ See https://www.python-ldap.org/ for details. """ -__version__ = '3.2.0' +__version__ = '3.4.0' __all__ = [ # constants @@ -16,7 +16,8 @@ 'LDAPUrlExtension','LDAPUrlExtensions','LDAPUrl' ] -from ldap.compat import UserDict, quote, unquote +from collections.abc import MutableMapping +from urllib.parse import quote, unquote LDAP_SCOPE_BASE = 0 LDAP_SCOPE_ONELEVEL = 1 @@ -47,21 +48,16 @@ def isLDAPUrl(s): + """Returns True if s is a LDAP URL, else False """ - Returns 1 if s is a LDAP URL, 0 else - """ - s_lower = s.lower() - return \ - s_lower.startswith('ldap://') or \ - s_lower.startswith('ldaps://') or \ - s_lower.startswith('ldapi://') + return s.lower().startswith(('ldap://', 'ldaps://', 'ldapi://')) def ldapUrlEscape(s): """Returns URL encoding of string s""" return quote(s).replace(',','%2C').replace('/','%2F') -class LDAPUrlExtension(object): +class LDAPUrlExtension: """ Class for parsing and unparsing LDAP URL extensions as described in RFC 4516. @@ -102,9 +98,9 @@ def _parse(self,extension): def unparse(self): if self.exvalue is None: - return '%s%s' % ('!'*(self.critical>0),self.extype) + return '{}{}'.format('!'*(self.critical>0),self.extype) else: - return '%s%s=%s' % ( + return '{}{}={}'.format( '!'*(self.critical>0), self.extype,quote(self.exvalue or '') ) @@ -113,7 +109,7 @@ def __str__(self): return self.unparse() def __repr__(self): - return '<%s.%s instance at %s: %s>' % ( + return '<{}.{} instance at {}: {}>'.format( self.__class__.__module__, self.__class__.__name__, hex(id(self)), @@ -130,61 +126,74 @@ def __ne__(self,other): return not self.__eq__(other) -class LDAPUrlExtensions(UserDict): - """ - Models a collection of LDAP URL extensions as - dictionary type - """ - - def __init__(self,default=None): - UserDict.__init__(self) - for k,v in (default or {}).items(): - self[k]=v - - def __setitem__(self,name,value): +class LDAPUrlExtensions(MutableMapping): """ - value - Either LDAPUrlExtension instance, (critical,exvalue) - or string'ed exvalue + Models a collection of LDAP URL extensions as + a mapping type """ - assert isinstance(value,LDAPUrlExtension) - assert name==value.extype - self.data[name] = value - - def values(self): - return [ - self[k] - for k in self.keys() - ] - - def __str__(self): - return ','.join(str(v) for v in self.values()) - - def __repr__(self): - return '<%s.%s instance at %s: %s>' % ( - self.__class__.__module__, - self.__class__.__name__, - hex(id(self)), - self.data - ) + __slots__ = ('_data', ) + + def __init__(self, default=None): + self._data = {} + if default is not None: + self.update(default) + + def __setitem__(self, name, value): + """Store an extension + + name + string + value + LDAPUrlExtension instance, whose extype nust match `name` + """ + if not isinstance(value, LDAPUrlExtension): + raise TypeError("value must be LDAPUrlExtension, not " + + type(value).__name__) + if name != value.extype: + raise ValueError( + "key {!r} does not match extension type {!r}".format( + name, value.extype)) + self._data[name] = value + + def __getitem__(self, name): + return self._data[name] + + def __delitem__(self, name): + del self._data[name] + + def __iter__(self): + return iter(self._data) + + def __len__(self): + return len(self._data) + + def __str__(self): + return ','.join(str(v) for v in self.values()) + + def __repr__(self): + return '<{}.{} instance at {}: {}>'.format( + self.__class__.__module__, + self.__class__.__name__, + hex(id(self)), + self._data + ) - def __eq__(self,other): - assert isinstance(other,self.__class__),TypeError( - "other has to be instance of %s" % (self.__class__) - ) - return self.data==other.data + def __eq__(self,other): + if not isinstance(other, self.__class__): + return NotImplemented + return self._data == other._data - def parse(self,extListStr): - for extension_str in extListStr.strip().split(','): - if extension_str: - e = LDAPUrlExtension(extension_str) - self[e.extype] = e + def parse(self,extListStr): + for extension_str in extListStr.strip().split(','): + if extension_str: + e = LDAPUrlExtension(extension_str) + self[e.extype] = e - def unparse(self): - return ','.join([ v.unparse() for v in self.values() ]) + def unparse(self): + return ','.join(v.unparse() for v in self.values()) -class LDAPUrl(object): +class LDAPUrl: """ Class for parsing and unparsing LDAP URLs as described in RFC 4516. @@ -221,7 +230,7 @@ def __init__( extensions=None, who=None,cred=None ): - self.urlscheme=urlscheme + self.urlscheme=urlscheme.lower() self.hostport=hostport self.dn=dn self.attrs=attrs @@ -256,9 +265,7 @@ def _parse(self,ldap_url): if not isLDAPUrl(ldap_url): raise ValueError('Value %s for ldap_url does not seem to be a LDAP URL.' % (repr(ldap_url))) scheme,rest = ldap_url.split('://',1) - self.urlscheme = scheme.strip() - if not self.urlscheme in ['ldap','ldaps','ldapi']: - raise ValueError('LDAP URL contains unsupported URL scheme %s.' % (self.urlscheme)) + self.urlscheme = scheme.lower() slash_pos = rest.find('/') qemark_pos = rest.find('?') if (slash_pos==-1) and (qemark_pos==-1): @@ -326,7 +333,7 @@ def initializeUrl(self): hostport = ldapUrlEscape(self.hostport) else: hostport = self.hostport - return '%s://%s' % (self.urlscheme,hostport) + return f'{self.urlscheme}://{hostport}' def unparse(self): """ @@ -347,7 +354,7 @@ def unparse(self): hostport = ldapUrlEscape(self.hostport) else: hostport = self.hostport - ldap_url = '%s://%s/%s?%s?%s?%s' % ( + ldap_url = '{}://{}/{}?{}?{}?{}'.format( self.urlscheme, hostport,dn,attrs_str,scope_str,filterstr ) @@ -366,24 +373,30 @@ def htmlHREF(self,urlPrefix='',hrefText=None,hrefTarget=None): hrefTarget string added as link target attribute """ - assert type(urlPrefix)==StringType, "urlPrefix must be StringType" + if not isinstance(urlPrefix, str): + raise TypeError("urlPrefix must be str, not " + + type(urlPrefix).__name__) if hrefText is None: - hrefText = self.unparse() - assert type(hrefText)==StringType, "hrefText must be StringType" + hrefText = self.unparse() + if not isinstance(hrefText, str): + raise TypeError("hrefText must be str, not " + + type(hrefText).__name__) if hrefTarget is None: - target = '' + target = '' else: - assert type(hrefTarget)==StringType, "hrefTarget must be StringType" - target = ' target="%s"' % hrefTarget - return '%s' % ( - target,urlPrefix,self.unparse(),hrefText + if not isinstance(hrefTarget, str): + raise TypeError("hrefTarget must be str, not " + + type(hrefTarget).__name__) + target = ' target="%s"' % hrefTarget + return '{}'.format( + target, urlPrefix, self.unparse(), hrefText ) def __str__(self): return self.unparse() def __repr__(self): - return '<%s.%s instance at %s: %s>' % ( + return '<{}.{} instance at {}: {}>'.format( self.__class__.__module__, self.__class__.__name__, hex(id(self)), @@ -400,7 +413,7 @@ def __getattr__(self,name): else: return None else: - raise AttributeError('%s has no attribute %s' % ( + raise AttributeError('{} has no attribute {}'.format( self.__class__.__name__,name )) return result # __getattr__() diff --git a/Lib/ldif.py b/Lib/ldif.py index a26c8ac1..7e69a594 100644 --- a/Lib/ldif.py +++ b/Lib/ldif.py @@ -3,10 +3,7 @@ See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - -__version__ = '3.2.0' +__version__ = '3.4.0' __all__ = [ # constants @@ -25,7 +22,8 @@ from io import StringIO import warnings -from ldap.compat import urlparse, urlopen +from urllib.parse import urlparse +from urllib.request import urlopen attrtype_pattern = r'[\w;.-]+(;[\w_-]+)*' attrvalue_pattern = r'(([^,]|\\,)+|".*?")' diff --git a/Lib/slapdtest/__init__.py b/Lib/slapdtest/__init__.py index 6b8c986f..bb59e7fa 100644 --- a/Lib/slapdtest/__init__.py +++ b/Lib/slapdtest/__init__.py @@ -1,12 +1,12 @@ -# -*- coding: utf-8 -*- """ slapdtest - module for spawning test instances of OpenLDAP's slapd server See https://www.python-ldap.org/ for details. """ -__version__ = '3.2.0' +__version__ = '3.4.0' from slapdtest._slapdtest import SlapdObject, SlapdTestCase, SysLogHandler from slapdtest._slapdtest import requires_ldapi, requires_sasl, requires_tls +from slapdtest._slapdtest import requires_init_fd from slapdtest._slapdtest import skip_unless_ci diff --git a/Lib/slapdtest/_slapdtest.py b/Lib/slapdtest/_slapdtest.py index f1885caf..36841110 100644 --- a/Lib/slapdtest/_slapdtest.py +++ b/Lib/slapdtest/_slapdtest.py @@ -1,12 +1,8 @@ -# -*- coding: utf-8 -*- """ slapdtest - module for spawning test instances of OpenLDAP's slapd server See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - import os import socket import sys @@ -16,43 +12,43 @@ import atexit from logging.handlers import SysLogHandler import unittest +from shutil import which +from urllib.parse import quote_plus # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' import ldap -from ldap.compat import quote_plus, which HERE = os.path.abspath(os.path.dirname(__file__)) -# a template string for generating simple slapd.conf file -SLAPD_CONF_TEMPLATE = r""" -serverID %(serverid)s -moduleload back_%(database)s -%(include_directives)s -loglevel %(loglevel)s -allow bind_v2 - -authz-regexp - "gidnumber=%(root_gid)s\\+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth" - "%(rootdn)s" - -database %(database)s -directory "%(directory)s" -suffix "%(suffix)s" -rootdn "%(rootdn)s" -rootpw "%(rootpw)s" - -TLSCACertificateFile "%(cafile)s" -TLSCertificateFile "%(servercert)s" -TLSCertificateKeyFile "%(serverkey)s" -# ignore missing client cert but fail with invalid client cert -TLSVerifyClient try - -authz-regexp - "C=DE, O=python-ldap, OU=slapd-test, CN=([A-Za-z]+)" - "ldap://ou=people,dc=local???($1)" - +# a template string for generating simple slapd.d file +SLAPD_CONF_TEMPLATE = r"""dn: cn=config +objectClass: olcGlobal +cn: config +olcServerID: %(serverid)s +olcLogLevel: %(loglevel)s +olcAllows: bind_v2 +olcAuthzRegexp: {0}"gidnumber=%(root_gid)s\+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth" "%(rootdn)s" +olcAuthzRegexp: {1}"C=DE, O=python-ldap, OU=slapd-test, CN=([A-Za-z]+)" "ldap://ou=people,dc=local???($1)" +olcTLSCACertificateFile: %(cafile)s +olcTLSCertificateFile: %(servercert)s +olcTLSCertificateKeyFile: %(serverkey)s +olcTLSVerifyClient: try + +dn: cn=module,cn=config +objectClass: olcModuleList +cn: module +olcModuleLoad: back_%(database)s + +dn: olcDatabase=%(database)s,cn=config +objectClass: olcDatabaseConfig +objectClass: olcMdbConfig +olcDatabase: %(database)s +olcSuffix: %(suffix)s +olcRootDN: %(rootdn)s +olcRootPW: %(rootpw)s +olcDbDirectory: %(directory)s """ LOCALHOST = '127.0.0.1' @@ -109,6 +105,14 @@ def requires_ldapi(): else: return identity +def requires_init_fd(): + if not ldap.INIT_FD_AVAIL: + return skip_unless_ci( + "test needs ldap.INIT_FD", feature='INIT_FD') + else: + return identity + + def _add_sbin(path): """Add /sbin and related directories to a command search path""" directories = path.split(os.pathsep) @@ -154,7 +158,7 @@ def combined_logger( return new_logger # end of combined_logger() -class SlapdObject(object): +class SlapdObject: """ Controller class for a slapd instance, OpenLDAP's server. @@ -169,6 +173,9 @@ class SlapdObject(object): manager, the slapd server is shut down and the temporary data store is removed. + :param openldap_schema_files: A list of schema names or schema paths to + load at startup. By default this only contains `core`. + .. versionchanged:: 3.1 Added context manager functionality @@ -179,12 +186,12 @@ class SlapdObject(object): root_cn = 'Manager' root_pw = 'password' slapd_loglevel = 'stats stats2' - local_host = '127.0.0.1' + local_host = LOCALHOST testrunsubdirs = ( - 'schema', + 'slapd.d', ) openldap_schema_files = ( - 'core.schema', + 'core.ldif', ) TMPDIR = os.environ.get('TMP', os.getcwd()) @@ -200,9 +207,6 @@ class SlapdObject(object): BIN_PATH = os.environ.get('BIN', os.environ.get('PATH', os.defpath)) SBIN_PATH = os.environ.get('SBIN', _add_sbin(BIN_PATH)) - # time in secs to wait before trying to access slapd via LDAP (again) - _start_sleep = 1.5 - # create loggers once, multiple calls mess up refleak tests _log = combined_logger('python-ldap-test') @@ -211,10 +215,9 @@ def __init__(self): self._port = self._avail_tcp_port() self.server_id = self._port % 4096 self.testrundir = os.path.join(self.TMPDIR, 'python-ldap-test-%d' % self._port) - self._schema_prefix = os.path.join(self.testrundir, 'schema') - self._slapd_conf = os.path.join(self.testrundir, 'slapd.conf') + self._slapd_conf = os.path.join(self.testrundir, 'slapd.d') self._db_directory = os.path.join(self.testrundir, "openldap-data") - self.ldap_uri = "ldap://%s:%d/" % (LOCALHOST, self._port) + self.ldap_uri = "ldap://%s:%d/" % (self.local_host, self._port) if HAVE_LDAPI: ldapi_path = os.path.join(self.testrundir, 'ldapi') self.ldapi_uri = "ldapi://%s" % quote_plus(ldapi_path) @@ -243,16 +246,24 @@ def __init__(self): def root_dn(self): return 'cn={self.root_cn},{self.suffix}'.format(self=self) + @property + def hostname(self): + return self.local_host + + @property + def port(self): + return self._port + def _find_commands(self): self.PATH_LDAPADD = self._find_command('ldapadd') self.PATH_LDAPDELETE = self._find_command('ldapdelete') self.PATH_LDAPMODIFY = self._find_command('ldapmodify') self.PATH_LDAPWHOAMI = self._find_command('ldapwhoami') + self.PATH_SLAPADD = self._find_command('slapadd') self.PATH_SLAPD = os.environ.get('SLAPD', None) if not self.PATH_SLAPD: self.PATH_SLAPD = self._find_command('slapd', in_sbin=True) - self.PATH_SLAPTEST = self._find_command('slaptest', in_sbin=True) def _find_command(self, cmd, in_sbin=False): if in_sbin: @@ -279,7 +290,6 @@ def setup_rundir(self): os.mkdir(self.testrundir) os.mkdir(self._db_directory) self._create_sub_dirs(self.testrunsubdirs) - self._ln_schema_files(self.openldap_schema_files, self.SCHEMADIR) def _cleanup_rundir(self): """ @@ -324,17 +334,8 @@ def gen_config(self): for generating specific static configuration files you have to override this method """ - include_directives = '\n'.join( - 'include "{schema_prefix}/{schema_file}"'.format( - schema_prefix=self._schema_prefix, - schema_file=schema_file, - ) - for schema_file in self.openldap_schema_files - ) config_dict = { 'serverid': hex(self.server_id), - 'schema_prefix':self._schema_prefix, - 'include_directives': include_directives, 'loglevel': self.slapd_loglevel, 'database': self.database, 'directory': self._db_directory, @@ -358,37 +359,39 @@ def _create_sub_dirs(self, dir_names): self._log.debug('Create directory %s', dir_name) os.mkdir(dir_name) - def _ln_schema_files(self, file_names, source_dir): - """ - write symbolic links to original schema files - """ - for fname in file_names: - ln_source = os.path.join(source_dir, fname) - ln_target = os.path.join(self._schema_prefix, fname) - self._log.debug('Create symlink %s -> %s', ln_source, ln_target) - os.symlink(ln_source, ln_target) - def _write_config(self): - """Writes the slapd.conf file out, and returns the path to it.""" - self._log.debug('Writing config to %s', self._slapd_conf) - with open(self._slapd_conf, 'w') as config_file: - config_file.write(self.gen_config()) - self._log.info('Wrote config to %s', self._slapd_conf) + """Loads the slapd.d configuration.""" + self._log.debug("importing configuration: %s", self._slapd_conf) + + self.slapadd(self.gen_config(), ["-n0"]) + ldif_paths = [ + schema + if os.path.exists(schema) + else os.path.join(self.SCHEMADIR, schema) + for schema in self.openldap_schema_files + ] + for ldif_path in ldif_paths: + self.slapadd(None, ["-n0", "-l", ldif_path]) + + self._log.debug("import ok: %s", self._slapd_conf) def _test_config(self): self._log.debug('testing config %s', self._slapd_conf) popen_list = [ - self.PATH_SLAPTEST, - "-f", self._slapd_conf, - '-u', + self.PATH_SLAPD, + "-Ttest", + "-F", self._slapd_conf, + "-u", + "-v", + "-d", "config" ] - if self._log.isEnabledFor(logging.DEBUG): - popen_list.append('-v') - popen_list.extend(['-d', 'config']) - else: - popen_list.append('-Q') - proc = subprocess.Popen(popen_list) - if proc.wait() != 0: + p = subprocess.run( + popen_list, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + if p.returncode != 0: + self._log.error(p.stdout.decode("utf-8")) raise RuntimeError("configuration test failed") self._log.info("config ok: %s", self._slapd_conf) @@ -401,8 +404,7 @@ def _start_slapd(self): urls.append(self.ldapi_uri) slapd_args = [ self.PATH_SLAPD, - '-f', self._slapd_conf, - '-F', self.testrundir, + '-F', self._slapd_conf, '-h', ' '.join(urls), ] if self._log.isEnabledFor(logging.DEBUG): @@ -412,20 +414,22 @@ def _start_slapd(self): self._log.info('starting slapd: %r', ' '.join(slapd_args)) self._proc = subprocess.Popen(slapd_args) # Waits until the LDAP server socket is open, or slapd crashed + deadline = time.monotonic() + 10 # no cover to avoid spurious coverage changes, see # https://github.com/python-ldap/python-ldap/issues/127 - for _ in range(10): # pragma: no cover + while True: # pragma: no cover if self._proc.poll() is not None: self._stopped() raise RuntimeError("slapd exited before opening port") - time.sleep(self._start_sleep) try: self._log.debug( "slapd connection check to %s", self.default_ldap_uri ) self.ldapwhoami() except RuntimeError: - pass + if time.monotonic() >= deadline: + break + time.sleep(0.2) else: return raise RuntimeError("slapd did not start properly") @@ -457,15 +461,7 @@ def stop(self): self._proc.terminate() self.wait() self._cleanup_rundir() - if hasattr(atexit, 'unregister'): - # Python 3 - atexit.unregister(self.stop) - elif hasattr(atexit, '_exithandlers'): - # Python 2, can be None during process shutdown - try: - atexit._exithandlers.remove(self.stop) - except ValueError: - pass + atexit.unregister(self.stop) def restart(self): """ @@ -507,10 +503,14 @@ def _cli_popen(self, ldapcommand, extra_args=None, ldap_uri=None, stdin_data=None): # pragma: no cover if ldap_uri is None: ldap_uri = self.default_ldap_uri - args = [ - ldapcommand, - '-H', ldap_uri, - ] + self._cli_auth_args() + (extra_args or []) + + if ldapcommand.split("/")[-1].startswith("ldap"): + args = [ldapcommand, '-H', ldap_uri] + self._cli_auth_args() + else: + args = [ldapcommand, '-F', self._slapd_conf] + + args += (extra_args or []) + self._log.debug('Run command: %r', ' '.join(args)) proc = subprocess.Popen( args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, @@ -561,6 +561,16 @@ def ldapdelete(self, dn, recursive=False, extra_args=None): extra_args.append(dn) self._cli_popen(self.PATH_LDAPDELETE, extra_args=extra_args) + def slapadd(self, ldif, extra_args=None): + """ + Runs slapadd on this slapd instance, passing it the ldif content + """ + self._cli_popen( + self.PATH_SLAPADD, + stdin_data=ldif.encode("utf-8") if ldif else None, + extra_args=extra_args, + ) + def __enter__(self): self.start() return self diff --git a/Lib/slapdtest/certs/ca.conf b/Lib/slapdtest/certs/ca.conf index 5046b0d6..d1d89e18 100644 --- a/Lib/slapdtest/certs/ca.conf +++ b/Lib/slapdtest/certs/ca.conf @@ -32,7 +32,7 @@ serial = $tmpdir/$ca.crt.srl crlnumber = $tmpdir/$ca.crl.srl database = $tmpdir/$ca.db unique_subject = no -default_days = 3652 +default_days = 365200 default_md = sha256 policy = match_pol email_in_dn = no @@ -40,7 +40,7 @@ preserve = no name_opt = $name_opt cert_opt = ca_default copy_extensions = none -default_crl_days = 3651 +default_crl_days = 365100 [match_pol] countryName = match diff --git a/Lib/slapdtest/certs/ca.pem b/Lib/slapdtest/certs/ca.pem index cf2ff33c..b52ffafb 100644 --- a/Lib/slapdtest/certs/ca.pem +++ b/Lib/slapdtest/certs/ca.pem @@ -5,31 +5,31 @@ Certificate: Signature Algorithm: sha256WithRSAEncryption Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA Validity - Not Before: Dec 2 11:57:47 2017 GMT - Not After : Sep 4 11:57:47 2027 GMT + Not Before: Apr 12 18:52:38 2019 GMT + Not After : Oct 17 18:52:38 2994 GMT Subject: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: - 00:af:1f:cf:0f:c5:95:66:2d:eb:85:cc:21:fc:0d: - 0f:44:d8:2f:a8:85:08:ef:60:67:57:fa:0b:c5:e4: - b3:fb:f1:6f:cb:30:7a:47:0d:a7:f1:b5:37:81:5f: - f6:39:28:e2:f9:4d:6c:2e:a6:5c:0e:3c:db:4d:c9: - 2a:64:ce:0d:15:30:c7:75:52:b8:74:c5:0b:00:4c: - 2f:94:1b:dd:fb:83:2c:58:02:73:b0:86:3a:6a:aa: - 55:f2:d5:49:99:17:a5:e2:44:ec:dd:62:5f:8d:ce: - 77:29:0b:8d:87:23:e2:4b:d6:1c:25:f3:06:a9:ee: - 33:6f:ac:ed:22:9e:35:ec:55:e7:1b:38:68:7e:46: - e3:c3:42:ac:06:0b:0a:7a:84:c9:3d:ef:3d:a5:6e: - e9:10:24:c3:28:fe:1f:4a:9a:23:8a:3c:db:0a:66: - 5d:07:f8:c5:17:68:53:e4:0e:37:33:c4:d2:ad:58: - 62:6b:8a:87:ab:73:eb:bc:2b:ac:07:69:84:8d:e3: - c4:a9:78:9b:6c:1e:03:63:df:b4:96:18:bd:3c:2e: - be:7f:2c:d5:a8:f8:12:b9:ab:27:52:b0:de:38:62: - 3c:54:a7:f3:aa:37:a3:11:12:b2:a7:6f:8d:96:10: - ce:01:cb:25:24:a6:51:18:93:69:9b:9e:5c:8a:ff: - fe:89 + 00:d7:30:73:20:44:7d:83:d4:c7:01:b8:ab:1e:7c: + 91:f4:38:ac:9c:41:43:64:0c:31:99:48:70:22:7d: + ae:1b:47:e7:2a:28:4d:f7:46:4e:b4:ba:ae:c0:9d: + d5:1f:4b:7a:79:2f:b9:dc:68:7f:79:84:88:50:51: + 3b:7d:dc:d5:57:17:66:45:c0:2c:20:13:f7:99:d6: + 9d:e2:12:7c:41:76:82:51:19:2c:b6:ff:46:cb:04: + 56:38:22:2a:c3:7a:b5:71:51:49:4e:62:68:a0:99: + 6f:de:f3:a2:0f:a2:aa:1b:72:a5:87:bc:42:5a:a7: + 22:8d:33:b4:88:a8:dc:5d:72:ca:dd:a0:9a:4e:db: + 7d:8b:10:de:c5:41:e9:e9:8d:fa:6c:dd:94:6e:b1: + 31:c2:6d:a1:69:6c:7a:3a:b2:76:65:c9:e5:95:38: + 62:40:81:c6:29:26:26:d1:d1:c1:f4:5e:fa:24:ef: + 13:da:24:13:6f:f5:5c:ba:b1:31:8f:30:94:71:7b: + c6:e5:da:b9:b5:64:39:39:09:c2:4a:80:64:58:1d: + 99:f5:65:3c:a7:26:08:95:26:35:7b:fa:e7:20:08: + ff:72:df:9b:8f:9f:da:8b:c3:a7:8b:fc:8c:c0:a5: + 31:87:1d:4c:14:f6:cf:90:5e:2e:6e:a6:db:27:08: + eb:df Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: critical @@ -37,44 +37,44 @@ Certificate: X509v3 Key Usage: critical Certificate Sign, CRL Sign X509v3 Subject Key Identifier: - 3B:1F:32:F4:FE:57:D1:6F:49:91:55:F2:24:F1:0A:66:3B:A5:EE:D4 + BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2 X509v3 Authority Key Identifier: - keyid:3B:1F:32:F4:FE:57:D1:6F:49:91:55:F2:24:F1:0A:66:3B:A5:EE:D4 + keyid:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2 Signature Algorithm: sha256WithRSAEncryption - 0a:e7:dc:38:ce:03:dd:a8:99:11:d0:24:be:ef:1a:18:9d:7c: - 95:75:4a:4a:29:44:23:28:fc:66:d5:81:ce:05:c2:c0:6b:71: - d6:8d:33:a9:53:a6:1c:f1:4e:50:ae:a3:b1:72:d6:69:53:ad: - a9:62:a9:45:27:68:17:35:41:97:ec:e9:65:91:62:12:ed:eb: - 45:3a:9b:cc:09:bc:e3:ad:22:6b:13:6b:b0:67:ef:ce:01:83: - 5e:6c:95:e2:b3:73:b9:69:9a:33:49:f9:5f:52:4e:39:94:c9: - db:93:6f:d8:ba:10:92:ce:fa:12:6b:bc:31:ff:c1:67:70:63: - 07:dc:53:7a:3a:a3:51:20:15:44:cf:1c:a9:cd:b7:30:1d:8e: - 55:93:8a:56:8c:3d:e9:8b:ae:0c:77:8d:5c:8b:fd:22:d8:4c: - 3e:e4:76:e8:d9:e8:c3:98:f4:98:ff:02:60:95:8e:3e:26:7a: - e2:fe:2c:0a:a4:52:8d:4c:3d:dd:4c:fd:2f:2c:db:83:4c:2b: - 25:24:37:78:9a:07:27:52:f9:1c:c0:65:65:cb:50:77:b4:2d: - fa:f4:af:bb:42:1c:43:65:c6:01:6e:f1:4b:fe:b8:4a:3c:29: - 8b:b6:84:1e:17:99:61:98:65:fe:f2:e9:ce:bb:ac:87:69:cb: - e6:13:42:bf + 06:20:1f:eb:42:6a:42:62:b1:ee:69:c8:cd:47:a6:2e:69:95: + 59:dc:49:09:69:40:93:25:a1:ec:6d:3a:dd:dc:e5:74:ab:33: + 9d:8f:cc:e3:bb:7a:3f:5b:51:58:74:f7:bd:6c:7c:3c:b6:5a: + 05:50:a8:8c:c3:fb:5b:75:2a:c2:6c:06:93:4c:a9:93:71:1c: + 51:e5:be:a1:24:93:e2:79:ca:ea:08:86:90:b9:70:e7:7a:40: + bf:f4:d6:71:f4:4d:c0:0f:e0:31:a0:23:46:77:30:72:a9:62: + 8a:2a:12:c4:dd:3d:86:ae:f7:6b:33:80:26:58:49:53:ff:cd: + 8a:c6:f6:11:2c:b3:ff:a5:8e:1c:f8:22:e2:1b:8e:04:33:fb: + 0d:da:31:86:12:9f:d1:03:86:9c:6a:78:5e:3c:5e:8a:52:aa: + 68:1f:ff:f9:17:75:b0:da:f2:99:3c:80:3c:96:2a:33:07:54: + 59:84:e7:92:34:0f:99:76:e3:d6:4d:4d:9c:fb:21:35:f9:cb: + a5:30:80:8b:9d:61:90:d3:d4:59:3a:2f:f2:f6:20:13:7e:26: + dc:50:b0:49:3e:19:fe:eb:7d:cf:b9:1a:5d:5c:3a:76:30:d9: + 0e:d7:df:de:ce:a9:c4:21:df:63:b9:d0:64:86:0b:28:9a:2e: + ab:51:73:e4 -----BEGIN CERTIFICATE----- -MIIDijCCAnKgAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU +MIIDjDCCAnSgAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV -BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwHhcNMTcxMjAyMTE1NzQ3WhcNMjcwOTA0 -MTE1NzQ3WjBWMQswCQYDVQQGEwJERTEUMBIGA1UECgwLcHl0aG9uLWxkYXAxEzAR -BgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNVBAMME1B5dGhvbiBMREFQIFRlc3QgQ0Ew -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvH88PxZVmLeuFzCH8DQ9E -2C+ohQjvYGdX+gvF5LP78W/LMHpHDafxtTeBX/Y5KOL5TWwuplwOPNtNySpkzg0V -MMd1Urh0xQsATC+UG937gyxYAnOwhjpqqlXy1UmZF6XiROzdYl+NzncpC42HI+JL -1hwl8wap7jNvrO0injXsVecbOGh+RuPDQqwGCwp6hMk97z2lbukQJMMo/h9KmiOK -PNsKZl0H+MUXaFPkDjczxNKtWGJrioerc+u8K6wHaYSN48SpeJtsHgNj37SWGL08 -Lr5/LNWo+BK5qydSsN44YjxUp/OqN6MRErKnb42WEM4ByyUkplEYk2mbnlyK//6J -AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud -DgQWBBQ7HzL0/lfRb0mRVfIk8QpmO6Xu1DAfBgNVHSMEGDAWgBQ7HzL0/lfRb0mR -VfIk8QpmO6Xu1DANBgkqhkiG9w0BAQsFAAOCAQEACufcOM4D3aiZEdAkvu8aGJ18 -lXVKSilEIyj8ZtWBzgXCwGtx1o0zqVOmHPFOUK6jsXLWaVOtqWKpRSdoFzVBl+zp -ZZFiEu3rRTqbzAm8460iaxNrsGfvzgGDXmyV4rNzuWmaM0n5X1JOOZTJ25Nv2LoQ -ks76Emu8Mf/BZ3BjB9xTejqjUSAVRM8cqc23MB2OVZOKVow96YuuDHeNXIv9IthM -PuR26Nnow5j0mP8CYJWOPiZ64v4sCqRSjUw93Uz9Lyzbg0wrJSQ3eJoHJ1L5HMBl -ZctQd7Qt+vSvu0IcQ2XGAW7xS/64Sjwpi7aEHheZYZhl/vLpzrush2nL5hNCvw== +BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwIBcNMTkwNDEyMTg1MjM4WhgPMjk5NDEw +MTcxODUyMzhaMFYxCzAJBgNVBAYTAkRFMRQwEgYDVQQKDAtweXRob24tbGRhcDET +MBEGA1UECwwKc2xhcGQtdGVzdDEcMBoGA1UEAwwTUHl0aG9uIExEQVAgVGVzdCBD +QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANcwcyBEfYPUxwG4qx58 +kfQ4rJxBQ2QMMZlIcCJ9rhtH5yooTfdGTrS6rsCd1R9Lenkvudxof3mEiFBRO33c +1VcXZkXALCAT95nWneISfEF2glEZLLb/RssEVjgiKsN6tXFRSU5iaKCZb97zog+i +qhtypYe8QlqnIo0ztIio3F1yyt2gmk7bfYsQ3sVB6emN+mzdlG6xMcJtoWlsejqy +dmXJ5ZU4YkCBxikmJtHRwfRe+iTvE9okE2/1XLqxMY8wlHF7xuXaubVkOTkJwkqA +ZFgdmfVlPKcmCJUmNXv65yAI/3Lfm4+f2ovDp4v8jMClMYcdTBT2z5BeLm6m2ycI +698CAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFL141UrxkJbF6OxmSSNHA18mc4ayMB8GA1UdIwQYMBaAFL141UrxkJbF +6OxmSSNHA18mc4ayMA0GCSqGSIb3DQEBCwUAA4IBAQAGIB/rQmpCYrHuacjNR6Yu +aZVZ3EkJaUCTJaHsbTrd3OV0qzOdj8zju3o/W1FYdPe9bHw8tloFUKiMw/tbdSrC +bAaTTKmTcRxR5b6hJJPiecrqCIaQuXDnekC/9NZx9E3AD+AxoCNGdzByqWKKKhLE +3T2GrvdrM4AmWElT/82KxvYRLLP/pY4c+CLiG44EM/sN2jGGEp/RA4acanhePF6K +UqpoH//5F3Ww2vKZPIA8liozB1RZhOeSNA+ZduPWTU2c+yE1+culMICLnWGQ09RZ +Oi/y9iATfibcULBJPhn+633PuRpdXDp2MNkO19/ezqnEId9judBkhgsomi6rUXPk -----END CERTIFICATE----- diff --git a/Lib/slapdtest/certs/client.key b/Lib/slapdtest/certs/client.key index 70600ba5..7213c0b4 100644 --- a/Lib/slapdtest/certs/client.key +++ b/Lib/slapdtest/certs/client.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDGxvbMEbahViK4 -P6aoWUkciIf1dEYTBuU8M1eShREUZ3Ytq/ee425pXRyxxDrAa8ygRjqs7tauwhgA -KNuPRGyw7hyZ1Ku4vQObwX9rzyHQ6Fj606U4HHbYfAVb0AF7OzLbhNH3isCRtNcm -EUSYG1Nkfn9zQkV4pz2KJM4ePt4GyGJV0NhRUdHdwgsWiuRt2EIRPwLXdkf/4svm -2EahAJax+SMaZYe/lg9w9SBxl6DTaF9lFpoPqzeW+KnXCmE8e+qSWea8EjkzIRXa -4KJ/EnUMP2fUL+Je2agGF2YfCId9YvLTQV8YDv7Im1Sp0sbIUDoWX14GDUbZ8cBr -wfOyI0TfAgMBAAECggEATWv1eGp1zcU05Lq1+OA938U1316YZJTM+HOu6jy1+FKL -7yIJ4nMG8Db6FCswDv5txwdTl0O3jn2+x2Eik1y9UPSNY0U4VU4Zd7MYJC+bJjk5 -XwjMU1yS1aMIm0gbK5pVJrdG6Lm8Y4QiQIt9Qhlyk7PJhGUNlf7ds06+kX0/ETiO -vx5SatExeKu5F+JRnGFdAN0106SF5vBum+UbrgOSnJmfwX5VoOXARD21ppxgMzAr -JyGBpgBgy++GpV15gXGuA7DVMIADdHw8hV4OuBLjpkUL+ntArjhpUi7TP7VU3WKR -uUmvLm9CX1l8O/xZMpt9N1+o71a//7asnz8AMtT6cQKBgQD4FgefUkVnXDA1xKDW -1JbArVQeHiLGlRdLakRUY/HdGj72YgAOLt3UsrON4VQXl0C6rks/8HKCFaMexBlF -OecJNWsEVgBEAfsQ+NvrApOQsTszc8Zqna0Kqe2vA0VNa+SAzdHzhBbFcaVkzXJb -JB7M0/OIt5IaqXg6Y5eX2eZF1QKBgQDNHkIoJ/2hYtlSgXpGaniM+0XemQJgJXig -edAQdGKKfqwmjSFjByDM01ZaidMu5fEkeGhMRE73IbwNw0pWsMXylD6bI6+sk7yQ -biM+fslFEEDbgSJe41Jy2eerh5am+dnrMWNhd7QZV1K6tmaqrIzkmIV21/EPXIPp -BNHO8GV14wKBgGOybrO/GzcTXChvcXeEDWU3AqPr1mvZhHgBJ56GX69MGdtnvL/2 -Y51Th0bQM7wbQ58B5im21j2itl/pzIH+Z/NSbURbz1WFOkEy0SYbbfPq1XCy6Rz1 -apHrgiIf/VzErBp7HBFxlrkYF7Bvw7IOzPXhg3AA3Y0rZ66HUWdr4NdVAoGBAJfC -E2Bydgy5feC1OypuC9MC9abDviY0kxLoDTCfa2jcX7IGKPWDiJkCo5lI7557Mfax -vzjuMR5XLzNfkdih4VKgq9FMjeU5SQHy+tB6LZ+Tbuj4md1qgs3GuskGAEh6Auko -GUc7sVwuZ18NJNiR4Ywf7F8JVajv4gi9MB3Tbr3RAoGARSnVu+6rYSQTyEqvbsaB -gIW7Ezea5q06GcQF072nk3tNSXuU/52YMlodAJ1UfFPbBAtaa7wEFN8oRG1IyKON -MGyf6RD8GoInJjaDihkdCsR28RkchwymG1UMPnPzqRxSAb7da5YuMR8PEioVbL68 -dxhsgNi1Wtc2nGqN96qufG0= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDjt5O6nRrnAWPm +T0JvRLBHMclll92IWF/O4GEdcJ5fbBxP3BxK0Dv+6aRcR7b2o0f6fk/bgNepXfv/ +MXDQcFlESbfmUNGshFmZr0sjPrYPD1R06TZs+/7RsMXnx1c79mFGEQ4wqzDOBHKQ +xeDhNJk+BcE0QABsqF8AA2XC2/dK14QCljKLC84k1zTFTnh8duN2eAalaPQFFOoj +4AnonUnswJ45zIx5V2BdG+oqO5dwo/cEukKgAEL8T2IJ9Cqlmh2sPbMqYC8cODq6 +YcugMznxrfHV5LNThfkvwMe26+vv68r65zalPDy0M+cUMTMyBVY4TL3fejrloY2t +YMhPJIclAgMBAAECggEAPXdd/u9NRbGQX6hhTFuEIZOEw1F80MLaCaNzU1kExskN +01icom0W5LX4UZhiAK0OTsUtlRhwHh1qWfXkd777uX0UkKycDC8laGByra7Nwb7n +ky8oK77Rh5RptyiNmXflxd3wsJ5k7BczPXTMQL3L53vyLMJh2vKPwhcorrJlS+Pi +JjINMaR4IrDlpMYlrn9NTjsGr+mj/pdmKfU/KVXeKzFcwKTjUnDJNSbGDIC0AxaJ +dGU0yIX9MPW+p5szcA9o22UWW4LsEFY4YABeCqbm9/UQt3jWVMjCy4AOgr/9HWSR +DvXI/Xtdl3CTCr8+qDnhBaUI27z+UelZfTBFKUb8AQKBgQD6SmtrTBgEfb6tuxJw +AAHRuUcWGjatZ7X+meHRC9B7UPxUrKl9tU5NC7Gz6YMt+vr4bNMwykI6Ndj+4tSJ +KqsAC86v19CH4usMBLZ68MeTRvtQGiPah71syYrxf0uvYOx/KzUUBX240Ls+lEbE +W33psMoNAezUPpJwKx7CMjcBgQKBgQDo6VaT59bKRc3DXJvqFjd7TPIex+ny6JK+ +8oOwyyFFBwkzfymoOxN4lxSrE6yf7uTemRRn+RIH3UGDottIDqzhjvtcV5uODeIN +8WzxTbl759qIxt+z7aF7SkwJLJAAZS3qqCXKtMBo7ln4xKaoRLT2RohqD1YXGrg8 +wmYcUZoPpQKBgQCm2QVSuZ8pH0oFNjfMQbT0wbYJnd/lKMXBu4M1f9Ky4gHT0GYM +Ttirs6f6byfrduvmv2TpmWscsti80SktZywnE7fssMlqTHKzyFB9FBV2sFLHyyUr +gGFeK9xbsKgbeVkuTPdNKXvtv/eSd/XU38jIB/opQadGtY+ZBqWyfxb8AQKBgBLc +SlmBzZ/llSr7xdhn4ihG69hYQfacpL13r/hSCqinUDRuWLY5ynLacR8FYdY1pyzr +Yn6k6bPfU93QA0fLgG5ngK1SntMbBrIwWa0UqS+Cb+zhhd3xIUF1m8CmbibKCrTU +1vKaPnaAzqJZclFv9uN2hLdp9IO8cyzgZRpn9TzNAoGAUfZF1983qknfBgD8Lgm3 +zzKYtc8q2Ukatfo4VCp66CEprbLcBq5mKx6JiBoMGqU8SI5XVG0F0aHH2n8gImcu +bO0vtEldDc1ylZ/H7xhHFWlMzmTlsbHdHVtetFfKLTpjq6duvgLA12lJNHNVu3OU +Z1bRWDeZIP70+jdYrmSoVi8= -----END PRIVATE KEY----- diff --git a/Lib/slapdtest/certs/client.pem b/Lib/slapdtest/certs/client.pem index 33b95a73..ca2989ca 100644 --- a/Lib/slapdtest/certs/client.pem +++ b/Lib/slapdtest/certs/client.pem @@ -5,31 +5,31 @@ Certificate: Signature Algorithm: sha256WithRSAEncryption Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA Validity - Not Before: Dec 2 11:57:48 2017 GMT - Not After : Dec 2 11:57:48 2027 GMT + Not Before: Apr 12 18:52:38 2019 GMT + Not After : Mar 1 18:52:38 3019 GMT Subject: C=DE, O=python-ldap, OU=slapd-test, CN=client Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: - 00:c6:c6:f6:cc:11:b6:a1:56:22:b8:3f:a6:a8:59: - 49:1c:88:87:f5:74:46:13:06:e5:3c:33:57:92:85: - 11:14:67:76:2d:ab:f7:9e:e3:6e:69:5d:1c:b1:c4: - 3a:c0:6b:cc:a0:46:3a:ac:ee:d6:ae:c2:18:00:28: - db:8f:44:6c:b0:ee:1c:99:d4:ab:b8:bd:03:9b:c1: - 7f:6b:cf:21:d0:e8:58:fa:d3:a5:38:1c:76:d8:7c: - 05:5b:d0:01:7b:3b:32:db:84:d1:f7:8a:c0:91:b4: - d7:26:11:44:98:1b:53:64:7e:7f:73:42:45:78:a7: - 3d:8a:24:ce:1e:3e:de:06:c8:62:55:d0:d8:51:51: - d1:dd:c2:0b:16:8a:e4:6d:d8:42:11:3f:02:d7:76: - 47:ff:e2:cb:e6:d8:46:a1:00:96:b1:f9:23:1a:65: - 87:bf:96:0f:70:f5:20:71:97:a0:d3:68:5f:65:16: - 9a:0f:ab:37:96:f8:a9:d7:0a:61:3c:7b:ea:92:59: - e6:bc:12:39:33:21:15:da:e0:a2:7f:12:75:0c:3f: - 67:d4:2f:e2:5e:d9:a8:06:17:66:1f:08:87:7d:62: - f2:d3:41:5f:18:0e:fe:c8:9b:54:a9:d2:c6:c8:50: - 3a:16:5f:5e:06:0d:46:d9:f1:c0:6b:c1:f3:b2:23: - 44:df + 00:e3:b7:93:ba:9d:1a:e7:01:63:e6:4f:42:6f:44: + b0:47:31:c9:65:97:dd:88:58:5f:ce:e0:61:1d:70: + 9e:5f:6c:1c:4f:dc:1c:4a:d0:3b:fe:e9:a4:5c:47: + b6:f6:a3:47:fa:7e:4f:db:80:d7:a9:5d:fb:ff:31: + 70:d0:70:59:44:49:b7:e6:50:d1:ac:84:59:99:af: + 4b:23:3e:b6:0f:0f:54:74:e9:36:6c:fb:fe:d1:b0: + c5:e7:c7:57:3b:f6:61:46:11:0e:30:ab:30:ce:04: + 72:90:c5:e0:e1:34:99:3e:05:c1:34:40:00:6c:a8: + 5f:00:03:65:c2:db:f7:4a:d7:84:02:96:32:8b:0b: + ce:24:d7:34:c5:4e:78:7c:76:e3:76:78:06:a5:68: + f4:05:14:ea:23:e0:09:e8:9d:49:ec:c0:9e:39:cc: + 8c:79:57:60:5d:1b:ea:2a:3b:97:70:a3:f7:04:ba: + 42:a0:00:42:fc:4f:62:09:f4:2a:a5:9a:1d:ac:3d: + b3:2a:60:2f:1c:38:3a:ba:61:cb:a0:33:39:f1:ad: + f1:d5:e4:b3:53:85:f9:2f:c0:c7:b6:eb:eb:ef:eb: + ca:fa:e7:36:a5:3c:3c:b4:33:e7:14:31:33:32:05: + 56:38:4c:bd:df:7a:3a:e5:a1:8d:ad:60:c8:4f:24: + 87:25 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: critical @@ -39,45 +39,45 @@ Certificate: X509v3 Extended Key Usage: critical TLS Web Client Authentication X509v3 Subject Key Identifier: - 67:63:38:F4:B4:BC:F3:6B:BC:74:0E:7C:27:C9:BB:C2:CC:58:AC:16 + 4F:E7:35:C7:C8:C1:01:C3:7C:53:86:B9:BF:AE:8B:D6:45:A2:78:20 X509v3 Authority Key Identifier: - keyid:3B:1F:32:F4:FE:57:D1:6F:49:91:55:F2:24:F1:0A:66:3B:A5:EE:D4 + keyid:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2 Signature Algorithm: sha256WithRSAEncryption - 76:24:42:6b:33:4f:d6:59:07:48:5b:04:9c:3c:d3:3f:63:80: - 75:4d:78:d7:d5:85:b1:77:81:31:a3:91:cb:c9:a3:8c:0e:00: - 28:08:74:71:6c:fc:83:8c:80:ec:1c:e8:ee:83:e0:7f:49:3b: - f3:42:33:5a:1f:68:0c:a5:41:42:ce:bf:77:29:07:f2:18:a7: - 81:17:d7:76:47:04:d9:8a:dd:e8:5a:26:26:ea:a4:76:70:e1: - f1:fa:e1:db:bc:f2:24:b2:37:a8:58:2f:e3:66:89:77:02:55: - 87:ef:3c:1f:66:ce:4e:86:b3:4c:57:43:86:7f:4c:ab:5a:33: - dd:ca:e3:2f:3b:af:b4:43:5a:53:8b:e0:12:da:e7:c0:13:76: - b2:68:d5:14:f8:1a:07:ce:8a:87:5c:91:bd:35:d7:83:c6:2a: - a4:e0:92:50:01:b9:c2:fa:69:06:5c:8a:80:ee:9c:24:f9:49: - 64:e3:59:c1:a6:69:29:ce:b7:89:20:a9:7c:d6:9f:df:2a:d1: - a4:98:2a:6d:7b:93:6a:52:e3:ae:de:1a:d8:f3:2e:cf:02:7e: - ba:9a:fa:f4:b3:b5:6e:9a:23:10:70:53:53:30:d5:8a:32:35: - 01:52:58:6d:9d:f5:8e:bb:b9:76:bd:41:16:88:26:f8:d3:ce: - 70:03:c8:59 + 1c:90:5f:cf:18:48:95:4d:9d:d3:8e:6d:d1:69:19:1e:7b:3f: + 1f:48:7c:c8:0d:2f:c4:53:0f:89:23:f4:be:ea:b4:7a:c6:dd: + cc:18:0f:e7:34:ea:2c:d4:07:0d:65:78:e8:20:40:3f:36:ef: + 2c:00:31:69:e6:20:48:65:be:57:03:0e:69:ff:b9:83:59:99: + 7d:4d:86:98:14:5b:8e:39:25:3a:a8:6d:51:dc:45:a5:0f:cd: + f3:7a:fd:55:af:5f:55:75:20:03:f5:4a:75:6a:79:2f:76:84: + f6:4e:3d:1d:59:45:9a:b1:6a:57:6f:16:76:76:f8:df:6e:96: + d5:25:27:34:4b:21:d8:c9:9a:36:55:45:a0:43:16:43:68:93: + 37:af:81:89:06:d1:56:1b:9e:0f:62:40:ad:3c:4c:f5:ef:6c: + a2:a4:7f:f2:fa:78:9c:0d:c0:19:f1:10:e8:d8:cf:03:67:3c: + 2d:4d:f3:5d:67:5c:41:a7:4f:d6:c5:0e:ff:2c:04:dd:23:bb: + 85:44:8e:25:ac:15:a3:82:fa:a4:4f:fa:1d:87:f0:58:dc:ae: + 53:05:b9:81:e8:cb:e5:0c:ac:a5:74:68:03:f9:22:a0:45:b6: + 62:58:e0:98:d9:8c:54:a4:22:03:7a:37:12:eb:7d:b1:ad:45: + 60:8e:7a:df -----BEGIN CERTIFICATE----- -MIIDkjCCAnqgAwIBAgIBAzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU +MIIDlDCCAnygAwIBAgIBAzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV -BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwHhcNMTcxMjAyMTE1NzQ4WhcNMjcxMjAy -MTE1NzQ4WjBJMQswCQYDVQQGEwJERTEUMBIGA1UECgwLcHl0aG9uLWxkYXAxEzAR -BgNVBAsMCnNsYXBkLXRlc3QxDzANBgNVBAMMBmNsaWVudDCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAMbG9swRtqFWIrg/pqhZSRyIh/V0RhMG5TwzV5KF -ERRndi2r957jbmldHLHEOsBrzKBGOqzu1q7CGAAo249EbLDuHJnUq7i9A5vBf2vP -IdDoWPrTpTgcdth8BVvQAXs7MtuE0feKwJG01yYRRJgbU2R+f3NCRXinPYokzh4+ -3gbIYlXQ2FFR0d3CCxaK5G3YQhE/Atd2R//iy+bYRqEAlrH5Ixplh7+WD3D1IHGX -oNNoX2UWmg+rN5b4qdcKYTx76pJZ5rwSOTMhFdrgon8SdQw/Z9Qv4l7ZqAYXZh8I -h31i8tNBXxgO/sibVKnSxshQOhZfXgYNRtnxwGvB87IjRN8CAwEAAaN4MHYwDAYD -VR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH -AwIwHQYDVR0OBBYEFGdjOPS0vPNrvHQOfCfJu8LMWKwWMB8GA1UdIwQYMBaAFDsf -MvT+V9FvSZFV8iTxCmY7pe7UMA0GCSqGSIb3DQEBCwUAA4IBAQB2JEJrM0/WWQdI -WwScPNM/Y4B1TXjX1YWxd4Exo5HLyaOMDgAoCHRxbPyDjIDsHOjug+B/STvzQjNa -H2gMpUFCzr93KQfyGKeBF9d2RwTZit3oWiYm6qR2cOHx+uHbvPIksjeoWC/jZol3 -AlWH7zwfZs5OhrNMV0OGf0yrWjPdyuMvO6+0Q1pTi+AS2ufAE3ayaNUU+BoHzoqH -XJG9NdeDxiqk4JJQAbnC+mkGXIqA7pwk+Ulk41nBpmkpzreJIKl81p/fKtGkmCpt -e5NqUuOu3hrY8y7PAn66mvr0s7VumiMQcFNTMNWKMjUBUlhtnfWOu7l2vUEWiCb4 -085wA8hZ +BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwIBcNMTkwNDEyMTg1MjM4WhgPMzAxOTAz +MDExODUyMzhaMEkxCzAJBgNVBAYTAkRFMRQwEgYDVQQKDAtweXRob24tbGRhcDET +MBEGA1UECwwKc2xhcGQtdGVzdDEPMA0GA1UEAwwGY2xpZW50MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA47eTup0a5wFj5k9Cb0SwRzHJZZfdiFhfzuBh +HXCeX2wcT9wcStA7/umkXEe29qNH+n5P24DXqV37/zFw0HBZREm35lDRrIRZma9L +Iz62Dw9UdOk2bPv+0bDF58dXO/ZhRhEOMKswzgRykMXg4TSZPgXBNEAAbKhfAANl +wtv3SteEApYyiwvOJNc0xU54fHbjdngGpWj0BRTqI+AJ6J1J7MCeOcyMeVdgXRvq +KjuXcKP3BLpCoABC/E9iCfQqpZodrD2zKmAvHDg6umHLoDM58a3x1eSzU4X5L8DH +tuvr7+vK+uc2pTw8tDPnFDEzMgVWOEy933o65aGNrWDITySHJQIDAQABo3gwdjAM +BgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEF +BQcDAjAdBgNVHQ4EFgQUT+c1x8jBAcN8U4a5v66L1kWieCAwHwYDVR0jBBgwFoAU +vXjVSvGQlsXo7GZJI0cDXyZzhrIwDQYJKoZIhvcNAQELBQADggEBAByQX88YSJVN +ndOObdFpGR57Px9IfMgNL8RTD4kj9L7qtHrG3cwYD+c06izUBw1leOggQD827ywA +MWnmIEhlvlcDDmn/uYNZmX1NhpgUW445JTqobVHcRaUPzfN6/VWvX1V1IAP1SnVq +eS92hPZOPR1ZRZqxaldvFnZ2+N9ultUlJzRLIdjJmjZVRaBDFkNokzevgYkG0VYb +ng9iQK08TPXvbKKkf/L6eJwNwBnxEOjYzwNnPC1N811nXEGnT9bFDv8sBN0ju4VE +jiWsFaOC+qRP+h2H8FjcrlMFuYHoy+UMrKV0aAP5IqBFtmJY4JjZjFSkIgN6NxLr +fbGtRWCOet8= -----END CERTIFICATE----- diff --git a/Lib/slapdtest/certs/gencerts.sh b/Lib/slapdtest/certs/gencerts.sh index 7a971a3a..8a99db58 100755 --- a/Lib/slapdtest/certs/gencerts.sh +++ b/Lib/slapdtest/certs/gencerts.sh @@ -29,7 +29,7 @@ openssl ca -selfsign \ -in $CATMPDIR/ca.csr \ -out $CAOUTDIR/ca.pem \ -extensions ca_ext \ - -days 3563 \ + -days 356300 \ -batch # server cert diff --git a/Lib/slapdtest/certs/server.key b/Lib/slapdtest/certs/server.key index a48ee567..a8916701 100644 --- a/Lib/slapdtest/certs/server.key +++ b/Lib/slapdtest/certs/server.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDAgcI7Pj89Aw4r -rb+N8j3t1ynJgXRhQNxbxQcQmUCi8AtpGKNXu+aM9u2HxZ677ALfhsEivtQA5QKz -Ll5G2G2IQa7uzgIco73OL/kMZIJt7sKfnvfACtSoOlD0IyOzVEEu0AVA7hMHb6Ul -I0mCNfdk2FWFbc1nUmIAEBhIODbFoNW+Rc6lz94NsUqArDayvWpAsXkUbubQikpi -KNX1OpOOC9GUbEhwI/G90ZnUg9STk/2oxsvwLlYdOhGhpyfl53tTu7eLMBriMxFl -UTvkY1GUgPj0fA/giWtCerGOyeKu1hFlbS4LjborsfrlyYPwfwTg3YL4hVnt9fF0 -rpjzY1mlAgMBAAECggEAJY6rSEeiqtKXxynEv3rNXkOmIWwiOn8e/sB32mMr2x4d -+8kUxR8hocrjGKQTjfJDtTxjHdZBIlOLrU2UkxnSdMzrxidm/hNsCngNjL9nOu9k -BSRMjakPSCrodFkOtAPyG6H2BG7uQ3siqxYxVzgUJhaWyMtdUZUfDYgWVLCy7udU -5ML/OTOi7virueMmshjXoyrDug9OpiEMKiLu3ndAaDk/26m05ePAXB6TjW8SFw1B -qn7cITSG0G5MZ9pOw0KwT9irY1SdppBHVWIg7dkYWRCni0BPCFewastU+GVKH5PJ -+dYSvafhkEGD1bBu484KN9yX1BcHV41ZKR8pGgMM2QKBgQD3/0R2vZsTxoO1CHNI -IT7nBnuPIOP45iTFm/SNRY7e4dhQBy6HM6JD3Sr6Iksm8jRoboz+tnAso6l6QHRS -842uqBiOHdnka2RslDmrEun1lJv1MWuPM8JN0o8pYjVG/IRtaAFnYSEk72UoNy2h -bHC4OGFNwMbAadVm7DK5OiMfXwKBgQDGuBRxz7jkVZoMbbaeIqmGZAIejWkJweDZ -AK+txM+6Sg+Li14t190N3Xf6tyyidKhUAEWaINzLjZB+luxNaDXtxqWzLYHCwQKA -qfrjWVeZOS1clLya7jwl1jJqBtBiGKHv9eRL21hgX/9gX3odxqFMvX3vm6L7F1q1 -5CNApW0ZewKBgGO8qNcsWBLy8oM7G8n1fOvCwqyEaMrwG/fRSeALCnN+1tUQnljH -nkm2yBMC+cB3Bja9xzylOKXrSDyfcWjvBJsqhX2aacggnKnCTxMLL0aR9sr8jipw -gYN03Bijo5Oh+MxbWL0v5fmJweATmOljyE1+dzui/QvjRGz5L0kpJXj3AoGBAIa4 -3+t1B4WN312TuB4no8Tf4mvyNQcPcS/Nfk0RxD8o3Lcfal8sHMq8ng3Ux6bv7frd -IFLo+qfpts+L5HJqNz2X0ljSfkmZ7udp1hTySigwEmfU0rU61H5WZGFrczU+O/Ni -Qj+HWrgj/Q/KSxEKy+oqAcpDOtB+Odpc6+V1Aa0nAoGBAItWHP9UjTNFqOfyjZhG -qaUiZd1S2KyRR0l/lVcn+rJ46Yg5i+lMGwHMF1xPyWH4ELz+QCUX3doOI4yB2ikg -XXFcc8/bqgaR4AfOvP98T86s7+f33kaAKZsgyAFB2cjo+fz8ArTz+GjPeHbiOPaR -Ra7+BVwl9GE0+bCdirq+99GO +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCsBk0ml3ERFJyg +I6ujIJYERVU4doTZZd4r4z/LOef0hyiYiIQAc9wetaoZpM+bl4Eherxy9SBaCBwR +zefbaYQz2f2hdEDb+sISOiTke1eiF2ugYNlS55Wk1KnCnORE9bjcSNLPsscoUSzE +2bnBSoUwdiVK18YOCZR6GTeC8eA3ekvlR+9g+FBOgQ9+StXPDdq+iIAGXZREJIua +munErtTOw85De4YFCnzGw3UeCITDD4wFmI2IWphRFwWPsSDwUJfATA8S+7Rm4vwr +Qj726gUDlicTzPXKhJjXjj6XL7xXHfpQwMPkBCrxesKceHMJ+mrRsuuqHciuixRi +g94mILElAgMBAAECggEADG5oJOHMye8zYl8xiBhSvvxDrFDkSNGTvJgvhAArQwCB +boRvBZlZzt5R7Ih8eEH6kvDLrYMJU3hCjwbSOojlhNm7+m7sQPleDPMmt1wyeQQ4 +Qt681cDmj4LOwcGUvWcEdObOVTQWMFOtaIxTYCSCe34OM9pj9Z+7mxc3a78O9PND +Ib/CwcTA1OyoupzkKirqkdLXwK3x2aT/1TMaPX94taHB51cxXc7AglL9QnuCkuaG +krqrexy3rGimzsP3OwQGEUjWKcZVSSPT8/k1pPE9hRgOqBy05BfkAzlebdvc3GO5 +AbZk0NX2sfVHl4dTEXs/hTBCTQ3XmaltumQ9MdL+AQKBgQDg2I5QxBA2UHb8vCtK +f31kfG6YQc4MkoslrrMrtJjZqDYaLZPS1ARPSfYRqcc+7GDreuLmw39f8ZECd+2W +BYUqzZv9g13R9DY99g0/sINnZGsESwfIdLNNlHvVx2UrD5ybCj4vLhuPsVV7XlWs +cpl+rcuBVpqy8UIXifQ/Z3xLvwKBgQDD3CLjuC0mcTO2sIWqEHqVkc8CY2NJA2Qh +C78fwpaCqJUUdWnS69QbRGWgkFJL+oO8lQVQ1bXhZLHyQmy7Z5d5olCH6AW4GRnf +hBAnKJ+QTm9B6QVWzjUuHuOeCukfiTQbha14pOS9ar3X2QFWjDnzCRrnAxJmoY3H +BJATLHhMGwKBgQDSxAy7xt4Pm+O9y8Gk5tcq771X+i9k96V54EZRzMuPFDAK3/h2 +o4marZD9Q7Hi2P+NHTc+67klvbKZpsPOYkRPOEdmH9M9cPe7oz8OGa9DpwzuDEsy +a7p8GZjvbyb1c3/wkWxzG3x4eNnReD9FFHOwHMfr6LvAy4iRuh57pM0NzwKBgDY3 +1DixnV4M7EHgb7/6O9T3vhRtKujlVWyIcen61etpe4tkTV0kB11c+70M9pstyBYG +MqiD4It6coAbvznJnXcAZcaZhivGVxE237nXVwR9kfLu7JlxD+uqhVwUrSAbvR75 +TGIfU2rUB6We3u30d349wQK+KPPcOQEk1DValBqNAoGBAKfXOXgFBkIVW79fOkup +aIZXdEmU3Up61Oo0KDbxsg4l73NnnvuEnNMBTx3nT3KCVIAcQL9MNpLX/Z0HjOn1 +aiWVtTNq2OFL0V0HueBhbkFiWp551jTS7LjndCYHpUB/B8/wXP0kxHUm8HrQrRvK +DhV3zcxsXts1INidXjzzOkPi -----END PRIVATE KEY----- diff --git a/Lib/slapdtest/certs/server.pem b/Lib/slapdtest/certs/server.pem index 7e750596..25ba06c0 100644 --- a/Lib/slapdtest/certs/server.pem +++ b/Lib/slapdtest/certs/server.pem @@ -5,31 +5,31 @@ Certificate: Signature Algorithm: sha256WithRSAEncryption Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA Validity - Not Before: Dec 2 11:57:48 2017 GMT - Not After : Dec 2 11:57:48 2027 GMT + Not Before: Apr 12 18:52:38 2019 GMT + Not After : Mar 1 18:52:38 3019 GMT Subject: C=DE, O=python-ldap, OU=slapd-test, CN=server cert for localhost Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: - 00:c0:81:c2:3b:3e:3f:3d:03:0e:2b:ad:bf:8d:f2: - 3d:ed:d7:29:c9:81:74:61:40:dc:5b:c5:07:10:99: - 40:a2:f0:0b:69:18:a3:57:bb:e6:8c:f6:ed:87:c5: - 9e:bb:ec:02:df:86:c1:22:be:d4:00:e5:02:b3:2e: - 5e:46:d8:6d:88:41:ae:ee:ce:02:1c:a3:bd:ce:2f: - f9:0c:64:82:6d:ee:c2:9f:9e:f7:c0:0a:d4:a8:3a: - 50:f4:23:23:b3:54:41:2e:d0:05:40:ee:13:07:6f: - a5:25:23:49:82:35:f7:64:d8:55:85:6d:cd:67:52: - 62:00:10:18:48:38:36:c5:a0:d5:be:45:ce:a5:cf: - de:0d:b1:4a:80:ac:36:b2:bd:6a:40:b1:79:14:6e: - e6:d0:8a:4a:62:28:d5:f5:3a:93:8e:0b:d1:94:6c: - 48:70:23:f1:bd:d1:99:d4:83:d4:93:93:fd:a8:c6: - cb:f0:2e:56:1d:3a:11:a1:a7:27:e5:e7:7b:53:bb: - b7:8b:30:1a:e2:33:11:65:51:3b:e4:63:51:94:80: - f8:f4:7c:0f:e0:89:6b:42:7a:b1:8e:c9:e2:ae:d6: - 11:65:6d:2e:0b:8d:ba:2b:b1:fa:e5:c9:83:f0:7f: - 04:e0:dd:82:f8:85:59:ed:f5:f1:74:ae:98:f3:63: - 59:a5 + 00:ac:06:4d:26:97:71:11:14:9c:a0:23:ab:a3:20: + 96:04:45:55:38:76:84:d9:65:de:2b:e3:3f:cb:39: + e7:f4:87:28:98:88:84:00:73:dc:1e:b5:aa:19:a4: + cf:9b:97:81:21:7a:bc:72:f5:20:5a:08:1c:11:cd: + e7:db:69:84:33:d9:fd:a1:74:40:db:fa:c2:12:3a: + 24:e4:7b:57:a2:17:6b:a0:60:d9:52:e7:95:a4:d4: + a9:c2:9c:e4:44:f5:b8:dc:48:d2:cf:b2:c7:28:51: + 2c:c4:d9:b9:c1:4a:85:30:76:25:4a:d7:c6:0e:09: + 94:7a:19:37:82:f1:e0:37:7a:4b:e5:47:ef:60:f8: + 50:4e:81:0f:7e:4a:d5:cf:0d:da:be:88:80:06:5d: + 94:44:24:8b:9a:9a:e9:c4:ae:d4:ce:c3:ce:43:7b: + 86:05:0a:7c:c6:c3:75:1e:08:84:c3:0f:8c:05:98: + 8d:88:5a:98:51:17:05:8f:b1:20:f0:50:97:c0:4c: + 0f:12:fb:b4:66:e2:fc:2b:42:3e:f6:ea:05:03:96: + 27:13:cc:f5:ca:84:98:d7:8e:3e:97:2f:bc:57:1d: + fa:50:c0:c3:e4:04:2a:f1:7a:c2:9c:78:73:09:fa: + 6a:d1:b2:eb:aa:1d:c8:ae:8b:14:62:83:de:26:20: + b1:25 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: critical @@ -39,48 +39,48 @@ Certificate: X509v3 Extended Key Usage: critical TLS Web Server Authentication X509v3 Subject Key Identifier: - 1B:78:45:40:0D:50:8A:8B:3B:C1:0A:F8:3F:7A:48:7B:A6:3C:28:09 + 08:D1:86:1B:82:0A:4F:71:31:E4:F5:31:23:CC:67:3B:FA:84:3B:A0 X509v3 Authority Key Identifier: - keyid:3B:1F:32:F4:FE:57:D1:6F:49:91:55:F2:24:F1:0A:66:3B:A5:EE:D4 + keyid:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2 X509v3 Subject Alternative Name: DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1 Signature Algorithm: sha256WithRSAEncryption - ad:08:3f:7d:b1:09:a1:a5:6c:c3:58:80:1d:e5:33:a5:bb:c0: - 33:39:95:aa:88:ee:c4:8e:38:3b:59:a7:0e:39:74:6c:fe:11: - 33:5e:fa:50:cb:20:4b:67:b7:c9:5e:96:a7:9e:d8:47:46:e1: - ab:fe:5d:8b:9a:2d:1a:1b:43:08:f9:93:0f:2a:e3:ce:83:4a: - 94:cd:02:f0:8e:25:f2:41:0d:55:10:f5:4c:5b:39:8b:77:5e: - ab:78:16:64:a1:48:d5:e1:f6:69:9a:0f:d8:30:a6:cc:92:4d: - 81:df:46:74:ab:cf:1d:b7:d4:01:b9:6d:d5:f4:14:b8:d5:54: - 84:79:11:42:69:55:7f:74:ce:01:96:2f:3f:51:23:b3:11:fb: - 72:dc:4c:b9:a3:89:ef:31:e4:c0:49:06:fa:8d:09:71:e1:c1: - 74:a9:ed:f8:96:87:67:16:b5:5d:16:5d:59:70:ff:1c:b5:a1: - 6c:d2:22:11:3a:0e:6f:76:9b:69:cb:f3:85:a7:79:ad:53:f5: - 34:e8:87:cc:dd:09:51:25:e0:28:ee:79:a0:a3:dc:0a:dd:f0: - 1b:e3:c9:5f:14:d3:95:f5:12:4d:23:95:45:2c:3c:32:94:ad: - ce:1e:a0:5f:e6:e8:28:c6:f9:c7:fb:57:06:ad:0b:eb:86:ca: - 0e:d2:a8:67 + 88:60:af:be:11:c4:aa:dc:9b:f1:e7:14:da:20:aa:6f:2f:06: + ae:38:b2:7c:ac:90:81:22:51:7e:cb:26:15:6e:fe:67:98:c1: + 0d:dc:aa:39:98:2b:d2:cc:3c:ff:1a:92:2f:56:0a:a9:6e:d8: + 9a:3d:c5:4d:6f:cc:91:2e:e3:4e:bf:22:ab:cb:92:1a:a0:8f: + 43:cd:82:bc:48:55:c4:95:cf:10:6b:6a:31:19:92:7d:e0:06: + 05:6f:0b:33:e7:2a:37:42:f9:ec:1b:29:99:e1:58:0c:01:a7: + c3:8b:58:71:21:9f:61:8c:a7:fb:b6:7e:32:8b:a9:4e:c7:1f: + f6:46:e8:dd:ac:a6:4c:53:f8:4d:93:e4:ec:73:ab:0b:be:98: + c5:78:c4:92:c0:4c:78:47:52:2f:93:07:67:20:a4:5a:7f:59: + 7e:4f:48:53:20:0d:37:bb:06:f8:44:42:64:b4:94:15:43:d1: + 4c:51:f3:97:1d:2d:cd:db:b9:bb:1a:69:10:89:7d:ae:1d:0d: + 94:78:45:29:cd:c4:42:67:67:96:05:bf:da:aa:23:65:7b:04: + ff:b7:ac:9d:ee:0b:e7:0f:c1:c5:0b:48:fe:0f:d6:3f:d8:b4: + 77:12:bb:f5:91:4f:43:e6:01:3f:a4:c0:ea:8c:c6:68:99:8e: + 49:e8:c4:8b -----BEGIN CERTIFICATE----- -MIID1TCCAr2gAwIBAgIBAjANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU +MIID1zCCAr+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV -BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwHhcNMTcxMjAyMTE1NzQ4WhcNMjcxMjAy -MTE1NzQ4WjBcMQswCQYDVQQGEwJERTEUMBIGA1UECgwLcHl0aG9uLWxkYXAxEzAR -BgNVBAsMCnNsYXBkLXRlc3QxIjAgBgNVBAMMGXNlcnZlciBjZXJ0IGZvciBsb2Nh -bGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAgcI7Pj89Aw4r -rb+N8j3t1ynJgXRhQNxbxQcQmUCi8AtpGKNXu+aM9u2HxZ677ALfhsEivtQA5QKz -Ll5G2G2IQa7uzgIco73OL/kMZIJt7sKfnvfACtSoOlD0IyOzVEEu0AVA7hMHb6Ul -I0mCNfdk2FWFbc1nUmIAEBhIODbFoNW+Rc6lz94NsUqArDayvWpAsXkUbubQikpi -KNX1OpOOC9GUbEhwI/G90ZnUg9STk/2oxsvwLlYdOhGhpyfl53tTu7eLMBriMxFl -UTvkY1GUgPj0fA/giWtCerGOyeKu1hFlbS4LjborsfrlyYPwfwTg3YL4hVnt9fF0 -rpjzY1mlAgMBAAGjgacwgaQwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAw -FgYDVR0lAQH/BAwwCgYIKwYBBQUHAwEwHQYDVR0OBBYEFBt4RUANUIqLO8EK+D96 -SHumPCgJMB8GA1UdIwQYMBaAFDsfMvT+V9FvSZFV8iTxCmY7pe7UMCwGA1UdEQQl -MCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0B -AQsFAAOCAQEArQg/fbEJoaVsw1iAHeUzpbvAMzmVqojuxI44O1mnDjl0bP4RM176 -UMsgS2e3yV6Wp57YR0bhq/5di5otGhtDCPmTDyrjzoNKlM0C8I4l8kENVRD1TFs5 -i3deq3gWZKFI1eH2aZoP2DCmzJJNgd9GdKvPHbfUAblt1fQUuNVUhHkRQmlVf3TO -AZYvP1EjsxH7ctxMuaOJ7zHkwEkG+o0JceHBdKnt+JaHZxa1XRZdWXD/HLWhbNIi -EToOb3abacvzhad5rVP1NOiHzN0JUSXgKO55oKPcCt3wG+PJXxTTlfUSTSOVRSw8 -MpStzh6gX+boKMb5x/tXBq0L64bKDtKoZw== +BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwIBcNMTkwNDEyMTg1MjM4WhgPMzAxOTAz +MDExODUyMzhaMFwxCzAJBgNVBAYTAkRFMRQwEgYDVQQKDAtweXRob24tbGRhcDET +MBEGA1UECwwKc2xhcGQtdGVzdDEiMCAGA1UEAwwZc2VydmVyIGNlcnQgZm9yIGxv +Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKwGTSaXcREU +nKAjq6MglgRFVTh2hNll3ivjP8s55/SHKJiIhABz3B61qhmkz5uXgSF6vHL1IFoI +HBHN59tphDPZ/aF0QNv6whI6JOR7V6IXa6Bg2VLnlaTUqcKc5ET1uNxI0s+yxyhR +LMTZucFKhTB2JUrXxg4JlHoZN4Lx4Dd6S+VH72D4UE6BD35K1c8N2r6IgAZdlEQk +i5qa6cSu1M7DzkN7hgUKfMbDdR4IhMMPjAWYjYhamFEXBY+xIPBQl8BMDxL7tGbi +/CtCPvbqBQOWJxPM9cqEmNeOPpcvvFcd+lDAw+QEKvF6wpx4cwn6atGy66odyK6L +FGKD3iYgsSUCAwEAAaOBpzCBpDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIF +oDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUCNGGG4IKT3Ex5PUx +I8xnO/qEO6AwHwYDVR0jBBgwFoAUvXjVSvGQlsXo7GZJI0cDXyZzhrIwLAYDVR0R +BCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3 +DQEBCwUAA4IBAQCIYK++EcSq3Jvx5xTaIKpvLwauOLJ8rJCBIlF+yyYVbv5nmMEN +3Ko5mCvSzDz/GpIvVgqpbtiaPcVNb8yRLuNOvyKry5IaoI9DzYK8SFXElc8Qa2ox +GZJ94AYFbwsz5yo3QvnsGymZ4VgMAafDi1hxIZ9hjKf7tn4yi6lOxx/2RujdrKZM +U/hNk+Tsc6sLvpjFeMSSwEx4R1IvkwdnIKRaf1l+T0hTIA03uwb4REJktJQVQ9FM +UfOXHS3N27m7GmkQiX2uHQ2UeEUpzcRCZ2eWBb/aqiNlewT/t6yd7gvnD8HFC0j+ +D9Y/2LR3Erv1kU9D5gE/pMDqjMZomY5J6MSL -----END CERTIFICATE----- diff --git a/Makefile b/Makefile index 2d3293e6..577ba883 100644 --- a/Makefile +++ b/Makefile @@ -5,13 +5,18 @@ LCOV_REPORT_OPTIONS=--show-details -no-branch-coverage \ --title "python-ldap LCOV report" SCAN_REPORT=build/scan_report PYTHON_SUPP=/usr/share/doc/python3-devel/valgrind-python.supp -AUTOPEP8_OPTS=--aggressive + .NOTPARALLEL: .PHONY: all all: +Modules/constants_generated.h: Lib/ldap/constants.py + $(PYTHON) $^ > $@ + indent Modules/constants_generated.h + rm -f Modules/constants_generated.h~ + .PHONY: clean clean: rm -rf build dist *.egg-info .tox MANIFEST @@ -30,7 +35,7 @@ lcov-clean: if [ -d build ]; then find build -name '*.gc??' -delete; fi lcov-coverage: - WITH_GCOV=1 tox -e py27,py36 + WITH_GCOV=1 tox -e py36 $(LCOV_INFO): build lcov --capture --directory build --output-file $(LCOV_INFO) @@ -80,13 +85,15 @@ valgrind: build $(PYTHON_SUPP) fi # Code autoformatter -.PHONY: autoformat indent autopep8 -autoformat: indent autopep8 +.PHONY: autoformat indent black black-check +autoformat: indent black indent: indent Modules/*.c Modules/*.h rm -f Modules/*.c~ Modules/*.h~ -autopep8: - $(PYTHON) -m autopep8 -r -i -j0 $(AUTOPEP8_OPTS) \ - Demo Lib Tests setup.py +black: + $(PYTHON) -m black $(CURDIR) + +black-check: + $(PYTHON) -m black $(CURDIR) --check diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c index bc26727e..da18d575 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -126,6 +126,7 @@ Tuple_to_LDAPMod(PyObject *tup, int no_op) } lm = PyMem_NEW(LDAPMod, 1); + if (lm == NULL) goto nomem; @@ -236,6 +237,7 @@ List_to_LDAPMods(PyObject *list, int no_op) } lms = PyMem_NEW(LDAPMod *, len + 1); + if (lms == NULL) goto nomem; @@ -335,7 +337,7 @@ attrs_from_List(PyObject *attrlist, char ***attrsp) * internal values that must be treated like const char. Python * 3.7 actually returns a const char. */ - attrs[i] = (char *)PyMem_NEW(char *, strlen + 1); + attrs[i] = (char *)PyMem_NEW(char, strlen + 1); if (attrs[i] == NULL) goto nomem; @@ -415,7 +417,7 @@ l_ldap_unbind_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_unbind_ext"); + return LDAPerror(self->ldap); self->valid = 0; Py_INCREF(Py_None); @@ -461,7 +463,7 @@ l_ldap_abandon_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_abandon_ext"); + return LDAPerror(self->ldap); Py_INCREF(Py_None); return Py_None; @@ -517,7 +519,7 @@ l_ldap_add_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_add_ext"); + return LDAPerror(self->ldap); return PyInt_FromLong(msgid); } @@ -568,7 +570,7 @@ l_ldap_simple_bind(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_simple_bind"); + return LDAPerror(self->ldap); return PyInt_FromLong(msgid); } @@ -727,7 +729,7 @@ l_ldap_sasl_bind_s(LDAPObject *self, PyObject *args) servercred->bv_len); } else if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "l_ldap_sasl_bind_s"); + return LDAPerror(self->ldap); return PyInt_FromLong(ldaperror); } @@ -806,7 +808,7 @@ l_ldap_sasl_interactive_bind_s(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (msgid != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_sasl_interactive_bind_s"); + return LDAPerror(self->ldap); return PyInt_FromLong(msgid); } #endif @@ -854,7 +856,7 @@ l_ldap_cancel(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_cancel"); + return LDAPerror(self->ldap); return PyInt_FromLong(msgid); } @@ -908,7 +910,7 @@ l_ldap_compare_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_compare_ext"); + return LDAPerror(self->ldap); return PyInt_FromLong(msgid); } @@ -954,7 +956,7 @@ l_ldap_delete_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_delete_ext"); + return LDAPerror(self->ldap); return PyInt_FromLong(msgid); } @@ -1011,7 +1013,7 @@ l_ldap_modify_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_modify_ext"); + return LDAPerror(self->ldap); return PyInt_FromLong(msgid); } @@ -1061,7 +1063,7 @@ l_ldap_rename(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_rename"); + return LDAPerror(self->ldap); return PyInt_FromLong(msgid); } @@ -1081,12 +1083,11 @@ l_ldap_result4(LDAPObject *self, PyObject *args) struct timeval *tvp; int res_type; LDAPMessage *msg = NULL; - PyObject *result_str, *retval, *pmsg, *pyctrls = 0; + PyObject *retval, *pmsg, *pyctrls = 0; int res_msgid = 0; char *retoid = 0; PyObject *valuestr = NULL; int result = LDAP_SUCCESS; - char **refs = NULL; LDAPControl **serverctrls = 0; if (!PyArg_ParseTuple @@ -1109,7 +1110,7 @@ l_ldap_result4(LDAPObject *self, PyObject *args) LDAP_END_ALLOW_THREADS(self); if (res_type < 0) /* LDAP or system error */ - return LDAPerror(self->ldap, "ldap_result4"); + return LDAPerror(self->ldap); if (res_type == 0) { /* Polls return (None, None, None, None); timeouts raise an exception */ @@ -1157,23 +1158,15 @@ l_ldap_result4(LDAPObject *self, PyObject *args) } LDAP_BEGIN_ALLOW_THREADS(self); - rc = ldap_parse_result(self->ldap, msg, &result, NULL, NULL, &refs, + rc = ldap_parse_result(self->ldap, msg, &result, NULL, NULL, NULL, &serverctrls, 0); LDAP_END_ALLOW_THREADS(self); } if (result != LDAP_SUCCESS) { /* result error */ - char *e, err[1024]; - - if (result == LDAP_REFERRAL && refs && refs[0]) { - snprintf(err, sizeof(err), "Referral:\n%s", refs[0]); - e = err; - } - else - e = "ldap_parse_result"; - ldap_msgfree(msg); + ldap_controls_free(serverctrls); Py_XDECREF(valuestr); - return LDAPerror(self->ldap, e); + return LDAPraise_for_message(self->ldap, msg); } if (!(pyctrls = LDAPControls_to_List(serverctrls))) { @@ -1182,36 +1175,29 @@ l_ldap_result4(LDAPObject *self, PyObject *args) LDAP_BEGIN_ALLOW_THREADS(self); ldap_set_option(self->ldap, LDAP_OPT_ERROR_NUMBER, &err); LDAP_END_ALLOW_THREADS(self); + ldap_controls_free(serverctrls); ldap_msgfree(msg); Py_XDECREF(valuestr); - return LDAPerror(self->ldap, "LDAPControls_to_List"); + return LDAPerror(self->ldap); } ldap_controls_free(serverctrls); pmsg = LDAPmessage_to_python(self->ldap, msg, add_ctrls, add_intermediates); - if (res_type == 0) { - result_str = Py_None; - Py_INCREF(Py_None); - } - else { - result_str = PyInt_FromLong(res_type); - } - if (pmsg == NULL) { retval = NULL; } else { /* s handles NULL, but O does not */ if (add_extop) { - retval = Py_BuildValue("(OOiOsO)", result_str, pmsg, res_msgid, + retval = Py_BuildValue("(iOiOsO)", res_type, pmsg, res_msgid, pyctrls, retoid, valuestr ? valuestr : Py_None); } else { retval = - Py_BuildValue("(OOiO)", result_str, pmsg, res_msgid, pyctrls); + Py_BuildValue("(iOiO)", res_type, pmsg, res_msgid, pyctrls); } if (pmsg != Py_None) { @@ -1220,7 +1206,6 @@ l_ldap_result4(LDAPObject *self, PyObject *args) } Py_XDECREF(valuestr); Py_XDECREF(pyctrls); - Py_DECREF(result_str); return retval; } @@ -1294,7 +1279,7 @@ l_ldap_search_ext(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_search_ext"); + return LDAPerror(self->ldap); return PyInt_FromLong(msgid); } @@ -1341,7 +1326,7 @@ l_ldap_whoami_s(LDAPObject *self, PyObject *args) if (ldaperror != LDAP_SUCCESS) { ber_bvfree(bvalue); - return LDAPerror(self->ldap, "ldap_whoami_s"); + return LDAPerror(self->ldap); } result = LDAPberval_to_unicode_object(bvalue); @@ -1368,7 +1353,7 @@ l_ldap_start_tls_s(LDAPObject *self, PyObject *args) LDAP_END_ALLOW_THREADS(self); if (ldaperror != LDAP_SUCCESS) { ldap_set_option(self->ldap, LDAP_OPT_ERROR_NUMBER, &ldaperror); - return LDAPerror(self->ldap, "ldap_start_tls_s"); + return LDAPerror(self->ldap); } Py_INCREF(Py_None); @@ -1380,14 +1365,16 @@ l_ldap_start_tls_s(LDAPObject *self, PyObject *args) /* ldap_set_option */ static PyObject * -l_ldap_set_option(PyObject *self, PyObject *args) +l_ldap_set_option(LDAPObject *self, PyObject *args) { PyObject *value; int option; if (!PyArg_ParseTuple(args, "iO:set_option", &option, &value)) return NULL; - if (!LDAP_set_option((LDAPObject *)self, option, value)) + if (not_valid(self)) + return NULL; + if (!LDAP_set_option(self, option, value)) return NULL; Py_INCREF(Py_None); return Py_None; @@ -1396,13 +1383,15 @@ l_ldap_set_option(PyObject *self, PyObject *args) /* ldap_get_option */ static PyObject * -l_ldap_get_option(PyObject *self, PyObject *args) +l_ldap_get_option(LDAPObject *self, PyObject *args) { int option; if (!PyArg_ParseTuple(args, "i:get_option", &option)) return NULL; - return LDAP_get_option((LDAPObject *)self, option); + if (not_valid(self)) + return NULL; + return LDAP_get_option(self, option); } /* ldap_passwd */ @@ -1460,7 +1449,7 @@ l_ldap_passwd(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_passwd"); + return LDAPerror(self->ldap); return PyInt_FromLong(msgid); } @@ -1511,7 +1500,7 @@ l_ldap_extended_operation(LDAPObject *self, PyObject *args) LDAPControl_List_DEL(client_ldcs); if (ldaperror != LDAP_SUCCESS) - return LDAPerror(self->ldap, "ldap_extended_operation"); + return LDAPerror(self->ldap); return PyInt_FromLong(msgid); } diff --git a/Modules/LDAPObject.h b/Modules/LDAPObject.h index a456bce0..4af0b382 100644 --- a/Modules/LDAPObject.h +++ b/Modules/LDAPObject.h @@ -5,21 +5,9 @@ #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 */ + PyThreadState *_save; /* for thread saving on referrals */ int valid; } LDAPObject; @@ -42,7 +30,7 @@ extern LDAPObject *newLDAPObject(LDAP *); #define LDAP_END_ALLOW_THREADS( l ) \ { \ LDAPObject *lo = (l); \ - _threadstate _save = lo->_save; \ + PyThreadState *_save = lo->_save; \ lo->_save = NULL; \ PyEval_RestoreThread( _save ); \ } diff --git a/Modules/berval.h b/Modules/berval.h index 2aa9c977..9c427240 100644 --- a/Modules/berval.h +++ b/Modules/berval.h @@ -4,7 +4,6 @@ #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); diff --git a/Modules/common.h b/Modules/common.h index affa5f93..886024f2 100644 --- a/Modules/common.h +++ b/Modules/common.h @@ -12,6 +12,35 @@ #include "config.h" #endif +#include +#include +#include + +#if LDAP_API_VERSION < 2040 +#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 +extern int ldap_init_fd(ber_socket_t fd, int proto, LDAP_CONST char *url, + LDAP **ldp); +#endif + #if defined(MS_WINDOWS) #include #else /* unix */ diff --git a/Modules/constants.c b/Modules/constants.c index f8da3736..07d60653 100644 --- a/Modules/constants.c +++ b/Modules/constants.c @@ -3,8 +3,7 @@ #include "common.h" #include "constants.h" -#include "lber.h" -#include "ldap.h" +#include "ldapcontrol.h" /* the base exception class */ @@ -32,7 +31,8 @@ static PyObject *errobjects[LDAP_ERROR_MAX - LDAP_ERROR_MIN + 1]; PyObject * LDAPerr(int errnum) { - if (errnum >= LDAP_ERROR_MIN && errnum <= LDAP_ERROR_MAX) { + if (errnum >= LDAP_ERROR_MIN && errnum <= LDAP_ERROR_MAX && + errobjects[errnum + LDAP_ERROR_OFFSET] != NULL) { PyErr_SetNone(errobjects[errnum + LDAP_ERROR_OFFSET]); } else { @@ -48,38 +48,82 @@ LDAPerr(int errnum) /* Convert an LDAP error into an informative python exception */ PyObject * -LDAPerror(LDAP *l, char *msg) +LDAPraise_for_message(LDAP *l, LDAPMessage *m) { if (l == NULL) { PyErr_SetFromErrno(LDAPexception_class); + ldap_msgfree(m); return NULL; } else { - int myerrno, errnum, opt_errnum; + int myerrno, errnum, opt_errnum, msgid = -1, msgtype = 0; PyObject *errobj; PyObject *info; PyObject *str; PyObject *pyerrno; - char *matched, *error; + PyObject *pyresult; + PyObject *pyctrls = NULL; + char *matched = NULL, *error = NULL, **refs = NULL; + LDAPControl **serverctrls = NULL; /* at first save errno for later use before it gets overwritten by another call */ myerrno = errno; - opt_errnum = ldap_get_option(l, LDAP_OPT_ERROR_NUMBER, &errnum); - if (opt_errnum != LDAP_OPT_SUCCESS) - errnum = opt_errnum; + if (m != NULL) { + msgid = ldap_msgid(m); + msgtype = ldap_msgtype(m); + ldap_parse_result(l, m, &errnum, &matched, &error, &refs, + &serverctrls, 1); + } - if (errnum == LDAP_NO_MEMORY) - return PyErr_NoMemory(); + if (msgtype <= 0) { + opt_errnum = ldap_get_option(l, LDAP_OPT_ERROR_NUMBER, &errnum); + if (opt_errnum != LDAP_OPT_SUCCESS) + errnum = opt_errnum; - if (errnum >= LDAP_ERROR_MIN && errnum <= LDAP_ERROR_MAX) + 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 + } + else { errobj = LDAPexception_class; + } info = PyDict_New(); - if (info == NULL) + if (info == NULL) { + ldap_memfree(matched); + ldap_memfree(error); + ldap_memvfree((void **)refs); + ldap_controls_free(serverctrls); return NULL; + } + + if (msgtype > 0) { + pyresult = PyInt_FromLong(msgtype); + if (pyresult) + PyDict_SetItemString(info, "msgtype", pyresult); + Py_XDECREF(pyresult); + } + + if (msgid >= 0) { + pyresult = PyInt_FromLong(msgid); + if (pyresult) + PyDict_SetItemString(info, "msgid", pyresult); + Py_XDECREF(pyresult); + } + + pyresult = PyInt_FromLong(errnum); + if (pyresult) + PyDict_SetItemString(info, "result", pyresult); + Py_XDECREF(pyresult); str = PyUnicode_FromString(ldap_err2string(errnum)); if (str) @@ -93,8 +137,21 @@ LDAPerror(LDAP *l, char *msg) Py_XDECREF(pyerrno); } - if (ldap_get_option(l, LDAP_OPT_MATCHED_DN, &matched) >= 0 - && matched != NULL) { + if (!(pyctrls = LDAPControls_to_List(serverctrls))) { + int err = LDAP_NO_MEMORY; + + ldap_set_option(l, LDAP_OPT_ERROR_NUMBER, &err); + ldap_memfree(matched); + ldap_memfree(error); + ldap_memvfree((void **)refs); + ldap_controls_free(serverctrls); + return PyErr_NoMemory(); + } + ldap_controls_free(serverctrls); + PyDict_SetItemString(info, "ctrls", pyctrls); + Py_XDECREF(pyctrls); + + if (matched != NULL) { if (*matched != '\0') { str = PyUnicode_FromString(matched); if (str) @@ -104,33 +161,42 @@ LDAPerror(LDAP *l, char *msg) ldap_memfree(matched); } - if (errnum == LDAP_REFERRAL) { - str = PyUnicode_FromString(msg); + if (errnum == LDAP_REFERRAL && refs != NULL && refs[0] != NULL) { + /* Keep old behaviour, overshadow error message */ + char err[1024]; + + snprintf(err, sizeof(err), "Referral:\n%s", refs[0]); + str = PyUnicode_FromString(err); + PyDict_SetItemString(info, "info", str); + Py_XDECREF(str); + } + else if (error != NULL && *error != '\0') { + str = PyUnicode_FromString(error); if (str) PyDict_SetItemString(info, "info", str); Py_XDECREF(str); } - else if (ldap_get_option(l, LDAP_OPT_ERROR_STRING, &error) >= 0) { - if (error != NULL && *error != '\0') { - str = PyUnicode_FromString(error); - if (str) - PyDict_SetItemString(info, "info", str); - Py_XDECREF(str); - } - ldap_memfree(error); - } + PyErr_SetObject(errobj, info); Py_DECREF(info); + ldap_memvfree((void **)refs); + ldap_memfree(error); return NULL; } } +PyObject * +LDAPerror(LDAP *l) +{ + return LDAPraise_for_message(l, NULL); +} + /* initialise the module constants */ int LDAPinit_constants(PyObject *m) { - PyObject *exc; + PyObject *exc, *nobj; /* simple constants */ @@ -160,6 +226,10 @@ LDAPinit_constants(PyObject *m) #define add_err(n) do { \ exc = PyErr_NewException("ldap." #n, LDAPexception_class, NULL); \ if (exc == NULL) return -1; \ + nobj = PyLong_FromLong(LDAP_##n); \ + if (nobj == NULL) return -1; \ + if (PyObject_SetAttrString(exc, "errnum", nobj) != 0) return -1; \ + Py_DECREF(nobj); \ errobjects[LDAP_##n+LDAP_ERROR_OFFSET] = exc; \ if (PyModule_AddObject(m, #n, exc) != 0) return -1; \ Py_INCREF(exc); \ diff --git a/Modules/constants.h b/Modules/constants.h index 8a390b5b..7b9ce53e 100644 --- a/Modules/constants.h +++ b/Modules/constants.h @@ -4,14 +4,13 @@ #define __h_constants_ #include "common.h" -#include "lber.h" -#include "ldap.h" extern int LDAPinit_constants(PyObject *m); extern PyObject *LDAPconstant(int); extern PyObject *LDAPexception_class; -extern PyObject *LDAPerror(LDAP *, char *msg); +extern PyObject *LDAPerror(LDAP *); +extern PyObject *LDAPraise_for_message(LDAP *, LDAPMessage *m); PyObject *LDAPerr(int errnum); #ifndef LDAP_CONTROL_PAGE_OID diff --git a/Modules/constants_generated.h b/Modules/constants_generated.h index 455852ed..e357fa23 100644 --- a/Modules/constants_generated.h +++ b/Modules/constants_generated.h @@ -213,10 +213,6 @@ add_int(OPT_X_TLS_DEMAND); add_int(OPT_X_TLS_ALLOW); add_int(OPT_X_TLS_TRY); -#if defined(LDAP_OPT_X_TLS_PEERCERT) -add_int(OPT_X_TLS_PEERCERT); -#endif - #if defined(LDAP_OPT_X_TLS_VERSION) add_int(OPT_X_TLS_VERSION); #endif @@ -253,6 +249,10 @@ add_int(OPT_X_TLS_PROTOCOL_MIN); add_int(OPT_X_TLS_PACKAGE); #endif +#if defined(LDAP_OPT_X_TLS_REQUIRE_SAN) +add_int(OPT_X_TLS_REQUIRE_SAN); +#endif + #endif add_int(OPT_X_SASL_MECH); @@ -333,6 +333,14 @@ 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); diff --git a/Modules/functions.c b/Modules/functions.c index 4731efb8..b811708f 100644 --- a/Modules/functions.c +++ b/Modules/functions.c @@ -15,15 +15,75 @@ l_ldap_initialize(PyObject *unused, PyObject *args) char *uri; LDAP *ld = NULL; int ret; + PyThreadState *save; if (!PyArg_ParseTuple(args, "s:initialize", &uri)) return NULL; - Py_BEGIN_ALLOW_THREADS ret = ldap_initialize(&ld, uri); - Py_END_ALLOW_THREADS if (ret != LDAP_SUCCESS) - return LDAPerror(ld, "ldap_initialize"); + save = PyEval_SaveThread(); + ret = ldap_initialize(&ld, uri); + PyEval_RestoreThread(save); + + if (ret != LDAP_SUCCESS) + return LDAPerror(ld); + + return (PyObject *)newLDAPObject(ld); +} + +#ifdef HAVE_LDAP_INIT_FD +/* initialize_fd(fileno, url) */ + +static PyObject * +l_ldap_initialize_fd(PyObject *unused, PyObject *args) +{ + char *url; + LDAP *ld = NULL; + int ret; + int fd; + int proto = -1; + LDAPURLDesc *lud = NULL; + + PyThreadState *save; + + if (!PyArg_ParseTuple(args, "is:initialize_fd", &fd, &url)) + return NULL; + + /* Get LDAP protocol from scheme */ + ret = ldap_url_parse(url, &lud); + if (ret != LDAP_SUCCESS) + return LDAPerr(ret); + + if (strcmp(lud->lud_scheme, "ldap") == 0) { + proto = LDAP_PROTO_TCP; + } + else if (strcmp(lud->lud_scheme, "ldaps") == 0) { + proto = LDAP_PROTO_TCP; + } + else if (strcmp(lud->lud_scheme, "ldapi") == 0) { + proto = LDAP_PROTO_IPC; + } +#ifdef LDAP_CONNECTIONLESS + else if (strcmp(lud->lud_scheme, "cldap") == 0) { + proto = LDAP_PROTO_UDP; + } +#endif + else { + ldap_free_urldesc(lud); + PyErr_SetString(PyExc_ValueError, "unsupported URL scheme"); + return NULL; + } + ldap_free_urldesc(lud); + + save = PyEval_SaveThread(); + ret = ldap_init_fd((ber_socket_t) fd, proto, url, &ld); + PyEval_RestoreThread(save); + + if (ret != LDAP_SUCCESS) + return LDAPerror(ld); + return (PyObject *)newLDAPObject(ld); } +#endif /* ldap_str2dn */ @@ -75,9 +135,8 @@ l_ldap_str2dn(PyObject *unused, PyObject *args) tuple = Py_BuildValue("(O&O&i)", LDAPberval_to_unicode_object, &ava->la_attr, LDAPberval_to_unicode_object, &ava->la_value, - ava-> - la_flags & ~(LDAP_AVA_FREE_ATTR | - LDAP_AVA_FREE_VALUE)); + ava->la_flags & ~(LDAP_AVA_FREE_ATTR | + LDAP_AVA_FREE_VALUE)); if (!tuple) { Py_DECREF(rdnlist); goto failed; @@ -133,6 +192,9 @@ l_ldap_get_option(PyObject *self, PyObject *args) static PyMethodDef methods[] = { {"initialize", (PyCFunction)l_ldap_initialize, METH_VARARGS}, +#ifdef HAVE_LDAP_INIT_FD + {"initialize_fd", (PyCFunction)l_ldap_initialize_fd, METH_VARARGS}, +#endif {"str2dn", (PyCFunction)l_ldap_str2dn, METH_VARARGS}, {"set_option", (PyCFunction)l_ldap_set_option, METH_VARARGS}, {"get_option", (PyCFunction)l_ldap_get_option, METH_VARARGS}, diff --git a/Modules/ldapcontrol.c b/Modules/ldapcontrol.c index f53e681a..e287e9a3 100644 --- a/Modules/ldapcontrol.c +++ b/Modules/ldapcontrol.c @@ -6,8 +6,6 @@ #include "berval.h" #include "constants.h" -#include "lber.h" - /* Prints to stdout the contents of an array of LDAPControl objects */ /* XXX: This is a debugging tool, and the printf generates some warnings @@ -82,6 +80,7 @@ Tuple_to_LDAPControl(PyObject *tup) return NULL; lc = PyMem_NEW(LDAPControl, 1); + if (lc == NULL) { PyErr_NoMemory(); return NULL; @@ -91,6 +90,7 @@ Tuple_to_LDAPControl(PyObject *tup) len = strlen(oid); lc->ldctl_oid = PyMem_NEW(char, len + 1); + if (lc->ldctl_oid == NULL) { PyErr_NoMemory(); LDAPControl_DEL(lc); @@ -137,6 +137,7 @@ LDAPControls_from_object(PyObject *list, LDAPControl ***controls_ret) len = PySequence_Length(list); ldcs = PyMem_NEW(LDAPControl *, len + 1); + if (ldcs == NULL) { PyErr_NoMemory(); return 0; @@ -339,6 +340,7 @@ encode_assertion_control(PyObject *self, PyObject *args) char *assertion_filterstr; struct berval ctrl_val; LDAP *ld = NULL; + PyThreadState *save; if (!PyArg_ParseTuple(args, "s:encode_assertion_control", &assertion_filterstr)) { @@ -348,21 +350,27 @@ encode_assertion_control(PyObject *self, PyObject *args) /* XXX: ldap_create() is a nasty and slow hack. It's creating a full blown * LDAP object just to encode assertion controls. */ - Py_BEGIN_ALLOW_THREADS err = ldap_create(&ld); - Py_END_ALLOW_THREADS if (err != LDAP_SUCCESS) - return LDAPerror(ld, "ldap_create"); + save = PyEval_SaveThread(); + err = ldap_create(&ld); + PyEval_RestoreThread(save); + if (err != LDAP_SUCCESS) + return LDAPerror(ld); - err = - ldap_create_assertion_control_value(ld, assertion_filterstr, - &ctrl_val); + err = ldap_create_assertion_control_value(ld, assertion_filterstr, + &ctrl_val); if (err != LDAP_SUCCESS) { - LDAPerror(ld, "ldap_create_assertion_control_value"); - Py_BEGIN_ALLOW_THREADS ldap_unbind_ext(ld, NULL, NULL); - Py_END_ALLOW_THREADS return NULL; + LDAPerror(ld); + save = PyEval_SaveThread(); + ldap_unbind_ext(ld, NULL, NULL); + PyEval_RestoreThread(save); + return NULL; } - Py_BEGIN_ALLOW_THREADS ldap_unbind_ext(ld, NULL, NULL); - Py_END_ALLOW_THREADS res = LDAPberval_to_object(&ctrl_val); + save = PyEval_SaveThread(); + ldap_unbind_ext(ld, NULL, NULL); + PyEval_RestoreThread(save); + res = LDAPberval_to_object(&ctrl_val); + if (ctrl_val.bv_val != NULL) { ber_memfree(ctrl_val.bv_val); } diff --git a/Modules/ldapcontrol.h b/Modules/ldapcontrol.h index de694c07..74cae423 100644 --- a/Modules/ldapcontrol.h +++ b/Modules/ldapcontrol.h @@ -4,7 +4,6 @@ #define __h_ldapcontrol #include "common.h" -#include "ldap.h" void LDAPinit_control(PyObject *d); void LDAPControl_List_DEL(LDAPControl **); diff --git a/Modules/ldapmodule.c b/Modules/ldapmodule.c index 8bd55ab4..34d5a24c 100644 --- a/Modules/ldapmodule.c +++ b/Modules/ldapmodule.c @@ -72,13 +72,6 @@ init_ldap_module(void) LDAPinit_functions(d); LDAPinit_control(d); - /* Marker for LDAPBytesWarning stack walking - * See _raise_byteswarning in ldapobject.py - */ - if (PyModule_AddIntConstant(m, "_LDAP_WARN_SKIP_FRAME", 1) != 0) { - return NULL; - } - /* Check for errors */ if (PyErr_Occurred()) Py_FatalError("can't initialize module _ldap"); diff --git a/Modules/message.c b/Modules/message.c index 2c05488a..22aa313c 100644 --- a/Modules/message.c +++ b/Modules/message.c @@ -53,7 +53,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, if (dn == NULL) { Py_DECREF(result); ldap_msgfree(m); - return LDAPerror(ld, "ldap_get_dn"); + return LDAPerror(ld); } attrdict = PyDict_New(); @@ -69,7 +69,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, Py_DECREF(result); ldap_msgfree(m); ldap_memfree(dn); - return LDAPerror(ld, "ldap_get_entry_controls"); + return LDAPerror(ld); } /* convert serverctrls to list of tuples */ @@ -81,7 +81,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, ldap_msgfree(m); ldap_memfree(dn); ldap_controls_free(serverctrls); - return LDAPerror(ld, "LDAPControls_to_List"); + return LDAPerror(ld); } ldap_controls_free(serverctrls); @@ -201,7 +201,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, Py_DECREF(reflist); Py_DECREF(result); ldap_msgfree(m); - return LDAPerror(ld, "ldap_parse_reference"); + return LDAPerror(ld); } /* convert serverctrls to list of tuples */ if (!(pyctrls = LDAPControls_to_List(serverctrls))) { @@ -212,7 +212,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, Py_DECREF(result); ldap_msgfree(m); ldap_controls_free(serverctrls); - return LDAPerror(ld, "LDAPControls_to_List"); + return LDAPerror(ld); } ldap_controls_free(serverctrls); if (refs) { @@ -255,7 +255,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, 0) != LDAP_SUCCESS) { Py_DECREF(result); ldap_msgfree(m); - return LDAPerror(ld, "ldap_parse_intermediate"); + return LDAPerror(ld); } /* convert serverctrls to list of tuples */ if (!(pyctrls = LDAPControls_to_List(serverctrls))) { @@ -267,26 +267,41 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls, ldap_controls_free(serverctrls); ldap_memfree(retoid); ber_bvfree(retdata); - return LDAPerror(ld, "LDAPControls_to_List"); + return LDAPerror(ld); } ldap_controls_free(serverctrls); valuestr = LDAPberval_to_object(retdata); ber_bvfree(retdata); + if (valuestr == NULL) { + ldap_memfree(retoid); + Py_DECREF(result); + ldap_msgfree(m); + return NULL; + } + pyoid = PyUnicode_FromString(retoid); ldap_memfree(retoid); if (pyoid == NULL) { + Py_DECREF(valuestr); + Py_DECREF(result); + ldap_msgfree(m); + return NULL; + } + + valtuple = Py_BuildValue("(NNN)", pyoid, valuestr, pyctrls); + if (valtuple == NULL) { + Py_DECREF(result); + ldap_msgfree(m); + return NULL; + } + + if (PyList_Append(result, valtuple) == -1) { + Py_DECREF(valtuple); Py_DECREF(result); ldap_msgfree(m); return NULL; } - valtuple = Py_BuildValue("(OOO)", pyoid, - valuestr ? valuestr : Py_None, - pyctrls); - Py_DECREF(pyoid); - Py_DECREF(valuestr); - Py_XDECREF(pyctrls); - PyList_Append(result, valtuple); Py_DECREF(valtuple); } } diff --git a/Modules/message.h b/Modules/message.h index 2978ea56..ed73f32c 100644 --- a/Modules/message.h +++ b/Modules/message.h @@ -4,8 +4,6 @@ #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); diff --git a/Modules/options.c b/Modules/options.c index 85560e62..db5fde3e 100644 --- a/Modules/options.c +++ b/Modules/options.c @@ -43,6 +43,10 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) double doubleval; char *strval; struct timeval tv; +#if HAVE_SASL + /* unsigned long */ + ber_len_t blen; +#endif void *ptr; LDAP *ld; LDAPControl **controls = NULL; @@ -88,10 +92,9 @@ 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_REQUIRE_SAN + case LDAP_OPT_X_TLS_REQUIRE_SAN: #endif -#ifdef HAVE_SASL - case LDAP_OPT_X_SASL_SSF_MIN: - case LDAP_OPT_X_SASL_SSF_MAX: #endif #ifdef LDAP_OPT_X_KEEPALIVE_IDLE case LDAP_OPT_X_KEEPALIVE_IDLE: @@ -108,6 +111,17 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) return 0; ptr = &intval; 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: + 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 @@ -135,6 +149,7 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) return 0; ptr = strval; break; + case LDAP_OPT_TIMEOUT: case LDAP_OPT_NETWORK_TIMEOUT: /* Float valued timeval options */ @@ -185,11 +200,18 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value) return 0; } - if (self) + if (self) { LDAP_BEGIN_ALLOW_THREADS(self); - res = ldap_set_option(ld, option, ptr); - if (self) + res = ldap_set_option(ld, option, ptr); LDAP_END_ALLOW_THREADS(self); + } + else { + PyThreadState *save; + + save = PyEval_SaveThread(); + res = ldap_set_option(NULL, option, ptr); + PyEval_RestoreThread(save); + } if ((option == LDAP_OPT_SERVER_CONTROLS) || (option == LDAP_OPT_CLIENT_CONTROLS)) @@ -203,6 +225,26 @@ 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) { @@ -212,20 +254,23 @@ LDAP_get_option(LDAPObject *self, int option) LDAPAPIInfo apiinfo; LDAPControl **lcs; char *strval; +#if HAVE_SASL + /* unsigned long */ + ber_len_t blen; +#endif PyObject *extensions, *v; Py_ssize_t i, num_extensions; - LDAP *ld; - - ld = self ? self->ldap : NULL; switch (option) { +#ifdef HAVE_SASL + case LDAP_OPT_X_SASL_SSF_EXTERNAL: + /* Write-only options */ + PyErr_SetString(PyExc_ValueError, "write-only option"); + return NULL; +#endif case LDAP_OPT_API_INFO: apiinfo.ldapai_info_version = LDAP_API_INFO_VERSION; - if (self) - LDAP_BEGIN_ALLOW_THREADS(self); - res = ldap_get_option(ld, option, &apiinfo); - if (self) - LDAP_END_ALLOW_THREADS(self); + res = LDAP_int_get_option(self, option, &apiinfo); if (res != LDAP_OPT_SUCCESS) return option_error(res, "ldap_get_option"); @@ -236,8 +281,8 @@ LDAP_get_option(LDAPObject *self, int option) extensions = PyTuple_New(num_extensions); for (i = 0; i < num_extensions; i++) PyTuple_SET_ITEM(extensions, i, - PyUnicode_FromString(apiinfo. - ldapai_extensions[i])); + PyUnicode_FromString(apiinfo.ldapai_extensions + [i])); /* return api info as a dictionary */ v = Py_BuildValue("{s:i, s:i, s:i, s:s, s:i, s:O}", @@ -257,9 +302,6 @@ LDAP_get_option(LDAPObject *self, int option) return v; -#ifdef HAVE_SASL - case LDAP_OPT_X_SASL_SSF: -#endif case LDAP_OPT_REFERRALS: case LDAP_OPT_RESTART: case LDAP_OPT_DEREF: @@ -278,10 +320,9 @@ 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_REQUIRE_SAN + case LDAP_OPT_X_TLS_REQUIRE_SAN: #endif -#ifdef HAVE_SASL - case LDAP_OPT_X_SASL_SSF_MIN: - case LDAP_OPT_X_SASL_SSF_MAX: #endif #ifdef LDAP_OPT_X_SASL_NOCANON case LDAP_OPT_X_SASL_NOCANON: @@ -299,15 +340,22 @@ LDAP_get_option(LDAPObject *self, int option) case LDAP_OPT_X_KEEPALIVE_INTERVAL: #endif /* Integer-valued options */ - if (self) - LDAP_BEGIN_ALLOW_THREADS(self); - res = ldap_get_option(ld, option, &intval); - if (self) - LDAP_END_ALLOW_THREADS(self); + res = LDAP_int_get_option(self, option, &intval); if (res != LDAP_OPT_SUCCESS) return option_error(res, "ldap_get_option"); return PyInt_FromLong(intval); +#ifdef HAVE_SASL + case LDAP_OPT_X_SASL_SSF: + case LDAP_OPT_X_SASL_SSF_MIN: + case LDAP_OPT_X_SASL_SSF_MAX: + /* 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: #ifdef LDAP_OPT_DEFBASE @@ -347,11 +395,7 @@ LDAP_get_option(LDAPObject *self, int option) #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); + res = LDAP_int_get_option(self, option, &strval); if (res != LDAP_OPT_SUCCESS) return option_error(res, "ldap_get_option"); if (strval == NULL) { @@ -365,11 +409,7 @@ LDAP_get_option(LDAPObject *self, int option) case LDAP_OPT_TIMEOUT: case LDAP_OPT_NETWORK_TIMEOUT: /* Double-valued timeval options */ - if (self) - LDAP_BEGIN_ALLOW_THREADS(self); - res = ldap_get_option(ld, option, &tv); - if (self) - LDAP_END_ALLOW_THREADS(self); + res = LDAP_int_get_option(self, option, &tv); if (res != LDAP_OPT_SUCCESS) return option_error(res, "ldap_get_option"); if (tv == NULL) { @@ -384,12 +424,7 @@ LDAP_get_option(LDAPObject *self, int option) case LDAP_OPT_SERVER_CONTROLS: case LDAP_OPT_CLIENT_CONTROLS: - if (self) - LDAP_BEGIN_ALLOW_THREADS(self); - res = ldap_get_option(ld, option, &lcs); - if (self) - LDAP_END_ALLOW_THREADS(self); - + res = LDAP_int_get_option(self, option, &lcs); if (res != LDAP_OPT_SUCCESS) return option_error(res, "ldap_get_option"); diff --git a/README b/README index 81db9bb3..8045555d 100644 --- a/README +++ b/README @@ -127,3 +127,27 @@ their contributions and input into this package. Thanks! We may have missed someone: please mail us if we have omitted your name. + +Licence +======= + +The python-ldap project comes with a LICENCE file. + +We are aware that its text is unclear, but it cannot be changed: +all authors of python-ldap would need to approve the licence change, +but a complete list of all the authors is not available. +(Note that the Git repository of the project is incomplete. +Furthermore, commits imported from CVS lack authorship information; users +"stroeder" or "leonard" are commiters (reviewers), but sometimes not +authors of the committed code.) + +The current maintainers assume that the license is the sentence that refers +to "Python-style license" and assume this means a highly permissive open source +license that only requires preservation of the text of the LICENCE file +(including the disclaimer paragraph). + +------------------------------------------------------------------------------- + +All contributions committed since July 1st, 2021, as well as some past +contributions, are licensed under the MIT license. +The MIT licence and more details are listed in the file LICENCE.MIT. diff --git a/Tests/__init__.py b/Tests/__init__.py index d6545732..ea28d0ce 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 diff --git a/Tests/t_bind.py b/Tests/t_bind.py index 3e2b67f8..ba90c4cd 100644 --- a/Tests/t_bind.py +++ b/Tests/t_bind.py @@ -1,14 +1,3 @@ -from __future__ import unicode_literals - -import sys - -if sys.version_info[0] <= 2: - PY2 = True - text_type = unicode -else: - PY2 = False - text_type = str - import os import unittest @@ -44,19 +33,6 @@ def test_unicode_bind(self): l = self._get_ldapobject(False) l.simple_bind("CN=user", self.unicode_val) - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_unicode_bind_bytesmode(self): - l = self._get_ldapobject(True) - with self.assertRaises(TypeError): - l.simple_bind_s(self.dn_unicode, self.unicode_val_bytes) - - with self.assertRaises(TypeError): - l.simple_bind_s(self.dn_bytes, self.unicode_val) - - # Works when encoded to UTF-8 - with self.assertRaises(ldap.INVALID_CREDENTIALS): - l.simple_bind_s(self.dn_bytes, self.unicode_val_bytes) - def test_unicode_bind_no_bytesmode(self): l = self._get_ldapobject(False) with self.assertRaises(TypeError): diff --git a/Tests/t_cext.py b/Tests/t_cext.py index 96c3b2c8..816af8dd 100644 --- a/Tests/t_cext.py +++ b/Tests/t_cext.py @@ -1,14 +1,12 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap's C wrapper module _ldap See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - +import contextlib import errno import os +import socket import unittest # Switch off processing .ldaprc or ldap.conf before importing _ldap @@ -16,7 +14,7 @@ # import the plain C wrapper module import _ldap -from slapdtest import SlapdTestCase, requires_tls +from slapdtest import SlapdTestCase, requires_tls, requires_init_fd class TestLdapCExtension(SlapdTestCase): @@ -29,7 +27,7 @@ class TestLdapCExtension(SlapdTestCase): @classmethod def setUpClass(cls): - super(TestLdapCExtension, cls).setUpClass() + super().setUpClass() # add two initial objects after server was started and is still empty suffix_dc = cls.server.suffix.split(',')[0][3:] cls.server._log.debug( @@ -53,14 +51,14 @@ def setUpClass(cls): ) def setUp(self): - super(TestLdapCExtension, self).setUp() + super().setUp() self._writesuffix = None def tearDown(self): # cleanup test subtree if self._writesuffix is not None: self.server.ldapdelete(self._writesuffix, recursive=True) - super(TestLdapCExtension, self).tearDown() + super().tearDown() @property def writesuffix(self): @@ -92,14 +90,35 @@ def _open_conn(self, bind=True): """ l = _ldap.initialize(self.server.ldap_uri) if bind: - # Perform a simple bind - l.set_option(_ldap.OPT_PROTOCOL_VERSION, _ldap.VERSION3) - m = l.simple_bind(self.server.root_dn, self.server.root_pw) - result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ONE, self.timeout) - self.assertEqual(result, _ldap.RES_BIND) - self.assertEqual(type(msgid), type(0)) + self._bind_conn(l) return l + @contextlib.contextmanager + def _open_conn_fd(self, bind=True): + sock = socket.create_connection( + (self.server.hostname, self.server.port) + ) + try: + l = _ldap.initialize_fd(sock.fileno(), self.server.ldap_uri) + if bind: + self._bind_conn(l) + yield sock, l + finally: + try: + sock.close() + except OSError: + # already closed + pass + + def _bind_conn(self, l): + # Perform a simple bind + l.set_option(_ldap.OPT_PROTOCOL_VERSION, _ldap.VERSION3) + m = l.simple_bind(self.server.root_dn, self.server.root_pw) + result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ONE, self.timeout) + self.assertEqual(result, _ldap.RES_BIND) + self.assertEqual(type(msgid), type(0)) + + # Test for the existence of a whole bunch of constants # that the C module is supposed to export def test_constants(self): @@ -224,6 +243,33 @@ def test_test_flags(self): 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("", "") @@ -241,7 +287,7 @@ def test_anon_rootdse_search(self): '', _ldap.SCOPE_BASE, '(objectClass=*)', - [str('objectClass'), str('namingContexts')], + ['objectClass', 'namingContexts'], ) self.assertEqual(type(m), type(0)) result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) @@ -381,30 +427,36 @@ def test_compare(self): self.assertEqual(type(m), type(0)) result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout) self.assertEqual(result, _ldap.RES_ADD) + # try a false compare m = l.compare_ext(dn, "userPassword", "bad_string") - try: + with self.assertRaises(_ldap.COMPARE_FALSE) as e: r = l.result4(m, _ldap.MSG_ALL, self.timeout) - except _ldap.COMPARE_FALSE: - pass - else: - self.fail("expected COMPARE_FALSE, got %r" % r) + + self.assertEqual(e.exception.args[0]['msgid'], m) + self.assertEqual(e.exception.args[0]['msgtype'], _ldap.RES_COMPARE) + self.assertEqual(e.exception.args[0]['result'], 5) + self.assertFalse(e.exception.args[0]['ctrls']) + # try a true compare m = l.compare_ext(dn, "userPassword", "the_password") - try: + with self.assertRaises(_ldap.COMPARE_TRUE) as e: r = l.result4(m, _ldap.MSG_ALL, self.timeout) - except _ldap.COMPARE_TRUE: - pass - else: - self.fail("expected COMPARE_TRUE, got %r" % r) + + self.assertEqual(e.exception.args[0]['msgid'], m) + self.assertEqual(e.exception.args[0]['msgtype'], _ldap.RES_COMPARE) + self.assertEqual(e.exception.args[0]['result'], 6) + self.assertFalse(e.exception.args[0]['ctrls']) + # try a compare on bad attribute m = l.compare_ext(dn, "badAttribute", "ignoreme") - try: + with self.assertRaises(_ldap.error) as e: r = l.result4(m, _ldap.MSG_ALL, self.timeout) - except _ldap.error: - pass - else: - self.fail("expected LDAPError, got %r" % r) + + self.assertEqual(e.exception.args[0]['msgid'], m) + self.assertEqual(e.exception.args[0]['msgtype'], _ldap.RES_COMPARE) + self.assertEqual(e.exception.args[0]['result'], 17) + self.assertFalse(e.exception.args[0]['ctrls']) def test_delete_no_such_object(self): """ @@ -880,6 +932,29 @@ def test_tls_packages(self): package = _ldap.get_option(_ldap.OPT_X_TLS_PACKAGE) self.assertIn(package, {"GnuTLS", "MozNSS", "OpenSSL"}) + @unittest.skipUnless( + hasattr(_ldap, "OPT_X_TLS_REQUIRE_SAN"), + reason="Test requires OPT_X_TLS_REQUIRE_SAN" + ) + def test_require_san(self): + l = self._open_conn(bind=False) + value = l.get_option(_ldap.OPT_X_TLS_REQUIRE_SAN) + self.assertIn( + value, + { + _ldap.OPT_X_TLS_NEVER, + _ldap.OPT_X_TLS_ALLOW, + _ldap.OPT_X_TLS_TRY, + _ldap.OPT_X_TLS_DEMAND, + _ldap.OPT_X_TLS_HARD, + } + ) + l.set_option(_ldap.OPT_X_TLS_REQUIRE_SAN, _ldap.OPT_X_TLS_TRY) + self.assertEqual( + l.get_option(_ldap.OPT_X_TLS_REQUIRE_SAN), + _ldap.OPT_X_TLS_TRY + ) + if __name__ == '__main__': unittest.main() diff --git a/Tests/t_cidict.py b/Tests/t_cidict.py index fa5a39b8..97146ec8 100644 --- a/Tests/t_cidict.py +++ b/Tests/t_cidict.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap's module ldap.cidict @@ -7,6 +6,7 @@ import os import unittest +import warnings # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' @@ -48,6 +48,42 @@ def test_cidict(self): self.assertEqual(cix.has_key("abcdef"), False) self.assertEqual(cix.has_key("AbCDef"), False) + def test_strlist_deprecated(self): + strlist_funcs = [ + ldap.cidict.strlist_intersection, + ldap.cidict.strlist_minus, + ldap.cidict.strlist_union + ] + for strlist_func in strlist_funcs: + with warnings.catch_warnings(record=True) as w: + warnings.resetwarnings() + warnings.simplefilter("always", DeprecationWarning) + strlist_func(["a"], ["b"]) + self.assertEqual(len(w), 1) + + def test_cidict_data(self): + """test the deprecated data atrtribute""" + d = ldap.cidict.cidict({'A': 1, 'B': 2}) + with warnings.catch_warnings(record=True) as w: + warnings.resetwarnings() + warnings.simplefilter('always', DeprecationWarning) + data = d.data + assert data == {'a': 1, 'b': 2} + self.assertEqual(len(w), 1) + + def test_copy(self): + cix1 = ldap.cidict.cidict( + {"a": 1, "B": 2} + ) + cix2 = cix1.copy() + self.assertEqual(cix1, cix2) + cix1["c"] = 3 + self.assertNotIn("c", cix2) + cix2["C"] = 4 + self.assertNotEqual(cix1, cix2) + self.assertEqual(list(cix1.keys()), ["a", "B", "c"]) + self.assertEqual(list(cix2.keys()), ["a", "B", "C"]) + if __name__ == '__main__': unittest.main() diff --git a/Tests/t_edit.py b/Tests/t_edit.py index a5b3f657..5d8b3f06 100644 --- a/Tests/t_edit.py +++ b/Tests/t_edit.py @@ -1,14 +1,3 @@ -from __future__ import unicode_literals - -import sys - -if sys.version_info[0] <= 2: - PY2 = True - text_type = unicode -else: - PY2 = False - text_type = str - import os import unittest @@ -24,7 +13,7 @@ class EditionTests(SlapdTestCase): @classmethod def setUpClass(cls): - super(EditionTests, cls).setUpClass() + super().setUpClass() base = cls.server.suffix suffix_dc = base.split(',')[0][3:] diff --git a/Tests/t_ldap_controls_readentry.py b/Tests/t_ldap_controls_readentry.py new file mode 100644 index 00000000..313a7905 --- /dev/null +++ b/Tests/t_ldap_controls_readentry.py @@ -0,0 +1,28 @@ +import os +import unittest + +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ['LDAPNOINIT'] = '1' + +from ldap.controls import readentry # noqa: E402 + + +PRC_ENC = b'db\x04)uid=Administrator,cn=users,l=school,l=dev0503\x04\tentryUUID1&\x04$5d96cc2c-8e13-103a-8ca5-2f74868e0e44' +PRC_DEC = b'0\x0b\x04\tentryUUID' + + +class TestLibldapControls(unittest.TestCase): + + def test_pagedresults_encode(self): + pr = readentry.PostReadControl(True, ['entryUUID']) + self.assertEqual(pr.encodeControlValue(), PRC_DEC) + + def test_readentry_decode(self): + pr = readentry.PostReadControl(True, ['entryUUID']) + pr.decodeControlValue(PRC_ENC) + self.assertIsInstance(pr.dn, str) + self.assertEqual(pr.entry, {'entryUUID': [b'5d96cc2c-8e13-103a-8ca5-2f74868e0e44']}) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/t_ldap_dn.py b/Tests/t_ldap_dn.py index 4b4dd319..86d36403 100644 --- a/Tests/t_ldap_dn.py +++ b/Tests/t_ldap_dn.py @@ -1,12 +1,8 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap's module ldap.dn See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - # from Python's standard lib import os import unittest @@ -50,6 +46,11 @@ def test_escape_dn_chars(self): 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(' foobar'), '\\ foobar') + self.assertEqual(ldap.dn.escape_dn_chars(' '), '\\ ') + self.assertEqual(ldap.dn.escape_dn_chars(' '), '\\ \\ ') + self.assertEqual(ldap.dn.escape_dn_chars('foobar '), 'foobar\\ ') + self.assertEqual(ldap.dn.escape_dn_chars('f+o>o,bo\\,b\\= (3, 5, 0): - # Python 3.4.x does not include 'search_ext()' in message - self.assertEqual( - "search_ext() argument 1 must be str, not bytes", - text_type(e.exception) - ) + # Python 3.4.x does not include 'search_ext()' in message + self.assertEqual( + "search_ext() argument 1 must be str, not bytes", + str(e.exception) + ) with self.assertRaises(TypeError) as e: l.search_s( base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*'] ) - if PY2: - self.assertIn( - u"got type 'str' for 'filterstr'", text_type(e.exception) - ) - elif sys.version_info >= (3, 5, 0): - self.assertEqual( - "search_ext() argument 3 must be str, not bytes", - text_type(e.exception) - ) + self.assertEqual( + "search_ext() argument 3 must be str, not bytes", + str(e.exception) + ) with self.assertRaises(TypeError) as e: l.search_s( base, ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*'] ) - if PY2: - self.assertIn( - u"got type 'str' for 'attrlist'", text_type(e.exception) - ) - elif sys.version_info >= (3, 5, 0): - self.assertEqual( - ('attrs_from_List(): expected string in list', b'*'), - e.exception.args - ) + self.assertEqual( + ('attrs_from_List(): expected string in list', b'*'), + e.exception.args + ) def test_search_keys_are_text(self): base = self.server.suffix @@ -157,143 +159,12 @@ def test_search_keys_are_text(self): result.sort() dn, fields = result[0] self.assertEqual(dn, 'cn=Foo1,%s' % base) - self.assertEqual(type(dn), text_type) + self.assertEqual(type(dn), str) for key, values in fields.items(): - self.assertEqual(type(key), text_type) + self.assertEqual(type(key), str) for value in values: self.assertEqual(type(value), bytes) - def _get_bytes_ldapobject(self, explicit=True, **kwargs): - if explicit: - kwargs.setdefault('bytes_mode', True) - else: - kwargs = {} - return self._open_ldap_conn( - who=self.server.root_dn.encode('utf-8'), - cred=self.server.root_pw.encode('utf-8'), - **kwargs - ) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_search_requires_bytes(self): - l = self._get_bytes_ldapobject() - base = self.server.suffix - - with self.assertRaises(TypeError): - l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*']) - with self.assertRaises(TypeError): - l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*']) - with self.assertRaises(TypeError): - l.search_s(base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', [b'*']) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_search_results_have_bytes(self): - l = self._get_bytes_ldapobject() - base = self.server.suffix - result = l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, b'(cn=Foo*)', [b'*']) - result.sort() - dn, fields = result[0] - self.assertEqual(dn, b'cn=Foo1,%s' % base) - self.assertEqual(type(dn), bytes) - for key, values in fields.items(): - self.assertEqual(type(key), bytes) - for value in values: - self.assertEqual(type(value), bytes) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_search_defaults(self): - l = self._get_bytes_ldapobject() - base = 'cn=Foo1,' + self.server.suffix - kwargs = dict( - base=base.encode('utf-8'), - scope=ldap.SCOPE_SUBTREE, - # filterstr=b'(objectClass=*)' - ) - expected = [ - ( - base, - {'cn': [b'Foo1'], 'objectClass': [b'organizationalRole']} - ), - ] - - result = l.search_s(**kwargs) - self.assertEqual(result, expected) - result = l.search_st(**kwargs) - self.assertEqual(result, expected) - result = l.search_ext_s(**kwargs) - self.assertEqual(result, expected) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_unset_bytesmode_search_warns_bytes(self): - l = self._get_bytes_ldapobject(explicit=False) - base = self.server.suffix - - l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*']) - l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*']) - l.search_s(base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', [b'*']) - - def _search_wrong_type(self, bytes_mode, strictness): - if bytes_mode: - l = self._get_bytes_ldapobject(bytes_strictness=strictness) - else: - l = self._open_ldap_conn(bytes_mode=False, - bytes_strictness=strictness) - base = 'cn=Foo1,' + self.server.suffix - if not bytes_mode: - base = base.encode('utf-8') - result = l.search_s(base, scope=ldap.SCOPE_SUBTREE) - return result[0][-1]['cn'] - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_silent(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - self._search_wrong_type(bytes_mode=True, strictness='silent') - self.assertEqual(w, []) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_warn(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - self._search_wrong_type(bytes_mode=True, strictness='warn') - self.assertEqual(len(w), 1) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_error(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - with self.assertRaises(TypeError): - self._search_wrong_type(bytes_mode=True, strictness='error') - self.assertEqual(w, []) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_textmode_silent(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - self._search_wrong_type(bytes_mode=True, strictness='silent') - self.assertEqual(w, []) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_textmode_warn(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - self._search_wrong_type(bytes_mode=True, strictness='warn') - self.assertEqual(len(w), 1) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_textmode_error(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - with self.assertRaises(TypeError): - self._search_wrong_type(bytes_mode=True, strictness='error') - self.assertEqual(w, []) - def test_search_accepts_unicode_dn(self): base = self.server.suffix l = self._ldap_conn @@ -315,7 +186,7 @@ def test_attrlist_accepts_unicode(self): result.sort() for dn, attrs in result: - self.assertIsInstance(dn, text_type) + self.assertIsInstance(dn, str) self.assertEqual(attrs, {}) def test001_search_subtree(self): @@ -418,37 +289,18 @@ def test_find_unique_entry(self): def test_search_subschema(self): l = self._ldap_conn dn = l.search_subschemasubentry_s() - self.assertIsInstance(dn, text_type) + self.assertIsInstance(dn, str) self.assertEqual(dn, "cn=Subschema") subschema = l.read_subschemasubentry_s(dn) self.assertIsInstance(subschema, dict) self.assertEqual( sorted(subschema), [ - u'attributeTypes', - u'ldapSyntaxes', - u'matchingRuleUse', - u'matchingRules', - u'objectClasses' - ] - ) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_search_subschema_have_bytes(self): - l = self._get_bytes_ldapobject() - dn = l.search_subschemasubentry_s() - self.assertIsInstance(dn, bytes) - self.assertEqual(dn, b"cn=Subschema") - subschema = l.read_subschemasubentry_s(dn) - self.assertIsInstance(subschema, dict) - self.assertEqual( - sorted(subschema), - [ - b'attributeTypes', - b'ldapSyntaxes', - b'matchingRuleUse', - b'matchingRules', - b'objectClasses' + 'attributeTypes', + 'ldapSyntaxes', + 'matchingRuleUse', + 'matchingRules', + 'objectClasses' ] ) @@ -465,7 +317,7 @@ def test004_enotconn(self): info = ldap_err.args[0]['info'] expected_info = os.strerror(errno.ENOTCONN) if info != expected_info: - self.fail("expected info=%r, got %d" % (expected_info, info)) + self.fail(f"expected info={expected_info!r}, got {info!r}") else: self.fail("expected SERVER_DOWN, got %r" % r) @@ -482,7 +334,7 @@ def test005_invalid_credentials(self): @requires_sasl() @requires_ldapi() - def test006_sasl_extenal_bind_s(self): + def test006_sasl_external_bind_s(self): l = self.ldap_object_class(self.server.ldapi_uri) l.sasl_external_bind_s() self.assertEqual(l.whoami_s(), 'dn:'+self.server.root_dn.lower()) @@ -491,6 +343,30 @@ def test006_sasl_extenal_bind_s(self): l.sasl_external_bind_s(authz_id=authz_id) self.assertEqual(l.whoami_s(), authz_id.lower()) + @requires_sasl() + @requires_ldapi() + def test006_sasl_options(self): + l = self.ldap_object_class(self.server.ldapi_uri) + + minssf = l.get_option(ldap.OPT_X_SASL_SSF_MIN) + self.assertGreaterEqual(minssf, 0) + self.assertLessEqual(minssf, 256) + maxssf = l.get_option(ldap.OPT_X_SASL_SSF_MAX) + self.assertGreaterEqual(maxssf, 0) + # libldap sets SSF_MAX to INT_MAX + self.assertLessEqual(maxssf, 2**31 - 1) + + l.set_option(ldap.OPT_X_SASL_SSF_MIN, 56) + l.set_option(ldap.OPT_X_SASL_SSF_MAX, 256) + self.assertEqual(l.get_option(ldap.OPT_X_SASL_SSF_MIN), 56) + self.assertEqual(l.get_option(ldap.OPT_X_SASL_SSF_MAX), 256) + + l.sasl_external_bind_s() + with self.assertRaisesRegex(ValueError, "write-only option"): + l.get_option(ldap.OPT_X_SASL_SSF_EXTERNAL) + l.set_option(ldap.OPT_X_SASL_SSF_EXTERNAL, 256) + self.assertEqual(l.whoami_s(), 'dn:' + self.server.root_dn.lower()) + def test007_timeout(self): l = self.ldap_object_class(self.server.ldap_uri) m = l.search_ext(self.server.suffix, ldap.SCOPE_SUBTREE, '(objectClass=*)') @@ -507,50 +383,14 @@ def assertIsSubclass(self, cls, other): def test_simple_bind_noarg(self): l = self.ldap_object_class(self.server.ldap_uri) l.simple_bind_s() - self.assertEqual(l.whoami_s(), u'') + self.assertEqual(l.whoami_s(), '') l = self.ldap_object_class(self.server.ldap_uri) l.simple_bind_s(None, None) - self.assertEqual(l.whoami_s(), u'') - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_ldapbyteswarning(self): - self.assertIsSubclass(ldap.LDAPBytesWarning, BytesWarning) - self.assertIsSubclass(ldap.LDAPBytesWarning, Warning) - self.assertIsInstance(self.server.suffix, text_type) - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - conn = self._get_bytes_ldapobject(explicit=False) - result = conn.search_s( - self.server.suffix, - ldap.SCOPE_SUBTREE, - b'(cn=Foo*)', - attrlist=[b'*'], - ) - self.assertEqual(len(result), 4) - - # ReconnectLDAP only emits one warning - self.assertGreaterEqual(len(w), 1, w) - msg = w[-1] - self.assertIs(msg.category, ldap.LDAPBytesWarning) - self.assertEqual( - text_type(msg.message), - "Received non-bytes value for 'base' in bytes " - "mode; please choose an explicit option for bytes_mode on your " - "LDAP connection" - ) - - @contextlib.contextmanager - def catch_byteswarnings(self, *args, **kwargs): - with warnings.catch_warnings(record=True) as w: - conn = self._get_bytes_ldapobject(*args, **kwargs) - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - yield conn, w + self.assertEqual(l.whoami_s(), '') def _check_byteswarning(self, warning, expected_message): self.assertIs(warning.category, ldap.LDAPBytesWarning) - self.assertIn(expected_message, text_type(warning.message)) + self.assertIn(expected_message, str(warning.message)) def _normalize(filename): # Python 2 likes to report the ".pyc" file in warnings, @@ -567,44 +407,6 @@ def _normalize(filename): linecache.getline(warning.filename, warning.lineno) ) - def _test_byteswarning_level_search(self, methodname): - with self.catch_byteswarnings(explicit=False) as (conn, w): - method = getattr(conn, methodname) - result = method( - self.server.suffix.encode('utf-8'), - ldap.SCOPE_SUBTREE, - '(cn=Foo*)', - attrlist=['*'], # CORRECT LINE - ) - self.assertEqual(len(result), 4) - - self.assertEqual(len(w), 2, w) - - self._check_byteswarning( - w[0], u"Received non-bytes value for 'filterstr'") - - self._check_byteswarning( - w[1], u"Received non-bytes value for 'attrlist'") - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_byteswarning_level_search(self): - self._test_byteswarning_level_search('search_s') - self._test_byteswarning_level_search('search_st') - self._test_byteswarning_level_search('search_ext_s') - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_byteswarning_initialize(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - bytes_uri = self.server.ldap_uri.decode('utf-8') - self.ldap_object_class(bytes_uri) # CORRECT LINE - - self.assertEqual(len(w), 1, w) - - self._check_byteswarning( - w[0], u"Under Python 2, python-ldap uses bytes by default.") - @requires_tls() def test_multiple_starttls(self): # Test for openldap does not re-register nss shutdown callbacks @@ -622,33 +424,22 @@ def test_multiple_starttls(self): def test_dse(self): dse = self._ldap_conn.read_rootdse_s() self.assertIsInstance(dse, dict) - self.assertEqual(dse[u'supportedLDAPVersion'], [b'3']) + self.assertEqual(dse['supportedLDAPVersion'], [b'3']) keys = set(dse) # SASL info may be missing in restricted build environments - keys.discard(u'supportedSASLMechanisms') + keys.discard('supportedSASLMechanisms') self.assertEqual( keys, - {u'configContext', u'entryDN', u'namingContexts', u'objectClass', - u'structuralObjectClass', u'subschemaSubentry', - u'supportedControl', u'supportedExtension', u'supportedFeatures', - u'supportedLDAPVersion'} + {'configContext', 'entryDN', 'namingContexts', 'objectClass', + 'structuralObjectClass', 'subschemaSubentry', + 'supportedControl', 'supportedExtension', 'supportedFeatures', + 'supportedLDAPVersion'} ) self.assertEqual( self._ldap_conn.get_naming_contexts(), [self.server.suffix.encode('utf-8')] ) - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_dse_bytes(self): - l = self._get_bytes_ldapobject() - dse = l.read_rootdse_s() - self.assertIsInstance(dse, dict) - self.assertEqual(dse[u'supportedLDAPVersion'], [b'3']) - self.assertEqual( - l.get_naming_contexts(), - [self.server.suffix.encode('utf-8')] - ) - def test_compare_s_true(self): base = self.server.suffix l = self._ldap_conn @@ -661,6 +452,85 @@ def test_compare_s_false(self): result = l.compare_s('cn=Foo1,%s' % base, 'cn', b'Foo2') self.assertIs(result, False) + def test_compare_s_notfound(self): + base = self.server.suffix + l = self._ldap_conn + with self.assertRaises(ldap.NO_SUCH_OBJECT): + result = l.compare_s('cn=invalid,%s' % base, 'cn', b'Foo2') + + def test_compare_s_invalidattr(self): + base = self.server.suffix + l = self._ldap_conn + with self.assertRaises(ldap.UNDEFINED_TYPE): + result = l.compare_s('cn=Foo1,%s' % base, 'invalidattr', b'invalid') + + def test_compare_true_exception_contains_message_id(self): + base = self.server.suffix + l = self._ldap_conn + msgid = l.compare('cn=Foo1,%s' % base, 'cn', b'Foo1') + with self.assertRaises(ldap.COMPARE_TRUE) as cm: + l.result() + self.assertEqual(cm.exception.args[0]["msgid"], msgid) + + def test_async_search_no_such_object_exception_contains_message_id(self): + msgid = self._ldap_conn.search("CN=XXX", ldap.SCOPE_SUBTREE) + with self.assertRaises(ldap.NO_SUCH_OBJECT) as cm: + self._ldap_conn.result() + self.assertEqual(cm.exception.args[0]["msgid"], msgid) + + def test_passwd_s(self): + l = self._ldap_conn + + # first, create a user to change password on + dn = "cn=PasswordTest," + self.server.suffix + result, pmsg, msgid, ctrls = l.add_ext_s( + dn, + [ + ('objectClass', b'person'), + ('sn', b'PasswordTest'), + ('cn', b'PasswordTest'), + ('userPassword', b'initial'), + ] + ) + self.assertEqual(result, ldap.RES_ADD) + self.assertIsInstance(msgid, int) + self.assertEqual(pmsg, []) + self.assertEqual(ctrls, []) + + # try changing password with a wrong old-pw + with self.assertRaises(ldap.UNWILLING_TO_PERFORM): + l.passwd_s(dn, "bogus", "ignored") + + # have the server generate a new random pw + respoid, respvalue = l.passwd_s(dn, "initial", None, extract_newpw=True) + self.assertEqual(respoid, None) + + password = respvalue.genPasswd + self.assertIsInstance(password, bytes) + + # try changing password back + respoid, respvalue = l.passwd_s(dn, password, "initial") + self.assertEqual(respoid, None) + self.assertEqual(respvalue, None) + + l.delete_s(dn) + + def test_slapadd(self): + with self.assertRaises(ldap.INVALID_DN_SYNTAX): + self._ldap_conn.add_s("myAttribute=foobar,ou=Container,%s" % self.server.suffix, [ + ("objectClass", b'myClass'), + ("myAttribute", b'foobar'), + ]) + + self.server.slapadd(SCHEMA_TEMPLATE, ["-n0"]) + self.server.restart() + self.reset_connection() + + self._ldap_conn.add_s("myAttribute=foobar,ou=Container,%s" % self.server.suffix, [ + ("objectClass", b'myClass'), + ("myAttribute", b'foobar'), + ]) + class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject): """ @@ -695,22 +565,20 @@ def test103_reconnect_get_state(self): self.assertEqual( l1.__getstate__(), { - str('_last_bind'): ( + '_last_bind': ( 'simple_bind_s', (bind_dn, 'user1_pw'), {} ), - str('_options'): [(17, 3)], - str('_reconnects_done'): 0, - str('_retry_delay'): 60.0, - str('_retry_max'): 1, - str('_start_tls'): 0, - str('_trace_level'): ldap._trace_level, - str('_trace_stack_limit'): 5, - str('_uri'): self.server.ldap_uri, - str('bytes_mode'): l1.bytes_mode, - str('bytes_strictness'): l1.bytes_strictness, - str('timeout'): -1, + '_options': [(17, 3)], + '_reconnects_done': 0, + '_retry_delay': 60.0, + '_retry_max': 1, + '_start_tls': 0, + '_trace_level': ldap._trace_level, + '_trace_stack_limit': 5, + '_uri': self.server.ldap_uri, + 'timeout': -1, }, ) @@ -724,6 +592,46 @@ def test104_reconnect_restore(self): 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._proc.terminate() + self.server.wait() + try: + l1.whoami_s() + except ldap.SERVER_DOWN: + pass + else: + self.assertEqual(True, False) + finally: + self.server._start_slapd() + self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn) + + +@requires_init_fd() +class Test03_SimpleLDAPObjectWithFileno(Test00_SimpleLDAPObject): + def _open_ldap_conn(self, who=None, cred=None, **kwargs): + if hasattr(self, '_sock'): + raise RuntimeError("socket already connected") + self._sock = socket.create_connection( + (self.server.hostname, self.server.port) + ) + return super()._open_ldap_conn( + who=who, cred=cred, fileno=self._sock.fileno(), **kwargs + ) + + def tearDown(self): + self._sock.close() + del self._sock + super().tearDown() + + def reset_connection(self): + self._sock.close() + del self._sock + super(Test03_SimpleLDAPObjectWithFileno, self).reset_connection() + if __name__ == '__main__': unittest.main() diff --git a/Tests/t_ldapurl.py b/Tests/t_ldapurl.py index 1408efcf..f9c72098 100644 --- a/Tests/t_ldapurl.py +++ b/Tests/t_ldapurl.py @@ -1,20 +1,15 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap's module ldapurl See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - import os import unittest +from urllib.parse import quote # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' -from ldap.compat import quote - import ldapurl from ldapurl import LDAPUrl @@ -38,18 +33,23 @@ class TestIsLDAPUrl(unittest.TestCase): 'ldap://host.com:6666/o=University%20of%20Michigan,':1, 'ldap://ldap.itd.umich.edu/c=GB?objectClass?one':1, 'ldap://ldap.question.com/o=Question%3f,c=US?mail':1, - 'ldap://ldap.netscape.com/o=Babsco,c=US??(int=%5c00%5c00%5c00%5c04)':1, + 'ldap://ldap.netscape.com/o=Babsco,c=US???(int=%5c00%5c00%5c00%5c04)':1, 'ldap:///??sub??bindname=cn=Manager%2co=Foo':1, 'ldap:///??sub??!bindname=cn=Manager%2co=Foo':1, # More examples from various sources 'ldap://ldap.nameflow.net:1389/c%3dDE':1, 'ldap://root.openldap.org/dc=openldap,dc=org':1, - 'ldap://root.openldap.org/dc=openldap,dc=org':1, + 'ldaps://root.openldap.org/dc=openldap,dc=org':1, 'ldap://x500.mh.se/o=Mitthogskolan,c=se????1.2.752.58.10.2=T.61':1, 'ldp://root.openldap.org/dc=openldap,dc=org':0, 'ldap://localhost:1389/ou%3DUnstructured%20testing%20tree%2Cdc%3Dstroeder%2Cdc%3Dcom??one':1, 'ldaps://ldap.example.com/c%3dDE':1, 'ldapi:///dc=stroeder,dc=de????x-saslmech=EXTERNAL':1, + 'LDAP://localhost': True, + 'LDAPS://localhost': True, + 'LDAPI://%2Frun%2Fldap.sock': True, + ' ldap://space.example': False, + 'ldap ://space.example': False, } def test_isLDAPUrl(self): @@ -61,6 +61,11 @@ def test_isLDAPUrl(self): ldap_url, result, expected, ) ) + if expected: + LDAPUrl(ldapUrl=ldap_url) + else: + with self.assertRaises(ValueError): + LDAPUrl(ldapUrl=ldap_url) class TestParseLDAPUrl(unittest.TestCase): @@ -149,6 +154,22 @@ class TestParseLDAPUrl(unittest.TestCase): dn='dc=stroeder,dc=com', ), ), + ( + 'LDAPS://localhost:12345/dc=stroeder,dc=com', + LDAPUrl( + urlscheme='ldaps', + hostport='localhost:12345', + dn='dc=stroeder,dc=com', + ), + ), + ( + 'ldaps://localhost:12345/dc=stroeder,dc=com', + LDAPUrl( + urlscheme='LDAPS', + hostport='localhost:12345', + dn='dc=stroeder,dc=com', + ), + ), ( 'ldapi://%2ftmp%2fopenldap2-1389/dc=stroeder,dc=com', LDAPUrl( @@ -164,7 +185,7 @@ def test_ldapurl(self): ldap_url_obj = LDAPUrl(ldapUrl=ldap_url_str) self.assertEqual( ldap_url_obj, test_ldap_url_obj, - 'Attributes of LDAPUrl(%s) are:\n%s\ninstead of:\n%s' % ( + 'Attributes of LDAPUrl({}) are:\n{}\ninstead of:\n{}'.format( repr(ldap_url_str), repr(ldap_url_obj), repr(test_ldap_url_obj), @@ -174,7 +195,7 @@ def test_ldapurl(self): unparsed_ldap_url_obj = LDAPUrl(ldapUrl=unparsed_ldap_url_str) self.assertEqual( unparsed_ldap_url_obj, test_ldap_url_obj, - 'Attributes of LDAPUrl(%s) are:\n%s\ninstead of:\n%s' % ( + 'Attributes of LDAPUrl({}) are:\n{}\ninstead of:\n{}'.format( repr(unparsed_ldap_url_str), repr(unparsed_ldap_url_obj), repr(test_ldap_url_obj), diff --git a/Tests/t_ldif.py b/Tests/t_ldif.py index 048b3f40..2f9ed679 100644 --- a/Tests/t_ldif.py +++ b/Tests/t_ldif.py @@ -1,12 +1,8 @@ -# -*- coding: utf-8 -*- """ Automatic tests for python-ldap's module ldif See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - import os import textwrap import unittest diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..dda8dbc1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[tool.black] +line-length = 88 +target-version = ['py36', 'py37', 'py38'] + +[tool.isort] +line_length=88 +known_first_party=['ldap', '_ldap', 'ldapurl', 'ldif', 'slapdtest'] +sections=['FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'] diff --git a/setup.cfg b/setup.cfg index c6546068..01d43a06 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 diff --git a/setup.py b/setup.py index e66ecbd6..119b5715 100644 --- a/setup.py +++ b/setup.py @@ -7,15 +7,12 @@ import sys,os from setuptools import setup, Extension -if sys.version_info[0] == 2 and sys.version_info[1] < 7: - raise RuntimeError('This software requires Python 2.7 or 3.x.') -if sys.version_info[0] >= 3 and sys.version_info < (3, 4): - raise RuntimeError('The C API from Python 3.4+ is required.') +if sys.version_info < (3, 6): + raise RuntimeError( + 'The C API from Python 3.6+ is required, found %s' % sys.version_info + ) -if sys.version_info[0] >= 3: - from configparser import ConfigParser -else: - from ConfigParser import ConfigParser +from configparser import ConfigParser sys.path.insert(0, os.path.join(os.getcwd(), 'Lib/ldap')) import pkginfo @@ -88,14 +85,12 @@ class OpenLDAP2: 'Programming Language :: C', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', - # Note: when updating Python versions, also change .travis.yml and tox.ini + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + # Note: when updating Python versions, also change tox.ini and .github/workflows/* 'Topic :: Database', 'Topic :: Internet', @@ -168,6 +163,6 @@ class OpenLDAP2: 'pyasn1_modules >= 0.1.5', ], zip_safe=False, - python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', + python_requires='>=3.6', test_suite = 'Tests', ) diff --git a/tox.ini b/tox.ini index 1434ba01..aaef8b5a 100644 --- a/tox.ini +++ b/tox.ini @@ -4,57 +4,45 @@ # and then run "tox" from this directory. [tox] -# Note: when updating Python versions, also change setup.py and .travis.yml -envlist = py27,py34,py35,py36,py37,py38,{py2,py3}-nosasltls,doc,py3-trace,coverage-report +# Note: when updating Python versions, also change setup.py and .github/worlflows/* +envlist = py{36,37,38,39,310},c90-py{36,37},py3-nosasltls,doc,py3-trace,pypy3 minver = 1.8 +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38, doc, py3-nosasltls + 3.9: py39, py3-trace + pypy3: pypy3 + [testenv] -deps = coverage +deps = passenv = WITH_GCOV # - Enable BytesWarning # - Turn all warnings into exceptions. -# - 'ignore:the imp module is deprecated' is required to ignore import of 'imp' -# in distutils. Python < 3.6 use PendingDeprecationWarning; Python >= 3.6 use -# DeprecationWarning. -commands = {envpython} -bb -Werror \ - "-Wignore:the imp module is deprecated:DeprecationWarning" \ - "-Wignore:the imp module is deprecated:PendingDeprecationWarning" \ - "-Wignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working:DeprecationWarning" \ - -m coverage run --parallel setup.py \ - clean --all \ - test - -[testenv:py27] -# No warnings with Python 2.7 -passenv = {[testenv]passenv} -commands = {envpython} \ - -m coverage run --parallel setup.py test - -[testenv:py34] -# No warnings with Python 3.4 -passenv = {[testenv]passenv} -commands = {envpython} \ - -m coverage run --parallel setup.py test - -[testenv:py2-nosasltls] -basepython = python2 -deps = {[testenv]deps} -passenv = {[testenv]passenv} setenv = - CI_DISABLED=LDAPI:SASL:TLS -# rebuild without SASL and TLS, run without LDAPI -commands = {envpython} \ - -m coverage run --parallel setup.py \ - clean --all \ - build_ext -UHAVE_SASL,HAVE_TLS \ - test + CFLAGS=-Wno-int-in-bool-context -Werror -Werror=declaration-after-statement -std=c99 +commands = {envpython} -bb -Werror \ + -m unittest discover -v -s Tests -p 't_*' {posargs} [testenv:py3-nosasltls] basepython = python3 -deps = {[testenv]deps} +# don't install, install dependencies manually +skip_install = true +deps = + {[testenv]deps} + pyasn1 + pyasn1_modules passenv = {[testenv]passenv} -setenv = {[testenv:py2-nosasltls]setenv} -commands = {[testenv:py2-nosasltls]commands} +setenv = + CI_DISABLED=LDAPI:SASL:TLS +# build and install without SASL and TLS, run without LDAPI +commands = + {envpython} setup.py clean --all + {envpython} setup.py build_ext -UHAVE_SASL,HAVE_TLS + {envpython} setup.py install --single-version-externally-managed --root=/ + {[testenv]commands} [testenv:py3-trace] basepython = python3 @@ -65,24 +53,35 @@ setenv = PYTHON_LDAP_TRACE_FILE={envtmpdir}/trace.log commands = {[testenv]commands} -[testenv:pypy] -# PyPy doesn't have working setup.py test -deps = pytest -commands = {envpython} -m pytest - -[testenv:pypy35] -# PyPy-5.9.0 -basepython = pypy3.5 -deps = {[testenv:pypy]deps} -commands = {[testenv:pypy]commands} +[testenv:c90] +setenv = + CFLAGS=-Wno-int-in-bool-context -Werror -Werror=declaration-after-statement -std=c90 +commands = {envpython} -Werror -c "import ldap" # we just test compilation here -[testenv:coverage-report] -deps = coverage -skip_install = true +[testenv:macos] +# Travis CI macOS image does not have slapd +# SDK libldap does not support ldap_init_fd +basepython = python3 +deps = {[testenv]deps} +passenv = {[testenv]passenv} +setenv = + CI_DISABLED=INIT_FD commands = - {envpython} -m coverage combine - {envpython} -m coverage report --show-missing - {envpython} -m coverage html + {envpython} -m unittest -v {posargs} \ + Tests/t_cidict.py \ + Tests/t_ldap_dn.py \ + Tests/t_ldap_filter.py \ + Tests/t_ldap_functions.py \ + Tests/t_ldap_modlist.py \ + Tests/t_ldap_schema_tokenizer.py \ + Tests/t_ldapurl.py \ + Tests/t_ldif.py \ + Tests/t_untested_mods.py + +[testenv:pypy3] +basepython = pypy3 +deps = pytest +commands = {envpython} -m pytest {posargs} [testenv:doc] basepython = python3