django-ldapdb-1.5.1/0000755000175000017500000000000013741076301015036 5ustar xelnorxelnet00000000000000django-ldapdb-1.5.1/AUTHORS0000644000175000017500000000442513741076301016113 0ustar xelnorxelnet00000000000000Maintainers ----------- The ``django-ldapdb`` project is operated and maintained by: * Jeremy Lainé (https://github.com/jlaine), primary author * Raphaël Barrois (https://github.com/rbarrois) .. _contributors: Contributors ------------ The django-ldapdb project has received contributions from (in alphabetical order): * Alessandro -oggei- Ogier * Andres Mejia * Jeremy Lainé (https://github.com/jlaine) * Michał Górny * Raphaël Barrois (https://github.com/rbarrois) * Theo Chatzimichos (https://github.com/tampakrap) * Thomas Güttler * Xaroth Brook Pending agreement ----------------- The CLA has only been put in place recently; the project has previously received help from the following contributors: * Dmitri Bogomolov <4glitch@gmail.com> * Fabs * Karsten Heymann Contributor license agreement ----------------------------- .. note:: This agreement is required to allow redistribution of submitted contributions. See http://oss-watch.ac.uk/resources/cla for an explanation. Any contributor proposing updates to the code or documentation of this project *MUST* add its name to the list in the :ref:`contributors` section, thereby "signing" the following contributor license agreement: They accept and agree to the following terms for their present end future contributions submitted to the ``django-ldapdb`` project: * They represent that they are legally entitled to grant this license, and that their contributions are their original creation * They grant the ``django-ldapdb`` project a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, sublicense and distribute their contributions and such derivative works. * They are not expected to provide support for their contributions, except to the extent they desire to provide support. .. note:: The above agreement is inspired by the Apache Contributor License Agreement. .. vim:set ft=rst: django-ldapdb-1.5.1/CODE_OF_CONDUCT.md0000644000175000017500000000624313741076301017642 0ustar xelnorxelnet00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at raphael DOT barrois AT xelmail DOT com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ django-ldapdb-1.5.1/CONTRIBUTING.rst0000644000175000017500000000536013741076301017503 0ustar xelnorxelnet00000000000000Contributing ============ Thanks for taking the time to contribute to django-ldapdb! Code of Conduct --------------- This project and everyone participating in it is governed by the `Code of Conduct`_. By participating, you are expected to uphold this code. Please report inappropriate behavior to raphael DOT barrois AT xelmail DOT com. .. _Code of Conduct: https://github.com/django-ldapdb/django-ldapdb/blob/master/CODE_OF_CONDUCT.md *(If I'm the person with the inappropriate behavior, please accept my apologies. I know I can mess up. I can't expect you to tell me, but if you chose to do so, I'll do my best to handle criticism constructively. -- Raphaël)* *(As the community around this project grows, we hope to have more core developers available to handle that kind of issues)* Contributions ------------- Bug reports, patches, documentation improvements and suggestions are welcome! Please open an issue_ or send a `pull request`_. Feedback about the documentation is especially valuable — the authors of ``django-ldapdb`` feel more confident about writing code than writing docs :-) .. _issue: https://github.com/django-ldapdb/django-ldapdb/issues/new .. _pull request: https://github.com/django-ldapdb/django-ldapdb/compare/ Where to start? --------------- If you're new to the project and want to help, a great first step would be: * Fixing an issue in the docs (outdated setup instructions, missing information, unclear feature, etc.); * Working on an existing issue (some should be marked ``BeginnerFriendly``); * Reviewing an existing pull request; * Or any other way you'd like to help. Code contributions ------------------ In order to merge some code, you'll need to open a `pull request`_. There are a few rules to keep in mind regarding pull requests: * A pull request should only solve a single issue / add a single feature; * If the code change is significant, please also create an issue_ for easier discussion; * We have automated testing; please make sure that the updated code passes automated checks; * We're striving to improve the quality of the library, with higher test and docs coverage. If you don't know how/where to add docs or tests, we'll be very happy to point you in the right direction! Questions --------- GitHub issues aren't a good medium for handling questions. There are better places to ask questions, for example Stack Overflow; please use the ``django-ldapdb`` tag to make those questions easy to find by the maintainers. If you want to ask a question anyway, please make sure that: - it's a question about ``django-ldapdb`` and not about ``python-ldap`` or ``LDAP``; - it isn't answered by the documentation; - it wasn't asked already. A good question can be written as a suggestion to improve the documentation. django-ldapdb-1.5.1/ChangeLog0000644000175000017500000000613113741076301016611 0ustar xelnorxelnet00000000000000ChangeLog ========= 1.5.1 (2020-10-12) ------------------ *Bugfix:* * Fix empty wheel (missing nested code) 1.5.0 (2020-10-10) ------------------ *New:* * Add support for Django >= 3.1 *Removed:* * Drop support for Django < 2.2 * Drop support for Python < 3.6 1.4.0 (2019-07-11) ------------------ *New:* * *[#177]* Add BooleanField. * Add support for Django 2.2 and python 3.7 *Bugfix:* * *[#3]* Allow to change objects found at subtree scope. * *[#190]* Check bind state of connection before unbinding. 1.3.0 (2018-12-12) ------------------ *New:* * *[#80]* Use ReconnectLDAPObject to survive flaky LDAP servers. * *[#164]* Register __in lookup for DateTimeField and TimestampField. * *[#166]* Support and test on Python 3.7. * *[#175]* Add support for Django 2.1 1.2.0 (2018-07-28) ------------------ *Bugfix:* * *[#153]* Don't send empty updates to the LDAP Server. * *[#159]* Fix saving composite PK objects. * *[#140]* Fix ordering by distinguished name. * *[#57]* Deduplicate values in ``ListField``. *New:* * Normalize value of nullable empty attributes: return ``0`` / ``0.0`` if the field is not nullable, ``None`` otherwise. *Packaging:* * Include required test files in the 'sdist' tarball; these can be required by projects. 1.1.0 (2018-07-14) ------------------ *New:* * Switch back to ``python-ldap`` instead of ``pyldap``. Users updating a virtualenv must uninstall ``pyldap`` **before** installing ``python-ldap``: .. code-block:: sh pip uninstall pyldap pip install python-ldap * Add ``DateTimeField`` and ``TimestampField``, with proper lookups. 1.0.0 (2018-03-06) ------------------ *New:* * Add support for Django 2.0 *Deprecation:* * Drop support for Django <1.11 (we only support the latest Django version, and the Django LTS preceding that version). 0.9.0 (2017-06-14) ------------------ *Bugfix:* * Restore support for raw LDAP options *New:* * Add support for Django 1.11 *Deprecation:* * Drop support for Django 1.9 (we only support 2 LTS and 2 latest releases) .. _v0.8.0: 0.8.0 (2017-01-24) ------------------ *Bufix:* * Improve Django 1.10 support: fix errors with ``makemigrations`` *New:* * Add query timeout support, through the ``QUERY_TIMEOUT`` setting * Handle paginated results, tunable by the ``PAGE_SIZE`` setting .. _v0.7.0: 0.7.0 (2016-10-24) ------------------ *New:* * Add support for Django ≥ 1.10 * Allow ``__in`` lookups for int/float fields *Bugfix:* * Remove all remaining warnings *Deprecation:* * Drop support for Django < 1.8 .. _v0.6.0: 0.6.0 (2016-06-06) ------------------ *Bugfix:* * Implement connection recycling according to Django defaults *Deprecation:* * Remove support code for Django < 1.7 .. _v0.5.1: 0.5.1 (2016-06-04) ------------------ *New:* * Add direct ``dn`` lookup (e.g ``MyModel.objects.get(dn=x)``) .. _v0.5.0: 0.5.0 (2016-06-01) ------------------ *New:* * Add support for Python ≥ 3.4, Django 1.9 .. vim:set ft=rst: django-ldapdb-1.5.1/LICENSE0000644000175000017500000000246413741076301016051 0ustar xelnorxelnet00000000000000Copyright (c) The django-ldapdb project All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-ldapdb-1.5.1/MANIFEST.in0000644000175000017500000000033413741076301016574 0ustar xelnorxelnet00000000000000include AUTHORS ChangeLog CODE_OF_CONDUCT.md CONTRIBUTING.rst LICENSE README.rst graft ldapdb graft tests graft examples global-exclude *.py[cod] __pycache__ .*swp prune .github include Makefile manage_dev.py tox.ini django-ldapdb-1.5.1/Makefile0000644000175000017500000000211713741076301016477 0ustar xelnorxelnet00000000000000PACKAGE := ldapdb TESTS_DIR := examples # Error on all warnings, except in python's site.py module and distutils' imp.py module. PYWARNINGS = -Wdefault -Werror \ -Wignore::DeprecationWarning:site:165 \ -Wignore::PendingDeprecationWarning:imp \ -Wignore::DeprecationWarning:imp \ -Wignore::PendingDeprecationWarning:distutils \ -Wignore::DeprecationWarning:distutils \ -Wignore::ImportWarning: default: install: python setup.py install clean: find . -type f -name '*.pyc' -delete find . -type f -path '*/__pycache__/*' -delete find . -type d -empty -delete upgrade: pip install --upgrade pip setuptools pip install --upgrade -e .[dev] pip freeze release: fullrelease .PHONY: default install clean upgrade release testall: tox test: python $(PYWARNINGS) manage_dev.py test .PHONY: test testall lint: flake8 isort check-manifest flake8: flake8 $(PACKAGE) $(TESTS_DIR) isort: isort $(PACKAGE) $(TESTS_DIR) --check-only --diff --project $(PACKAGE) --project $(TESTS_DIR) check-manifest: check-manifest .PHONY: isort lint flake8 check-manifest django-ldapdb-1.5.1/PKG-INFO0000644000175000017500000002461513741076301016143 0ustar xelnorxelnet00000000000000Metadata-Version: 2.1 Name: django-ldapdb Version: 1.5.1 Summary: A LDAP database backend for Django Home-page: https://github.com/django-ldapdb/django-ldapdb Author: Jeremy Lainé Author-email: jeremy.laine@m4x.org Maintainer: Raphaël Barrois Maintainer-email: raphael.barrois+django-ldapdb@polytechnique.org License: BSD Description: django-ldapdb ============= .. image:: https://secure.travis-ci.org/django-ldapdb/django-ldapdb.png?branch=master :target: http://travis-ci.org/django-ldapdb/django-ldapdb/ .. image:: https://img.shields.io/pypi/v/django-ldapdb.svg :target: https://pypi.python.org/pypi/django-ldapdb/ :alt: Latest Version .. image:: https://img.shields.io/pypi/pyversions/django-ldapdb.svg :target: https://pypi.python.org/pypi/django-ldapdb/ :alt: Supported Python versions .. image:: https://img.shields.io/pypi/wheel/django-ldapdb.svg :target: https://pypi.python.org/pypi/django-ldapdb/ :alt: Wheel status .. image:: https://img.shields.io/pypi/l/django-ldapdb.svg :target: https://pypi.python.org/pypi/django-ldapdb/ :alt: License ``django-ldapdb`` is an LDAP database backend for Django, allowing to manipulate LDAP entries through Django models. It supports most of the same APIs as a Django model: * ``MyModel.objects.create()`` * ``MyModel.objects.filter(x=1, y__contains=2)`` * Full admin support and browsing ``django-ldapdb`` supports every upstream-supported Django version, based on the `Django support policy `_. For the current version, the following versions are supported: - Django 2.2 (LTS), under Python 3.6 - 3.8 (Python 3.5 has reached its end of life); - Django 3.0, under Python 3.6 - 3.8; - Django 3.1, under Python 3.6 - 3.8. Installing django-ldapdb ------------------------ Linux ~~~~~ Use pip: ``pip install django-ldapdb`` You might also need the usual ``LDAP`` packages from your distribution, usually named ``openldap`` or ``ldap-utils``. Windows ~~~~~~~ ``django-ldapdb`` depends on the `python-ldap ` project. Either follow `its Windows installation guide `_, or install a pre-built version from https://www.lfd.uci.edu/~gohlke/pythonlibs/#python-ldap (choose the ``.whl`` file matching your Python/Windows combination, and install it with ``pip install python-ldap-3...whl``). You may then install ``django-ldapdb`` with ``pip install django-ldapdb`` Using django-ldapdb ------------------- Add the following to your ``settings.py``: .. code-block:: python DATABASES = { 'ldap': { 'ENGINE': 'ldapdb.backends.ldap', 'NAME': 'ldap://ldap.nodomain.org/', 'USER': 'cn=admin,dc=nodomain,dc=org', 'PASSWORD': 'some_secret_password', }, 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), }, } DATABASE_ROUTERS = ['ldapdb.router.Router'] If you want to access posixGroup entries in your application, you can add something like this to your ``models.py``: .. code-block:: python from ldapdb.models.fields import CharField, IntegerField, ListField import ldapdb.models class LdapGroup(ldapdb.models.Model): """ Class for representing an LDAP group entry. """ # LDAP meta-data base_dn = "ou=groups,dc=nodomain,dc=org" object_classes = ['posixGroup'] # posixGroup attributes gid = IntegerField(db_column='gidNumber', unique=True) name = CharField(db_column='cn', max_length=200, primary_key=True) members = ListField(db_column='memberUid') def __str__(self): return self.name def __unicode__(self): return self.name and add this to your ``admin.py``: .. code-block:: python from django.contrib import admin from . import models class LDAPGroupAdmin(admin.ModelAdmin): exclude = ['dn', 'objectClass'] list_display = ['gid', 'name'] admin.site.register(models.LDAPGroup, LDAPGroupAdmin) **Important note:** You **must** declare an attribute to be used as the primary key. This attribute will play a special role, as it will be used to build the Relative Distinguished Name of the entry. For instance in the example above, a group whose cn is ``foo`` will have the DN ``cn=foo,ou=groups,dc=nodomain,dc=org``. Supported fields ---------------- djanglo-ldapdb provides the following fields, all imported from ``ldapdb.models.fields``: Similar to Django: * ``IntegerField`` * ``FloatField`` * ``BooleanField`` * ``CharField`` * ``ImageField`` * ``DateTimeField`` Specific to a LDAP server: * ``ListField`` (holds a list of text values) * ``TimestampField`` (Stores a datetime as a posix timestamp, typically for posixAccount) Legacy: * ``DateField`` (Stores a date in an arbitrary format. A LDAP server has no notion of ``Date``). Tuning django-ldapdb -------------------- It is possible to adjust django-ldapdb's behavior by defining a few parameters in the ``DATABASE`` section: ``PAGE_SIZE`` (default: ``1000``) Define the maximum size of a results page to be returned by the server ``QUERY_TIMEOUT`` (default: no limit) Define the maximum time in seconds we'll wait to get a reply from the server (on a per-query basis). .. note:: This setting applies on individual requests; if a high-level operation requires many queries (for instance a paginated search yielding thousands of entries), the timeout will be used on each individual request; the overall processing time might be much higher. Developing with a LDAP server ----------------------------- When developing against a LDAP server, having access to a development LDAP server often proves useful. django-ldapdb uses the `volatildap project `_ for this purpose: - A LDAP server is instantiated for each TestClass; - Its content is reset at the start of each test function; - It can be customized to embark any schemas required by the application; - Starting with volatildap 1.4.0, the volatildap server can be controlled remotely, avoiding the need to install a LDAP server on the host. Applications using django-ldapdb may use the following code snippet when setting up their tests: .. code-block:: python # This snippet is released in the Public Domain from django.conf import settings from django.test import TestCase import volatildap class LdapEnabledTestCase(TestCase): @classmethod def setUpClass(cls): super().setUpClass() cls.ldap = volatildap.LdapServer( # Load some initial data initial={'ou=people': { 'ou': ['people'], 'objectClass': ['organizationalUnit'], }}, # Enable more LDAP schemas schemas=['core.schema', 'cosine.schema', 'inetorgperson.schema', 'nis.schema'], ) # The volatildap server uses specific defaults, and listens on an arbitrary port. # Copy the server-side values to Django settings settings.DATABASES['ldap']['USER'] = cls.ldap.rootdn settings.DATABASES['ldap']['PASSWORD'] = cls.ldap.rootpw settings.DATABASES['ldap']['NAME'] = cls.ldap.uri def setUp(self): super().setUp() # Starting an already-started volatildap server performs a data reset self.ldap.start() @classmethod def tearDownClass(cls): # Free up resources on teardown. cls.ldap.stop() super().tearDownClass() Keywords: django,ldap,database,ldapdb Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Framework :: Django :: 2.2 Classifier: Framework :: Django :: 3.0 Classifier: Framework :: Django :: 3.1 Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.6 Provides-Extra: dev django-ldapdb-1.5.1/README.rst0000644000175000017500000001661413741076301016535 0ustar xelnorxelnet00000000000000django-ldapdb ============= .. image:: https://secure.travis-ci.org/django-ldapdb/django-ldapdb.png?branch=master :target: http://travis-ci.org/django-ldapdb/django-ldapdb/ .. image:: https://img.shields.io/pypi/v/django-ldapdb.svg :target: https://pypi.python.org/pypi/django-ldapdb/ :alt: Latest Version .. image:: https://img.shields.io/pypi/pyversions/django-ldapdb.svg :target: https://pypi.python.org/pypi/django-ldapdb/ :alt: Supported Python versions .. image:: https://img.shields.io/pypi/wheel/django-ldapdb.svg :target: https://pypi.python.org/pypi/django-ldapdb/ :alt: Wheel status .. image:: https://img.shields.io/pypi/l/django-ldapdb.svg :target: https://pypi.python.org/pypi/django-ldapdb/ :alt: License ``django-ldapdb`` is an LDAP database backend for Django, allowing to manipulate LDAP entries through Django models. It supports most of the same APIs as a Django model: * ``MyModel.objects.create()`` * ``MyModel.objects.filter(x=1, y__contains=2)`` * Full admin support and browsing ``django-ldapdb`` supports every upstream-supported Django version, based on the `Django support policy `_. For the current version, the following versions are supported: - Django 2.2 (LTS), under Python 3.6 - 3.8 (Python 3.5 has reached its end of life); - Django 3.0, under Python 3.6 - 3.8; - Django 3.1, under Python 3.6 - 3.8. Installing django-ldapdb ------------------------ Linux ~~~~~ Use pip: ``pip install django-ldapdb`` You might also need the usual ``LDAP`` packages from your distribution, usually named ``openldap`` or ``ldap-utils``. Windows ~~~~~~~ ``django-ldapdb`` depends on the `python-ldap ` project. Either follow `its Windows installation guide `_, or install a pre-built version from https://www.lfd.uci.edu/~gohlke/pythonlibs/#python-ldap (choose the ``.whl`` file matching your Python/Windows combination, and install it with ``pip install python-ldap-3...whl``). You may then install ``django-ldapdb`` with ``pip install django-ldapdb`` Using django-ldapdb ------------------- Add the following to your ``settings.py``: .. code-block:: python DATABASES = { 'ldap': { 'ENGINE': 'ldapdb.backends.ldap', 'NAME': 'ldap://ldap.nodomain.org/', 'USER': 'cn=admin,dc=nodomain,dc=org', 'PASSWORD': 'some_secret_password', }, 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), }, } DATABASE_ROUTERS = ['ldapdb.router.Router'] If you want to access posixGroup entries in your application, you can add something like this to your ``models.py``: .. code-block:: python from ldapdb.models.fields import CharField, IntegerField, ListField import ldapdb.models class LdapGroup(ldapdb.models.Model): """ Class for representing an LDAP group entry. """ # LDAP meta-data base_dn = "ou=groups,dc=nodomain,dc=org" object_classes = ['posixGroup'] # posixGroup attributes gid = IntegerField(db_column='gidNumber', unique=True) name = CharField(db_column='cn', max_length=200, primary_key=True) members = ListField(db_column='memberUid') def __str__(self): return self.name def __unicode__(self): return self.name and add this to your ``admin.py``: .. code-block:: python from django.contrib import admin from . import models class LDAPGroupAdmin(admin.ModelAdmin): exclude = ['dn', 'objectClass'] list_display = ['gid', 'name'] admin.site.register(models.LDAPGroup, LDAPGroupAdmin) **Important note:** You **must** declare an attribute to be used as the primary key. This attribute will play a special role, as it will be used to build the Relative Distinguished Name of the entry. For instance in the example above, a group whose cn is ``foo`` will have the DN ``cn=foo,ou=groups,dc=nodomain,dc=org``. Supported fields ---------------- djanglo-ldapdb provides the following fields, all imported from ``ldapdb.models.fields``: Similar to Django: * ``IntegerField`` * ``FloatField`` * ``BooleanField`` * ``CharField`` * ``ImageField`` * ``DateTimeField`` Specific to a LDAP server: * ``ListField`` (holds a list of text values) * ``TimestampField`` (Stores a datetime as a posix timestamp, typically for posixAccount) Legacy: * ``DateField`` (Stores a date in an arbitrary format. A LDAP server has no notion of ``Date``). Tuning django-ldapdb -------------------- It is possible to adjust django-ldapdb's behavior by defining a few parameters in the ``DATABASE`` section: ``PAGE_SIZE`` (default: ``1000``) Define the maximum size of a results page to be returned by the server ``QUERY_TIMEOUT`` (default: no limit) Define the maximum time in seconds we'll wait to get a reply from the server (on a per-query basis). .. note:: This setting applies on individual requests; if a high-level operation requires many queries (for instance a paginated search yielding thousands of entries), the timeout will be used on each individual request; the overall processing time might be much higher. Developing with a LDAP server ----------------------------- When developing against a LDAP server, having access to a development LDAP server often proves useful. django-ldapdb uses the `volatildap project `_ for this purpose: - A LDAP server is instantiated for each TestClass; - Its content is reset at the start of each test function; - It can be customized to embark any schemas required by the application; - Starting with volatildap 1.4.0, the volatildap server can be controlled remotely, avoiding the need to install a LDAP server on the host. Applications using django-ldapdb may use the following code snippet when setting up their tests: .. code-block:: python # This snippet is released in the Public Domain from django.conf import settings from django.test import TestCase import volatildap class LdapEnabledTestCase(TestCase): @classmethod def setUpClass(cls): super().setUpClass() cls.ldap = volatildap.LdapServer( # Load some initial data initial={'ou=people': { 'ou': ['people'], 'objectClass': ['organizationalUnit'], }}, # Enable more LDAP schemas schemas=['core.schema', 'cosine.schema', 'inetorgperson.schema', 'nis.schema'], ) # The volatildap server uses specific defaults, and listens on an arbitrary port. # Copy the server-side values to Django settings settings.DATABASES['ldap']['USER'] = cls.ldap.rootdn settings.DATABASES['ldap']['PASSWORD'] = cls.ldap.rootpw settings.DATABASES['ldap']['NAME'] = cls.ldap.uri def setUp(self): super().setUp() # Starting an already-started volatildap server performs a data reset self.ldap.start() @classmethod def tearDownClass(cls): # Free up resources on teardown. cls.ldap.stop() super().tearDownClass() django-ldapdb-1.5.1/django_ldapdb.egg-info/0000755000175000017500000000000013741076301021300 5ustar xelnorxelnet00000000000000django-ldapdb-1.5.1/django_ldapdb.egg-info/PKG-INFO0000644000175000017500000002461513741076301022405 0ustar xelnorxelnet00000000000000Metadata-Version: 2.1 Name: django-ldapdb Version: 1.5.1 Summary: A LDAP database backend for Django Home-page: https://github.com/django-ldapdb/django-ldapdb Author: Jeremy Lainé Author-email: jeremy.laine@m4x.org Maintainer: Raphaël Barrois Maintainer-email: raphael.barrois+django-ldapdb@polytechnique.org License: BSD Description: django-ldapdb ============= .. image:: https://secure.travis-ci.org/django-ldapdb/django-ldapdb.png?branch=master :target: http://travis-ci.org/django-ldapdb/django-ldapdb/ .. image:: https://img.shields.io/pypi/v/django-ldapdb.svg :target: https://pypi.python.org/pypi/django-ldapdb/ :alt: Latest Version .. image:: https://img.shields.io/pypi/pyversions/django-ldapdb.svg :target: https://pypi.python.org/pypi/django-ldapdb/ :alt: Supported Python versions .. image:: https://img.shields.io/pypi/wheel/django-ldapdb.svg :target: https://pypi.python.org/pypi/django-ldapdb/ :alt: Wheel status .. image:: https://img.shields.io/pypi/l/django-ldapdb.svg :target: https://pypi.python.org/pypi/django-ldapdb/ :alt: License ``django-ldapdb`` is an LDAP database backend for Django, allowing to manipulate LDAP entries through Django models. It supports most of the same APIs as a Django model: * ``MyModel.objects.create()`` * ``MyModel.objects.filter(x=1, y__contains=2)`` * Full admin support and browsing ``django-ldapdb`` supports every upstream-supported Django version, based on the `Django support policy `_. For the current version, the following versions are supported: - Django 2.2 (LTS), under Python 3.6 - 3.8 (Python 3.5 has reached its end of life); - Django 3.0, under Python 3.6 - 3.8; - Django 3.1, under Python 3.6 - 3.8. Installing django-ldapdb ------------------------ Linux ~~~~~ Use pip: ``pip install django-ldapdb`` You might also need the usual ``LDAP`` packages from your distribution, usually named ``openldap`` or ``ldap-utils``. Windows ~~~~~~~ ``django-ldapdb`` depends on the `python-ldap ` project. Either follow `its Windows installation guide `_, or install a pre-built version from https://www.lfd.uci.edu/~gohlke/pythonlibs/#python-ldap (choose the ``.whl`` file matching your Python/Windows combination, and install it with ``pip install python-ldap-3...whl``). You may then install ``django-ldapdb`` with ``pip install django-ldapdb`` Using django-ldapdb ------------------- Add the following to your ``settings.py``: .. code-block:: python DATABASES = { 'ldap': { 'ENGINE': 'ldapdb.backends.ldap', 'NAME': 'ldap://ldap.nodomain.org/', 'USER': 'cn=admin,dc=nodomain,dc=org', 'PASSWORD': 'some_secret_password', }, 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), }, } DATABASE_ROUTERS = ['ldapdb.router.Router'] If you want to access posixGroup entries in your application, you can add something like this to your ``models.py``: .. code-block:: python from ldapdb.models.fields import CharField, IntegerField, ListField import ldapdb.models class LdapGroup(ldapdb.models.Model): """ Class for representing an LDAP group entry. """ # LDAP meta-data base_dn = "ou=groups,dc=nodomain,dc=org" object_classes = ['posixGroup'] # posixGroup attributes gid = IntegerField(db_column='gidNumber', unique=True) name = CharField(db_column='cn', max_length=200, primary_key=True) members = ListField(db_column='memberUid') def __str__(self): return self.name def __unicode__(self): return self.name and add this to your ``admin.py``: .. code-block:: python from django.contrib import admin from . import models class LDAPGroupAdmin(admin.ModelAdmin): exclude = ['dn', 'objectClass'] list_display = ['gid', 'name'] admin.site.register(models.LDAPGroup, LDAPGroupAdmin) **Important note:** You **must** declare an attribute to be used as the primary key. This attribute will play a special role, as it will be used to build the Relative Distinguished Name of the entry. For instance in the example above, a group whose cn is ``foo`` will have the DN ``cn=foo,ou=groups,dc=nodomain,dc=org``. Supported fields ---------------- djanglo-ldapdb provides the following fields, all imported from ``ldapdb.models.fields``: Similar to Django: * ``IntegerField`` * ``FloatField`` * ``BooleanField`` * ``CharField`` * ``ImageField`` * ``DateTimeField`` Specific to a LDAP server: * ``ListField`` (holds a list of text values) * ``TimestampField`` (Stores a datetime as a posix timestamp, typically for posixAccount) Legacy: * ``DateField`` (Stores a date in an arbitrary format. A LDAP server has no notion of ``Date``). Tuning django-ldapdb -------------------- It is possible to adjust django-ldapdb's behavior by defining a few parameters in the ``DATABASE`` section: ``PAGE_SIZE`` (default: ``1000``) Define the maximum size of a results page to be returned by the server ``QUERY_TIMEOUT`` (default: no limit) Define the maximum time in seconds we'll wait to get a reply from the server (on a per-query basis). .. note:: This setting applies on individual requests; if a high-level operation requires many queries (for instance a paginated search yielding thousands of entries), the timeout will be used on each individual request; the overall processing time might be much higher. Developing with a LDAP server ----------------------------- When developing against a LDAP server, having access to a development LDAP server often proves useful. django-ldapdb uses the `volatildap project `_ for this purpose: - A LDAP server is instantiated for each TestClass; - Its content is reset at the start of each test function; - It can be customized to embark any schemas required by the application; - Starting with volatildap 1.4.0, the volatildap server can be controlled remotely, avoiding the need to install a LDAP server on the host. Applications using django-ldapdb may use the following code snippet when setting up their tests: .. code-block:: python # This snippet is released in the Public Domain from django.conf import settings from django.test import TestCase import volatildap class LdapEnabledTestCase(TestCase): @classmethod def setUpClass(cls): super().setUpClass() cls.ldap = volatildap.LdapServer( # Load some initial data initial={'ou=people': { 'ou': ['people'], 'objectClass': ['organizationalUnit'], }}, # Enable more LDAP schemas schemas=['core.schema', 'cosine.schema', 'inetorgperson.schema', 'nis.schema'], ) # The volatildap server uses specific defaults, and listens on an arbitrary port. # Copy the server-side values to Django settings settings.DATABASES['ldap']['USER'] = cls.ldap.rootdn settings.DATABASES['ldap']['PASSWORD'] = cls.ldap.rootpw settings.DATABASES['ldap']['NAME'] = cls.ldap.uri def setUp(self): super().setUp() # Starting an already-started volatildap server performs a data reset self.ldap.start() @classmethod def tearDownClass(cls): # Free up resources on teardown. cls.ldap.stop() super().tearDownClass() Keywords: django,ldap,database,ldapdb Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Framework :: Django :: 2.2 Classifier: Framework :: Django :: 3.0 Classifier: Framework :: Django :: 3.1 Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.6 Provides-Extra: dev django-ldapdb-1.5.1/django_ldapdb.egg-info/SOURCES.txt0000644000175000017500000000137713741076301023174 0ustar xelnorxelnet00000000000000AUTHORS CODE_OF_CONDUCT.md CONTRIBUTING.rst ChangeLog LICENSE MANIFEST.in Makefile README.rst manage_dev.py setup.cfg setup.py tox.ini django_ldapdb.egg-info/PKG-INFO django_ldapdb.egg-info/SOURCES.txt django_ldapdb.egg-info/dependency_links.txt django_ldapdb.egg-info/not-zip-safe django_ldapdb.egg-info/requires.txt django_ldapdb.egg-info/top_level.txt examples/__init__.py examples/admin.py examples/models.py examples/settings.py examples/tests.py examples/urls.py examples/fixtures/test_users.json ldapdb/__init__.py ldapdb/router.py ldapdb/tests.py ldapdb/version.py ldapdb/backends/__init__.py ldapdb/backends/ldap/__init__.py ldapdb/backends/ldap/base.py ldapdb/backends/ldap/compiler.py ldapdb/models/__init__.py ldapdb/models/base.py ldapdb/models/fields.pydjango-ldapdb-1.5.1/django_ldapdb.egg-info/dependency_links.txt0000644000175000017500000000000113741076301025346 0ustar xelnorxelnet00000000000000 django-ldapdb-1.5.1/django_ldapdb.egg-info/not-zip-safe0000644000175000017500000000000113741076301023526 0ustar xelnorxelnet00000000000000 django-ldapdb-1.5.1/django_ldapdb.egg-info/requires.txt0000644000175000017500000000021213741076301023673 0ustar xelnorxelnet00000000000000Django>=2.2 python-ldap>=3.0 [dev] check-manifest flake8 isort>=5.0.0 tox factory_boy volatildap>=1.1.0 wheel zest.releaser[recommended] django-ldapdb-1.5.1/django_ldapdb.egg-info/top_level.txt0000644000175000017500000000000713741076301024027 0ustar xelnorxelnet00000000000000ldapdb django-ldapdb-1.5.1/examples/0000755000175000017500000000000013741076301016654 5ustar xelnorxelnet00000000000000django-ldapdb-1.5.1/examples/__init__.py0000644000175000017500000000000013741076301020753 0ustar xelnorxelnet00000000000000django-ldapdb-1.5.1/examples/admin.py0000644000175000017500000000237313741076301020323 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project from django import forms from django.contrib import admin from django.contrib.admin.widgets import FilteredSelectMultiple from examples.models import LdapGroup, LdapUser class LdapUserAdmin(admin.ModelAdmin): exclude = ['dn', 'password', 'photo'] list_display = ['username', 'first_name', 'last_name', 'email', 'uid'] search_fields = ['first_name', 'last_name', 'full_name', 'username'] class LdapGroupForm(forms.ModelForm): usernames = forms.ModelMultipleChoiceField( queryset=LdapUser.objects.all(), widget=FilteredSelectMultiple('Users', is_stacked=False), required=False, ) class Meta: exclude = [] model = LdapGroup def clean_usernames(self): data = self.cleaned_data['usernames'] if not data: return [] return list(data.values_list('username', flat=True)) class LdapGroupAdmin(admin.ModelAdmin): form = LdapGroupForm exclude = ['dn', 'usernames', 'member'] list_display = ['name', 'gid'] search_fields = ['name'] admin.site.register(LdapGroup, LdapGroupAdmin) admin.site.register(LdapUser, LdapUserAdmin) django-ldapdb-1.5.1/examples/fixtures/0000755000175000017500000000000013741076301020525 5ustar xelnorxelnet00000000000000django-ldapdb-1.5.1/examples/fixtures/test_users.json0000644000175000017500000000110313741076301023613 0ustar xelnorxelnet00000000000000[ { "pk": "1", "model": "auth.user", "fields": { "username": "test_user", "first_name": "Test", "last_name": "User", "is_active": true, "is_superuser": true, "is_staff": true, "last_login": "2006-12-17 07:03:31", "groups": [], "user_permissions": [], "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", "email": "test_user@example.com", "date_joined": "2006-12-17 07:03:31" } } ] django-ldapdb-1.5.1/examples/models.py0000644000175000017500000000627313741076301020521 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project import ldapdb.models from ldapdb.models import fields class LdapUser(ldapdb.models.Model): """ Class for representing an LDAP user entry. """ # LDAP meta-data base_dn = "ou=people,dc=example,dc=org" object_classes = ['posixAccount', 'shadowAccount', 'inetOrgPerson'] last_modified = fields.DateTimeField(db_column='modifyTimestamp', editable=False) # inetOrgPerson first_name = fields.CharField(db_column='givenName', verbose_name="Prime name") last_name = fields.CharField("Final name", db_column='sn') full_name = fields.CharField(db_column='cn') email = fields.CharField(db_column='mail') phone = fields.CharField(db_column='telephoneNumber', blank=True) mobile_phone = fields.CharField(db_column='mobile', blank=True) photo = fields.ImageField(db_column='jpegPhoto') # posixAccount uid = fields.IntegerField(db_column='uidNumber', unique=True) group = fields.IntegerField(db_column='gidNumber') gecos = fields.CharField(db_column='gecos') home_directory = fields.CharField(db_column='homeDirectory') login_shell = fields.CharField(db_column='loginShell', default='/bin/bash') username = fields.CharField(db_column='uid', primary_key=True) password = fields.CharField(db_column='userPassword') # shadowAccount last_password_change = fields.TimestampField(db_column='shadowLastChange') def __str__(self): return self.username def __unicode__(self): return self.full_name class LdapGroup(ldapdb.models.Model): """ Class for representing an LDAP group entry. """ # LDAP meta-data base_dn = "ou=groups,dc=example,dc=org" object_classes = ['posixGroup'] # posixGroup attributes gid = fields.IntegerField(db_column='gidNumber', unique=True) name = fields.CharField(db_column='cn', max_length=200, primary_key=True) usernames = fields.ListField(db_column='memberUid') def __str__(self): return self.name def __unicode__(self): return self.name class LdapMultiPKRoom(ldapdb.models.Model): """ Class for representing a room, using a composite primary key. """ # LDAP meta-data base_dn = "ou=rooms,dc=example,dc=org" object_classes = ['room'] # room attributes name = fields.CharField(db_column='cn', max_length=200, primary_key=True) number = fields.CharField(db_column='roomNumber', max_length=10, primary_key=True) phone = fields.CharField(db_column='telephoneNumber', max_length=20, blank=True, null=True) def __str__(self): return "%s (%s)" % (self.name, self.number) class AbstractGroup(ldapdb.models.Model): class Meta: abstract = True object_classes = ['posixGroup'] gid = fields.IntegerField(db_column='gidNumber', unique=True) name = fields.CharField(db_column='cn', max_length=200, primary_key=True) usernames = fields.ListField(db_column='memberUid') def __str__(self): return self.name def __unicode__(self): return self.name class ConcreteGroup(AbstractGroup): base_dn = "ou=groups,dc=example,dc=org" django-ldapdb-1.5.1/examples/settings.py0000644000175000017500000000660513741076301021075 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project import ldap DEBUG = True DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'ldapdb.db', 'USER': '', 'PASSWORD': '', 'HOST': '', 'PORT': '', }, 'ldap': { 'ENGINE': 'ldapdb.backends.ldap', 'NAME': 'ldap://localhost', 'USER': 'cn=admin,dc=nodomain', 'PASSWORD': 'test', # 'TLS': True, 'CONNECTION_OPTIONS': { ldap.OPT_X_TLS_DEMAND: True, } } } DATABASE_ROUTERS = ['ldapdb.router.Router'] # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. # On Unix systems, a value of None will cause Django to use the same # timezone as the operating system. # If running in a Windows environment this must be set to the same as your # system time zone. TIME_ZONE = 'America/Chicago' # Language code for this installation. All choices can be found here: # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en-us' # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. USE_I18N = True # If you set this to False, Django will not format dates, numbers and # calendars according to the current locale USE_L10N = True USE_TZ = True # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: "/home/media/media.lawrence.com/" MEDIA_ROOT = '' # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash if there is a path component (optional in other cases). # Examples: "http://media.lawrence.com", "http://example.com/media/" MEDIA_URL = '' # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". ADMIN_MEDIA_PREFIX = '/media/' # Make this unique, and don't share it with anybody. SECRET_KEY = 'some_random_secret_key' MIDDLEWARE = [ 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', ] ROOT_URLCONF = 'examples.urls' STATIC_URL = '/static/' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.contrib.auth.context_processors.auth', 'django.template.context_processors.debug', 'django.template.context_processors.i18n', 'django.template.context_processors.media', 'django.template.context_processors.request', 'django.template.context_processors.static', 'django.template.context_processors.tz', 'django.contrib.messages.context_processors.messages', ], }, }, ] INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'ldapdb', 'examples', 'django.contrib.admin', ) django-ldapdb-1.5.1/examples/tests.py0000644000175000017500000010144013741076301020370 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project import time import factory import factory.django import factory.fuzzy import ldap import volatildap from django.conf import settings from django.contrib.auth import hashers as auth_hashers from django.contrib.auth import models as auth_models from django.core import management from django.db import connections from django.db.models import Count, Q from django.test import TestCase from django.utils import timezone from examples.models import ConcreteGroup, LdapGroup, LdapMultiPKRoom, LdapUser from ldapdb.backends.ldap.compiler import SQLCompiler, query_as_ldap groups = ('ou=groups,dc=example,dc=org', { 'objectClass': ['top', 'organizationalUnit'], 'ou': ['groups']}) people = ('ou=people,dc=example,dc=org', { 'objectClass': ['top', 'organizationalUnit'], 'ou': ['groups']}) contacts = ('ou=contacts,ou=groups,dc=example,dc=org', { 'objectClass': ['top', 'organizationalUnit'], 'ou': ['groups']}) users = ('ou=users,ou=people,dc=example,dc=org', { 'objectClass': ['top', 'organizationalUnit'], 'ou': ['users']}) rooms = ('ou=rooms,dc=example,dc=org', { 'objectClass': ['top', 'organizationalUnit'], 'ou': ['rooms']}) foogroup = ('cn=foogroup,ou=groups,dc=example,dc=org', { 'objectClass': ['posixGroup'], 'memberUid': ['foouser', 'baruser'], 'gidNumber': ['1000'], 'cn': ['foogroup']}) bargroup = ('cn=bargroup,ou=groups,dc=example,dc=org', { 'objectClass': ['posixGroup'], 'memberUid': ['zoouser', 'baruser'], 'gidNumber': ['1001'], 'cn': ['bargroup']}) wizgroup = ('cn=wizgroup,ou=groups,dc=example,dc=org', { 'objectClass': ['posixGroup'], 'memberUid': ['wizuser', 'baruser'], 'gidNumber': ['1002'], 'cn': ['wizgroup']}) foouser = ('uid=foouser,ou=people,dc=example,dc=org', { 'cn': [b'F\xc3\xb4o Us\xc3\xa9r'], 'objectClass': ['posixAccount', 'shadowAccount', 'inetOrgPerson'], 'loginShell': ['/bin/bash'], 'jpegPhoto': [ b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xff' b'\xfe\x00\x1cCreated with GIMP on a Mac\xff\xdb\x00C\x00\x05\x03\x04' b'\x04\x04\x03\x05\x04\x04\x04\x05\x05\x05\x06\x07\x0c\x08\x07\x07\x07' b'\x07\x0f\x0b\x0b\t\x0c\x11\x0f\x12\x12\x11\x0f\x11\x11\x13\x16\x1c' b'\x17\x13\x14\x1a\x15\x11\x11\x18!\x18\x1a\x1d\x1d\x1f\x1f\x1f\x13' b'\x17"$"\x1e$\x1c\x1e\x1f\x1e\xff\xdb\x00C\x01\x05\x05\x05\x07\x06\x07' b'\x0e\x08\x08\x0e\x1e\x14\x11\x14\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' b'\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' b'\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' b'\x1e\x1e\x1e\x1e\x1e\x1e\x1e\xff\xc0\x00\x11\x08\x00\x08\x00\x08\x03' b'\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x15\x00\x01\x01\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00' b'\x19\x10\x00\x03\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x01\x02\x06\x11A\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x14\x11\x01' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff' b'\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\x9d\xf29wU5Q\xd6' b'\xfd\x00\x01\xff\xd9'], 'uidNumber': ['2000'], 'gidNumber': ['1000'], 'sn': [b'Us\xc3\xa9r'], 'homeDirectory': ['/home/foouser'], 'givenName': [b'F\xc3\xb4o'], 'uid': ['foouser']}) baruser = ('uid=baruser,ou=users,ou=people,dc=example,dc=org', { 'objectClass': ['posixAccount', 'shadowAccount', 'inetOrgPerson'], 'cn': ['Bar Test'], 'givenName': ['Test'], 'sn': ['Bar'], 'uid': ['baruser'], 'uidNumber': ['2001'], 'gidNumber': ['1000'], 'homeDirectory': ['/home/baruser'], 'loginShell': ['/bin/bash'], 'jpegPhoto': []}) class UserFactory(factory.django.DjangoModelFactory): username = factory.Faker('username') email = factory.Faker('email') is_active = True password = factory.LazyAttribute(lambda o: auth_hashers.make_password(o.cleartext_password)) class Meta: model = auth_models.User class Params: cleartext_password = factory.fuzzy.FuzzyText(30) superuser = factory.Trait( is_staff=True, is_superuser=True, ) class BaseTestCase(TestCase): directory = {} databases = ['default', 'ldap'] @classmethod def setUpClass(cls): super().setUpClass() cls.ldap_server = volatildap.LdapServer( initial_data=cls.directory, schemas=['core.schema', 'cosine.schema', 'inetorgperson.schema', 'nis.schema'], ) settings.DATABASES['ldap']['USER'] = cls.ldap_server.rootdn settings.DATABASES['ldap']['PASSWORD'] = cls.ldap_server.rootpw settings.DATABASES['ldap']['NAME'] = cls.ldap_server.uri @classmethod def tearDownClass(cls): cls.ldap_server.stop() super().tearDownClass() def setUp(self): super().setUp() self.ldap_server.start() class ConnectionTestCase(BaseTestCase): directory = dict([people, foouser]) def test_system_checks(self): management.call_command('check') def test_make_migrations(self): management.call_command('makemigrations', dry_run=True) def test_connection_options(self): LdapUser.objects.get(username='foouser') # self.assertEqual(self.ldapobj.get_option(ldap.OPT_X_TLS_DEMAND), True) def test_start_tls(self): # self.assertFalse(self.ldapobj.tls_enabled) LdapUser.objects.get(username='foouser') def test_bound_as_admin(self): LdapUser.objects.get(username='foouser') # self.assertEqual(self.ldapobj.bound_as, admin[0]) def test_reconect(self): LdapUser.objects.get(username='foouser') self.ldap_server.stop() self.ldap_server.start() LdapUser.objects.get(username='foouser') class GroupTestCase(BaseTestCase): directory = dict([groups, foogroup, bargroup, wizgroup, people, foouser]) def test_count_none(self): qs = LdapGroup.objects.none() self.assertEqual(qs.count(), 0) def test_count_all(self): qs = LdapGroup.objects.all() self.assertEqual(qs.count(), 3) def test_aggregate_count(self): qs = LdapGroup.objects.all() result = qs.aggregate(num_groups=Count('name')) self.assertEqual(result['num_groups'], 3) def test_annotate_count(self): groups = LdapGroup.objects.order_by('name').annotate(num_usernames=Count('usernames')) self.assertEqual(len(groups), 3) self.assertEqual(groups[0].name, 'bargroup') self.assertEqual(groups[0].num_usernames, 2) self.assertEqual(groups[1].name, 'foogroup') self.assertEqual(groups[1].num_usernames, 2) self.assertEqual(groups[2].name, 'wizgroup') self.assertEqual(groups[2].num_usernames, 2) groups = LdapGroup.objects.filter(name='foogroup').annotate(num_usernames=Count('usernames')) self.assertEqual(len(groups), 1) self.assertEqual(groups[0].name, 'foogroup') self.assertEqual(groups[0].num_usernames, 2) groups = LdapGroup.objects.annotate(num_usernames=Count('usernames')).filter(name='foogroup') self.assertEqual(len(groups), 1) self.assertEqual(groups[0].name, 'foogroup') self.assertEqual(groups[0].num_usernames, 2) def test_length_all(self): qs = LdapGroup.objects.all() self.assertEqual(len(qs), 3) def test_length_none(self): qs = LdapGroup.objects.none() self.assertEqual(len(qs), 0) def test_ldap_filter(self): def get_filterstr(qs): connection = connections['ldap'] compiler = SQLCompiler( query=qs.query, connection=connection, using=None, ) return query_as_ldap(qs.query, compiler, connection).filterstr # single filter qs = LdapGroup.objects.filter(name='foogroup') self.assertEqual(get_filterstr(qs), '(&(objectClass=posixGroup)(cn=foogroup))') qs = LdapGroup.objects.filter(Q(name='foogroup')) self.assertEqual(get_filterstr(qs), '(&(objectClass=posixGroup)(cn=foogroup))') # AND filter qs = LdapGroup.objects.filter(gid=1000, name='foogroup') self.assertIn(get_filterstr(qs), [ '(&(objectClass=posixGroup)(&(gidNumber=1000)(cn=foogroup)))', '(&(objectClass=posixGroup)(&(cn=foogroup)(gidNumber=1000)))', ]) qs = LdapGroup.objects.filter(Q(gid=1000) & Q(name='foogroup')) self.assertIn(get_filterstr(qs), [ '(&(objectClass=posixGroup)(&(gidNumber=1000)(cn=foogroup)))', '(&(objectClass=posixGroup)(&(cn=foogroup)(gidNumber=1000)))', ]) # OR filter qs = LdapGroup.objects.filter(Q(gid=1000) | Q(name='foogroup')) self.assertIn(get_filterstr(qs), [ '(&(objectClass=posixGroup)(|(gidNumber=1000)(cn=foogroup)))', '(&(objectClass=posixGroup)(|(cn=foogroup)(gidNumber=1000)))', ]) # single exclusion qs = LdapGroup.objects.exclude(name='foogroup') self.assertEqual(get_filterstr(qs), '(&(objectClass=posixGroup)(!(cn=foogroup)))') qs = LdapGroup.objects.filter(~Q(name='foogroup')) self.assertEqual(get_filterstr(qs), '(&(objectClass=posixGroup)(!(cn=foogroup)))') # multiple exclusion qs = LdapGroup.objects.exclude(name='foogroup', gid=1000) self.assertIn(get_filterstr(qs), [ '(&(objectClass=posixGroup)(!(&(gidNumber=1000)(cn=foogroup))))', '(&(objectClass=posixGroup)(!(&(cn=foogroup)(gidNumber=1000))))', ]) qs = LdapGroup.objects.filter(name='foogroup').exclude(gid=1000) self.assertIn(get_filterstr(qs), [ '(&(objectClass=posixGroup)(&(cn=foogroup)(!(gidNumber=1000))))', '(&(objectClass=posixGroup)(&(!(gidNumber=1000))(cn=foogroup)))', ]) def test_filter(self): qs = LdapGroup.objects.filter(name='foogroup') self.assertEqual(qs.count(), 1) qs = LdapGroup.objects.filter(name='foogroup') self.assertEqual(len(qs), 1) g = qs[0] self.assertEqual(g.dn, 'cn=foogroup,%s' % LdapGroup.base_dn) self.assertEqual(g.name, 'foogroup') self.assertEqual(g.gid, 1000) self.assertCountEqual(g.usernames, ['foouser', 'baruser']) # try to filter non-existent entries qs = LdapGroup.objects.filter(name='does_not_exist') self.assertEqual(qs.count(), 0) qs = LdapGroup.objects.filter(name='does_not_exist') self.assertEqual(len(qs), 0) def test_get(self): g = LdapGroup.objects.get(name='foogroup') self.assertEqual(g.dn, 'cn=foogroup,%s' % LdapGroup.base_dn) self.assertEqual(g.name, 'foogroup') self.assertEqual(g.gid, 1000) self.assertCountEqual(g.usernames, ['foouser', 'baruser']) # try to get a non-existent entry self.assertRaises(LdapGroup.DoesNotExist, LdapGroup.objects.get, name='does_not_exist') def test_exists(self): qs = LdapGroup.objects.filter(name='foogroup') self.assertTrue(qs.exists()) qs2 = LdapGroup.objects.filter(name='missing') self.assertFalse(qs2.exists()) def test_get_by_dn(self): g = LdapGroup.objects.get(dn='cn=foogroup,%s' % LdapGroup.base_dn) self.assertEqual(g.dn, 'cn=foogroup,%s' % LdapGroup.base_dn) self.assertEqual(g.name, 'foogroup') self.assertEqual(g.gid, 1000) self.assertCountEqual(g.usernames, ['foouser', 'baruser']) def test_gid_lookup(self): g = LdapGroup.objects.get(gid__in=[1000, 2000, 3000]) self.assertEqual(g.dn, 'cn=foogroup,%s' % LdapGroup.base_dn) self.assertEqual(g.name, 'foogroup') self.assertEqual(g.gid, 1000) self.assertCountEqual(g.usernames, ['foouser', 'baruser']) def test_insert(self): g = LdapGroup() g.name = 'newgroup' g.gid = 1010 g.usernames = ['someuser', 'foouser'] g.save() # check group was created new = LdapGroup.objects.get(name='newgroup') self.assertEqual(new.name, 'newgroup') self.assertEqual(new.gid, 1010) self.assertCountEqual(new.usernames, ['someuser', 'foouser']) def test_create(self): LdapGroup.objects.create( name='newgroup', gid=1010, usernames=['someuser', 'foouser'], ) # check group was created new = LdapGroup.objects.get(name='newgroup') self.assertEqual(new.name, 'newgroup') self.assertEqual(new.gid, 1010) self.assertCountEqual(new.usernames, ['someuser', 'foouser']) def test_order_by(self): # ascending name qs = LdapGroup.objects.order_by('name') self.assertEqual(len(qs), 3) self.assertEqual(qs[0].name, 'bargroup') self.assertEqual(qs[1].name, 'foogroup') self.assertEqual(qs[2].name, 'wizgroup') # descending name qs = LdapGroup.objects.order_by('-name') self.assertEqual(len(qs), 3) self.assertEqual(qs[0].name, 'wizgroup') self.assertEqual(qs[1].name, 'foogroup') self.assertEqual(qs[2].name, 'bargroup') # ascending gid qs = LdapGroup.objects.order_by('gid') self.assertEqual(len(qs), 3) self.assertEqual(qs[0].gid, 1000) self.assertEqual(qs[1].gid, 1001) self.assertEqual(qs[2].gid, 1002) # descending gid qs = LdapGroup.objects.order_by('-gid') self.assertEqual(len(qs), 3) self.assertEqual(qs[0].gid, 1002) self.assertEqual(qs[1].gid, 1001) self.assertEqual(qs[2].gid, 1000) # ascending pk qs = LdapGroup.objects.order_by('pk') self.assertEqual(len(qs), 3) self.assertEqual(qs[0].name, 'bargroup') self.assertEqual(qs[1].name, 'foogroup') self.assertEqual(qs[2].name, 'wizgroup') # descending pk qs = LdapGroup.objects.order_by('-pk') self.assertEqual(len(qs), 3) self.assertEqual(qs[0].name, 'wizgroup') self.assertEqual(qs[1].name, 'foogroup') self.assertEqual(qs[2].name, 'bargroup') # ascending dn qs = LdapGroup.objects.order_by('dn') self.assertEqual(len(qs), 3) self.assertEqual(qs[0].name, 'bargroup') self.assertEqual(qs[1].name, 'foogroup') self.assertEqual(qs[2].name, 'wizgroup') # descending dn qs = LdapGroup.objects.order_by('-dn') self.assertEqual(len(qs), 3) self.assertEqual(qs[0].name, 'wizgroup') self.assertEqual(qs[1].name, 'foogroup') self.assertEqual(qs[2].name, 'bargroup') def test_bulk_delete(self): LdapGroup.objects.all().delete() qs = LdapGroup.objects.all() self.assertEqual(len(qs), 0) def test_bulk_delete_none(self): LdapGroup.objects.none().delete() qs = LdapGroup.objects.all() self.assertEqual(len(qs), 3) def test_slice(self): qs = LdapGroup.objects.order_by('gid') objs = list(qs) self.assertEqual(len(objs), 3) self.assertEqual(objs[0].gid, 1000) self.assertEqual(objs[1].gid, 1001) self.assertEqual(objs[2].gid, 1002) # limit only qs = LdapGroup.objects.order_by('gid') objs = qs[:2] self.assertEqual(objs.count(), 2) objs = qs[:2] self.assertEqual(len(objs), 2) self.assertEqual(objs[0].gid, 1000) self.assertEqual(objs[1].gid, 1001) # offset only qs = LdapGroup.objects.order_by('gid') objs = qs[1:] self.assertEqual(objs.count(), 2) objs = qs[1:] self.assertEqual(len(objs), 2) self.assertEqual(objs[0].gid, 1001) self.assertEqual(objs[1].gid, 1002) # offset and limit qs = LdapGroup.objects.order_by('gid') objs = qs[1:2] self.assertEqual(objs.count(), 1) objs = qs[1:2] self.assertEqual(len(objs), 1) self.assertEqual(objs[0].gid, 1001) def test_update(self): g = LdapGroup.objects.get(name='foogroup') g.gid = 1002 g.usernames = ['foouser2', u'baruseeer2'] g.save() # check group was updated new = LdapGroup.objects.get(name='foogroup') self.assertEqual(new.name, 'foogroup') self.assertEqual(new.gid, 1002) self.assertCountEqual(new.usernames, ['foouser2', u'baruseeer2']) def test_update_change_dn(self): g = LdapGroup.objects.get(name='foogroup') g.name = 'foogroup2' g.save() self.assertEqual(g.dn, 'cn=foogroup2,%s' % LdapGroup.base_dn) # check group was updated new = LdapGroup.objects.get(name='foogroup2') self.assertEqual(new.name, 'foogroup2') self.assertEqual(new.gid, 1000) self.assertCountEqual(new.usernames, ['foouser', 'baruser']) def test_values(self): qs = sorted(LdapGroup.objects.values_list('name', flat=True)) self.assertEqual(len(qs), 3) self.assertEqual(qs[0], 'bargroup') self.assertEqual(qs[1], 'foogroup') self.assertEqual(qs[2], 'wizgroup') def test_search(self): qs = sorted(LdapGroup.objects.filter(name__contains='foo')) self.assertEqual(len(qs), 1) self.assertEqual(qs[0].name, 'foogroup') def test_values_list(self): qs = sorted(LdapGroup.objects.values_list('name')) self.assertEqual(len(qs), 3) self.assertEqual(list(qs[0]), ['bargroup']) self.assertEqual(list(qs[1]), ['foogroup']) self.assertEqual(list(qs[2]), ['wizgroup']) def test_delete(self): g = LdapGroup.objects.get(name='foogroup') g.delete() qs = LdapGroup.objects.all() self.assertEqual(len(qs), 2) def test_paginated_search(self): # This test will change the settings, assert we don't break things self.assertIsNone(settings.DATABASES['ldap'].get('CONNECTION_OPTIONS', {}).get('page_size')) # Test without BATCH_SIZE qs = LdapGroup.objects.filter(name__contains='group').order_by('name') self.assertEqual(len(qs), 3) self.assertEqual(qs[0].name, 'bargroup') self.assertEqual(qs[1].name, 'foogroup') self.assertEqual(qs[2].name, 'wizgroup') # Set new page size settings.DATABASES['ldap']['CONNECTION_OPTIONS'] = settings.DATABASES['ldap'].get('CONNECTION_OPTIONS', {}) settings.DATABASES['ldap']['CONNECTION_OPTIONS']['page_size'] = 1 connections['ldap'].close() # Force connection reload qs = LdapGroup.objects.filter(name__contains='group').order_by('name') self.assertEqual(len(qs), 3) self.assertEqual(qs[0].name, 'bargroup') self.assertEqual(qs[1].name, 'foogroup') self.assertEqual(qs[2].name, 'wizgroup') # Restore previous configuration del settings.DATABASES['ldap']['CONNECTION_OPTIONS']['page_size'] def test_listfield(self): g = LdapGroup.objects.get(name='foogroup') self.assertCountEqual(['foouser', 'baruser'], g.usernames) # Try to filter on the list field, with and without contains qs = LdapGroup.objects.filter(usernames='foouser') self.assertEqual(qs.count(), 1) qs = LdapGroup.objects.filter(usernames__contains='foouser') self.assertEqual(qs.count(), 1) # Try to filter negatively on the list field, with and without contains qs = LdapGroup.objects.filter(~Q(usernames='foouser')) self.assertEqual(qs.count(), 2) qs = LdapGroup.objects.filter(~Q(usernames__contains='foouser')) self.assertEqual(qs.count(), 2) # Try to exclude on the list field, with and without contains qs = LdapGroup.objects.exclude(usernames='foouser') self.assertEqual(qs.count(), 2) qs = LdapGroup.objects.exclude(usernames__contains='foouser') self.assertEqual(qs.count(), 2) def test_listfield_manipulation(self): g = LdapGroup.objects.get(name='foogroup') self.assertCountEqual(['foouser', 'baruser'], g.usernames) # Replace values, with duplicated. g.usernames = ['john', 'jane', 'john'] g.save() g = LdapGroup.objects.get(name='foogroup') self.assertCountEqual(['john', 'jane'], g.usernames) # Clear values g.usernames = [] g.save() g = LdapGroup.objects.get(name='foogroup') self.assertEqual([], g.usernames) class GroupSubclassingTestCase(BaseTestCase): directory = dict([groups, foogroup, bargroup, wizgroup, people, foouser]) def test_concrete_group(self): g = ConcreteGroup.objects.get(name='foogroup') self.assertCountEqual(['foouser', 'baruser'], g.usernames) g.name = 'modified' g.save() g = ConcreteGroup.objects.get(name='modified') self.assertCountEqual(['foouser', 'baruser'], g.usernames) class UserTestCase(BaseTestCase): directory = dict([groups, people, users, foouser, baruser]) def test_verbose_name(self): self.assertEqual("Prime name", LdapUser._meta.get_field('first_name').verbose_name) self.assertEqual("Final name", LdapUser._meta.get_field('last_name').verbose_name) def test_get(self): u = LdapUser.objects.get(username='foouser') self.assertEqual(u.first_name, u'Fôo') self.assertEqual(u.last_name, u'Usér') self.assertEqual(u.full_name, u'Fôo Usér') self.assertEqual(u.group, 1000) self.assertEqual(u.home_directory, '/home/foouser') self.assertEqual(u.uid, 2000) self.assertEqual(u.username, 'foouser') self.assertEqual( u.photo, b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01' b'\x01\x00H\x00H\x00\x00\xff\xfe\x00\x1cCreated with ' b'GIMP on a Mac\xff\xdb\x00C\x00\x05\x03\x04\x04\x04' b'\x03\x05\x04\x04\x04\x05\x05\x05\x06\x07\x0c\x08' b'\x07\x07\x07\x07\x0f\x0b\x0b\t\x0c\x11\x0f\x12\x12' b'\x11\x0f\x11\x11\x13\x16\x1c\x17\x13\x14\x1a\x15' b'\x11\x11\x18!\x18\x1a\x1d\x1d\x1f\x1f\x1f\x13\x17' b'"$"\x1e$\x1c\x1e\x1f\x1e\xff\xdb\x00C\x01\x05\x05' b'\x05\x07\x06\x07\x0e\x08\x08\x0e\x1e\x14\x11\x14' b'\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' b'\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' b'\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' b'\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' b'\x1e\x1e\xff\xc0\x00\x11\x08\x00\x08\x00\x08\x03' b'\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x15' b'\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x19\x10' b'\x00\x03\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x01\x02\x06\x11A\xff\xc4\x00' b'\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x14\x11' b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00' b'\x02\x11\x03\x11\x00?\x00\x9d\xf29wU5Q\xd6\xfd\x00' b'\x01\xff\xd9', ) self.assertRaises(LdapUser.DoesNotExist, LdapUser.objects.get, username='does_not_exist') def test_update(self): # slapd removes microsecond details. before = timezone.now().replace(microsecond=0) u = LdapUser.objects.get(username='foouser') u.first_name = u'Fôo2' u.save() after = timezone.now().replace(microsecond=0) self.assertLessEqual(before, u.last_modified) self.assertLessEqual(u.last_modified, after) # make sure DN gets updated if we change the pk u.username = 'foouser2' u.save() self.assertEqual(u.dn, 'uid=foouser2,%s' % LdapUser.base_dn) def test_charfield_empty_values(self): """CharField should accept empty values.""" u = LdapUser.objects.get(username='foouser') # Both '' and None are accepted u.phone = '' u.mobile_phone = None u.save() # '' and None are normalized to ''. u2 = LdapUser.objects.get(dn=u.dn) self.assertEqual('', u2.phone) self.assertEqual('', u2.mobile_phone) def test_intfield_empty_value(self): u = LdapUser.objects.get(username='foouser') # Set to 0 u.uid = 0 u.save() # Ensure we still fetch a '0', not an empty ID. u2 = LdapUser.objects.get(dn=u.dn) self.assertEqual(0, u2.uid) def test_datetime_lookup(self): # Due to slapd ignoring microsecond in last_modified, # wait for one second to ensure that all timestamps are on the proper # side of the 'before' boundary. time.sleep(1) before = timezone.now().replace(microsecond=0) qs = LdapUser.objects.filter(last_modified__gte=before) self.assertEqual([], list(qs)) u = LdapUser.objects.get(username='foouser') u.first_name = u"Foo2" u.save() u = LdapUser.objects.get(username='foouser') self.assertLessEqual(before, u.last_modified) qs = LdapUser.objects.filter(last_modified__gte=before) self.assertEqual([u], list(qs)) # Test __in lookup lm = u.last_modified u = LdapUser.objects.get(last_modified__in=[before, lm]) self.assertEqual(u.username, 'foouser') def test_dn_consistency(self): u = LdapUser.objects.get(username='baruser') u.first_name = u"Barr" try: u.save() except ldap.LDAPError: self.fail("Cannot save object") # DN shouldn't be changed self.assertEqual( LdapUser.objects.get(username='baruser').dn, 'uid=baruser,ou=users,ou=people,dc=example,dc=org') class ScopedTestCase(BaseTestCase): directory = dict([groups, people, foogroup, contacts]) def setUp(self): super().setUp() self.scoped_model = LdapGroup.scoped("ou=contacts,%s" % LdapGroup.base_dn) def test_scope(self): ScopedGroup = self.scoped_model qs = LdapGroup.objects.all() self.assertEqual(qs.count(), 1) qs = ScopedGroup.objects.all() self.assertEqual(qs.count(), 0) # create scoped group g2 = ScopedGroup() g2.name = "scopedgroup" g2.gid = 5000 g2.save() qs = LdapGroup.objects.all() self.assertEqual(qs.count(), 2) qs = ScopedGroup.objects.all() self.assertEqual(qs.count(), 1) g2 = ScopedGroup.objects.get(name="scopedgroup") self.assertEqual(g2.name, u'scopedgroup') self.assertEqual(g2.gid, 5000) class CompositePKTests(BaseTestCase): directory = dict([rooms]) def test_create(self): room = LdapMultiPKRoom( name="Director office", number="42.01", ) room.save() room = LdapMultiPKRoom.objects.get() self.assertEqual("cn=Director office+roomNumber=42.01,ou=rooms,dc=example,dc=org", room.dn) def test_fetch(self): room = LdapMultiPKRoom( name="Director office", number="42.01", ) room.save() room2 = LdapMultiPKRoom.objects.get(number="42.01") self.assertEqual("Director office", room2.name) self.assertEqual("42.01", room2.number) def test_move(self): room = LdapMultiPKRoom.objects.create( name="Director office", number="42.01", ) room.number = "42.02" room.save() qs = LdapMultiPKRoom.objects.all() self.assertEqual(1, len(qs)) new_room = qs.get() self.assertEqual(room, new_room) self.assertEqual("42.02", new_room.number) self.assertEqual("cn=Director office+roomNumber=42.02,ou=rooms,dc=example,dc=org", new_room.dn) def test_update(self): room = LdapMultiPKRoom.objects.create( name="Director office", number="42.01", phone='+001234', ) room.phone = '+004444' room.save() qs = LdapMultiPKRoom.objects.all() self.assertEqual(1, len(qs)) new_room = qs.get() self.assertEqual(room, new_room) self.assertEqual("42.01", new_room.number) self.assertEqual('+004444', new_room.phone) self.assertEqual("cn=Director office+roomNumber=42.01,ou=rooms,dc=example,dc=org", new_room.dn) def test_update_ambiguous_pk(self): """Updating an object where two entries with close pks exist shouldn't fail. See #159. """ room1 = LdapMultiPKRoom.objects.create( name="Director office", number="42.01", phone='+001234', ) LdapMultiPKRoom.objects.create( name="Director office", number="42.01b", phone='+001111', ) room1.phone = '+004444' room1.save() qs = LdapMultiPKRoom.objects.all() self.assertEqual(2, len(qs)) new_room = qs.get(number="42.01") self.assertEqual("42.01", new_room.number) self.assertEqual('+004444', new_room.phone) self.assertEqual("cn=Director office+roomNumber=42.01,ou=rooms,dc=example,dc=org", new_room.dn) class AdminTestCase(BaseTestCase): directory = dict([groups, people, foouser, foogroup, bargroup]) def setUp(self): super().setUp() self._user = UserFactory( username='test_user', cleartext_password='password', superuser=True, ) self.client.login(username="test_user", password="password") def test_index(self): response = self.client.get('/admin/examples/') self.assertContains(response, "Ldap groups") self.assertContains(response, "Ldap users") def test_group_list(self): response = self.client.get('/admin/examples/ldapgroup/') self.assertContains(response, "Ldap groups") self.assertContains(response, "foogroup") self.assertContains(response, "1000") # order by name response = self.client.get('/admin/examples/ldapgroup/?o=1') self.assertContains(response, "Ldap groups") self.assertContains(response, "foogroup") self.assertContains(response, "1000") # order by gid response = self.client.get('/admin/examples/ldapgroup/?o=2') self.assertContains(response, "Ldap groups") self.assertContains(response, "foogroup") self.assertContains(response, "1000") def test_group_detail(self): response = self.client.get('/admin/examples/ldapgroup/foogroup/', follow=True) self.assertContains(response, "foogroup") self.assertContains(response, "1000") def test_group_add(self): response = self.client.post('/admin/examples/ldapgroup/add/', {'gid': '1002', 'name': 'wizgroup'}) self.assertRedirects(response, '/admin/examples/ldapgroup/') qs = LdapGroup.objects.all() self.assertEqual(qs.count(), 3) def test_group_delete(self): response = self.client.post( '/admin/examples/ldapgroup/foogroup/delete/', {'yes': 'post'}) self.assertRedirects(response, '/admin/examples/ldapgroup/') qs = LdapGroup.objects.all() self.assertEqual(qs.count(), 1) def test_group_search(self): response = self.client.get('/admin/examples/ldapgroup/?q=foo') self.assertContains(response, "Ldap groups") self.assertContains(response, "foogroup") self.assertContains(response, "1000") def test_user_list(self): response = self.client.get('/admin/examples/ldapuser/') self.assertContains(response, "Ldap users") self.assertContains(response, "foouser") self.assertContains(response, "2000") # order by username response = self.client.get('/admin/examples/ldapuser/?o=1') self.assertContains(response, "Ldap users") self.assertContains(response, "foouser") self.assertContains(response, "2000") # order by uid response = self.client.get('/admin/examples/ldapuser/?o=2') self.assertContains(response, "Ldap users") self.assertContains(response, "foouser") self.assertContains(response, "2000") def test_user_detail(self): response = self.client.get('/admin/examples/ldapuser/foouser/', follow=True) self.assertContains(response, "foouser") self.assertContains(response, "2000") def test_user_delete(self): response = self.client.post('/admin/examples/ldapuser/foouser/delete/', {'yes': 'post'}) self.assertRedirects(response, '/admin/examples/ldapuser/') django-ldapdb-1.5.1/examples/urls.py0000644000175000017500000000053413741076301020215 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project from django.contrib import admin from django.urls import include, path admin.autodiscover() urlpatterns = [ path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/', admin.site.urls), ] django-ldapdb-1.5.1/ldapdb/0000755000175000017500000000000013741076301016264 5ustar xelnorxelnet00000000000000django-ldapdb-1.5.1/ldapdb/__init__.py0000644000175000017500000000137413741076301020402 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project import ldap.filter from django.conf import settings from .version import __version__ # noqa def escape_ldap_filter(value): return ldap.filter.escape_filter_chars(str(value)) # Legacy single database support if hasattr(settings, 'LDAPDB_SERVER_URI'): from django import db from ldapdb.router import Router # Add the LDAP backend settings.DATABASES['ldap'] = { 'ENGINE': 'ldapdb.backends.ldap', 'NAME': settings.LDAPDB_SERVER_URI, 'USER': settings.LDAPDB_BIND_DN, 'PASSWORD': settings.LDAPDB_BIND_PASSWORD} # Add the LDAP router db.router.routers.append(Router()) django-ldapdb-1.5.1/ldapdb/backends/0000755000175000017500000000000013741076301020036 5ustar xelnorxelnet00000000000000django-ldapdb-1.5.1/ldapdb/backends/__init__.py0000644000175000017500000000000013741076301022135 0ustar xelnorxelnet00000000000000django-ldapdb-1.5.1/ldapdb/backends/ldap/0000755000175000017500000000000013741076301020756 5ustar xelnorxelnet00000000000000django-ldapdb-1.5.1/ldapdb/backends/ldap/__init__.py0000644000175000017500000000000013741076301023055 0ustar xelnorxelnet00000000000000django-ldapdb-1.5.1/ldapdb/backends/ldap/base.py0000644000175000017500000002651613741076301022254 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project import django import ldap import ldap.controls from django.db.backends.base.base import BaseDatabaseWrapper from django.db.backends.base.client import BaseDatabaseClient from django.db.backends.base.creation import BaseDatabaseCreation from django.db.backends.base.features import BaseDatabaseFeatures from django.db.backends.base.introspection import BaseDatabaseIntrospection from django.db.backends.base.operations import BaseDatabaseOperations from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.backends.base.validation import BaseDatabaseValidation class DatabaseCreation(BaseDatabaseCreation): def create_test_db(self, *args, **kwargs): """ Creates a test database, prompting the user for confirmation if the database already exists. Returns the name of the test database created. """ pass def destroy_test_db(self, *args, **kwargs): """ Destroy a test database, prompting the user for confirmation if the database already exists. Returns the name of the test database created. """ pass class DatabaseCursor(object): def __init__(self, ldap_connection): self.connection = ldap_connection def __enter__(self): return self def __exit__(self): pass def close(self): pass class DatabaseFeatures(BaseDatabaseFeatures): can_use_chunked_reads = False supports_transactions = False supports_column_check_constraints = False supports_table_check_constraints = False supports_ignore_conflicts = False uses_savepoints = False supports_partial_indexes = False def __init__(self, connection): self.connection = connection class DatabaseIntrospection(BaseDatabaseIntrospection): def get_table_list(self, cursor): return [] class DatabaseOperations(BaseDatabaseOperations): compiler_module = "ldapdb.backends.ldap.compiler" def quote_name(self, name): return name def no_limit_value(self): return -1 def sql_flush(self, style, tables, *, allow_cascade=False, reset_sequences=False): return [] if django.VERSION < (3, 1): # Backwards compatibility def sql_flush(self, style, tables, sequences, allow_cascade=False): # noqa return [] class DatabaseValidation(BaseDatabaseValidation): pass class LdapDatabase(object): # Base class for all exceptions Error = ldap.LDAPError class DatabaseError(Error): """Database-side errors.""" class OperationalError( DatabaseError, ldap.ADMINLIMIT_EXCEEDED, ldap.AUTH_METHOD_NOT_SUPPORTED, ldap.AUTH_UNKNOWN, ldap.BUSY, ldap.CONFIDENTIALITY_REQUIRED, ldap.CONNECT_ERROR, ldap.INAPPROPRIATE_AUTH, ldap.INVALID_CREDENTIALS, ldap.OPERATIONS_ERROR, ldap.RESULTS_TOO_LARGE, ldap.SASL_BIND_IN_PROGRESS, ldap.SERVER_DOWN, ldap.SIZELIMIT_EXCEEDED, ldap.STRONG_AUTH_NOT_SUPPORTED, ldap.STRONG_AUTH_REQUIRED, ldap.TIMELIMIT_EXCEEDED, ldap.TIMEOUT, ldap.UNAVAILABLE, ldap.UNAVAILABLE_CRITICAL_EXTENSION, ldap.UNWILLING_TO_PERFORM, ): """Exceptions related to the database operations, out of the programmer control.""" class IntegrityError( DatabaseError, ldap.AFFECTS_MULTIPLE_DSAS, ldap.ALREADY_EXISTS, ldap.CONSTRAINT_VIOLATION, ldap.TYPE_OR_VALUE_EXISTS, ): """Exceptions related to database Integrity.""" class DataError( DatabaseError, ldap.INVALID_DN_SYNTAX, ldap.INVALID_SYNTAX, ldap.NOT_ALLOWED_ON_NONLEAF, ldap.NOT_ALLOWED_ON_RDN, ldap.OBJECT_CLASS_VIOLATION, ldap.UNDEFINED_TYPE, ): """Exceptions related to invalid data""" class InterfaceError( ldap.CLIENT_LOOP, ldap.DECODING_ERROR, ldap.ENCODING_ERROR, ldap.LOCAL_ERROR, ldap.LOOP_DETECT, ldap.NO_MEMORY, ldap.PROTOCOL_ERROR, ldap.REFERRAL_LIMIT_EXCEEDED, ldap.USER_CANCELLED, Error, ): """Exceptions related to the python-ldap interface.""" class InternalError( DatabaseError, ldap.ALIAS_DEREF_PROBLEM, ldap.ALIAS_PROBLEM, ): """Exceptions encountered within the database.""" class ProgrammingError( DatabaseError, ldap.CONTROL_NOT_FOUND, ldap.FILTER_ERROR, ldap.INAPPROPRIATE_MATCHING, ldap.NAMING_VIOLATION, ldap.NO_SUCH_ATTRIBUTE, ldap.NO_SUCH_OBJECT, ldap.PARAM_ERROR, ): """Invalid data send by the programmer.""" class NotSupportedError( DatabaseError, ldap.NOT_SUPPORTED, ): """Exception for unsupported actions.""" class LdapSchemaEditor(BaseDatabaseSchemaEditor): def create_model(self, cursor): pass class LdapClient(BaseDatabaseClient): executable_name = 'ldapsearch' class DatabaseWrapper(BaseDatabaseWrapper): vendor = 'ldap' Database = LdapDatabase SchemaEditorClass = LdapSchemaEditor # Hook for sibling classes client_class = LdapClient creation_class = DatabaseCreation features_class = DatabaseFeatures introspection_class = DatabaseIntrospection ops_class = DatabaseOperations validation_class = DatabaseValidation # NOTE: These are copied from the mysql DatabaseWrapper operators = { 'exact': '= %s', 'iexact': 'LIKE %s', 'contains': 'LIKE BINARY %s', 'icontains': 'LIKE %s', 'regex': 'REGEXP BINARY %s', 'iregex': 'REGEXP %s', 'gt': '> %s', 'gte': '>= %s', 'lt': '< %s', 'lte': '<= %s', 'startswith': 'LIKE BINARY %s', 'endswith': 'LIKE BINARY %s', 'istartswith': 'LIKE %s', 'iendswith': 'LIKE %s', } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Charset used for LDAP text *values* self.charset = "utf-8" self.settings_dict['SUPPORTS_TRANSACTIONS'] = True self.autocommit = True # Default page size of 1000 items, ActiveDirectory's default # See https://support.microsoft.com/en-us/help/315071/how-to-view-and-set-ldap-policy-in-active-directory-by-using-ntdsutil.exe # noqa self.page_size = 1000 def close(self): if hasattr(self, 'validate_thread_sharing'): # django >= 1.4 self.validate_thread_sharing() if self.connection is not None: if hasattr(self.connection, '_l'): self.connection.unbind_s() self.connection = None def get_connection_params(self): """Compute appropriate parameters for establishing a new connection. Computed at system startup. """ return { 'uri': self.settings_dict['NAME'], 'tls': self.settings_dict.get('TLS', False), 'bind_dn': self.settings_dict['USER'], 'bind_pw': self.settings_dict['PASSWORD'], 'retry_max': self.settings_dict.get('RETRY_MAX', 1), 'retry_delay': self.settings_dict.get('RETRY_DELAY', 60.0), 'options': { k if isinstance(k, int) else k.lower(): v for k, v in self.settings_dict.get('CONNECTION_OPTIONS', {}).items() }, } def ensure_connection(self): super().ensure_connection() # Do a test bind, which will revive the connection if interrupted, or reconnect conn_params = self.get_connection_params() try: self.connection.simple_bind_s( conn_params['bind_dn'], conn_params['bind_pw'], ) except ldap.SERVER_DOWN: self.connect() def get_new_connection(self, conn_params): """Build a connection from its parameters.""" connection = ldap.ldapobject.ReconnectLDAPObject( uri=conn_params['uri'], retry_max=conn_params['retry_max'], retry_delay=conn_params['retry_delay'], bytes_mode=False) options = conn_params['options'] for opt, value in options.items(): if opt == 'query_timeout': connection.timeout = int(value) elif opt == 'page_size': self.page_size = int(value) else: connection.set_option(opt, value) if conn_params['tls']: connection.start_tls_s() connection.simple_bind_s( conn_params['bind_dn'], conn_params['bind_pw'], ) return connection def init_connection_state(self): """Initialize python-side connection state.""" pass def _commit(self): pass def create_cursor(self, name=None): return DatabaseCursor(self.connection) def _rollback(self): pass def _set_autocommit(self, autocommit): pass def add_s(self, dn, modlist): with self.cursor() as cursor: return cursor.connection.add_s(dn, modlist) def delete_s(self, dn): cursor = self._cursor() with self.cursor() as cursor: return cursor.connection.delete_s(dn) def modify_s(self, dn, modlist): with self.cursor() as cursor: return cursor.connection.modify_s(dn, modlist) def rename_s(self, dn, newrdn): cursor = self._cursor() with self.cursor() as cursor: return cursor.connection.rename_s(dn, newrdn) def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None): with self.cursor() as cursor: query_timeout = cursor.connection.timeout # Request pagination; don't fail if the server doesn't support it. ldap_control = ldap.controls.SimplePagedResultsControl( criticality=False, size=self.page_size, cookie='', ) # Fetch results page = 0 while True: msgid = cursor.connection.search_ext( base=base, scope=scope, filterstr=filterstr, attrlist=attrlist, serverctrls=[ldap_control], timeout=query_timeout, ) _res_type, results, _res_msgid, server_controls = cursor.connection.result3( msgid, timeout=query_timeout, ) page_controls = [ctrl for ctrl in server_controls if ctrl.controlType == ldap.CONTROL_PAGEDRESULTS] for dn, attrs in results: # skip referrals if dn is not None: yield dn, attrs page_control = page_controls[0] page += 1 if page_control.cookie: ldap_control.cookie = page_control.cookie else: # End of pages break django-ldapdb-1.5.1/ldapdb/backends/ldap/compiler.py0000644000175000017500000002462413741076301023152 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project import collections import re import ldap from django.db.models import aggregates from django.db.models.sql import compiler from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE from django.db.models.sql.where import AND, OR, WhereNode from ldapdb import escape_ldap_filter from ldapdb.models.fields import ListField _ORDER_BY_LIMIT_OFFSET_RE = re.compile( r'(?:\bORDER BY\b\s+(.+?))?\s*(?:\bLIMIT\b\s+(-?\d+))?\s*(?:\bOFFSET\b\s+(\d+))?$') class LdapDBError(Exception): """Base class for LDAPDB errors.""" LdapLookup = collections.namedtuple('LdapLookup', ['base', 'scope', 'filterstr']) def query_as_ldap(query, compiler, connection): """Convert a django.db.models.sql.query.Query to a LdapLookup.""" if query.is_empty(): return if query.model._meta.model_name == 'migration' and not hasattr(query.model, 'object_classes'): # FIXME(rbarrois): Support migrations return # FIXME(rbarrois): this could be an extra Where clause filterstr = ''.join(['(objectClass=%s)' % cls for cls in query.model.object_classes]) # FIXME(rbarrois): Remove this code as part of #101 if (len(query.where.children) == 1 and not isinstance(query.where.children[0], WhereNode) and query.where.children[0].lhs.target.column == 'dn'): lookup = query.where.children[0] if lookup.lookup_name != 'exact': raise LdapDBError("Unsupported dn lookup: %s" % lookup.lookup_name) return LdapLookup( base=lookup.rhs, scope=ldap.SCOPE_BASE, filterstr='(&%s)' % filterstr, ) sql, params = compiler.compile(query.where) if sql: filterstr += '(%s)' % (sql % tuple(escape_ldap_filter(param) for param in params)) return LdapLookup( base=query.model.base_dn, scope=query.model.search_scope, filterstr='(&%s)' % filterstr, ) def where_node_as_ldap(where, compiler, connection): """Parse a django.db.models.sql.where.WhereNode. Returns: (clause, [params]): the filter clause, with a list of unescaped parameters. """ bits, params = [], [] for item in where.children: if isinstance(item, WhereNode): clause, clause_params = compiler.compile(item) else: clause, clause_params = item.as_sql(compiler, connection) bits.append(clause) params.extend(clause_params) if not bits: return '', [] # FIXME(rbarrois): shouldn't we flatten recursive AND / OR? if len(bits) == 1: clause = bits[0] elif where.connector == AND: clause = '&' + ''.join('(%s)' % bit for bit in bits) elif where.connector == OR: clause = '|' + ''.join('(%s)' % bit for bit in bits) else: raise LdapDBError("Unhandled WHERE connector: %s" % where.connector) if where.negated: clause = ('!(%s)' % clause) return clause, params class SQLCompiler(compiler.SQLCompiler): """LDAP-based SQL compiler.""" def compile(self, node, *args, **kwargs): """Parse a WhereNode to a LDAP filter string.""" if isinstance(node, WhereNode): return where_node_as_ldap(node, self, self.connection) return super().compile(node, *args, **kwargs) def execute_sql(self, result_type=compiler.SINGLE, chunked_fetch=False, chunk_size=GET_ITERATOR_CHUNK_SIZE): if result_type != compiler.SINGLE: raise Exception("LDAP does not support MULTI queries") # Setup self.select, self.klass_info, self.annotation_col_map # All expected from ModelIterable.__iter__ self.pre_sql_setup() lookup = query_as_ldap(self.query, compiler=self, connection=self.connection) if lookup is None: return try: vals = self.connection.search_s( base=lookup.base, scope=lookup.scope, filterstr=lookup.filterstr, attrlist=['dn'], ) # Flatten iterator vals = list(vals) except ldap.NO_SUCH_OBJECT: vals = [] if not vals: return None output = [] self.setup_query() for e in self.select: if isinstance(e[0], aggregates.Count): # Check if the SQL query has a limit value and append # that value, else append the length of the return values # from LDAP. sql = self.as_sql()[0] if hasattr(self.query, 'subquery') and self.query.subquery: sql = self.query.subquery m = _ORDER_BY_LIMIT_OFFSET_RE.search(sql) limit = m.group(2) offset = m.group(3) if limit and int(limit) >= 0: output.append(int(limit)) elif offset: output.append(len(vals) - int(offset)) else: output.append(len(vals)) else: output.append(e[0]) return output def results_iter(self, results=None, tuple_expected=False, chunked_fetch=False, chunk_size=GET_ITERATOR_CHUNK_SIZE): lookup = query_as_ldap(self.query, compiler=self, connection=self.connection) if lookup is None: return if len(self.query.select): fields = [x.field for x in self.query.select] else: fields = self.query.model._meta.fields attrlist = [x.db_column for x in fields if x.db_column] try: vals = self.connection.search_s( base=lookup.base, scope=lookup.scope, filterstr=lookup.filterstr, attrlist=attrlist, ) except ldap.NO_SUCH_OBJECT: return # perform sorting if self.query.extra_order_by: ordering = self.query.extra_order_by elif not self.query.default_ordering: ordering = self.query.order_by else: ordering = self.query.order_by or self.query.model._meta.ordering for fieldname in reversed(ordering): if fieldname.startswith('-'): sort_field = fieldname[1:] reverse = True else: sort_field = fieldname reverse = False if sort_field == 'pk': sort_field = self.query.model._meta.pk.name field = self.query.model._meta.get_field(sort_field) if sort_field == 'dn': vals = sorted(vals, key=lambda pair: pair[0], reverse=reverse) else: def get_key(obj): attr = field.from_ldap( obj[1].get(field.db_column, []), connection=self.connection, ) if hasattr(attr, 'lower'): attr = attr.lower() return attr vals = sorted(vals, key=get_key, reverse=reverse) # process results pos = 0 results = [] for dn, attrs in vals: # FIXME : This is not optimal, we retrieve more results than we # need but there is probably no other options as we can't perform # ordering server side. if (self.query.low_mark and pos < self.query.low_mark) or \ (self.query.high_mark is not None and pos >= self.query.high_mark): pos += 1 continue row = [] self.setup_query() for e in self.select: if isinstance(e[0], aggregates.Count): value = 0 input_field = e[0].get_source_expressions()[0].field if input_field.attname == 'dn': value = 1 elif hasattr(input_field, 'from_ldap'): result = input_field.from_ldap( attrs.get(input_field.db_column, []), connection=self.connection) if result: value = 1 if isinstance(input_field, ListField): value = len(result) row.append(value) else: if e[0].field.attname == 'dn': row.append(dn) elif hasattr(e[0].field, 'from_ldap'): row.append(e[0].field.from_ldap( attrs.get(e[0].field.db_column, []), connection=self.connection)) else: row.append(None) if self.query.distinct: if row in results: continue else: results.append(row) yield row pos += 1 def has_results(self): import inspect iterator = self.results_iter() if inspect.isgenerator(iterator): try: next(iterator) return True except StopIteration: return False else: return False class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler): pass class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler): def execute_sql(self, result_type=compiler.MULTI): lookup = query_as_ldap(self.query, compiler=self, connection=self.connection) if not lookup: return try: vals = self.connection.search_s( base=lookup.base, scope=lookup.scope, filterstr=lookup.filterstr, attrlist=['dn'], ) except ldap.NO_SUCH_OBJECT: return # FIXME : there is probably a more efficient way to do this for dn, attrs in vals: self.connection.delete_s(dn) class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler): pass class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler): def execute_sql(self, result_type=compiler.SINGLE): # Return only number values through the aggregate compiler output = super().execute_sql(result_type) return filter(lambda a: isinstance(a, int), output) django-ldapdb-1.5.1/ldapdb/models/0000755000175000017500000000000013741076301017547 5ustar xelnorxelnet00000000000000django-ldapdb-1.5.1/ldapdb/models/__init__.py0000644000175000017500000000026113741076301021657 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project from ldapdb.models.base import Model # noqa django-ldapdb-1.5.1/ldapdb/models/base.py0000644000175000017500000001257113741076301021041 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project import logging import django.db.models import ldap from django.db import connections, router from django.db.models import signals from . import fields as ldapdb_fields logger = logging.getLogger('ldapdb') class Model(django.db.models.base.Model): """ Base class for all LDAP models. """ dn = ldapdb_fields.CharField(max_length=200, primary_key=True) # meta-data base_dn = None search_scope = ldap.SCOPE_SUBTREE object_classes = ['top'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._saved_dn = self.dn if self.dn: self.base_dn = self.dn.split(',', 1)[1] def build_rdn(self): """ Build the Relative Distinguished Name for this entry. """ bits = [] for field in self._meta.fields: if field.db_column and field.primary_key: bits.append("%s=%s" % (field.db_column, getattr(self, field.name))) if not len(bits): raise Exception("Could not build Distinguished Name") return '+'.join(bits) def build_dn(self): """ Build the Distinguished Name for this entry. """ return "%s,%s" % (self.build_rdn(), self.base_dn) def delete(self, using=None): """ Delete this entry. """ using = using or router.db_for_write(self.__class__, instance=self) connection = connections[using] logger.debug("Deleting LDAP entry %s" % self.dn) connection.delete_s(self.dn) signals.post_delete.send(sender=self.__class__, instance=self) def _save_table(self, raw=False, cls=None, force_insert=None, force_update=None, using=None, update_fields=None): """ Saves the current instance. """ # Connection aliasing connection = connections[using] create = bool(force_insert or not self.dn) # Prepare fields if update_fields: target_fields = [ self._meta.get_field(name) for name in update_fields ] else: target_fields = [ field for field in cls._meta.get_fields(include_hidden=True) if field.concrete and not field.primary_key ] def get_field_value(field, instance): python_value = getattr(instance, field.attname) return field.get_db_prep_save(python_value, connection=connection) if create: old = None else: old = cls.objects.using(using).get(dn=self._saved_dn) changes = { field.db_column: ( None if old is None else get_field_value(field, old), get_field_value(field, self), ) for field in target_fields } # Actual saving old_dn = self.dn new_dn = self.build_dn() updated = False # Insertion if create: # FIXME(rbarrois): This should be handled through a hidden field. hidden_values = [ ('objectClass', [obj_class.encode('utf-8') for obj_class in self.object_classes]) ] new_values = hidden_values + [ (colname, change[1]) for colname, change in sorted(changes.items()) if change[1] != [] ] new_dn = self.build_dn() logger.debug("Creating new LDAP entry %s", new_dn) connection.add_s(new_dn, new_values) # Update else: modlist = [] for colname, change in sorted(changes.items()): old_value, new_value = change if old_value == new_value: continue modlist.append(( ldap.MOD_DELETE if new_value == [] else ldap.MOD_REPLACE, colname, new_value, )) if new_dn != old_dn: logger.debug("renaming ldap entry %s to %s", old_dn, new_dn) connection.rename_s(old_dn, self.build_rdn()) if modlist: logger.debug("Modifying existing LDAP entry %s", new_dn) connection.modify_s(new_dn, modlist) updated = True self.dn = new_dn # Finishing self._saved_dn = self.dn return updated @classmethod def scoped(base_class, base_dn): """ Returns a copy of the current class with a different base_dn. """ class Meta: proxy = True verbose_name = base_class._meta.verbose_name verbose_name_plural = base_class._meta.verbose_name_plural import re suffix = re.sub('[=,]', '_', base_dn) name = "%s_%s" % (base_class.__name__, str(suffix)) new_class = type(str(name), (base_class,), { 'base_dn': base_dn, '__module__': base_class.__module__, 'Meta': Meta}) return new_class @classmethod def _check_single_primary_key(cls): """ Always return an empty list to circumvent the models.E026 system check. ldapdb allows multiple primary keys. """ return [] class Meta: abstract = True django-ldapdb-1.5.1/ldapdb/models/fields.py0000644000175000017500000002574413741076301021403 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project import datetime import re from django.db.models import fields, lookups from django.utils import timezone class LdapLookup(lookups.Lookup): rhs_is_iterable = False LDAP_PLACEHOLDER = '%s' def _as_ldap(self, lhs): raise NotImplementedError() def process_lhs(self, compiler, connection): return (self.lhs.target.column, []) def as_sql(self, compiler, connection): # lhs: field name; lhs_params: [] lhs, lhs_params = self.process_lhs(compiler, connection) rhs_params = self.rhs if self.rhs_is_iterable else [self.rhs] params = lhs_params + rhs_params if self.rhs_is_iterable: # Convert (x__in=[a, b, c]) to |(x=a)(x=b)(x=c) return '|' + ''.join(['({})'.format(self._as_ldap(lhs))] * len(rhs_params)), params else: return self._as_ldap(lhs), params def get_prep_lookup(self): """ Convert the Python value(s) used in the lookup to LDAP values. """ field = self.lhs.output_field if self.rhs_is_iterable and not field.multi_valued_field: # self.rhs is an iterable, and the field expects single-valued options. return [field.get_prep_value(v) for v in self.rhs] else: # self.rhs is 'as multi-valued' as the field. return field.get_prep_value(self.rhs) class ContainsLookup(LdapLookup): lookup_name = 'contains' def _as_ldap(self, lhs): return '%s=*%s*' % (lhs, self.LDAP_PLACEHOLDER) class IContainsLookup(ContainsLookup): lookup_name = 'icontains' class StartsWithLookup(LdapLookup): lookup_name = 'startswith' def _as_ldap(self, lhs): return '%s=%s*' % (lhs, self.LDAP_PLACEHOLDER) class EndsWithLookup(LdapLookup): lookup_name = 'endswith' def _as_ldap(self, lhs): return '%s=*%s' % (lhs, self.LDAP_PLACEHOLDER) class ExactLookup(LdapLookup): lookup_name = 'exact' def _as_ldap(self, lhs): return '%s=%s' % (lhs, self.LDAP_PLACEHOLDER) class GteLookup(LdapLookup): lookup_name = 'gte' def _as_ldap(self, lhs): return '%s>=%s' % (lhs, self.LDAP_PLACEHOLDER) class LteLookup(LdapLookup): lookup_name = 'lte' def _as_ldap(self, lhs): return '%s<=%s' % (lhs, self.LDAP_PLACEHOLDER) class InLookup(LdapLookup): lookup_name = 'in' rhs_is_iterable = True def _as_ldap(self, lhs): return '%s=%s' % (lhs, self.LDAP_PLACEHOLDER) class ListContainsLookup(ExactLookup): lookup_name = 'contains' class LdapFieldMixin(object): multi_valued_field = False binary_field = False def get_db_prep_value(self, value, connection, prepared=False): """Prepare a value for DB interaction. Returns: - list(bytes) if not prepared - list(str) if prepared """ if prepared: return value if value is None: return [] values = value if self.multi_valued_field else [value] prepared_values = [self.get_prep_value(v) for v in values] # Remove duplicates. # https://tools.ietf.org/html/rfc4511#section-4.1.7 : # "The set of attribute values is unordered." # We keep those values sorted in natural order to avoid useless # updates to the LDAP server. return list(sorted(set(v for v in prepared_values if v))) def get_db_prep_save(self, value, connection): values = self.get_db_prep_value(value, connection, prepared=False) if self.binary_field: # Already raw values; don't encode it twice. return values else: return [v.encode(connection.charset) for v in values] class CharField(LdapFieldMixin, fields.CharField): def __init__(self, *args, **kwargs): defaults = {'max_length': 200} defaults.update(kwargs) super().__init__(*args, **defaults) def from_ldap(self, value, connection): if len(value) == 0: return '' else: return value[0].decode(connection.charset) CharField.register_lookup(ContainsLookup) CharField.register_lookup(IContainsLookup) CharField.register_lookup(StartsWithLookup) CharField.register_lookup(EndsWithLookup) CharField.register_lookup(InLookup) CharField.register_lookup(ExactLookup) class ImageField(LdapFieldMixin, fields.Field): binary_field = True def from_ldap(self, value, connection): if len(value) == 0: return '' else: return value[0] ImageField.register_lookup(ExactLookup) class IntegerField(LdapFieldMixin, fields.IntegerField): def from_ldap(self, value, connection): if len(value) == 0: return None if self.null else 0 else: return int(value[0]) def get_prep_value(self, value): value = super().get_prep_value(value) return str(value) IntegerField.register_lookup(ExactLookup) IntegerField.register_lookup(GteLookup) IntegerField.register_lookup(LteLookup) IntegerField.register_lookup(InLookup) class FloatField(LdapFieldMixin, fields.FloatField): def from_ldap(self, value, connection): if len(value) == 0: return None if self.null else 0.0 else: return float(value[0]) def get_prep_value(self, value): value = super().get_prep_value(value) return str(value) FloatField.register_lookup(ExactLookup) FloatField.register_lookup(GteLookup) FloatField.register_lookup(LteLookup) FloatField.register_lookup(InLookup) class BooleanField(LdapFieldMixin, fields.BooleanField): def from_ldap(self, value, connection): if len(value) == 0: return None if self.null else False else: return value[0].upper() == b'TRUE' def get_prep_value(self, value): value = super().get_prep_value(value) return 'TRUE' if value else 'FALSE' BooleanField.register_lookup(ExactLookup) class ListField(LdapFieldMixin, fields.Field): multi_valued_field = True def from_ldap(self, value, connection): return [x.decode(connection.charset) for x in value] def from_db_value(self, value, expression, connection, context): """Convert from the database format. This should be the inverse of self.get_prep_value() """ return self.to_python(value) def to_python(self, value): if not value: return [] return value ListField.register_lookup(ListContainsLookup) ListField.register_lookup(ExactLookup) class DateField(LdapFieldMixin, fields.DateField): """ A text field containing date, in specified format. The format can be specified as 'format' argument, as strptime() format string. It defaults to ISO8601 (%Y-%m-%d). Note: 'lte' and 'gte' lookups are done string-wise. Therefore, they will onlywork correctly on Y-m-d dates with constant component widths. """ def __init__(self, *args, **kwargs): if 'format' in kwargs: self._date_format = kwargs.pop('format') else: self._date_format = '%Y-%m-%d' super().__init__(*args, **kwargs) def from_ldap(self, value, connection): if len(value) == 0: return None else: return datetime.datetime.strptime(value[0].decode(connection.charset), self._date_format).date() def get_prep_value(self, value): value = super().get_prep_value(value) if not isinstance(value, datetime.date) \ and not isinstance(value, datetime.datetime): raise ValueError( 'DateField can be only set to a datetime.date instance; got {}'.format(repr(value))) return value.strftime(self._date_format) DateField.register_lookup(ExactLookup) LDAP_DATETIME_RE = re.compile( r'(?P\d{4})' r'(?P\d{2})' r'(?P\d{2})' r'(?P\d{2})' r'(?P\d{2})?' r'(?P\d{2})?' r'(?:[.,](?P\d+))?' r'(?PZ|[+-]\d{2}(?:\d{2})?)' r'$' ) LDAP_DATE_FORMAT = '%Y%m%d%H%M%S.%fZ' def datetime_from_ldap(value): """Convert a LDAP-style datetime to a Python aware object. See https://tools.ietf.org/html/rfc4517#section-3.3.13 for details. Args: value (str): the datetime to parse """ if not value: return None match = LDAP_DATETIME_RE.match(value) if not match: return None groups = match.groupdict() if groups['microsecond']: groups['microsecond'] = groups['microsecond'].ljust(6, '0')[:6] tzinfo = groups.pop('tzinfo') if tzinfo == 'Z': tzinfo = timezone.utc else: offset_mins = int(tzinfo[-2:]) if len(tzinfo) == 5 else 0 offset = 60 * int(tzinfo[1:3]) + offset_mins if tzinfo[0] == '-': offset = - offset tzinfo = timezone.get_fixed_timezone(offset) kwargs = {k: int(v) for k, v in groups.items() if v is not None} kwargs['tzinfo'] = tzinfo return datetime.datetime(**kwargs) class DateTimeField(LdapFieldMixin, fields.DateTimeField): """ A field containing a UTC timestamp, in Generalized Time syntax. That syntax is ``YYYYmmddHH[MM[SS[.ff](Z|+XX[YY]|-XX[YY])``. See: https://tools.ietf.org/html/rfc4517#section-3.3.13 """ def from_ldap(self, value, connection): if len(value) == 0: return None return datetime_from_ldap(value[0].decode(connection.charset)) def get_prep_value(self, value): value = super().get_prep_value(value) if not isinstance(value, datetime.date) \ and not isinstance(value, datetime.datetime): raise ValueError( 'DateTimeField can be only set to a datetime.datetime instance; got {}'.format(repr(value))) value = timezone.utc.normalize(value) return value.strftime(LDAP_DATE_FORMAT) DateTimeField.register_lookup(ExactLookup) DateTimeField.register_lookup(LteLookup) DateTimeField.register_lookup(GteLookup) DateTimeField.register_lookup(InLookup) EPOCH = timezone.utc.localize(datetime.datetime.utcfromtimestamp(0)) def datetime_from_timestamp(ts): return timezone.utc.localize(datetime.datetime.utcfromtimestamp(ts)) def timestamp_from_datetime(dt): return int((timezone.utc.normalize(dt) - EPOCH).total_seconds()) class TimestampField(LdapFieldMixin, fields.DateTimeField): """ A field storing a datetime as a UNIX timestamp. See for instance nis.schema > shadowAccount > shadowLastChange. """ def from_ldap(self, value, connection): if len(value) == 0: return None return datetime_from_timestamp(value[0].decode(connection.charset)) def get_prep_value(self, value): return str(timestamp_from_datetime(value)) TimestampField.register_lookup(ExactLookup) TimestampField.register_lookup(LteLookup) TimestampField.register_lookup(GteLookup) TimestampField.register_lookup(InLookup) django-ldapdb-1.5.1/ldapdb/router.py0000644000175000017500000000256413741076301020165 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project def is_ldap_model(model): # FIXME: there is probably a better check than testing 'base_dn' return hasattr(model, 'base_dn') class Router(object): """ A router to point database operations on LDAP models to the LDAP database. NOTE: if you have more than one LDAP database, you will need to write your own router. """ def __init__(self): "Find the name of the LDAP database" from django.conf import settings self.ldap_alias = None for alias, settings_dict in settings.DATABASES.items(): if settings_dict['ENGINE'] == 'ldapdb.backends.ldap': self.ldap_alias = alias break def allow_migrate(self, db, app_label, model_name=None, **hints): if 'model' in hints and is_ldap_model(hints['model']): return False return None def db_for_read(self, model, **hints): "Point all operations on LDAP models to the LDAP database" if is_ldap_model(model): return self.ldap_alias return None def db_for_write(self, model, **hints): "Point all operations on LDAP models to the LDAP database" if is_ldap_model(model): return self.ldap_alias return None django-ldapdb-1.5.1/ldapdb/tests.py0000644000175000017500000002462713741076301020013 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project import datetime from django.db import connections from django.db.models import expressions from django.db.models.sql import query as django_query from django.db.models.sql.where import AND, OR, WhereNode from django.test import TestCase from django.utils import timezone from ldapdb import escape_ldap_filter, models from ldapdb.backends.ldap import compiler as ldapdb_compiler from ldapdb.models import fields UTC = timezone.utc UTC_PLUS_ONE = timezone.get_fixed_timezone(60) UTC_MINUS_2_HALF = timezone.get_fixed_timezone(-150) class FakeModel(models.Model): class Meta: abstract = True base_dn = 'ou=test,dc=example,dc=org' object_classes = ['inetOrgPerson'] name = fields.CharField(db_column='cn') class DateTimeTests(TestCase): CONVERSIONS = { '': None, '20180102030405.067874Z': datetime.datetime(2018, 1, 2, 3, 4, 5, 67874, tzinfo=UTC), # Sub-microsecond is ignored by Python '20180102030405.067874846Z': datetime.datetime(2018, 1, 2, 3, 4, 5, 67874, tzinfo=UTC), # Sub-hour precision is optional '2018010203Z': datetime.datetime(2018, 1, 2, 3, tzinfo=UTC), # Support UTC offsets '201801020304+0100': datetime.datetime(2018, 1, 2, 3, 4, tzinfo=UTC_PLUS_ONE), # Minutes are optional for UTC offsets '201801020304+01': datetime.datetime(2018, 1, 2, 3, 4, tzinfo=UTC_PLUS_ONE), # Check negative offsets '201801020304-0230': datetime.datetime(2018, 1, 2, 3, 4, tzinfo=UTC_MINUS_2_HALF), } def test_conversions(self): for raw, expected in sorted(self.CONVERSIONS.items()): converted = fields.datetime_from_ldap(raw) self.assertEqual( expected, converted, "Mismatch for %r: expected=%r, got=%r" % (raw, expected, converted), ) class TimestampTests(TestCase): CONVERSIONS = { 0: datetime.datetime(1970, 1, 1, tzinfo=UTC), 1530139989: datetime.datetime(2018, 6, 27, 22, 53, 9, tzinfo=UTC), } def test_conversions(self): for raw, expected in sorted(self.CONVERSIONS.items()): converted = fields.datetime_from_timestamp(raw) self.assertEqual( expected, converted, "Mismatch for %r: expected=%r, got=%r" % (raw, expected, converted), ) retro_converted = fields.timestamp_from_datetime(converted) self.assertEqual( raw, retro_converted, "Mismatch for %r: expected=%r, got=%r" % (raw, raw, retro_converted), ) class WhereTestCase(TestCase): def _build_lookup(self, field_name, lookup, value, field=fields.CharField): fake_field = field() fake_field.set_attributes_from_name(field_name) lhs = expressions.Col('faketable', fake_field, fake_field) lookup = lhs.get_lookup(lookup) return lookup(lhs, value) def _where_as_ldap(self, where): query = django_query.Query(model=FakeModel) compiler = ldapdb_compiler.SQLCompiler( query=query, connection=connections['ldap'], using=None, ) pattern, params = compiler.compile(where) return '(%s)' % (pattern % tuple(escape_ldap_filter(param) for param in params)) def test_escape(self): self.assertEqual(escape_ldap_filter(u'fôöbàr'), u'fôöbàr') self.assertEqual(escape_ldap_filter('foo*bar'), 'foo\\2abar') self.assertEqual(escape_ldap_filter('foo(bar'), 'foo\\28bar') self.assertEqual(escape_ldap_filter('foo)bar'), 'foo\\29bar') self.assertEqual(escape_ldap_filter('foo\\bar'), 'foo\\5cbar') self.assertEqual(escape_ldap_filter('foo\\bar*wiz'), 'foo\\5cbar\\2awiz') def test_char_field_max_length(self): self.assertEqual(fields.CharField(max_length=42).max_length, 42) def test_char_field_exact(self): where = WhereNode() where.add(self._build_lookup('cn', 'exact', "test"), AND) self.assertEqual(self._where_as_ldap(where), "(cn=test)") where = WhereNode() where.add(self._build_lookup('cn', 'exact', "(test)"), AND) self.assertEqual(self._where_as_ldap(where), "(cn=\\28test\\29)") def test_char_field_in(self): where = WhereNode() where.add(self._build_lookup("cn", 'in', ["foo", "bar"]), AND) self.assertEqual(self._where_as_ldap(where), "(|(cn=foo)(cn=bar))") where = WhereNode() where.add(self._build_lookup("cn", 'in', ["(foo)", "(bar)"]), AND) self.assertEqual(self._where_as_ldap(where), "(|(cn=\\28foo\\29)(cn=\\28bar\\29))") def test_char_field_startswith(self): where = WhereNode() where.add(self._build_lookup("cn", 'startswith', "test"), AND) self.assertEqual(self._where_as_ldap(where), "(cn=test*)") where = WhereNode() where.add(self._build_lookup("cn", 'startswith', "te*st"), AND) self.assertEqual(self._where_as_ldap(where), "(cn=te\\2ast*)") def test_char_field_endswith(self): where = WhereNode() where.add(self._build_lookup("cn", 'endswith', "test"), AND) self.assertEqual(self._where_as_ldap(where), "(cn=*test)") where = WhereNode() where.add(self._build_lookup("cn", 'endswith', "te*st"), AND) self.assertEqual(self._where_as_ldap(where), "(cn=*te\\2ast)") def test_char_field_contains(self): where = WhereNode() where.add(self._build_lookup("cn", 'contains', "test"), AND) self.assertEqual(self._where_as_ldap(where), "(cn=*test*)") where = WhereNode() where.add(self._build_lookup("cn", 'contains', "te*st"), AND) self.assertEqual(self._where_as_ldap(where), "(cn=*te\\2ast*)") def test_integer_field(self): where = WhereNode() where.add(self._build_lookup("uid", 'exact', 1, field=fields.IntegerField), AND) self.assertEqual(self._where_as_ldap(where), "(uid=1)") where = WhereNode() where.add(self._build_lookup("uid", 'gte', 1, field=fields.IntegerField), AND) self.assertEqual(self._where_as_ldap(where), "(uid>=1)") where = WhereNode() where.add(self._build_lookup("uid", 'lte', 1, field=fields.IntegerField), AND) self.assertEqual(self._where_as_ldap(where), "(uid<=1)") where = WhereNode() where.add(self._build_lookup("uid", 'in', [1, 2], field=fields.IntegerField), AND) self.assertEqual(self._where_as_ldap(where), "(|(uid=1)(uid=2))") def test_float_field(self): where = WhereNode() where.add(self._build_lookup("uid", 'exact', 1.2, field=fields.FloatField), AND) self.assertEqual(self._where_as_ldap(where), "(uid=1.2)") where = WhereNode() where.add(self._build_lookup("uid", 'gte', 1.2, field=fields.FloatField), AND) self.assertEqual(self._where_as_ldap(where), "(uid>=1.2)") where = WhereNode() where.add(self._build_lookup("uid", 'lte', 1.2, field=fields.FloatField), AND) self.assertEqual(self._where_as_ldap(where), "(uid<=1.2)") def test_boolean_field(self): where = WhereNode() where.add(self._build_lookup("isSuperuser", 'exact', True, field=fields.BooleanField), AND) self.assertEqual(self._where_as_ldap(where), "(isSuperuser=TRUE)") where = WhereNode() where.add(self._build_lookup("isSuperuser", 'exact', False, field=fields.BooleanField), AND) self.assertEqual(self._where_as_ldap(where), "(isSuperuser=FALSE)") where = WhereNode() where.add(self._build_lookup("isSuperuser", 'exact', 1, field=fields.BooleanField), AND) self.assertEqual(self._where_as_ldap(where), "(isSuperuser=TRUE)") where = WhereNode() where.add(self._build_lookup("isSuperuser", 'exact', 0, field=fields.BooleanField), AND) self.assertEqual(self._where_as_ldap(where), "(isSuperuser=FALSE)") def test_list_field_contains(self): where = WhereNode() where.add(self._build_lookup("memberUid", 'contains', 'foouser', field=fields.ListField), AND) self.assertEqual(self._where_as_ldap(where), "(memberUid=foouser)") where = WhereNode() where.add(self._build_lookup("memberUid", 'contains', '(foouser)', field=fields.ListField), AND) self.assertEqual(self._where_as_ldap(where), "(memberUid=\\28foouser\\29)") def test_date_field(self): where = WhereNode() where.add(self._build_lookup("birthday", 'exact', '2013-09-03', field=fields.DateField), AND) self.assertEqual(self._where_as_ldap(where), "(birthday=2013-09-03)") def test_datetime_field(self): dt = datetime.datetime(2018, 6, 25, 20, 21, 22, tzinfo=UTC) where = WhereNode() where.add(self._build_lookup("modifyTimestamp", 'exact', dt, field=fields.DateTimeField,), AND) self.assertEqual(self._where_as_ldap(where), "(modifyTimestamp=20180625202122.000000Z)") where = WhereNode() where.add(self._build_lookup("modifyTimestamp", 'lte', dt, field=fields.DateTimeField,), AND) self.assertEqual(self._where_as_ldap(where), "(modifyTimestamp<=20180625202122.000000Z)") where = WhereNode() where.add(self._build_lookup("modifyTimestamp", 'gte', dt, field=fields.DateTimeField,), AND) self.assertEqual(self._where_as_ldap(where), "(modifyTimestamp>=20180625202122.000000Z)") def test_timestamp_field(self): dt = datetime.datetime(2018, 6, 25, 20, 21, 22, tzinfo=UTC) where = WhereNode() where.add(self._build_lookup("shadowLastChange", 'exact', dt, field=fields.TimestampField), AND) self.assertEqual(self._where_as_ldap(where), "(shadowLastChange=1529958082)") def test_and(self): where = WhereNode() where.add(self._build_lookup("cn", 'exact', "foo", field=fields.CharField), AND) where.add(self._build_lookup("givenName", 'exact', "bar", field=fields.CharField), AND) self.assertEqual(self._where_as_ldap(where), "(&(cn=foo)(givenName=bar))") def test_or(self): where = WhereNode() where.add(self._build_lookup("cn", 'exact', "foo", field=fields.CharField), AND) where.add(self._build_lookup("givenName", 'exact', "bar", field=fields.CharField), OR) self.assertEqual(self._where_as_ldap(where), "(|(cn=foo)(givenName=bar))") django-ldapdb-1.5.1/ldapdb/version.py0000644000175000017500000000061513741076301020325 0ustar xelnorxelnet00000000000000# -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project import sys if sys.version_info < (3, 8): import pkg_resources __version__ = pkg_resources.get_distribution('django-ldapdb').version else: import importlib.metadata __version__ = importlib.metadata.version('django-ldapdb') VERSION = __version__ django-ldapdb-1.5.1/manage_dev.py0000755000175000017500000000075213741076301017505 0ustar xelnorxelnet00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project import os import sys import django from django.core.management import execute_from_command_line os.environ.setdefault("DJANGO_SETTINGS_MODULE", "examples.settings") def run_tests(): execute_from_command_line([os.path.abspath(__file__), 'test']) sys.exit(0) if __name__ == "__main__": execute_from_command_line(sys.argv) django-ldapdb-1.5.1/setup.cfg0000644000175000017500000000303213741076301016655 0ustar xelnorxelnet00000000000000[metadata] name = django-ldapdb version = 1.5.1 description = A LDAP database backend for Django long_description = file: README.rst keywords = django, ldap, database, ldapdb url = https://github.com/django-ldapdb/django-ldapdb author = Jeremy Lainé author_email = jeremy.laine@m4x.org maintainer = Raphaël Barrois maintainer_email = raphael.barrois+django-ldapdb@polytechnique.org license = BSD classifiers = Development Status :: 5 - Production/Stable Environment :: Web Environment Framework :: Django :: 2.2 Framework :: Django :: 3.0 Framework :: Django :: 3.1 Intended Audience :: Developers Intended Audience :: System Administrators License :: OSI Approved :: BSD License Programming Language :: Python Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Topic :: Internet :: WWW/HTTP Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP Topic :: Software Development :: Libraries :: Python Modules [options] zip_safe = false packages = find: include_package_data = true python_requires = >=3.6 install_requires = Django>=2.2 python-ldap>=3.0 setup_requires = setuptools [options.packages.find] include = ldapdb [options.extras_require] dev = check-manifest flake8 isort>=5.0.0 tox factory_boy volatildap>=1.1.0 wheel zest.releaser[recommended] [bdist_wheel] universal = true [zest.releaser] version-levels = 3 [distutils] index-servers = pypi [flake8] max-line-length = 120 ignore = W503 [egg_info] tag_build = tag_date = 0 django-ldapdb-1.5.1/setup.py0000755000175000017500000000030013741076301016544 0ustar xelnorxelnet00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # This software is distributed under the two-clause BSD license. # Copyright (c) The django-ldapdb project from setuptools import setup setup() django-ldapdb-1.5.1/tox.ini0000644000175000017500000000045613741076301016356 0ustar xelnorxelnet00000000000000[tox] envlist = py{36,37,38}-django{22,30,31}, lint [testenv] extras = dev deps = django22: Django>=2.2,<2.3 django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 whitelist_externals = make commands = make test [testenv:lint] extras = dev whitelist_externals = make commands = make lint