././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1591141796.9014704 django-auth-ldap-2.2.0/0000775000175000017500000000000000000000000014233 5ustar00jonjon00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591141540.0 django-auth-ldap-2.2.0/CHANGES0000664000175000017500000001650500000000000015235 0ustar00jonjon000000000000002.2.0 - 2020-06-02 ------------------ - Added support for the escape argument in ``LDAPSearchUnion.execute()``. 2.1.1 - 2020-03-26 - Removed drepecated ``providing_args`` from ``Signal`` instances. 2.1.0 - 2019-12-03 ------------------ - Reject authentication requests without a username. - Added support for Django 3.0 and Python 3.8. - Removed support for Django end of life Django 2.1. 2.0.0 - 2019-06-05 ------------------ - Removed support for Python 2 and 3.4. - Removed support for end of life Django 2.0. - Added support for Django 2.2. - Add testing and support for Python 3.7 with Django 1.11 and 2.1. - When :setting:`AUTH_LDAP_SERVER_URI` is set to a callable, it is now passed a positional ``request`` argument. Support for no arguments will continue for backwards compatibility but will be removed in a future version. - Added new :setting:`AUTH_LDAP_NO_NEW_USERS` to prevent the creation of new users during authentication. Any users not already in the Django user database will not be able to login. 1.6.1 - 2018-06-02 ------------------ - Renamed ``requirements.txt`` to ``dev-requirements.txt`` to fix Read the Docs build. 1.6.0 - 2018-06-02 ------------------ - Updated ``LDAPBackend.authenticate()`` signature to match Django's documentation. - Fixed group membership queries with DNs containing non-ascii characters on Python 2.7. - The setting :setting:`AUTH_LDAP_CACHE_TIMEOUT` now replaces deprecated `AUTH_LDAP_CACHE_GROUPS` and `AUTH_LDAP_GROUP_CACHE_TIMEOUT`. In addition to caching groups, it also controls caching of distinguished names (which were previously cached by default). A compatibility shim is provided so the deprecated settings will continue to work. 1.5.0 - 2018-04-18 ------------------ - django-auth-ldap is now hosted at https://github.com/django-auth-ldap/django-auth-ldap. - Removed NISGroupType class. It searched by attribute nisNetgroupTriple, which has no defined EQAULITY rule. - The python-ldap library is now initialized with ``bytes_mode=False``, requiring all LDAP values to be handled as Unicode text (``str`` in Python 3 and ``unicode`` in Python 2), not bytes. For additional information, see the python-ldap documentation on :ref:`bytes mode `. - Removed deprecated function ``LDAPBackend.get_or_create_user()``. Use :meth:`~django_auth_ldap.backend.LDAPBackend.get_or_build_user` instead. 1.4.0 - 2018-03-22 ------------------ - Honor the attrlist argument to :setting:`AUTH_LDAP_GROUP_SEARCH` - **Backwards incompatible**: Removed support for Django < 1.11. - Support for Python 2.7 and 3.4+ now handled by the same dependency, `python-ldap >= 3.0 `_. 1.3.0 - 2017-11-20 ------------------ - **Backwards incompatible**: Removed support for obsolete versions of Django (<=1.7, plus 1.9). - Delay saving new users as long as possible. This will allow :setting:`AUTH_LDAP_USER_ATTR_MAP` to populate required fields before creating a new Django user. ``LDAPBackend.get_or_create_user()`` is now :meth:`~django_auth_ldap.backend.LDAPBackend.get_or_build_user` to avoid confusion. The old name may still be overridden for now. - Support querying by a field other than the username field with :setting:`AUTH_LDAP_USER_QUERY_FIELD`. - New method :meth:`~django_auth_ldap.backend.LDAPBackend.authenticate_ldap_user` to provide pre- and post-authentication hooks. - Add support for Django 2.0. 1.2.16 - 2017-09-30 ------------------- - Better cache key sanitizing. - Improved handling of LDAPError. A case existed where the error would not get caught while loading group permissions. 1.2.15 - 2017-08-17 ------------------- - Improved documentation for finding the official repository and contributing. 1.2.14 - 2017-07-24 ------------------- - Under search/bind mode, the user's DN will now be cached for performance. 1.2.13 - 2017-06-19 ------------------- - Support selective group mirroring with :setting:`AUTH_LDAP_MIRROR_GROUPS` and :setting:`AUTH_LDAP_MIRROR_GROUPS_EXCEPT`. - Work around Django 1.11 bug with multiple authentication backends. 1.2.12 - 2017-05-20 ------------------- - Support for complex group queries via :class:`~django_auth_ldap.config.LDAPGroupQuery`. 1.2.11 - 2017-04-22 ------------------- - Some more descriptive object representations. - Improved tox.ini organization. 1.2.9 - 2017-02-14 ------------------ - Ignore python-ldap documentation and accept ``ldap.RES_SEARCH_ENTRY`` from :meth:`ldap.LDAPObject.result`. 1.2.8 - 2016-04-18 ------------------ - Add :setting:`AUTH_LDAP_USER_ATTRLIST` to override the set of attributes requested from the LDAP server. 1.2.7 - 2015-09-29 ------------------ - Support Python 3 with `pyldap `_. 1.2.6 - 2015-03-29 ------------------ - Performance improvements to group mirroring (from `Denver Janke `_). - Add :data:`django_auth_ldap.backend.ldap_error` signal for custom handling of :exc:`~ldap.LDAPError` exceptions. - Add :data:`django_auth_ldap.backend.LDAPBackend.default_settings` for per-subclass default settings. 1.2.5 - 2015-01-30 ------------------ - Fix interaction between :setting:`AUTH_LDAP_AUTHORIZE_ALL_USERS` and :setting:`AUTH_LDAP_USER_SEARCH`. 1.2.4 - 2014-12-28 ------------------ - Add support for nisNetgroup groups (thanks to Christopher Bartz). 1.2.3 - 2014-11-18 ------------------ - Improved escaping for filter strings. - Accept (and ignore) arbitrary keyword arguments to ``LDAPBackend.authenticate``. 1.2.2 - 2014-09-22 ------------------ - Include test harness in source distribution. Some package maintainers find this helpful. 1.2.1 - 2014-08-24 ------------------ - More verbose log messages for authentication failures. 1.2.0 - 2014-04-10 ------------------ - django-auth-ldap now provides experimental Python 3 support. Python 2.5 was dropped. To sum up, django-auth-ldap works with Python 2.6, 2.7, 3.3 and 3.4. Since python-ldap isn't making progress toward Python 3, if you're using Python 3, you need to install a fork: .. code-block:: bash $ pip install git+https://github.com/rbarrois/python-ldap.git@py3 Thanks to `Aymeric Augustin `_ for making this happen. 1.1.8 - 2014-02-01 ------------------ * Update :class:`~django_auth_ldap.config.LDAPSearchUnion` to work for group searches in addition to user searches. * Tox no longer supports Python 2.5, so our tests now run on 2.6 and 2.7 only. 1.1.7 - 2013-11-19 ------------------ * Bug fix: :setting:`AUTH_LDAP_GLOBAL_OPTIONS` could be ignored in some cases (such as :func:`~django_auth_ldap.backend.LDAPBackend.populate_user`). 1.1.5 - 2013-10-25 ------------------ * Support POSIX group permissions with no gidNumber attribute. * Support multiple group DNs for \*_FLAGS_BY_GROUP. 1.1.4 - 2013-03-09 ------------------ * Add support for Django 1.5's custom user models. 1.1.3 - 2013-01-05 ------------------ * Reject empty passwords by default. Unless :setting:`AUTH_LDAP_PERMIT_EMPTY_PASSWORD` is set to True, LDAPBackend.authenticate() will immediately return None if the password is empty. This is technically backwards-incompatible, but it's a more secure default for those LDAP servers that are configured such that binds without passwords always succeed. * Add support for pickling LDAP-authenticated users. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/LICENSE0000664000175000017500000000242100000000000015237 0ustar00jonjon00000000000000Copyright (c) 2009, Peter Sagerson All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - 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 HOLDER 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/MANIFEST.in0000664000175000017500000000016700000000000015775 0ustar00jonjon00000000000000include README.md LICENSE CHANGES include tox.ini recursive-include docs * recursive-include tests * prune docs/build ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1591141796.9014704 django-auth-ldap-2.2.0/PKG-INFO0000664000175000017500000002043200000000000015331 0ustar00jonjon00000000000000Metadata-Version: 1.2 Name: django-auth-ldap Version: 2.2.0 Summary: Django LDAP authentication backend. Home-page: https://github.com/django-auth-ldap/django-auth-ldap Author: Peter Sagerson Author-email: psagers@ignorare.net License: BSD Project-URL: Documentation, https://django-auth-ldap.readthedocs.io/ Project-URL: Source, https://github.com/django-auth-ldap/django-auth-ldap Project-URL: Tracker, https://github.com/django-auth-ldap/django-auth-ldap/issues Description: ================================ Django Authentication Using LDAP ================================ .. image:: https://readthedocs.org/projects/django-auth-ldap/badge/?version=latest :target: https://django-auth-ldap.readthedocs.io/en/latest/ .. image:: https://img.shields.io/pypi/v/django-auth-ldap.svg :target: https://pypi.org/project/django-auth-ldap/ .. image:: https://img.shields.io/travis/django-auth-ldap/django-auth-ldap/master.svg?label=travis-ci :target: http://travis-ci.org/django-auth-ldap/django-auth-ldap .. image:: https://img.shields.io/pypi/l/django-auth-ldap.svg :target: https://raw.githubusercontent.com/django-auth-ldap/django-auth-ldap/master/LICENSE This is a Django authentication backend that authenticates against an LDAP service. Configuration can be as simple as a single distinguished name template, but there are many rich configuration options for working with users, groups, and permissions. * Documentation: https://django-auth-ldap.readthedocs.io/ * PyPI: https://pypi.org/project/django-auth-ldap/ * Repository: https://github.com/django-auth-ldap/django-auth-ldap * Tests: http://travis-ci.org/django-auth-ldap/django-auth-ldap * License: BSD 2-Clause This version is supported on Python 3.5+; and Django 1.11+. It requires `python-ldap`_ >= 3.1. .. _`python-ldap`: https://pypi.org/project/python-ldap/ Installation ============ Install the package with pip: .. code-block:: sh $ pip install django-auth-ldap It requires `python-ldap`_ >= 3.1. You'll need the `OpenLDAP`_ libraries and headers available on your system. To use the auth backend in a Django project, add ``'django_auth_ldap.backend.LDAPBackend'`` to ``AUTHENTICATION_BACKENDS``. Do not add anything to ``INSTALLED_APPS``. .. code-block:: python AUTHENTICATION_BACKENDS = [ 'django_auth_ldap.backend.LDAPBackend', ] ``LDAPBackend`` should work with custom user models, but it does assume that a database is present. .. note:: ``LDAPBackend`` does not inherit from ``ModelBackend``. It is possible to use ``LDAPBackend`` exclusively by configuring it to draw group membership from the LDAP server. However, if you would like to assign permissions to individual users or add users to groups within Django, you'll need to have both backends installed: .. code-block:: python AUTHENTICATION_BACKENDS = [ 'django_auth_ldap.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend', ] .. _`python-ldap`: https://pypi.org/project/python-ldap/ .. _`OpenLDAP`: https://www.openldap.org/ Example Configuration ===================== Here is a complete example configuration from ``settings.py`` that exercises nearly all of the features. In this example, we're authenticating against a global pool of users in the directory, but we have a special area set aside for Django groups (``ou=django,ou=groups,dc=example,dc=com``). Remember that most of this is optional if you just need simple authentication. Some default settings and arguments are included for completeness. .. code-block:: python import ldap from django_auth_ldap.config import LDAPSearch, GroupOfNamesType # Baseline configuration. AUTH_LDAP_SERVER_URI = 'ldap://ldap.example.com' AUTH_LDAP_BIND_DN = 'cn=django-agent,dc=example,dc=com' AUTH_LDAP_BIND_PASSWORD = 'phlebotinum' AUTH_LDAP_USER_SEARCH = LDAPSearch( 'ou=users,dc=example,dc=com', ldap.SCOPE_SUBTREE, '(uid=%(user)s)', ) # Or: # AUTH_LDAP_USER_DN_TEMPLATE = 'uid=%(user)s,ou=users,dc=example,dc=com' # Set up the basic group parameters. AUTH_LDAP_GROUP_SEARCH = LDAPSearch( 'ou=django,ou=groups,dc=example,dc=com', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)', ) AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr='cn') # Simple group restrictions AUTH_LDAP_REQUIRE_GROUP = 'cn=enabled,ou=django,ou=groups,dc=example,dc=com' AUTH_LDAP_DENY_GROUP = 'cn=disabled,ou=django,ou=groups,dc=example,dc=com' # Populate the Django user from the LDAP directory. AUTH_LDAP_USER_ATTR_MAP = { 'first_name': 'givenName', 'last_name': 'sn', 'email': 'mail', } AUTH_LDAP_USER_FLAGS_BY_GROUP = { 'is_active': 'cn=active,ou=django,ou=groups,dc=example,dc=com', 'is_staff': 'cn=staff,ou=django,ou=groups,dc=example,dc=com', 'is_superuser': 'cn=superuser,ou=django,ou=groups,dc=example,dc=com', } # This is the default, but I like to be explicit. AUTH_LDAP_ALWAYS_UPDATE_USER = True # Use LDAP group membership to calculate group permissions. AUTH_LDAP_FIND_GROUP_PERMS = True # Cache distinguished names and group memberships for an hour to minimize # LDAP traffic. AUTH_LDAP_CACHE_TIMEOUT = 3600 # Keep ModelBackend around for per-user permissions and maybe a local # superuser. AUTHENTICATION_BACKENDS = ( 'django_auth_ldap.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend', ) Contributing ============ If you'd like to contribute, the best approach is to send a well-formed pull request, complete with tests and documentation. Pull requests should be focused: trying to do more than one thing in a single request will make it more difficult to process. If you have a bug or feature request you can try `logging an issue`_. There's no harm in creating an issue and then submitting a pull request to resolve it. This can be a good way to start a conversation and can serve as an anchor point. .. _`logging an issue`: https://github.com/django-auth-ldap/django-auth-ldap/issues Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Framework :: Django :: 1.11 Classifier: Framework :: Django :: 2.2 Classifier: Framework :: Django :: 3.0 Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.5 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 :: Software Development :: Libraries :: Python Modules Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP Requires-Python: >=3.5 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574651035.0 django-auth-ldap-2.2.0/README.rst0000664000175000017500000001307300000000000015726 0ustar00jonjon00000000000000================================ Django Authentication Using LDAP ================================ .. image:: https://readthedocs.org/projects/django-auth-ldap/badge/?version=latest :target: https://django-auth-ldap.readthedocs.io/en/latest/ .. image:: https://img.shields.io/pypi/v/django-auth-ldap.svg :target: https://pypi.org/project/django-auth-ldap/ .. image:: https://img.shields.io/travis/django-auth-ldap/django-auth-ldap/master.svg?label=travis-ci :target: http://travis-ci.org/django-auth-ldap/django-auth-ldap .. image:: https://img.shields.io/pypi/l/django-auth-ldap.svg :target: https://raw.githubusercontent.com/django-auth-ldap/django-auth-ldap/master/LICENSE This is a Django authentication backend that authenticates against an LDAP service. Configuration can be as simple as a single distinguished name template, but there are many rich configuration options for working with users, groups, and permissions. * Documentation: https://django-auth-ldap.readthedocs.io/ * PyPI: https://pypi.org/project/django-auth-ldap/ * Repository: https://github.com/django-auth-ldap/django-auth-ldap * Tests: http://travis-ci.org/django-auth-ldap/django-auth-ldap * License: BSD 2-Clause This version is supported on Python 3.5+; and Django 1.11+. It requires `python-ldap`_ >= 3.1. .. _`python-ldap`: https://pypi.org/project/python-ldap/ Installation ============ Install the package with pip: .. code-block:: sh $ pip install django-auth-ldap It requires `python-ldap`_ >= 3.1. You'll need the `OpenLDAP`_ libraries and headers available on your system. To use the auth backend in a Django project, add ``'django_auth_ldap.backend.LDAPBackend'`` to ``AUTHENTICATION_BACKENDS``. Do not add anything to ``INSTALLED_APPS``. .. code-block:: python AUTHENTICATION_BACKENDS = [ 'django_auth_ldap.backend.LDAPBackend', ] ``LDAPBackend`` should work with custom user models, but it does assume that a database is present. .. note:: ``LDAPBackend`` does not inherit from ``ModelBackend``. It is possible to use ``LDAPBackend`` exclusively by configuring it to draw group membership from the LDAP server. However, if you would like to assign permissions to individual users or add users to groups within Django, you'll need to have both backends installed: .. code-block:: python AUTHENTICATION_BACKENDS = [ 'django_auth_ldap.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend', ] .. _`python-ldap`: https://pypi.org/project/python-ldap/ .. _`OpenLDAP`: https://www.openldap.org/ Example Configuration ===================== Here is a complete example configuration from ``settings.py`` that exercises nearly all of the features. In this example, we're authenticating against a global pool of users in the directory, but we have a special area set aside for Django groups (``ou=django,ou=groups,dc=example,dc=com``). Remember that most of this is optional if you just need simple authentication. Some default settings and arguments are included for completeness. .. code-block:: python import ldap from django_auth_ldap.config import LDAPSearch, GroupOfNamesType # Baseline configuration. AUTH_LDAP_SERVER_URI = 'ldap://ldap.example.com' AUTH_LDAP_BIND_DN = 'cn=django-agent,dc=example,dc=com' AUTH_LDAP_BIND_PASSWORD = 'phlebotinum' AUTH_LDAP_USER_SEARCH = LDAPSearch( 'ou=users,dc=example,dc=com', ldap.SCOPE_SUBTREE, '(uid=%(user)s)', ) # Or: # AUTH_LDAP_USER_DN_TEMPLATE = 'uid=%(user)s,ou=users,dc=example,dc=com' # Set up the basic group parameters. AUTH_LDAP_GROUP_SEARCH = LDAPSearch( 'ou=django,ou=groups,dc=example,dc=com', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)', ) AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr='cn') # Simple group restrictions AUTH_LDAP_REQUIRE_GROUP = 'cn=enabled,ou=django,ou=groups,dc=example,dc=com' AUTH_LDAP_DENY_GROUP = 'cn=disabled,ou=django,ou=groups,dc=example,dc=com' # Populate the Django user from the LDAP directory. AUTH_LDAP_USER_ATTR_MAP = { 'first_name': 'givenName', 'last_name': 'sn', 'email': 'mail', } AUTH_LDAP_USER_FLAGS_BY_GROUP = { 'is_active': 'cn=active,ou=django,ou=groups,dc=example,dc=com', 'is_staff': 'cn=staff,ou=django,ou=groups,dc=example,dc=com', 'is_superuser': 'cn=superuser,ou=django,ou=groups,dc=example,dc=com', } # This is the default, but I like to be explicit. AUTH_LDAP_ALWAYS_UPDATE_USER = True # Use LDAP group membership to calculate group permissions. AUTH_LDAP_FIND_GROUP_PERMS = True # Cache distinguished names and group memberships for an hour to minimize # LDAP traffic. AUTH_LDAP_CACHE_TIMEOUT = 3600 # Keep ModelBackend around for per-user permissions and maybe a local # superuser. AUTHENTICATION_BACKENDS = ( 'django_auth_ldap.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend', ) Contributing ============ If you'd like to contribute, the best approach is to send a well-formed pull request, complete with tests and documentation. Pull requests should be focused: trying to do more than one thing in a single request will make it more difficult to process. If you have a bug or feature request you can try `logging an issue`_. There's no harm in creating an issue and then submitting a pull request to resolve it. This can be a good way to start a conversation and can serve as an anchor point. .. _`logging an issue`: https://github.com/django-auth-ldap/django-auth-ldap/issues ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1591141796.8964705 django-auth-ldap-2.2.0/django_auth_ldap/0000775000175000017500000000000000000000000017516 5ustar00jonjon00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591141491.0 django-auth-ldap-2.2.0/django_auth_ldap/__init__.py0000664000175000017500000000024700000000000021632 0ustar00jonjon00000000000000VERSION = (2, 2, 0) __version__ = ".".join(str(i) for i in VERSION) # Deprecated. Use VERSION and __version__ instead. version = VERSION version_string = __version__ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1586011300.0 django-auth-ldap-2.2.0/django_auth_ldap/backend.py0000664000175000017500000010516500000000000021467 0ustar00jonjon00000000000000# Copyright (c) 2009, Peter Sagerson # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # - Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # - 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 HOLDER 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. """ LDAP authentication backend Complete documentation can be found in docs/howto/auth-ldap.txt (or the thing it compiles to). Use of this backend requires the python-ldap module. To support unit tests, we import ldap in a single centralized place (config._LDAPConfig) so that the test harness can insert a mock object. A few notes on naming conventions. If an identifier ends in _dn, it is a string representation of a distinguished name. If it ends in _info, it is a 2-tuple containing a DN and a dictionary of lists of attributes. ldap.search_s returns a list of such structures. An identifier that ends in _attrs is the dictionary of attributes from the _info structure. A connection is an LDAPObject that has been successfully bound with a DN and password. The identifier 'user' always refers to a User model object; LDAP user information will be user_dn or user_info. Additional classes can be found in the config module next to this one. """ import copy import operator import pprint import re import warnings from functools import reduce import django.conf import django.dispatch import ldap from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, Permission from django.core.cache import cache from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.utils.inspect import func_supports_parameter from django_auth_ldap.config import ( ConfigurationWarning, LDAPGroupQuery, LDAPSearch, _LDAPConfig, ) logger = _LDAPConfig.get_logger() # Exported signals # Allows clients to perform custom user population. # Passed arguments: user, ldap_user populate_user = django.dispatch.Signal() # Allows clients to inspect and perform special handling of LDAPError # exceptions. Exceptions raised by handlers will be propagated out. # Passed arguments: context, user, exception ldap_error = django.dispatch.Signal() class LDAPBackend: """ The main backend class. This implements the auth backend API, although it actually delegates most of its work to _LDAPUser, which is defined next. """ supports_anonymous_user = False supports_object_permissions = True supports_inactive_user = False _settings = None _ldap = None # The cached ldap module (or mock object) # This is prepended to our internal setting names to produce the names we # expect in Django's settings file. Subclasses can change this in order to # support multiple collections of settings. settings_prefix = "AUTH_LDAP_" # Default settings to override the built-in defaults. default_settings = {} def __getstate__(self): """ Exclude certain cached properties from pickling. """ return { k: v for k, v in self.__dict__.items() if k not in ["_settings", "_ldap"] } @property def settings(self): if self._settings is None: self._settings = LDAPSettings(self.settings_prefix, self.default_settings) return self._settings @settings.setter def settings(self, settings): self._settings = settings @property def ldap(self): if self._ldap is None: options = getattr(django.conf.settings, "AUTH_LDAP_GLOBAL_OPTIONS", None) self._ldap = _LDAPConfig.get_ldap(options) return self._ldap def get_user_model(self): """ By default, this will return the model class configured by AUTH_USER_MODEL. Subclasses may wish to override it and return a proxy model. """ return get_user_model() # # The Django auth backend API # def authenticate(self, request, username=None, password=None, **kwargs): if username is None: return None if password or self.settings.PERMIT_EMPTY_PASSWORD: ldap_user = _LDAPUser(self, username=username.strip(), request=request) user = self.authenticate_ldap_user(ldap_user, password) else: logger.debug("Rejecting empty password for {}".format(username)) user = None return user def get_user(self, user_id): user = None try: user = self.get_user_model().objects.get(pk=user_id) _LDAPUser(self, user=user) # This sets user.ldap_user except ObjectDoesNotExist: pass return user def has_perm(self, user, perm, obj=None): return perm in self.get_all_permissions(user, obj) def has_module_perms(self, user, app_label): for perm in self.get_all_permissions(user): if perm[: perm.index(".")] == app_label: return True return False def get_all_permissions(self, user, obj=None): return self.get_group_permissions(user, obj) def get_group_permissions(self, user, obj=None): if not hasattr(user, "ldap_user") and self.settings.AUTHORIZE_ALL_USERS: _LDAPUser(self, user=user) # This sets user.ldap_user if hasattr(user, "ldap_user"): permissions = user.ldap_user.get_group_permissions() else: permissions = set() return permissions # # Bonus API: populate the Django user from LDAP without authenticating. # def populate_user(self, username): ldap_user = _LDAPUser(self, username=username) return ldap_user.populate_user() # # Hooks for subclasses # def authenticate_ldap_user(self, ldap_user, password): """ Returns an authenticated Django user or None. """ return ldap_user.authenticate(password) def get_or_build_user(self, username, ldap_user): """ This must return a (User, built) 2-tuple for the given LDAP user. username is the Django-friendly username of the user. ldap_user.dn is the user's DN and ldap_user.attrs contains all of their LDAP attributes. The returned User object may be an unsaved model instance. """ model = self.get_user_model() if self.settings.USER_QUERY_FIELD: query_field = self.settings.USER_QUERY_FIELD query_value = ldap_user.attrs[self.settings.USER_ATTR_MAP[query_field]][0] lookup = query_field else: query_field = model.USERNAME_FIELD query_value = username.lower() lookup = "{}__iexact".format(query_field) try: user = model.objects.get(**{lookup: query_value}) except model.DoesNotExist: user = model(**{query_field: query_value}) built = True else: built = False return (user, built) def ldap_to_django_username(self, username): return username def django_to_ldap_username(self, username): return username class _LDAPUser: """ Represents an LDAP user and ultimately fields all requests that the backend receives. This class exists for two reasons. First, it's convenient to have a separate object for each request so that we can use object attributes without running into threading problems. Second, these objects get attached to the User objects, which allows us to cache expensive LDAP information, especially around groups and permissions. self.backend is a reference back to the LDAPBackend instance, which we need to access the ldap module and any hooks that a subclass has overridden. """ class AuthenticationFailed(Exception): pass # Defaults _user = None _user_dn = None _user_attrs = None _groups = None _group_permissions = None _connection = None _connection_bound = False # # Initialization # def __init__(self, backend, username=None, user=None, request=None): """ A new LDAPUser must be initialized with either a username or an authenticated User object. If a user is given, the username will be ignored. """ self.backend = backend self._username = username self._request = request if user is not None: self._set_authenticated_user(user) if username is None and user is None: raise Exception("Internal error: _LDAPUser improperly initialized.") def __deepcopy__(self, memo): obj = object.__new__(type(self)) obj.backend = self.backend obj._user = copy.deepcopy(self._user, memo) # This is all just cached immutable data. There's no point copying it. obj._username = self._username obj._user_dn = self._user_dn obj._user_attrs = self._user_attrs obj._groups = self._groups obj._group_permissions = self._group_permissions # The connection couldn't be copied even if we wanted to obj._connection = self._connection obj._connection_bound = self._connection_bound return obj def __getstate__(self): """ Most of our properties are cached from the LDAP server. We only want to pickle a few crucial things. """ return { k: v for k, v in self.__dict__.items() if k in ["backend", "_username", "_user"] } def _set_authenticated_user(self, user): self._user = user self._username = self.backend.django_to_ldap_username(user.get_username()) user.ldap_user = self user.ldap_username = self._username @property def ldap(self): return self.backend.ldap @property def settings(self): return self.backend.settings # # Entry points # def authenticate(self, password): """ Authenticates against the LDAP directory and returns the corresponding User object if successful. Returns None on failure. """ user = None try: self._authenticate_user_dn(password) self._check_requirements() self._get_or_create_user() user = self._user except self.AuthenticationFailed as e: logger.debug("Authentication failed for {}: {}".format(self._username, e)) except ldap.LDAPError as e: results = ldap_error.send( type(self.backend), context="authenticate", user=self._user, exception=e, ) if len(results) == 0: logger.warning( "Caught LDAPError while authenticating {}: {}".format( self._username, pprint.pformat(e) ) ) except Exception as e: logger.warning("{} while authenticating {}".format(e, self._username)) raise return user def get_group_permissions(self): """ If allowed by the configuration, this returns the set of permissions defined by the user's LDAP group memberships. """ if self._group_permissions is None: self._group_permissions = set() if self.settings.FIND_GROUP_PERMS: try: if self.dn is not None: self._load_group_permissions() except ldap.LDAPError as e: results = ldap_error.send( type(self.backend), context="get_group_permissions", user=self._user, exception=e, ) if len(results) == 0: logger.warning( "Caught LDAPError loading group permissions: {}".format( pprint.pformat(e) ) ) return self._group_permissions def populate_user(self): """ Populates the Django user object using the default bind credentials. """ user = None try: # self.attrs will only be non-None if we were able to load this user # from the LDAP directory, so this filters out nonexistent users. if self.attrs is not None: self._get_or_create_user(force_populate=True) user = self._user except ldap.LDAPError as e: results = ldap_error.send( type(self.backend), context="populate_user", user=self._user, exception=e, ) if len(results) == 0: logger.warning( "Caught LDAPError while authenticating {}: {}".format( self._username, pprint.pformat(e) ) ) except Exception as e: logger.warning("{} while authenticating {}".format(e, self._username)) raise return user # # Public properties (callbacks). These are all lazy for performance reasons. # @property def dn(self): if self._user_dn is None: self._load_user_dn() return self._user_dn @property def attrs(self): if self._user_attrs is None: self._load_user_attrs() return self._user_attrs @property def group_dns(self): return self._get_groups().get_group_dns() @property def group_names(self): return self._get_groups().get_group_names() @property def connection(self): if not self._connection_bound: self._bind() return self._get_connection() # # Authentication # def _authenticate_user_dn(self, password): """ Binds to the LDAP server with the user's DN and password. Raises AuthenticationFailed on failure. """ if self.dn is None: raise self.AuthenticationFailed("failed to map the username to a DN.") try: sticky = self.settings.BIND_AS_AUTHENTICATING_USER self._bind_as(self.dn, password, sticky=sticky) except ldap.INVALID_CREDENTIALS: raise self.AuthenticationFailed("user DN/password rejected by LDAP server.") def _load_user_attrs(self): if self.dn is not None: search = LDAPSearch( self.dn, ldap.SCOPE_BASE, attrlist=self.settings.USER_ATTRLIST ) results = search.execute(self.connection) if results is not None and len(results) > 0: self._user_attrs = results[0][1] def _load_user_dn(self): """ Populates self._user_dn with the distinguished name of our user. This will either construct the DN from a template in AUTH_LDAP_USER_DN_TEMPLATE or connect to the server and search for it. If we have to search, we'll cache the DN. """ if self._using_simple_bind_mode(): self._user_dn = self._construct_simple_user_dn() else: if self.settings.CACHE_TIMEOUT > 0: cache_key = valid_cache_key( "django_auth_ldap.user_dn.{}".format(self._username) ) self._user_dn = cache.get_or_set( cache_key, self._search_for_user_dn, self.settings.CACHE_TIMEOUT ) else: self._user_dn = self._search_for_user_dn() def _using_simple_bind_mode(self): return self.settings.USER_DN_TEMPLATE is not None def _construct_simple_user_dn(self): template = self.settings.USER_DN_TEMPLATE username = ldap.dn.escape_dn_chars(self._username) return template % {"user": username} def _search_for_user_dn(self): """ Searches the directory for a user matching AUTH_LDAP_USER_SEARCH. Populates self._user_dn and self._user_attrs. """ search = self.settings.USER_SEARCH if search is None: raise ImproperlyConfigured( "AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance." ) results = search.execute(self.connection, {"user": self._username}) if results is not None and len(results) == 1: (user_dn, self._user_attrs) = next(iter(results)) else: user_dn = None return user_dn def _check_requirements(self): """ Checks all authentication requirements beyond credentials. Raises AuthenticationFailed on failure. """ self._check_required_group() self._check_denied_group() def _check_required_group(self): """ Returns True if the group requirement (AUTH_LDAP_REQUIRE_GROUP) is met. Always returns True if AUTH_LDAP_REQUIRE_GROUP is None. """ required_group_dn = self.settings.REQUIRE_GROUP if required_group_dn is not None: if not isinstance(required_group_dn, LDAPGroupQuery): required_group_dn = LDAPGroupQuery(required_group_dn) result = required_group_dn.resolve(self) if not result: raise self.AuthenticationFailed( "user does not satisfy AUTH_LDAP_REQUIRE_GROUP" ) return True def _check_denied_group(self): """ Returns True if the negative group requirement (AUTH_LDAP_DENY_GROUP) is met. Always returns True if AUTH_LDAP_DENY_GROUP is None. """ denied_group_dn = self.settings.DENY_GROUP if denied_group_dn is not None: is_member = self._get_groups().is_member_of(denied_group_dn) if is_member: raise self.AuthenticationFailed( "user does not satisfy AUTH_LDAP_DENY_GROUP" ) return True # # User management # def _get_or_create_user(self, force_populate=False): """ Loads the User model object from the database or creates it if it doesn't exist. Also populates the fields, subject to AUTH_LDAP_ALWAYS_UPDATE_USER. """ save_user = False username = self.backend.ldap_to_django_username(self._username) self._user, built = self.backend.get_or_build_user(username, self) self._user.ldap_user = self self._user.ldap_username = self._username should_populate = force_populate or self.settings.ALWAYS_UPDATE_USER or built if built: if self.settings.NO_NEW_USERS: raise self.AuthenticationFailed( "user does not satisfy AUTH_LDAP_NO_NEW_USERS" ) logger.debug("Creating Django user {}".format(username)) self._user.set_unusable_password() save_user = True if should_populate: logger.debug("Populating Django user {}".format(username)) self._populate_user() save_user = True # Give the client a chance to finish populating the user just # before saving. populate_user.send(type(self.backend), user=self._user, ldap_user=self) if save_user: self._user.save() # This has to wait until we're sure the user has a pk. if self.settings.MIRROR_GROUPS or self.settings.MIRROR_GROUPS_EXCEPT: self._normalize_mirror_settings() self._mirror_groups() def _populate_user(self): """ Populates our User object with information from the LDAP directory. """ self._populate_user_from_attributes() self._populate_user_from_group_memberships() def _populate_user_from_attributes(self): for field, attr in self.settings.USER_ATTR_MAP.items(): try: value = self.attrs[attr][0] except (TypeError, LookupError): # TypeError occurs when self.attrs is None as we were unable to # load this user's attributes. logger.warning( "{} does not have a value for the attribute {}".format( self.dn, attr ) ) else: setattr(self._user, field, value) def _populate_user_from_group_memberships(self): for field, group_dns in self.settings.USER_FLAGS_BY_GROUP.items(): try: query = self._normalize_group_dns(group_dns) except ValueError as e: raise ImproperlyConfigured( "{}: {}", self.settings._name("USER_FLAGS_BY_GROUP"), e ) value = query.resolve(self) setattr(self._user, field, value) def _normalize_group_dns(self, group_dns): """ Converts one or more group DNs to an LDAPGroupQuery. group_dns may be a string, a non-empty list or tuple of strings, or an LDAPGroupQuery. The result will be an LDAPGroupQuery. A list or tuple will be joined with the | operator. """ if isinstance(group_dns, LDAPGroupQuery): query = group_dns elif isinstance(group_dns, str): query = LDAPGroupQuery(group_dns) elif isinstance(group_dns, (list, tuple)) and len(group_dns) > 0: query = reduce(operator.or_, map(LDAPGroupQuery, group_dns)) else: raise ValueError(group_dns) return query def _normalize_mirror_settings(self): """ Validates the group mirroring settings and converts them as necessary. """ def malformed_mirror_groups_except(): return ImproperlyConfigured( "{} must be a collection of group names".format( self.settings._name("MIRROR_GROUPS_EXCEPT") ) ) def malformed_mirror_groups(): return ImproperlyConfigured( "{} must be True or a collection of group names".format( self.settings._name("MIRROR_GROUPS") ) ) mge = self.settings.MIRROR_GROUPS_EXCEPT mg = self.settings.MIRROR_GROUPS if mge is not None: if isinstance(mge, (set, frozenset)): pass elif isinstance(mge, (list, tuple)): mge = self.settings.MIRROR_GROUPS_EXCEPT = frozenset(mge) else: raise malformed_mirror_groups_except() if not all(isinstance(value, str) for value in mge): raise malformed_mirror_groups_except() elif mg: warnings.warn( ConfigurationWarning( "Ignoring {} in favor of {}".format( self.settings._name("MIRROR_GROUPS"), self.settings._name("MIRROR_GROUPS_EXCEPT"), ) ) ) mg = self.settings.MIRROR_GROUPS = None if mg is not None: if isinstance(mg, (bool, set, frozenset)): pass elif isinstance(mg, (list, tuple)): mg = self.settings.MIRROR_GROUPS = frozenset(mg) else: raise malformed_mirror_groups() if isinstance(mg, (set, frozenset)) and ( not all(isinstance(value, str) for value in mg) ): raise malformed_mirror_groups() def _mirror_groups(self): """ Mirrors the user's LDAP groups in the Django database and updates the user's membership. """ target_group_names = frozenset(self._get_groups().get_group_names()) current_group_names = frozenset( self._user.groups.values_list("name", flat=True).iterator() ) # These were normalized to sets above. MIRROR_GROUPS_EXCEPT = self.settings.MIRROR_GROUPS_EXCEPT MIRROR_GROUPS = self.settings.MIRROR_GROUPS # If the settings are white- or black-listing groups, we'll update # target_group_names such that we won't modify the membership of groups # beyond our purview. if isinstance(MIRROR_GROUPS_EXCEPT, (set, frozenset)): target_group_names = (target_group_names - MIRROR_GROUPS_EXCEPT) | ( current_group_names & MIRROR_GROUPS_EXCEPT ) elif isinstance(MIRROR_GROUPS, (set, frozenset)): target_group_names = (target_group_names & MIRROR_GROUPS) | ( current_group_names - MIRROR_GROUPS ) if target_group_names != current_group_names: existing_groups = list( Group.objects.filter(name__in=target_group_names).iterator() ) existing_group_names = frozenset(group.name for group in existing_groups) new_groups = [ Group.objects.get_or_create(name=name)[0] for name in target_group_names if name not in existing_group_names ] self._user.groups.set(existing_groups + new_groups) # # Group information # def _load_group_permissions(self): """ Populates self._group_permissions based on LDAP group membership and Django group permissions. """ group_names = self._get_groups().get_group_names() perms = Permission.objects.filter(group__name__in=group_names) perms = perms.values_list("content_type__app_label", "codename") perms = perms.order_by() self._group_permissions = {"{}.{}".format(ct, name) for ct, name in perms} def _get_groups(self): """ Returns an _LDAPUserGroups object, which can determine group membership. """ if self._groups is None: self._groups = _LDAPUserGroups(self) return self._groups # # LDAP connection # def _bind(self): """ Binds to the LDAP server with AUTH_LDAP_BIND_DN and AUTH_LDAP_BIND_PASSWORD. """ self._bind_as(self.settings.BIND_DN, self.settings.BIND_PASSWORD, sticky=True) def _bind_as(self, bind_dn, bind_password, sticky=False): """ Binds to the LDAP server with the given credentials. This does not trap exceptions. If sticky is True, then we will consider the connection to be bound for the life of this object. If False, then the caller only wishes to test the credentials, after which the connection will be considered unbound. """ self._get_connection().simple_bind_s(bind_dn, bind_password) self._connection_bound = sticky def _get_connection(self): """ Returns our cached LDAPObject, which may or may not be bound. """ if self._connection is None: uri = self.settings.SERVER_URI if callable(uri): if func_supports_parameter(uri, "request"): uri = uri(self._request) else: warnings.warn( "Update AUTH_LDAP_SERVER_URI callable %s.%s to accept " "a positional `request` argument. Support for callables " "accepting no arguments will be removed in a future " "version." % (uri.__module__, uri.__name__), DeprecationWarning, ) uri = uri() self._connection = self.backend.ldap.initialize(uri, bytes_mode=False) for opt, value in self.settings.CONNECTION_OPTIONS.items(): self._connection.set_option(opt, value) if self.settings.START_TLS: logger.debug("Initiating TLS") self._connection.start_tls_s() return self._connection class _LDAPUserGroups: """ Represents the set of groups that a user belongs to. """ def __init__(self, ldap_user): self.settings = ldap_user.settings self._ldap_user = ldap_user self._group_type = None self._group_search = None self._group_infos = None self._group_dns = None self._group_names = None self._init_group_settings() def _init_group_settings(self): """ Loads the settings we need to deal with groups. Raises ImproperlyConfigured if anything's not right. """ self._group_type = self.settings.GROUP_TYPE if self._group_type is None: raise ImproperlyConfigured( "AUTH_LDAP_GROUP_TYPE must be an LDAPGroupType instance." ) self._group_search = self.settings.GROUP_SEARCH if self._group_search is None: raise ImproperlyConfigured( "AUTH_LDAP_GROUP_SEARCH must be an LDAPSearch instance." ) def get_group_names(self): """ Returns the set of Django group names that this user belongs to by virtue of LDAP group memberships. """ if self._group_names is None: self._load_cached_attr("_group_names") if self._group_names is None: group_infos = self._get_group_infos() self._group_names = { self._group_type.group_name_from_info(group_info) for group_info in group_infos } self._cache_attr("_group_names") return self._group_names def is_member_of(self, group_dn): """ Returns true if our user is a member of the given group. """ is_member = None # Normalize the DN group_dn = group_dn.lower() # If we have self._group_dns, we'll use it. Otherwise, we'll try to # avoid the cost of loading it. if self._group_dns is None: is_member = self._group_type.is_member(self._ldap_user, group_dn) if is_member is None: is_member = group_dn in self.get_group_dns() logger.debug( "{} is{}a member of {}".format( self._ldap_user.dn, is_member and " " or " not ", group_dn ) ) return is_member def get_group_dns(self): """ Returns a (cached) set of the distinguished names in self._group_infos. """ if self._group_dns is None: group_infos = self._get_group_infos() self._group_dns = {group_info[0] for group_info in group_infos} return self._group_dns def _get_group_infos(self): """ Returns a (cached) list of group_info structures for the groups that our user is a member of. """ if self._group_infos is None: self._group_infos = self._group_type.user_groups( self._ldap_user, self._group_search ) return self._group_infos def _load_cached_attr(self, attr_name): if self.settings.CACHE_TIMEOUT > 0: key = self._cache_key(attr_name) value = cache.get(key) setattr(self, attr_name, value) def _cache_attr(self, attr_name): if self.settings.CACHE_TIMEOUT > 0: key = self._cache_key(attr_name) value = getattr(self, attr_name, None) cache.set(key, value, self.settings.CACHE_TIMEOUT) def _cache_key(self, attr_name): """ Memcache keys can't have spaces in them, so we'll remove them from the DN for maximum compatibility. """ dn = self._ldap_user.dn return valid_cache_key( "auth_ldap.{}.{}.{}".format(type(self).__name__, attr_name, dn) ) class LDAPSettings: """ This is a simple class to take the place of the global settings object. An instance will contain all of our settings as attributes, with default values if they are not specified by the configuration. """ _prefix = "AUTH_LDAP_" defaults = { "ALWAYS_UPDATE_USER": True, "AUTHORIZE_ALL_USERS": False, "BIND_AS_AUTHENTICATING_USER": False, "BIND_DN": "", "BIND_PASSWORD": "", "CONNECTION_OPTIONS": {}, "DENY_GROUP": None, "FIND_GROUP_PERMS": False, "CACHE_TIMEOUT": 0, "GROUP_SEARCH": None, "GROUP_TYPE": None, "MIRROR_GROUPS": None, "MIRROR_GROUPS_EXCEPT": None, "PERMIT_EMPTY_PASSWORD": False, "REQUIRE_GROUP": None, "NO_NEW_USERS": False, "SERVER_URI": "ldap://localhost", "START_TLS": False, "USER_QUERY_FIELD": None, "USER_ATTRLIST": None, "USER_ATTR_MAP": {}, "USER_DN_TEMPLATE": None, "USER_FLAGS_BY_GROUP": {}, "USER_SEARCH": None, } def __init__(self, prefix="AUTH_LDAP_", defaults={}): """ Loads our settings from django.conf.settings, applying defaults for any that are omitted. """ self._prefix = prefix defaults = dict(self.defaults, **defaults) for name, default in defaults.items(): value = getattr(django.conf.settings, prefix + name, default) setattr(self, name, value) # Compatibility with old caching settings. if getattr( django.conf.settings, self._name("CACHE_GROUPS"), defaults.get("CACHE_GROUPS"), ): warnings.warn( "Found deprecated setting AUTH_LDAP_CACHE_GROUP. Use " "AUTH_LDAP_CACHE_TIMEOUT instead.", DeprecationWarning, ) self.CACHE_TIMEOUT = getattr( django.conf.settings, self._name("GROUP_CACHE_TIMEOUT"), defaults.get("GROUP_CACHE_TIMEOUT", 3600), ) def _name(self, suffix): return self._prefix + suffix def valid_cache_key(key): """ Sanitizes a cache key for memcached. """ return re.sub(r"\s+", "+", key)[:250] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591140955.0 django-auth-ldap-2.2.0/django_auth_ldap/config.py0000664000175000017500000005501200000000000021340 0ustar00jonjon00000000000000# Copyright (c) 2009, Peter Sagerson # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # - Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # - 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 HOLDER 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. """ This module contains classes that will be needed for configuration of LDAP authentication. Unlike backend.py, this is safe to import into settings.py. Please see the docstring on the backend module for more information, including notes on naming conventions. """ import logging import pprint import ldap import ldap.filter from django.utils.tree import Node class ConfigurationWarning(UserWarning): pass class _LDAPConfig: """ A private class that loads and caches some global objects. """ logger = None _ldap_configured = False @classmethod def get_ldap(cls, global_options=None): """ Returns the configured ldap module. """ # Apply global LDAP options once if not cls._ldap_configured and global_options is not None: for opt, value in global_options.items(): ldap.set_option(opt, value) cls._ldap_configured = True return ldap @classmethod def get_logger(cls): """ Initializes and returns our logger instance. """ if cls.logger is None: cls.logger = logging.getLogger("django_auth_ldap") cls.logger.addHandler(logging.NullHandler()) return cls.logger # Our global logger logger = _LDAPConfig.get_logger() class LDAPSearch: """ Public class that holds a set of LDAP search parameters. Objects of this class should be considered immutable. Only the initialization method is documented for configuration purposes. Internal clients may use the other methods to refine and execute the search. """ def __init__(self, base_dn, scope, filterstr="(objectClass=*)", attrlist=None): """ These parameters are the same as the first three parameters to ldap.search_s. """ self.base_dn = base_dn self.scope = scope self.filterstr = filterstr self.attrlist = attrlist self.ldap = _LDAPConfig.get_ldap() def __repr__(self): return "<{}: {}>".format(type(self).__name__, self.base_dn) def search_with_additional_terms(self, term_dict, escape=True): """ Returns a new search object with additional search terms and-ed to the filter string. term_dict maps attribute names to assertion values. If you don't want the values escaped, pass escape=False. """ term_strings = [self.filterstr] for name, value in term_dict.items(): if escape: value = self.ldap.filter.escape_filter_chars(value) term_strings.append("({}={})".format(name, value)) filterstr = "(&{})".format("".join(term_strings)) return type(self)(self.base_dn, self.scope, filterstr, attrlist=self.attrlist) def search_with_additional_term_string(self, filterstr): """ Returns a new search object with filterstr and-ed to the original filter string. The caller is responsible for passing in a properly escaped string. """ filterstr = "(&{}{})".format(self.filterstr, filterstr) return type(self)(self.base_dn, self.scope, filterstr, attrlist=self.attrlist) def execute(self, connection, filterargs=(), escape=True): """ Executes the search on the given connection (an LDAPObject). filterargs is an object that will be used for expansion of the filter string. If escape is True, values in filterargs will be escaped. The python-ldap library returns utf8-encoded strings. For the sake of sanity, this method will decode all result strings and return them as Unicode. """ if escape: filterargs = self._escape_filterargs(filterargs) try: filterstr = self.filterstr % filterargs results = connection.search_s( self.base_dn, self.scope, filterstr, self.attrlist ) except ldap.LDAPError as e: results = [] logger.error( "search_s('{}', {}, '{}') raised {}".format( self.base_dn, self.scope, filterstr, pprint.pformat(e) ) ) return self._process_results(results) def _begin(self, connection, filterargs=(), escape=True): """ Begins an asynchronous search and returns the message id to retrieve the results. filterargs is an object that will be used for expansion of the filter string. If escape is True, values in filterargs will be escaped. """ if escape: filterargs = self._escape_filterargs(filterargs) try: filterstr = self.filterstr % filterargs msgid = connection.search( self.base_dn, self.scope, filterstr, self.attrlist ) except ldap.LDAPError as e: msgid = None logger.error( "search('{}', {}, '{}') raised {}".format( self.base_dn, self.scope, filterstr, pprint.pformat(e) ) ) return msgid def _results(self, connection, msgid): """ Returns the result of a previous asynchronous query. """ try: kind, results = connection.result(msgid) if kind not in (ldap.RES_SEARCH_ENTRY, ldap.RES_SEARCH_RESULT): results = [] except ldap.LDAPError as e: results = [] logger.error("result({}) raised {}".format(msgid, pprint.pformat(e))) return self._process_results(results) def _escape_filterargs(self, filterargs): """ Escapes values in filterargs. filterargs is a value suitable for Django's string formatting operator (%), which means it's either a tuple or a dict. This return a new tuple or dict with all values escaped for use in filter strings. """ if isinstance(filterargs, tuple): filterargs = tuple( self.ldap.filter.escape_filter_chars(value) for value in filterargs ) elif isinstance(filterargs, dict): filterargs = { key: self.ldap.filter.escape_filter_chars(value) for key, value in filterargs.items() } else: raise TypeError("filterargs must be a tuple or dict.") return filterargs def _process_results(self, results): """ Returns a sanitized copy of raw LDAP results. This scrubs out references, decodes utf8, normalizes DNs, etc. """ results = [r for r in results if r[0] is not None] results = _DeepStringCoder("utf-8").decode(results) # The normal form of a DN is lower case. results = [(r[0].lower(), r[1]) for r in results] result_dns = [result[0] for result in results] logger.debug( "search_s('{}', {}, '{}') returned {} objects: {}".format( self.base_dn, self.scope, self.filterstr, len(result_dns), "; ".join(result_dns), ) ) return results class LDAPSearchUnion: """ A compound search object that returns the union of the results. Instantiate it with one or more LDAPSearch objects. """ def __init__(self, *args): self.searches = args self.ldap = _LDAPConfig.get_ldap() def search_with_additional_terms(self, term_dict, escape=True): searches = [ s.search_with_additional_terms(term_dict, escape) for s in self.searches ] return type(self)(*searches) def search_with_additional_term_string(self, filterstr): searches = [ s.search_with_additional_term_string(filterstr) for s in self.searches ] return type(self)(*searches) def execute(self, connection, filterargs=(), escape=True): msgids = [ search._begin(connection, filterargs, escape) for search in self.searches ] results = {} for search, msgid in zip(self.searches, msgids): if msgid is not None: result = search._results(connection, msgid) results.update(dict(result)) return results.items() class _DeepStringCoder: """ Encodes and decodes strings in a nested structure of lists, tuples, and dicts. This is helpful when interacting with the Unicode-unaware python-ldap. """ def __init__(self, encoding): self.encoding = encoding self.ldap = _LDAPConfig.get_ldap() def decode(self, value): try: if isinstance(value, bytes): value = value.decode(self.encoding) elif isinstance(value, list): value = self._decode_list(value) elif isinstance(value, tuple): value = tuple(self._decode_list(value)) elif isinstance(value, dict): value = self._decode_dict(value) except UnicodeDecodeError: pass return value def _decode_list(self, value): return [self.decode(v) for v in value] def _decode_dict(self, value): # Attribute dictionaries should be case-insensitive. python-ldap # defines this, although for some reason, it doesn't appear to use it # for search results. decoded = self.ldap.cidict.cidict() for k, v in value.items(): decoded[self.decode(k)] = self.decode(v) return decoded class LDAPGroupType: """ This is an abstract base class for classes that determine LDAP group membership. A group can mean many different things in LDAP, so we will need a concrete subclass for each grouping mechanism. Clients may subclass this if they have a group mechanism that is not handled by a built-in implementation. name_attr is the name of the LDAP attribute from which we will take the Django group name. Subclasses in this file must use self.ldap to access the python-ldap module. This will be a mock object during unit tests. """ def __init__(self, name_attr="cn"): self.name_attr = name_attr self.ldap = _LDAPConfig.get_ldap() def user_groups(self, ldap_user, group_search): """ Returns a list of group_info structures, each one a group to which ldap_user belongs. group_search is an LDAPSearch object that returns all of the groups that the user might belong to. Typical implementations will apply additional filters to group_search and return the results of the search. ldap_user represents the user and has the following three properties: dn: the distinguished name attrs: a dictionary of LDAP attributes (with lists of values) connection: an LDAPObject that has been bound with credentials This is the primitive method in the API and must be implemented. """ return [] def is_member(self, ldap_user, group_dn): """ This method is an optimization for determining group membership without loading all of the user's groups. Subclasses that are able to do this may return True or False. ldap_user is as above. group_dn is the distinguished name of the group in question. The base implementation returns None, which means we don't have enough information. The caller will have to call user_groups() instead and look for group_dn in the results. """ return None def group_name_from_info(self, group_info): """ Given the (DN, attrs) 2-tuple of an LDAP group, this returns the name of the Django group. This may return None to indicate that a particular LDAP group has no corresponding Django group. The base implementation returns the value of the cn attribute, or whichever attribute was given to __init__ in the name_attr parameter. """ try: name = group_info[1][self.name_attr][0] except (KeyError, IndexError): name = None return name class PosixGroupType(LDAPGroupType): """ An LDAPGroupType subclass that handles groups of class posixGroup. """ def user_groups(self, ldap_user, group_search): """ Searches for any group that is either the user's primary or contains the user as a member. """ groups = [] try: user_uid = ldap_user.attrs["uid"][0] if "gidNumber" in ldap_user.attrs: user_gid = ldap_user.attrs["gidNumber"][0] filterstr = "(|(gidNumber={})(memberUid={}))".format( self.ldap.filter.escape_filter_chars(user_gid), self.ldap.filter.escape_filter_chars(user_uid), ) else: filterstr = "(memberUid={})".format( self.ldap.filter.escape_filter_chars(user_uid) ) search = group_search.search_with_additional_term_string(filterstr) groups = search.execute(ldap_user.connection) except (KeyError, IndexError): pass return groups def is_member(self, ldap_user, group_dn): """ Returns True if the group is the user's primary group or if the user is listed in the group's memberUid attribute. """ try: user_uid = ldap_user.attrs["uid"][0] try: is_member = ldap_user.connection.compare_s( group_dn, "memberUid", user_uid.encode() ) except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): is_member = False if not is_member: try: user_gid = ldap_user.attrs["gidNumber"][0] is_member = ldap_user.connection.compare_s( group_dn, "gidNumber", user_gid.encode() ) except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): is_member = False except (KeyError, IndexError): is_member = False return is_member class MemberDNGroupType(LDAPGroupType): """ A group type that stores lists of members as distinguished names. """ def __init__(self, member_attr, name_attr="cn"): """ member_attr is the attribute on the group object that holds the list of member DNs. """ self.member_attr = member_attr super().__init__(name_attr) def __repr__(self): return "<{}: {}>".format(type(self).__name__, self.member_attr) def user_groups(self, ldap_user, group_search): search = group_search.search_with_additional_terms( {self.member_attr: ldap_user.dn} ) return search.execute(ldap_user.connection) def is_member(self, ldap_user, group_dn): try: result = ldap_user.connection.compare_s( group_dn, self.member_attr, ldap_user.dn.encode() ) except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): result = 0 return result class NestedMemberDNGroupType(LDAPGroupType): """ A group type that stores lists of members as distinguished names and supports nested groups. There is no shortcut for is_member in this case, so it's left unimplemented. """ def __init__(self, member_attr, name_attr="cn"): """ member_attr is the attribute on the group object that holds the list of member DNs. """ self.member_attr = member_attr super().__init__(name_attr) def user_groups(self, ldap_user, group_search): """ This searches for all of a user's groups from the bottom up. In other words, it returns the groups that the user belongs to, the groups that those groups belong to, etc. Circular references will be detected and pruned. """ group_info_map = {} # Maps group_dn to group_info of groups we've found member_dn_set = {ldap_user.dn} # Member DNs to search with next handled_dn_set = set() # Member DNs that we've already searched with while len(member_dn_set) > 0: group_infos = self.find_groups_with_any_member( member_dn_set, group_search, ldap_user.connection ) new_group_info_map = {info[0]: info for info in group_infos} group_info_map.update(new_group_info_map) handled_dn_set.update(member_dn_set) # Get ready for the next iteration. To avoid cycles, we make sure # never to search with the same member DN twice. member_dn_set = set(new_group_info_map.keys()) - handled_dn_set return group_info_map.values() def find_groups_with_any_member(self, member_dn_set, group_search, connection): terms = [ "({}={})".format(self.member_attr, self.ldap.filter.escape_filter_chars(dn)) for dn in member_dn_set ] filterstr = "(|{})".format("".join(terms)) search = group_search.search_with_additional_term_string(filterstr) return search.execute(connection) class GroupOfNamesType(MemberDNGroupType): """ An LDAPGroupType subclass that handles groups of class groupOfNames. """ def __init__(self, name_attr="cn"): super().__init__("member", name_attr) class NestedGroupOfNamesType(NestedMemberDNGroupType): """ An LDAPGroupType subclass that handles groups of class groupOfNames with nested group references. """ def __init__(self, name_attr="cn"): super().__init__("member", name_attr) class GroupOfUniqueNamesType(MemberDNGroupType): """ An LDAPGroupType subclass that handles groups of class groupOfUniqueNames. """ def __init__(self, name_attr="cn"): super().__init__("uniqueMember", name_attr) class NestedGroupOfUniqueNamesType(NestedMemberDNGroupType): """ An LDAPGroupType subclass that handles groups of class groupOfUniqueNames with nested group references. """ def __init__(self, name_attr="cn"): super().__init__("uniqueMember", name_attr) class ActiveDirectoryGroupType(MemberDNGroupType): """ An LDAPGroupType subclass that handles Active Directory groups. """ def __init__(self, name_attr="cn"): super().__init__("member", name_attr) class NestedActiveDirectoryGroupType(NestedMemberDNGroupType): """ An LDAPGroupType subclass that handles Active Directory groups with nested group references. """ def __init__(self, name_attr="cn"): super().__init__("member", name_attr) class OrganizationalRoleGroupType(MemberDNGroupType): """ An LDAPGroupType subclass that handles groups of class organizationalRole. """ def __init__(self, name_attr="cn"): super().__init__("roleOccupant", name_attr) class NestedOrganizationalRoleGroupType(NestedMemberDNGroupType): """ An LDAPGroupType subclass that handles groups of class OrganizationalRoleGroupType with nested group references. """ def __init__(self, name_attr="cn"): super().__init__("roleOccupant", name_attr) class LDAPGroupQuery(Node): """ Represents a compound query for group membership. This can be used to construct an arbitrarily complex group membership query with AND, OR, and NOT logical operators. Construct primitive queries with a group DN as the only argument. These queries can then be combined with the ``&``, ``|``, and ``~`` operators. :param str group_dn: The DN of a group to test for membership. """ # Connection types AND = "AND" OR = "OR" default = AND _CONNECTORS = [AND, OR] def __init__(self, *args, **kwargs): super().__init__(children=list(args) + list(kwargs.items())) def __and__(self, other): return self._combine(other, self.AND) def __or__(self, other): return self._combine(other, self.OR) def __invert__(self): obj = type(self)() obj.add(self, self.AND) obj.negate() return obj def _combine(self, other, conn): if not isinstance(other, LDAPGroupQuery): raise TypeError(other) if conn not in self._CONNECTORS: raise ValueError(conn) obj = type(self)() obj.connector = conn obj.add(self, conn) obj.add(other, conn) return obj def resolve(self, ldap_user, groups=None): if groups is None: groups = ldap_user._get_groups() result = self.aggregator(self._resolve_children(ldap_user, groups)) if self.negated: result = not result return result @property def aggregator(self): """ Returns a function for aggregating a sequence of sub-results. """ if self.connector == self.AND: aggregator = all elif self.connector == self.OR: aggregator = any else: raise ValueError(self.connector) return aggregator def _resolve_children(self, ldap_user, groups): """ Generates the query result for each child. """ for child in self.children: if isinstance(child, LDAPGroupQuery): yield child.resolve(ldap_user, groups) else: yield groups.is_member_of(child) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1591141796.8974705 django-auth-ldap-2.2.0/django_auth_ldap.egg-info/0000775000175000017500000000000000000000000021210 5ustar00jonjon00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591141796.0 django-auth-ldap-2.2.0/django_auth_ldap.egg-info/PKG-INFO0000664000175000017500000002043200000000000022306 0ustar00jonjon00000000000000Metadata-Version: 1.2 Name: django-auth-ldap Version: 2.2.0 Summary: Django LDAP authentication backend. Home-page: https://github.com/django-auth-ldap/django-auth-ldap Author: Peter Sagerson Author-email: psagers@ignorare.net License: BSD Project-URL: Documentation, https://django-auth-ldap.readthedocs.io/ Project-URL: Source, https://github.com/django-auth-ldap/django-auth-ldap Project-URL: Tracker, https://github.com/django-auth-ldap/django-auth-ldap/issues Description: ================================ Django Authentication Using LDAP ================================ .. image:: https://readthedocs.org/projects/django-auth-ldap/badge/?version=latest :target: https://django-auth-ldap.readthedocs.io/en/latest/ .. image:: https://img.shields.io/pypi/v/django-auth-ldap.svg :target: https://pypi.org/project/django-auth-ldap/ .. image:: https://img.shields.io/travis/django-auth-ldap/django-auth-ldap/master.svg?label=travis-ci :target: http://travis-ci.org/django-auth-ldap/django-auth-ldap .. image:: https://img.shields.io/pypi/l/django-auth-ldap.svg :target: https://raw.githubusercontent.com/django-auth-ldap/django-auth-ldap/master/LICENSE This is a Django authentication backend that authenticates against an LDAP service. Configuration can be as simple as a single distinguished name template, but there are many rich configuration options for working with users, groups, and permissions. * Documentation: https://django-auth-ldap.readthedocs.io/ * PyPI: https://pypi.org/project/django-auth-ldap/ * Repository: https://github.com/django-auth-ldap/django-auth-ldap * Tests: http://travis-ci.org/django-auth-ldap/django-auth-ldap * License: BSD 2-Clause This version is supported on Python 3.5+; and Django 1.11+. It requires `python-ldap`_ >= 3.1. .. _`python-ldap`: https://pypi.org/project/python-ldap/ Installation ============ Install the package with pip: .. code-block:: sh $ pip install django-auth-ldap It requires `python-ldap`_ >= 3.1. You'll need the `OpenLDAP`_ libraries and headers available on your system. To use the auth backend in a Django project, add ``'django_auth_ldap.backend.LDAPBackend'`` to ``AUTHENTICATION_BACKENDS``. Do not add anything to ``INSTALLED_APPS``. .. code-block:: python AUTHENTICATION_BACKENDS = [ 'django_auth_ldap.backend.LDAPBackend', ] ``LDAPBackend`` should work with custom user models, but it does assume that a database is present. .. note:: ``LDAPBackend`` does not inherit from ``ModelBackend``. It is possible to use ``LDAPBackend`` exclusively by configuring it to draw group membership from the LDAP server. However, if you would like to assign permissions to individual users or add users to groups within Django, you'll need to have both backends installed: .. code-block:: python AUTHENTICATION_BACKENDS = [ 'django_auth_ldap.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend', ] .. _`python-ldap`: https://pypi.org/project/python-ldap/ .. _`OpenLDAP`: https://www.openldap.org/ Example Configuration ===================== Here is a complete example configuration from ``settings.py`` that exercises nearly all of the features. In this example, we're authenticating against a global pool of users in the directory, but we have a special area set aside for Django groups (``ou=django,ou=groups,dc=example,dc=com``). Remember that most of this is optional if you just need simple authentication. Some default settings and arguments are included for completeness. .. code-block:: python import ldap from django_auth_ldap.config import LDAPSearch, GroupOfNamesType # Baseline configuration. AUTH_LDAP_SERVER_URI = 'ldap://ldap.example.com' AUTH_LDAP_BIND_DN = 'cn=django-agent,dc=example,dc=com' AUTH_LDAP_BIND_PASSWORD = 'phlebotinum' AUTH_LDAP_USER_SEARCH = LDAPSearch( 'ou=users,dc=example,dc=com', ldap.SCOPE_SUBTREE, '(uid=%(user)s)', ) # Or: # AUTH_LDAP_USER_DN_TEMPLATE = 'uid=%(user)s,ou=users,dc=example,dc=com' # Set up the basic group parameters. AUTH_LDAP_GROUP_SEARCH = LDAPSearch( 'ou=django,ou=groups,dc=example,dc=com', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)', ) AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr='cn') # Simple group restrictions AUTH_LDAP_REQUIRE_GROUP = 'cn=enabled,ou=django,ou=groups,dc=example,dc=com' AUTH_LDAP_DENY_GROUP = 'cn=disabled,ou=django,ou=groups,dc=example,dc=com' # Populate the Django user from the LDAP directory. AUTH_LDAP_USER_ATTR_MAP = { 'first_name': 'givenName', 'last_name': 'sn', 'email': 'mail', } AUTH_LDAP_USER_FLAGS_BY_GROUP = { 'is_active': 'cn=active,ou=django,ou=groups,dc=example,dc=com', 'is_staff': 'cn=staff,ou=django,ou=groups,dc=example,dc=com', 'is_superuser': 'cn=superuser,ou=django,ou=groups,dc=example,dc=com', } # This is the default, but I like to be explicit. AUTH_LDAP_ALWAYS_UPDATE_USER = True # Use LDAP group membership to calculate group permissions. AUTH_LDAP_FIND_GROUP_PERMS = True # Cache distinguished names and group memberships for an hour to minimize # LDAP traffic. AUTH_LDAP_CACHE_TIMEOUT = 3600 # Keep ModelBackend around for per-user permissions and maybe a local # superuser. AUTHENTICATION_BACKENDS = ( 'django_auth_ldap.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend', ) Contributing ============ If you'd like to contribute, the best approach is to send a well-formed pull request, complete with tests and documentation. Pull requests should be focused: trying to do more than one thing in a single request will make it more difficult to process. If you have a bug or feature request you can try `logging an issue`_. There's no harm in creating an issue and then submitting a pull request to resolve it. This can be a good way to start a conversation and can serve as an anchor point. .. _`logging an issue`: https://github.com/django-auth-ldap/django-auth-ldap/issues Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Framework :: Django :: 1.11 Classifier: Framework :: Django :: 2.2 Classifier: Framework :: Django :: 3.0 Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.5 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 :: Software Development :: Libraries :: Python Modules Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP Requires-Python: >=3.5 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591141796.0 django-auth-ldap-2.2.0/django_auth_ldap.egg-info/SOURCES.txt0000664000175000017500000000135100000000000023074 0ustar00jonjon00000000000000CHANGES LICENSE MANIFEST.in README.rst setup.cfg setup.py tox.ini django_auth_ldap/__init__.py django_auth_ldap/backend.py django_auth_ldap/config.py django_auth_ldap.egg-info/PKG-INFO django_auth_ldap.egg-info/SOURCES.txt django_auth_ldap.egg-info/dependency_links.txt django_auth_ldap.egg-info/requires.txt django_auth_ldap.egg-info/top_level.txt docs/Makefile docs/authentication.rst docs/changes.rst docs/conf.py docs/contributing.rst docs/custombehavior.rst docs/example.rst docs/groups.rst docs/index.rst docs/install.rst docs/logging.rst docs/multiconfig.rst docs/performance.rst docs/permissions.rst docs/reference.rst docs/users.rst docs/ext/daldocs.py tests/__init__.py tests/models.py tests/settings.py tests/tests.ldif tests/tests.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591141796.0 django-auth-ldap-2.2.0/django_auth_ldap.egg-info/dependency_links.txt0000664000175000017500000000000100000000000025256 0ustar00jonjon00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591141796.0 django-auth-ldap-2.2.0/django_auth_ldap.egg-info/requires.txt0000664000175000017500000000003600000000000023607 0ustar00jonjon00000000000000Django>=1.11 python-ldap>=3.1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591141796.0 django-auth-ldap-2.2.0/django_auth_ldap.egg-info/top_level.txt0000664000175000017500000000002100000000000023733 0ustar00jonjon00000000000000django_auth_ldap ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1591141796.9004705 django-auth-ldap-2.2.0/docs/0000775000175000017500000000000000000000000015163 5ustar00jonjon00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/Makefile0000664000175000017500000000115400000000000016624 0ustar00jonjon00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -n -W SPHINXBUILD = sphinx-build SPHINXPROJ = django-auth-ldap SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/authentication.rst0000664000175000017500000001622100000000000020736 0ustar00jonjon00000000000000Authentication ============== Server Config ------------- If your LDAP server isn't running locally on the default port, you'll want to start by setting :setting:`AUTH_LDAP_SERVER_URI` to point to your server. The value of this setting can be anything that your LDAP library supports. For instance, openldap may allow you to give a comma- or space-separated list of URIs to try in sequence. .. code-block:: python AUTH_LDAP_SERVER_URI = "ldap://ldap.example.com" If your server location is even more dynamic than this, you may provide a function (or any callable object) that returns the URI. The callable is passed a single positional argument: ``request``. You should assume that this will be called on every request, so if it's an expensive operation, some caching is in order. .. code-block:: python from my_module import find_my_ldap_server AUTH_LDAP_SERVER_URI = find_my_ldap_server If you need to configure any python-ldap options, you can set :setting:`AUTH_LDAP_GLOBAL_OPTIONS` and/or :setting:`AUTH_LDAP_CONNECTION_OPTIONS`. For example, disabling referrals is not uncommon: .. code-block:: python import ldap AUTH_LDAP_CONNECTION_OPTIONS = {ldap.OPT_REFERRALS: 0} .. versionchanged:: 1.7.0 When ``AUTH_LDAP_SERVER_URI`` is set to a callable, it is now passed a positional ``request`` argument. Support for no arguments will continue for backwards compatibility but will be removed in a future version. Search/Bind ----------- Now that you can talk to your LDAP server, the next step is to authenticate a username and password. There are two ways to do this, called search/bind and direct bind. The first one involves connecting to the LDAP server either anonymously or with a fixed account and searching for the distinguished name of the authenticating user. Then we can attempt to bind again with the user's password. The second method is to derive the user's DN from his username and attempt to bind as the user directly. Because LDAP searches appear elsewhere in the configuration, the :class:`~django_auth_ldap.config.LDAPSearch` class is provided to encapsulate search information. In this case, the filter parameter should contain the placeholder ``%(user)s``. A simple configuration for the search/bind approach looks like this (some defaults included for completeness): .. code-block:: python import ldap from django_auth_ldap.config import LDAPSearch AUTH_LDAP_BIND_DN = "" AUTH_LDAP_BIND_PASSWORD = "" AUTH_LDAP_USER_SEARCH = LDAPSearch( "ou=users,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ) This will perform an anonymous bind, search under ``"ou=users,dc=example,dc=com"`` for an object with a uid matching the user's name, and try to bind using that DN and the user's password. The search must return exactly one result or authentication will fail. If you can't search anonymously, you can set :setting:`AUTH_LDAP_BIND_DN` to the distinguished name of an authorized user and :setting:`AUTH_LDAP_BIND_PASSWORD` to the password. Search Unions ^^^^^^^^^^^^^ .. versionadded:: 1.1 If you need to search in more than one place for a user, you can use :class:`~django_auth_ldap.config.LDAPSearchUnion`. This takes multiple LDAPSearch objects and returns the union of the results. The precedence of the underlying searches is unspecified. .. code-block:: python import ldap from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion AUTH_LDAP_USER_SEARCH = LDAPSearchUnion( LDAPSearch("ou=users,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"), LDAPSearch("ou=otherusers,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"), ) Direct Bind ----------- To skip the search phase, set :setting:`AUTH_LDAP_USER_DN_TEMPLATE` to a template that will produce the authenticating user's DN directly. This template should have one placeholder, ``%(user)s``. If the first example had used ``ldap.SCOPE_ONELEVEL``, the following would be a more straightforward (and efficient) equivalent: .. code-block:: python AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com" .. _customizing-authentication: Customizing Authentication -------------------------- .. versionadded:: 1.3 It is possible to further customize the authentication process by subclassing :class:`~django_auth_ldap.backend.LDAPBackend` and overriding :meth:`~django_auth_ldap.backend.LDAPBackend.authenticate_ldap_user`. The first argument is the unauthenticated :ref:`ldap_user `, the second is the supplied password. The intent is to give subclasses a simple pre- and post-authentication hook. If a subclass decides to proceed with the authentication, it must call the inherited implementation. It may then return either the authenticated user or ``None``. The behavior of any other return value--such as substituting a different user object--is undefined. :doc:`users` has more on managing Django user objects. Obviously, it is always safe to access ``ldap_user.dn`` before authenticating the user. Accessing ``ldap_user.attrs`` and others should be safe unless you're relying on special binding behavior, such as :setting:`AUTH_LDAP_BIND_AS_AUTHENTICATING_USER`. Notes ----- LDAP is fairly flexible when it comes to matching DNs. :class:`~django_auth_ldap.backend.LDAPBackend` makes an effort to accommodate this by forcing usernames to lower case when creating Django users and trimming whitespace when authenticating. Some LDAP servers are configured to allow users to bind without a password. As a precaution against false positives, :class:`~django_auth_ldap.backend.LDAPBackend` will summarily reject any authentication attempt with an empty password. You can disable this behavior by setting :setting:`AUTH_LDAP_PERMIT_EMPTY_PASSWORD` to True. By default, all LDAP operations are performed with the :setting:`AUTH_LDAP_BIND_DN` and :setting:`AUTH_LDAP_BIND_PASSWORD` credentials, not with the user's. Otherwise, the LDAP connection would be bound as the authenticating user during login requests and as the default credentials during other requests, so you might see inconsistent LDAP attributes depending on the nature of the Django view. If you're willing to accept the inconsistency in order to retrieve attributes while bound as the authenticating user, see :setting:`AUTH_LDAP_BIND_AS_AUTHENTICATING_USER`. By default, LDAP connections are unencrypted and make no attempt to protect sensitive information, such as passwords. When communicating with an LDAP server on localhost or on a local network, this might be fine. If you need a secure connection to the LDAP server, you can either use an ``ldaps://`` URL or enable the StartTLS extension. The latter is generally the preferred mechanism. To enable StartTLS, set :setting:`AUTH_LDAP_START_TLS` to ``True``: .. code-block:: python AUTH_LDAP_START_TLS = True If :class:`~django_auth_ldap.backend.LDAPBackend` receives an :exc:`~ldap.LDAPError` from python_ldap, it will normally swallow it and log a warning. If you'd like to perform any special handling for these exceptions, you can add a signal handler to :data:`django_auth_ldap.backend.ldap_error`. The signal handler can handle the exception any way you like, including re-raising it or any other exception. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/changes.rst0000664000175000017500000000005700000000000017327 0ustar00jonjon00000000000000Change Log ========== .. include:: ../CHANGES ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1591141572.0 django-auth-ldap-2.2.0/docs/conf.py0000664000175000017500000001234600000000000016470 0ustar00jonjon00000000000000# Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. import os import sys sys.path.insert(0, os.path.abspath("ext")) # -- Project information ----------------------------------------------------- project = "django-auth-ldap" copyright = "2009, Peter Sagerson" author = "Peter Sagerson" # The short X.Y version version = "2.2" # The full version, including alpha/beta/rc tags release = "2.2.0" # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ["sphinx.ext.intersphinx", "daldocs"] # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The master toctree document. master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = "django-auth-ldapdoc" # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # # Additional stuff for the LaTeX preamble. # # 'preamble': '', # # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( master_doc, "django-auth-ldap.tex", "django-auth-ldap Documentation", "Peter Sagerson", "manual", ) ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, "django-auth-ldap", "django-auth-ldap Documentation", [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "django-auth-ldap", "django-auth-ldap Documentation", author, "django-auth-ldap", "One line description of project.", "Miscellaneous", ) ] # -- Extension configuration ------------------------------------------------- # -- Options for intersphinx extension --------------------------------------- intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "django": ( "https://docs.djangoproject.com/en/stable/", "https://docs.djangoproject.com/en/stable/_objects/", ), "pythonldap": ("https://python-ldap.readthedocs.io/en/latest/", None), } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/contributing.rst0000664000175000017500000000217600000000000020432 0ustar00jonjon00000000000000Contributing ============ If you'd like to contribute, the best approach is to send a well-formed pull request, complete with tests and documentation. Pull requests should be focused: trying to do more than one thing in a single request will make it more difficult to process. If you have a bug or feature request you can try `logging an issue`_. There's no harm in creating an issue and then submitting a pull request to resolve it. This can be a good way to start a conversation and can serve as an anchor point. .. _`logging an issue`: https://github.com/django-auth-ldap/django-auth-ldap/issues Development ----------- To get set up for development, activate your virtualenv and use pip to install from ``dev-requirements.txt``: .. code-block:: sh $ pip install -r dev-requirements.txt To run the tests: .. code-block:: sh $ django-admin test --settings tests.settings To run the full test suite in a range of environments, run `tox`_ from the root of the project: .. code-block:: sh $ tox This includes some static analysis to detect potential runtime errors and style issues. .. _`tox`: https://tox.readthedocs.io/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/custombehavior.rst0000664000175000017500000001167300000000000020757 0ustar00jonjon00000000000000Custom Behavior =============== There are times that the default :class:`~django_auth_ldap.backend.LDAPBackend` behavior may be insufficient for your needs. In those cases, you can further customize the behavior by following these general steps: * Create your own :class:`~django_auth_ldap.backend.LDAPBackend` subclass. * Use :attr:`~django_auth_ldap.backend.LDAPBackend.default_settings` to define any custom settings you may want to use. * Override :meth:`~django_auth_ldap.backend.LDAPBackend.authenticate_ldap_user` hook and/or any other method as needed. * Define additional methods and attributes as needed. * Access your custom settings via ``self.settings`` inside your :class:`~django_auth_ldap.backend.LDAPBackend` subclass. Subclassing LDAPBackend ----------------------- You can implement your own :class:`~django_auth_ldap.backend.LDAPBackend` subclass if you need some custom behavior. For example, you want to only allow 50 login attempts every 30 minutes, and those numbers may change as needed. Furthermore, any successful login attempt against the LDAP server must send out an SMS notification, but there should be an option to limit this behavior to a specific set of usernames based on a regex. One can accomplish that by doing something like this: .. code-block:: python # mypackage.ldap import re from django.core.cache import cache from django_auth_ldap.backend import LDAPBackend class CustomLDAPBackend(LDAPBackend): default_settings = { "LOGIN_COUNTER_KEY": "CUSTOM_LDAP_LOGIN_ATTEMPT_COUNT", "LOGIN_ATTEMPT_LIMIT": 50, "RESET_TIME": 30 * 60, "USERNAME_REGEX": r"^.*$", } def authenticate_ldap_user(self, ldap_user, password): if self.exceeded_login_attempt_limit(): # Or you can raise a 403 if you do not want # to continue checking other auth backends print("Login attempts exceeded.") return None self.increment_login_attempt_count() user = ldap_user.authenticate(password) if user and self.username_matches_regex(user.username): self.send_sms(user.username) return user @property def login_attempt_count(self): return cache.get_or_set( self.settings.LOGIN_COUNTER_KEY, 0, self.settings.RESET_TIME ) def increment_login_attempt_count(self): try: cache.incr(self.settings.LOGIN_COUNTER_KEY) except ValueError: cache.set(self.settings.LOGIN_COUNTER_KEY, 1, self.settings.RESET_TIME) def exceeded_login_attempt_limit(self): return self.login_attempt_count >= self.settings.LOGIN_ATTEMPT_LIMIT def username_matches_regex(self, username): return re.match(self.settings.USERNAME_REGEX, username) def send_sms(self, username): # Implement your SMS logic here print("SMS sent!") .. code-block:: python # settings.py AUTHENTICATION_BACKENDS = [ # ... "mypackage.ldap.CustomLDAPBackend", # ... ] Using default_settings ---------------------- While you can use your own custom Django settings to create something similar to the sample code above, there are a couple of advantages in using :attr:`~django_auth_ldap.backend.LDAPBackend.default_settings` instead. Following the sample code above, one advantage is that the subclass will now automatically check your Django settings for ``AUTH_LDAP_LOGIN_COUNTER_KEY``, ``AUTH_LDAP_LOGIN_ATTEMPT_LIMIT``, ``AUTH_LDAP_RESET_TIME``, and ``AUTH_LDAP_USERNAME_REGEX``. Another advantage is that for each setting not explicitly defined in your Django settings, the subclass will then use the corresponding default values. This behavior will be very handy in case you will need to override certain settings. Overriding default_settings --------------------------- If down the line, you want to increase the login attempt limit to 100 every 15 minutes, and you only want SMS notifications for usernames with a "zz\_" prefix, then you can simply modify your settings.py like so. .. code-block:: python # settings.py AUTH_LDAP_LOGIN_ATTEMPT_LIMIT = 100 AUTH_LDAP_RESET_TIME = 15 * 60 AUTH_LDAP_USERNAME_REGEX = r"^zz_.*$" AUTHENTICATION_BACKENDS = [ # ... "mypackage.ldap.CustomLDAPBackend", # ... ] If the :attr:`~django_auth_ldap.backend.LDAPBackend.settings_prefix` of the subclass was also changed, then the prefix must also be used in your settings. For example, if the prefix was changed to "AUTH_LDAP_1\_", then it should look like this. .. code-block:: python # settings.py AUTH_LDAP_1_LOGIN_ATTEMPT_LIMIT = 100 AUTH_LDAP_1_RESET_TIME = 15 * 60 AUTH_LDAP_1_USERNAME_REGEX = r"^zz_.*$" AUTHENTICATION_BACKENDS = [ # ... "mypackage.ldap.CustomLDAPBackend", # ... ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/example.rst0000664000175000017500000000463600000000000017361 0ustar00jonjon00000000000000Example Configuration ===================== Here is a complete example configuration from :file:`settings.py` that exercises nearly all of the features. In this example, we're authenticating against a global pool of users in the directory, but we have a special area set aside for Django groups (``ou=django,ou=groups,dc=example,dc=com``). Remember that most of this is optional if you just need simple authentication. Some default settings and arguments are included for completeness. .. code-block:: python import ldap from django_auth_ldap.config import LDAPSearch, GroupOfNamesType # Baseline configuration. AUTH_LDAP_SERVER_URI = "ldap://ldap.example.com" AUTH_LDAP_BIND_DN = "cn=django-agent,dc=example,dc=com" AUTH_LDAP_BIND_PASSWORD = "phlebotinum" AUTH_LDAP_USER_SEARCH = LDAPSearch( "ou=users,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ) # Or: # AUTH_LDAP_USER_DN_TEMPLATE = 'uid=%(user)s,ou=users,dc=example,dc=com' # Set up the basic group parameters. AUTH_LDAP_GROUP_SEARCH = LDAPSearch( "ou=django,ou=groups,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ) AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr="cn") # Simple group restrictions AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=django,ou=groups,dc=example,dc=com" AUTH_LDAP_DENY_GROUP = "cn=disabled,ou=django,ou=groups,dc=example,dc=com" # Populate the Django user from the LDAP directory. AUTH_LDAP_USER_ATTR_MAP = { "first_name": "givenName", "last_name": "sn", "email": "mail", } AUTH_LDAP_USER_FLAGS_BY_GROUP = { "is_active": "cn=active,ou=django,ou=groups,dc=example,dc=com", "is_staff": "cn=staff,ou=django,ou=groups,dc=example,dc=com", "is_superuser": "cn=superuser,ou=django,ou=groups,dc=example,dc=com", } # This is the default, but I like to be explicit. AUTH_LDAP_ALWAYS_UPDATE_USER = True # Use LDAP group membership to calculate group permissions. AUTH_LDAP_FIND_GROUP_PERMS = True # Cache distinguished names and group memberships for an hour to minimize # LDAP traffic. AUTH_LDAP_CACHE_TIMEOUT = 3600 # Keep ModelBackend around for per-user permissions and maybe a local # superuser. AUTHENTICATION_BACKENDS = ( "django_auth_ldap.backend.LDAPBackend", "django.contrib.auth.backends.ModelBackend", ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1591141796.9004705 django-auth-ldap-2.2.0/docs/ext/0000775000175000017500000000000000000000000015763 5ustar00jonjon00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/ext/daldocs.py0000664000175000017500000000030400000000000017743 0ustar00jonjon00000000000000""" Extra stuff for the django-auth-ldap Sphinx docs. """ def setup(app): app.add_crossref_type( directivename="setting", rolename="setting", indextemplate="pair: %s; setting" ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/groups.rst0000664000175000017500000001023000000000000017230 0ustar00jonjon00000000000000Working With Groups =================== Types of Groups --------------- Working with groups in LDAP can be a tricky business, mostly because there are so many different kinds. This module includes an extensible API for working with any kind of group and includes implementations for the most common ones. :class:`~django_auth_ldap.config.LDAPGroupType` is a base class whose concrete subclasses can determine group membership for particular grouping mechanisms. Four built-in subclasses cover most grouping mechanisms: * :class:`~django_auth_ldap.config.PosixGroupType` * :class:`~django_auth_ldap.config.MemberDNGroupType` * :class:`~django_auth_ldap.config.NestedMemberDNGroupType` posixGroup and nisNetgroup objects are somewhat specialized, so they get their own classes. The other two cover mechanisms whereby a group object stores a list of its members as distinguished names. This includes groupOfNames, groupOfUniqueNames, and Active Directory groups, among others. The nested variant allows groups to contain other groups, to as many levels as you like. For convenience and readability, several trivial subclasses of the above are provided: * :class:`~django_auth_ldap.config.GroupOfNamesType` * :class:`~django_auth_ldap.config.NestedGroupOfNamesType` * :class:`~django_auth_ldap.config.GroupOfUniqueNamesType` * :class:`~django_auth_ldap.config.NestedGroupOfUniqueNamesType` * :class:`~django_auth_ldap.config.ActiveDirectoryGroupType` * :class:`~django_auth_ldap.config.NestedActiveDirectoryGroupType` * :class:`~django_auth_ldap.config.OrganizationalRoleGroupType` * :class:`~django_auth_ldap.config.NestedOrganizationalRoleGroupType` Finding Groups -------------- To get started, you'll need to provide some basic information about your LDAP groups. :setting:`AUTH_LDAP_GROUP_SEARCH` is an :class:`~django_auth_ldap.config.LDAPSearch` object that identifies the set of relevant group objects. That is, all groups that users might belong to as well as any others that we might need to know about (in the case of nested groups, for example). :setting:`AUTH_LDAP_GROUP_TYPE` is an instance of the class corresponding to the type of group that will be returned by :setting:`AUTH_LDAP_GROUP_SEARCH`. All groups referenced elsewhere in the configuration must be of this type and part of the search results. .. code-block:: python import ldap from django_auth_ldap.config import LDAPSearch, GroupOfNamesType AUTH_LDAP_GROUP_SEARCH = LDAPSearch( "ou=groups,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" ) AUTH_LDAP_GROUP_TYPE = GroupOfNamesType() .. _limiting-access: Limiting Access --------------- The simplest use of groups is to limit the users who are allowed to log in. If :setting:`AUTH_LDAP_REQUIRE_GROUP` is set, then only users who are members of that group will successfully authenticate. :setting:`AUTH_LDAP_DENY_GROUP` is the reverse: if given, members of this group will be rejected. .. code-block:: python AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=groups,dc=example,dc=com" AUTH_LDAP_DENY_GROUP = "cn=disabled,ou=groups,dc=example,dc=com" However, these two settings alone may not be enough to satisfy your needs. In such cases, you can use the :class:`~django_auth_ldap.config.LDAPGroupQuery` object to perform more complex matches against a user's groups. For example: .. code-block:: python from django_auth_ldap.config import LDAPGroupQuery AUTH_LDAP_REQUIRE_GROUP = ( LDAPGroupQuery("cn=enabled,ou=groups,dc=example,dc=com") | LDAPGroupQuery("cn=also_enabled,ou=groups,dc=example,dc=com") ) & ~LDAPGroupQuery("cn=disabled,ou=groups,dc=example,dc=com") It is important to note a couple features of the example above. First and foremost, this handles the case of both `AUTH_LDAP_REQUIRE_GROUP` and `AUTH_LDAP_DENY_GROUP` in one setting. Second, you can use three operators on these queries: ``&``, ``|``, and ``~``: ``and``, ``or``, and ``not``, respectively. When groups are configured, you can always get the list of a user's groups from ``user.ldap_user.group_dns`` or ``user.ldap_user.group_names``. More advanced uses of groups are covered in the next two sections. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/index.rst0000664000175000017500000000200700000000000017023 0ustar00jonjon00000000000000================================ Django Authentication Using LDAP ================================ This is a Django authentication backend that authenticates against an LDAP service. Configuration can be as simple as a single distinguished name template, but there are many rich configuration options for working with users, groups, and permissions. * Documentation: https://django-auth-ldap.readthedocs.io/ * PyPI: https://pypi.org/project/django-auth-ldap/ * Repository: https://github.com/django-auth-ldap/django-auth-ldap * Tests: http://travis-ci.org/django-auth-ldap/django-auth-ldap * License: BSD 2-Clause This version is supported on Python 3.5+; and Django 1.11+. It requires `python-ldap`_ >= 3.1. .. toctree:: :maxdepth: 2 install authentication groups users permissions multiconfig custombehavior logging performance example reference changes contributing .. _`python-ldap`: https://pypi.org/project/python-ldap/ License ======= .. include:: ../LICENSE ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/install.rst0000664000175000017500000000254000000000000017364 0ustar00jonjon00000000000000Installation ============ Install the package with pip: .. code-block:: sh $ pip install django-auth-ldap It requires `python-ldap`_ >= 3.0. You'll need the `OpenLDAP`_ libraries and headers available on your system. To use the auth backend in a Django project, add ``'django_auth_ldap.backend.LDAPBackend'`` to :setting:`AUTHENTICATION_BACKENDS`. Do not add anything to :setting:`INSTALLED_APPS`. .. code-block:: python AUTHENTICATION_BACKENDS = ["django_auth_ldap.backend.LDAPBackend"] :class:`~django_auth_ldap.backend.LDAPBackend` should work with custom user models, but it does assume that a database is present. .. note:: :class:`~django_auth_ldap.backend.LDAPBackend` does not inherit from :class:`~django.contrib.auth.backends.ModelBackend`. It is possible to use :class:`~django_auth_ldap.backend.LDAPBackend` exclusively by configuring it to draw group membership from the LDAP server. However, if you would like to assign permissions to individual users or add users to groups within Django, you'll need to have both backends installed: .. code-block:: python AUTHENTICATION_BACKENDS = [ "django_auth_ldap.backend.LDAPBackend", "django.contrib.auth.backends.ModelBackend", ] .. _`python-ldap`: https://pypi.org/project/python-ldap/ .. _`OpenLDAP`: https://www.openldap.org/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/logging.rst0000664000175000017500000000117700000000000017351 0ustar00jonjon00000000000000Logging ======= :class:`~django_auth_ldap.backend.LDAPBackend` uses the standard Python :mod:`logging` module to log debug and warning messages to the logger named ``'django_auth_ldap'``. If you need debug messages to help with configuration issues, you should add a handler to this logger. Using Django's :setting:`LOGGING` setting, you can add an entry to your config. .. code-block:: python LOGGING = { "version": 1, "disable_existing_loggers": False, "handlers": {"console": {"class": "logging.StreamHandler"}}, "loggers": {"django_auth_ldap": {"level": "DEBUG", "handlers": ["console"]}}, } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/multiconfig.rst0000664000175000017500000000332400000000000020237 0ustar00jonjon00000000000000Multiple LDAP Configs ===================== .. versionadded:: 1.1 You've probably noticed that all of the settings for this backend have the prefix AUTH_LDAP\_. This is the default, but it can be customized by subclasses of :class:`~django_auth_ldap.backend.LDAPBackend`. The main reason you would want to do this is to create two backend subclasses that reference different collections of settings and thus operate independently. For example, you might have two separate LDAP servers that you want to authenticate against. A short example should demonstrate this: .. code-block:: python # mypackage.ldap from django_auth_ldap.backend import LDAPBackend class LDAPBackend1(LDAPBackend): settings_prefix = "AUTH_LDAP_1_" class LDAPBackend2(LDAPBackend): settings_prefix = "AUTH_LDAP_2_" .. code-block:: python # settings.py AUTH_LDAP_1_SERVER_URI = "ldap://ldap1.example.com" AUTH_LDAP_1_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com" AUTH_LDAP_2_SERVER_URI = "ldap://ldap2.example.com" AUTH_LDAP_2_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com" AUTHENTICATION_BACKENDS = ("mypackage.ldap.LDAPBackend1", "mypackage.ldap.LDAPBackend2") All of the usual rules apply: Django will attempt to authenticate a user with each backend in turn until one of them succeeds. When a particular backend successfully authenticates a user, that user will be linked to the backend for the duration of their session. .. note:: Due to its global nature, :setting:`AUTH_LDAP_GLOBAL_OPTIONS` ignores the settings prefix. Regardless of how many backends are installed, this setting is referenced once by its default name at the time we load the ldap module. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/performance.rst0000664000175000017500000000322700000000000020222 0ustar00jonjon00000000000000Performance =========== :class:`~django_auth_ldap.backend.LDAPBackend` is carefully designed not to require a connection to the LDAP service for every request. Of course, this depends heavily on how it is configured. If LDAP traffic or latency is a concern for your deployment, this section has a few tips on minimizing it, in decreasing order of impact. #. **Cache groups**. If :setting:`AUTH_LDAP_FIND_GROUP_PERMS` is ``True``, the default behavior is to reload a user's group memberships on every request. This is the safest behavior, as any membership change takes effect immediately, but it is expensive. If possible, set :setting:`AUTH_LDAP_CACHE_TIMEOUT` to remove most of this traffic. #. **Don't access user.ldap_user.***. Except for ``ldap_user.dn``, these properties are only cached on a per-request basis. If you can propagate LDAP attributes to a :class:`~django.contrib.auth.models.User`, they will only be updated at login. ``user.ldap_user.attrs`` triggers an LDAP connection for every request in which it's accessed. #. **Use simpler group types**. Some grouping mechanisms are more expensive than others. This will often be outside your control, but it's important to note that the extra functionality of more complex group types like :class:`~django_auth_ldap.config.NestedGroupOfNamesType` is not free and will generally require a greater number and complexity of LDAP queries. #. **Use direct binding**. Binding with :setting:`AUTH_LDAP_USER_DN_TEMPLATE` is a little bit more efficient than relying on :setting:`AUTH_LDAP_USER_SEARCH`. Specifically, it saves two LDAP operations (one bind and one search) per login. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/permissions.rst0000664000175000017500000000754400000000000020302 0ustar00jonjon00000000000000Permissions =========== Groups are useful for more than just populating the user's ``is_*`` fields. :class:`~django_auth_ldap.backend.LDAPBackend` would not be complete without some way to turn a user's LDAP group memberships into Django model permissions. In fact, there are two ways to do this. Ultimately, both mechanisms need some way to map LDAP groups to Django groups. Implementations of :class:`~django_auth_ldap.config.LDAPGroupType` will have an algorithm for deriving the Django group name from the LDAP group. Clients that need to modify this behavior can subclass the :class:`~django_auth_ldap.config.LDAPGroupType` class. All of the built-in implementations take a ``name_attr`` argument to ``__init__``, which specifies the LDAP attribute from which to take the Django group name. By default, the ``cn`` attribute is used. Using Groups Directly --------------------- The least invasive way to map group permissions is to set :setting:`AUTH_LDAP_FIND_GROUP_PERMS` to ``True``. :class:`~django_auth_ldap.backend.LDAPBackend` will then find all of the LDAP groups that a user belongs to, map them to Django groups, and load the permissions for those groups. You will need to create the Django groups and associate permissions yourself, generally through the admin interface. To minimize traffic to the LDAP server, :class:`~django_auth_ldap.backend.LDAPBackend` can make use of Django's cache framework to keep a copy of a user's LDAP group memberships. To enable this feature, set :setting:`AUTH_LDAP_CACHE_TIMEOUT`, which determines the timeout of cache entries in seconds. .. code-block:: python AUTH_LDAP_CACHE_TIMEOUT = 3600 Group Mirroring --------------- The second way to turn LDAP group memberships into permissions is to mirror the groups themselves. This approach has some important disadvantages and should be avoided if possible. For one thing, membership will only be updated when the user authenticates, which may be especially inappropriate for sites with long session timeouts. If :setting:`AUTH_LDAP_MIRROR_GROUPS` is ``True``, then every time a user logs in, :class:`~django_auth_ldap.backend.LDAPBackend` will update the database with the user's LDAP groups. Any group that doesn't exist will be created and the user's Django group membership will be updated to exactly match their LDAP group membership. If the LDAP server has nested groups, the Django database will end up with a flattened representation. For group mirroring to have any effect, you of course need :class:`~django.contrib.auth.backends.ModelBackend` installed as an authentication backend. By default, we assume that LDAP is the sole authority on group membership; if you remove a user from a group in LDAP, they will be removed from the corresponding Django group the next time they log in. It is also possible to have django-auth-ldap ignore some Django groups, presumably because they are managed manually or through some other mechanism. If :setting:`AUTH_LDAP_MIRROR_GROUPS` is a list of group names, we will manage these groups and no others. If :setting:`AUTH_LDAP_MIRROR_GROUPS_EXCEPT` is a list of group names, we will manage all groups except those named; :setting:`AUTH_LDAP_MIRROR_GROUPS` is ignored in this case. Non-LDAP Users -------------- :class:`~django_auth_ldap.backend.LDAPBackend` has one more feature pertaining to permissions, which is the ability to handle authorization for users that it did not authenticate. For example, you might be using :class:`~django.contrib.auth.backends.RemoteUserBackend` to map externally authenticated users to Django users. By setting :setting:`AUTH_LDAP_AUTHORIZE_ALL_USERS`, :class:`~django_auth_ldap.backend.LDAPBackend` will map these users to LDAP users in the normal way in order to provide authorization information. Note that this does *not* work with :setting:`AUTH_LDAP_MIRROR_GROUPS`; group mirroring is a feature of authentication, not authorization. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1586011300.0 django-auth-ldap-2.2.0/docs/reference.rst0000664000175000017500000005013500000000000017657 0ustar00jonjon00000000000000Reference ========= Settings -------- .. setting:: AUTH_LDAP_ALWAYS_UPDATE_USER AUTH_LDAP_ALWAYS_UPDATE_USER ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``True`` If ``True``, the fields of a :class:`~django.contrib.auth.models.User` object will be updated with the latest values from the LDAP directory every time the user logs in. Otherwise the :class:`~django.contrib.auth.models.User` object will only be populated when it is automatically created. .. setting:: AUTH_LDAP_AUTHORIZE_ALL_USERS AUTH_LDAP_AUTHORIZE_ALL_USERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``False`` If ``True``, :class:`~django_auth_ldap.backend.LDAPBackend` will be able furnish permissions for any Django user, regardless of which backend authenticated it. .. setting:: AUTH_LDAP_BIND_AS_AUTHENTICATING_USER AUTH_LDAP_BIND_AS_AUTHENTICATING_USER ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``False`` If ``True``, authentication will leave the LDAP connection bound as the authenticating user, rather than forcing it to re-bind with the default credentials after authentication succeeds. This may be desirable if you do not have global credentials that are able to access the user's attributes. django-auth-ldap never stores the user's password, so this only applies to requests where the user is authenticated. Thus, the downside to this setting is that LDAP results may vary based on whether the user was authenticated earlier in the Django view, which could be surprising to code not directly concerned with authentication. .. setting:: AUTH_LDAP_BIND_DN AUTH_LDAP_BIND_DN ~~~~~~~~~~~~~~~~~ Default: ``''`` (Empty string) The distinguished name to use when binding to the LDAP server (with :setting:`AUTH_LDAP_BIND_PASSWORD`). Use the empty string (the default) for an anonymous bind. To authenticate a user, we will bind with that user's DN and password, but for all other LDAP operations, we will be bound as the DN in this setting. For example, if :setting:`AUTH_LDAP_USER_DN_TEMPLATE` is not set, we'll use this to search for the user. If :setting:`AUTH_LDAP_FIND_GROUP_PERMS` is ``True``, we'll also use it to determine group membership. .. setting:: AUTH_LDAP_BIND_PASSWORD AUTH_LDAP_BIND_PASSWORD ~~~~~~~~~~~~~~~~~~~~~~~ Default: ``''`` (Empty string) The password to use with :setting:`AUTH_LDAP_BIND_DN`. .. setting:: AUTH_LDAP_CACHE_TIMEOUT AUTH_LDAP_CACHE_TIMEOUT ~~~~~~~~~~~~~~~~~~~~~~~ Default: ``0`` The value determines the amount of time, in seconds, a user's group memberships and distinguished name are cached. The value ``0``, the default, disables caching entirely. .. versionchanged:: 1.6.0 Previously caching was controlled by the settings `AUTH_LDAP_CACHE_GROUPS` and `AUTH_LDAP_GROUP_CACHE_TIMEOUT`. If `AUTH_LDAP_CACHE_GROUPS` is set, the `AUTH_LDAP_CACHE_TIMEOUT` value is derievd from these deprecated settings. .. setting:: AUTH_LDAP_CONNECTION_OPTIONS AUTH_LDAP_CONNECTION_OPTIONS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``{}`` A dictionary of options to pass to each connection to the LDAP server via :meth:`LDAPObject.set_option() `. Keys are :ref:`ldap.OPT_* ` constants. .. setting:: AUTH_LDAP_DENY_GROUP AUTH_LDAP_DENY_GROUP ~~~~~~~~~~~~~~~~~~~~ Default: ``None`` The distinguished name of a group; authentication will fail for any user that belongs to this group. .. setting:: AUTH_LDAP_FIND_GROUP_PERMS AUTH_LDAP_FIND_GROUP_PERMS ~~~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``False`` If ``True``, :class:`~django_auth_ldap.backend.LDAPBackend` will furnish group permissions based on the LDAP groups the authenticated user belongs to. :setting:`AUTH_LDAP_GROUP_SEARCH` and :setting:`AUTH_LDAP_GROUP_TYPE` must also be set. .. setting:: AUTH_LDAP_GLOBAL_OPTIONS AUTH_LDAP_GLOBAL_OPTIONS ~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``{}`` A dictionary of options to pass to :func:`ldap.set_option`. Keys are :ref:`ldap.OPT_* ` constants. .. note:: Due to its global nature, this setting ignores the :doc:`settings prefix `. Regardless of how many backends are installed, this setting is referenced once by its default name at the time we load the ldap module. .. setting:: AUTH_LDAP_GROUP_SEARCH AUTH_LDAP_GROUP_SEARCH ~~~~~~~~~~~~~~~~~~~~~~ Default: ``None`` An :class:`~django_auth_ldap.config.LDAPSearch` object that finds all LDAP groups that users might belong to. If your configuration makes any references to LDAP groups, this and :setting:`AUTH_LDAP_GROUP_TYPE` must be set. .. setting:: AUTH_LDAP_GROUP_TYPE AUTH_LDAP_GROUP_TYPE ~~~~~~~~~~~~~~~~~~~~ Default: ``None`` An :class:`~django_auth_ldap.config.LDAPGroupType` instance describing the type of group returned by :setting:`AUTH_LDAP_GROUP_SEARCH`. .. setting:: AUTH_LDAP_MIRROR_GROUPS AUTH_LDAP_MIRROR_GROUPS ~~~~~~~~~~~~~~~~~~~~~~~ Default: ``None`` If ``True``, :class:`~django_auth_ldap.backend.LDAPBackend` will mirror a user's LDAP group membership in the Django database. Any time a user authenticates, we will create all of their LDAP groups as Django groups and update their Django group membership to exactly match their LDAP group membership. If the LDAP server has nested groups, the Django database will end up with a flattened representation. This can also be a list or other collection of group names, in which case we'll only mirror those groups and leave the rest alone. This is ignored if :setting:`AUTH_LDAP_MIRROR_GROUPS_EXCEPT` is set. .. setting:: AUTH_LDAP_MIRROR_GROUPS_EXCEPT AUTH_LDAP_MIRROR_GROUPS_EXCEPT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``None`` If this is not ``None``, it must be a list or other collection of group names. This will enable group mirroring, except that we'll never change the membership of the indicated groups. :setting:`AUTH_LDAP_MIRROR_GROUPS` is ignored in this case. .. setting:: AUTH_LDAP_PERMIT_EMPTY_PASSWORD AUTH_LDAP_PERMIT_EMPTY_PASSWORD ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``False`` If ``False`` (the default), authentication with an empty password will fail immediately, without any LDAP communication. This is a secure default, as some LDAP servers are configured to allow binds to succeed with no password, perhaps at a reduced level of access. If you need to make use of this LDAP feature, you can change this setting to ``True``. .. setting:: AUTH_LDAP_REQUIRE_GROUP AUTH_LDAP_REQUIRE_GROUP ~~~~~~~~~~~~~~~~~~~~~~~ Default: ``None`` The distinguished name of a group; authentication will fail for any user that does not belong to this group. This can also be an :class:`~django_auth_ldap.config.LDAPGroupQuery` instance. .. setting:: AUTH_LDAP_NO_NEW_USERS AUTH_LDAP_NO_NEW_USERS ~~~~~~~~~~~~~~~~~~~~~~~ Default: ``False`` Prevent the creation of new users during authentication. Any users not already in the Django user database will not be able to login. .. setting:: AUTH_LDAP_SERVER_URI AUTH_LDAP_SERVER_URI ~~~~~~~~~~~~~~~~~~~~ Default: ``'ldap://localhost'`` The URI of the LDAP server. This can be any URI that is supported by your underlying LDAP libraries. Can also be a callable that returns the URI. The callable is passed a single positional argument: ``request``. .. versionchanged:: 1.7.0 When ``AUTH_LDAP_SERVER_URI`` is set to a callable, it is now passed a positional ``request`` argument. Support for no arguments will continue for backwards compatibility but will be removed in a future version. .. setting:: AUTH_LDAP_START_TLS AUTH_LDAP_START_TLS ~~~~~~~~~~~~~~~~~~~ Default: ``False`` If ``True``, each connection to the LDAP server will call :meth:`~ldap.LDAPObject.start_tls_s` to enable TLS encryption over the standard LDAP port. There are a number of configuration options that can be given to :setting:`AUTH_LDAP_GLOBAL_OPTIONS` that affect the TLS connection. For example, :data:`ldap.OPT_X_TLS_REQUIRE_CERT` can be set to :data:`ldap.OPT_X_TLS_NEVER` to disable certificate verification, perhaps to allow self-signed certificates. .. setting:: AUTH_LDAP_USER_QUERY_FIELD AUTH_LDAP_USER_QUERY_FIELD ~~~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``None`` The field on the user model used to query the authenticating user in the database. If unset, uses the value of ``USERNAME_FIELD`` of the model class. When set, the value used to query is obtained through the :setting:`AUTH_LDAP_USER_ATTR_MAP`. For example, setting :setting:`AUTH_LDAP_USER_QUERY_FIELD` to ``username`` and adding ``"username": "sAMAccountName",`` to :setting:`AUTH_LDAP_USER_ATTR_MAP` will cause django to query local database using ``username`` column and LDAP using ``sAMAccountName`` attribute. .. setting:: AUTH_LDAP_USER_ATTRLIST AUTH_LDAP_USER_ATTRLIST ~~~~~~~~~~~~~~~~~~~~~~~ Default: ``None`` A list of attribute names to load for the authenticated user. Normally, you can ignore this and the LDAP server will send back all of the attributes of the directory entry. One reason you might need to override this is to get operational attributes, which are not normally included: .. code-block:: python AUTH_LDAP_USER_ATTRLIST = ["*", "+"] .. setting:: AUTH_LDAP_USER_ATTR_MAP AUTH_LDAP_USER_ATTR_MAP ~~~~~~~~~~~~~~~~~~~~~~~ Default: ``{}`` A mapping from :class:`~django.contrib.auth.models.User` field names to LDAP attribute names. A users's :class:`~django.contrib.auth.models.User` object will be populated from his LDAP attributes at login. .. setting:: AUTH_LDAP_USER_DN_TEMPLATE AUTH_LDAP_USER_DN_TEMPLATE ~~~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``None`` A string template that describes any user's distinguished name based on the username. This must contain the placeholder ``%(user)s``. .. setting:: AUTH_LDAP_USER_FLAGS_BY_GROUP AUTH_LDAP_USER_FLAGS_BY_GROUP ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``{}`` A mapping from boolean :class:`~django.contrib.auth.models.User` field names to distinguished names of LDAP groups. The corresponding field is set to ``True`` or ``False`` according to whether the user is a member of the group. Values may be strings for simple group membership tests or :class:`~django_auth_ldap.config.LDAPGroupQuery` instances for more complex cases. .. setting:: AUTH_LDAP_USER_SEARCH AUTH_LDAP_USER_SEARCH ~~~~~~~~~~~~~~~~~~~~~ Default: ``None`` An :class:`~django_auth_ldap.config.LDAPSearch` object that will locate a user in the directory. The filter parameter should contain the placeholder ``%(user)s`` for the username. It must return exactly one result for authentication to succeed. Module Properties ----------------- .. module:: django_auth_ldap .. data:: version The library's current version number as a 3-tuple. .. data:: version_string The library's current version number as a string. Configuration ------------- .. module:: django_auth_ldap.config .. class:: LDAPSearch .. method:: __init__(base_dn, scope, filterstr='(objectClass=*)') :param str base_dn: The distinguished name of the search base. :param int scope: One of ``ldap.SCOPE_*``. :param str filterstr: An optional filter string (e.g. '(objectClass=person)'). In order to be valid, ``filterstr`` must be enclosed in parentheses. .. class:: LDAPSearchUnion .. versionadded:: 1.1 .. method:: __init__(\*searches) :param searches: Zero or more LDAPSearch objects. The result of the overall search is the union (by DN) of the results of the underlying searches. The precedence of the underlying results and the ordering of the final results are both undefined. :type searches: :class:`LDAPSearch` .. class:: LDAPGroupType The base class for objects that will determine group membership for various LDAP grouping mechanisms. Implementations are provided for common group types or you can write your own. See the source code for subclassing notes. .. method:: __init__(name_attr='cn') By default, LDAP groups will be mapped to Django groups by taking the first value of the cn attribute. You can specify a different attribute with ``name_attr``. .. class:: PosixGroupType A concrete subclass of :class:`~django_auth_ldap.config.LDAPGroupType` that handles the ``posixGroup`` object class. This checks for both primary group and group membership. .. method:: __init__(name_attr='cn') .. class:: MemberDNGroupType A concrete subclass of :class:`~django_auth_ldap.config.LDAPGroupType` that handles grouping mechanisms wherein the group object contains a list of its member DNs. .. method:: __init__(member_attr, name_attr='cn') :param str member_attr: The attribute on the group object that contains a list of member DNs. 'member' and 'uniqueMember' are common examples. .. class:: NestedMemberDNGroupType Similar to :class:`~django_auth_ldap.config.MemberDNGroupType`, except this allows groups to contain other groups as members. Group hierarchies will be traversed to determine membership. .. method:: __init__(member_attr, name_attr='cn') As above. .. class:: GroupOfNamesType A concrete subclass of :class:`~django_auth_ldap.config.MemberDNGroupType` that handles the ``groupOfNames`` object class. Equivalent to ``MemberDNGroupType('member')``. .. method:: __init__(name_attr='cn') .. class:: NestedGroupOfNamesType A concrete subclass of :class:`~django_auth_ldap.config.NestedMemberDNGroupType` that handles the ``groupOfNames`` object class. Equivalent to ``NestedMemberDNGroupType('member')``. .. method:: __init__(name_attr='cn') .. class:: GroupOfUniqueNamesType A concrete subclass of :class:`~django_auth_ldap.config.MemberDNGroupType` that handles the ``groupOfUniqueNames`` object class. Equivalent to ``MemberDNGroupType('uniqueMember')``. .. method:: __init__(name_attr='cn') .. class:: NestedGroupOfUniqueNamesType A concrete subclass of :class:`~django_auth_ldap.config.NestedMemberDNGroupType` that handles the ``groupOfUniqueNames`` object class. Equivalent to ``NestedMemberDNGroupType('uniqueMember')``. .. method:: __init__(name_attr='cn') .. class:: ActiveDirectoryGroupType A concrete subclass of :class:`~django_auth_ldap.config.MemberDNGroupType` that handles Active Directory groups. Equivalent to ``MemberDNGroupType('member')``. .. method:: __init__(name_attr='cn') .. class:: NestedActiveDirectoryGroupType A concrete subclass of :class:`~django_auth_ldap.config.NestedMemberDNGroupType` that handles Active Directory groups. Equivalent to ``NestedMemberDNGroupType('member')``. .. method:: __init__(name_attr='cn') .. class:: OrganizationalRoleGroupType A concrete subclass of :class:`~django_auth_ldap.config.MemberDNGroupType` that handles the ``organizationalRole`` object class. Equivalent to ``MemberDNGroupType('roleOccupant')``. .. method:: __init__(name_attr='cn') .. class:: NestedOrganizationalRoleGroupType A concrete subclass of :class:`~django_auth_ldap.config.NestedMemberDNGroupType` that handles the ``organizationalRole`` object class. Equivalent to ``NestedMemberDNGroupType('roleOccupant')``. .. method:: __init__(name_attr='cn') .. class:: LDAPGroupQuery Represents a compound query for group membership. This can be used to construct an arbitrarily complex group membership query with AND, OR, and NOT logical operators. Construct primitive queries with a group DN as the only argument. These queries can then be combined with the ``&``, ``|``, and ``~`` operators. This is used by certain settings, including :setting:`AUTH_LDAP_REQUIRE_GROUP` and :setting:`AUTH_LDAP_USER_FLAGS_BY_GROUP`. An example is shown in :ref:`limiting-access`. .. method:: __init__(group_dn) :param str group_dn: The distinguished name of a group to test for membership. Backend ------- .. module:: django_auth_ldap.backend .. data:: populate_user This is a Django signal that is sent when clients should perform additional customization of a :class:`~django.contrib.auth.models.User` object. It is sent after a user has been authenticated and the backend has finished populating it, and just before it is saved. The client may take this opportunity to populate additional model fields, perhaps based on ``ldap_user.attrs``. This signal has two keyword arguments: ``user`` is the :class:`~django.contrib.auth.models.User` object and ``ldap_user`` is the same as ``user.ldap_user``. The sender is the :class:`~django_auth_ldap.backend.LDAPBackend` class. .. data:: ldap_error This is a Django signal that is sent when we receive an :exc:`ldap.LDAPError` exception. The signal has three keyword arguments: - ``context``: one of ``'authenticate'``, ``'get_group_permissions'``, or ``'populate_user'``, indicating which API was being called when the exception was caught. - ``user``: the Django user being processed (if available). - ``exception``: the :exc:`~ldap.LDAPError` object itself. The sender is the :class:`~django_auth_ldap.backend.LDAPBackend` class (or subclass). .. class:: LDAPBackend :class:`~django_auth_ldap.backend.LDAPBackend` has one method that may be called directly and several that may be overridden in subclasses. .. data:: settings_prefix A prefix for all of our Django settings. By default, this is ``'AUTH_LDAP_'``, but subclasses can override this. When different subclasses use different prefixes, they can both be installed and operate independently. .. data:: default_settings A dictionary of default settings. This is empty in :class:`~django_auth_ldap.backend.LDAPBackend`, but subclasses can populate this with values that will override the built-in defaults. Note that the keys should omit the ``'AUTH_LDAP_'`` prefix. .. method:: populate_user(username) Populates the Django user for the given LDAP username. This connects to the LDAP directory with the default credentials and attempts to populate the indicated Django user as if they had just logged in. :setting:`AUTH_LDAP_ALWAYS_UPDATE_USER` is ignored (assumed ``True``). .. method:: get_user_model(self) Returns the user model that :meth:`~django_auth_ldap.backend.LDAPBackend.get_or_build_user` will instantiate. By default, custom user models will be respected. Subclasses would most likely override this in order to substitute a :ref:`proxy model `. .. method:: authenticate_ldap_user(self, ldap_user, password) Given an LDAP user object and password, authenticates the user and returns a Django user object. See :ref:`customizing-authentication`. .. method:: get_or_build_user(self, username, ldap_user) Given a username and an LDAP user object, this must return a valid Django user model instance. The ``username`` argument has already been passed through :meth:`~django_auth_ldap.backend.LDAPBackend.ldap_to_django_username`. You can get information about the LDAP user via ``ldap_user.dn`` and ``ldap_user.attrs``. The return value must be an (instance, created) two-tuple. The instance does not need to be saved. The default implementation looks for the username with a case-insensitive query; if it's not found, the model returned by :meth:`~django_auth_ldap.backend.LDAPBackend.get_user_model` will be created with the lowercased username. New users will not be saved to the database until after the :data:`django_auth_ldap.backend.populate_user` signal has been sent. A subclass may override this to associate LDAP users to Django users any way it likes. .. method:: ldap_to_django_username(username) Returns a valid Django username based on the given LDAP username (which is what the user enters). By default, ``username`` is returned unchanged. This can be overridden by subclasses. .. method:: django_to_ldap_username(username) The inverse of :meth:`~django_auth_ldap.backend.LDAPBackend.ldap_to_django_username`. If this is not symmetrical to :meth:`~django_auth_ldap.backend.LDAPBackend.ldap_to_django_username`, the behavior is undefined. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/docs/users.rst0000664000175000017500000001464000000000000017063 0ustar00jonjon00000000000000User objects ============ Authenticating against an external source is swell, but Django's auth module is tightly bound to a user model. When a user logs in, we have to create a model object to represent them in the database. Because the LDAP search is case-insensitive, the default implementation also searches for existing Django users with an iexact query and new users are created with lowercase usernames. See :meth:`~django_auth_ldap.backend.LDAPBackend.get_or_build_user` if you'd like to override this behavior. See :meth:`~django_auth_ldap.backend.LDAPBackend.get_user_model` if you'd like to substitute a proxy model. By default, lookups on existing users are done using the user model's :attr:`~django.contrib.auth.models.CustomUser.USERNAME_FIELD`. To lookup by a different field, use :setting:`AUTH_LDAP_USER_QUERY_FIELD`. When set, the username field is ignored. When using the default for lookups, the only required field for a user is the username. The default :class:`~django.contrib.auth.models.User` model can be picky about the characters allowed in usernames, so :class:`~django_auth_ldap.backend.LDAPBackend` includes a pair of hooks, :meth:`~django_auth_ldap.backend.LDAPBackend.ldap_to_django_username` and :meth:`~django_auth_ldap.backend.LDAPBackend.django_to_ldap_username`, to translate between LDAP usernames and Django usernames. You may need this, for example, if your LDAP names have periods in them. You can subclass :class:`~django_auth_ldap.backend.LDAPBackend` to implement these hooks; by default the username is not modified. :class:`~django.contrib.auth.models.User` objects that are authenticated by :class:`~django_auth_ldap.backend.LDAPBackend` will have an ``ldap_username`` attribute with the original (LDAP) username. :attr:`~django.contrib.auth.models.User.username` (or :meth:`~django.contrib.auth.models.AbstractBaseUser.get_username`) will, of course, be the Django username. .. note:: Users created by :class:`~django_auth_ldap.backend.LDAPBackend` will have an unusable password set. This will only happen when the user is created, so if you set a valid password in Django, the user will be able to log in through :class:`~django.contrib.auth.backends.ModelBackend` (if configured) even if they are rejected by LDAP. This is not generally recommended, but could be useful as a fail-safe for selected users in case the LDAP server is unavailable. Populating Users ---------------- You can perform arbitrary population of your user models by adding listeners to the :mod:`Django signal `: :data:`django_auth_ldap.backend.populate_user`. This signal is sent after the user object has been constructed (but not necessarily saved) and any configured attribute mapping has been applied (see below). You can use this to propagate information from the LDAP directory to the user object any way you like. If you need the user object to exist in the database at this point, you can save it in your signal handler or override :meth:`~django_auth_ldap.backend.LDAPBackend.get_or_build_user`. In either case, the user instance will be saved automatically after the signal handlers are run. If you need an attribute that isn't included by default in the LDAP search results, see :setting:`AUTH_LDAP_USER_ATTRLIST`. Easy Attributes --------------- If you just want to copy a few attribute values directly from the user's LDAP directory entry to their Django user, the setting, :setting:`AUTH_LDAP_USER_ATTR_MAP`, makes it easy. This is a dictionary that maps user model keys, respectively, to (case-insensitive) LDAP attribute names: .. code-block:: python AUTH_LDAP_USER_ATTR_MAP = {"first_name": "givenName", "last_name": "sn"} Only string fields can be mapped to attributes. Boolean fields can be defined by group membership: .. code-block:: python AUTH_LDAP_USER_FLAGS_BY_GROUP = { "is_active": "cn=active,ou=groups,dc=example,dc=com", "is_staff": ( LDAPGroupQuery("cn=staff,ou=groups,dc=example,dc=com") | LDAPGroupQuery("cn=admin,ou=groups,dc=example,dc=com") ), "is_superuser": "cn=superuser,ou=groups,dc=example,dc=com", } Values in this dictionary may be simple DNs (as strings), lists or tuples of DNs, or :class:`~django_auth_ldap.config.LDAPGroupQuery` instances. Lists are converted to queries joined by ``|``. Remember that if these settings don't do quite what you want, you can always use the signals described in the previous section to implement your own logic. Updating Users -------------- By default, all mapped user fields will be updated each time the user logs in. To disable this, set :setting:`AUTH_LDAP_ALWAYS_UPDATE_USER` to ``False``. If you need to populate a user outside of the authentication process—for example, to create associated model objects before the user logs in for the first time—you can call :meth:`django_auth_ldap.backend.LDAPBackend.populate_user`. You'll need an instance of :class:`~django_auth_ldap.backend.LDAPBackend`, which you should feel free to create yourself. :meth:`~django_auth_ldap.backend.LDAPBackend.populate_user` returns the :class:`~django.contrib.auth.models.User` or `None` if the user could not be found in LDAP. .. code-block:: python from django_auth_ldap.backend import LDAPBackend user = LDAPBackend().populate_user("alice") if user is None: raise Exception("No user named alice") .. _ldap_user: Direct Attribute Access ----------------------- If you need to access multi-value attributes or there is some other reason that the above is inadequate, you can also access the user's raw LDAP attributes. ``user.ldap_user`` is an object with four public properties. The group properties are, of course, only valid if groups are configured. * ``dn``: The user's distinguished name. * ``attrs``: The user's LDAP attributes as a dictionary of lists of string values. The dictionaries are modified to use case-insensitive keys. * ``group_dns``: The set of groups that this user belongs to, as DNs. * ``group_names``: The set of groups that this user belongs to, as simple names. These are the names that will be used if :setting:`AUTH_LDAP_MIRROR_GROUPS` is used. Python-ldap returns all attribute values as utf8-encoded strings. For convenience, this module will try to decode all values into Unicode strings. Any string that can not be successfully decoded will be left as-is; this may apply to binary values such as Active Directory's objectSid. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1591141796.9024706 django-auth-ldap-2.2.0/setup.cfg0000664000175000017500000000312400000000000016054 0ustar00jonjon00000000000000[metadata] name = django-auth-ldap version = attr: django_auth_ldap.__version__ description = Django LDAP authentication backend. long_description = file: README.rst author = Peter Sagerson author_email = psagers@ignorare.net url = https://github.com/django-auth-ldap/django-auth-ldap license = BSD classifiers = Development Status :: 5 - Production/Stable Environment :: Web Environment Framework :: Django Framework :: Django :: 1.11 Framework :: Django :: 2.2 Framework :: Django :: 3.0 Intended Audience :: Developers Intended Audience :: System Administrators License :: OSI Approved :: BSD License Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Topic :: Internet :: WWW/HTTP Topic :: Software Development :: Libraries :: Python Modules Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP project_urls = Documentation = https://django-auth-ldap.readthedocs.io/ Source = https://github.com/django-auth-ldap/django-auth-ldap Tracker = https://github.com/django-auth-ldap/django-auth-ldap/issues [options] python_requires = >=3.5 packages = django_auth_ldap install_requires = Django>=1.11 python-ldap>=3.1 [flake8] ignore = E501 W503 [isort] combine_as_imports = True default_section = THIRDPARTY include_trailing_comma = True known_first_party = django_auth_ldap line_length = 88 multi_line_output = 3 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574651035.0 django-auth-ldap-2.2.0/setup.py0000664000175000017500000000004600000000000015745 0ustar00jonjon00000000000000from setuptools import setup setup() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1591141796.9014704 django-auth-ldap-2.2.0/tests/0000775000175000017500000000000000000000000015375 5ustar00jonjon00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/tests/__init__.py0000664000175000017500000000000000000000000017474 0ustar00jonjon00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/tests/models.py0000664000175000017500000000110500000000000017227 0ustar00jonjon00000000000000from django.contrib.auth.models import AbstractBaseUser from django.db import models class TestUser(AbstractBaseUser): identifier = models.CharField(max_length=40, unique=True, db_index=True) uid_number = models.IntegerField() USERNAME_FIELD = "identifier" def get_full_name(self): return self.identifier def get_short_name(self): return self.identifier def get_first_name(self): return "Alice" def set_first_name(self, value): raise Exception("Oops...") first_name = property(get_first_name, set_first_name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/tests/settings.py0000664000175000017500000000041500000000000017607 0ustar00jonjon00000000000000SECRET_KEY = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" INSTALLED_APPS = ("django.contrib.auth", "django.contrib.contenttypes", "tests") DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3"}} AUTHENTICATION_BACKENDS = ["django_auth_ldap.backend.LDAPBackend"] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572220524.0 django-auth-ldap-2.2.0/tests/tests.ldif0000664000175000017500000000770500000000000017410 0ustar00jonjon00000000000000dn: o=test objectClass: organization o: test dn: ou=people,o=test objectClass: organizationalUnit ou: people dn: ou=groups,o=test objectClass: organizationalUnit ou: groups dn: ou=moregroups,o=test objectClass: organizationalUnit ou: moregroups dn: ou=mirror_groups,o=test objectClass: organizationalUnit ou: mirror_groups dn: uid=alice,ou=people,o=test objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson objectClass: posixAccount cn: alice uid: alice userPassword: password uidNumber: 1000 gidNumber: 1000 givenName: Alice sn: Adams homeDirectory: /home/alice dn: uid=bob,ou=people,o=test objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson objectClass: posixAccount cn: bob uid: bob userPassword: password uidNumber: 1001 gidNumber: 50 givenName: Robert sn: Barker homeDirectory: /home/bob dn: uid=dreßler,ou=people,o=test objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson objectClass: posixAccount cn: dreßler uid: dreßler userPassword: password uidNumber: 1002 gidNumber: 50 givenName: Wolfgang sn: Dreßler homeDirectory: /home/dressler dn: uid=nobody,ou=people,o=test objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson objectClass: posixAccount cn: nobody uid: nobody userPassword: password uidNumber: 1003 gidNumber: 50 sn: nobody homeDirectory: /home/nobody dn: uid=nonposix,ou=people,o=test objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: nonposix uid: nonposix userPassword: password sn: nonposix # posixGroup objects dn: cn=active_px,ou=groups,o=test objectClass: posixGroup cn: active_px gidNumber: 1000 memberUid: nonposix dn: cn=staff_px,ou=groups,o=test objectClass: posixGroup cn: staff_px gidNumber: 1001 memberUid: alice memberUid: nonposix dn: cn=superuser_px,ou=groups,o=test objectClass: posixGroup cn: superuser_px gidNumber: 1002 memberUid: alice memberUid: nonposix # groupOfNames groups dn: cn=empty_gon,ou=groups,o=test cn: empty_gon objectClass: groupOfNames member: dn: cn=active_gon,ou=groups,o=test cn: active_gon objectClass: groupOfNames member: uid=alice,ou=people,o=test dn: cn=staff_gon,ou=groups,o=test cn: staff_gon objectClass: groupOfNames member: uid=alice,ou=people,o=test dn: cn=superuser_gon,ou=groups,o=test cn: superuser_gon objectClass: groupOfNames member: uid=alice,ou=people,o=test dn: cn=other_gon,ou=moregroups,o=test cn: other_gon objectClass: groupOfNames member: uid=bob,ou=people,o=test # groupOfNames objects for LDAPGroupQuery testing dn: ou=query_groups,o=test objectClass: organizationalUnit ou: query_groups dn: cn=alice_gon,ou=query_groups,o=test cn: alice_gon objectClass: groupOfNames member: uid=alice,ou=people,o=test dn: cn=mutual_gon,ou=query_groups,o=test cn: mutual_gon objectClass: groupOfNames member: uid=alice,ou=people,o=test member: uid=bob,ou=people,o=test dn: cn=bob_gon,ou=query_groups,o=test cn: bob_gon objectClass: groupOfNames member: uid=bob,ou=people,o=test dn: cn=dreßler_gon,ou=query_groups,o=test cn: dreßler_gon objectClass: groupOfNames member: uid=dreßler,ou=people,o=test # groupOfNames objects for selective group mirroring. dn: cn=mirror1,ou=mirror_groups,o=test cn: mirror1 objectClass: groupOfNames member: uid=alice,ou=people,o=test dn: cn=mirror2,ou=mirror_groups,o=test cn: mirror2 objectClass: groupOfNames member: dn: cn=mirror3,ou=mirror_groups,o=test cn: mirror3 objectClass: groupOfNames member: uid=alice,ou=people,o=test dn: cn=mirror4,ou=mirror_groups,o=test cn: mirror4 objectClass: groupOfNames member: # Nested groups with a circular reference dn: cn=parent_gon,ou=groups,o=test cn: parent_gon objectClass: groupOfNames member: cn=nested_gon,ou=groups,o=test dn: CN=nested_gon,ou=groups,o=test cn: nested_gon objectClass: groupOfNames member: uid=alice,ou=people,o=test member: cn=circular_gon,ou=groups,o=test dn: cn=circular_gon,ou=groups,o=test cn: circular_gon objectClass: groupOfNames member: cn=parent_gon,ou=groups,o=test ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575432371.0 django-auth-ldap-2.2.0/tests/tests.py0000664000175000017500000016270300000000000017122 0ustar00jonjon00000000000000# Copyright (c) 2009, Peter Sagerson # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # - Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # - 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 HOLDER 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. import contextlib import functools import logging import os import pickle import warnings from copy import deepcopy import ldap import mock import slapdtest from django.contrib.auth import authenticate, get_backends from django.contrib.auth.models import Group, Permission, User from django.core.cache import cache from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings from django_auth_ldap.backend import LDAPBackend, ldap_error, populate_user from django_auth_ldap.config import ( GroupOfNamesType, LDAPGroupQuery, LDAPSearch, LDAPSearchUnion, MemberDNGroupType, NestedMemberDNGroupType, PosixGroupType, ) from .models import TestUser def get_backend(): backends = get_backends() return backends[0] def _override_settings(**settings): def decorator(func): @functools.wraps(func) def wrapped_test(self, *args, **kwargs): cm = override_settings(**settings) cm.enable() self.addCleanup(cm.disable) return func(self, *args, **kwargs) return wrapped_test return decorator def spy_ldap(name): """ Patch the python-ldap method. The patched method records all calls and passes execution to the original method. """ ldap_method = getattr(ldap.ldapobject.SimpleLDAPObject, name) ldap_mock = mock.MagicMock() @functools.wraps(ldap_method) def wrapped_ldap_method(self, *args, **kwargs): ldap_mock(*args, **kwargs) return ldap_method(self, *args, **kwargs) def decorator(test): @functools.wraps(test) def wrapped_test(self, *args, **kwargs): with mock.patch.object( ldap.ldapobject.SimpleLDAPObject, name, wrapped_ldap_method ): return test(self, ldap_mock, *args, **kwargs) return wrapped_test return decorator @contextlib.contextmanager def catch_signal(signal): """Catch Django signal and return the mocked call.""" handler = mock.Mock() signal.connect(handler) try: yield handler finally: signal.disconnect(handler) class LDAPTest(TestCase): @classmethod def configure_logger(cls): logger = logging.getLogger("django_auth_ldap") formatter = logging.Formatter("LDAP auth - %(levelname)s - %(message)s") handler = logging.StreamHandler() handler.setLevel(logging.DEBUG) handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.CRITICAL) @classmethod def setUpClass(cls): super().setUpClass() cls.configure_logger() here = os.path.dirname(__file__) cls.server = slapdtest.SlapdObject() cls.server.suffix = "o=test" cls.server.openldap_schema_files = [ "core.schema", "cosine.schema", "inetorgperson.schema", "nis.schema", ] cls.server.start() with open(os.path.join(here, "tests.ldif")) as fp: ldif = fp.read() cls.server.ldapadd(ldif) @classmethod def tearDownClass(cls): cls.server.stop() super().tearDownClass() def setUp(self): super().setUp() cache.clear() def test_options(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", CONNECTION_OPTIONS={ldap.OPT_REFERRALS: 0}, ) user = authenticate(username="alice", password="password") self.assertEqual(user.ldap_user.connection.get_option(ldap.OPT_REFERRALS), 0) def test_callable_server_uri(self): request = RequestFactory().get("/") cb_mock = mock.Mock(return_value=self.server.ldap_uri) self._init_settings( SERVER_URI=lambda request: cb_mock(request), USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", ) user_count = User.objects.count() user = authenticate(request=request, username="alice", password="password") self.assertIs(user.has_usable_password(), False) self.assertEqual(user.username, "alice") self.assertEqual(User.objects.count(), user_count + 1) cb_mock.assert_called_with(request) def test_deprecated_callable_server_uri(self): self._init_settings( SERVER_URI=lambda: self.server.ldap_uri, USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", ) user_count = User.objects.count() with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") user = authenticate(username="alice", password="password") self.assertIs(user.has_usable_password(), False) self.assertEqual(user.username, "alice") self.assertEqual(User.objects.count(), user_count + 1) self.assertEqual(len(w), 1) self.assertEqual(w[0].category, DeprecationWarning) self.assertEqual( str(w[0].message), "Update AUTH_LDAP_SERVER_URI callable tests.tests. to " "accept a positional `request` argument. Support for callables " "accepting no arguments will be removed in a future version.", ) def test_simple_bind(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") user_count = User.objects.count() user = authenticate(username="alice", password="password") self.assertIs(user.has_usable_password(), False) self.assertEqual(user.username, "alice") self.assertEqual(User.objects.count(), user_count + 1) def test_default_settings(self): class MyBackend(LDAPBackend): default_settings = { "SERVER_URI": self.server.ldap_uri, "USER_DN_TEMPLATE": "uid=%(user)s,ou=people,o=test", } backend = MyBackend() user_count = User.objects.count() user = backend.authenticate(None, username="alice", password="password") self.assertIs(user.has_usable_password(), False) self.assertEqual(user.username, "alice") self.assertEqual(User.objects.count(), user_count + 1) @_override_settings( AUTHENTICATION_BACKENDS=[ "django_auth_ldap.backend.LDAPBackend", "django.contrib.auth.backends.ModelBackend", ] ) def test_login_with_multiple_auth_backends(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ) ) user = authenticate(username="alice", password="password") self.assertIsNotNone(user) @_override_settings( AUTHENTICATION_BACKENDS=[ "django_auth_ldap.backend.LDAPBackend", "django.contrib.auth.backends.ModelBackend", ] ) def test_bad_login_with_multiple_auth_backends(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ) ) user = authenticate(username="invalid", password="i_do_not_exist") self.assertIsNone(user) def test_username_none(self): self._init_settings() user = authenticate(username=None, password="password") self.assertIsNone(user) @spy_ldap("simple_bind_s") def test_simple_bind_escaped(self, mock): """ Bind with a username that requires escaping. """ self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") user = authenticate(username="alice,1", password="password") self.assertIsNone(user) mock.assert_called_once_with("uid=alice\\,1,ou=people,o=test", "password") def test_new_user_lowercase(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") user_count = User.objects.count() user = authenticate(username="Alice", password="password") self.assertIs(user.has_usable_password(), False) self.assertEqual(user.username, "alice") self.assertEqual(User.objects.count(), user_count + 1) def test_deepcopy(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") user = authenticate(username="Alice", password="password") user = deepcopy(user) @_override_settings(AUTH_USER_MODEL="tests.TestUser") def test_auth_custom_user(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", USER_ATTR_MAP={"uid_number": "uidNumber"}, ) user = authenticate(username="Alice", password="password") self.assertIsInstance(user, TestUser) @_override_settings(AUTH_USER_MODEL="tests.TestUser") def test_get_custom_user(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", USER_ATTR_MAP={"uid_number": "uidNumber"}, ) backend = get_backend() user = authenticate(username="Alice", password="password") user = backend.get_user(user.id) self.assertIsInstance(user, TestUser) @_override_settings(AUTH_USER_MODEL="tests.TestUser") def test_get_custom_field(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", USER_ATTR_MAP={"uid_number": "uidNumber"}, USER_QUERY_FIELD="uid_number", ) alice = TestUser.objects.create(identifier="abcdef", uid_number=1000) user = authenticate(username="Alice", password="password") self.assertIsInstance(user, TestUser) self.assertEqual(user.pk, alice.pk) def test_new_user_whitespace(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") user_count = User.objects.count() user = authenticate(username=" alice", password="password") user = authenticate(username="alice ", password="password") self.assertIs(user.has_usable_password(), False) self.assertEqual(user.username, "alice") self.assertEqual(User.objects.count(), user_count + 1) def test_simple_bind_bad_user(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") user_count = User.objects.count() user = authenticate(username="evil_alice", password="password") self.assertIsNone(user) self.assertEqual(User.objects.count(), user_count) def test_simple_bind_bad_password(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") user_count = User.objects.count() user = authenticate(username="alice", password="bogus") self.assertIsNone(user) self.assertEqual(User.objects.count(), user_count) def test_existing_user(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") User.objects.create(username="alice") user_count = User.objects.count() user = authenticate(username="alice", password="password") # Make sure we only created one user self.assertIsNotNone(user) self.assertEqual(User.objects.count(), user_count) def test_existing_user_insensitive(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ) ) User.objects.create(username="alice") user = authenticate(username="Alice", password="password") self.assertIsNotNone(user) self.assertEqual(user.username, "alice") self.assertEqual(User.objects.count(), 1) def test_convert_username(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") class MyBackend(LDAPBackend): def ldap_to_django_username(self, username): return "ldap_%s" % username def django_to_ldap_username(self, username): return username[5:] backend = MyBackend() user_count = User.objects.count() user1 = backend.authenticate(None, username="alice", password="password") user2 = backend.get_user(user1.pk) self.assertEqual(User.objects.count(), user_count + 1) self.assertEqual(user1.username, "ldap_alice") self.assertEqual(user1.ldap_user._username, "alice") self.assertEqual(user1.ldap_username, "alice") self.assertEqual(user2.username, "ldap_alice") self.assertEqual(user2.ldap_user._username, "alice") self.assertEqual(user2.ldap_username, "alice") def test_search_bind(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ) ) user_count = User.objects.count() user = authenticate(username="alice", password="password") self.assertIsNotNone(user) self.assertEqual(User.objects.count(), user_count + 1) @spy_ldap("search_s") def test_search_bind_escaped(self, mock): """ Search for a username that requires escaping. """ self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ) ) user = authenticate(username="alice*", password="password") self.assertIsNone(user) mock.assert_called_once_with( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=alice\\2a)", None ) def test_search_bind_no_user(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uidNumber=%(user)s)" ) ) user = authenticate(username="alice", password="password") self.assertIsNone(user) def test_search_bind_multiple_users(self): self._init_settings( USER_SEARCH=LDAPSearch("ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=*)") ) user = authenticate(username="alice", password="password") self.assertIsNone(user) def test_search_bind_bad_password(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ) ) user = authenticate(username="alice", password="bogus") self.assertIsNone(user) def test_search_bind_with_credentials(self): self._init_settings( BIND_DN="uid=bob,ou=people,o=test", BIND_PASSWORD="password", USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ), ) user = authenticate(username="alice", password="password") self.assertIsNotNone(user) self.assertIsNotNone(user.ldap_user) self.assertEqual(user.ldap_user.dn, "uid=alice,ou=people,o=test") self.assertEqual( dict(user.ldap_user.attrs), { "objectClass": [ "person", "organizationalPerson", "inetOrgPerson", "posixAccount", ], "cn": ["alice"], "uid": ["alice"], "userPassword": ["password"], "uidNumber": ["1000"], "gidNumber": ["1000"], "givenName": ["Alice"], "sn": ["Adams"], "homeDirectory": ["/home/alice"], }, ) def test_search_bind_with_bad_credentials(self): self._init_settings( BIND_DN="uid=bob,ou=people,o=test", BIND_PASSWORD="bogus", USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ), ) user = authenticate(username="alice", password="password") self.assertIsNone(user) def test_unicode_user(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", USER_ATTR_MAP={"first_name": "givenName", "last_name": "sn"}, ) user = authenticate(username="dreßler", password="password") self.assertIsNotNone(user) self.assertEqual(user.username, "dreßler") self.assertEqual(user.last_name, "Dreßler") def test_cidict(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") user = authenticate(username="alice", password="password") self.assertIsInstance(user.ldap_user.attrs, ldap.cidict.cidict) def test_populate_user(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", USER_ATTR_MAP={"first_name": "givenName", "last_name": "sn"}, ) user = authenticate(username="alice", password="password") self.assertEqual(user.username, "alice") self.assertEqual(user.first_name, "Alice") self.assertEqual(user.last_name, "Adams") def test_populate_user_with_missing_attribute(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", USER_ATTR_MAP={ "first_name": "givenName", "last_name": "sn", "email": "mail", }, ) user = authenticate(username="alice", password="password") self.assertEqual(user.username, "alice") self.assertEqual(user.first_name, "Alice") self.assertEqual(user.last_name, "Adams") self.assertEqual(user.email, "") @mock.patch.object(LDAPSearch, "execute", return_value=None) def test_populate_user_with_bad_search(self, mock_execute): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", USER_ATTR_MAP={"first_name": "givenName", "last_name": "sn"}, ) user = authenticate(username="alice", password="password") self.assertEqual(user.username, "alice") self.assertEqual(user.first_name, "") self.assertEqual(user.last_name, "") @_override_settings(AUTH_USER_MODEL="tests.TestUser") def test_authenticate_with_buggy_setter_raises_exception(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", USER_ATTR_MAP={"first_name": "givenName", "uid_number": "uidNumber"}, ) with self.assertRaisesMessage(Exception, "Oops..."): authenticate(username="alice", password="password") @_override_settings(AUTH_USER_MODEL="tests.TestUser") def test_populate_user_with_buggy_setter_raises_exception(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", USER_ATTR_MAP={"first_name": "givenName", "uid_number": "uidNumber"}, ) backend = get_backend() with self.assertRaisesMessage(Exception, "Oops..."): backend.populate_user("alice") @spy_ldap("search_s") def test_populate_with_attrlist(self, mock): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", USER_ATTR_MAP={"first_name": "givenName", "last_name": "sn"}, USER_ATTRLIST=["*", "+"], ) user = authenticate(username="alice", password="password") self.assertEqual(user.username, "alice") # lookup user attrs mock.assert_called_once_with( "uid=alice,ou=people,o=test", ldap.SCOPE_BASE, "(objectClass=*)", ["*", "+"] ) def test_bind_as_user(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", USER_ATTR_MAP={"first_name": "givenName", "last_name": "sn"}, BIND_AS_AUTHENTICATING_USER=True, ) user = authenticate(username="alice", password="password") self.assertEqual(user.username, "alice") self.assertEqual(user.first_name, "Alice") self.assertEqual(user.last_name, "Adams") def test_signal_populate_user(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") with catch_signal(populate_user) as handler: user = authenticate(username="alice", password="password") handler.assert_called_once_with( signal=populate_user, sender=LDAPBackend, user=user, ldap_user=user.ldap_user, ) def test_auth_signal_ldap_error(self): self._init_settings( BIND_DN="uid=bob,ou=people,o=test", BIND_PASSWORD="bogus", USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ), ) def handle_ldap_error(sender, **kwargs): raise kwargs["exception"] with catch_signal(ldap_error) as handler: handler.side_effect = handle_ldap_error with self.assertRaises(ldap.LDAPError): authenticate(username="alice", password="password") handler.assert_called_once() _args, kwargs = handler.call_args self.assertEqual(kwargs["context"], "authenticate") def test_populate_signal_ldap_error(self): self._init_settings( BIND_DN="uid=bob,ou=people,o=test", BIND_PASSWORD="bogus", USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ), ) backend = get_backend() user = backend.populate_user("alice") self.assertIsNone(user) def test_no_update_existing(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", USER_ATTR_MAP={"first_name": "givenName", "last_name": "sn"}, ALWAYS_UPDATE_USER=False, ) User.objects.create(username="alice", first_name="Alicia", last_name="Astro") alice = authenticate(username="alice", password="password") bob = authenticate(username="bob", password="password") self.assertEqual(alice.first_name, "Alicia") self.assertEqual(alice.last_name, "Astro") self.assertEqual(bob.first_name, "Robert") self.assertEqual(bob.last_name, "Barker") def test_require_group(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), REQUIRE_GROUP="cn=active_gon,ou=groups,o=test", ) alice = authenticate(username="alice", password="password") bob = authenticate(username="bob", password="password") self.assertIsNotNone(alice) self.assertIsNone(bob) def test_no_new_users(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", NO_NEW_USERS=True ) user = authenticate(username="alice", password="password") # No user was created. self.assertIsNone(user) self.assertEqual(0, User.objects.count()) def test_simple_group_query(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), ) alice = authenticate(username="alice", password="password") query = LDAPGroupQuery("cn=alice_gon,ou=query_groups,o=test") self.assertIs(query.resolve(alice.ldap_user), True) def test_group_query_utf8(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), ) user = authenticate(username="dreßler", password="password") query = LDAPGroupQuery("cn=dreßler_gon,ou=query_groups,o=test") self.assertIs(query.resolve(user.ldap_user), True) def test_negated_group_query(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), ) alice = authenticate(username="alice", password="password") query = ~LDAPGroupQuery("cn=alice_gon,ou=query_groups,o=test") self.assertIs(query.resolve(alice.ldap_user), False) def test_or_group_query(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), ) alice = authenticate(username="alice", password="password") bob = authenticate(username="bob", password="password") query = LDAPGroupQuery("cn=alice_gon,ou=query_groups,o=test") | LDAPGroupQuery( "cn=bob_gon,ou=query_groups,o=test" ) self.assertIs(query.resolve(alice.ldap_user), True) self.assertIs(query.resolve(bob.ldap_user), True) def test_and_group_query(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), ) alice = authenticate(username="alice", password="password") bob = authenticate(username="bob", password="password") query = LDAPGroupQuery("cn=alice_gon,ou=query_groups,o=test") & LDAPGroupQuery( "cn=mutual_gon,ou=query_groups,o=test" ) self.assertIs(query.resolve(alice.ldap_user), True) self.assertIs(query.resolve(bob.ldap_user), False) def test_nested_group_query(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), ) alice = authenticate(username="alice", password="password") bob = authenticate(username="bob", password="password") query = ( LDAPGroupQuery("cn=alice_gon,ou=query_groups,o=test") & LDAPGroupQuery("cn=mutual_gon,ou=query_groups,o=test") ) | LDAPGroupQuery("cn=bob_gon,ou=query_groups,o=test") self.assertIs(query.resolve(alice.ldap_user), True) self.assertIs(query.resolve(bob.ldap_user), True) def test_require_group_as_group_query(self): query = LDAPGroupQuery("cn=alice_gon,ou=query_groups,o=test") & LDAPGroupQuery( "cn=mutual_gon,ou=query_groups,o=test" ) self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), REQUIRE_GROUP=query, ) alice = authenticate(username="alice", password="password") bob = authenticate(username="bob", password="password") self.assertIsNotNone(alice) self.assertIsNone(bob) def test_group_union(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearchUnion( LDAPSearch( "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" ), LDAPSearch( "ou=moregroups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), REQUIRE_GROUP="cn=other_gon,ou=moregroups,o=test", ) alice = authenticate(username="alice", password="password") bob = authenticate(username="bob", password="password") self.assertIsNone(alice) self.assertIsNotNone(bob) self.assertEqual(bob.ldap_user.group_names, {"other_gon"}) def test_nested_group_union(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearchUnion( LDAPSearch( "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" ), LDAPSearch( "ou=moregroups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), ), GROUP_TYPE=NestedMemberDNGroupType(member_attr="member"), REQUIRE_GROUP="cn=other_gon,ou=moregroups,o=test", ) alice = authenticate(username="alice", password="password") bob = authenticate(username="bob", password="password") self.assertIsNone(alice) self.assertIsNotNone(bob) self.assertEqual(bob.ldap_user.group_names, {"other_gon"}) def test_denied_group(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), DENY_GROUP="cn=active_gon,ou=groups,o=test", ) alice = authenticate(username="alice", password="password") bob = authenticate(username="bob", password="password") self.assertIsNone(alice) self.assertIsNotNone(bob) def test_group_dns(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), ) alice = authenticate(username="alice", password="password") self.assertEqual( alice.ldap_user.group_dns, { "cn=active_gon,ou=groups,o=test", "cn=staff_gon,ou=groups,o=test", "cn=superuser_gon,ou=groups,o=test", "cn=nested_gon,ou=groups,o=test", }, ) def test_group_names(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), ) alice = authenticate(username="alice", password="password") self.assertEqual( alice.ldap_user.group_names, {"active_gon", "staff_gon", "superuser_gon", "nested_gon"}, ) def test_dn_group_membership(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), USER_FLAGS_BY_GROUP={ "is_active": LDAPGroupQuery("cn=active_gon,ou=groups,o=test"), "is_staff": [ "cn=empty_gon,ou=groups,o=test", "cn=staff_gon,ou=groups,o=test", ], "is_superuser": "cn=superuser_gon,ou=groups,o=test", }, ) alice = authenticate(username="alice", password="password") bob = authenticate(username="bob", password="password") self.assertIs(alice.is_active, True) self.assertIs(alice.is_staff, True) self.assertIs(alice.is_superuser, True) self.assertIs(bob.is_active, False) self.assertIs(bob.is_staff, False) self.assertIs(bob.is_superuser, False) def test_user_flags_misconfigured(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), USER_FLAGS_BY_GROUP={ "is_active": LDAPGroupQuery("cn=active_gon,ou=groups,o=test"), "is_staff": [], "is_superuser": "cn=superuser_gon,ou=groups,o=test", }, ) with self.assertRaises(ImproperlyConfigured): authenticate(username="alice", password="password") def test_posix_membership(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=PosixGroupType(), USER_FLAGS_BY_GROUP={ "is_active": "cn=active_px,ou=groups,o=test", "is_staff": "cn=staff_px,ou=groups,o=test", "is_superuser": "cn=superuser_px,ou=groups,o=test", }, ) alice = authenticate(username="alice", password="password") bob = authenticate(username="bob", password="password") self.assertIs(alice.is_active, True) self.assertIs(alice.is_staff, True) self.assertIs(alice.is_superuser, True) self.assertIs(bob.is_active, False) self.assertIs(bob.is_staff, False) self.assertIs(bob.is_superuser, False) def test_nested_dn_group_membership(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=NestedMemberDNGroupType(member_attr="member"), USER_FLAGS_BY_GROUP={ "is_active": "cn=parent_gon,ou=groups,o=test", "is_staff": "cn=parent_gon,ou=groups,o=test", }, ) alice = authenticate(username="alice", password="password") bob = authenticate(username="bob", password="password") self.assertIs(alice.is_active, True) self.assertIs(alice.is_staff, True) self.assertIs(bob.is_active, False) self.assertIs(bob.is_staff, False) def test_posix_missing_attributes(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=PosixGroupType(), USER_FLAGS_BY_GROUP={"is_active": "cn=active_px,ou=groups,o=test"}, ) nobody = authenticate(username="nobody", password="password") self.assertIs(nobody.is_active, False) def test_dn_group_permissions(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, ) self._init_groups() backend = get_backend() alice = User.objects.create(username="alice") alice = backend.get_user(alice.pk) self.assertEqual( backend.get_group_permissions(alice), {"auth.add_user", "auth.change_user"} ) self.assertEqual( backend.get_all_permissions(alice), {"auth.add_user", "auth.change_user"} ) self.assertIs(backend.has_perm(alice, "auth.add_user"), True) self.assertIs(backend.has_module_perms(alice, "auth"), True) def test_group_permissions_ldap_error(self): self._init_settings( BIND_DN="uid=bob,ou=people,o=test", BIND_PASSWORD="bogus", USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, ) self._init_groups() backend = get_backend() alice = User.objects.create(username="alice") alice = backend.get_user(alice.pk) self.assertEqual(backend.get_group_permissions(alice), set()) def test_empty_group_permissions(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, ) self._init_groups() backend = get_backend() bob = User.objects.create(username="bob") bob = backend.get_user(bob.pk) self.assertEqual(backend.get_group_permissions(bob), set()) self.assertEqual(backend.get_all_permissions(bob), set()) self.assertIs(backend.has_perm(bob, "auth.add_user"), False) self.assertIs(backend.has_module_perms(bob, "auth"), False) def test_posix_group_permissions(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)" ), GROUP_TYPE=PosixGroupType(), FIND_GROUP_PERMS=True, ) self._init_groups() backend = get_backend() alice = User.objects.create(username="alice") alice = backend.get_user(alice.pk) self.assertEqual( backend.get_group_permissions(alice), {"auth.add_user", "auth.change_user"} ) self.assertEqual( backend.get_all_permissions(alice), {"auth.add_user", "auth.change_user"} ) self.assertIs(backend.has_perm(alice, "auth.add_user"), True) self.assertIs(backend.has_module_perms(alice, "auth"), True) def test_posix_group_permissions_no_gid(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)" ), GROUP_TYPE=PosixGroupType(), FIND_GROUP_PERMS=True, ) self._init_groups() backend = get_backend() nonposix = User.objects.create(username="nonposix") nonposix = backend.get_user(nonposix.pk) self.assertEqual( backend.get_group_permissions(nonposix), {"auth.add_user", "auth.change_user"}, ) self.assertEqual( backend.get_all_permissions(nonposix), {"auth.add_user", "auth.change_user"} ) self.assertIs(backend.has_perm(nonposix, "auth.add_user"), True) self.assertIs(backend.has_module_perms(nonposix, "auth"), True) def test_foreign_user_permissions(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, ) self._init_groups() backend = get_backend() alice = User.objects.create(username="alice") self.assertEqual(backend.get_group_permissions(alice), set()) @spy_ldap("search_s") def test_group_cache(self, mock): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, CACHE_TIMEOUT=3600, ) self._init_groups() backend = get_backend() alice_id = User.objects.create(username="alice").pk bob_id = User.objects.create(username="bob").pk # Check permissions twice for each user for i in range(2): alice = backend.get_user(alice_id) self.assertEqual( backend.get_group_permissions(alice), {"auth.add_user", "auth.change_user"}, ) bob = backend.get_user(bob_id) self.assertEqual(backend.get_group_permissions(bob), set()) # Should have executed one LDAP search per user self.assertEqual(mock.call_count, 2) def test_group_mirroring(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)" ), GROUP_TYPE=PosixGroupType(), MIRROR_GROUPS=True, ) self.assertEqual(Group.objects.count(), 0) alice = authenticate(username="alice", password="password") self.assertEqual(Group.objects.count(), 3) self.assertEqual(set(alice.groups.all()), set(Group.objects.all())) def test_nested_group_mirroring(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" ), GROUP_TYPE=NestedMemberDNGroupType(member_attr="member"), MIRROR_GROUPS=True, ) alice = authenticate(username="alice", password="password") self.assertEqual( set(Group.objects.all().values_list("name", flat=True)), { "active_gon", "staff_gon", "superuser_gon", "nested_gon", "parent_gon", "circular_gon", }, ) self.assertEqual(set(alice.groups.all()), set(Group.objects.all())) # # When selectively mirroring groups, there are eight scenarios for any # given user/group pair: # # (is-member-in-LDAP, not-member-in-LDAP) # x (is-member-in-Django, not-member-in-Django) # x (synced, not-synced) # # The four test cases below take these scenarios four at a time for each of # the two settings. def test_group_mirroring_whitelist_update(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=mirror_groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=GroupOfNamesType(), MIRROR_GROUPS=["mirror1", "mirror2"], ) backend = get_backend() groups = {} for name in ("mirror{}".format(i) for i in range(1, 5)): groups[name] = Group.objects.create(name=name) alice = backend.populate_user("alice") alice.groups.set([groups["mirror2"], groups["mirror4"]]) alice = authenticate(username="alice", password="password") self.assertEqual( set(alice.groups.values_list("name", flat=True)), {"mirror1", "mirror4"} ) def test_group_mirroring_whitelist_noop(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=mirror_groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=GroupOfNamesType(), MIRROR_GROUPS=["mirror1", "mirror2"], ) backend = get_backend() groups = {} for name in ("mirror{}".format(i) for i in range(1, 5)): groups[name] = Group.objects.create(name=name) alice = backend.populate_user("alice") alice.groups.set([groups["mirror1"], groups["mirror3"]]) alice = authenticate(username="alice", password="password") self.assertEqual( set(alice.groups.values_list("name", flat=True)), {"mirror1", "mirror3"} ) def test_group_mirroring_blacklist_update(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=mirror_groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=GroupOfNamesType(), MIRROR_GROUPS_EXCEPT=["mirror1", "mirror2"], ) backend = get_backend() groups = {} for name in ("mirror{}".format(i) for i in range(1, 5)): groups[name] = Group.objects.create(name=name) alice = backend.populate_user("alice") alice.groups.set([groups["mirror2"], groups["mirror4"]]) alice = authenticate(username="alice", password="password") self.assertEqual( set(alice.groups.values_list("name", flat=True)), {"mirror2", "mirror3"} ) def test_group_mirroring_blacklist_noop(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=mirror_groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=GroupOfNamesType(), MIRROR_GROUPS_EXCEPT=["mirror1", "mirror2"], ) backend = get_backend() groups = {} for name in ("mirror{}".format(i) for i in range(1, 5)): groups[name] = Group.objects.create(name=name) alice = backend.populate_user("alice") alice.groups.set([groups["mirror1"], groups["mirror3"]]) alice = authenticate(username="alice", password="password") self.assertEqual( set(alice.groups.values_list("name", flat=True)), {"mirror1", "mirror3"} ) def test_authorize_external_users(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, AUTHORIZE_ALL_USERS=True, ) self._init_groups() backend = get_backend() alice = User.objects.create(username="alice") self.assertEqual( backend.get_group_permissions(alice), {"auth.add_user", "auth.change_user"} ) def test_authorize_external_unknown(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ), GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, AUTHORIZE_ALL_USERS=True, ) self._init_groups() backend = get_backend() alice = User.objects.create(username="not-in-ldap") self.assertEqual(backend.get_group_permissions(alice), set()) def test_create_without_auth(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") backend = get_backend() alice = backend.populate_user("alice") bob = backend.populate_user("bob") self.assertIsNotNone(alice) self.assertEqual(alice.first_name, "") self.assertEqual(alice.last_name, "") self.assertIs(alice.is_active, True) self.assertIs(alice.is_staff, False) self.assertIs(alice.is_superuser, False) self.assertIsNotNone(bob) self.assertEqual(bob.first_name, "") self.assertEqual(bob.last_name, "") self.assertIs(bob.is_active, True) self.assertIs(bob.is_staff, False) self.assertIs(bob.is_superuser, False) def test_populate_without_auth(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", ALWAYS_UPDATE_USER=False, USER_ATTR_MAP={"first_name": "givenName", "last_name": "sn"}, GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=GroupOfNamesType(), USER_FLAGS_BY_GROUP={ "is_active": "cn=active_gon,ou=groups,o=test", "is_staff": "cn=staff_gon,ou=groups,o=test", "is_superuser": "cn=superuser_gon,ou=groups,o=test", }, ) User.objects.create(username="alice") User.objects.create(username="bob") backend = get_backend() alice = backend.populate_user("alice") bob = backend.populate_user("bob") self.assertIsNotNone(alice) self.assertEqual(alice.first_name, "Alice") self.assertEqual(alice.last_name, "Adams") self.assertIs(alice.is_active, True) self.assertIs(alice.is_staff, True) self.assertIs(alice.is_superuser, True) self.assertIsNotNone(bob) self.assertEqual(bob.first_name, "Robert") self.assertEqual(bob.last_name, "Barker") self.assertIs(bob.is_active, False) self.assertIs(bob.is_staff, False) self.assertIs(bob.is_superuser, False) def test_populate_bogus_user(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") backend = get_backend() bogus = backend.populate_user("bogus") self.assertIsNone(bogus) @spy_ldap("start_tls_s") def test_start_tls_missing(self, mock): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", START_TLS=False ) authenticate(username="alice", password="password") mock.assert_not_called() @spy_ldap("start_tls_s") def test_start_tls(self, mock): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", START_TLS=True ) authenticate(username="alice", password="password") mock.assert_called_once() def test_null_search_results(self): """ Make sure we're not phased by referrals. """ self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ) ) authenticate(username="alice", password="password") def test_union_search(self): self._init_settings( USER_SEARCH=LDAPSearchUnion( LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"), LDAPSearch("ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"), ) ) alice = authenticate(username="alice", password="password") self.assertIsNotNone(alice) @spy_ldap("simple_bind_s") def test_deny_empty_password(self, mock): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") alice = authenticate(username="alice", password="") self.assertIsNone(alice) mock.assert_not_called() @spy_ldap("simple_bind_s") def test_permit_empty_password(self, mock): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", PERMIT_EMPTY_PASSWORD=True ) alice = authenticate(username="alice", password="") self.assertIsNone(alice) mock.assert_called_once() @spy_ldap("simple_bind_s") def test_permit_null_password(self, mock): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", PERMIT_EMPTY_PASSWORD=True ) alice = authenticate(username="alice", password=None) self.assertIsNone(alice) mock.assert_called_once() def test_pickle(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, ) self._init_groups() backend = get_backend() alice0 = authenticate(username="alice", password="password") pickled = pickle.dumps(alice0, pickle.HIGHEST_PROTOCOL) alice = pickle.loads(pickled) self.assertIsNotNone(alice) self.assertEqual( backend.get_group_permissions(alice), {"auth.add_user", "auth.change_user"} ) self.assertEqual( backend.get_all_permissions(alice), {"auth.add_user", "auth.change_user"} ) self.assertIs(backend.has_perm(alice, "auth.add_user"), True) self.assertIs(backend.has_module_perms(alice, "auth"), True) @mock.patch("ldap.ldapobject.SimpleLDAPObject.search_s") def test_search_attrlist(self, mock_search): backend = get_backend() connection = backend.ldap.initialize(self.server.ldap_uri, bytes_mode=False) search = LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=alice)", ["*", "+"] ) search.execute(connection) mock_search.assert_called_once_with( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=alice)", ["*", "+"] ) def test_override_authenticate_access_ldap_user(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") class MyBackend(LDAPBackend): def authenticate_ldap_user(self, ldap_user, password): ldap_user.foo = "bar" return super().authenticate_ldap_user(ldap_user, password) backend = MyBackend() user = backend.authenticate(None, username="alice", password="password") self.assertEqual(user.ldap_user.foo, "bar") @spy_ldap("search_s") def test_dn_not_cached(self, mock): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ) ) for _ in range(2): user = authenticate(username="alice", password="password") self.assertIsNotNone(user) # Should have executed once per auth. self.assertEqual(mock.call_count, 2) # DN is not cached. self.assertIsNone(cache.get("django_auth_ldap.user_dn.alice")) @spy_ldap("search_s") def test_dn_cached(self, mock): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ), CACHE_TIMEOUT=60, ) for _ in range(2): user = authenticate(username="alice", password="password") self.assertIsNotNone(user) # Should have executed only once. self.assertEqual(mock.call_count, 1) # DN is cached. self.assertEqual( cache.get("django_auth_ldap.user_dn.alice"), "uid=alice,ou=people,o=test" ) def test_deprecated_cache_groups(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ), CACHE_GROUPS=True, ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") user = authenticate(username="alice", password="password") self.assertIsNotNone(user) self.assertEqual(len(w), 1) self.assertEqual(w[0].category, DeprecationWarning) self.assertEqual( str(w[0].message), "Found deprecated setting AUTH_LDAP_CACHE_GROUP. Use " "AUTH_LDAP_CACHE_TIMEOUT instead.", ) # DN is cached. self.assertEqual( cache.get("django_auth_ldap.user_dn.alice"), "uid=alice,ou=people,o=test" ) # # Utilities # def _init_settings(self, **kwargs): kwargs.setdefault("SERVER_URI", self.server.ldap_uri) settings = {} for key, value in kwargs.items(): settings["AUTH_LDAP_%s" % key] = value cm = override_settings(**settings) cm.enable() self.addCleanup(cm.disable) def _init_groups(self): permissions = [ Permission.objects.get(codename="add_user"), Permission.objects.get(codename="change_user"), ] active_gon = Group.objects.create(name="active_gon") active_gon.permissions.add(*permissions) active_px = Group.objects.create(name="active_px") active_px.permissions.add(*permissions) active_nis = Group.objects.create(name="active_nis") active_nis.permissions.add(*permissions) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575432371.0 django-auth-ldap-2.2.0/tox.ini0000664000175000017500000000167200000000000015554 0ustar00jonjon00000000000000[tox] envlist = black flake8 isort docs py{35,36,37}-django111 py{35,36,37,38}-django22 py{36,37,38}-django30 py{36,37,38}-djangomaster [testenv] commands = {envpython} -Wa -b -m django test --settings tests.settings deps = django111: Django~=1.11.0 django22: Django~=2.2.0 django30: Django>=3.0<3.1 djangomaster: https://github.com/django/django/archive/master.tar.gz mock >= 2.0.0 [testenv:black] basepython = python3 deps = black commands = black --check --diff . skip_install = true [testenv:flake8] basepython = python3 deps = flake8 commands = flake8 skip_install = true [testenv:isort] basepython = python3 deps = isort commands = isort --check-only --diff skip_install = true [testenv:docs] basepython = python3 deps = readme_renderer sphinx commands = make -C docs html {envpython} setup.py check --restructuredtext --strict skip_install = true whitelist_externals = make