New Upstream Release - django-python3-ldap
Ready changes
Summary
Merged new upstream version: 0.15.4 (was: 0.15.3).
Resulting package
Built on 2022-12-21T13:27 (took 11m45s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-releases python3-django-python3-ldap
Lintian Result
Diff
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 0997508..5b3f6af 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,57 @@
django-python3-ldap changelog
=============================
+0.15.4
+------
+
+- BUGFIX: Fixing redundant rebind when `LDAP_AUTH_CONNECTION_USERNAME` matches the existing username (@gagantrivedi).
+
+
+0.15.3
+------
+
+- BUGFIX: TLS connection is not started even if `LDAP_AUTH_USE_TLS = True` (@githubuserx).
+
+0.15.2
+------
+
+- Added ``ldap_clean_users`` command (@jordiromera).
+
+0.15.1
+------
+
+- Bugfix: Allows overriding the SSL/TLS version (@FlipperPA).
+
+
+0.15.0
+------
+
+- Allows overriding the SSL/TLS version (@FlipperPA).
+
+0.14.0
+------
+
+- Added support for multiple LDAP servers to enable a high-availability server pool (@hho6643).
+
+0.13.1
+------
+
+- Django 4.0 compatibility (@sn1c).
+
+
+0.13.0
+------
+
+- Allow syncing individual users with ``ldap_sync_users`` management command (@CristopherH95).
+
+
+0.12.1
+------
+
+- Added support for additional (ignored) keyword arguments to ``authenticate(...)``. This allows use with other
+ authentication backends that require different keyword arguments (@stinovlas).
+
+
0.12.0
------
diff --git a/PKG-INFO b/PKG-INFO
index 23ffbc8..8809368 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,13 +1,11 @@
-Metadata-Version: 1.1
+Metadata-Version: 2.1
Name: django-python3-ldap
-Version: 0.12.0
+Version: 0.15.4
Summary: Django LDAP user authentication backend for Python 3.
Home-page: https://github.com/etianen/django-python3-ldap
Author: Dave Hall
Author-email: dave@etianen.com
License: BSD
-Description: UNKNOWN
-Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
@@ -18,3 +16,274 @@ Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Framework :: Django
+License-File: LICENSE
+
+django-python3-ldap
+===================
+
+**django-python3-ldap** provides a Django LDAP user authentication backend. Python 3.6+ is required.
+
+
+Features
+--------
+
+- Authenticate users with an LDAP server.
+- Sync LDAP users with a local Django database.
+- Supports custom Django user models.
+
+
+Installation
+------------
+
+1. Install using ``pip install django-python3-ldap``.
+2. Add ``'django_python3_ldap'`` to your ``INSTALLED_APPS`` setting.
+3. Set your ``AUTHENTICATION_BACKENDS`` setting to ``("django_python3_ldap.auth.LDAPBackend",)``
+4. Configure the settings for your LDAP server(s) (see Available settings, below).
+5. Optionally, run ``./manage.py ldap_sync_users`` (or ``./manage.py ldap_sync_users <list of user lookups>``) to perform an initial sync of LDAP users.
+6. Optionally, run ``./manage.py ldap_promote <username>`` to grant superuser admin access to a given user.
+
+
+Available settings
+------------------
+
+**Note**: The settings below show their default values. You only need to add settings to your ``settings.py`` file that you intend to override.
+
+
+.. code:: python
+
+ # The URL of the LDAP server(s). List multiple servers for high availability ServerPool connection.
+ LDAP_AUTH_URL = ["ldap://localhost:389"]
+
+ # Initiate TLS on connection.
+ LDAP_AUTH_USE_TLS = False
+
+ # Specify which TLS version to use (Python 3.10 requires TLSv1 or higher)
+ import ssl
+ LDAP_AUTH_TLS_VERSION = ssl.PROTOCOL_TLSv1_2
+
+ # The LDAP search base for looking up users.
+ LDAP_AUTH_SEARCH_BASE = "ou=people,dc=example,dc=com"
+
+ # The LDAP class that represents a user.
+ LDAP_AUTH_OBJECT_CLASS = "inetOrgPerson"
+
+ # User model fields mapped to the LDAP
+ # attributes that represent them.
+ LDAP_AUTH_USER_FIELDS = {
+ "username": "uid",
+ "first_name": "givenName",
+ "last_name": "sn",
+ "email": "mail",
+ }
+
+ # A tuple of django model fields used to uniquely identify a user.
+ LDAP_AUTH_USER_LOOKUP_FIELDS = ("username",)
+
+ # Path to a callable that takes a dict of {model_field_name: value},
+ # returning a dict of clean model data.
+ # Use this to customize how data loaded from LDAP is saved to the User model.
+ LDAP_AUTH_CLEAN_USER_DATA = "django_python3_ldap.utils.clean_user_data"
+
+ # Path to a callable that takes a user model, a dict of {ldap_field_name: [value]}
+ # a LDAP connection object (to allow further lookups), and saves any additional
+ # user relationships based on the LDAP data.
+ # Use this to customize how data loaded from LDAP is saved to User model relations.
+ # For customizing non-related User model fields, use LDAP_AUTH_CLEAN_USER_DATA.
+ LDAP_AUTH_SYNC_USER_RELATIONS = "django_python3_ldap.utils.sync_user_relations"
+
+ # Path to a callable that takes a dict of {ldap_field_name: value},
+ # returning a list of [ldap_search_filter]. The search filters will then be AND'd
+ # together when creating the final search filter.
+ LDAP_AUTH_FORMAT_SEARCH_FILTERS = "django_python3_ldap.utils.format_search_filters"
+
+ # Path to a callable that takes a dict of {model_field_name: value}, and returns
+ # a string of the username to bind to the LDAP server.
+ # Use this to support different types of LDAP server.
+ LDAP_AUTH_FORMAT_USERNAME = "django_python3_ldap.utils.format_username_openldap"
+
+ # Sets the login domain for Active Directory users.
+ LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN = None
+
+ # The LDAP username and password of a user for querying the LDAP database for user
+ # details. If None, then the authenticated user will be used for querying, and
+ # the `ldap_sync_users`, `ldap_clean_users` commands will perform an anonymous query.
+ LDAP_AUTH_CONNECTION_USERNAME = None
+ LDAP_AUTH_CONNECTION_PASSWORD = None
+
+ # Set connection/receive timeouts (in seconds) on the underlying `ldap3` library.
+ LDAP_AUTH_CONNECT_TIMEOUT = None
+ LDAP_AUTH_RECEIVE_TIMEOUT = None
+
+
+Microsoft Active Directory support
+----------------------------------
+
+django-python3-ldap is configured by default to support login via OpenLDAP. To connect to
+a Microsoft Active Directory, you need to modify your settings file.
+
+For simple usernames (e.g. "username"):
+
+.. code:: python
+
+ LDAP_AUTH_FORMAT_USERNAME = "django_python3_ldap.utils.format_username_active_directory"
+
+For down-level login name formats (e.g. "DOMAIN\\username"):
+
+.. code:: python
+
+ LDAP_AUTH_FORMAT_USERNAME = "django_python3_ldap.utils.format_username_active_directory"
+ LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN = "DOMAIN"
+
+For user-principal-name formats (e.g. "user@domain.com"):
+
+.. code:: python
+
+ LDAP_AUTH_FORMAT_USERNAME = "django_python3_ldap.utils.format_username_active_directory_principal"
+ LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN = "domain.com"
+
+Depending on how your Active Directory server is configured, the following additional settings may match your server
+better than the defaults used by django-python3-ldap:
+
+.. code:: python
+
+ LDAP_AUTH_USER_FIELDS = {
+ "username": "sAMAccountName",
+ "first_name": "givenName",
+ "last_name": "sn",
+ "email": "mail",
+ }
+
+ LDAP_AUTH_OBJECT_CLASS = "user"
+
+
+Sync User Relations
+-------------------
+
+As part of the user authentication process, django-python3-ldap calls a function specified by the
+LDAP_AUTH_SYNC_USER_RELATIONS configuraton item. This function can be used for making additional
+updates to the user database (for example updaing the groups the user is a member of), or getting
+further information from the LDAP server.
+
+The signature of the called function is:-
+
+.. code:: python
+
+ def sync_user_relations(user, ldap_attributes, *, connection=None, dn=None):
+
+The parameters are:-
+
+- ``user`` - a Django user model object
+- ``ldap_attributes`` - a dict of LDAP attributes
+- ``connection`` - the LDAP connection object (optional keyword only parameter)
+- ``dn`` - the DN (Distinguished Name) of the LDAP matched user (optional keyword only parameter)
+
+
+Clean User
+----------
+
+When a LDAP user is removed from server it could be interresting to deactive or delete its local Django account
+to prevent unauthorized access.
+
+To do so run:
+
+ ``./manage.py ldap_clean_users`` (or ``./manage.py ldap_clean_users --purge``).
+
+It will deactivate all local users non declared on LDAP server. If ``--purge`` is specified, all local users will be deleted.
+
+
+Can't get authentication to work?
+---------------------------------
+
+LDAP is a very complicated protocol. Enable logging (see below), and see what error messages the LDAP connection is throwing.
+
+
+Logging
+-------
+
+Print information about failed logins to your console by adding the following to your ``settings.py`` file.
+
+.. code:: python
+
+ LOGGING = {
+ "version": 1,
+ "disable_existing_loggers": False,
+ "handlers": {
+ "console": {
+ "class": "logging.StreamHandler",
+ },
+ },
+ "loggers": {
+ "django_python3_ldap": {
+ "handlers": ["console"],
+ "level": "INFO",
+ },
+ },
+ }
+
+
+Custom user filters
+-------------------
+
+By default, any users within ``LDAP_AUTH_SEARCH_BASE`` and of the correct ``LDAP_AUTH_OBJECT_CLASS``
+will be considered a valid user. You can apply further filtering by setting a custom ``LDAP_AUTH_FORMAT_SEARCH_FILTERS``
+callable.
+
+.. code:: python
+
+ # settings.py
+ LDAP_AUTH_FORMAT_SEARCH_FILTERS = "path.to.your.custom_format_search_filters"
+
+ # path/to/your/module.py
+ from django_python3_ldap.utils import format_search_filters
+
+ def custom_format_search_filters(ldap_fields):
+ # Add in simple filters.
+ ldap_fields["memberOf"] = "foo"
+ # Call the base format callable.
+ search_filters = format_search_filters(ldap_fields)
+ # Advanced: apply custom LDAP filter logic.
+ search_filters.append("(|(memberOf=groupA)(memberOf=GroupB))")
+ # All done!
+ return search_filters
+
+The returned list of search filters will be AND'd together to make the final search filter.
+
+
+How it works
+------------
+
+When a user attempts to authenticate, a connection is made to one of the listed LDAP
+servers, and the application attempts to bind using the provided username and password.
+
+If the bind attempt is successful, the user details are loaded from the LDAP server
+and saved in a local Django ``User`` model. The local model is only created once,
+and the details will be kept updated with the LDAP record details on every login.
+
+To perform a full sync of all LDAP users to the local database, run ``./manage.py ldap_sync_users``.
+This is not required, as the authentication backend will create users on demand. Syncing users has
+the advantage of allowing you to assign permissions and groups to the existing users using the Django
+admin interface.
+
+Running ``ldap_sync_users`` as a background cron task is another optional way to
+keep all users in sync on a regular basis.
+
+
+Support and announcements
+-------------------------
+
+Downloads and bug tracking can be found at the `main project
+website <http://github.com/etianen/django-python3-ldap>`_.
+
+
+More information
+----------------
+
+The django-python3-ldap project was developed by Dave Hall. You can get the code
+from the `django-python3-ldap project site <http://github.com/etianen/django-python3-ldap>`_.
+
+Dave Hall is a freelance web developer, based in Cambridge, UK. You can usually
+find him on the Internet in a number of different places:
+
+- `Website <http://www.etianen.com/>`_
+- `Twitter <http://twitter.com/etianen>`_
+- `Google Profile <http://www.google.com/profiles/david.etianen>`_
diff --git a/README.rst b/README.rst
index 7bd08f2..5b5e334 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,7 @@
django-python3-ldap
===================
-**django-python3-ldap** provides a Django LDAP user authentication backend.
+**django-python3-ldap** provides a Django LDAP user authentication backend. Python 3.6+ is required.
Features
@@ -18,8 +18,8 @@ Installation
1. Install using ``pip install django-python3-ldap``.
2. Add ``'django_python3_ldap'`` to your ``INSTALLED_APPS`` setting.
3. Set your ``AUTHENTICATION_BACKENDS`` setting to ``("django_python3_ldap.auth.LDAPBackend",)``
-4. Configure the settings for your LDAP server (see Available settings, below).
-5. Optionally, run ``./manage.py ldap_sync_users`` to perform an initial sync of LDAP users.
+4. Configure the settings for your LDAP server(s) (see Available settings, below).
+5. Optionally, run ``./manage.py ldap_sync_users`` (or ``./manage.py ldap_sync_users <list of user lookups>``) to perform an initial sync of LDAP users.
6. Optionally, run ``./manage.py ldap_promote <username>`` to grant superuser admin access to a given user.
@@ -31,12 +31,16 @@ Available settings
.. code:: python
- # The URL of the LDAP server.
- LDAP_AUTH_URL = "ldap://localhost:389"
+ # The URL of the LDAP server(s). List multiple servers for high availability ServerPool connection.
+ LDAP_AUTH_URL = ["ldap://localhost:389"]
# Initiate TLS on connection.
LDAP_AUTH_USE_TLS = False
+ # Specify which TLS version to use (Python 3.10 requires TLSv1 or higher)
+ import ssl
+ LDAP_AUTH_TLS_VERSION = ssl.PROTOCOL_TLSv1_2
+
# The LDAP search base for looking up users.
LDAP_AUTH_SEARCH_BASE = "ou=people,dc=example,dc=com"
@@ -82,7 +86,7 @@ Available settings
# The LDAP username and password of a user for querying the LDAP database for user
# details. If None, then the authenticated user will be used for querying, and
- # the `ldap_sync_users` command will perform an anonymous query.
+ # the `ldap_sync_users`, `ldap_clean_users` commands will perform an anonymous query.
LDAP_AUTH_CONNECTION_USERNAME = None
LDAP_AUTH_CONNECTION_PASSWORD = None
@@ -154,6 +158,19 @@ The parameters are:-
- ``dn`` - the DN (Distinguished Name) of the LDAP matched user (optional keyword only parameter)
+Clean User
+----------
+
+When a LDAP user is removed from server it could be interresting to deactive or delete its local Django account
+to prevent unauthorized access.
+
+To do so run:
+
+ ``./manage.py ldap_clean_users`` (or ``./manage.py ldap_clean_users --purge``).
+
+It will deactivate all local users non declared on LDAP server. If ``--purge`` is specified, all local users will be deleted.
+
+
Can't get authentication to work?
---------------------------------
@@ -215,8 +232,8 @@ The returned list of search filters will be AND'd together to make the final sea
How it works
------------
-When a user attempts to authenticate, a connection is made to the LDAP
-server, and the application attempts to bind using the provided username and password.
+When a user attempts to authenticate, a connection is made to one of the listed LDAP
+servers, and the application attempts to bind using the provided username and password.
If the bind attempt is successful, the user details are loaded from the LDAP server
and saved in a local Django ``User`` model. The local model is only created once,
diff --git a/debian/changelog b/debian/changelog
index c63131b..aa580ac 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,10 @@
-django-python3-ldap (0.12.0-2) UNRELEASED; urgency=medium
+django-python3-ldap (0.15.4-1) UNRELEASED; urgency=medium
* Update standards version to 4.6.1, no changes needed.
+ * New upstream release.
+ * New upstream release.
- -- Debian Janitor <janitor@jelmer.uk> Fri, 14 Oct 2022 21:46:49 -0000
+ -- Debian Janitor <janitor@jelmer.uk> Wed, 21 Dec 2022 13:17:05 -0000
django-python3-ldap (0.12.0-1) unstable; urgency=low
diff --git a/django_python3_ldap.egg-info/PKG-INFO b/django_python3_ldap.egg-info/PKG-INFO
index 23ffbc8..8809368 100644
--- a/django_python3_ldap.egg-info/PKG-INFO
+++ b/django_python3_ldap.egg-info/PKG-INFO
@@ -1,13 +1,11 @@
-Metadata-Version: 1.1
+Metadata-Version: 2.1
Name: django-python3-ldap
-Version: 0.12.0
+Version: 0.15.4
Summary: Django LDAP user authentication backend for Python 3.
Home-page: https://github.com/etianen/django-python3-ldap
Author: Dave Hall
Author-email: dave@etianen.com
License: BSD
-Description: UNKNOWN
-Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
@@ -18,3 +16,274 @@ Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Framework :: Django
+License-File: LICENSE
+
+django-python3-ldap
+===================
+
+**django-python3-ldap** provides a Django LDAP user authentication backend. Python 3.6+ is required.
+
+
+Features
+--------
+
+- Authenticate users with an LDAP server.
+- Sync LDAP users with a local Django database.
+- Supports custom Django user models.
+
+
+Installation
+------------
+
+1. Install using ``pip install django-python3-ldap``.
+2. Add ``'django_python3_ldap'`` to your ``INSTALLED_APPS`` setting.
+3. Set your ``AUTHENTICATION_BACKENDS`` setting to ``("django_python3_ldap.auth.LDAPBackend",)``
+4. Configure the settings for your LDAP server(s) (see Available settings, below).
+5. Optionally, run ``./manage.py ldap_sync_users`` (or ``./manage.py ldap_sync_users <list of user lookups>``) to perform an initial sync of LDAP users.
+6. Optionally, run ``./manage.py ldap_promote <username>`` to grant superuser admin access to a given user.
+
+
+Available settings
+------------------
+
+**Note**: The settings below show their default values. You only need to add settings to your ``settings.py`` file that you intend to override.
+
+
+.. code:: python
+
+ # The URL of the LDAP server(s). List multiple servers for high availability ServerPool connection.
+ LDAP_AUTH_URL = ["ldap://localhost:389"]
+
+ # Initiate TLS on connection.
+ LDAP_AUTH_USE_TLS = False
+
+ # Specify which TLS version to use (Python 3.10 requires TLSv1 or higher)
+ import ssl
+ LDAP_AUTH_TLS_VERSION = ssl.PROTOCOL_TLSv1_2
+
+ # The LDAP search base for looking up users.
+ LDAP_AUTH_SEARCH_BASE = "ou=people,dc=example,dc=com"
+
+ # The LDAP class that represents a user.
+ LDAP_AUTH_OBJECT_CLASS = "inetOrgPerson"
+
+ # User model fields mapped to the LDAP
+ # attributes that represent them.
+ LDAP_AUTH_USER_FIELDS = {
+ "username": "uid",
+ "first_name": "givenName",
+ "last_name": "sn",
+ "email": "mail",
+ }
+
+ # A tuple of django model fields used to uniquely identify a user.
+ LDAP_AUTH_USER_LOOKUP_FIELDS = ("username",)
+
+ # Path to a callable that takes a dict of {model_field_name: value},
+ # returning a dict of clean model data.
+ # Use this to customize how data loaded from LDAP is saved to the User model.
+ LDAP_AUTH_CLEAN_USER_DATA = "django_python3_ldap.utils.clean_user_data"
+
+ # Path to a callable that takes a user model, a dict of {ldap_field_name: [value]}
+ # a LDAP connection object (to allow further lookups), and saves any additional
+ # user relationships based on the LDAP data.
+ # Use this to customize how data loaded from LDAP is saved to User model relations.
+ # For customizing non-related User model fields, use LDAP_AUTH_CLEAN_USER_DATA.
+ LDAP_AUTH_SYNC_USER_RELATIONS = "django_python3_ldap.utils.sync_user_relations"
+
+ # Path to a callable that takes a dict of {ldap_field_name: value},
+ # returning a list of [ldap_search_filter]. The search filters will then be AND'd
+ # together when creating the final search filter.
+ LDAP_AUTH_FORMAT_SEARCH_FILTERS = "django_python3_ldap.utils.format_search_filters"
+
+ # Path to a callable that takes a dict of {model_field_name: value}, and returns
+ # a string of the username to bind to the LDAP server.
+ # Use this to support different types of LDAP server.
+ LDAP_AUTH_FORMAT_USERNAME = "django_python3_ldap.utils.format_username_openldap"
+
+ # Sets the login domain for Active Directory users.
+ LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN = None
+
+ # The LDAP username and password of a user for querying the LDAP database for user
+ # details. If None, then the authenticated user will be used for querying, and
+ # the `ldap_sync_users`, `ldap_clean_users` commands will perform an anonymous query.
+ LDAP_AUTH_CONNECTION_USERNAME = None
+ LDAP_AUTH_CONNECTION_PASSWORD = None
+
+ # Set connection/receive timeouts (in seconds) on the underlying `ldap3` library.
+ LDAP_AUTH_CONNECT_TIMEOUT = None
+ LDAP_AUTH_RECEIVE_TIMEOUT = None
+
+
+Microsoft Active Directory support
+----------------------------------
+
+django-python3-ldap is configured by default to support login via OpenLDAP. To connect to
+a Microsoft Active Directory, you need to modify your settings file.
+
+For simple usernames (e.g. "username"):
+
+.. code:: python
+
+ LDAP_AUTH_FORMAT_USERNAME = "django_python3_ldap.utils.format_username_active_directory"
+
+For down-level login name formats (e.g. "DOMAIN\\username"):
+
+.. code:: python
+
+ LDAP_AUTH_FORMAT_USERNAME = "django_python3_ldap.utils.format_username_active_directory"
+ LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN = "DOMAIN"
+
+For user-principal-name formats (e.g. "user@domain.com"):
+
+.. code:: python
+
+ LDAP_AUTH_FORMAT_USERNAME = "django_python3_ldap.utils.format_username_active_directory_principal"
+ LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN = "domain.com"
+
+Depending on how your Active Directory server is configured, the following additional settings may match your server
+better than the defaults used by django-python3-ldap:
+
+.. code:: python
+
+ LDAP_AUTH_USER_FIELDS = {
+ "username": "sAMAccountName",
+ "first_name": "givenName",
+ "last_name": "sn",
+ "email": "mail",
+ }
+
+ LDAP_AUTH_OBJECT_CLASS = "user"
+
+
+Sync User Relations
+-------------------
+
+As part of the user authentication process, django-python3-ldap calls a function specified by the
+LDAP_AUTH_SYNC_USER_RELATIONS configuraton item. This function can be used for making additional
+updates to the user database (for example updaing the groups the user is a member of), or getting
+further information from the LDAP server.
+
+The signature of the called function is:-
+
+.. code:: python
+
+ def sync_user_relations(user, ldap_attributes, *, connection=None, dn=None):
+
+The parameters are:-
+
+- ``user`` - a Django user model object
+- ``ldap_attributes`` - a dict of LDAP attributes
+- ``connection`` - the LDAP connection object (optional keyword only parameter)
+- ``dn`` - the DN (Distinguished Name) of the LDAP matched user (optional keyword only parameter)
+
+
+Clean User
+----------
+
+When a LDAP user is removed from server it could be interresting to deactive or delete its local Django account
+to prevent unauthorized access.
+
+To do so run:
+
+ ``./manage.py ldap_clean_users`` (or ``./manage.py ldap_clean_users --purge``).
+
+It will deactivate all local users non declared on LDAP server. If ``--purge`` is specified, all local users will be deleted.
+
+
+Can't get authentication to work?
+---------------------------------
+
+LDAP is a very complicated protocol. Enable logging (see below), and see what error messages the LDAP connection is throwing.
+
+
+Logging
+-------
+
+Print information about failed logins to your console by adding the following to your ``settings.py`` file.
+
+.. code:: python
+
+ LOGGING = {
+ "version": 1,
+ "disable_existing_loggers": False,
+ "handlers": {
+ "console": {
+ "class": "logging.StreamHandler",
+ },
+ },
+ "loggers": {
+ "django_python3_ldap": {
+ "handlers": ["console"],
+ "level": "INFO",
+ },
+ },
+ }
+
+
+Custom user filters
+-------------------
+
+By default, any users within ``LDAP_AUTH_SEARCH_BASE`` and of the correct ``LDAP_AUTH_OBJECT_CLASS``
+will be considered a valid user. You can apply further filtering by setting a custom ``LDAP_AUTH_FORMAT_SEARCH_FILTERS``
+callable.
+
+.. code:: python
+
+ # settings.py
+ LDAP_AUTH_FORMAT_SEARCH_FILTERS = "path.to.your.custom_format_search_filters"
+
+ # path/to/your/module.py
+ from django_python3_ldap.utils import format_search_filters
+
+ def custom_format_search_filters(ldap_fields):
+ # Add in simple filters.
+ ldap_fields["memberOf"] = "foo"
+ # Call the base format callable.
+ search_filters = format_search_filters(ldap_fields)
+ # Advanced: apply custom LDAP filter logic.
+ search_filters.append("(|(memberOf=groupA)(memberOf=GroupB))")
+ # All done!
+ return search_filters
+
+The returned list of search filters will be AND'd together to make the final search filter.
+
+
+How it works
+------------
+
+When a user attempts to authenticate, a connection is made to one of the listed LDAP
+servers, and the application attempts to bind using the provided username and password.
+
+If the bind attempt is successful, the user details are loaded from the LDAP server
+and saved in a local Django ``User`` model. The local model is only created once,
+and the details will be kept updated with the LDAP record details on every login.
+
+To perform a full sync of all LDAP users to the local database, run ``./manage.py ldap_sync_users``.
+This is not required, as the authentication backend will create users on demand. Syncing users has
+the advantage of allowing you to assign permissions and groups to the existing users using the Django
+admin interface.
+
+Running ``ldap_sync_users`` as a background cron task is another optional way to
+keep all users in sync on a regular basis.
+
+
+Support and announcements
+-------------------------
+
+Downloads and bug tracking can be found at the `main project
+website <http://github.com/etianen/django-python3-ldap>`_.
+
+
+More information
+----------------
+
+The django-python3-ldap project was developed by Dave Hall. You can get the code
+from the `django-python3-ldap project site <http://github.com/etianen/django-python3-ldap>`_.
+
+Dave Hall is a freelance web developer, based in Cambridge, UK. You can usually
+find him on the Internet in a number of different places:
+
+- `Website <http://www.etianen.com/>`_
+- `Twitter <http://twitter.com/etianen>`_
+- `Google Profile <http://www.google.com/profiles/david.etianen>`_
diff --git a/django_python3_ldap.egg-info/SOURCES.txt b/django_python3_ldap.egg-info/SOURCES.txt
index 4ccf85b..2d41e69 100644
--- a/django_python3_ldap.egg-info/SOURCES.txt
+++ b/django_python3_ldap.egg-info/SOURCES.txt
@@ -17,6 +17,7 @@ django_python3_ldap.egg-info/requires.txt
django_python3_ldap.egg-info/top_level.txt
django_python3_ldap/management/__init__.py
django_python3_ldap/management/commands/__init__.py
+django_python3_ldap/management/commands/ldap_clean_users.py
django_python3_ldap/management/commands/ldap_promote.py
django_python3_ldap/management/commands/ldap_sync_users.py
tests/manage.py
diff --git a/django_python3_ldap/__init__.py b/django_python3_ldap/__init__.py
index ac323d9..3b4743a 100644
--- a/django_python3_ldap/__init__.py
+++ b/django_python3_ldap/__init__.py
@@ -3,4 +3,4 @@ Django LDAP user authentication backend for Python 3.
"""
-__version__ = (0, 12, 0)
+__version__ = (0, 15, 4)
diff --git a/django_python3_ldap/conf.py b/django_python3_ldap/conf.py
index 35811fb..f598759 100644
--- a/django_python3_ldap/conf.py
+++ b/django_python3_ldap/conf.py
@@ -1,6 +1,7 @@
"""
Settings used by django-python3.
"""
+from ssl import PROTOCOL_TLS
from django.conf import settings
@@ -35,7 +36,7 @@ class LazySettings(object):
LDAP_AUTH_URL = LazySetting(
name="LDAP_AUTH_URL",
- default="ldap://localhost:389",
+ default=["ldap://localhost:389"],
)
LDAP_AUTH_USE_TLS = LazySetting(
@@ -43,6 +44,11 @@ class LazySettings(object):
default=False,
)
+ LDAP_AUTH_TLS_VERSION = LazySetting(
+ name="LDAP_AUTH_TLS_VERSION",
+ default=PROTOCOL_TLS,
+ )
+
LDAP_AUTH_SEARCH_BASE = LazySetting(
name="LDAP_AUTH_SEARCH_BASE",
default="ou=people,dc=example,dc=com",
@@ -100,6 +106,11 @@ class LazySettings(object):
default="",
)
+ LDAP_AUTH_TEST_USER_EMAIL = LazySetting(
+ name="LDAP_AUTH_TEST_USER_EMAIL",
+ default=""
+ )
+
LDAP_AUTH_TEST_USER_PASSWORD = LazySetting(
name="LDAP_AUTH_TEST_USER_PASSWORD",
default="",
diff --git a/django_python3_ldap/ldap.py b/django_python3_ldap/ldap.py
index cd8b0ab..a45e5e8 100644
--- a/django_python3_ldap/ldap.py
+++ b/django_python3_ldap/ldap.py
@@ -116,17 +116,27 @@ class Connection(object):
in settings.LDAP_AUTH_USER_LOOKUP_FIELDS.
"""
# Search the LDAP database.
- if self._connection.search(
+ if self.has_user(**kwargs):
+ return self._get_or_create_user(self._connection.response[0])
+ logger.warning("LDAP user lookup failed")
+ return None
+
+ def has_user(self, **kwargs):
+ """
+ Returns True if the user with the given identifier exists.
+
+ The user identifier should be keyword arguments matching the fields
+ in settings.LDAP_AUTH_USER_LOOKUP_FIELDS.
+ """
+ # Search the LDAP database.
+ return self._connection.search(
search_base=settings.LDAP_AUTH_SEARCH_BASE,
search_filter=format_search_filter(kwargs),
search_scope=ldap3.SUBTREE,
attributes=ldap3.ALL_ATTRIBUTES,
get_operational_attributes=True,
size_limit=1,
- ):
- return self._get_or_create_user(self._connection.response[0])
- logger.warning("LDAP user lookup failed")
- return None
+ )
@contextmanager
@@ -150,20 +160,41 @@ def connection(**kwargs):
if kwargs:
password = kwargs.pop("password")
username = format_username(kwargs)
+ # Build server pool
+ server_pool = ldap3.ServerPool(None, ldap3.RANDOM, active=True, exhaust=5)
+ auth_url = settings.LDAP_AUTH_URL
+ if not isinstance(auth_url, list):
+ auth_url = [auth_url]
+ for u in auth_url:
+ # Include SSL / TLS, if requested.
+ server_args = {
+ "allowed_referral_hosts": [("*", True)],
+ "get_info": ldap3.NONE,
+ "connect_timeout": settings.LDAP_AUTH_CONNECT_TIMEOUT,
+ }
+ if settings.LDAP_AUTH_USE_TLS:
+ server_args["tls"] = ldap3.Tls(
+ ciphers="ALL",
+ version=settings.LDAP_AUTH_TLS_VERSION,
+ )
+ server_pool.add(
+ ldap3.Server(
+ u,
+ **server_args,
+ )
+ )
# Connect.
try:
+ connection_args = {
+ "user": username,
+ "password": password,
+ "auto_bind": False,
+ "raise_exceptions": True,
+ "receive_timeout": settings.LDAP_AUTH_RECEIVE_TIMEOUT,
+ }
c = ldap3.Connection(
- ldap3.Server(
- settings.LDAP_AUTH_URL,
- allowed_referral_hosts=[("*", True)],
- get_info=ldap3.NONE,
- connect_timeout=settings.LDAP_AUTH_CONNECT_TIMEOUT,
- ),
- user=username,
- password=password,
- auto_bind=False,
- raise_exceptions=True,
- receive_timeout=settings.LDAP_AUTH_RECEIVE_TIMEOUT,
+ server_pool,
+ **connection_args,
)
except LDAPException as ex:
logger.warning("LDAP connect failed: {ex}".format(ex=ex))
@@ -176,18 +207,22 @@ def connection(**kwargs):
c.start_tls(read_server_info=False)
# Perform initial authentication bind.
c.bind(read_server_info=True)
+ User = get_user_model()
# If the settings specify an alternative username and password for querying, rebind as that.
- if (
- (settings.LDAP_AUTH_CONNECTION_USERNAME or settings.LDAP_AUTH_CONNECTION_PASSWORD) and
- (
- settings.LDAP_AUTH_CONNECTION_USERNAME != username or
- settings.LDAP_AUTH_CONNECTION_PASSWORD != password
+ settings_username = (
+ format_username(
+ {User.USERNAME_FIELD: settings.LDAP_AUTH_CONNECTION_USERNAME}
)
+ if settings.LDAP_AUTH_CONNECTION_USERNAME
+ else None
+ )
+ settings_password = settings.LDAP_AUTH_CONNECTION_PASSWORD
+ if (settings_username or settings_password) and (
+ settings_username != username or settings_password != password
):
- User = get_user_model()
c.rebind(
- user=format_username({User.USERNAME_FIELD: settings.LDAP_AUTH_CONNECTION_USERNAME}),
- password=settings.LDAP_AUTH_CONNECTION_PASSWORD,
+ user=settings_username,
+ password=settings_password,
)
# Return the connection.
logger.info("LDAP connect succeeded")
@@ -208,11 +243,18 @@ def authenticate(*args, **kwargs):
in settings.LDAP_AUTH_USER_LOOKUP_FIELDS, plus a `password` argument.
"""
password = kwargs.pop("password", None)
+ auth_user_lookup_fields = frozenset(settings.LDAP_AUTH_USER_LOOKUP_FIELDS)
+ ldap_kwargs = {
+ key: value for (key, value) in kwargs.items()
+ if key in auth_user_lookup_fields
+ }
+
# Check that this is valid login data.
- if not password or frozenset(kwargs.keys()) != frozenset(settings.LDAP_AUTH_USER_LOOKUP_FIELDS):
+ if not password or frozenset(ldap_kwargs.keys()) != auth_user_lookup_fields:
return None
+
# Connect to LDAP.
- with connection(password=password, **kwargs) as c:
+ with connection(password=password, **ldap_kwargs) as c:
if c is None:
return None
- return c.get_user(**kwargs)
+ return c.get_user(**ldap_kwargs)
diff --git a/django_python3_ldap/management/commands/ldap_clean_users.py b/django_python3_ldap/management/commands/ldap_clean_users.py
new file mode 100644
index 0000000..2b5cdba
--- /dev/null
+++ b/django_python3_ldap/management/commands/ldap_clean_users.py
@@ -0,0 +1,112 @@
+from django.contrib.auth import get_user_model
+from django.core.management.base import BaseCommand, CommandError
+from django.db import transaction
+from django.db.models import ProtectedError
+
+from django_python3_ldap import ldap
+from django_python3_ldap.conf import settings
+from django_python3_ldap.utils import group_lookup_args
+
+
+class Command(BaseCommand):
+
+ help = "Remove local user models for users not find anymore in the remote LDAP authentication server."
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '-p',
+ '--purge',
+ action='store_true',
+ help='Purge instead of deactive local user models'
+ )
+ parser.add_argument(
+ 'lookups',
+ nargs='*',
+ type=str,
+ help='A list of lookup values, matching the fields specified in LDAP_AUTH_USER_LOOKUP_FIELDS. '
+ 'If this is not provided then ALL users are concerned.'
+ )
+ parser.add_argument(
+ '--superuser',
+ action='store_true',
+ help='Handle superuser (by default, superusers are excluded)'
+ )
+ parser.add_argument(
+ '--staff',
+ action='store_true',
+ help='Handle staff user (by default,staff users are excluded)'
+ )
+
+ @staticmethod
+ def _iter_local_users(User, lookups, superuser, staff):
+ """
+ Iterates over local users. If the list of lookups is empty, then all users are returned.
+ However, if lookups are provided, User.object.get is used to clean each user found using the lookups.
+ Exclude or not superuser and or staff user.
+ """
+
+ if len(lookups) < 1:
+ for user in User.objects.filter(is_superuser=superuser,
+ is_staff=staff):
+ yield user
+ else:
+ for lookup in group_lookup_args(*lookups):
+ try:
+ yield User.objects.get(**lookup,
+ is_superuser=superuser,
+ is_staff=staff)
+ except User.DoesNotExist:
+ raise CommandError("Could not find user with lookup : {lookup}".format(
+ lookup=lookup,
+ ))
+
+ @staticmethod
+ def _remove(user, purge):
+ """
+ Deactivate or purge a given local user
+ """
+ if purge:
+ # Delete local user
+ try:
+ user.delete()
+ except ProtectedError as e:
+ raise CommandError("Could not purge user {user} : {e}".format(
+ user=user,
+ e=e
+ ))
+ else:
+ # Deactivate local user
+ user.is_active = False
+ user.save()
+
+ @transaction.atomic()
+ def handle(self, *args, **kwargs):
+ verbosity = int(kwargs.get("verbosity", 1))
+ purge = kwargs.get('purge', False)
+ lookups = kwargs.get('lookups', [])
+ superuser = kwargs.get('superuser', False)
+ staff = kwargs.get('staff', False)
+ User = get_user_model()
+ auth_kwargs = {
+ User.USERNAME_FIELD: settings.LDAP_AUTH_CONNECTION_USERNAME,
+ 'password': settings.LDAP_AUTH_CONNECTION_PASSWORD
+ }
+ with ldap.connection(**auth_kwargs) as connection:
+ if connection is None:
+ raise CommandError("Could not connect to LDAP server")
+ for user in self._iter_local_users(User, lookups, superuser, staff):
+ # For each local users
+ # Check if user still exists
+ user_kwargs = {
+ User.USERNAME_FIELD: getattr(user, User.USERNAME_FIELD)
+ }
+ if connection.has_user(**user_kwargs):
+ # User still exists on LDAP side
+ continue
+ # Clean user
+ self._remove(user, purge)
+ if verbosity >= 1:
+ self.stdout.write("{action} {user}".format(
+ action=('Purged' if purge else 'Deactivated'),
+ user=user,
+ ))
diff --git a/django_python3_ldap/management/commands/ldap_sync_users.py b/django_python3_ldap/management/commands/ldap_sync_users.py
index d184c3a..29a53e4 100644
--- a/django_python3_ldap/management/commands/ldap_sync_users.py
+++ b/django_python3_ldap/management/commands/ldap_sync_users.py
@@ -4,15 +4,39 @@ from django.db import transaction
from django_python3_ldap import ldap
from django_python3_ldap.conf import settings
+from django_python3_ldap.utils import group_lookup_args
class Command(BaseCommand):
- help = "Creates local user models for all users found in the remote LDAP authentication server."
+ help = "Creates local user models for users found in the remote LDAP authentication server."
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'lookups',
+ nargs='*',
+ type=str,
+ help='A list of lookup values, matching the fields specified in LDAP_AUTH_USER_LOOKUP_FIELDS. '
+ 'If this is not provided then ALL users are synced.'
+ )
+
+ @staticmethod
+ def _iter_synced_users(connection, lookups):
+ """
+ Iterates over synced users. If the list of lookups is empty, then all users are synced using iter_users.
+ However, if lookups are provided, get_user is used to sync each user found using the lookups.
+ """
+ if len(lookups) < 1:
+ for user in connection.iter_users():
+ yield user
+ else:
+ for lookup in group_lookup_args(*lookups):
+ yield connection.get_user(**lookup)
@transaction.atomic()
def handle(self, *args, **kwargs):
verbosity = int(kwargs.get("verbosity", 1))
+ lookups = kwargs.get('lookups', [])
User = get_user_model()
auth_kwargs = {
User.USERNAME_FIELD: settings.LDAP_AUTH_CONNECTION_USERNAME,
@@ -21,7 +45,7 @@ class Command(BaseCommand):
with ldap.connection(**auth_kwargs) as connection:
if connection is None:
raise CommandError("Could not connect to LDAP server")
- for user in connection.iter_users():
+ for user in self._iter_synced_users(connection, lookups):
if verbosity >= 1:
self.stdout.write("Synced {user}".format(
user=user,
diff --git a/django_python3_ldap/tests.py b/django_python3_ldap/tests.py
index 91e53b7..dc7cbe6 100644
--- a/django_python3_ldap/tests.py
+++ b/django_python3_ldap/tests.py
@@ -4,7 +4,7 @@ from __future__ import unicode_literals
from unittest import skipUnless, skip
from io import StringIO
-from django.test import TestCase
+from django.test import TestCase, override_settings
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
from django.conf import settings as django_settings
@@ -57,6 +57,20 @@ class TestLdap(TestCase):
)
self.assertEqual(user, None)
+ def testHasUserKwargsSuccess(self):
+ with connection() as c:
+ exist = c.has_user(
+ username=settings.LDAP_AUTH_TEST_USER_USERNAME,
+ )
+ self.assertEqual(exist, True)
+
+ def testHasUserKwargsIncorrectUsername(self):
+ with connection() as c:
+ exist = c.has_user(
+ username="bad" + settings.LDAP_AUTH_TEST_USER_USERNAME,
+ )
+ self.assertEqual(exist, False)
+
# Authentication tests.
def testAuthenticateUserSuccess(self):
@@ -81,6 +95,15 @@ class TestLdap(TestCase):
)
self.assertEqual(user, None)
+ def testAuthenticateWithAdditonalKwargsUserSuccess(self):
+ user = authenticate(
+ username=settings.LDAP_AUTH_TEST_USER_USERNAME,
+ password=settings.LDAP_AUTH_TEST_USER_PASSWORD,
+ another_kwarg="whatever",
+ )
+ self.assertIsInstance(user, User)
+ self.assertEqual(user.username, settings.LDAP_AUTH_TEST_USER_USERNAME)
+
def testRepeatedUserAuthenticationDoestRecreateUsers(self):
user_1 = authenticate(
username=settings.LDAP_AUTH_TEST_USER_USERNAME,
@@ -132,6 +155,20 @@ class TestLdap(TestCase):
call_command("ldap_sync_users", verbosity=0)
self.assertGreater(User.objects.count(), 0)
+ def testSyncUserWithLookup(self):
+ call_command("ldap_sync_users", settings.LDAP_AUTH_TEST_USER_USERNAME, verbosity=0)
+ self.assertEqual(User.objects.filter(username=settings.LDAP_AUTH_TEST_USER_USERNAME).count(), 1)
+
+ @override_settings(LDAP_AUTH_USER_LOOKUP_FIELDS=('username', 'email'))
+ def testSyncUserWithMultipleLookups(self):
+ call_command(
+ "ldap_sync_users",
+ settings.LDAP_AUTH_TEST_USER_USERNAME,
+ settings.LDAP_AUTH_TEST_USER_EMAIL,
+ verbosity=0
+ )
+ self.assertEqual(User.objects.filter(username=settings.LDAP_AUTH_TEST_USER_USERNAME).count(), 1)
+
def testSyncUsersCommandOutput(self):
out = StringIO()
call_command("ldap_sync_users", verbosity=1, stdout=out)
@@ -224,3 +261,154 @@ class TestLdap(TestCase):
with self.settings(LDAP_AUTH_SYNC_USER_RELATIONS='django.contrib.auth.get_user_model'):
self.assertTrue(callable(import_func(settings.LDAP_AUTH_SYNC_USER_RELATIONS)))
+
+ def testCleanUsersDeactivate(self):
+ """
+ ldap_clean_users management command test
+ """
+ from django.contrib.auth import get_user_model
+ User = get_user_model()
+ _username = "nonldap{user}".format(user=settings.LDAP_AUTH_TEST_USER_USERNAME)
+ user = User.objects.create_user(
+ _username,
+ "nonldap{mail}".format(mail=settings.LDAP_AUTH_TEST_USER_EMAIL),
+ settings.LDAP_AUTH_TEST_USER_PASSWORD)
+ user.save()
+ user_count_1 = User.objects.count()
+ self.assertEqual(User.objects.get(username=_username).is_active, True)
+ call_command("ldap_clean_users", verbosity=0)
+ user_count_2 = User.objects.count()
+ self.assertEqual(user_count_1, user_count_2)
+ self.assertEqual(User.objects.get(username=_username).is_active, False)
+
+ """
+ Test with lookup
+ """
+ # Reactivate user
+ user = User.objects.get(username=_username)
+ user.is_active = True
+ user.save()
+ # Create second user
+ _usernameLookup = "nonldaplookup{user}".format(user=settings.LDAP_AUTH_TEST_USER_USERNAME)
+ user = User.objects.create_user(
+ _usernameLookup,
+ "nonldaplookup{mail}".format(mail=settings.LDAP_AUTH_TEST_USER_EMAIL),
+ settings.LDAP_AUTH_TEST_USER_PASSWORD)
+ user.save()
+ user_count_1 = User.objects.count()
+ self.assertEqual(User.objects.get(username=_usernameLookup).is_active, True)
+ # Clean second user
+ call_command("ldap_clean_users", _usernameLookup, verbosity=0)
+ user_count_2 = User.objects.count()
+ self.assertEqual(user_count_1, user_count_2)
+ self.assertEqual(User.objects.get(username=_usernameLookup).is_active, False)
+ self.assertEqual(User.objects.get(username=_username).is_active, True)
+ # Reactivate second user
+ user = User.objects.get(username=_usernameLookup)
+ user.is_active = True
+ user.save()
+ # Clean first user
+ call_command("ldap_clean_users", _username, verbosity=0)
+ self.assertEqual(User.objects.get(username=_username).is_active, False)
+ self.assertEqual(User.objects.get(username=_usernameLookup).is_active, True)
+ # Lookup a non existing user (raise a CommandError)
+ with self.assertRaises(CommandError):
+ call_command("ldap_clean_users", 'doesnonexist', verbosity=0)
+
+ """
+ Test with superuser
+ """
+ # Reactivate first user and promote to superuser
+ user = User.objects.get(username=_username)
+ user.is_active = True
+ user.is_superuser = True
+ user.save()
+ # Reactivate second user
+ user = User.objects.get(username=_usernameLookup)
+ user.is_active = True
+ user.save()
+ call_command("ldap_clean_users", superuser=False, verbosity=0)
+ self.assertEqual(User.objects.get(username=_username).is_active, True)
+ self.assertEqual(User.objects.get(username=_usernameLookup).is_active, False)
+ call_command("ldap_clean_users", superuser=True, verbosity=0)
+ self.assertEqual(User.objects.get(username=_username).is_active, False)
+
+ """
+ Test with staff user
+ """
+ # Reactivate first user and promote to staff
+ user = User.objects.get(username=_username)
+ user.is_active = True
+ user.is_superuser = False
+ user.is_staff = True
+ user.save()
+ # Reactivate second user
+ user = User.objects.get(username=_usernameLookup)
+ user.is_active = True
+ user.save()
+ call_command("ldap_clean_users", staff=False, verbosity=0)
+ self.assertEqual(User.objects.get(username=_username).is_active, True)
+ self.assertEqual(User.objects.get(username=_usernameLookup).is_active, False)
+ call_command("ldap_clean_users", staff=True, verbosity=0)
+ self.assertEqual(User.objects.get(username=_username).is_active, False)
+
+ def testCleanUsersPurge(self):
+ """
+ ldap_clean_users management command test with purge argument
+ """
+ from django.contrib.auth import get_user_model
+ User = get_user_model()
+ user = User.objects.create_user(
+ "nonldap{user}".format(user=settings.LDAP_AUTH_TEST_USER_USERNAME),
+ "nonldap{mail}".format(mail=settings.LDAP_AUTH_TEST_USER_EMAIL),
+ settings.LDAP_AUTH_TEST_USER_PASSWORD)
+ user.save()
+ user_count_1 = User.objects.count()
+ call_command("ldap_clean_users", verbosity=0, purge=True)
+ user_count_2 = User.objects.count()
+ self.assertGreater(user_count_1, user_count_2)
+
+ def testCleanUsersCommandOutput(self):
+ # Test without purge
+ out = StringIO()
+ from django.contrib.auth import get_user_model
+ User = get_user_model()
+ user = User.objects.create_user(
+ "nonldap{user}".format(user=settings.LDAP_AUTH_TEST_USER_USERNAME),
+ "nonldap{mail}".format(mail=settings.LDAP_AUTH_TEST_USER_EMAIL),
+ settings.LDAP_AUTH_TEST_USER_PASSWORD)
+ user.save()
+ call_command("ldap_clean_users", stdout=out, verbosity=1)
+ rows = out.getvalue().split("\n")[:-1]
+ self.assertEqual(len(rows), 1)
+ for row in rows:
+ self.assertRegex(row, r'^Deactivated ')
+ # Reset for next test
+ user.delete()
+ out.truncate(0)
+ out.seek(0)
+ # Test with purge
+ user = User.objects.create_user(
+ "nonldap{user}".format(user=settings.LDAP_AUTH_TEST_USER_USERNAME),
+ "nonldap{mail}".format(mail=settings.LDAP_AUTH_TEST_USER_EMAIL),
+ settings.LDAP_AUTH_TEST_USER_PASSWORD)
+ user.save()
+ call_command("ldap_clean_users", stdout=out, verbosity=1, purge=True)
+ rows = out.getvalue().split("\n")[:-1]
+ self.assertEqual(len(rows), 1)
+ for row in rows:
+ self.assertRegex(row, r'^Purged ')
+
+ def testReCleanUsersDoesntRecreateUsers(self):
+ from django.contrib.auth import get_user_model
+ User = get_user_model()
+ user = User.objects.create_user(
+ "nonldap{user}".format(user=settings.LDAP_AUTH_TEST_USER_USERNAME),
+ "nonldap{mail}".format(mail=settings.LDAP_AUTH_TEST_USER_EMAIL),
+ settings.LDAP_AUTH_TEST_USER_PASSWORD)
+ user.save()
+ call_command("ldap_clean_users", verbosity=0, purge=True)
+ user_count_1 = User.objects.count()
+ call_command("ldap_clean_users", verbosity=0, purge=True)
+ user_count_2 = User.objects.count()
+ self.assertEqual(user_count_1, user_count_2)
diff --git a/django_python3_ldap/utils.py b/django_python3_ldap/utils.py
index e60b42e..44f5053 100644
--- a/django_python3_ldap/utils.py
+++ b/django_python3_ldap/utils.py
@@ -4,7 +4,13 @@ Some useful LDAP utilities.
import re
import binascii
-from django.utils.encoding import force_text
+import itertools
+
+try:
+ from django.utils.encoding import force_str
+except ImportError:
+ from django.utils.encoding import force_text as force_str
+
from django.utils.module_loading import import_string
from django_python3_ldap.conf import settings
@@ -25,8 +31,8 @@ def clean_ldap_name(name):
"""
return re.sub(
r'[^a-zA-Z0-9 _\-.@:*]',
- lambda c: "\\" + force_text(binascii.hexlify(c.group(0).encode("latin-1", errors="ignore"))).upper(),
- force_text(name),
+ lambda c: "\\" + force_str(binascii.hexlify(c.group(0).encode("latin-1", errors="ignore"))).upper(),
+ force_str(name),
)
@@ -121,3 +127,19 @@ def format_search_filters(ldap_fields):
for field_name, field_value
in ldap_fields.items()
]
+
+
+def group_lookup_args(*args):
+ """
+ Yields the given series of arguments as chunks, formatted as dictionaries, which represent field lookups
+ according to the LDAP_AUTH_USER_LOOKUP_FIELDS setting.
+
+ Based on the itertools grouper recipe: https://docs.python.org/3/library/itertools.html#itertools-recipes
+ """
+ fields_len = len(settings.LDAP_AUTH_USER_LOOKUP_FIELDS)
+ fields = [iter(args)] * fields_len
+ for chunk in itertools.zip_longest(*fields, fillvalue=None):
+ lookup = {}
+ for i in range(fields_len):
+ lookup[settings.LDAP_AUTH_USER_LOOKUP_FIELDS[i]] = chunk[i]
+ yield lookup
diff --git a/setup.py b/setup.py
index 6a0ad6d..0dd82fc 100644
--- a/setup.py
+++ b/setup.py
@@ -6,11 +6,16 @@ from django_python3_ldap import __version__
version_str = ".".join(str(n) for n in __version__)
+with open("README.rst", "r", encoding="utf-8") as fh:
+ long_description = fh.read()
+
+
setup(
name="django-python3-ldap",
version=version_str,
license="BSD",
description="Django LDAP user authentication backend for Python 3.",
+ long_description=long_description,
author="Dave Hall",
author_email="dave@etianen.com",
url="https://github.com/etianen/django-python3-ldap",
diff --git a/tests/django_python3_ldap_test/settings.py b/tests/django_python3_ldap_test/settings.py
index 616ac10..f444aac 100644
--- a/tests/django_python3_ldap_test/settings.py
+++ b/tests/django_python3_ldap_test/settings.py
@@ -29,7 +29,7 @@ ALLOWED_HOSTS = []
# LDAP auth settings.
-LDAP_AUTH_URL = "ldap://ldap.forumsys.com:389"
+LDAP_AUTH_URL = ["ldap://ldap.forumsys.com:389"]
LDAP_AUTH_SEARCH_BASE = "dc=example,dc=com"
@@ -42,6 +42,8 @@ AUTHENTICATION_BACKENDS = (
LDAP_AUTH_TEST_USER_USERNAME = "tesla"
+LDAP_AUTH_TEST_USER_EMAIL = "tesla@ldap.forumsys.com"
+
LDAP_AUTH_TEST_USER_PASSWORD = "password"