├── .bumpversion.cfg ├── .envrc ├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ └── pythonapp.yml ├── .gitignore ├── .gitreview ├── .mergify.yml ├── .readthedocs.yml ├── .tool-versions ├── AUTHORS.txt ├── CHANGES.rst ├── COPYING.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── __init__.py ├── changes.rst ├── conf.py ├── future.rst ├── getting_started.rst ├── history.rst ├── index.rst ├── ldap.inv ├── modules.rst ├── settings.py ├── tldap.backend.rst ├── tldap.database.rst ├── tldap.django.migrations.rst ├── tldap.django.rst ├── tldap.rst └── tldap.test.rst ├── flake.lock ├── flake.nix ├── poetry.lock ├── pylint.conf ├── pyproject.toml ├── pytest.ini ├── setup.cfg ├── tests ├── __init__.py ├── a_unit │ ├── __init__.py │ ├── conftest.py │ ├── test_backend_fake_transactions.py │ ├── test_database.py │ ├── test_dict.py │ ├── test_dn.py │ ├── test_ldap_passwd.py │ ├── test_modlist.py │ └── test_query.py ├── b_integration │ ├── __init__.py │ ├── accounts.feature │ ├── conftest.py │ ├── groups.feature │ ├── test_accounts.py │ └── test_groups.py ├── database.py └── django │ ├── __init__.py │ ├── database.py │ └── settings.py ├── tldap ├── __init__.py ├── backend │ ├── __init__.py │ ├── base.py │ ├── fake_transactions.py │ └── no_transactions.py ├── database │ ├── __init__.py │ └── helpers.py ├── dict.py ├── django │ ├── __init__.py │ ├── apps.py │ ├── helpers.py │ ├── middleware.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ └── models.py ├── dn.py ├── exceptions.py ├── fields.py ├── filter.py ├── ldap_passwd.py ├── modlist.py ├── query.py ├── query_utils.py ├── test │ ├── __init__.py │ ├── ldap_schemas │ │ ├── 00-core.schema │ │ ├── 10-cosine.schema │ │ ├── 50-inetorgperson.schema │ │ ├── 70-eduperson.schema │ │ ├── 90-aueduperson.schema │ │ ├── 90-schac.schema │ │ └── nis.schema │ └── slapd.py ├── transaction.py ├── tree.py └── utils.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.0.8 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? 7 | serialize = 8 | {major}.{minor}.{patch}-{release} 9 | {major}.{minor}.{patch} 10 | 11 | [bumpversion:file:pyproject.toml] 12 | search = version = "{current_version}" 13 | replace = version = "{new_version}" 14 | 15 | [bumpversion:file:tldap/__init__.py] 16 | search = __version__ = '{current_version}' 17 | replace = __version__ = '{new_version}' 18 | 19 | [bumpversion:file:CHANGES.rst] 20 | search = 21 | UNRELEASED 22 | ---------- 23 | replace = 24 | {new_version} ({now:%Y-%m-%d}) 25 | ------------------ 26 | 27 | [bumpversion:part:release] 28 | optional_value = gamma 29 | first_value = gamma 30 | values = 31 | alpha 32 | beta 33 | gamma 34 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '33 17 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: Python application 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Tests 8 | runs-on: ubuntu-22.04 9 | 10 | steps: 11 | - name: Checkout source code 12 | uses: actions/checkout@v4 13 | - name: Install system dependancies 14 | run: | 15 | sudo apt-get update 16 | sudo apt-get install slapd ldap-utils 17 | - name: Remove apparmor restrictions on slapd 18 | run: | 19 | sudo apt-get install apparmor-utils 20 | sudo aa-complain /usr/sbin/slapd 21 | - name: Set up Python 3.10 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: "3.10" 25 | - name: Install poetry 26 | uses: abatilo/actions-poetry@v4.0.0 27 | with: 28 | poetry-version: 1.3.0 29 | - name: Install python dependancies 30 | run: | 31 | poetry install --extras=docs 32 | - name: Run tests 33 | run: | 34 | export TZ=Australia/Melbourne 35 | poetry run make -C docs html 36 | poetry run isort --check --diff tldap 37 | poetry run flake8 tldap 38 | poetry run python -m tldap.test.slapd python -m pytest --cov=tldap --junitxml=test-reports/junit.xml 39 | 40 | publish-pypi-prod: 41 | name: Publish Pypi Prod 42 | runs-on: ubuntu-latest 43 | needs: [test] 44 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 45 | 46 | steps: 47 | - name: Check out the repo 48 | uses: actions/checkout@v4 49 | - name: Set up Python 3.10 50 | uses: actions/setup-python@v5 51 | with: 52 | python-version: "3.10" 53 | - name: Install poetry 54 | uses: abatilo/actions-poetry@v4.0.0 55 | with: 56 | poetry-version: 1.3.0 57 | - name: Install python dependancies 58 | run: | 59 | poetry install 60 | - name: Verify git tag vs. version 61 | run: | 62 | VERSION=${GITHUB_REF#refs/tags/} 63 | test "$(poetry version)" = "python-tldap ${VERSION}" 64 | - name: Create packages 65 | run: | 66 | poetry build 67 | - name: Publish distribution 📦 to PyPI 68 | uses: pypa/gh-action-pypi-publish@master 69 | with: 70 | password: ${{ secrets.PYPI_PASSWORD }} 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/_build/ 2 | /.cache/ 3 | /.eggs/ 4 | /.tox/ 5 | /.idea/ 6 | /.venv/ 7 | /python_tldap.egg-info/ 8 | /build/ 9 | /dist/ 10 | *.bak 11 | *.pyc 12 | /.direnv 13 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=code.vpac.org 3 | port=29418 4 | project=python-tldap 5 | defaultbranch=master 6 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | conditions: 4 | - "check-success=Tests" 5 | 6 | pull_request_rules: 7 | - name: Automatic merge on approval 8 | conditions: 9 | - "#approved-reviews-by>=1" 10 | - "check-success=Tests" 11 | actions: 12 | queue: 13 | name: default 14 | method: rebase 15 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.10" 10 | 11 | python: 12 | install: 13 | - method: pip 14 | path: . 15 | extra_requirements: 16 | - docs 17 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | python 3.11.0 2 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Brian May 2 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Change log 3 | ========== 4 | All notable changes to this project will be documented in this file. The format 5 | is based on `Keep a Changelog`_ and this project 6 | adheres to `Semantic Versioning`_. 7 | 8 | .. _`Keep a Changelog`: http://keepachangelog.com/ 9 | .. _`Semantic Versioning`: http://semver.org/ 10 | 11 | 1.0.8 (2023-06-28) 12 | ------------------ 13 | 14 | Changed 15 | ~~~~~~~ 16 | * Allow overriding SSL CIPHERS. 17 | 18 | 19 | 1.0.7 (2023-06-09) 20 | ------------------ 21 | 22 | Changed 23 | ~~~~~~~ 24 | * Bump dependancies. 25 | 26 | 27 | 1.0.6 (2021-04-08) 28 | ------------------ 29 | 30 | Changed 31 | ~~~~~~~ 32 | * Bump dependancies. 33 | * Switch from pipenv to poetry. 34 | 35 | 36 | 1.0.5 (2020-07-31) 37 | ------------------ 38 | 39 | Changed 40 | ~~~~~~~ 41 | * Bump dependancies. 42 | 43 | 44 | 1.0.4 - 2020-07-27 45 | ------------------ 46 | 47 | Changed 48 | ~~~~~~~ 49 | * Bump dependancies. 50 | 51 | 52 | 1.0.3 - 2019-03-06 53 | ------------------ 54 | 55 | Changed 56 | ~~~~~~~ 57 | * Use circleci for builds. 58 | 59 | 60 | 1.0.2 - 2019-02-19 61 | ------------------ 62 | 63 | Fixed 64 | ~~~~~ 65 | * Ensure get_and_increment is run in transaction. 66 | * Give Django app sensible short name. 67 | * Pass database parameter as required in load() method. 68 | 69 | 70 | 1.0.1 - 2018-12-03 71 | ------------------ 72 | 73 | Fixed 74 | ~~~~~ 75 | * Add missing tldap.django package. 76 | 77 | 78 | 1.0.0 - 2018-12-03 79 | ------------------ 80 | 81 | Changed 82 | ~~~~~~~ 83 | * Complete rewrite/simplification of API. 84 | * Not compatible with previous versions. 85 | 86 | 87 | 0.4.4 - 2018-03-02 88 | ------------------ 89 | 90 | Changed 91 | ~~~~~~~ 92 | * Django middleware now inherits from django.utils.deprecation.MiddlewareMixin 93 | * Update pytest requirement. 94 | 95 | 96 | 0.4.3 - 2018-02-13 97 | ------------------ 98 | Forgot to merge master before releasing 0.4.2; retry. 99 | 100 | 101 | 0.4.2 - 2018-02-13 102 | ------------------ 103 | 104 | Changed 105 | ~~~~~~~ 106 | * Updated requirements. 107 | * Changed filter string to byte string. 108 | 109 | Removed 110 | ~~~~~~~ 111 | * Python 3.5 support. 112 | 113 | 114 | 0.4.1 - 2017-05-01 115 | ------------------ 116 | 117 | Fixed 118 | ~~~~~ 119 | * Remove unused dependancy on pytest-mock. 120 | * Added upload information to setup.cfg 121 | 122 | 123 | 0.4.0 - 2017-05-01 124 | ------------------ 125 | Increment minor version as we changed the default password hash to a new one 126 | that isn't supported by earlier versions of TLDAP. 127 | 128 | Added 129 | ~~~~~ 130 | * Supports ldap3 2.2.3 131 | 132 | Changed 133 | ~~~~~~~ 134 | * Rewrote test cases. Now smaller in scope for what each test covers. Needs 135 | more work for queries. 136 | 137 | Fixed 138 | ~~~~~ 139 | * Fixed bug setting primary group if primary group already set. 140 | * Allow clearing/setting primary group if current value invalid. 141 | * Fix incorrect DN calculated in cached data after move. 142 | 143 | Security 144 | ~~~~~~~~ 145 | * Use sha512_crypt by default for passwords instead of ldap_salted_sha1. We 146 | still support salted ldap_salted_sha1 for existing passwords. 147 | 148 | 149 | 0.3.20 - 2017-04-21 150 | ------------------- 151 | 152 | Deprecated 153 | ~~~~~~~~~~ 154 | * Remove setuptools_scm/readthedocs hack. 155 | 156 | Fixed 157 | ~~~~~ 158 | * Remove registeredAddresss attribute which is undefined in OpenLDAP. 159 | 160 | 161 | 0.3.19 - 2017-04-21 162 | ------------------- 163 | Changes to work with latest software. Note that ldap3 >= 2 still has 164 | problems that are being worked on. Also we get warnings that the 165 | `encode` method in passlib has been replaced by the `hash` method. 166 | 167 | Added 168 | ~~~~~ 169 | * Python 3.6 support. 170 | * No longer depends on Django. Django support is optional. 171 | 172 | Deprecated 173 | ~~~~~~~~~~ 174 | * Python 3.3 support. 175 | 176 | Fixed 177 | ~~~~~ 178 | * Include ``version.py`` on PyPi source. 179 | * Use ``requirements.txt`` to declare knowed good versions of 180 | software we depend on. 181 | * Update ``90-ppolicy.schema`` to work with latest slapd. 182 | * Various updates to fix problems with ldap3 >= 2. 183 | * Fix PEP8 errors. 184 | * Fix `verbose_name` undefined error. 185 | * Fix name of project in documentation. 186 | 187 | 188 | 0.3.18 - 2016-05-03 189 | ------------------- 190 | * Update my email address. 191 | * Remove dependancy on Django. 192 | * Add tox tests. 193 | * Use setuptools-scm for versiong. 194 | * Fix documentation. 195 | * Add changelog to documentation. 196 | 197 | 198 | 0.3.17 - 2016-04-26 199 | ------------------- 200 | * Unbreak tests by using Node directly from Django. 201 | 202 | 203 | 0.3.16 - 2016-04-26 204 | ------------------- 205 | * Ensure we install test schemas. 206 | 207 | 208 | 0.3.15 - 2016-01-10 209 | ------------------- 210 | * Bugs fixed. 211 | * Split Debian packaging. 212 | 213 | 214 | 0.3.14 - 2015-11-10 215 | ------------------- 216 | * Don't include docs directory in package. Closes: #804643. 217 | 218 | 219 | 0.3.13 - 2015-10-26 220 | ------------------- 221 | * Ensure tests run for Python3.4 and Python3.5. 222 | 223 | 224 | 0.3.13 - 2015-10-18 225 | ------------------- 226 | * Fix FTBFS issues. Closes: #801943 227 | 228 | 229 | 0.3.12 - 2015-08-24 230 | ------------------- 231 | * Fix FTBFS issues. #796756. 232 | * Update git repository location. 233 | 234 | 235 | 0.3.11 - 2015-06-11 236 | ------------------- 237 | * Fix ds389 account locking/unlocking. 238 | * Define new LOCKED_ROLE setting for ds389. 239 | 240 | 241 | 0.3.10 - 2015-02-20 242 | ------------------- 243 | * Fix TLS configuration. Will break existing setups if validation fails. 244 | * python3-ldap renamed to ldap3 upstream. 245 | 246 | 247 | 0.3.9 - 2015-02-19 248 | ------------------ 249 | * Various bug fixes. 250 | 251 | 252 | 0.3.8 - 2014-11-18 253 | ------------------ 254 | * Works with python3-ldap 0.9.6.2. 255 | * Don't use depreciated django.utils.importlib. 256 | * Update standards version to 3.9.6. 257 | 258 | 259 | 0.3.7 - 2014-09-09 260 | ------------------ 261 | * Add more read only attributes. 262 | * Add Django 1.7 migration. 263 | 264 | 265 | 0.3.6 - 2014-09-08 266 | ------------------ 267 | * Rename migrations to south_migrations. 268 | * Add groupOfNames objectClass. 269 | * hasSubordinates is read only attribute. 270 | 271 | 272 | 0.3.5 - 2014-08-07 273 | ------------------- 274 | * Update override_dh_auto_test. 275 | * Really fix debian/copyright file. 276 | 277 | 278 | 0.3.4 - 2014-07-15 279 | ------------------ 280 | * Don't die if default LDAP server not configured. 281 | 282 | 283 | 0.3.3 - 2014-07-14 284 | ------------------ 285 | * Fix typo. 286 | * Remove hard dependency on Django. 287 | * Rename source project. 288 | * Move ldap_passwd from tldap.methods. 289 | * Fix Debian copyright. 290 | * Retry upload to Debian. Closes: #753482. 291 | 292 | 293 | 0.3.2 - 2014-07-09 294 | ------------------- 295 | * Fix PEP8 issues. 296 | * FIx close() undefined error, python-ldap3 0.9.4.2 297 | * Trick pep8 into ignoring E721. 298 | * Revert "Copy escape_bytes function from ldap3." 299 | 300 | 301 | 0.3.1 - 2014-07-06 302 | ------------------ 303 | * Add link to homepage. 304 | * Remove unneeded file. 305 | * New release for Debian. 306 | * Add Vcs headers. 307 | * Declare Python 3 compatible. 308 | * Fix __unicode__ string methods for Python 3. 309 | * Don't connect to LDAP until we need to. 310 | * Python 3 tests. 311 | * PEP8 fixes. 312 | * Run flake8 tests during build. 313 | 314 | 315 | 0.3.0 - 2014-07-01 316 | ------------------ 317 | * Python3 support. 318 | * Python3 package. 319 | 320 | 321 | 0.2.17 - 2014-03-28 322 | ------------------- 323 | * Replace USE_TLS setting with REQUIRE_TLS and START_TLS settings. 324 | Old USE_TLS setting will no longer work. 325 | 326 | 327 | 0.2.16 - 2014-03-24 328 | ------------------- 329 | * New release. 330 | * Fix PEP8 style issues. 331 | * Replace ldap_passwd with passlib code. 332 | * Testing: check LDAP port not already in use. 333 | 334 | 335 | 0.2.15 - 2014-03-11 336 | ------------------- 337 | * Move tests to tldap.tests. 338 | * Update Python packaging. 339 | * Update documentation. 340 | 341 | 342 | 0.2.14 - 2014-02-17 343 | ------------------- 344 | * Support moving objects in LDAP tree. 345 | * Fix replaces/breaks header for upgrades from legacy package. 346 | 347 | 348 | 0.2.13 - 2014-02-05 349 | ------------------- 350 | * Initial documentation. 351 | * Make transactions operate on all connections by default. 352 | * Remove obsolete functions. 353 | 354 | 0.2.12 - 2014-01-28 355 | ------------------- 356 | * Use dh_python2 for packaging. 357 | 358 | 359 | 0.2.11 - 2014-01-21 360 | ------------------- 361 | * Fix bug in samba specific function. 362 | * Works with no LDAP servers configured. 363 | 364 | 365 | 0.2.10 - 2013-12-17 366 | ------------------- 367 | * Bug fixes. 368 | 369 | 370 | 0.2.9 - 2013-08-14 371 | ------------------ 372 | * Update referenced backend names. 373 | * Rewrite method functions. 374 | * Fix creating gid and uid for different servers. 375 | * Updates to 389 support. 376 | 377 | 378 | 0.2.8 - 2013-07-26 379 | ------------------ 380 | * Rename backends. 381 | tldap.backend.transaction to tldap.backend.fake_transactions 382 | tldap.backend.python to tldap.backend.no_transactions 383 | * Remove prefixes from LDAP names. 384 | 385 | 386 | 0.2.7 - 2013-07-18 387 | ------------------ 388 | * New methods submodule, moved from placard schema. 389 | * Add depends on python-ldap. 390 | * Fix LDAP bind if connection failed. 391 | * Fix md5-crypt password comparison. 392 | * Write LDAP entries to ldif_writer. 393 | 394 | 395 | 0.2.6 - 2013-05-27 396 | ------------------ 397 | * Tests: Purge environment when calling slapd. 398 | * Update description to reflect what tldap does. 399 | 400 | 401 | 0.2.5 - 2013-05-01 402 | ------------------ 403 | * Support new method of creating schemas. 404 | 405 | 406 | 0.2.4 - 2013-03-22 407 | ------------------ 408 | * Add classes that were deleted in error. 409 | 410 | 411 | 0.2.3 - 2013-03-15 412 | ------------------ 413 | * Fix copy of CaseInsensitiveDict. 414 | * PEP8 formatting fixed. 415 | 416 | 417 | 0.2.2 - 2013-02-19 418 | ------------------ 419 | * Fix bug in processing commit flag. 420 | 421 | 422 | 0.2.1 - 2013-02-18 423 | ------------------ 424 | * Fix tests. 425 | 426 | 427 | 0.2 - 2013-02-08 428 | ---------------- 429 | * Lots and lots and lots of updates. 430 | 431 | 432 | 0.1 - 2012-04-03 433 | ---------------- 434 | * Initial release. 435 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | include *.py 4 | include *.ini 5 | include *.sh 6 | include .gitreview 7 | include MANIFEST.in 8 | include pylint.conf 9 | recursive-include tldap *.py *.schema 10 | recursive-include docs Makefile *.py *.rst *.inv 11 | recursive-exclude docs/_build * 12 | include Pipfile* 13 | recursive-include tests *.feature 14 | recursive-include tests *.py 15 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | python-tldap 2 | ============ 3 | TLDAP is a high level LDAP library for Python that uses Ecto like models 4 | to define LDAP schemas that can then be used in an easy way from Python code. 5 | It also supports fake LDAP transactions, to try and ensure LDAP database 6 | remains in a consistent state, even if there are errors that cause the 7 | transaction to fail. 8 | 9 | Documentation can be found at http://python-tldap.readthedocs.org/ 10 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Karaage.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Karaage.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Karaage" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Karaage" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # FILE COPIED FROM conf.orig.py; DO NOT CHANGE 2 | # -*- coding: utf-8 -*- 3 | # 4 | # python-tldap documentation build configuration file, created by 5 | # sphinx-quickstart on Mon Feb 3 09:23:20 2014. 6 | # 7 | # This file is execfile()d with the current directory set to its containing 8 | # dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import six 17 | import sys 18 | import os 19 | import django 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | os.environ['DJANGO_SETTINGS_MODULE'] = 'docs.settings' 25 | sys.path.insert(0, os.path.abspath('..')) 26 | django.setup() 27 | 28 | import tldap # NOQA 29 | 30 | # -- General configuration ---------------------------------------------------- 31 | 32 | # If your documentation needs a minimal Sphinx version, state it here. 33 | # needs_sphinx = '1.0' 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 37 | extensions = [ 38 | 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 39 | 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # The suffix of source filenames. 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | # source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = six.u('python-tldap') 55 | copyright = six.u('2014, Brian May') 56 | 57 | # The version info for the project you're documenting, acts as replacement for 58 | # |version| and |release|, also used in various other places throughout the 59 | # built documents. 60 | # 61 | # The full version, including alpha/beta/rc tags. 62 | release = tldap.__version__ 63 | # The short X.Y version. 64 | version = '.'.join(release.split('.')[:2]) 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # language = None 69 | 70 | # There are two options for replacing |today|: either, you set today to some 71 | # non-false value, then it is used: 72 | # today = '' 73 | # Else, today_fmt is used as the format for a strftime call. 74 | # today_fmt = '%B %d, %Y' 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | exclude_patterns = ['_build'] 79 | 80 | # The reST default role (used for this markup: `text`) to use for all 81 | # documents. 82 | # default_role = None 83 | 84 | # If true, '()' will be appended to :func: etc. cross-reference text. 85 | # add_function_parentheses = True 86 | 87 | # If true, the current module name will be prepended to all description 88 | # unit titles (such as .. function::). 89 | # add_module_names = True 90 | 91 | # If true, sectionauthor and moduleauthor directives will be shown in the 92 | # output. They are ignored by default. 93 | # show_authors = False 94 | 95 | # The name of the Pygments (syntax highlighting) style to use. 96 | pygments_style = 'sphinx' 97 | 98 | # A list of ignored prefixes for module index sorting. 99 | # modindex_common_prefix = [] 100 | 101 | 102 | # -- Options for HTML output -------------------------------------------------- 103 | 104 | # The theme to use for HTML and HTML Help pages. See the documentation for 105 | # a list of builtin themes. 106 | html_theme = 'furo' 107 | 108 | # Theme options are theme-specific and customize the look and feel of a theme 109 | # further. For a list of options available for each theme, see the 110 | # documentation. 111 | # html_theme_options = {} 112 | 113 | # Add any paths that contain custom themes here, relative to this directory. 114 | # html_theme_path = [] 115 | 116 | # The name for this set of Sphinx documents. If None, it defaults to 117 | # " v documentation". 118 | # html_title = None 119 | 120 | # A shorter title for the navigation bar. Default is the same as html_title. 121 | # html_short_title = None 122 | 123 | # The name of an image file (relative to this directory) to place at the top 124 | # of the sidebar. 125 | # html_logo = None 126 | 127 | # The name of an image file (within the static path) to use as favicon of the 128 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 129 | # pixels large. 130 | # html_favicon = None 131 | 132 | # Add any paths that contain custom static files (such as style sheets) here, 133 | # relative to this directory. They are copied after the builtin static files, 134 | # so a file named "default.css" will overwrite the builtin "default.css". 135 | html_static_path = ['_static'] 136 | 137 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 138 | # using the given strftime format. 139 | # html_last_updated_fmt = '%b %d, %Y' 140 | 141 | # If true, SmartyPants will be used to convert quotes and dashes to 142 | # typographically correct entities. 143 | # html_use_smartypants = True 144 | 145 | # Custom sidebar templates, maps document names to template names. 146 | # html_sidebars = {} 147 | 148 | # Additional templates that should be rendered to pages, maps page names to 149 | # template names. 150 | # html_additional_pages = {} 151 | 152 | # If false, no module index is generated. 153 | # html_domain_indices = True 154 | 155 | # If false, no index is generated. 156 | # html_use_index = True 157 | 158 | # If true, the index is split into individual pages for each letter. 159 | # html_split_index = False 160 | 161 | # If true, links to the reST sources are added to the pages. 162 | # html_show_sourcelink = True 163 | 164 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 165 | # html_show_sphinx = True 166 | 167 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 168 | # html_show_copyright = True 169 | 170 | # If true, an OpenSearch description file will be output, and all pages will 171 | # contain a tag referring to it. The value of this option must be the 172 | # base URL from which the finished HTML is served. 173 | # html_use_opensearch = '' 174 | 175 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 176 | # html_file_suffix = None 177 | 178 | # Output file base name for HTML help builder. 179 | htmlhelp_basename = 'python-tldapdoc' 180 | 181 | 182 | # -- Options for LaTeX output ------------------------------------------------- 183 | 184 | latex_elements = { 185 | # The paper size ('letterpaper' or 'a4paper'). 186 | # 'papersize': 'letterpaper', 187 | 188 | # The font size ('10pt', '11pt' or '12pt'). 189 | # 'pointsize': '10pt', 190 | 191 | # Additional stuff for the LaTeX preamble. 192 | # 'preamble': '', 193 | } 194 | 195 | # Grouping the document tree into LaTeX files. List of tuples 196 | # (source start file, target name, title, author, documentclass 197 | # [howto/manual]). 198 | latex_documents = [ 199 | ('index', 'python-tldap.tex', six.u('python-tldap Documentation'), 200 | six.u('Brian May'), 'manual'), 201 | ] 202 | 203 | # The name of an image file (relative to this directory) to place at the top of 204 | # the title page. 205 | # latex_logo = None 206 | 207 | # For "manual" documents, if this is true, then toplevel headings are parts, 208 | # not chapters. 209 | # latex_use_parts = False 210 | 211 | # If true, show page references after internal links. 212 | # latex_show_pagerefs = False 213 | 214 | # If true, show URL addresses after external links. 215 | # latex_show_urls = False 216 | 217 | # Documents to append as an appendix to all manuals. 218 | # latex_appendices = [] 219 | 220 | # If false, no module index is generated. 221 | # latex_domain_indices = True 222 | 223 | 224 | # -- Options for manual page output ------------------------------------------- 225 | 226 | # One entry per manual page. List of tuples 227 | # (source start file, name, description, authors, manual section). 228 | man_pages = [ 229 | ('index', 'python-tldap', six.u('python-tldap Documentation'), 230 | [six.u('Brian May')], 1) 231 | ] 232 | 233 | # If true, show URL addresses after external links. 234 | # man_show_urls = False 235 | 236 | 237 | # -- Options for Texinfo output ----------------------------------------------- 238 | 239 | # Grouping the document tree into Texinfo files. List of tuples 240 | # (source start file, target name, title, author, 241 | # dir menu entry, description, category) 242 | texinfo_documents = [ 243 | ('index', 'python-tldap', six.u('python-tldap Documentation'), 244 | six.u('Brian May'), 'python-tldap', 'One line description of project.', 245 | 'Miscellaneous'), 246 | ] 247 | 248 | # Documents to append as an appendix to all manuals. 249 | # texinfo_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | # texinfo_domain_indices = True 253 | 254 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 255 | # texinfo_show_urls = 'footnote' 256 | 257 | 258 | # -- Options for Epub output -------------------------------------------------- 259 | 260 | # Bibliographic Dublin Core info. 261 | epub_title = six.u('python-tldap') 262 | epub_author = six.u('Brian May') 263 | epub_publisher = six.u('Brian May') 264 | epub_copyright = six.u('2014, Brian May') 265 | 266 | # The language of the text. It defaults to the language option 267 | # or en if the language is not set. 268 | # epub_language = '' 269 | 270 | # The scheme of the identifier. Typical schemes are ISBN or URL. 271 | # epub_scheme = '' 272 | 273 | # The unique identifier of the text. This can be a ISBN number 274 | # or the project homepage. 275 | # epub_identifier = '' 276 | 277 | # A unique identification for the text. 278 | # epub_uid = '' 279 | 280 | # A tuple containing the cover image and cover page html template filenames. 281 | # epub_cover = () 282 | 283 | # HTML files that should be inserted before the pages created by sphinx. 284 | # The format is a list of tuples containing the path and title. 285 | # epub_pre_files = [] 286 | 287 | # HTML files shat should be inserted after the pages created by sphinx. 288 | # The format is a list of tuples containing the path and title. 289 | # epub_post_files = [] 290 | 291 | # A list of files that should not be packed into the epub file. 292 | # epub_exclude_files = [] 293 | 294 | # The depth of the table of contents in toc.ncx. 295 | # epub_tocdepth = 3 296 | 297 | # Allow duplicate toc entries. 298 | # epub_tocdup = True 299 | 300 | 301 | # Example configuration for intersphinx: refer to the Python standard library. 302 | intersphinx_mapping = { 303 | 'ldap': ('http://www.python-ldap.org/doc/html/', 'ldap.inv')} 304 | -------------------------------------------------------------------------------- /docs/future.rst: -------------------------------------------------------------------------------- 1 | Future work 2 | =========== 3 | 4 | * Some servers, e.g. Active Directory, support transactions properly in the 5 | server. Need to support these transactions natively. 6 | 7 | * The Active Directory primary group field type has never been implemented 8 | correctly for the entire set of operations. This is due to the complicated 9 | nature of some of the side effects when dealing with this field. 10 | 11 | * Initial tests seem to indicate that the account locking/unlocking code for 12 | Directory Server 389 does not lock the account. Need to fix this. 13 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | As tldap is a library for use for applications, this documentation 4 | is aimed at Django developers, who are already reasonable competent 5 | at programming with Django. 6 | 7 | Basic Usage 8 | ----------- 9 | #. (Django only) Add the following to the django settings file: 10 | 11 | .. code-block:: python 12 | 13 | LDAP = { 14 | 'default': { 15 | 'ENGINE': 'tldap.backend.fake_transactions', 16 | 'URI': 'ldap://localhost', 17 | 'USER': 'cn=admin,dc=example,dc=org', 18 | 'PASSWORD': 'XXXXXXXX', 19 | 'REQUIRE_TLS': False, 20 | 'START_TLS': False, 21 | 'TLS_CA' : None, 22 | } 23 | } 24 | 25 | INSTALLED_APPS += ( 26 | 'tldap.django' 27 | ) 28 | 29 | The database model in Django allows automatically generating uidNumber and gidNumber values, and also automatically 30 | configures the backends. 31 | 32 | #. (No Django) Initialize tldap with: 33 | 34 | .. code-block:: python 35 | 36 | import tldap.backends 37 | 38 | settings = { 39 | 'default': { 40 | 'ENGINE': 'tldap.backend.fake_transactions', 41 | 'URI': 'ldap://localhost', 42 | 'USER': 'cn=admin,dc=example,dc=org', 43 | 'PASSWORD': 'XXXXXXXX', 44 | 'REQUIRE_TLS': False, 45 | 'START_TLS': False, 46 | 'TLS_CA' : None, 47 | } 48 | } 49 | 50 | tldap.backends.setup(settings) 51 | 52 | #. Create an application specific layer with LDAP schema information. 53 | See ``tests/database.py`` and ``tests/django/database.py`` for examples. 54 | 55 | #. Create an object: 56 | 57 | .. code-block:: python 58 | 59 | account = Account({ 60 | 'uid': "tux", 61 | 'givenName': "Tux", 62 | 'sn': "Torvalds", 63 | 'cn': "Tux Torvalds", 64 | 'telephoneNumber': "000", 65 | 'mail': "tuz@example.org", 66 | 'o': "Linux Rules", 67 | 'userPassword': "silly", 68 | 'homeDirectory': "/home/tux", 69 | 'gidNumber': 10, 70 | 'uidNumber': 10, # Not required if using Django helper method. 71 | }) 72 | 73 | account = tldap.database.insert(account) 74 | 75 | #. Retrieve one object: 76 | 77 | .. code-block:: python 78 | 79 | account = tldap.database.get_one(Account, Q(uid='tux')) 80 | 81 | #. Search for objects. 82 | 83 | .. code-block:: python 84 | 85 | for account in tldap.database.search(Account): 86 | print(account.get_as_single("cn")) 87 | 88 | #. For some real examples on how methods are used, see the `karaage 89 | `_. 90 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | Ideas that seemed like a good idea at the time that have been adandoned. These 5 | are documented here to avoid accidentally reimplementing bad ideas without 6 | consideration at to why they were discarded. 7 | 8 | * ORM model. 9 | 10 | Ended up being overly complicated. This approach was abandoned in 1.0.0. 11 | 12 | * Process actions immediately. 13 | 14 | Previously, would delay processing actions until commit() called. 15 | 16 | This requires caching the attributes, and simulating the actions in 17 | cache. Unfortunately this is very difficult to get right, particular 18 | as certain actions can have different side effects (e.g. alter other 19 | attributes) depending on the server. 20 | 21 | Also we run into the problem that any errors during the commit() 22 | phase may happen too late to abort changes to other databases. e.g. 23 | if the Django middleware is used, any errors generated in the middleware 24 | will not affect the other middleware transaction layers for other 25 | databases, and they will continue to commit all results (regardless 26 | of order of invocation). 27 | 28 | So we process the results immediately. This means rollback is 29 | more likely to be required when something doesn't work, which could 30 | introduce problems if this failes. However these changes, I believe 31 | will result in an overall simpler and more robust design. 32 | 33 | commit: a6180486dc788c6ba81dcbff1e6c9a0bbbc481f3 34 | 35 | * Remove caching in backend. No longer needed now we process actions 36 | immediately. 37 | 38 | commit: 21e357bfc612c1fd1ef1ca599a5dbb39a94fac1e 39 | 40 | * Remove using= parameter from object methods. 41 | 42 | Initially this was copied from Django db models. 43 | 44 | This means if we load object from LDAP server we have to save it to that 45 | same LDAP server. 46 | 47 | In a practical sense, this was the case anyway. 48 | 49 | Trying to make things too generic just over complicates everything. 50 | 51 | This also means that the dn for an object will not change, unless rename 52 | is called. To try and do otherwise is just so very very confusing. 53 | 54 | As a result, a lot of complicated code that was potentially broken has now 55 | been simplified. 56 | 57 | commit: 9f79c500bc971ad7fdd3dd6e4eb45d1b187d5f03 58 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. python-tldap documentation master file, created by 2 | sphinx-quickstart on Mon Feb 3 09:23:20 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | python-tldap's documentation 7 | ============================ 8 | 9 | :Date: |today| 10 | :Version: |version| 11 | 12 | Contents: 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | getting_started 18 | history 19 | future 20 | 21 | Appendices: 22 | 23 | .. toctree:: 24 | :maxdepth: 1 25 | 26 | changes 27 | modules 28 | 29 | Indices and tables 30 | ================== 31 | 32 | * :ref:`genindex` 33 | * :ref:`modindex` 34 | * :ref:`search` 35 | 36 | -------------------------------------------------------------------------------- /docs/ldap.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Karaage-Cluster/python-tldap/6cb877b28e07c0fd78a449e4c1b140fcab0117ef/docs/ldap.inv -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | tldap 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | tldap 8 | -------------------------------------------------------------------------------- /docs/settings.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | DEBUG = True 19 | SECRET_KEY = '5hvhpe6gv2t5x4$3dtq(w2v#vg@)sx4p3r_@wv%l41g!stslc*' 20 | 21 | INSTALLED_APPS = [ 22 | 'tldap.django', 23 | ] 24 | 25 | LDAP = { 26 | 'default': { 27 | 'ENGINE': 'tldap.backend.fake_transactions', 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/tldap.backend.rst: -------------------------------------------------------------------------------- 1 | tldap.backend package 2 | ===================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | tldap.backend.base module 8 | ------------------------- 9 | 10 | .. automodule:: tldap.backend.base 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | tldap.backend.fake\_transactions module 16 | --------------------------------------- 17 | 18 | .. automodule:: tldap.backend.fake_transactions 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | tldap.backend.no\_transactions module 24 | ------------------------------------- 25 | 26 | .. automodule:: tldap.backend.no_transactions 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: tldap.backend 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/tldap.database.rst: -------------------------------------------------------------------------------- 1 | tldap.database package 2 | ====================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | tldap.database.helpers module 8 | ----------------------------- 9 | 10 | .. automodule:: tldap.database.helpers 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: tldap.database 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/tldap.django.migrations.rst: -------------------------------------------------------------------------------- 1 | tldap.django.migrations package 2 | =============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | tldap.django.migrations.0001\_initial module 8 | -------------------------------------------- 9 | 10 | .. automodule:: tldap.django.migrations.0001_initial 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: tldap.django.migrations 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/tldap.django.rst: -------------------------------------------------------------------------------- 1 | tldap.django package 2 | ==================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | tldap.django.migrations 10 | 11 | Submodules 12 | ---------- 13 | 14 | tldap.django.apps module 15 | ------------------------ 16 | 17 | .. automodule:: tldap.django.apps 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | tldap.django.helpers module 23 | --------------------------- 24 | 25 | .. automodule:: tldap.django.helpers 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | tldap.django.middleware module 31 | ------------------------------ 32 | 33 | .. automodule:: tldap.django.middleware 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | tldap.django.models module 39 | -------------------------- 40 | 41 | .. automodule:: tldap.django.models 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | 47 | Module contents 48 | --------------- 49 | 50 | .. automodule:: tldap.django 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /docs/tldap.rst: -------------------------------------------------------------------------------- 1 | tldap package 2 | ============= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | tldap.backend 10 | tldap.database 11 | tldap.django 12 | tldap.test 13 | 14 | Submodules 15 | ---------- 16 | 17 | tldap.dict module 18 | ----------------- 19 | 20 | .. automodule:: tldap.dict 21 | :members: 22 | :undoc-members: 23 | :show-inheritance: 24 | 25 | tldap.dn module 26 | --------------- 27 | 28 | .. automodule:: tldap.dn 29 | :members: 30 | :undoc-members: 31 | :show-inheritance: 32 | 33 | tldap.exceptions module 34 | ----------------------- 35 | 36 | .. automodule:: tldap.exceptions 37 | :members: 38 | :undoc-members: 39 | :show-inheritance: 40 | 41 | tldap.fields module 42 | ------------------- 43 | 44 | .. automodule:: tldap.fields 45 | :members: 46 | :undoc-members: 47 | :show-inheritance: 48 | 49 | tldap.filter module 50 | ------------------- 51 | 52 | .. automodule:: tldap.filter 53 | :members: 54 | :undoc-members: 55 | :show-inheritance: 56 | 57 | tldap.ldap\_passwd module 58 | ------------------------- 59 | 60 | .. automodule:: tldap.ldap_passwd 61 | :members: 62 | :undoc-members: 63 | :show-inheritance: 64 | 65 | tldap.modlist module 66 | -------------------- 67 | 68 | .. automodule:: tldap.modlist 69 | :members: 70 | :undoc-members: 71 | :show-inheritance: 72 | 73 | tldap.query module 74 | ------------------ 75 | 76 | .. automodule:: tldap.query 77 | :members: 78 | :undoc-members: 79 | :show-inheritance: 80 | 81 | tldap.query\_utils module 82 | ------------------------- 83 | 84 | .. automodule:: tldap.query_utils 85 | :members: 86 | :undoc-members: 87 | :show-inheritance: 88 | 89 | tldap.transaction module 90 | ------------------------ 91 | 92 | .. automodule:: tldap.transaction 93 | :members: 94 | :undoc-members: 95 | :show-inheritance: 96 | 97 | tldap.tree module 98 | ----------------- 99 | 100 | .. automodule:: tldap.tree 101 | :members: 102 | :undoc-members: 103 | :show-inheritance: 104 | 105 | tldap.utils module 106 | ------------------ 107 | 108 | .. automodule:: tldap.utils 109 | :members: 110 | :undoc-members: 111 | :show-inheritance: 112 | 113 | 114 | Module contents 115 | --------------- 116 | 117 | .. automodule:: tldap 118 | :members: 119 | :undoc-members: 120 | :show-inheritance: 121 | -------------------------------------------------------------------------------- /docs/tldap.test.rst: -------------------------------------------------------------------------------- 1 | tldap.test package 2 | ================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | tldap.test.slapd module 8 | ----------------------- 9 | 10 | .. automodule:: tldap.test.slapd 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: tldap.test 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "flake-utils_2": { 22 | "inputs": { 23 | "systems": "systems_2" 24 | }, 25 | "locked": { 26 | "lastModified": 1710146030, 27 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "numtide", 35 | "repo": "flake-utils", 36 | "type": "github" 37 | } 38 | }, 39 | "nix-github-actions": { 40 | "inputs": { 41 | "nixpkgs": [ 42 | "poetry2nix", 43 | "nixpkgs" 44 | ] 45 | }, 46 | "locked": { 47 | "lastModified": 1703863825, 48 | "narHash": "sha256-rXwqjtwiGKJheXB43ybM8NwWB8rO2dSRrEqes0S7F5Y=", 49 | "owner": "nix-community", 50 | "repo": "nix-github-actions", 51 | "rev": "5163432afc817cf8bd1f031418d1869e4c9d5547", 52 | "type": "github" 53 | }, 54 | "original": { 55 | "owner": "nix-community", 56 | "repo": "nix-github-actions", 57 | "type": "github" 58 | } 59 | }, 60 | "nixpkgs": { 61 | "locked": { 62 | "lastModified": 1720954236, 63 | "narHash": "sha256-1mEKHp4m9brvfQ0rjCca8P1WHpymK3TOr3v34ydv9bs=", 64 | "owner": "NixOS", 65 | "repo": "nixpkgs", 66 | "rev": "53e81e790209e41f0c1efa9ff26ff2fd7ab35e27", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "NixOS", 71 | "ref": "nixos-24.05", 72 | "repo": "nixpkgs", 73 | "type": "github" 74 | } 75 | }, 76 | "poetry2nix": { 77 | "inputs": { 78 | "flake-utils": "flake-utils_2", 79 | "nix-github-actions": "nix-github-actions", 80 | "nixpkgs": [ 81 | "nixpkgs" 82 | ], 83 | "systems": "systems_3", 84 | "treefmt-nix": "treefmt-nix" 85 | }, 86 | "locked": { 87 | "lastModified": 1721010580, 88 | "narHash": "sha256-qxN9it4uicRKdEjKlSt3BvXC+mWgLlJHNwMztSQsQsE=", 89 | "owner": "nix-community", 90 | "repo": "poetry2nix", 91 | "rev": "7f304a86324aea2026e65e508c82af7127f9b00d", 92 | "type": "github" 93 | }, 94 | "original": { 95 | "owner": "nix-community", 96 | "repo": "poetry2nix", 97 | "type": "github" 98 | } 99 | }, 100 | "root": { 101 | "inputs": { 102 | "flake-utils": "flake-utils", 103 | "nixpkgs": "nixpkgs", 104 | "poetry2nix": "poetry2nix" 105 | } 106 | }, 107 | "systems": { 108 | "locked": { 109 | "lastModified": 1681028828, 110 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 111 | "owner": "nix-systems", 112 | "repo": "default", 113 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 114 | "type": "github" 115 | }, 116 | "original": { 117 | "owner": "nix-systems", 118 | "repo": "default", 119 | "type": "github" 120 | } 121 | }, 122 | "systems_2": { 123 | "locked": { 124 | "lastModified": 1681028828, 125 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 126 | "owner": "nix-systems", 127 | "repo": "default", 128 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 129 | "type": "github" 130 | }, 131 | "original": { 132 | "owner": "nix-systems", 133 | "repo": "default", 134 | "type": "github" 135 | } 136 | }, 137 | "systems_3": { 138 | "locked": { 139 | "lastModified": 1681028828, 140 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 141 | "owner": "nix-systems", 142 | "repo": "default", 143 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 144 | "type": "github" 145 | }, 146 | "original": { 147 | "id": "systems", 148 | "type": "indirect" 149 | } 150 | }, 151 | "treefmt-nix": { 152 | "inputs": { 153 | "nixpkgs": [ 154 | "poetry2nix", 155 | "nixpkgs" 156 | ] 157 | }, 158 | "locked": { 159 | "lastModified": 1719749022, 160 | "narHash": "sha256-ddPKHcqaKCIFSFc/cvxS14goUhCOAwsM1PbMr0ZtHMg=", 161 | "owner": "numtide", 162 | "repo": "treefmt-nix", 163 | "rev": "8df5ff62195d4e67e2264df0b7f5e8c9995fd0bd", 164 | "type": "github" 165 | }, 166 | "original": { 167 | "owner": "numtide", 168 | "repo": "treefmt-nix", 169 | "type": "github" 170 | } 171 | } 172 | }, 173 | "root": "root", 174 | "version": 7 175 | } 176 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Python LDAP library"; 3 | 4 | inputs.flake-utils.url = "github:numtide/flake-utils"; 5 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; 6 | inputs.poetry2nix = { 7 | url = "github:nix-community/poetry2nix"; 8 | # url = "github:sciyoshi/poetry2nix/new-bootstrap-fixes"; 9 | inputs.nixpkgs.follows = "nixpkgs"; 10 | }; 11 | 12 | outputs = { self, nixpkgs, flake-utils, poetry2nix }: 13 | flake-utils.lib.eachDefaultSystem (system: 14 | let 15 | p2n = import poetry2nix { inherit pkgs; }; 16 | mkPoetryApplication = p2n.mkPoetryApplication; 17 | pkgs = nixpkgs.legacyPackages.${system}; 18 | slapd = pkgs.writeShellScriptBin "slapd" '' 19 | exec ${pkgs.openldap}/libexec/slapd "$@" 20 | ''; 21 | 22 | in { 23 | packages = { 24 | python-tldap = mkPoetryApplication { 25 | projectDir = self; 26 | overrides = p2n.overrides.withDefaults (final: prev: { 27 | nh3 = prev.nh3.override { preferWheel = true; }; 28 | furo = prev.furo.override { preferWheel = true; }; 29 | bump2version = prev.bump2version.overridePythonAttrs (oldAttrs: { 30 | buildInputs = oldAttrs.buildInputs ++ [ final.setuptools ]; 31 | }); 32 | }); 33 | }; 34 | default = self.packages.${system}.python-tldap; 35 | }; 36 | 37 | devShells.default = pkgs.mkShell { 38 | packages = [ pkgs.poetry pkgs.libffi slapd pkgs.openldap ]; 39 | }; 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /pylint.conf: -------------------------------------------------------------------------------- 1 | # lint Python modules using external checkers. 2 | # 3 | # This is the main checker controlling the other ones and the reports 4 | # generation. It is itself both a raw checker and an astng checker in order 5 | # to: 6 | # * handle message activation / deactivation at the module level 7 | # * handle some basic but necessary stats'data (number of classes, methods...) 8 | # 9 | [MASTER] 10 | 11 | # Specify a configuration file. 12 | #rcfile= 13 | 14 | # Python code to execute, usually for sys.path manipulation such as 15 | # pygtk.require(). 16 | #init-hook= 17 | 18 | # Profiled execution. 19 | profile=no 20 | 21 | # Add to the black list. It should be a base name, not a 22 | # path. You may set this option multiple times. 23 | ignore=global_settings.py,test_settings.py,development_settings.py,production_settings.py,settings.py,private_settings.py,tests.py,slapd.py,junitxmlrunner.py 24 | 25 | # Pickle collected data for later comparisons. 26 | persistent=yes 27 | 28 | # Set the cache size for astng objects. 29 | cache-size=500 30 | 31 | # List of plugins (as comma separated values of python modules names) to load, 32 | # usually to register additional checkers. 33 | load-plugins= 34 | 35 | 36 | [MESSAGES CONTROL] 37 | 38 | # Enable only checker(s) with the given id(s). This option conflicts with the 39 | # disable-checker option 40 | #enable-checker= 41 | 42 | # Enable all checker(s) except those with the given id(s). This option 43 | # conflicts with the enable-checker option 44 | #disable-checker= 45 | 46 | # Enable all messages in the listed categories (IRCWEF). 47 | #enable-msg-cat= 48 | 49 | # Disable all messages in the listed categories (IRCWEF). 50 | disable-msg-cat=I 51 | 52 | # Enable the message(s) with the given id(s). 53 | #enable-msg= 54 | 55 | # Disable the message(s) with the given id(s). 56 | disable-msg=C0301 57 | 58 | 59 | [REPORTS] 60 | 61 | # Set the output format. Available formats are text, parseable, colorized, msvs 62 | # (visual studio) and html 63 | output-format=parseable 64 | 65 | # Include message's id in output 66 | include-ids=yes 67 | 68 | # Put messages in a separate file for each module / package specified on the 69 | # command line instead of printing them on stdout. Reports (if any) will be 70 | # written in a file name "pylint_global.[txt|html]". 71 | files-output=no 72 | 73 | # Tells whether to display a full report or only the messages 74 | reports=yes 75 | 76 | # Python expression which should return a note less than 10 (10 is the highest 77 | # note). You have access to the variables errors warning, statement which 78 | # respectively contain the number of errors / warnings messages and the total 79 | # number of statements analyzed. This is used by the global evaluation report 80 | # (R0004). 81 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 82 | 83 | # Add a comment according to your evaluation note. This is used by the global 84 | # evaluation report (R0004). 85 | comment=no 86 | 87 | # Enable the report(s) with the given id(s). 88 | #enable-report= 89 | 90 | # Disable the report(s) with the given id(s). 91 | #disable-report= 92 | 93 | 94 | # checks for 95 | # * unused variables / imports 96 | # * undefined variables 97 | # * redefinition of variable from builtins or from an outer scope 98 | # * use of variable before assignment 99 | # 100 | [VARIABLES] 101 | 102 | # Tells whether we should check for unused import in __init__ files. 103 | init-import=no 104 | 105 | # A regular expression matching names used for dummy variables (i.e. not used). 106 | dummy-variables-rgx=_|dummy 107 | 108 | # List of additional names supposed to be defined in builtins. Remember that 109 | # you should avoid to define new builtins when possible. 110 | additional-builtins= 111 | 112 | 113 | # checks for : 114 | # * doc strings 115 | # * modules / classes / functions / methods / arguments / variables name 116 | # * number of arguments, local variables, branches, returns and statements in 117 | # functions, methods 118 | # * required module attributes 119 | # * dangerous default values as arguments 120 | # * redefinition of function / method / class 121 | # * uses of the global statement 122 | # 123 | [BASIC] 124 | 125 | # Required attributes for module, separated by a comma 126 | required-attributes= 127 | 128 | # Regular expression which should only match functions or classes name which do 129 | # not require a docstring 130 | no-docstring-rgx=__.*__ 131 | 132 | # Regular expression which should only match correct module names 133 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 134 | 135 | # Regular expression which should only match correct module level names 136 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 137 | 138 | # Regular expression which should only match correct class names 139 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 140 | 141 | # Regular expression which should only match correct function names 142 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 143 | 144 | # Regular expression which should only match correct method names 145 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 146 | 147 | # Regular expression which should only match correct instance attribute names 148 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 149 | 150 | # Regular expression which should only match correct argument names 151 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 152 | 153 | # Regular expression which should only match correct variable names 154 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 155 | 156 | # Regular expression which should only match correct list comprehension / 157 | # generator expression variable names 158 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 159 | 160 | # Good variable names which should always be accepted, separated by a comma 161 | good-names=i,j,k,ex,Run,_ 162 | 163 | # Bad variable names which should always be refused, separated by a comma 164 | bad-names=foo,bar,baz,toto,tutu,tata 165 | 166 | # List of builtins function names that should not be used, separated by a comma 167 | bad-functions=map,filter,apply,input 168 | 169 | 170 | # try to find bugs in the code using type inference 171 | # 172 | [TYPECHECK] 173 | 174 | # Tells whether missing members accessed in mixin class should be ignored. A 175 | # mixin class is detected if its name ends with "mixin" (case insensitive). 176 | ignore-mixin-members=yes 177 | 178 | # List of classes names for which member attributes should not be checked 179 | # (useful for classes with attributes dynamically set). 180 | ignored-classes=LDAPGroup,LDAPUser 181 | 182 | # When zope mode is activated, add a predefined set of Zope acquired attributes 183 | # to generated-members. 184 | zope=no 185 | 186 | # List of members which are set dynamically and missed by pylint inference 187 | # system, and so shouldn't trigger E0201 when accessed. 188 | generated-members=objects 189 | 190 | 191 | # checks for 192 | # * external modules dependencies 193 | # * relative / wildcard imports 194 | # * cyclic imports 195 | # * uses of deprecated modules 196 | # 197 | [IMPORTS] 198 | 199 | # Deprecated modules which should not be used, separated by a comma 200 | deprecated-modules=regsub,string,TERMIOS,Bastion,rexec 201 | 202 | # Create a graph of every (i.e. internal and external) dependencies in the 203 | # given file (report R0402 must not be disabled) 204 | import-graph= 205 | 206 | # Create a graph of external dependencies in the given file (report R0402 must 207 | # not be disabled) 208 | ext-import-graph= 209 | 210 | # Create a graph of internal dependencies in the given file (report R0402 must 211 | # not be disabled) 212 | int-import-graph= 213 | 214 | 215 | # checks for : 216 | # * methods without self as first argument 217 | # * overridden methods signature 218 | # * access only to existent members via self 219 | # * attributes not defined in the __init__ method 220 | # * supported interfaces implementation 221 | # * unreachable code 222 | # 223 | [CLASSES] 224 | 225 | # List of interface methods to ignore, separated by a comma. This is used for 226 | # instance to not check methods defines in Zope's Interface base class. 227 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 228 | 229 | # List of method names used to declare (i.e. assign) instance attributes. 230 | defining-attr-methods=__init__,__new__,setUp 231 | 232 | 233 | # checks for sign of poor/misdesign: 234 | # * number of methods, attributes, local variables... 235 | # * size, complexity of functions, methods 236 | # 237 | [DESIGN] 238 | 239 | # Maximum number of arguments for function / method 240 | max-args=5 241 | 242 | # Maximum number of locals for function / method body 243 | max-locals=15 244 | 245 | # Maximum number of return / yield for function / method body 246 | max-returns=6 247 | 248 | # Maximum number of branch for function / method body 249 | max-branchs=12 250 | 251 | # Maximum number of statements in function / method body 252 | max-statements=50 253 | 254 | # Maximum number of parents for a class (see R0901). 255 | max-parents=7 256 | 257 | # Maximum number of attributes for a class (see R0902). 258 | max-attributes=7 259 | 260 | # Minimum number of public methods for a class (see R0903). 261 | min-public-methods=2 262 | 263 | # Maximum number of public methods for a class (see R0904). 264 | max-public-methods=20 265 | 266 | 267 | # checks for: 268 | # * warning notes in the code like FIXME, XXX 269 | # * PEP 263: source code with non ascii character but no encoding declaration 270 | # 271 | [MISCELLANEOUS] 272 | 273 | # List of note tags to take in consideration, separated by a comma. 274 | notes=FIXME,XXX,TODO 275 | 276 | 277 | # checks for : 278 | # * unauthorized constructions 279 | # * strict indentation 280 | # * line length 281 | # * use of <> instead of != 282 | # 283 | [FORMAT] 284 | 285 | # Maximum number of characters on a single line. 286 | max-line-length=80 287 | 288 | # Maximum number of lines in a module 289 | max-module-lines=1000 290 | 291 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 292 | # tab). 293 | indent-string=' ' 294 | 295 | 296 | # checks for similarities and duplicated code. This computation may be 297 | # memory / CPU intensive, so you should disable it if you experiments some 298 | # problems. 299 | # 300 | [SIMILARITIES] 301 | 302 | # Minimum lines number of a similarity. 303 | min-similarity-lines=4 304 | 305 | # Ignore comments when computing similarities. 306 | ignore-comments=yes 307 | 308 | # Ignore docstrings when computing similarities. 309 | ignore-docstrings=yes 310 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "python-tldap" 3 | version = "1.0.8" 4 | description = "High level python LDAP Library" 5 | authors = ["Brian May "] 6 | license = "GPL3+" 7 | packages = [ 8 | { include = "tldap" }, 9 | ] 10 | readme = "README.rst" 11 | homepage = "https://github.com/Karaage-Cluster/python-tldap/" 12 | classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 16 | "Operating System :: OS Independent", 17 | "Programming Language :: Python :: 3.6", 18 | "Programming Language :: Python :: 3.7", 19 | "Programming Language :: Python :: 3.8", 20 | "Topic :: Software Development :: Libraries :: Python Modules", 21 | ] 22 | 23 | [tool.poetry.dependencies] 24 | python = "^3.10" 25 | passlib = "*" 26 | pyasn1 = "*" 27 | ldap3 = "*" 28 | six = "*" 29 | django = {version = "*", optional = true} 30 | sphinx = {version = "*", optional = true} 31 | furo = {version = "*", optional = true} 32 | cryptography = "45.0.3" 33 | 34 | [tool.poetry.dev-dependencies] 35 | pytest = "*" 36 | mock = "*" 37 | pytest-runner = "*" 38 | pytest-bdd = "*" 39 | pytest-django = "*" 40 | django = "*" 41 | tox = "*" 42 | flake8 = "*" 43 | twine = "*" 44 | pipenv-to-requirements = "*" 45 | pytest-cov = "*" 46 | isort = "*" 47 | wheel = "*" 48 | bump2version = "*" 49 | 50 | [tool.poetry.extras] 51 | docs = ["sphinx", "django", "furo"] 52 | 53 | [build-system] 54 | requires = ["poetry-core>=1.0.0"] 55 | build-backend = "poetry.core.masonry.api" 56 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = tests.django.settings 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test = pytest 3 | 4 | [upload] 5 | sign = true 6 | identity = 0x1784577F811F6EAC 7 | 8 | [flake8] 9 | max-line-length = 120 10 | exclude = migrations,.tox 11 | 12 | [isort] 13 | multi_line_output = 3 14 | include_trailing_comma = true 15 | use_parentheses = true 16 | lines_after_imports = 2 17 | skip_glob = */migrations/*.py 18 | 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | -------------------------------------------------------------------------------- /tests/a_unit/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | -------------------------------------------------------------------------------- /tests/a_unit/conftest.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import pytest 3 | 4 | import tldap.backend 5 | import tests.database 6 | 7 | 8 | @pytest.fixture 9 | def mock_ldap(): 10 | ldap = { 11 | 'default': { 12 | 'ENGINE': 'tldap.backend.fake_transactions', 13 | 'URI': 'ldap://localhost:38911/', 14 | 'USER': 'cn=Manager,dc=python-ldap,dc=org', 15 | 'PASSWORD': 'password', 16 | 'USE_TLS': False, 17 | 'TLS_CA': None, 18 | 'LDAP_ACCOUNT_BASE': 'ou=People, dc=python-ldap,dc=org', 19 | 'LDAP_GROUP_BASE': 'ou=Group, dc=python-ldap,dc=org' 20 | } 21 | } 22 | tldap.backend.setup(ldap) 23 | connection = mock.Mock() 24 | connection.settings_dict = ldap['default'] 25 | setattr(tldap.backend.connections._connections, 'default', connection) 26 | return connection 27 | -------------------------------------------------------------------------------- /tests/a_unit/test_dict.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tldap.dict import CaseInsensitiveDict, ImmutableDict 4 | 5 | 6 | @pytest.fixture 7 | def ci(): 8 | """ Get group 1. """ 9 | allowed_values = {'NumberOfPenguins', 'NumberOfSharks'} 10 | return CaseInsensitiveDict(allowed_values) 11 | 12 | 13 | @pytest.fixture 14 | def immutable(): 15 | """ Get group 1. """ 16 | allowed_values = {'NumberOfPenguins', 'NumberOfSharks'} 17 | return ImmutableDict(allowed_values) 18 | 19 | 20 | class TestCaseInsensitive: 21 | def test_init_lowercase(self): 22 | allowed_values = {'NumberOfPenguins', 'NumberOfSharks'} 23 | ci = CaseInsensitiveDict(allowed_values, {'numberofpenguins': 10}) 24 | assert ci.keys() == {'NumberOfPenguins'} 25 | 26 | def test_init_mixedcase(self, ci): 27 | allowed_values = {'NumberOfPenguins', 'NumberOfSharks'} 28 | ci = CaseInsensitiveDict(allowed_values, {'numberOFpenguins': 10}) 29 | assert ci.keys() == {'NumberOfPenguins'} 30 | 31 | def test_init_uppercase(self, ci): 32 | allowed_values = {'NumberOfPenguins', 'NumberOfSharks'} 33 | ci = CaseInsensitiveDict(allowed_values, {'NUMBEROFPENGUINS': 10}) 34 | assert ci.keys() == {'NumberOfPenguins'} 35 | 36 | def test_init_not_valid(self, ci): 37 | allowed_values = {'NumberOfPenguins', 'NumberOfSharks'} 38 | with pytest.raises(KeyError): 39 | CaseInsensitiveDict(allowed_values, {'numberOFfish': 10}) 40 | 41 | def test_set_lowercase(self, ci): 42 | ci['numberofpenguins'] = 10 43 | assert ci.keys() == {'NumberOfPenguins'} 44 | 45 | def test_set_mixedcase(self, ci): 46 | ci['numberOFpenguins'] = 10 47 | assert ci.keys() == {'NumberOfPenguins'} 48 | 49 | def test_set_uppercase(self, ci): 50 | ci['NUMBEROFPENGUINS'] = 10 51 | assert ci.keys() == {'NumberOfPenguins'} 52 | 53 | def test_set_not_valid(self, ci): 54 | with pytest.raises(KeyError): 55 | ci['numberOFfish'] = 10 56 | 57 | def test_get(self, ci): 58 | ci['numberOFpenguins'] = 10 59 | assert ci['numberofpenguins'] == 10 60 | assert ci['NumberOfPenguins'] == 10 61 | assert ci['NUMBEROFPENGUINS'] == 10 62 | 63 | def test_get_not_set(self, ci): 64 | ci['numberOFpenguins'] = 10 65 | 66 | with pytest.raises(KeyError): 67 | assert ci['NumberOfSharks'] == 10 68 | 69 | def test_get_valid(self, ci): 70 | ci['numberOFpenguins'] = 10 71 | 72 | with pytest.raises(KeyError): 73 | assert ci['nUmberoFfIsh'] == 10 74 | 75 | 76 | class TestImmutable: 77 | def test_init_lowercase(self): 78 | allowed_values = {'NumberOfPenguins', 'NumberOfSharks'} 79 | ci = ImmutableDict(allowed_values, {'numberofpenguins': 10}) 80 | assert ci.keys() == {'NumberOfPenguins'} 81 | 82 | def test_init_mixedcase(self, ci): 83 | allowed_values = {'NumberOfPenguins', 'NumberOfSharks'} 84 | ci = ImmutableDict(allowed_values, {'numberOFpenguins': 10}) 85 | assert ci.keys() == {'NumberOfPenguins'} 86 | 87 | def test_init_uppercase(self, ci): 88 | allowed_values = {'NumberOfPenguins', 'NumberOfSharks'} 89 | ci = ImmutableDict(allowed_values, {'NUMBEROFPENGUINS': 10}) 90 | assert ci.keys() == {'NumberOfPenguins'} 91 | 92 | def test_init_not_valid(self, ci): 93 | allowed_values = {'NumberOfPenguins', 'NumberOfSharks'} 94 | with pytest.raises(KeyError): 95 | ImmutableDict(allowed_values, {'numberOFfish': 10}) 96 | 97 | def test_set_fails(self, immutable): 98 | with pytest.raises(TypeError): 99 | immutable['numberofpenguins'] = 10 100 | with pytest.raises(TypeError): 101 | immutable['numberoffish'] = 10 102 | 103 | def test_set_lowercase(self, immutable): 104 | immutable = immutable.set('numberofpenguins', 10) 105 | assert immutable.keys() == {'NumberOfPenguins'} 106 | 107 | def test_set_mixedcase(self, immutable): 108 | immutable = immutable.set('numberOFpenguins', 10) 109 | assert immutable.keys() == {'NumberOfPenguins'} 110 | 111 | def test_set_uppercase(self, immutable): 112 | immutable = immutable.set('NUMBEROFPENGUINS', 10) 113 | assert immutable.keys() == {'NumberOfPenguins'} 114 | 115 | def test_set_not_valid(self, immutable): 116 | with pytest.raises(KeyError): 117 | immutable.set('numberOFfish', 10) 118 | 119 | def test_get(self, immutable): 120 | immutable = immutable.set('numberOFpenguins', 10) 121 | assert immutable['numberofpenguins'] == 10 122 | assert immutable['NumberOfPenguins'] == 10 123 | assert immutable['NUMBEROFPENGUINS'] == 10 124 | 125 | def test_get_not_set(self, immutable): 126 | immutable = immutable.set('numberOFpenguins', 10) 127 | 128 | with pytest.raises(KeyError): 129 | assert immutable['NumberOfSharks'] == 10 130 | 131 | def test_get_valid(self, immutable): 132 | immutable = immutable.set('numberOFpenguins', 10) 133 | 134 | with pytest.raises(KeyError): 135 | assert immutable['nUmberoFfIsh'] == 10 136 | 137 | -------------------------------------------------------------------------------- /tests/a_unit/test_dn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | # Copyright 2012-2014 Brian May 5 | # 6 | # This file is part of python-tldap. 7 | # 8 | # python-tldap is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # python-tldap is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with python-tldap If not, see . 20 | 21 | import six 22 | import unittest 23 | import tldap.dn 24 | 25 | 26 | class DNTest(unittest.TestCase): 27 | 28 | def test_rfc4512_char(self): 29 | self.assertTrue(tldap.dn._isALPHA('A')) 30 | self.assertFalse(tldap.dn._isALPHA('0')) 31 | 32 | def test_rfc4512_number(self): 33 | value = "0" 34 | (result, i) = tldap.dn._number(value, 0) 35 | self.assertIsNotNone(result) 36 | self.assertEqual(result, value) 37 | self.assertEqual(i, len(value)) 38 | 39 | value = "10" 40 | (result, i) = tldap.dn._number(value, 0) 41 | self.assertIsNotNone(result) 42 | self.assertEqual(result, value) 43 | self.assertEqual(i, len(value)) 44 | 45 | value = "1a" 46 | (result, i) = tldap.dn._number(value, 0) 47 | self.assertIsNotNone(result) 48 | value = "1" 49 | self.assertEqual(result, value) 50 | self.assertEqual(i, len(value)) 51 | 52 | value = "" 53 | (result, i) = tldap.dn._number(value, 0) 54 | self.assertIsNone(result) 55 | self.assertEqual(i, 0) 56 | 57 | def test_rfc4512_keystring(self): 58 | value = "A0b-d" 59 | (result, i) = tldap.dn._keystring(value, 0) 60 | self.assertIsNotNone(result) 61 | self.assertEqual(result, value) 62 | self.assertEqual(i, len(value)) 63 | 64 | value = "A0b-d=" 65 | (result, i) = tldap.dn._keystring(value, 0) 66 | self.assertIsNotNone(result) 67 | value = value[:-1] 68 | self.assertEqual(result, value) 69 | self.assertEqual(i, len(value)) 70 | 71 | value = "A" 72 | (result, i) = tldap.dn._keystring(value, 0) 73 | self.assertIsNotNone(result) 74 | self.assertEqual(result, value) 75 | self.assertEqual(i, len(value)) 76 | 77 | value = "O" 78 | (result, i) = tldap.dn._keystring(value, 0) 79 | self.assertIsNotNone(result) 80 | self.assertEqual(result, value) 81 | self.assertEqual(i, len(value)) 82 | 83 | value = "O=" 84 | (result, i) = tldap.dn._keystring(value, 0) 85 | self.assertIsNotNone(result) 86 | value = "O" 87 | self.assertEqual(result, value) 88 | self.assertEqual(i, len(value)) 89 | 90 | value = "0b-d" 91 | (result, i) = tldap.dn._keystring(value, 0) 92 | self.assertIsNone(result) 93 | self.assertEqual(i, 0) 94 | 95 | def test_rfc4514_attributeType(self): 96 | value = "A0b-d" 97 | (result, i) = tldap.dn._attributeType(value, 0) 98 | self.assertIsNotNone(result) 99 | self.assertEqual(result, value) 100 | self.assertEqual(i, len(value)) 101 | 102 | value = "A0b-d=" 103 | (result, i) = tldap.dn._attributeType(value, 0) 104 | self.assertIsNotNone(result) 105 | value = value[:-1] 106 | self.assertEqual(result, value) 107 | self.assertEqual(i, len(value)) 108 | 109 | value = "O" 110 | (result, i) = tldap.dn._attributeType(value, 0) 111 | self.assertIsNotNone(result) 112 | self.assertEqual(result, value) 113 | self.assertEqual(i, len(value)) 114 | 115 | value = "O=" 116 | (result, i) = tldap.dn._attributeType(value, 0) 117 | self.assertIsNotNone(result) 118 | value = "O" 119 | self.assertEqual(result, value) 120 | self.assertEqual(i, len(value)) 121 | 122 | value = "0b-d" 123 | (result, i) = tldap.dn._attributeType(value, 0) 124 | self.assertIsNotNone(result) 125 | value = "0" 126 | self.assertEqual(result, value) 127 | self.assertEqual(i, len(value)) 128 | 129 | value = "1.3.6.1.4.1.1466.0" 130 | (result, i) = tldap.dn._attributeType(value, 0) 131 | self.assertIsNotNone(result) 132 | self.assertEqual(result, value) 133 | self.assertEqual(i, len(value)) 134 | 135 | def test_rfc4514_string(self): 136 | value = "AD" 137 | (result, i) = tldap.dn._string(value, 0) 138 | self.assertIsNotNone(result) 139 | self.assertEqual(result, value) 140 | self.assertEqual(i, len(value)) 141 | 142 | value = "ABCD" 143 | (result, i) = tldap.dn._string(value, 0) 144 | self.assertIsNotNone(result) 145 | self.assertEqual(result, value) 146 | self.assertEqual(i, len(value)) 147 | 148 | value = "AD," 149 | (result, i) = tldap.dn._string(value, 0) 150 | self.assertIsNotNone(result) 151 | value = value[:-1] 152 | self.assertEqual(result, value) 153 | self.assertEqual(i, len(value)) 154 | 155 | value = "ABCD," 156 | (result, i) = tldap.dn._string(value, 0) 157 | self.assertIsNotNone(result) 158 | value = value[:-1] 159 | self.assertEqual(result, value) 160 | self.assertEqual(i, len(value)) 161 | 162 | value = "\\\\a\\ \\#\\=\\+\\,\\;\\<\\>\\41" 163 | (result, i) = tldap.dn._string(value, 0) 164 | self.assertIsNotNone(result) 165 | self.assertEqual(result, "\\a #=+,;<>A") 166 | self.assertEqual(i, len(value)) 167 | 168 | def test_rfc4514_attributeValue(self): 169 | value = "AD" 170 | (result, i) = tldap.dn._attributeValue(value, 0) 171 | self.assertIsNotNone(result) 172 | self.assertEqual(result, value) 173 | self.assertEqual(i, len(value)) 174 | 175 | value = "ABCD" 176 | (result, i) = tldap.dn._attributeValue(value, 0) 177 | self.assertIsNotNone(result) 178 | self.assertEqual(result, value) 179 | self.assertEqual(i, len(value)) 180 | 181 | value = "AD," 182 | (result, i) = tldap.dn._attributeValue(value, 0) 183 | self.assertIsNotNone(result) 184 | value = value[:-1] 185 | self.assertEqual(result, value) 186 | self.assertEqual(i, len(value)) 187 | 188 | value = "ABCD," 189 | (result, i) = tldap.dn._attributeValue(value, 0) 190 | self.assertIsNotNone(result) 191 | value = value[:-1] 192 | self.assertEqual(result, value) 193 | self.assertEqual(i, len(value)) 194 | 195 | value = "\\\\a\\ \\#\\=\\+\\,\\;\\<\\>\\41" 196 | (result, i) = tldap.dn._attributeValue(value, 0) 197 | self.assertIsNotNone(result) 198 | self.assertEqual(result, "\\a #=+,;<>A") 199 | self.assertEqual(i, len(value)) 200 | 201 | value = "#414243" 202 | (result, i) = tldap.dn._attributeValue(value, 0) 203 | self.assertIsNotNone(result) 204 | self.assertEqual(result, "ABC") 205 | self.assertEqual(i, len(value)) 206 | 207 | value = "#" 208 | (result, i) = tldap.dn._attributeValue(value, 0) 209 | self.assertIsNone(result) 210 | self.assertEqual(i, 0) 211 | 212 | def test_rfc4514_attributeTypeAndValue(self): 213 | value = "ABC=DEF" 214 | (result, i) = tldap.dn._attributeTypeAndValue(value, 0) 215 | self.assertIsNotNone(result) 216 | self.assertEqual(result, ("ABC", "DEF", 1)) 217 | self.assertEqual(i, len(value)) 218 | 219 | value = "O=Isode Limited" 220 | (result, i) = tldap.dn._attributeTypeAndValue(value, 0) 221 | self.assertIsNotNone(result) 222 | self.assertEqual(result, ("O", "Isode Limited", 1)) 223 | self.assertEqual(i, len(value)) 224 | 225 | def test_rfc4514_relativeDistinguishedName(self): 226 | value = "ABC=DEF" 227 | (result, i) = tldap.dn._relativeDistinguishedName(value, 0) 228 | self.assertIsNotNone(result) 229 | self.assertEqual(result, [("ABC", "DEF", 1)]) 230 | self.assertEqual(i, len(value)) 231 | 232 | value = "ABC=DEF+HIJ=KIF" 233 | (result, i) = tldap.dn._relativeDistinguishedName(value, 0) 234 | self.assertIsNotNone(result) 235 | self.assertEqual(result, [("ABC", "DEF", 1), ("HIJ", "KIF", 1)]) 236 | self.assertEqual(i, len(value)) 237 | 238 | value = "ABC=DEF,HIJ=KIF" 239 | (result, i) = tldap.dn._relativeDistinguishedName(value, 0) 240 | self.assertIsNotNone(result) 241 | self.assertEqual(result, [("ABC", "DEF", 1)]) 242 | self.assertEqual(i, len("ABC=DEF")) 243 | 244 | def test_rfc4514_distinguishedName(self): 245 | value = "ABC=DEF,HIJ=KIF" 246 | (result, i) = tldap.dn._distinguishedName(value, 0) 247 | self.assertIsNotNone(result) 248 | self.assertEqual(result, [[('ABC', 'DEF', 1)], [('HIJ', 'KIF', 1)]]) 249 | self.assertEqual(i, len(value)) 250 | 251 | def test_str2dn(self): 252 | value = "ABC=DEF,HIJ=KIF\\" 253 | self.assertRaises( 254 | tldap.exceptions.InvalidDN, lambda: tldap.dn.str2dn(value, 0)) 255 | 256 | value = "CN=Steve Kille,O=Isode Limited,C=GB" 257 | result = tldap.dn.str2dn(value) 258 | self.assertIsNotNone(result) 259 | self.assertEqual(result, [ 260 | [('CN', 'Steve Kille', 1)], 261 | [('O', 'Isode Limited', 1)], 262 | [('C', 'GB', 1)], 263 | ]) 264 | result = tldap.dn.dn2str(result) 265 | self.assertEqual(result, value) 266 | 267 | value = "OU=Sales+CN=J. Smith,O=Widget Inc.,C=US" 268 | result = tldap.dn.str2dn(value) 269 | self.assertIsNotNone(result) 270 | self.assertEqual(result, [ 271 | [('OU', 'Sales', 1), ('CN', 'J. Smith', 1)], 272 | [('O', 'Widget Inc.', 1)], 273 | [('C', 'US', 1)], 274 | ]) 275 | result = tldap.dn.dn2str(result) 276 | self.assertEqual(result, value) 277 | 278 | value = "CN=L. Eagle,O=Sue\\, Grabbit and Runn,C=GB" 279 | result = tldap.dn.str2dn(value) 280 | self.assertIsNotNone(result) 281 | self.assertEqual(result, [ 282 | [('CN', 'L. Eagle', 1)], 283 | [('O', 'Sue, Grabbit and Runn', 1)], 284 | [('C', 'GB', 1)], 285 | ]) 286 | result = tldap.dn.dn2str(result) 287 | self.assertEqual(result, value) 288 | 289 | value = "CN=Before\\0DAfter,O=Test,C=GB" 290 | result = tldap.dn.str2dn(value) 291 | self.assertIsNotNone(result) 292 | self.assertEqual(result, [ 293 | [('CN', 'Before\rAfter', 1)], 294 | [('O', 'Test', 1)], 295 | [('C', 'GB', 1)], 296 | ]) 297 | result = tldap.dn.dn2str(result) 298 | self.assertEqual(result, "CN=Before\rAfter,O=Test,C=GB") 299 | 300 | value = "CN=Before\rAfter,O=Test,C=GB" 301 | result = tldap.dn.str2dn(value) 302 | self.assertIsNotNone(result) 303 | self.assertEqual(result, [ 304 | [('CN', 'Before\rAfter', 1)], 305 | [('O', 'Test', 1)], 306 | [('C', 'GB', 1)], 307 | ]) 308 | result = tldap.dn.dn2str(result) 309 | self.assertEqual(result, value) 310 | 311 | value = "1.3.6.1.4.1.1466.0=#04024869,O=Test,C=GB" 312 | result = tldap.dn.str2dn(value) 313 | self.assertIsNotNone(result) 314 | self.assertEqual(result, [ 315 | [('1.3.6.1.4.1.1466.0', '\x04\x02Hi', 1)], 316 | [('O', 'Test', 1)], 317 | [('C', 'GB', 1)], 318 | ]) 319 | result = tldap.dn.dn2str(result) 320 | self.assertEqual(result, "1.3.6.1.4.1.1466.0=\x04\x02Hi,O=Test,C=GB") 321 | 322 | value = "1.3.6.1.4.1.1466.0=\x04\x02Hi,O=Test,C=GB" 323 | result = tldap.dn.str2dn(value) 324 | self.assertIsNotNone(result) 325 | self.assertEqual(result, [ 326 | [('1.3.6.1.4.1.1466.0', '\x04\x02Hi', 1)], 327 | [('O', 'Test', 1)], 328 | [('C', 'GB', 1)], 329 | ]) 330 | result = tldap.dn.dn2str(result) 331 | self.assertEqual(result, value) 332 | 333 | def test_utf8(self): 334 | # 2 byte UTF8 335 | # UTF: 0x00A3 336 | # UTF8: 0xC2 0xA3 337 | value = six.u("ABC=DEF,HIJ=KIF£") 338 | result = tldap.dn.str2dn(value) 339 | self.assertIsNotNone(result) 340 | self.assertEqual(result, [ 341 | [('ABC', 'DEF', 1)], [('HIJ', six.u('KIF£'), 1)] 342 | ]) 343 | result = tldap.dn.dn2str(result) 344 | self.assertEqual(result, value) 345 | 346 | # 3 byte UTF8 347 | # UTF: 0x0982 348 | # UTF8: 0xE0 0xA6 0x82 349 | value = six.u("ABC=DEFং,HIJ=KIF") 350 | result = tldap.dn.str2dn(value) 351 | self.assertIsNotNone(result) 352 | self.assertEqual(result, [ 353 | [('ABC', six.u('DEFং'), 1)], [('HIJ', 'KIF', 1)] 354 | ]) 355 | result = tldap.dn.dn2str(result) 356 | self.assertEqual(result, value) 357 | 358 | # 3 byte UTF8 359 | # UTF: 0x4F60, 0x597D 360 | # UTF8: 0xE4 0xBD 0xA0, 0xE5 0xA5 0xBD 361 | value = six.u("ABC=DEF你好,HIJ=KIF") 362 | result = tldap.dn.str2dn(value) 363 | self.assertIsNotNone(result) 364 | self.assertEqual(result, [ 365 | [('ABC', six.u('DEF你好'), 1)], [('HIJ', 'KIF', 1)] 366 | ]) 367 | result = tldap.dn.dn2str(result) 368 | self.assertEqual(result, value) 369 | 370 | # 4 byte UTF8 371 | # UTF: 0x10300, 0x10301, 0x10302 372 | # UTF8: 0xF0 0x90 0x8C 0x80, 0xF0 0x90 0x8C 0x81, 0xF0 0x90 0x8C 0x82 373 | value = six.u("ABC=DEF𐌀𐌁𐌂,HIJ=KIF") 374 | result = tldap.dn.str2dn(value) 375 | self.assertIsNotNone(result) 376 | self.assertEqual(result, [ 377 | [('ABC', six.u('DEF𐌀𐌁𐌂'), 1)], [('HIJ', 'KIF', 1)] 378 | ]) 379 | result = tldap.dn.dn2str(result) 380 | self.assertEqual(result, value) 381 | -------------------------------------------------------------------------------- /tests/a_unit/test_ldap_passwd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | # Copyright 2012-2014 Brian May 5 | # 6 | # This file is part of python-tldap. 7 | # 8 | # python-tldap is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # python-tldap is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with python-tldap If not, see . 20 | 21 | import unittest 22 | import warnings 23 | 24 | import tldap.ldap_passwd as lp 25 | 26 | server = None 27 | 28 | 29 | class PasswordTest(unittest.TestCase): 30 | 31 | def test_password_check_ldap_md5_crypt(self): 32 | self.assertTrue(lp.check_password( 33 | "test", "{MD5}CY9rzUYh03PK3k6DJie09g==")) 34 | 35 | def test_password_check_ldap_sha1(self): 36 | self.assertTrue(lp.check_password( 37 | "test", "{SHA}qUqP5cyxm6YcTAhz05Hph5gvu9M=")) 38 | 39 | def test_password_check_ldap_salted_sha1(self): 40 | self.assertTrue(lp.check_password( 41 | "test", "{SSHA}sAloRnCFgBV+SjStZB0lIr8jCCq21to7")) 42 | 43 | def test_password_check_ldap_salted_md5(self): 44 | self.assertTrue(lp.check_password( 45 | "test", "{SMD5}xosLPIl3lM7lKx4xeEDPmdpjTig=")) 46 | 47 | def test_password_check_md5_crypt(self): 48 | self.assertTrue(lp.check_password( 49 | "test", "{CRYPT}$1$U1TmLCl7$MZS59PDJxAE8j9fO/Zs4A0")) 50 | # some old passwords have crypt in lower case 51 | self.assertTrue(lp.check_password( 52 | "test", "{crypt}$1$U1TmLCl7$MZS59PDJxAE8j9fO/Zs4A0")) 53 | 54 | def test_password_check_des_crypt(self): 55 | self.assertTrue(lp.check_password( 56 | "test", "{CRYPT}PQl1.p7BcJRuM")) 57 | # some old passwords have crypt in lower case 58 | self.assertTrue(lp.check_password( 59 | "test", "{crypt}PQl1.p7BcJRuM")) 60 | 61 | def test_password_encode(self): 62 | encrypted = lp.encode_password("test") 63 | self.assertTrue(encrypted.startswith("{CRYPT}$6$")) 64 | self.assertTrue(lp.check_password("test", encrypted)) 65 | self.assertFalse(lp.check_password("teddst", encrypted)) 66 | -------------------------------------------------------------------------------- /tests/a_unit/test_modlist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | # Copyright 2012-2014 Brian May 5 | # 6 | # This file is part of python-tldap. 7 | # 8 | # python-tldap is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # python-tldap is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with python-tldap If not, see . 20 | 21 | import unittest 22 | import ldap3 23 | from distutils.version import LooseVersion 24 | 25 | import tldap.modlist 26 | 27 | 28 | class DNTest(unittest.TestCase): 29 | 30 | def test_addModlist(self): 31 | A = { 32 | 'A': ['ABC'], 33 | 'B': ['DEF'], 34 | } 35 | EXPECTED = { 36 | 'A': ['ABC'], 37 | 'B': ['DEF'], 38 | } 39 | modlist = tldap.modlist.addModlist(A) 40 | self.assertEqual(modlist, EXPECTED) 41 | 42 | def test_modifyModlist(self): 43 | A = { 44 | 'A': ['ABC'], 45 | 'B': ['DEF'], 46 | 'I': [''], 47 | 'X': ['AA', 'BB', 'CC'], 48 | 'Y': ['AA', 'BB', 'DD'], 49 | } 50 | B = { 51 | 'A': ['ABC'], 52 | 'C': ['HIJ'], 53 | 'I': [''], 54 | 'X': ['CC', 'BB', 'AA'], 55 | 'Y': ['CC', 'BB', 'AA'], 56 | } 57 | EXPECTED = { 58 | 'B': (ldap3.MODIFY_DELETE, []), 59 | 'C': (ldap3.MODIFY_ADD, ['HIJ']), 60 | 'Y': (ldap3.MODIFY_REPLACE, ['CC', 'BB', 'AA']), 61 | } 62 | modlist = tldap.modlist.modifyModlist(A, B) 63 | self.assertEqual(modlist, EXPECTED) 64 | -------------------------------------------------------------------------------- /tests/a_unit/test_query.py: -------------------------------------------------------------------------------- 1 | import tldap 2 | import tldap.query 3 | import tests.database 4 | 5 | 6 | def test_filter_normal(): 7 | """ Test filter. """ 8 | ldap_filter = tldap.query.get_filter( 9 | tldap.Q(uid='tux'), 10 | tests.database.Account.get_fields(), 11 | "uid" 12 | ) 13 | assert ldap_filter == b"(uid=tux)" 14 | 15 | 16 | def test_filter_backslash(): 17 | """ Test filter with backslash. """ 18 | ldap_filter = tldap.query.get_filter( 19 | tldap.Q(uid='t\\ux'), 20 | tests.database.Account.get_fields(), 21 | "uid" 22 | ) 23 | assert ldap_filter == b"(uid=t\\5cux)" 24 | 25 | 26 | def test_filter_negated(): 27 | """ Test filter with negated value. """ 28 | ldap_filter = tldap.query.get_filter( 29 | ~tldap.Q(uid='tux'), 30 | tests.database.Account.get_fields(), 31 | "uid" 32 | ) 33 | assert ldap_filter == b"(!(uid=tux))" 34 | 35 | 36 | def test_filter_or_2(): 37 | """ Test filter with OR condition. """ 38 | ldap_filter = tldap.query.get_filter( 39 | tldap.Q(uid='tux') | tldap.Q(uid='tuz'), 40 | tests.database.Account.get_fields(), 41 | "uid" 42 | ) 43 | assert ldap_filter == b"(|(uid=tux)(uid=tuz))" 44 | 45 | 46 | def test_filter_or_3(): 47 | """ Test filter with OR condition """ 48 | ldap_filter = tldap.query.get_filter( 49 | tldap.Q() | tldap.Q(uid='tux') | tldap.Q(uid='tuz'), 50 | tests.database.Account.get_fields(), 51 | "uid" 52 | ) 53 | assert ldap_filter == b"(|(uid=tux)(uid=tuz))" 54 | 55 | 56 | def test_filter_and(): 57 | """ Test filter with AND condition. """ 58 | ldap_filter = tldap.query.get_filter( 59 | tldap.Q() & tldap.Q(uid='tux') & tldap.Q(uid='tuz'), 60 | tests.database.Account.get_fields(), 61 | "uid" 62 | ) 63 | assert ldap_filter == b"(&(uid=tux)(uid=tuz))" 64 | 65 | 66 | def test_filter_and_or(): 67 | """ Test filter with AND and OR condition. """ 68 | ldap_filter = tldap.query.get_filter( 69 | tldap.Q(uid='tux') & (tldap.Q(uid='tuz') | tldap.Q(uid='meow')), 70 | tests.database.Account.get_fields(), 71 | "uid" 72 | ) 73 | assert ldap_filter == b"(&(uid=tux)(|(uid=tuz)(uid=meow)))" 74 | -------------------------------------------------------------------------------- /tests/b_integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Karaage-Cluster/python-tldap/6cb877b28e07c0fd78a449e4c1b140fcab0117ef/tests/b_integration/__init__.py -------------------------------------------------------------------------------- /tests/b_integration/accounts.feature: -------------------------------------------------------------------------------- 1 | Feature: Testing account functions 2 | 3 | Scenario: Create account 4 | When we enter a transaction 5 | And we create a account called tux 6 | And we commit the transaction 7 | Then we should be able to get a account called tux 8 | And we should be able confirm the cn attribute is Tux Torvalds 9 | And we should be able to find 1 accounts 10 | 11 | Scenario: Create account with rollback 12 | When we enter a transaction 13 | And we create a account called tux 14 | And we rollback the transaction 15 | Then we should not be able to get a account called tux 16 | And we should be able to find 0 accounts 17 | 18 | Scenario: Create 2 accounts 19 | When we enter a transaction 20 | And we create a account called tux 21 | And we create a account called tuz 22 | And we commit the transaction 23 | Then we should be able to get a account called tux 24 | And we should be able to get a account called tuz 25 | And we should be able to find 2 accounts 26 | 27 | Scenario: Modify account 28 | When we create a account called tux 29 | And we enter a transaction 30 | And we modify a account called tux 31 | And we commit the transaction 32 | Then we should be able to get a account called tux 33 | And we should be able confirm the cn attribute is Super Tux 34 | 35 | Scenario: Modify account with rollback 36 | When we create a account called tux 37 | And we enter a transaction 38 | And we modify a account called tux 39 | And we rollback the transaction 40 | Then we should be able to get a account called tux 41 | And we should be able confirm the cn attribute is Tux Torvalds 42 | 43 | Scenario: Create 2 accounts with rollback 44 | When we enter a transaction 45 | And we create a account called tux 46 | And we create a account called tuz 47 | And we rollback the transaction 48 | Then we should not be able to get a account called tux 49 | And we should not be able to get a account called tuz 50 | And we should be able to find 0 accounts 51 | 52 | Scenario: Delete account 53 | When we create a account called tux 54 | And we enter a transaction 55 | And we delete a account called tux 56 | And we commit the transaction 57 | Then we should not be able to get a account called tux 58 | And we should be able to find 0 accounts 59 | 60 | Scenario: Delete account with rollback 61 | When we create a account called tux 62 | And we enter a transaction 63 | And we delete a account called tux 64 | And we rollback the transaction 65 | Then we should be able to get a account called tux 66 | And we should be able to find 1 accounts 67 | 68 | Scenario: Rename account 69 | When we create a account called tux 70 | And we enter a transaction 71 | And we rename a account called tux to tuz 72 | And we commit the transaction 73 | Then we should not be able to get a account called tux 74 | Then we should be able to get a account called tuz 75 | And we should be able to find 1 accounts 76 | 77 | Scenario: Rename account with rollback 78 | When we create a account called tux 79 | And we enter a transaction 80 | And we rename a account called tux to tuz 81 | And we rollback the transaction 82 | Then we should be able to get a account called tux 83 | Then we should not be able to get a account called tuz 84 | And we should be able to find 1 accounts 85 | 86 | Scenario: Move account 87 | When we create a account called tux 88 | And we enter a transaction 89 | And we move a account called tux to ou=Groups,dc=python-ldap,dc=org 90 | And we commit the transaction 91 | Then we should not be able to get a account called tux 92 | And we should be able to get a account at dn ou=Groups,dc=python-ldap,dc=org called tux 93 | And we should be able to find 0 accounts 94 | 95 | Scenario: Move account with rollback 96 | When we create a account called tux 97 | And we enter a transaction 98 | And we move a account called tux to ou=Groups,dc=python-ldap,dc=org 99 | And we rollback the transaction 100 | Then we should be able to get a account called tux 101 | And we should not be able to get a account at dn ou=Groups,dc=python-ldap,dc=org called tux 102 | And we should be able to find 1 accounts 103 | -------------------------------------------------------------------------------- /tests/b_integration/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ldap3.core import exceptions 4 | import pytest 5 | from pytest_bdd import given, when, then, parsers 6 | 7 | import tldap 8 | from tldap import transaction 9 | import tldap.backend 10 | import tldap.database 11 | 12 | 13 | @pytest.fixture 14 | def settings(): 15 | return { 16 | 'default': { 17 | 'ENGINE': 'tldap.backend.fake_transactions', 18 | 'URI': os.environ['LDAP_URL'], 19 | 'USER': os.environ['LDAP_DN'], 20 | 'PASSWORD': os.environ['LDAP_PASSWORD'], 21 | 'USE_TLS': False, 22 | 'TLS_CA': None, 23 | 'LDAP_ACCOUNT_BASE': os.environ['LDAP_ACCOUNT_BASE'], 24 | 'LDAP_GROUP_BASE': os.environ['LDAP_GROUP_BASE'], 25 | 'NUMBER_SCHEME': 'default', 26 | } 27 | } 28 | 29 | 30 | @pytest.fixture 31 | def ldap(settings): 32 | tldap.backend.setup(settings) 33 | 34 | connection = tldap.backend.connections['default'] 35 | 36 | with tldap.transaction.commit_manually(): 37 | yield connection 38 | tldap.transaction.rollback() 39 | 40 | if tldap.transaction.is_managed(): 41 | raise RuntimeError("Unexpected still inside a transaction error.") 42 | 43 | 44 | @pytest.fixture 45 | def context(): 46 | return {} 47 | 48 | 49 | @when('we enter a transaction') 50 | def step_start_transaction(ldap): 51 | transaction.enter_transaction_management() 52 | 53 | 54 | @when('we commit the transaction') 55 | def step_commit_transaction(ldap): 56 | transaction.commit() 57 | transaction.leave_transaction_management() 58 | 59 | 60 | @when('we rollback the transaction') 61 | def step_rollback_transaction(ldap): 62 | transaction.rollback() 63 | transaction.leave_transaction_management() 64 | 65 | 66 | @then(parsers.cfparse( 67 | 'we should be able confirm the {attribute} attribute is {value}')) 68 | def step_confirm_attribute(context, attribute, value): 69 | actual_value = context['obj'].get_as_single(attribute) 70 | assert str(actual_value) == value, attribute 71 | -------------------------------------------------------------------------------- /tests/b_integration/groups.feature: -------------------------------------------------------------------------------- 1 | Feature: Testing group functions 2 | 3 | Scenario: Create group 4 | When we enter a transaction 5 | And we create a group called tux 6 | And we commit the transaction 7 | Then we should be able to get a group called tux 8 | And we should be able confirm the gidNumber attribute is 10 9 | And we should be able to find 1 groups 10 | 11 | Scenario: Create group with rollback 12 | When we enter a transaction 13 | And we create a group called tux 14 | And we rollback the transaction 15 | Then we should not be able to get a group called tux 16 | And we should be able to find 0 groups 17 | 18 | Scenario: Create 2 groups 19 | When we enter a transaction 20 | And we create a group called tux 21 | And we create a group called tuz 22 | And we commit the transaction 23 | Then we should be able to get a group called tux 24 | And we should be able to get a group called tuz 25 | And we should be able to find 2 groups 26 | 27 | Scenario: Modify group 28 | When we create a group called tux 29 | And we enter a transaction 30 | And we modify a group called tux 31 | And we commit the transaction 32 | Then we should be able to get a group called tux 33 | And we should be able confirm the gidNumber attribute is 11 34 | 35 | Scenario: Modify group with rollback 36 | When we create a group called tux 37 | And we enter a transaction 38 | And we modify a group called tux 39 | And we rollback the transaction 40 | Then we should be able to get a group called tux 41 | And we should be able confirm the gidNumber attribute is 10 42 | 43 | Scenario: Create 2 groups with rollback 44 | When we enter a transaction 45 | And we create a group called tux 46 | And we create a group called tuz 47 | And we rollback the transaction 48 | Then we should not be able to get a group called tux 49 | And we should not be able to get a group called tuz 50 | And we should be able to find 0 groups 51 | 52 | Scenario: Delete group 53 | When we create a group called tux 54 | And we enter a transaction 55 | And we delete a group called tux 56 | And we commit the transaction 57 | Then we should not be able to get a group called tux 58 | And we should be able to find 0 groups 59 | 60 | Scenario: Delete group with rollback 61 | When we create a group called tux 62 | And we enter a transaction 63 | And we delete a group called tux 64 | And we rollback the transaction 65 | Then we should be able to get a group called tux 66 | And we should be able to find 1 groups 67 | 68 | Scenario: Rename group 69 | When we create a group called tux 70 | And we enter a transaction 71 | And we rename a group called tux to tuz 72 | And we commit the transaction 73 | Then we should not be able to get a group called tux 74 | Then we should be able to get a group called tuz 75 | And we should be able to find 1 groups 76 | 77 | Scenario: Rename group with rollback 78 | When we create a group called tux 79 | And we enter a transaction 80 | And we rename a group called tux to tuz 81 | And we rollback the transaction 82 | Then we should be able to get a group called tux 83 | Then we should not be able to get a group called tuz 84 | And we should be able to find 1 groups 85 | 86 | Scenario: Move group 87 | When we create a group called tux 88 | And we enter a transaction 89 | And we move a group called tux to ou=People,dc=python-ldap,dc=org 90 | And we commit the transaction 91 | Then we should not be able to get a group called tux 92 | And we should be able to get a group at dn ou=People,dc=python-ldap,dc=org called tux 93 | And we should be able to find 0 groups 94 | 95 | Scenario: Move group with rollback 96 | When we create a group called tux 97 | And we enter a transaction 98 | And we move a group called tux to ou=People,dc=python-ldap,dc=org 99 | And we rollback the transaction 100 | Then we should be able to get a group called tux 101 | And we should not be able to get a group at dn ou=People,dc=python-ldap,dc=org called tux 102 | And we should be able to find 1 groups 103 | -------------------------------------------------------------------------------- /tests/b_integration/test_accounts.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ldap3.core import exceptions 4 | import pytest 5 | from pytest_bdd import scenarios, when, then, parsers 6 | 7 | import tldap.database 8 | from tldap import Q 9 | import tldap.backend.base 10 | from tldap.django.models import Counters 11 | from tldap.exceptions import ObjectDoesNotExist 12 | from tests.database import Account 13 | 14 | scenarios('accounts.feature') 15 | 16 | 17 | @pytest.fixture 18 | def account(ldap): 19 | account = Account({ 20 | 'uid': 'tux', 21 | 'givenName': "Tux", 22 | 'sn': "Torvalds", 23 | 'cn': "Tux Torvalds", 24 | 'telephoneNumber': "000", 25 | 'mail': "tuz@example.org", 26 | 'o': "Linux Rules", 27 | 'userPassword': "silly", 28 | 'homeDirectory': "/home/tux", 29 | 'uidNumber': 10, 30 | 'gidNumber': 10, 31 | }) 32 | yield tldap.database.insert(account) 33 | 34 | 35 | @when(parsers.cfparse('we create a account called {name}')) 36 | def step_create_account(ldap, name): 37 | """ Test if we can create a account. """ 38 | account = Account({ 39 | 'uid': name, 40 | 'givenName': "Tux", 41 | 'sn': "Torvalds", 42 | 'cn': "Tux Torvalds", 43 | 'telephoneNumber': "000", 44 | 'mail': "tuz@example.org", 45 | 'o': "Linux Rules", 46 | 'userPassword': "silly", 47 | 'homeDirectory': "/home/tux", 48 | 'uidNumber': 10, 49 | 'gidNumber': 10, 50 | }) 51 | tldap.database.insert(account) 52 | 53 | 54 | @when(parsers.cfparse('we modify a account called {name}')) 55 | def step_modify_account(ldap, name): 56 | """ Test if we can modify a account. """ 57 | account = tldap.database.get_one(Account, Q(uid=name)) 58 | changes = tldap.database.changeset(account, { 59 | 'sn': "Tux", 60 | 'givenName': "Super", 61 | 'cn': "Super Tux", 62 | }) 63 | tldap.database.save(changes) 64 | account = tldap.database.get_one(Account, Q(uid=name)) 65 | print("modify", account['cn']) 66 | 67 | 68 | @when(parsers.cfparse('we rename a account called {name} to {new_name}')) 69 | def step_rename_account(ldap, name, new_name): 70 | """ Test if we can rename a account. """ 71 | account = tldap.database.get_one(Account, Q(uid=name)) 72 | tldap.database.rename(account, uid=new_name) 73 | 74 | 75 | @when(parsers.cfparse('we move a account called {name} to {new_dn}')) 76 | def step_move_account(ldap, name, new_dn): 77 | """ Test if we can move a account. """ 78 | account = tldap.database.get_one(Account, Q(uid=name)) 79 | tldap.database.rename(account, new_dn) 80 | 81 | 82 | @when(parsers.cfparse('we delete a account called {name}')) 83 | def step_delete_account(ldap, name): 84 | """ Test if we can delete a account. """ 85 | account = tldap.database.get_one(Account, Q(uid=name)) 86 | tldap.database.delete(account) 87 | 88 | 89 | @then('we should be able to search for a account') 90 | def step_search_account(ldap): 91 | """ Test we can search. """ 92 | list(tldap.database.search(Account)) 93 | 94 | 95 | @then('we should not be able to search for a account') 96 | def step_not_search_account(ldap): 97 | """ Test we can search. """ 98 | with pytest.raises(exceptions.LDAPInvalidCredentialsResult): 99 | list(tldap.database.search(Account)) 100 | 101 | 102 | @then(parsers.cfparse('we should be able to get a account called {name}')) 103 | def step_get_account_success(ldap, context, name): 104 | account = tldap.database.get_one(Account, Q(uid=name)) 105 | context['obj'] = account 106 | print("get", account['cn']) 107 | 108 | 109 | @then(parsers.cfparse('we should not be able to get a account called {name}')) 110 | def step_get_account_not_found(ldap, name): 111 | with pytest.raises(ObjectDoesNotExist): 112 | tldap.database.get_one(Account, Q(uid=name)) 113 | 114 | 115 | @then(parsers.cfparse( 116 | 'we should be able to get a account at dn {dn} called {name}')) 117 | def step_get_account_dn_success(ldap, context, name, dn): 118 | context['obj'] = tldap.database.get_one(Account, Q(uid=name), base_dn=dn) 119 | 120 | 121 | @then(parsers.cfparse( 122 | 'we should not be able to get a account at dn {dn} called {name}')) 123 | def step_get_account_dn_not_found(ldap, name, dn): 124 | with pytest.raises(ObjectDoesNotExist): 125 | tldap.database.get_one(Account, Q(uid=name), base_dn=dn) 126 | 127 | 128 | @then(parsers.cfparse('we should be able to find {count:d} accounts')) 129 | def step_count_accounts(ldap, count): 130 | assert count == len(list(tldap.database.search(Account, None))) 131 | 132 | 133 | @pytest.mark.django_db(transaction=True) 134 | def test_create(ldap): 135 | """ Test create LDAP object. """ 136 | 137 | # Create the object. 138 | account_1 = Account({ 139 | 'uid': "tux1", 140 | 'givenName': "Tux", 141 | 'sn': "Torvalds", 142 | 'cn': "Tux Torvalds", 143 | 'telephoneNumber': "000", 144 | 'mail': "tuz@example.org", 145 | 'o': "Linux Rules", 146 | 'userPassword': "silly", 147 | 'homeDirectory': "/home/tux", 148 | 'gidNumber': 10, 149 | }) 150 | 151 | account_1 = tldap.database.insert(account_1) 152 | assert account_1['uidNumber'] == [10000] 153 | 154 | account_2 = Account({ 155 | 'uid': "tux2", 156 | 'givenName': "Tux", 157 | 'sn': "Torvalds", 158 | 'cn': "Tux Torvalds", 159 | 'telephoneNumber': "000", 160 | 'mail': "tuz@example.org", 161 | 'o': "Linux Rules", 162 | 'userPassword': "silly", 163 | 'homeDirectory': "/home/tux", 164 | 'gidNumber': 10, 165 | }) 166 | 167 | account_2 = tldap.database.insert(account_2) 168 | assert account_2['uidNumber'] == [10001] 169 | 170 | account_3 = Account({ 171 | 'uid': "tux3", 172 | 'givenName': "Tux", 173 | 'sn': "Torvalds", 174 | 'cn': "Tux Torvalds", 175 | 'telephoneNumber': "000", 176 | 'mail': "tuz@example.org", 177 | 'o': "Linux Rules", 178 | 'userPassword': "silly", 179 | 'homeDirectory': "/home/tux", 180 | 'gidNumber': 10, 181 | }) 182 | 183 | account_3 = tldap.database.insert(account_3) 184 | assert account_3['uidNumber'] == [10002] 185 | 186 | 187 | @pytest.mark.django_db(transaction=True) 188 | def test_create_with_reset(ldap): 189 | """ Test create LDAP object. """ 190 | 191 | # Create the object. 192 | account_1 = Account({ 193 | 'uid': "tux1", 194 | 'givenName': "Tux", 195 | 'sn': "Torvalds", 196 | 'cn': "Tux Torvalds", 197 | 'telephoneNumber': "000", 198 | 'mail': "tuz@example.org", 199 | 'o': "Linux Rules", 200 | 'userPassword': "silly", 201 | 'homeDirectory': "/home/tux", 202 | 'gidNumber': 10, 203 | }) 204 | 205 | account_1 = tldap.database.insert(account_1) 206 | assert account_1['uidNumber'] == [10000] 207 | 208 | Counters.objects.all().delete() 209 | 210 | account_2 = Account({ 211 | 'uid': "tux2", 212 | 'givenName': "Tux", 213 | 'sn': "Torvalds", 214 | 'cn': "Tux Torvalds", 215 | 'telephoneNumber': "000", 216 | 'mail': "tuz@example.org", 217 | 'o': "Linux Rules", 218 | 'userPassword': "silly", 219 | 'homeDirectory': "/home/tux", 220 | 'gidNumber': 10, 221 | }) 222 | 223 | account_2 = tldap.database.insert(account_2) 224 | assert account_2['uidNumber'] == [10001] 225 | 226 | account_3 = Account({ 227 | 'uid': "tux3", 228 | 'givenName': "Tux", 229 | 'sn': "Torvalds", 230 | 'cn': "Tux Torvalds", 231 | 'telephoneNumber': "000", 232 | 'mail': "tuz@example.org", 233 | 'o': "Linux Rules", 234 | 'userPassword': "silly", 235 | 'homeDirectory': "/home/tux", 236 | 'gidNumber': 10, 237 | }) 238 | 239 | account_3 = tldap.database.insert(account_3) 240 | assert account_3['uidNumber'] == [10002] 241 | 242 | 243 | def test_lock_account(account, ldap: tldap.backend.base.LdapBase): 244 | # Check account is unlocked. 245 | assert account.get_as_single('locked') is False 246 | assert account.get_as_single('loginShell') == "/bin/bash" 247 | assert ldap.check_password(account.get_as_single('dn'), 'silly') is True 248 | 249 | # Lock account. 250 | changes = tldap.database.changeset(account, {'locked': True}) 251 | account = tldap.database.save(changes) 252 | 253 | # Check account is locked. 254 | assert account.get_as_single('locked') is True 255 | assert account.get_as_single('loginShell') == "/locked/bin/bash" 256 | 257 | account = tldap.database.get_one(Account, Q(uid='tux')) 258 | 259 | assert account.get_as_single('locked') is True 260 | assert account.get_as_single('loginShell') == "/locked/bin/bash" 261 | 262 | assert ldap.check_password(account.get_as_single('dn'), 'silly') is False 263 | 264 | # Change the login shell 265 | changes = tldap.database.changeset(account, {'loginShell': '/bin/zsh'}) 266 | account = tldap.database.save(changes) 267 | 268 | # Check the account is still locked. 269 | assert account.get_as_single('locked') is True 270 | assert account.get_as_single('loginShell') == "/locked/bin/zsh" 271 | 272 | account = tldap.database.get_one(Account, Q(uid='tux')) 273 | 274 | assert account.get_as_single('locked') == True 275 | assert account.get_as_single('loginShell') == "/locked/bin/zsh" 276 | 277 | assert ldap.check_password(account.get_as_single('dn'), 'silly') is False 278 | 279 | # Unlock the account. 280 | changes = tldap.database.changeset(account, {'locked': False}) 281 | account = tldap.database.save(changes) 282 | 283 | # Check the account is now unlocked. 284 | assert account.get_as_single('locked') is False 285 | assert account.get_as_single('loginShell') == "/bin/zsh" 286 | 287 | account = tldap.database.get_one(Account, Q(uid='tux')) 288 | 289 | assert account.get_as_single('locked') is False 290 | assert account.get_as_single('loginShell') == "/bin/zsh" 291 | 292 | assert ldap.check_password(account.get_as_single('dn'), 'silly') is True 293 | 294 | 295 | @pytest.mark.skipif(os.environ['LDAP_TYPE'] != 'openldap', reason="Require OpenLDAP") 296 | def test_lock_account_openldap(account, ldap: tldap.backend.base.LdapBase): 297 | # Check account is unlocked. 298 | assert account.get_as_single('locked') is False 299 | assert account.get_as_list('pwdAccountLockedTime') == [] 300 | assert ldap.check_password(account.get_as_single('dn'), 'silly') is True 301 | 302 | # Lock account. 303 | changes = tldap.database.changeset(account, {'locked': True}) 304 | account = tldap.database.save(changes) 305 | 306 | # Check account is locked. 307 | assert account.get_as_single('locked') is True 308 | assert account.get_as_list('pwdAccountLockedTime') == ["000001010000Z"] 309 | 310 | account = tldap.database.get_one(Account, Q(uid='tux')) 311 | 312 | assert account.get_as_single('locked') is True 313 | assert account.get_as_list('pwdAccountLockedTime') == ["000001010000Z"] 314 | 315 | assert ldap.check_password(account.get_as_single('dn'), 'silly') is False 316 | 317 | # Unlock the account. 318 | changes = tldap.database.changeset(account, {'locked': False}) 319 | account = tldap.database.save(changes) 320 | 321 | # Check the account is now unlocked. 322 | assert account.get_as_single('locked') is False 323 | assert account.get_as_list('pwdAccountLockedTime') == [] 324 | 325 | account = tldap.database.get_one(Account, Q(uid='tux')) 326 | 327 | assert account.get_as_single('locked') is False 328 | assert account.get_as_list('pwdAccountLockedTime') == [] 329 | 330 | assert ldap.check_password(account.get_as_single('dn'), 'silly') is True 331 | -------------------------------------------------------------------------------- /tests/b_integration/test_groups.py: -------------------------------------------------------------------------------- 1 | from ldap3.core import exceptions 2 | import pytest 3 | from pytest_bdd import scenarios, when, then, parsers 4 | 5 | import tldap.database 6 | from tldap import Q 7 | from tldap.django.models import Counters 8 | from tldap.exceptions import ObjectDoesNotExist 9 | from tests.database import Group 10 | 11 | scenarios('groups.feature') 12 | 13 | 14 | @when(parsers.cfparse('we create a group called {name}')) 15 | def step_create_group(ldap, name): 16 | """ Test if we can create a group. """ 17 | group = Group({ 18 | 'cn': name, 19 | 'gidNumber': 10, 20 | 'memberUid': [], 21 | }) 22 | tldap.database.insert(group) 23 | 24 | 25 | @when(parsers.cfparse('we modify a group called {name}')) 26 | def step_modify_group(ldap, name): 27 | """ Test if we can modify a group. """ 28 | group = tldap.database.get_one(Group, Q(cn=name)) 29 | changes = tldap.database.changeset(group, {'gidNumber': 11}) 30 | tldap.database.save(changes) 31 | group = tldap.database.get_one(Group, Q(cn=name)) 32 | print("modify", group['cn']) 33 | 34 | 35 | @when(parsers.cfparse('we rename a group called {name} to {new_name}')) 36 | def step_rename_group(ldap, name, new_name): 37 | """ Test if we can rename a group. """ 38 | group = tldap.database.get_one(Group, Q(cn=name)) 39 | tldap.database.rename(group, cn=new_name) 40 | 41 | 42 | @when(parsers.cfparse('we move a group called {name} to {new_dn}')) 43 | def step_move_group(ldap, name, new_dn): 44 | """ Test if we can move a group. """ 45 | group = tldap.database.get_one(Group, Q(cn=name)) 46 | tldap.database.rename(group, new_dn) 47 | 48 | 49 | @when(parsers.cfparse('we delete a group called {name}')) 50 | def step_delete_group(ldap, name): 51 | """ Test if we can delete a group. """ 52 | group = tldap.database.get_one(Group, Q(cn=name)) 53 | tldap.database.delete(group) 54 | 55 | 56 | @then('we should be able to search for a group') 57 | def step_search_group(ldap): 58 | """ Test we can search. """ 59 | 60 | 61 | @then('we should not be able to search for a group') 62 | def step_not_search_group(ldap): 63 | """ Test we can search. """ 64 | with pytest.raises(exceptions.LDAPInvalidCredentialsResult): 65 | list(tldap.database.search(Group)) 66 | 67 | 68 | @then(parsers.cfparse('we should be able to get a group called {name}')) 69 | def step_get_group_success(ldap, context, name): 70 | group = tldap.database.get_one(Group, Q(cn=name)) 71 | context['obj'] = group 72 | print("get", group['cn']) 73 | 74 | 75 | @then(parsers.cfparse('we should not be able to get a group called {name}')) 76 | def step_get_group_not_found(ldap, name): 77 | with pytest.raises(ObjectDoesNotExist): 78 | tldap.database.get_one(Group, Q(cn=name)) 79 | 80 | 81 | @then(parsers.cfparse( 82 | 'we should be able to get a group at dn {dn} called {name}')) 83 | def step_get_group_dn_success(ldap, context, name, dn): 84 | context['obj'] = tldap.database.get_one(Group, Q(cn=name), base_dn=dn) 85 | 86 | 87 | @then(parsers.cfparse( 88 | 'we should not be able to get a group at dn {dn} called {name}')) 89 | def step_get_group_dn_not_found(ldap, name, dn): 90 | with pytest.raises(ObjectDoesNotExist): 91 | tldap.database.get_one(Group, Q(cn=name), base_dn=dn) 92 | 93 | 94 | @then(parsers.cfparse('we should be able to find {count:d} groups')) 95 | def step_count_groups(ldap, count): 96 | assert count == len(list(tldap.database.search(Group))) 97 | 98 | 99 | @pytest.mark.django_db(transaction=True) 100 | def test_create(ldap): 101 | """ Test create LDAP object. """ 102 | 103 | # Create the object. 104 | group_1 = Group({ 105 | 'cn': 'penguins1', 106 | 'memberUid': [], 107 | }) 108 | 109 | group_1 = tldap.database.insert(group_1) 110 | assert group_1['gidNumber'] == [10000] 111 | 112 | group_2 = Group({ 113 | 'cn': 'penguins2', 114 | 'memberUid': [], 115 | }) 116 | 117 | group_2 = tldap.database.insert(group_2) 118 | assert group_2['gidNumber'] == [10001] 119 | 120 | group_3 = Group({ 121 | 'cn': 'penguins3', 122 | 'memberUid': [], 123 | }) 124 | 125 | group_3 = tldap.database.insert(group_3) 126 | assert group_3['gidNumber'] == [10002] 127 | 128 | 129 | @pytest.mark.django_db(transaction=True) 130 | def test_create_with_reset(ldap): 131 | """ Test create LDAP object. """ 132 | 133 | # Create the object. 134 | group_1 = Group({ 135 | 'cn': 'penguins1', 136 | 'memberUid': [], 137 | }) 138 | 139 | group_1 = tldap.database.insert(group_1) 140 | assert group_1['gidNumber'] == [10000] 141 | 142 | Counters.objects.all().delete() 143 | 144 | group_2 = Group({ 145 | 'cn': 'penguins2', 146 | 'memberUid': [], 147 | }) 148 | 149 | group_2 = tldap.database.insert(group_2) 150 | assert group_2['gidNumber'] == [10001] 151 | 152 | Counters.objects.all().delete() 153 | 154 | group_3 = Group({ 155 | 'cn': 'penguins3', 156 | 'memberUid': [], 157 | }) 158 | 159 | group_3 = tldap.database.insert(group_3) 160 | assert group_3['gidNumber'] == [10002] -------------------------------------------------------------------------------- /tests/database.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict 3 | 4 | import tldap.fields 5 | from tldap.database import helpers, LdapObject, Changeset, SearchOptions, Database 6 | import tldap.django.helpers as dhelpers 7 | 8 | 9 | class Account(LdapObject): 10 | 11 | @classmethod 12 | def get_fields(cls) -> Dict[str, tldap.fields.Field]: 13 | fields = { 14 | **helpers.get_fields_common(), 15 | **helpers.get_fields_person(), 16 | **helpers.get_fields_account(), 17 | **helpers.get_fields_shadow(), 18 | } 19 | 20 | if os.environ['LDAP_TYPE'] == "openldap": 21 | fields.update(helpers.get_fields_pwdpolicy()) 22 | elif os.environ['LDAP_TYPE'] == 'ds389': 23 | fields.update(helpers.get_fields_password_object()) 24 | 25 | return fields 26 | 27 | @classmethod 28 | def get_search_options(cls, database: Database) -> SearchOptions: 29 | settings = database.settings 30 | return SearchOptions( 31 | base_dn=settings['LDAP_ACCOUNT_BASE'], 32 | object_class={'inetOrgPerson', 'organizationalPerson', 'person'}, 33 | pk_field="uid", 34 | ) 35 | 36 | @classmethod 37 | def on_load(cls, python_data: LdapObject, database: Database) -> LdapObject: 38 | python_data = helpers.load_person(python_data, Group) 39 | python_data = helpers.load_account(python_data, Group) 40 | python_data = helpers.load_shadow(python_data) 41 | 42 | if os.environ['LDAP_TYPE'] == "openldap": 43 | python_data = helpers.load_pwdpolicy(python_data) 44 | elif os.environ['LDAP_TYPE'] == 'ds389': 45 | python_data = helpers.load_password_object(python_data) 46 | 47 | return python_data 48 | 49 | @classmethod 50 | def on_save(cls, changes: Changeset, database: Database) -> Changeset: 51 | settings = database.settings 52 | changes = helpers.save_person(changes, database) 53 | changes = helpers.save_account(changes, database) 54 | changes = helpers.save_shadow(changes) 55 | 56 | classes = ['top', 'person', 'inetOrgPerson', 'organizationalPerson', 57 | 'shadowAccount', 'posixAccount'] 58 | 59 | if os.environ['LDAP_TYPE'] == "openldap": 60 | changes = helpers.save_pwdpolicy(changes) 61 | classes = classes + ['pwdPolicy'] 62 | elif os.environ['LDAP_TYPE'] == 'ds389': 63 | changes = helpers.save_password_object(changes) 64 | classes = classes + ['passwordObject'] 65 | 66 | changes = dhelpers.save_account(changes, Account, database) 67 | changes = helpers.set_object_class(changes, classes) 68 | changes = helpers.rdn_to_dn(changes, 'uid', settings['LDAP_ACCOUNT_BASE']) 69 | return changes 70 | 71 | 72 | class Group(LdapObject): 73 | 74 | @classmethod 75 | def get_fields(cls) -> Dict[str, tldap.fields.Field]: 76 | fields = { 77 | **helpers.get_fields_common(), 78 | **helpers.get_fields_group(), 79 | } 80 | return fields 81 | 82 | @classmethod 83 | def get_search_options(cls, database: Database) -> SearchOptions: 84 | settings = database.settings 85 | return SearchOptions( 86 | base_dn=settings['LDAP_GROUP_BASE'], 87 | object_class={'posixGroup'}, 88 | pk_field="cn", 89 | ) 90 | 91 | @classmethod 92 | def on_load(cls, python_data: LdapObject, _database: Database) -> LdapObject: 93 | python_data = helpers.load_group(python_data, Account) 94 | return python_data 95 | 96 | @classmethod 97 | def on_save(cls, changes: Changeset, database: Database) -> Changeset: 98 | settings = database.settings 99 | changes = helpers.save_group(changes) 100 | changes = dhelpers.save_group(changes, Group, database) 101 | changes = helpers.set_object_class(changes, ['top', 'posixGroup']) 102 | changes = helpers.rdn_to_dn(changes, 'cn', settings['LDAP_GROUP_BASE']) 103 | return changes 104 | 105 | @classmethod 106 | def add_member(cls, changes: Changeset, member: 'Account') -> Changeset: 107 | assert isinstance(changes.src, cls) 108 | return helpers.add_group_member(changes, member) 109 | 110 | @classmethod 111 | def remove_member(cls, changes: Changeset, member: 'Account') -> Changeset: 112 | assert isinstance(changes.src, cls) 113 | return helpers.remove_group_member(changes, member) 114 | 115 | 116 | class OU(LdapObject): 117 | 118 | @classmethod 119 | def get_fields(cls) -> Dict[str, tldap.fields.Field]: 120 | fields = helpers.get_fields_common() 121 | return fields 122 | 123 | @classmethod 124 | def get_search_options(cls, database: Database) -> SearchOptions: 125 | return SearchOptions( 126 | base_dn="", 127 | object_class={'organizationalUnit'}, 128 | pk_field="ou", 129 | ) 130 | 131 | @classmethod 132 | def on_load(cls, python_data: LdapObject, _database: Database) -> LdapObject: 133 | return python_data 134 | 135 | @classmethod 136 | def on_save(cls, changes: Changeset, _database: Database) -> Changeset: 137 | changes = helpers.set_object_class(changes, ['top', 'organizationalUnit']) 138 | return changes 139 | -------------------------------------------------------------------------------- /tests/django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Karaage-Cluster/python-tldap/6cb877b28e07c0fd78a449e4c1b140fcab0117ef/tests/django/__init__.py -------------------------------------------------------------------------------- /tests/django/database.py: -------------------------------------------------------------------------------- 1 | from tldap.database import Changeset, Database 2 | from tests import database as parent 3 | 4 | 5 | class Account(parent.Account): 6 | 7 | @classmethod 8 | def on_save(cls, changes: Changeset, database: Database) -> Changeset: 9 | return changes 10 | 11 | 12 | class Group(parent.Group): 13 | 14 | @classmethod 15 | def on_save(cls, changes: Changeset, database: Database) -> Changeset: 16 | return changes -------------------------------------------------------------------------------- /tests/django/settings.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | DEBUG = True 19 | SECRET_KEY = '5hvhpe6gv2t5x4$3dtq(w2v#vg@)sx4p3r_@wv%l41g!stslc*' 20 | 21 | INSTALLED_APPS = [ 22 | 'tldap.django', 23 | ] 24 | 25 | DATABASES = { 26 | 'default': { 27 | 'ENGINE': 'django.db.backends.sqlite3', 28 | 'NAME': 'test.sqlite3', 29 | } 30 | } 31 | 32 | USE_TZ = True 33 | -------------------------------------------------------------------------------- /tldap/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | """ 19 | Holds global stuff for tldap. 20 | 21 | Q 22 | Shortcut to :py:class:`tldap.query_utils.Q`, allows combining query terms. 23 | 24 | DEFAULT_LDAP_ALIAS 25 | Alias for default LDAP connection. 26 | """ 27 | 28 | from tldap.query_utils import Q # noqa: F401 29 | 30 | 31 | __author__ = """Brian May""" 32 | __email__ = 'brian@linuxpenguins.xyz' 33 | __version__ = '1.0.8' 34 | -------------------------------------------------------------------------------- /tldap/backend/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | from tldap.utils import DEFAULT_LDAP_ALIAS, ConnectionHandler 18 | 19 | 20 | connections = None 21 | """An object containing a list of all LDAP connections.""" 22 | 23 | 24 | def setup(settings): 25 | """ Function used to initialize LDAP settings. """ 26 | global connections 27 | connections = ConnectionHandler(settings) 28 | 29 | 30 | # DatabaseWrapper.__init__() takes a dictionary, not a settings module, so 31 | # we manually create the dictionary from the settings, passing only the 32 | # settings that the database backends care about. Note that TIME_ZONE is used 33 | # by the PostgreSQL backends. 34 | # We load all these up for backwards compatibility, you should use 35 | # connections['default'] instead. 36 | 37 | class DefaultConnectionProxy(object): 38 | """ 39 | Proxy for accessing the default DatabaseWrapper object's attributes. If you 40 | need to access the DatabaseWrapper object itself, use 41 | connections[DEFAULT_LDAP_ALIAS] instead. 42 | """ 43 | def __getattr__(self, item): 44 | return getattr(connections[DEFAULT_LDAP_ALIAS], item) 45 | 46 | def __setattr__(self, name, value): 47 | return setattr(connections[DEFAULT_LDAP_ALIAS], name, value) 48 | 49 | def __delattr__(self, name): 50 | return delattr(connections[DEFAULT_LDAP_ALIAS], name) 51 | 52 | def __eq__(self, other): 53 | return connections[DEFAULT_LDAP_ALIAS] == other 54 | 55 | def __ne__(self, other): 56 | return connections[DEFAULT_LDAP_ALIAS] != other 57 | 58 | 59 | """ The default LDAP connection. """ 60 | connection = DefaultConnectionProxy() 61 | -------------------------------------------------------------------------------- /tldap/backend/base.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | """ This module provides the LDAP base functions 19 | with a subset of the functions from the real ldap module. """ 20 | 21 | import logging 22 | import ssl 23 | from typing import Callable, Generator, Optional, Tuple, TypeVar 24 | from urllib.parse import urlparse 25 | 26 | import ldap3 27 | import ldap3.core.exceptions as exceptions 28 | 29 | 30 | logger = logging.getLogger(__name__) 31 | 32 | 33 | def _debug(*argv): 34 | argv = [str(arg) for arg in argv] 35 | logger.debug(" ".join(argv)) 36 | 37 | 38 | Entity = TypeVar('Entity') 39 | 40 | 41 | class LdapBase(object): 42 | """ The vase LDAP connection class. """ 43 | 44 | def __init__(self, settings_dict: dict) -> None: 45 | self.settings_dict = settings_dict 46 | self._obj = None 47 | self._connection_class = ldap3.Connection 48 | 49 | def close(self) -> None: 50 | if self._obj is not None: 51 | self._obj.unbind() 52 | self._obj = None 53 | 54 | ######################### 55 | # Connection Management # 56 | ######################### 57 | 58 | def set_connection_class(self, connection_class): 59 | self._connection_class = connection_class 60 | 61 | def check_password(self, dn: str, password: str) -> bool: 62 | try: 63 | conn = self._connect(user=dn, password=password) 64 | conn.unbind() 65 | return True 66 | except exceptions.LDAPInvalidCredentialsResult: 67 | return False 68 | except exceptions.LDAPUnwillingToPerformResult: 69 | return False 70 | 71 | def _connect(self, user: str, password: str) -> ldap3.Connection: 72 | settings = self.settings_dict 73 | 74 | _debug("connecting") 75 | url = urlparse(settings['URI']) 76 | 77 | if url.scheme == "ldaps": 78 | use_ssl = True 79 | elif url.scheme == "ldap": 80 | use_ssl = False 81 | else: 82 | raise RuntimeError("Unknown scheme '%s'" % url.scheme) 83 | 84 | if ":" in url.netloc: 85 | host, port = url.netloc.split(":") 86 | port = int(port) 87 | else: 88 | host = url.netloc 89 | if use_ssl: 90 | port = 636 91 | else: 92 | port = 389 93 | 94 | start_tls = False 95 | if 'START_TLS' in settings and settings['START_TLS']: 96 | start_tls = True 97 | 98 | tls = None 99 | if use_ssl or start_tls: 100 | tls = ldap3.Tls() 101 | 102 | if 'CIPHERS' in settings: 103 | tls.ciphers = settings['CIPHERS'] 104 | 105 | if 'TLS_CA' in settings and settings['TLS_CA']: 106 | tls.ca_certs_file = settings['TLS_CA'] 107 | 108 | if 'REQUIRE_TLS' in settings and settings['REQUIRE_TLS']: 109 | tls.validate = ssl.CERT_REQUIRED 110 | 111 | s = ldap3.Server(host, port=port, use_ssl=use_ssl, tls=tls) 112 | c = self._connection_class( 113 | s, # client_strategy=ldap3.STRATEGY_SYNC_RESTARTABLE, 114 | user=user, password=password, authentication=ldap3.SIMPLE) 115 | c.strategy.restartable_sleep_time = 0 116 | c.strategy.restartable_tries = 1 117 | c.raise_exceptions = True 118 | 119 | c.open() 120 | 121 | if start_tls: 122 | c.start_tls() 123 | 124 | try: 125 | c.bind() 126 | except: # noqa: E722 127 | c.unbind() 128 | raise 129 | 130 | return c 131 | 132 | def _reconnect(self) -> None: 133 | settings = self.settings_dict 134 | try: 135 | self._obj = self._connect( 136 | user=settings['USER'], password=settings['PASSWORD']) 137 | except Exception: 138 | self._obj = None 139 | raise 140 | assert self._obj is not None 141 | 142 | def _do_with_retry(self, fn: Callable[[ldap3.Connection], Entity]) -> Entity: 143 | if self._obj is None: 144 | self._reconnect() 145 | assert self._obj is not None 146 | 147 | try: 148 | return fn(self._obj) 149 | except ldap3.core.exceptions.LDAPSessionTerminatedByServerError: 150 | # if it fails, reconnect then retry 151 | _debug("SERVER_DOWN, reconnecting") 152 | self._reconnect() 153 | return fn(self._obj) 154 | 155 | ################### 156 | # read only stuff # 157 | ################### 158 | 159 | def search(self, base, scope, filterstr='(objectClass=*)', 160 | attrlist=None, limit=None) -> Generator[Tuple[str, dict], None, None]: 161 | """ 162 | Search for entries in LDAP database. 163 | """ 164 | 165 | _debug("search", base, scope, filterstr, attrlist, limit) 166 | 167 | # first results 168 | if attrlist is None: 169 | attrlist = ldap3.ALL_ATTRIBUTES 170 | elif isinstance(attrlist, set): 171 | attrlist = list(attrlist) 172 | 173 | def first_results(obj): 174 | _debug("---> searching ldap", limit) 175 | obj.search( 176 | base, filterstr, scope, attributes=attrlist, paged_size=limit) 177 | return obj.response 178 | 179 | # get the 1st result 180 | result_list = self._do_with_retry(first_results) 181 | 182 | # Loop over list of search results 183 | for result_item in result_list: 184 | # skip searchResRef for now 185 | if result_item['type'] != "searchResEntry": 186 | continue 187 | dn = result_item['dn'] 188 | attributes = result_item['raw_attributes'] 189 | # did we already retrieve this from cache? 190 | _debug("---> got ldap result", dn) 191 | _debug("---> yielding", result_item) 192 | yield (dn, attributes) 193 | 194 | # we are finished - return results, eat cake 195 | _debug("---> done") 196 | return 197 | 198 | #################### 199 | # Cache Management # 200 | #################### 201 | 202 | def reset(self, force_flush_cache: bool = False) -> None: 203 | """ 204 | Reset transaction back to original state, discarding all 205 | uncompleted transactions. 206 | """ 207 | pass 208 | 209 | ########################## 210 | # Transaction Management # 211 | ########################## 212 | 213 | # Fake it 214 | 215 | def is_dirty(self) -> bool: 216 | """ Are there uncommitted changes? """ 217 | raise NotImplementedError() 218 | 219 | def is_managed(self) -> bool: 220 | """ Are we inside transaction management? """ 221 | raise NotImplementedError() 222 | 223 | def enter_transaction_management(self) -> None: 224 | """ Start a transaction. """ 225 | raise NotImplementedError() 226 | 227 | def leave_transaction_management(self) -> None: 228 | """ 229 | End a transaction. Must not be dirty when doing so. ie. commit() or 230 | rollback() must be called if changes made. If dirty, changes will be 231 | discarded. 232 | """ 233 | raise NotImplementedError() 234 | 235 | def commit(self) -> None: 236 | """ 237 | Attempt to commit all changes to LDAP database. i.e. forget all 238 | rollbacks. However stay inside transaction management. 239 | """ 240 | raise NotImplementedError() 241 | 242 | def rollback(self) -> None: 243 | """ 244 | Roll back to previous database state. However stay inside transaction 245 | management. 246 | """ 247 | raise NotImplementedError() 248 | 249 | ################################## 250 | # Functions needing Transactions # 251 | ################################## 252 | 253 | def add(self, dn: str, mod_list: dict) -> None: 254 | """ 255 | Add a DN to the LDAP database; See ldap module. Doesn't return a result 256 | if transactions enabled. 257 | """ 258 | raise NotImplementedError() 259 | 260 | def modify(self, dn: str, mod_list: dict) -> None: 261 | """ 262 | Modify a DN in the LDAP database; See ldap module. Doesn't return a 263 | result if transactions enabled. 264 | """ 265 | raise NotImplementedError() 266 | 267 | def modify_no_rollback(self, dn: str, mod_list: dict) -> None: 268 | """ 269 | Modify a DN in the LDAP database; See ldap module. Doesn't return a 270 | result if transactions enabled. 271 | """ 272 | raise NotImplementedError() 273 | 274 | def delete(self, dn: str) -> None: 275 | """ 276 | delete a dn in the ldap database; see ldap module. doesn't return a 277 | result if transactions enabled. 278 | """ 279 | raise NotImplementedError() 280 | 281 | def rename(self, dn: str, new_rdn: str, new_base_dn: Optional[str] = None) -> None: 282 | """ 283 | rename a dn in the ldap database; see ldap module. doesn't return a 284 | result if transactions enabled. 285 | """ 286 | raise NotImplementedError() 287 | -------------------------------------------------------------------------------- /tldap/backend/no_transactions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | """ This module provides the LDAP functions with transaction support disabled, 19 | with a subset of the functions from the real ldap module. """ 20 | from typing import Optional 21 | 22 | from .base import LdapBase 23 | 24 | 25 | # wrapper class 26 | 27 | class LDAPwrapper(LdapBase): 28 | """ The LDAP connection class. """ 29 | 30 | #################### 31 | # Cache Management # 32 | #################### 33 | 34 | def reset(self, force_flush_cache: bool = False) -> None: 35 | """ 36 | Reset transaction back to original state, discarding all 37 | uncompleted transactions. 38 | """ 39 | pass 40 | 41 | ########################## 42 | # Transaction Management # 43 | ########################## 44 | 45 | # Fake it 46 | 47 | def is_dirty(self) -> bool: 48 | """ Are there uncommitted changes? """ 49 | return False 50 | 51 | def is_managed(self) -> bool: 52 | """ Are we inside transaction management? """ 53 | return False 54 | 55 | def enter_transaction_management(self) -> None: 56 | """ Start a transaction. """ 57 | pass 58 | 59 | def leave_transaction_management(self) -> None: 60 | """ 61 | End a transaction. Must not be dirty when doing so. ie. commit() or 62 | rollback() must be called if changes made. If dirty, changes will be 63 | discarded. 64 | """ 65 | pass 66 | 67 | def commit(self) -> None: 68 | """ 69 | Attempt to commit all changes to LDAP database. i.e. forget all 70 | rollbacks. However stay inside transaction management. 71 | """ 72 | pass 73 | 74 | def rollback(self) -> None: 75 | """ 76 | Roll back to previous database state. However stay inside transaction 77 | management. 78 | """ 79 | pass 80 | 81 | ################################## 82 | # Functions needing Transactions # 83 | ################################## 84 | 85 | def add(self, dn: str, mod_list: dict) -> None: 86 | """ 87 | Add a DN to the LDAP database; See ldap module. Doesn't return a result 88 | if transactions enabled. 89 | """ 90 | 91 | return self._do_with_retry(lambda obj: obj.add_s(dn, mod_list)) 92 | 93 | def modify(self, dn: str, mod_list: dict) -> None: 94 | """ 95 | Modify a DN in the LDAP database; See ldap module. Doesn't return a 96 | result if transactions enabled. 97 | """ 98 | 99 | return self._do_with_retry(lambda obj: obj.modify_s(dn, mod_list)) 100 | 101 | def modify_no_rollback(self, dn: str, mod_list: dict) -> None: 102 | """ 103 | Modify a DN in the LDAP database; See ldap module. Doesn't return a 104 | result if transactions enabled. 105 | """ 106 | 107 | return self._do_with_retry(lambda obj: obj.modify_s(dn, mod_list)) 108 | 109 | def delete(self, dn: str) -> None: 110 | """ 111 | delete a dn in the ldap database; see ldap module. doesn't return a 112 | result if transactions enabled. 113 | """ 114 | 115 | return self._do_with_retry(lambda obj: obj.delete_s(dn)) 116 | 117 | def rename(self, dn: str, new_rdn: str, new_base_dn: Optional[str] = None) -> None: 118 | """ 119 | rename a dn in the ldap database; see ldap module. doesn't return a 120 | result if transactions enabled. 121 | """ 122 | 123 | return self._do_with_retry( 124 | lambda obj: obj.rename_s(dn, new_rdn, new_base_dn)) 125 | -------------------------------------------------------------------------------- /tldap/dict.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | """ Dictionary related classes. """ 18 | from typing import Dict, ItemsView, KeysView, Optional, Set, TypeVar 19 | 20 | 21 | Entity = TypeVar('Entity', bound='CaseInsensitiveDict') 22 | 23 | 24 | class CaseInsensitiveDict: 25 | """ 26 | Case insensitve dictionary for searches however preserves the case for 27 | retrieval. Needs to be supplied with a set of allowed keys. 28 | """ 29 | 30 | def __init__(self, allowed_keys: Set[str], d: Optional[dict] = None) -> None: 31 | self._lc: Dict[str, str] = { 32 | value.lower(): value for value in allowed_keys 33 | } 34 | self._dict = dict() 35 | if d is not None: 36 | for k, v in d.items(): 37 | self[k] = v 38 | 39 | def fix_key(self, key: str) -> str: 40 | key = key.lower() 41 | 42 | if key not in self._lc: 43 | raise KeyError(key) 44 | 45 | return self._lc[key.lower()] 46 | 47 | def __setitem__(self, key: str, value: any): 48 | key = self.fix_key(key) 49 | self._dict.__setitem__(key, value) 50 | 51 | def __delitem__(self, key: str): 52 | key = self.fix_key(key) 53 | del self._lc[key] 54 | self._dict.__delitem__(key) 55 | 56 | def __getitem__(self, key: str): 57 | key = self.fix_key(key) 58 | return self._dict.__getitem__(key) 59 | 60 | def __contains__(self, key: str): 61 | key = self.fix_key(key) 62 | return self._dict.__contains__(key) 63 | 64 | def get(self, key: str, default: any = None): 65 | key = self.fix_key(key) 66 | return self._dict.get(key, default) 67 | 68 | def keys(self) -> KeysView[str]: 69 | return self._dict.keys() 70 | 71 | def items(self) -> ItemsView[str, any]: 72 | return self._dict.items() 73 | 74 | def to_dict(self) -> dict: 75 | return self._dict 76 | 77 | 78 | ImmutableDictEntity = TypeVar('ImmutableDictEntity', bound='ImmutableDict') 79 | 80 | 81 | class ImmutableDict: 82 | """ 83 | Immutable dictionary that cannot be changed without creating a new instance. 84 | """ 85 | def __init__(self, allowed_keys: Optional[Set[str]] = None, d: Optional[dict] = None) -> None: 86 | self._allowed_keys = allowed_keys 87 | self._dict = CaseInsensitiveDict(allowed_keys) 88 | if d is not None: 89 | for key, value in d.items(): 90 | self._set(key, value) 91 | 92 | def fix_key(self, key: str) -> str: 93 | return self._dict.fix_key(key) 94 | 95 | def __getitem__(self, key: str): 96 | return self._dict.__getitem__(key) 97 | 98 | def get(self, key: str, default: any = None): 99 | key = self.fix_key(key) 100 | try: 101 | return self._dict.get(key, default) 102 | except KeyError: 103 | return default 104 | 105 | def __contains__(self, key: str): 106 | return self._dict.__contains__(key) 107 | 108 | def keys(self) -> KeysView[str]: 109 | return self._dict.keys() 110 | 111 | def items(self) -> ItemsView[str, any]: 112 | return self._dict.items() 113 | 114 | def __copy__(self: ImmutableDictEntity) -> ImmutableDictEntity: 115 | return self.__class__(self._allowed_keys, self._dict) 116 | 117 | def _set(self, key: str, value: any) -> None: 118 | self._dict[key] = value 119 | 120 | def merge(self: ImmutableDictEntity, d: dict) -> ImmutableDictEntity: 121 | clone = self.__copy__() 122 | for key, value in d.items(): 123 | clone._set(key, value) 124 | return clone 125 | 126 | def set(self: ImmutableDictEntity, key: str, value: any) -> ImmutableDictEntity: 127 | clone = self.__copy__() 128 | clone._set(key, value) 129 | return clone 130 | 131 | def to_dict(self) -> dict: 132 | return self._dict.to_dict() 133 | -------------------------------------------------------------------------------- /tldap/django/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | """ Django function support. """ 19 | 20 | from __future__ import absolute_import 21 | 22 | import django.conf 23 | 24 | from tldap.backend import setup 25 | from tldap.utils import DEFAULT_LDAP_ALIAS 26 | 27 | 28 | default_app_config = 'tldap.django.apps.TldapConfig' 29 | 30 | # For backwards compatibility - Port any old database settings over to 31 | # the new values. 32 | if not hasattr(django.conf.settings, 'LDAP'): 33 | django.conf.settings.LDAP = {} 34 | 35 | # ok to use django settings 36 | if not django.conf.settings.LDAP and hasattr(django.conf.settings, 'LDAP_URL'): 37 | django.conf.settings.LDAP[DEFAULT_LDAP_ALIAS] = { 38 | 'ENGINE': 'tldap.backend.fake_transactions', 39 | 'URI': django.conf.settings.LDAP_URL, 40 | 'USER': django.conf.settings.LDAP_ADMIN_USER, 41 | 'PASSWORD': django.conf.settings.LDAP_ADMIN_PASSWORD, 42 | 'START_TLS': False, 43 | 'TLS_CA': None, 44 | 'LDAP_ACCOUNT_BASE': django.conf.settings.LDAP_USER_BASE, 45 | 'LDAP_GROUP_BASE': django.conf.settings.LDAP_GROUP_BASE, 46 | } 47 | if hasattr(django.conf.settings, 'LDAP_USE_TLS'): 48 | django.conf.settings.LDAP[DEFAULT_LDAP_ALIAS]["START_TLS"] = ( 49 | django.conf.settings.LDAP_USE_TLS) 50 | django.conf.settings.LDAP[DEFAULT_LDAP_ALIAS]["TLS_CA"] = ( 51 | django.conf.settings.LDAP_TLS_CA) 52 | 53 | setup(django.conf.settings.LDAP) 54 | -------------------------------------------------------------------------------- /tldap/django/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TldapConfig(AppConfig): 5 | name = 'tldap.django' 6 | label = 'tldap' 7 | verbose_name = "TLDAP Django Support" 8 | -------------------------------------------------------------------------------- /tldap/django/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | """ Django specific database helper functions. """ 19 | 20 | from tldap import Q 21 | from tldap.database import Changeset, Database, LdapObjectClass, get_one 22 | from tldap.django.models import Counters 23 | from tldap.exceptions import ObjectDoesNotExist 24 | 25 | 26 | def _check_exists(database: Database, table: LdapObjectClass, key: str, value: str): 27 | """ Check if a given LDAP object exists. """ 28 | try: 29 | get_one(table, Q(**{key: value}), database=database) 30 | return True 31 | except ObjectDoesNotExist: 32 | return False 33 | 34 | 35 | def save_account(changes: Changeset, table: LdapObjectClass, database: Database) -> Changeset: 36 | """ Modify a changes to add an automatically generated uidNumber. """ 37 | d = {} 38 | settings = database.settings 39 | 40 | uid_number = changes.get_value_as_single('uidNumber') 41 | if uid_number is None: 42 | scheme = settings['NUMBER_SCHEME'] 43 | first = settings.get('UID_FIRST', 10000) 44 | d['uidNumber'] = Counters.get_and_increment( 45 | scheme, "uidNumber", first, 46 | lambda n: not _check_exists(database, table, 'uidNumber', n) 47 | ) 48 | 49 | changes = changes.merge(d) 50 | return changes 51 | 52 | 53 | def save_group(changes: Changeset, table: LdapObjectClass, database: Database) -> Changeset: 54 | """ Modify a changes to add an automatically generated gidNumber. """ 55 | d = {} 56 | settings = database.settings 57 | 58 | gid_number = changes.get_value_as_single('gidNumber') 59 | if gid_number is None: 60 | scheme = settings['NUMBER_SCHEME'] 61 | first = settings.get('GID_FIRST', 10000) 62 | d['gidNumber'] = Counters.get_and_increment( 63 | scheme, "gidNumber", first, 64 | lambda n: not _check_exists(database, table, 'gidNumber', n) 65 | ) 66 | 67 | changes = changes.merge(d) 68 | return changes 69 | -------------------------------------------------------------------------------- /tldap/django/middleware.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | """ Transaction middleware for Django. """ 19 | 20 | from django.utils.deprecation import MiddlewareMixin 21 | 22 | import tldap.transaction 23 | 24 | 25 | class TransactionMiddleware(MiddlewareMixin): 26 | """ 27 | Transaction middleware. If this is enabled, each view function will be run 28 | with commit_on_response activated - that way a save() doesn't do a direct 29 | commit, the commit is done when a successful response is created. If an 30 | exception happens, the database is rolled back. 31 | """ 32 | def process_request(self, request): 33 | """Enters transaction management""" 34 | tldap.transaction.enter_transaction_management() 35 | 36 | def process_exception(self, request, exception): 37 | """Rolls back the database and leaves transaction management""" 38 | tldap.transaction.rollback() 39 | 40 | def process_response(self, request, response): 41 | """Commits and leaves transaction management.""" 42 | if tldap.transaction.is_managed(): 43 | tldap.transaction.commit() 44 | tldap.transaction.leave_transaction_management() 45 | return response 46 | -------------------------------------------------------------------------------- /tldap/django/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Counters', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, 17 | auto_created=True, primary_key=True)), 18 | ('scheme', models.CharField(max_length=20, db_index=True)), 19 | ('name', models.CharField(max_length=20, db_index=True)), 20 | ('count', models.IntegerField()), 21 | ], 22 | options={ 23 | 'db_table': 'tldap_counters', 24 | }, 25 | bases=(models.Model,), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /tldap/django/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Karaage-Cluster/python-tldap/6cb877b28e07c0fd78a449e4c1b140fcab0117ef/tldap/django/migrations/__init__.py -------------------------------------------------------------------------------- /tldap/django/models.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | """ DB model for a counter to keep track of next uidNumber and gidNumber to use 19 | for new LDAP objects. """ 20 | 21 | from django.db import models, transaction 22 | 23 | 24 | class Counters(models.Model): 25 | """ Keep track of next uidNumber and gidNumber to use for new LDAP objects. 26 | """ 27 | scheme = models.CharField(max_length=20, db_index=True) 28 | name = models.CharField(max_length=20, db_index=True) 29 | count = models.IntegerField() 30 | 31 | class Meta: 32 | db_table = 'tldap_counters' 33 | 34 | @classmethod 35 | @transaction.atomic 36 | def get_and_increment(cls, scheme, name, default, test): 37 | entry, c = cls.objects.select_for_update().get_or_create( 38 | scheme=scheme, name=name, defaults={'count': default}) 39 | 40 | while not test(entry.count): 41 | entry.count = entry.count + 1 42 | 43 | n = entry.count 44 | 45 | entry.count = entry.count + 1 46 | entry.save() 47 | 48 | return n 49 | -------------------------------------------------------------------------------- /tldap/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | """ Various TLDAP exceptions. """ 18 | 19 | 20 | class InvalidDN(Exception): 21 | """ DN value is invalid and cannot be parsed. """ 22 | 23 | 24 | class TestFailure(Exception): 25 | """Simulated failure for testing.""" 26 | pass 27 | 28 | 29 | class FieldError(Exception): 30 | """Some kind of problem with a field.""" 31 | pass 32 | 33 | 34 | class ObjectDoesNotExist(Exception): 35 | "The requested object does not exist" 36 | pass 37 | 38 | 39 | class MultipleObjectsReturned(Exception): 40 | "The query returned multiple objects when only one was expected." 41 | pass 42 | 43 | 44 | class ObjectAlreadyExists(Exception): 45 | "The requested object already exists" 46 | pass 47 | 48 | 49 | class ValidationError(Exception): 50 | """An error while validating data.""" 51 | pass 52 | 53 | 54 | class RollbackError(Exception): 55 | """An error in rollback and consistency cannot be guaranteed.""" 56 | pass 57 | -------------------------------------------------------------------------------- /tldap/filter.py: -------------------------------------------------------------------------------- 1 | """ 2 | filters.py - misc stuff for handling LDAP filter strings (see RFC2254) 3 | 4 | """ 5 | import six 6 | 7 | 8 | def escape_filter_chars(assertion_value, escape_mode=0): 9 | """ 10 | Replace all special characters found in assertion_value 11 | by quoted notation. 12 | 13 | escape_mode 14 | If 0 only special chars mentioned in RFC 4515 are escaped. 15 | If 1 all NON-ASCII chars are escaped. 16 | If 2 all chars are escaped. 17 | """ 18 | 19 | if isinstance(assertion_value, six.text_type): 20 | assertion_value = assertion_value.encode("utf_8") 21 | 22 | s = [] 23 | for c in assertion_value: 24 | do_escape = False 25 | 26 | if str != bytes: # Python 3 27 | pass 28 | else: # Python 2 29 | c = ord(c) 30 | 31 | if escape_mode == 0: 32 | if c == ord('\\') or c == ord('*') \ 33 | or c == ord('(') or c == ord(')') \ 34 | or c == ord('\x00'): 35 | do_escape = True 36 | elif escape_mode == 1: 37 | if c < '0' or c > 'z' or c in "\\*()": 38 | do_escape = True 39 | elif escape_mode == 2: 40 | do_escape = True 41 | else: 42 | raise ValueError('escape_mode must be 0, 1 or 2.') 43 | 44 | if do_escape: 45 | s.append(b"\\%02x" % c) 46 | else: 47 | b = None 48 | if str != bytes: # Python 3 49 | b = bytes([c]) 50 | else: # Python 2 51 | b = chr(c) 52 | s.append(b) 53 | 54 | return b''.join(s) 55 | 56 | 57 | def filter_format(filter_template, assertion_values): 58 | """ 59 | filter_template 60 | String containing %s as placeholder for assertion values. 61 | assertion_values 62 | List or tuple of assertion values. Length must match 63 | count of %s in filter_template. 64 | """ 65 | assert isinstance(filter_template, bytes) 66 | return filter_template % ( 67 | tuple(map(escape_filter_chars, assertion_values))) 68 | -------------------------------------------------------------------------------- /tldap/ldap_passwd.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2018 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | """ Hash and check passwords. """ 19 | from passlib.context import CryptContext 20 | 21 | 22 | pwd_context = CryptContext( 23 | schemes=[ 24 | "ldap_sha512_crypt", 25 | "ldap_salted_sha1", 26 | "ldap_md5", 27 | "ldap_sha1", 28 | "ldap_salted_md5", 29 | "ldap_des_crypt", 30 | "ldap_md5_crypt", 31 | ], 32 | default="ldap_sha512_crypt", 33 | ) 34 | 35 | 36 | def check_password(password: str, encrypted: str) -> bool: 37 | """ Check a plaintext password against a hashed password. """ 38 | # some old passwords have {crypt} in lower case, and passlib wants it to be 39 | # in upper case. 40 | if encrypted.startswith("{crypt}"): 41 | encrypted = "{CRYPT}" + encrypted[7:] 42 | return pwd_context.verify(password, encrypted) 43 | 44 | 45 | def encode_password(password: str) -> str: 46 | """ Encode a password. """ 47 | return pwd_context.hash(password) 48 | -------------------------------------------------------------------------------- /tldap/modlist.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | """ 19 | This module contains a ``modifyModlist`` function adopted from 20 | :py:mod:`ldap:ldap.modlist`. 21 | """ 22 | from typing import Dict, Iterator, List, Optional, Tuple 23 | 24 | import ldap3 25 | import ldap3.utils.conv 26 | 27 | import tldap.dict 28 | 29 | 30 | def _list_dict(line: Iterator[str], case_insensitive: bool = False): 31 | """ 32 | return a dictionary with all items of l being the keys of the dictionary 33 | 34 | If argument case_insensitive is non-zero ldap.cidict.cidict will be 35 | used for case-insensitive string keys 36 | """ 37 | if case_insensitive: 38 | raise NotImplementedError() 39 | d = tldap.dict.CaseInsensitiveDict() 40 | else: 41 | d = {} 42 | for i in line: 43 | d[i] = None 44 | return d 45 | 46 | 47 | def escape_list(bytes_list): 48 | assert isinstance(bytes_list, list) 49 | return bytes_list 50 | 51 | 52 | def addModlist(entry: dict, ignore_attr_types: Optional[List[str]] = None) -> Dict[str, List[bytes]]: 53 | """Build modify list for call of method LDAPObject.add()""" 54 | ignore_attr_types = _list_dict(map(str.lower, (ignore_attr_types or []))) 55 | modlist: Dict[str, List[bytes]] = {} 56 | for attrtype in entry.keys(): 57 | if attrtype.lower() in ignore_attr_types: 58 | # This attribute type is ignored 59 | continue 60 | for value in entry[attrtype]: 61 | assert value is not None 62 | if len(entry[attrtype]) > 0: 63 | modlist[attrtype] = escape_list(entry[attrtype]) 64 | return modlist # addModlist() 65 | 66 | 67 | def modifyModlist( 68 | old_entry: dict, new_entry: dict, ignore_attr_types: Optional[List[str]] = None, 69 | ignore_oldexistent: bool = False) -> Dict[str, Tuple[str, List[bytes]]]: 70 | """ 71 | Build differential modify list for calling LDAPObject.modify()/modify_s() 72 | 73 | :param old_entry: 74 | Dictionary holding the old entry 75 | :param new_entry: 76 | Dictionary holding what the new entry should be 77 | :param ignore_attr_types: 78 | List of attribute type names to be ignored completely 79 | :param ignore_oldexistent: 80 | If true attribute type names which are in old_entry 81 | but are not found in new_entry at all are not deleted. 82 | This is handy for situations where your application 83 | sets attribute value to '' for deleting an attribute. 84 | In most cases leave zero. 85 | 86 | :return: List of tuples suitable for 87 | :py:meth:`ldap:ldap.LDAPObject.modify`. 88 | 89 | This function is the same as :py:func:`ldap:ldap.modlist.modifyModlist` 90 | except for the following changes: 91 | 92 | * MOD_DELETE/MOD_DELETE used in preference to MOD_REPLACE when updating 93 | an existing value. 94 | """ 95 | ignore_attr_types = _list_dict(map(str.lower, (ignore_attr_types or []))) 96 | modlist: Dict[str, Tuple[str, List[bytes]]] = {} 97 | attrtype_lower_map = {} 98 | for a in old_entry.keys(): 99 | attrtype_lower_map[a.lower()] = a 100 | for attrtype in new_entry.keys(): 101 | attrtype_lower = attrtype.lower() 102 | if attrtype_lower in ignore_attr_types: 103 | # This attribute type is ignored 104 | continue 105 | # Filter away null-strings 106 | new_value = list(filter(lambda x: x is not None, new_entry[attrtype])) 107 | if attrtype_lower in attrtype_lower_map: 108 | old_value = old_entry.get(attrtype_lower_map[attrtype_lower], []) 109 | old_value = list(filter(lambda x: x is not None, old_value)) 110 | del attrtype_lower_map[attrtype_lower] 111 | else: 112 | old_value = [] 113 | if not old_value and new_value: 114 | # Add a new attribute to entry 115 | modlist[attrtype] = (ldap3.MODIFY_ADD, escape_list(new_value)) 116 | elif old_value and new_value: 117 | # Replace existing attribute 118 | old_value_dict = _list_dict(old_value) 119 | new_value_dict = _list_dict(new_value) 120 | 121 | delete_values = [] 122 | for v in old_value: 123 | if v not in new_value_dict: 124 | delete_values.append(v) 125 | 126 | add_values = [] 127 | for v in new_value: 128 | if v not in old_value_dict: 129 | add_values.append(v) 130 | 131 | if len(delete_values) > 0 or len(add_values) > 0: 132 | modlist[attrtype] = ( 133 | ldap3.MODIFY_REPLACE, escape_list(new_value)) 134 | 135 | elif old_value and not new_value: 136 | # Completely delete an existing attribute 137 | modlist[attrtype] = (ldap3.MODIFY_DELETE, []) 138 | if not ignore_oldexistent: 139 | # Remove all attributes of old_entry which are not present 140 | # in new_entry at all 141 | for a in attrtype_lower_map.keys(): 142 | if a in ignore_attr_types: 143 | # This attribute type is ignored 144 | continue 145 | attrtype = attrtype_lower_map[a] 146 | modlist[attrtype] = (ldap3.MODIFY_DELETE, []) 147 | return modlist # modifyModlist() 148 | -------------------------------------------------------------------------------- /tldap/query.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | from typing import Dict, Iterator, Optional, Set, Tuple 18 | 19 | import ldap3 20 | from ldap3.core.exceptions import LDAPNoSuchObjectResult 21 | 22 | import tldap 23 | import tldap.fields 24 | from tldap.backend.base import LdapBase 25 | from tldap.filter import filter_format 26 | 27 | 28 | def get_filter_item(name: str, operation: bytes, value: bytes) -> bytes: 29 | """ 30 | A field could be found for this term, try to get filter string for it. 31 | """ 32 | assert isinstance(name, str) 33 | assert isinstance(value, bytes) 34 | if operation is None: 35 | return filter_format(b"(%s=%s)", [name, value]) 36 | elif operation == "contains": 37 | assert value != "" 38 | return filter_format(b"(%s=*%s*)", [name, value]) 39 | else: 40 | raise ValueError("Unknown search operation %s" % operation) 41 | 42 | 43 | def get_filter(q: tldap.Q, fields: Dict[str, tldap.fields.Field], pk: str): 44 | """ 45 | Translate the Q tree into a filter string to search for, or None 46 | if no results possible. 47 | """ 48 | # check the details are valid 49 | if q.negated and len(q.children) == 1: 50 | op = b"!" 51 | elif q.connector == tldap.Q.AND: 52 | op = b"&" 53 | elif q.connector == tldap.Q.OR: 54 | op = b"|" 55 | else: 56 | raise ValueError("Invalid value of op found") 57 | 58 | # scan through every child 59 | search = [] 60 | for child in q.children: 61 | # if this child is a node, then descend into it 62 | if isinstance(child, tldap.Q): 63 | search.append(get_filter(child, fields, pk)) 64 | else: 65 | # otherwise get the values in this node 66 | name, value = child 67 | 68 | # split the name if possible 69 | name, _, operation = name.rpartition("__") 70 | if name == "": 71 | name, operation = operation, None 72 | 73 | # replace pk with the real attribute 74 | if name == "pk": 75 | name = pk 76 | 77 | # DN is a special case 78 | if name == "dn": 79 | dn_name = "entryDN:" 80 | if isinstance(value, list): 81 | s = [] 82 | for v in value: 83 | assert isinstance(v, str) 84 | v = v.encode('utf_8') 85 | s.append(get_filter_item(dn_name, operation, v)) 86 | search.append("(&".join(search) + ")") 87 | 88 | # or process just the single value 89 | else: 90 | assert isinstance(value, str) 91 | v = value.encode('utf_8') 92 | search.append(get_filter_item(dn_name, operation, v)) 93 | continue 94 | 95 | # try to find field associated with name 96 | field = fields[name] 97 | if isinstance(value, list) and len(value) == 1: 98 | value = value[0] 99 | assert isinstance(value, str) 100 | 101 | # process as list 102 | if isinstance(value, list): 103 | s = [] 104 | for v in value: 105 | v = field.value_to_filter(v) 106 | s.append(get_filter_item(name, operation, v)) 107 | search.append(b"(&".join(search) + b")") 108 | 109 | # or process just the single value 110 | else: 111 | value = field.value_to_filter(value) 112 | search.append(get_filter_item(name, operation, value)) 113 | 114 | # output the results 115 | if len(search) == 1 and not q.negated: 116 | # just one non-negative term, return it 117 | return search[0] 118 | else: 119 | # multiple terms 120 | return b"(" + op + b"".join(search) + b")" 121 | 122 | 123 | def _get_search_params(query: Optional[tldap.Q], fields: Dict[str, tldap.fields.Field], 124 | object_classes: Set[str], pk: str): 125 | # add object classes to search array 126 | oc_query = tldap.Q() 127 | for oc in sorted(object_classes): 128 | oc_query = oc_query & tldap.Q(objectClass=oc) 129 | 130 | if query is None: 131 | query = oc_query 132 | else: 133 | query = oc_query & query 134 | 135 | # do a SUBTREE search 136 | scope = ldap3.SUBTREE 137 | 138 | # construct search filter string 139 | if query is not None: 140 | search_filter = get_filter(query, fields, pk) 141 | else: 142 | search_filter = None 143 | 144 | return scope, search_filter 145 | 146 | 147 | def search( 148 | connection: LdapBase, query: Optional[tldap.Q], fields: Dict[str, tldap.fields.Field], 149 | base_dn: str, object_classes: Set[str], pk: str) -> Iterator[Tuple[str, dict]]: 150 | field_names = list(fields.keys()) 151 | 152 | scope, search_filter = _get_search_params(query, fields, object_classes, pk) 153 | 154 | try: 155 | results = connection.search(base_dn, scope, search_filter, field_names) 156 | for result in results: 157 | dn = result[0] 158 | data = result[1] 159 | yield dn, data 160 | except LDAPNoSuchObjectResult: 161 | pass 162 | -------------------------------------------------------------------------------- /tldap/query_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | from __future__ import absolute_import 18 | 19 | import six 20 | 21 | from .tree import Node 22 | 23 | 24 | class Q(Node): 25 | """ 26 | Encapsulates filters as objects that can then be combined logically 27 | (using ``&`` and ``|``). 28 | """ 29 | # Connection types 30 | AND = 'AND' 31 | OR = 'OR' 32 | default = AND 33 | 34 | def __init__(self, *args, **kwargs): 35 | super(Q, self).__init__( 36 | children=list(args) + list(six.iteritems(kwargs))) 37 | 38 | def _combine(self, other: 'Q', conn: str) -> 'Q': 39 | if not isinstance(other, Q): 40 | raise TypeError(other) 41 | if len(self.children) < 1: 42 | self.connector = conn 43 | obj = type(self)() 44 | obj.connector = conn 45 | obj.add(self, conn) 46 | obj.add(other, conn) 47 | return obj 48 | 49 | def __or__(self, other: 'Q'): 50 | return self._combine(other, self.OR) 51 | 52 | def __and__(self, other: 'Q'): 53 | return self._combine(other, self.AND) 54 | 55 | def __invert__(self): 56 | obj = type(self)() 57 | obj.add(self, self.AND) 58 | obj.negate() 59 | return obj 60 | -------------------------------------------------------------------------------- /tldap/test/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | -------------------------------------------------------------------------------- /tldap/test/ldap_schemas/50-inetorgperson.schema: -------------------------------------------------------------------------------- 1 | # inetorgperson.schema -- InetOrgPerson (RFC2798) 2 | # $OpenLDAP: pkg/ldap/servers/slapd/schema/inetorgperson.schema,v 1.18.2.3 2008/02/11 23:26:49 kurt Exp $ 3 | ## This work is part of OpenLDAP Software . 4 | ## 5 | ## Copyright 1998-2008 The OpenLDAP Foundation. 6 | ## All rights reserved. 7 | ## 8 | ## Redistribution and use in source and binary forms, with or without 9 | ## modification, are permitted only as authorized by the OpenLDAP 10 | ## Public License. 11 | ## 12 | ## A copy of this license is available in the file LICENSE in the 13 | ## top-level directory of the distribution or, alternatively, at 14 | ## . 15 | # 16 | # InetOrgPerson (RFC2798) 17 | # 18 | # Depends upon 19 | # Definition of an X.500 Attribute Type and an Object Class to Hold 20 | # Uniform Resource Identifiers (URIs) [RFC2079] 21 | # (core.schema) 22 | # 23 | # A Summary of the X.500(96) User Schema for use with LDAPv3 [RFC2256] 24 | # (core.schema) 25 | # 26 | # The COSINE and Internet X.500 Schema [RFC1274] (cosine.schema) 27 | 28 | # carLicense 29 | # This multivalued field is used to record the values of the license or 30 | # registration plate associated with an individual. 31 | attributetype ( 2.16.840.1.113730.3.1.1 32 | NAME 'carLicense' 33 | DESC 'RFC2798: vehicle license or registration plate' 34 | EQUALITY caseIgnoreMatch 35 | SUBSTR caseIgnoreSubstringsMatch 36 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) 37 | 38 | # departmentNumber 39 | # Code for department to which a person belongs. This can also be 40 | # strictly numeric (e.g., 1234) or alphanumeric (e.g., ABC/123). 41 | attributetype ( 2.16.840.1.113730.3.1.2 42 | NAME 'departmentNumber' 43 | DESC 'RFC2798: identifies a department within an organization' 44 | EQUALITY caseIgnoreMatch 45 | SUBSTR caseIgnoreSubstringsMatch 46 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) 47 | 48 | # displayName 49 | # When displaying an entry, especially within a one-line summary list, it 50 | # is useful to be able to identify a name to be used. Since other attri- 51 | # bute types such as 'cn' are multivalued, an additional attribute type is 52 | # needed. Display name is defined for this purpose. 53 | attributetype ( 2.16.840.1.113730.3.1.241 54 | NAME 'displayName' 55 | DESC 'RFC2798: preferred name to be used when displaying entries' 56 | EQUALITY caseIgnoreMatch 57 | SUBSTR caseIgnoreSubstringsMatch 58 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 59 | SINGLE-VALUE ) 60 | 61 | # employeeNumber 62 | # Numeric or alphanumeric identifier assigned to a person, typically based 63 | # on order of hire or association with an organization. Single valued. 64 | attributetype ( 2.16.840.1.113730.3.1.3 65 | NAME 'employeeNumber' 66 | DESC 'RFC2798: numerically identifies an employee within an organization' 67 | EQUALITY caseIgnoreMatch 68 | SUBSTR caseIgnoreSubstringsMatch 69 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 70 | SINGLE-VALUE ) 71 | 72 | # employeeType 73 | # Used to identify the employer to employee relationship. Typical values 74 | # used will be "Contractor", "Employee", "Intern", "Temp", "External", and 75 | # "Unknown" but any value may be used. 76 | attributetype ( 2.16.840.1.113730.3.1.4 77 | NAME 'employeeType' 78 | DESC 'RFC2798: type of employment for a person' 79 | EQUALITY caseIgnoreMatch 80 | SUBSTR caseIgnoreSubstringsMatch 81 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) 82 | 83 | # jpegPhoto 84 | # Used to store one or more images of a person using the JPEG File 85 | # Interchange Format [JFIF]. 86 | # Note that the jpegPhoto attribute type was defined for use in the 87 | # Internet X.500 pilots but no referencable definition for it could be 88 | # located. 89 | attributetype ( 0.9.2342.19200300.100.1.60 90 | NAME 'jpegPhoto' 91 | DESC 'RFC2798: a JPEG image' 92 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 ) 93 | 94 | # preferredLanguage 95 | # Used to indicate an individual's preferred written or spoken 96 | # language. This is useful for international correspondence or human- 97 | # computer interaction. Values for this attribute type MUST conform to 98 | # the definition of the Accept-Language header field defined in 99 | # [RFC2068] with one exception: the sequence "Accept-Language" ":" 100 | # should be omitted. This is a single valued attribute type. 101 | attributetype ( 2.16.840.1.113730.3.1.39 102 | NAME 'preferredLanguage' 103 | DESC 'RFC2798: preferred written or spoken language for a person' 104 | EQUALITY caseIgnoreMatch 105 | SUBSTR caseIgnoreSubstringsMatch 106 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 107 | SINGLE-VALUE ) 108 | 109 | # userSMIMECertificate 110 | # A PKCS#7 [RFC2315] SignedData, where the content that is signed is 111 | # ignored by consumers of userSMIMECertificate values. It is 112 | # recommended that values have a `contentType' of data with an absent 113 | # `content' field. Values of this attribute contain a person's entire 114 | # certificate chain and an smimeCapabilities field [RFC2633] that at a 115 | # minimum describes their SMIME algorithm capabilities. Values for 116 | # this attribute are to be stored and requested in binary form, as 117 | # 'userSMIMECertificate;binary'. If available, this attribute is 118 | # preferred over the userCertificate attribute for S/MIME applications. 119 | ## OpenLDAP note: ";binary" transfer should NOT be used as syntax is binary 120 | attributetype ( 2.16.840.1.113730.3.1.40 121 | NAME 'userSMIMECertificate' 122 | DESC 'RFC2798: PKCS#7 SignedData used to support S/MIME' 123 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 ) 124 | 125 | # userPKCS12 126 | # PKCS #12 [PKCS12] provides a format for exchange of personal identity 127 | # information. When such information is stored in a directory service, 128 | # the userPKCS12 attribute should be used. This attribute is to be stored 129 | # and requested in binary form, as 'userPKCS12;binary'. The attribute 130 | # values are PFX PDUs stored as binary data. 131 | ## OpenLDAP note: ";binary" transfer should NOT be used as syntax is binary 132 | attributetype ( 2.16.840.1.113730.3.1.216 133 | NAME 'userPKCS12' 134 | DESC 'RFC2798: personal identity information, a PKCS #12 PFX' 135 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 ) 136 | 137 | 138 | # inetOrgPerson 139 | # The inetOrgPerson represents people who are associated with an 140 | # organization in some way. It is a structural class and is derived 141 | # from the organizationalPerson which is defined in X.521 [X521]. 142 | objectclass ( 2.16.840.1.113730.3.2.2 143 | NAME 'inetOrgPerson' 144 | DESC 'RFC2798: Internet Organizational Person' 145 | SUP organizationalPerson 146 | STRUCTURAL 147 | MAY ( 148 | audio $ businessCategory $ carLicense $ departmentNumber $ 149 | displayName $ employeeNumber $ employeeType $ givenName $ 150 | homePhone $ homePostalAddress $ initials $ jpegPhoto $ 151 | labeledURI $ mail $ manager $ mobile $ o $ pager $ 152 | photo $ roomNumber $ secretary $ uid $ userCertificate $ 153 | x500uniqueIdentifier $ preferredLanguage $ 154 | userSMIMECertificate $ userPKCS12 ) 155 | ) 156 | -------------------------------------------------------------------------------- /tldap/test/ldap_schemas/70-eduperson.schema: -------------------------------------------------------------------------------- 1 | # eduPerson 200806 2 | 3 | # eduPersonAffiliation 4 | # Specifies a person's relationship(s) to the institution in 5 | # broad categories such as student, faculty, staff, alum, etc. 6 | attributetype ( 1.3.6.1.4.1.5923.1.1.1.1 7 | NAME 'eduPersonAffiliation' 8 | DESC 'eduPerson per Internet2 and EDUCAUSE' 9 | EQUALITY caseIgnoreMatch 10 | SUBSTR caseIgnoreSubstringsMatch 11 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' ) 12 | 13 | # eduPersonNickname 14 | # Specifies a person's nickname, or the informal name by which 15 | # they are accustomed to be hailed. 16 | attributetype ( 1.3.6.1.4.1.5923.1.1.1.2 17 | NAME 'eduPersonNickname' 18 | DESC 'eduPerson per Internet2 and EDUCAUSE' 19 | EQUALITY caseIgnoreMatch 20 | SUBSTR caseIgnoreSubstringsMatch 21 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' ) 22 | 23 | # eduPersonOrgDN 24 | # The distinguished name (DN) of the directory entry 25 | # representing the institution with which the person 26 | # is associated. 27 | attributetype ( 1.3.6.1.4.1.5923.1.1.1.3 28 | NAME 'eduPersonOrgDN' 29 | DESC 'eduPerson per Internet2 and EDUCAUSE' 30 | EQUALITY distinguishedNameMatch 31 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' SINGLE-VALUE ) 32 | 33 | # eduPersonOrgUnitDN 34 | # The distinguished name (DN) of the directory entries representing 35 | # the person's Organizational Unit(s). 36 | attributetype ( 1.3.6.1.4.1.5923.1.1.1.4 37 | NAME 'eduPersonOrgUnitDN' 38 | DESC 'eduPerson per Internet2 and EDUCAUSE' 39 | EQUALITY distinguishedNameMatch 40 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' ) 41 | 42 | # eduPersonPrimaryAffiliation 43 | # Specifies a person's PRIMARY relationship to the institution 44 | # in broad categories such as student, faculty, staff, alum, etc. 45 | attributetype ( 1.3.6.1.4.1.5923.1.1.1.5 46 | NAME 'eduPersonPrimaryAffiliation' 47 | DESC 'eduPerson per Internet2 and EDUCAUSE' 48 | EQUALITY caseIgnoreMatch 49 | SUBSTR caseIgnoreSubstringsMatch 50 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE ) 51 | 52 | # eduPersonPrincipalName 53 | # The "NetID" of the person for the purposes of inter-institutional 54 | # authentication. Should be stored in the form of user@univ.edu, 55 | # where univ.edu is the name of the local security domain. 56 | attributetype ( 1.3.6.1.4.1.5923.1.1.1.6 57 | NAME 'eduPersonPrincipalName' 58 | DESC 'eduPerson per Internet2 and EDUCAUSE' 59 | EQUALITY caseIgnoreMatch 60 | SUBSTR caseIgnoreSubstringsMatch 61 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE ) 62 | 63 | # eduPersonEntitlement 64 | attributetype ( 1.3.6.1.4.1.5923.1.1.1.7 65 | NAME 'eduPersonEntitlement' 66 | DESC 'eduPerson per Internet2 and EDUCAUSE' 67 | EQUALITY caseExactMatch 68 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' ) 69 | 70 | # eduPersonPrimaryOrgUnitDN 71 | attributetype ( 1.3.6.1.4.1.5923.1.1.1.8 72 | NAME 'eduPersonPrimaryOrgUnitDN' 73 | DESC 'eduPerson per Internet2 and EDUCAUSE' 74 | EQUALITY distinguishedNameMatch 75 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' SINGLE-VALUE ) 76 | 77 | # eduPersonScopedAffiliation 78 | attributetype ( 1.3.6.1.4.1.5923.1.1.1.9 79 | NAME 'eduPersonScopedAffiliation' 80 | DESC 'eduPerson per Internet2 and EDUCAUSE' 81 | EQUALITY caseIgnoreMatch 82 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE ) 83 | 84 | # eduPersonTargetedID 85 | attributetype ( 1.3.6.1.4.1.5923.1.1.1.10 86 | NAME 'eduPersonTargetedID' 87 | DESC 'eduPerson per Internet2 and EDUCASE' 88 | EQUALITY caseIgnoreMatch 89 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' ) 90 | 91 | # eduPersonAssurance 92 | attributetype ( 1.3.6.1.4.1.5923.1.1.1.11 93 | NAME 'eduPersonAssurance' 94 | DESC 'eduPerson per Internet2 and EDUCAUSE' 95 | EQUALITY caseExactMatch 96 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' ) 97 | 98 | # eduPerson 99 | # The eduPerson objectclass is used to represent people who are 100 | # associated with a university/school in some way. It is derived 101 | # from the inetOrgPerson objectclass. 102 | objectclass ( 1.3.6.1.4.1.5923.1.1.2 103 | NAME 'eduPerson' 104 | AUXILIARY 105 | MAY ( eduPersonAffiliation $ eduPersonNickname $ 106 | eduPersonOrgDN $ eduPersonOrgUnitDN $ 107 | eduPersonPrimaryAffiliation $ eduPersonPrincipalName $ 108 | eduPersonEntitlement $ eduPersonPrimaryOrgUnitDN $ 109 | eduPersonScopedAffiliation $ eduPersonTargetedID $ 110 | eduPersonAssurance 111 | ) 112 | ) 113 | -------------------------------------------------------------------------------- /tldap/test/ldap_schemas/90-schac.schema: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Karaage-Cluster/python-tldap/6cb877b28e07c0fd78a449e4c1b140fcab0117ef/tldap/test/ldap_schemas/90-schac.schema -------------------------------------------------------------------------------- /tldap/test/ldap_schemas/nis.schema: -------------------------------------------------------------------------------- 1 | # $OpenLDAP: pkg/ldap/servers/slapd/schema/nis.schema,v 1.15.2.3 2008/02/11 23:26:49 kurt Exp $ 2 | ## This work is part of OpenLDAP Software . 3 | ## 4 | ## Copyright 1998-2008 The OpenLDAP Foundation. 5 | ## All rights reserved. 6 | ## 7 | ## Redistribution and use in source and binary forms, with or without 8 | ## modification, are permitted only as authorized by the OpenLDAP 9 | ## Public License. 10 | ## 11 | ## A copy of this license is available in the file LICENSE in the 12 | ## top-level directory of the distribution or, alternatively, at 13 | ## . 14 | 15 | # Definitions from RFC2307 (Experimental) 16 | # An Approach for Using LDAP as a Network Information Service 17 | 18 | # Depends upon core.schema and cosine.schema 19 | 20 | # Note: The definitions in RFC2307 are given in syntaxes closely related 21 | # to those in RFC2252, however, some liberties are taken that are not 22 | # supported by RFC2252. This file has been written following RFC2252 23 | # strictly. 24 | 25 | # OID Base is iso(1) org(3) dod(6) internet(1) directory(1) nisSchema(1). 26 | # i.e. nisSchema in RFC2307 is 1.3.6.1.1.1 27 | # 28 | # Syntaxes are under 1.3.6.1.1.1.0 (two new syntaxes are defined) 29 | # validaters for these syntaxes are incomplete, they only 30 | # implement printable string validation (which is good as the 31 | # common use of these syntaxes violates the specification). 32 | # Attribute types are under 1.3.6.1.1.1.1 33 | # Object classes are under 1.3.6.1.1.1.2 34 | 35 | # Attribute Type Definitions 36 | 37 | # builtin 38 | #attributetype ( 1.3.6.1.1.1.1.0 NAME 'uidNumber' 39 | # DESC 'An integer uniquely identifying a user in an administrative domain' 40 | # EQUALITY integerMatch 41 | # SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) 42 | 43 | # builtin 44 | #attributetype ( 1.3.6.1.1.1.1.1 NAME 'gidNumber' 45 | # DESC 'An integer uniquely identifying a group in an administrative domain' 46 | # EQUALITY integerMatch 47 | # SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) 48 | 49 | attributetype ( 1.3.6.1.1.1.1.2 NAME 'gecos' 50 | DESC 'The GECOS field; the common name' 51 | EQUALITY caseIgnoreIA5Match 52 | SUBSTR caseIgnoreIA5SubstringsMatch 53 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) 54 | 55 | attributetype ( 1.3.6.1.1.1.1.3 NAME 'homeDirectory' 56 | DESC 'The absolute path to the home directory' 57 | EQUALITY caseExactIA5Match 58 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) 59 | 60 | attributetype ( 1.3.6.1.1.1.1.4 NAME 'loginShell' 61 | DESC 'The path to the login shell' 62 | EQUALITY caseExactIA5Match 63 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) 64 | 65 | attributetype ( 1.3.6.1.1.1.1.5 NAME 'shadowLastChange' 66 | EQUALITY integerMatch 67 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) 68 | 69 | attributetype ( 1.3.6.1.1.1.1.6 NAME 'shadowMin' 70 | EQUALITY integerMatch 71 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) 72 | 73 | attributetype ( 1.3.6.1.1.1.1.7 NAME 'shadowMax' 74 | EQUALITY integerMatch 75 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) 76 | 77 | attributetype ( 1.3.6.1.1.1.1.8 NAME 'shadowWarning' 78 | EQUALITY integerMatch 79 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) 80 | 81 | attributetype ( 1.3.6.1.1.1.1.9 NAME 'shadowInactive' 82 | EQUALITY integerMatch 83 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) 84 | 85 | attributetype ( 1.3.6.1.1.1.1.10 NAME 'shadowExpire' 86 | EQUALITY integerMatch 87 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) 88 | 89 | attributetype ( 1.3.6.1.1.1.1.11 NAME 'shadowFlag' 90 | EQUALITY integerMatch 91 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) 92 | 93 | attributetype ( 1.3.6.1.1.1.1.12 NAME 'memberUid' 94 | EQUALITY caseExactIA5Match 95 | SUBSTR caseExactIA5SubstringsMatch 96 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) 97 | 98 | attributetype ( 1.3.6.1.1.1.1.13 NAME 'memberNisNetgroup' 99 | EQUALITY caseExactIA5Match 100 | SUBSTR caseExactIA5SubstringsMatch 101 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) 102 | 103 | attributetype ( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple' 104 | DESC 'Netgroup triple' 105 | SYNTAX 1.3.6.1.1.1.0.0 ) 106 | 107 | attributetype ( 1.3.6.1.1.1.1.15 NAME 'ipServicePort' 108 | EQUALITY integerMatch 109 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) 110 | 111 | attributetype ( 1.3.6.1.1.1.1.16 NAME 'ipServiceProtocol' 112 | SUP name ) 113 | 114 | attributetype ( 1.3.6.1.1.1.1.17 NAME 'ipProtocolNumber' 115 | EQUALITY integerMatch 116 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) 117 | 118 | attributetype ( 1.3.6.1.1.1.1.18 NAME 'oncRpcNumber' 119 | EQUALITY integerMatch 120 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) 121 | 122 | attributetype ( 1.3.6.1.1.1.1.19 NAME 'ipHostNumber' 123 | DESC 'IP address' 124 | EQUALITY caseIgnoreIA5Match 125 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} ) 126 | 127 | attributetype ( 1.3.6.1.1.1.1.20 NAME 'ipNetworkNumber' 128 | DESC 'IP network' 129 | EQUALITY caseIgnoreIA5Match 130 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} SINGLE-VALUE ) 131 | 132 | attributetype ( 1.3.6.1.1.1.1.21 NAME 'ipNetmaskNumber' 133 | DESC 'IP netmask' 134 | EQUALITY caseIgnoreIA5Match 135 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} SINGLE-VALUE ) 136 | 137 | attributetype ( 1.3.6.1.1.1.1.22 NAME 'macAddress' 138 | DESC 'MAC address' 139 | EQUALITY caseIgnoreIA5Match 140 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} ) 141 | 142 | attributetype ( 1.3.6.1.1.1.1.23 NAME 'bootParameter' 143 | DESC 'rpc.bootparamd parameter' 144 | SYNTAX 1.3.6.1.1.1.0.1 ) 145 | 146 | attributetype ( 1.3.6.1.1.1.1.24 NAME 'bootFile' 147 | DESC 'Boot image name' 148 | EQUALITY caseExactIA5Match 149 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) 150 | 151 | attributetype ( 1.3.6.1.1.1.1.26 NAME 'nisMapName' 152 | SUP name ) 153 | 154 | attributetype ( 1.3.6.1.1.1.1.27 NAME 'nisMapEntry' 155 | EQUALITY caseExactIA5Match 156 | SUBSTR caseExactIA5SubstringsMatch 157 | SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{1024} SINGLE-VALUE ) 158 | 159 | # Object Class Definitions 160 | 161 | objectclass ( 1.3.6.1.1.1.2.0 NAME 'posixAccount' 162 | DESC 'Abstraction of an account with POSIX attributes' 163 | SUP top AUXILIARY 164 | MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) 165 | MAY ( userPassword $ loginShell $ gecos $ description ) ) 166 | 167 | objectclass ( 1.3.6.1.1.1.2.1 NAME 'shadowAccount' 168 | DESC 'Additional attributes for shadow passwords' 169 | SUP top AUXILIARY 170 | MUST uid 171 | MAY ( userPassword $ shadowLastChange $ shadowMin $ 172 | shadowMax $ shadowWarning $ shadowInactive $ 173 | shadowExpire $ shadowFlag $ description ) ) 174 | 175 | objectclass ( 1.3.6.1.1.1.2.2 NAME 'posixGroup' 176 | DESC 'Abstraction of a group of accounts' 177 | SUP top STRUCTURAL 178 | MUST ( cn $ gidNumber ) 179 | MAY ( userPassword $ memberUid $ description ) ) 180 | 181 | objectclass ( 1.3.6.1.1.1.2.3 NAME 'ipService' 182 | DESC 'Abstraction an Internet Protocol service' 183 | SUP top STRUCTURAL 184 | MUST ( cn $ ipServicePort $ ipServiceProtocol ) 185 | MAY ( description ) ) 186 | 187 | objectclass ( 1.3.6.1.1.1.2.4 NAME 'ipProtocol' 188 | DESC 'Abstraction of an IP protocol' 189 | SUP top STRUCTURAL 190 | MUST ( cn $ ipProtocolNumber $ description ) 191 | MAY description ) 192 | 193 | objectclass ( 1.3.6.1.1.1.2.5 NAME 'oncRpc' 194 | DESC 'Abstraction of an ONC/RPC binding' 195 | SUP top STRUCTURAL 196 | MUST ( cn $ oncRpcNumber $ description ) 197 | MAY description ) 198 | 199 | objectclass ( 1.3.6.1.1.1.2.6 NAME 'ipHost' 200 | DESC 'Abstraction of a host, an IP device' 201 | SUP top AUXILIARY 202 | MUST ( cn $ ipHostNumber ) 203 | MAY ( l $ description $ manager ) ) 204 | 205 | objectclass ( 1.3.6.1.1.1.2.7 NAME 'ipNetwork' 206 | DESC 'Abstraction of an IP network' 207 | SUP top STRUCTURAL 208 | MUST ( cn $ ipNetworkNumber ) 209 | MAY ( ipNetmaskNumber $ l $ description $ manager ) ) 210 | 211 | objectclass ( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup' 212 | DESC 'Abstraction of a netgroup' 213 | SUP top STRUCTURAL 214 | MUST cn 215 | MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) ) 216 | 217 | objectclass ( 1.3.6.1.1.1.2.9 NAME 'nisMap' 218 | DESC 'A generic abstraction of a NIS map' 219 | SUP top STRUCTURAL 220 | MUST nisMapName 221 | MAY description ) 222 | 223 | objectclass ( 1.3.6.1.1.1.2.10 NAME 'nisObject' 224 | DESC 'An entry in a NIS map' 225 | SUP top STRUCTURAL 226 | MUST ( cn $ nisMapEntry $ nisMapName ) 227 | MAY description ) 228 | 229 | objectclass ( 1.3.6.1.1.1.2.11 NAME 'ieee802Device' 230 | DESC 'A device with a MAC address' 231 | SUP top AUXILIARY 232 | MAY macAddress ) 233 | 234 | objectclass ( 1.3.6.1.1.1.2.12 NAME 'bootableDevice' 235 | DESC 'A device with boot parameters' 236 | SUP top AUXILIARY 237 | MAY ( bootFile $ bootParameter ) ) 238 | -------------------------------------------------------------------------------- /tldap/transaction.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | """ 19 | This module implements a transaction manager that can be used to define 20 | transaction handling in a request or view function. It is used by transaction 21 | control middleware and decorators. 22 | 23 | The transaction manager can be in managed or in auto state. Auto state means 24 | the system is using a commit-on-save strategy (actually it's more like 25 | commit-on-change). As soon as the .save() or .delete() (or related) methods are 26 | called, a commit is made. 27 | 28 | Managed transactions don't do those commits, but will need some kind of manual 29 | or implicit commits or rollbacks. 30 | """ 31 | import sys 32 | from functools import wraps 33 | 34 | import tldap.backend 35 | 36 | 37 | class TransactionManagementError(Exception): 38 | """ 39 | This exception is thrown when something bad happens with transaction 40 | management. 41 | """ 42 | pass 43 | 44 | 45 | def enter_transaction_management(using=None): 46 | """ 47 | Enters transaction management for a running thread. It must be balanced 48 | with the appropriate leave_transaction_management call, since the actual 49 | state is managed as a stack. 50 | 51 | The state and dirty flag are carried over from the surrounding block or 52 | from the settings, if there is no surrounding block (dirty is always false 53 | when no current block is running). 54 | """ 55 | if using is None: 56 | for using in tldap.backend.connections: 57 | connection = tldap.backend.connections[using] 58 | connection.enter_transaction_management() 59 | return 60 | connection = tldap.backend.connections[using] 61 | connection.enter_transaction_management() 62 | 63 | 64 | def leave_transaction_management(using=None): 65 | """ 66 | Leaves transaction management for a running thread. A dirty flag is carried 67 | over to the surrounding block, as a commit will commit all changes, even 68 | those from outside. (Commits are on connection level.) 69 | """ 70 | if using is None: 71 | for using in tldap.backend.connections: 72 | connection = tldap.backend.connections[using] 73 | connection.leave_transaction_management() 74 | return 75 | connection = tldap.backend.connections[using] 76 | connection.leave_transaction_management() 77 | 78 | 79 | def is_dirty(using=None): 80 | """ 81 | Returns True if the current transaction requires a commit for changes to 82 | happen. 83 | """ 84 | if using is None: 85 | dirty = False 86 | for using in tldap.backend.connections: 87 | connection = tldap.backend.connections[using] 88 | if connection.is_dirty(): 89 | dirty = True 90 | return dirty 91 | connection = tldap.backend.connections[using] 92 | return connection.is_dirty() 93 | 94 | 95 | def is_managed(using=None): 96 | """ 97 | Checks whether the transaction manager is in manual or in auto state. 98 | """ 99 | if using is None: 100 | managed = False 101 | for using in tldap.backend.connections: 102 | connection = tldap.backend.connections[using] 103 | if connection.is_managed(): 104 | managed = True 105 | return managed 106 | connection = tldap.backend.connections[using] 107 | return connection.is_managed() 108 | 109 | 110 | def commit(using=None): 111 | """ 112 | Does the commit itself and resets the dirty flag. 113 | """ 114 | if using is None: 115 | for using in tldap.backend.connections: 116 | connection = tldap.backend.connections[using] 117 | connection.commit() 118 | return 119 | connection = tldap.backend.connections[using] 120 | connection.commit() 121 | 122 | 123 | def rollback(using=None): 124 | """ 125 | This function does the rollback itself and resets the dirty flag. 126 | """ 127 | if using is None: 128 | for using in tldap.backend.connections: 129 | connection = tldap.backend.connections[using] 130 | connection.rollback() 131 | return 132 | connection = tldap.backend.connections[using] 133 | connection.rollback() 134 | 135 | ############## 136 | # DECORATORS # 137 | ############## 138 | 139 | 140 | class Transaction(object): 141 | """ 142 | Acts as either a decorator, or a context manager. If it's a decorator it 143 | takes a function and returns a wrapped function. If it's a contextmanager 144 | it's used with the ``with`` statement. In either event entering/exiting 145 | are called before and after, respectively, the function/block is executed. 146 | 147 | autocommit, commit_on_success, and commit_manually contain the 148 | implementations of entering and exiting. 149 | """ 150 | def __init__(self, entering, exiting, using): 151 | self.entering = entering 152 | self.exiting = exiting 153 | self.using = using 154 | 155 | def __enter__(self): 156 | self.entering(self.using) 157 | 158 | def __exit__(self, exc_type, exc_value, traceback): 159 | self.exiting(exc_value, self.using) 160 | 161 | def __call__(self, func): 162 | @wraps(func) 163 | def inner(*args, **kwargs): 164 | # Once we drop support for Python 2.4 this block should become: 165 | # with self: 166 | # func(*args, **kwargs) 167 | self.__enter__() 168 | try: 169 | res = func(*args, **kwargs) 170 | except: # noqa: E722 171 | self.__exit__(*sys.exc_info()) 172 | raise 173 | else: 174 | self.__exit__(None, None, None) 175 | return res 176 | return inner 177 | 178 | 179 | def _transaction_func(entering, exiting, using): 180 | """ 181 | Takes 3 things, an entering function (what to do to start this block of 182 | transaction management), an exiting function (what to do to end it, on both 183 | success and failure, and using which can be: None, indiciating transaction 184 | should occur on all defined servers, or a callable, indicating that using 185 | is None and to return the function already wrapped. 186 | 187 | Returns either a Transaction objects, which is both a decorator and a 188 | context manager, or a wrapped function, if using is a callable. 189 | """ 190 | # Note that although the first argument is *called* `using`, it 191 | # may actually be a function; @autocommit and @autocommit('foo') 192 | # are both allowed forms. 193 | if callable(using): 194 | return Transaction(entering, exiting, None)(using) 195 | return Transaction(entering, exiting, using) 196 | 197 | 198 | def commit_on_success(using=None): 199 | """ 200 | This decorator activates commit on response. This way, if the view function 201 | runs successfully, a commit is made; if the viewfunc produces an exception, 202 | a rollback is made. This is one of the most common ways to do transaction 203 | control in Web apps. 204 | """ 205 | def entering(using): 206 | enter_transaction_management(using=using) 207 | 208 | def exiting(exc_value, using): 209 | try: 210 | if exc_value is not None: 211 | if is_dirty(using=using): 212 | rollback(using=using) 213 | else: 214 | commit(using=using) 215 | finally: 216 | leave_transaction_management(using=using) 217 | 218 | return _transaction_func(entering, exiting, using) 219 | 220 | 221 | def commit_manually(using=None): 222 | """ 223 | Decorator that activates manual transaction control. It just disables 224 | automatic transaction control and doesn't do any commit/rollback of its 225 | own -- it's up to the user to call the commit and rollback functions 226 | themselves. 227 | """ 228 | def entering(using): 229 | enter_transaction_management(using=using) 230 | 231 | def exiting(exc_value, using): 232 | leave_transaction_management(using=using) 233 | 234 | return _transaction_func(entering, exiting, using) 235 | -------------------------------------------------------------------------------- /tldap/tree.py: -------------------------------------------------------------------------------- 1 | """ 2 | A class for storing a tree graph. Primarily used for filter constructs in the 3 | ORM. 4 | """ 5 | 6 | import copy 7 | 8 | 9 | class Node(object): 10 | """ 11 | A single internal node in the tree graph. A Node should be viewed as a 12 | connection (the root) with the children being either leaf nodes or other 13 | Node instances. 14 | """ 15 | # Standard connector type. Clients usually won't use this at all and 16 | # subclasses will usually override the value. 17 | default = 'DEFAULT' 18 | 19 | def __init__(self, children=None, connector=None, negated=False): 20 | """ 21 | Constructs a new Node. If no connector is given, the default will be 22 | used. 23 | """ 24 | self.children = children[:] if children else [] 25 | self.connector = connector or self.default 26 | self.negated = negated 27 | 28 | # We need this because of django.db.models.query_utils.Q. Q. __init__() is 29 | # problematic, but it is a natural Node subclass in all other respects. 30 | @classmethod 31 | def _new_instance(cls, children=None, connector=None, negated=False): 32 | """ 33 | This is called to create a new instance of this class when we need new 34 | Nodes (or subclasses) in the internal code in this class. Normally, it 35 | just shadows __init__(). However, subclasses with an __init__ signature 36 | that is not an extension of Node.__init__ might need to implement this 37 | method to allow a Node to create a new instance of them (if they have 38 | any extra setting up to do). 39 | """ 40 | obj = Node(children, connector, negated) 41 | obj.__class__ = cls 42 | return obj 43 | 44 | def __str__(self): 45 | if self.negated: 46 | return '(NOT (%s: %s))' % (self.connector, ', '.join(str(c) for c 47 | in self.children)) 48 | return '(%s: %s)' % (self.connector, ', '.join(str(c) for c in 49 | self.children)) 50 | 51 | def __repr__(self): 52 | return "<%s: %s>" % (self.__class__.__name__, self) 53 | 54 | def __deepcopy__(self, memodict): 55 | """ 56 | Utility method used by copy.deepcopy(). 57 | """ 58 | obj = Node(connector=self.connector, negated=self.negated) 59 | obj.__class__ = self.__class__ 60 | obj.children = copy.deepcopy(self.children, memodict) 61 | return obj 62 | 63 | def __len__(self): 64 | """ 65 | The size of a node if the number of children it has. 66 | """ 67 | return len(self.children) 68 | 69 | def __bool__(self): 70 | """ 71 | For truth value testing. 72 | """ 73 | return bool(self.children) 74 | 75 | def __nonzero__(self): # Python 2 compatibility 76 | return type(self).__bool__(self) 77 | 78 | def __contains__(self, other): 79 | """ 80 | Returns True is 'other' is a direct child of this instance. 81 | """ 82 | return other in self.children 83 | 84 | def add(self, data, conn_type, squash=True): 85 | """ 86 | Combines this tree and the data represented by data using the 87 | connector conn_type. The combine is done by squashing the node other 88 | away if possible. 89 | 90 | This tree (self) will never be pushed to a child node of the 91 | combined tree, nor will the connector or negated properties change. 92 | 93 | The function returns a node which can be used in place of data 94 | regardless if the node other got squashed or not. 95 | 96 | If `squash` is False the data is prepared and added as a child to 97 | this tree without further logic. 98 | """ 99 | if data in self.children: 100 | return data 101 | if not squash: 102 | self.children.append(data) 103 | return data 104 | if self.connector == conn_type: 105 | # We can reuse self.children to append or squash the node other. 106 | if (isinstance(data, Node) and not data.negated 107 | and (data.connector == conn_type or len(data) == 1)): 108 | # We can squash the other node's children directly into this 109 | # node. We are just doing (AB)(CD) == (ABCD) here, with the 110 | # addition that if the length of the other node is 1 the 111 | # connector doesn't matter. However, for the len(self) == 1 112 | # case we don't want to do the squashing, as it would alter 113 | # self.connector. 114 | self.children.extend(data.children) 115 | return self 116 | else: 117 | # We could use perhaps additional logic here to see if some 118 | # children could be used for pushdown here. 119 | self.children.append(data) 120 | return data 121 | else: 122 | obj = self._new_instance(self.children, self.connector, 123 | self.negated) 124 | self.connector = conn_type 125 | self.children = [obj, data] 126 | return data 127 | 128 | def negate(self): 129 | """ 130 | Negate the sense of the root connector. 131 | """ 132 | self.negated = not self.negated 133 | -------------------------------------------------------------------------------- /tldap/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2014 Brian May 2 | # 3 | # This file is part of python-tldap. 4 | # 5 | # python-tldap is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # python-tldap is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-tldap If not, see . 17 | 18 | """ Contains ConnectionHandler which represents a list of connections. """ 19 | 20 | import sys 21 | from threading import local 22 | 23 | 24 | DEFAULT_LDAP_ALIAS = "default" 25 | 26 | 27 | def load_backend(backend_name): 28 | __import__(backend_name) 29 | return sys.modules[backend_name] 30 | 31 | 32 | class ConnectionHandler(object): 33 | """ Contains a list of known LDAP connections. """ 34 | 35 | def __init__(self, databases): 36 | self.databases = databases 37 | self._connections = local() 38 | 39 | def __getitem__(self, alias): 40 | if hasattr(self._connections, alias): 41 | return getattr(self._connections, alias) 42 | 43 | db = self.databases[alias] 44 | 45 | backend = load_backend(db['ENGINE']) 46 | conn = backend.LDAPwrapper(db) 47 | setattr(self._connections, alias, conn) 48 | return conn 49 | 50 | def __iter__(self): 51 | return iter(self.databases) 52 | 53 | def all(self): 54 | """ Return list of all connections. """ 55 | return [self[alias] for alias in self] 56 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = true 3 | isolated_build = True 4 | downloadcache = {toxworkdir}/cache/ 5 | envlist = 6 | py36, 7 | py37, 8 | py38, 9 | 10 | [testenv] 11 | basepython = 12 | py36: python3.6 13 | py37: python3.7 14 | py38: python3.8 15 | commands = 16 | poetry install -v 17 | poetry run flake8 tldap 18 | poetry run python -m tldap.test.slapd python -m pytest 19 | deps = 20 | poetry 21 | setuptools>=17.1 22 | --------------------------------------------------------------------------------