././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1625834145.9746225 django-python3-ldap-0.12.0/0000755000175100001710000000000000000000000014714 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/CHANGELOG.rst0000644000175100001710000000637200000000000016745 0ustar00runnerdockerdjango-python3-ldap changelog ============================= 0.12.0 ------ - Added ``connection`` and ``dn`` parameters to the ``LDAP_AUTH_SYNC_USER_RELATIONS`` callback (@nigelm). See: https://github.com/etianen/django-python3-ldap#sync-user-relations 0.11.4 ------ - Fix error when using ldap auth backend with other authentication backends (@leavest). 0.11.3 ------ - Django 3.0 compatibility. - Improved cleanup of LDAP connections that fail initial bind. - Updated `ldap3` and `pyasn1` dependencies. - Dropped Python 2.7 support. 0.11.2 ------ - Python 3.7 support. 0.11.1 ------ - Added `LDAP_AUTH_CONNECT_TIMEOUT` and `LDAP_AUTH_RECEIVE_TIMEOUT` settings (@alizain). 0.11.0 ------ - Support added for User models with a ``USERNAME_FIELD`` other than ``username`` (@audiolion). - Fixed bug with `pyasn1` version (@etianen). 0.10.0 ------ - If `settings.LDAP_AUTH_CONNECTION_USERNAME` or `settings.LDAP_AUTH_CONNECTION_PASSWORD` is set, then it will be used to query the LDAP database for user details during authentication. - Added `django_python3_ldap.utils.format_username_active_directory_principal` option for formatting user-principal-name (e.g. "user@domain.com") Active Directory usernames. 0.9.14 ------ - Django 1.11 compatibility (@aritas1). 0.9.13 ------ - Fixed issue with LDAP servers returning attributes that are not a list (@etianen). 0.9.12 ------ - Update for compatibility with ldap3 2.x release (@etianen). 0.9.11 ------ - Added support for LDAP referrals (@etianen). - Fixed issue with missing LDAP attributes (@smills2929). - Tweaks and bugfixes (@smills2929). 0.9.10 ------ - Django 1.10 compatibility (@etianen, @frennkie). - Added operational LDAP attributes to ``iter_users`` (@frennkie). - Dropped Django 1.7 compatibility (@etianen). - Dropped Python 3.2 and 3.3 compatibility (@etianen). 0.9.9 ----- - Fixing anonymous bind in some LDAP servers (@etianen). 0.9.8 ----- - Fixing security vulnerability allowing users to authenticate with a valid username but with an empty password if anonymous authentication is allowed on the LDAP server (Petros Moisiadis). - Fixing sync_users command for Microsoft Active Directory (@jjagielka). 0.9.7 ----- - Ability to configure extra filters for user lookup using LDAP_AUTH_SEARCH_FILTERS (@etianen, @Ernest0x). - Support for Active Directory LDAP servers (@etianen, @brandonusher). - Python 2.7 compatibility (@NotSqrt). - Ability to configure relations on loaded user models using LDAP_AUTH_SYNC_USER_RELATIONS (@mnach). - Switched to specifying paths to functions using dotted string paths in settings (@mnach). 0.9.6 ----- - Added settings option for a username and password to be specified incase anonymous user queries are not allowed (@brandonusher). 0.9.5 ----- - Fixing security vulnerability where username and password could be transmitted in plain text before starting TLS (reported by Weitzhofer Bernhard). 0.9.4 ----- - Fixing broken ldap3 dependency (@levisaya). - Honoring LDAP_AUTH_CLEAN_USER_DATA setting (@etianen, @akaariai). 0.9.3 ----- - Fixing broken python3-ldap dependency (@ricard33). 0.9.2 ----- - Added setting for initiating TLS on connection (@saraheiting). 0.9.1 ----- - Adding ldap_promote management command. 0.9.0 ----- - First production release. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/LICENSE0000644000175100001710000000276400000000000015732 0ustar00runnerdockerCopyright (c) 2015, David Hall. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of David Hall nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/MANIFEST.in0000644000175100001710000000012600000000000016451 0ustar00runnerdockerinclude LICENSE include README.rst include CHANGELOG.rst recursive-include tests *.py ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1625834145.9746225 django-python3-ldap-0.12.0/PKG-INFO0000644000175100001710000000134300000000000016012 0ustar00runnerdockerMetadata-Version: 1.1 Name: django-python3-ldap Version: 0.12.0 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 Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Framework :: Django ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/README.rst0000644000175100001710000002132400000000000016405 0ustar00runnerdockerdjango-python3-ldap =================== **django-python3-ldap** provides a Django LDAP user authentication backend. 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 (see Available settings, below). 5. Optionally, run ``./manage.py ldap_sync_users`` to perform an initial sync of LDAP users. 6. Optionally, run ``./manage.py ldap_promote `` 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. LDAP_AUTH_URL = "ldap://localhost:389" # Initiate TLS on connection. LDAP_AUTH_USE_TLS = False # 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` command 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) 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 the LDAP server, 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 `_. More information ---------------- The django-python3-ldap project was developed by Dave Hall. You can get the code from the `django-python3-ldap project site `_. 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 `_ - `Twitter `_ - `Google Profile `_ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1625834145.9746225 django-python3-ldap-0.12.0/django_python3_ldap/0000755000175100001710000000000000000000000020642 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/django_python3_ldap/__init__.py0000644000175100001710000000013100000000000022746 0ustar00runnerdocker""" Django LDAP user authentication backend for Python 3. """ __version__ = (0, 12, 0) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/django_python3_ldap/auth.py0000644000175100001710000000076000000000000022160 0ustar00runnerdocker""" Django authentication backend. """ from django.contrib.auth.backends import ModelBackend from django_python3_ldap import ldap class LDAPBackend(ModelBackend): """ An authentication backend that delegates to an LDAP server. User models authenticated with LDAP are created on the fly, and syncronised with the LDAP credentials. """ supports_inactive_user = False def authenticate(self, *args, **kwargs): return ldap.authenticate(*args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/django_python3_ldap/conf.py0000644000175100001710000000577100000000000022153 0ustar00runnerdocker""" Settings used by django-python3. """ from django.conf import settings class LazySetting(object): """ A proxy to a named Django setting. """ def __init__(self, name, default=None): self.name = name self.default = default def __get__(self, obj, cls): if obj is None: return self return getattr(obj._settings, self.name, self.default) class LazySettings(object): """ A proxy to ldap-specific django settings. Settings are resolved at runtime, allowing tests to change settings at runtime. """ def __init__(self, settings): self._settings = settings LDAP_AUTH_URL = LazySetting( name="LDAP_AUTH_URL", default="ldap://localhost:389", ) LDAP_AUTH_USE_TLS = LazySetting( name="LDAP_AUTH_USE_TLS", default=False, ) LDAP_AUTH_SEARCH_BASE = LazySetting( name="LDAP_AUTH_SEARCH_BASE", default="ou=people,dc=example,dc=com", ) LDAP_AUTH_OBJECT_CLASS = LazySetting( name="LDAP_AUTH_OBJECT_CLASS", default="inetOrgPerson", ) LDAP_AUTH_USER_FIELDS = LazySetting( name="LDAP_AUTH_USER_FIELDS", default={ "username": "uid", "first_name": "givenName", "last_name": "sn", "email": "mail", }, ) LDAP_AUTH_USER_LOOKUP_FIELDS = LazySetting( name="LDAP_AUTH_USER_LOOKUP_FIELDS", default=( "username", ), ) LDAP_AUTH_CLEAN_USER_DATA = LazySetting( name="LDAP_AUTH_CLEAN_USER_DATA", default="django_python3_ldap.utils.clean_user_data", ) LDAP_AUTH_FORMAT_SEARCH_FILTERS = LazySetting( name="LDAP_AUTH_FORMAT_SEARCH_FILTERS", default="django_python3_ldap.utils.format_search_filters", ) LDAP_AUTH_SYNC_USER_RELATIONS = LazySetting( name="LDAP_AUTH_SYNC_USER_RELATIONS", default="django_python3_ldap.utils.sync_user_relations", ) LDAP_AUTH_FORMAT_USERNAME = LazySetting( name="LDAP_AUTH_FORMAT_USERNAME", default="django_python3_ldap.utils.format_username_openldap", ) LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN = LazySetting( name="LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN", default=None, ) LDAP_AUTH_TEST_USER_USERNAME = LazySetting( name="LDAP_AUTH_TEST_USER_USERNAME", default="", ) LDAP_AUTH_TEST_USER_PASSWORD = LazySetting( name="LDAP_AUTH_TEST_USER_PASSWORD", default="", ) LDAP_AUTH_CONNECTION_USERNAME = LazySetting( name="LDAP_AUTH_CONNECTION_USERNAME", default=None, ) LDAP_AUTH_CONNECTION_PASSWORD = LazySetting( name="LDAP_AUTH_CONNECTION_PASSWORD", default=None, ) LDAP_AUTH_CONNECT_TIMEOUT = LazySetting( name="LDAP_AUTH_CONNECT_TIMEOUT", default=None ) LDAP_AUTH_RECEIVE_TIMEOUT = LazySetting( name="LDAP_AUTH_RECEIVE_TIMEOUT", default=None ) settings = LazySettings(settings) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/django_python3_ldap/ldap.py0000644000175100001710000001635500000000000022146 0ustar00runnerdocker""" Low-level LDAP hooks. """ import ldap3 from ldap3.core.exceptions import LDAPException import logging from inspect import getfullargspec from contextlib import contextmanager from django.contrib.auth import get_user_model from django_python3_ldap.conf import settings from django_python3_ldap.utils import import_func, format_search_filter logger = logging.getLogger(__name__) class Connection(object): """ A connection to an LDAP server. """ def __init__(self, connection): """ Creates the LDAP connection. No need to call this manually, the `connection()` context manager handles initialization. """ self._connection = connection def _get_or_create_user(self, user_data): """ Returns a Django user for the given LDAP user data. If the user does not exist, then it will be created. """ attributes = user_data.get("attributes") if attributes is None: logger.warning("LDAP user attributes empty") return None User = get_user_model() # Create the user data. user_fields = { field_name: ( attributes[attribute_name][0] if isinstance(attributes[attribute_name], (list, tuple)) else attributes[attribute_name] ) for field_name, attribute_name in settings.LDAP_AUTH_USER_FIELDS.items() if attribute_name in attributes } user_fields = import_func(settings.LDAP_AUTH_CLEAN_USER_DATA)(user_fields) # Create the user lookup. user_lookup = { field_name: user_fields.pop(field_name, "") for field_name in settings.LDAP_AUTH_USER_LOOKUP_FIELDS } # Update or create the user. user, created = User.objects.update_or_create( defaults=user_fields, **user_lookup ) # If the user was created, set them an unusable password. if created: user.set_unusable_password() user.save() # Update relations sync_user_relations_func = import_func(settings.LDAP_AUTH_SYNC_USER_RELATIONS) sync_user_relations_arginfo = getfullargspec(sync_user_relations_func) args = {} # additional keyword arguments for argname in sync_user_relations_arginfo.kwonlyargs: if argname == "connection": args["connection"] = self._connection elif argname == "dn": args["dn"] = user_data.get("dn") else: raise TypeError(f"Unknown kw argument {argname} in signature for LDAP_AUTH_SYNC_USER_RELATIONS") # call sync_user_relations_func() with original args plus supported named extras sync_user_relations_func(user, attributes, **args) # All done! logger.info("LDAP user lookup succeeded") return user def iter_users(self): """ Returns an iterator of Django users that correspond to users in the LDAP database. """ paged_entries = self._connection.extend.standard.paged_search( search_base=settings.LDAP_AUTH_SEARCH_BASE, search_filter=format_search_filter({}), search_scope=ldap3.SUBTREE, attributes=ldap3.ALL_ATTRIBUTES, get_operational_attributes=True, paged_size=30, ) return filter(None, ( self._get_or_create_user(entry) for entry in paged_entries if entry["type"] == "searchResEntry" )) def get_user(self, **kwargs): """ Returns the user with the given identifier. The user identifier should be keyword arguments matching the fields in settings.LDAP_AUTH_USER_LOOKUP_FIELDS. """ # Search the LDAP database. if 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 def connection(**kwargs): """ Creates and returns a connection to the LDAP server. The user identifier, if given, should be keyword arguments matching the fields in settings.LDAP_AUTH_USER_LOOKUP_FIELDS, plus a `password` argument. """ # Format the DN for the username. format_username = import_func(settings.LDAP_AUTH_FORMAT_USERNAME) kwargs = { key: value for key, value in kwargs.items() if value } username = None password = None if kwargs: password = kwargs.pop("password") username = format_username(kwargs) # Connect. try: 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, ) except LDAPException as ex: logger.warning("LDAP connect failed: {ex}".format(ex=ex)) yield None return # Configure. try: # Start TLS, if requested. if settings.LDAP_AUTH_USE_TLS: c.start_tls(read_server_info=False) # Perform initial authentication bind. c.bind(read_server_info=True) # 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 ) ): User = get_user_model() c.rebind( user=format_username({User.USERNAME_FIELD: settings.LDAP_AUTH_CONNECTION_USERNAME}), password=settings.LDAP_AUTH_CONNECTION_PASSWORD, ) # Return the connection. logger.info("LDAP connect succeeded") yield Connection(c) except LDAPException as ex: logger.warning("LDAP bind failed: {ex}".format(ex=ex)) yield None finally: c.unbind() def authenticate(*args, **kwargs): """ Authenticates with the LDAP server, and returns the corresponding Django user instance. The user identifier should be keyword arguments matching the fields in settings.LDAP_AUTH_USER_LOOKUP_FIELDS, plus a `password` argument. """ password = kwargs.pop("password", None) # Check that this is valid login data. if not password or frozenset(kwargs.keys()) != frozenset(settings.LDAP_AUTH_USER_LOOKUP_FIELDS): return None # Connect to LDAP. with connection(password=password, **kwargs) as c: if c is None: return None return c.get_user(**kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1625834145.9746225 django-python3-ldap-0.12.0/django_python3_ldap/management/0000755000175100001710000000000000000000000022756 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/django_python3_ldap/management/__init__.py0000644000175100001710000000000000000000000025055 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1625834145.9746225 django-python3-ldap-0.12.0/django_python3_ldap/management/commands/0000755000175100001710000000000000000000000024557 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/django_python3_ldap/management/commands/__init__.py0000644000175100001710000000000000000000000026656 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/django_python3_ldap/management/commands/ldap_promote.py0000644000175100001710000000233700000000000027623 0ustar00runnerdockerfrom django.core.management.base import BaseCommand, CommandError from django.db import transaction from django.contrib.auth import get_user_model class Command(BaseCommand): help = "Promotes the named users to an admin superuser." def add_arguments(self, parser): super(Command, self).add_arguments(parser) parser.add_argument( "usernames", metavar="usernames", nargs="*", help="Usernames to promote to admin superuser.", ) @transaction.atomic() def handle(self, **kwargs): verbosity = kwargs["verbosity"] User = get_user_model() for username in kwargs["usernames"]: try: user = User.objects.get(username=username) except User.DoesNotExist: raise CommandError("User with username {username} does not exist".format( username=username, )) else: user.is_staff = True user.is_superuser = True user.save() if verbosity >= 1: self.stdout.write("Promoted {user} to admin superuser".format( user=user, )) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/django_python3_ldap/management/commands/ldap_sync_users.py0000644000175100001710000000203400000000000030325 0ustar00runnerdockerfrom django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError from django.db import transaction from django_python3_ldap import ldap from django_python3_ldap.conf import settings class Command(BaseCommand): help = "Creates local user models for all users found in the remote LDAP authentication server." @transaction.atomic() def handle(self, *args, **kwargs): verbosity = int(kwargs.get("verbosity", 1)) 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 connection.iter_users(): if verbosity >= 1: self.stdout.write("Synced {user}".format( user=user, )) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/django_python3_ldap/tests.py0000644000175100001710000002202500000000000022357 0ustar00runnerdocker# encoding=utf-8 from __future__ import unicode_literals from unittest import skipUnless, skip from io import StringIO from django.test import TestCase from django.contrib.auth import authenticate from django.contrib.auth.models import User from django.conf import settings as django_settings from django.core.management import call_command, CommandError from django_python3_ldap.conf import settings from django_python3_ldap.ldap import connection from django_python3_ldap.utils import clean_ldap_name, import_func @skipUnless(settings.LDAP_AUTH_TEST_USER_USERNAME, "No settings.LDAP_AUTH_TEST_USER_USERNAME supplied.") @skipUnless(settings.LDAP_AUTH_TEST_USER_PASSWORD, "No settings.LDAP_AUTH_TEST_USER_PASSWORD supplied.") @skipUnless(settings.LDAP_AUTH_USER_LOOKUP_FIELDS == ("username",), "Cannot test using custom lookup fields.") @skipUnless(django_settings.AUTH_USER_MODEL == "auth.User", "Cannot test using a custom user model.") class TestLdap(TestCase): def setUp(self): super(TestLdap, self).setUp() User.objects.all().delete() # Lazy settings tests. def testLazySettingsInstanceLookup(self): self.assertTrue(settings.LDAP_AUTH_TEST_USER_USERNAME) def testLazySettingsClassLookup(self): self.assertEqual(settings.__class__.LDAP_AUTH_TEST_USER_USERNAME.name, "LDAP_AUTH_TEST_USER_USERNAME") self.assertEqual(settings.__class__.LDAP_AUTH_TEST_USER_USERNAME.default, "") # Utils tests. def testCleanLdapName(self): self.assertEqual(clean_ldap_name("foo@bar.com"), r'foo@bar.com') self.assertEqual(clean_ldap_name("café"), r'caf\E9') # LDAP tests. def testGetUserKwargsSuccess(self): with connection() as c: user = c.get_user( username=settings.LDAP_AUTH_TEST_USER_USERNAME, ) self.assertIsInstance(user, User) self.assertEqual(user.username, settings.LDAP_AUTH_TEST_USER_USERNAME) def testGetUserKwargsIncorrectUsername(self): with connection() as c: user = c.get_user( username="bad" + settings.LDAP_AUTH_TEST_USER_USERNAME, ) self.assertEqual(user, None) # Authentication tests. def testAuthenticateUserSuccess(self): user = authenticate( username=settings.LDAP_AUTH_TEST_USER_USERNAME, password=settings.LDAP_AUTH_TEST_USER_PASSWORD, ) self.assertIsInstance(user, User) self.assertEqual(user.username, settings.LDAP_AUTH_TEST_USER_USERNAME) def testAuthenticateUserBadUsername(self): user = authenticate( username="bad" + settings.LDAP_AUTH_TEST_USER_USERNAME, password=settings.LDAP_AUTH_TEST_USER_PASSWORD, ) self.assertEqual(user, None) def testAuthenticateUserBadPassword(self): user = authenticate( username=settings.LDAP_AUTH_TEST_USER_USERNAME, password="bad" + settings.LDAP_AUTH_TEST_USER_PASSWORD, ) self.assertEqual(user, None) def testRepeatedUserAuthenticationDoestRecreateUsers(self): user_1 = authenticate( username=settings.LDAP_AUTH_TEST_USER_USERNAME, password=settings.LDAP_AUTH_TEST_USER_PASSWORD, ) user_2 = authenticate( username=settings.LDAP_AUTH_TEST_USER_USERNAME, password=settings.LDAP_AUTH_TEST_USER_PASSWORD, ) # Ensure that the user isn't recreated on second access. self.assertEqual(user_1.pk, user_2.pk) @skip("FIXME: test server currently uses outdated TLS cyphers") def testAuthenticateWithTLS(self): with self.settings(LDAP_AUTH_USE_TLS=True): user = authenticate( username=settings.LDAP_AUTH_TEST_USER_USERNAME, password=settings.LDAP_AUTH_TEST_USER_PASSWORD, ) self.assertIsInstance(user, User) self.assertEqual(user.username, settings.LDAP_AUTH_TEST_USER_USERNAME) def testAuthenticateWithRebind(self): with self.settings( LDAP_AUTH_CONNECTION_USERNAME=settings.LDAP_AUTH_TEST_USER_USERNAME, LDAP_AUTH_CONNECTION_PASSWORD=settings.LDAP_AUTH_TEST_USER_PASSWORD, ): user = authenticate( username=settings.LDAP_AUTH_TEST_USER_USERNAME, password=settings.LDAP_AUTH_TEST_USER_PASSWORD, ) self.assertIsInstance(user, User) self.assertEqual(user.username, settings.LDAP_AUTH_TEST_USER_USERNAME) def testAuthenticateWithFailedRebind(self): with self.settings( LDAP_AUTH_CONNECTION_USERNAME="bad" + settings.LDAP_AUTH_TEST_USER_USERNAME, LDAP_AUTH_CONNECTION_PASSWORD=settings.LDAP_AUTH_TEST_USER_PASSWORD, ): user = authenticate( username=settings.LDAP_AUTH_TEST_USER_USERNAME, password=settings.LDAP_AUTH_TEST_USER_PASSWORD, ) self.assertIs(user, None) # User synchronisation. def testSyncUsersCreatesUsers(self): call_command("ldap_sync_users", verbosity=0) self.assertGreater(User.objects.count(), 0) def testSyncUsersCommandOutput(self): out = StringIO() call_command("ldap_sync_users", verbosity=1, stdout=out) rows = out.getvalue().split("\n")[:-1] self.assertEqual(len(rows), User.objects.count()) for row in rows: self.assertRegex(row, r'^Synced ') def testReSyncUsersDoesntRecreateUsers(self): call_command("ldap_sync_users", verbosity=0) user_count_1 = User.objects.count() call_command("ldap_sync_users", verbosity=0) user_count_2 = User.objects.count() self.assertEqual(user_count_1, user_count_2) # User promotion. def testPromoteUser(self): user = User.objects.create( username="test", ) self.assertFalse(user.is_staff) self.assertFalse(user.is_superuser) # Promote the user. call_command("ldap_promote", "test", stdout=StringIO()) user = User.objects.get(username="test") self.assertTrue(user.is_staff) self.assertTrue(user.is_superuser) def testPromoteMissingUser(self): with self.assertRaises(CommandError, msg="User with username missing_user does not exist"): call_command("ldap_promote", "missing_user", verbosity=0) def testSyncUserRelations(self): def check_sync_user_relation(user, data, *, connection=None, dn=None): # id have been created self.assertIsNotNone(user.id) # connection was passed self.assertIsNotNone(connection) # dn was passed self.assertIsNotNone(dn) # model is saved self.assertEqual(user.username, User.objects.get(pk=user.id).username) # save all groups self.assertIn('cn', data) ldap_groups = list(data.get('memberOf', ())) ldap_groups.append('default_group') for group in ldap_groups: user.groups.create(name=group) with self.settings(LDAP_AUTH_SYNC_USER_RELATIONS=check_sync_user_relation): user = authenticate( username=settings.LDAP_AUTH_TEST_USER_USERNAME, password=settings.LDAP_AUTH_TEST_USER_PASSWORD, ) self.assertIsInstance(user, User) self.assertGreaterEqual(user.groups.count(), 1) self.assertEqual(user.groups.filter(name='default_group').count(), 1) def testOldSyncUserRelations(self): def check_sync_user_relation(user, data): # id have been created self.assertIsNotNone(user.id) # model is saved self.assertEqual(user.username, User.objects.get(pk=user.id).username) # save all groups self.assertIn('cn', data) ldap_groups = list(data.get('memberOf', ())) ldap_groups.append('default_group') for group in ldap_groups: user.groups.create(name=group) with self.settings(LDAP_AUTH_SYNC_USER_RELATIONS=check_sync_user_relation): user = authenticate( username=settings.LDAP_AUTH_TEST_USER_USERNAME, password=settings.LDAP_AUTH_TEST_USER_PASSWORD, ) self.assertIsInstance(user, User) self.assertGreaterEqual(user.groups.count(), 1) self.assertEqual(user.groups.filter(name='default_group').count(), 1) def testImportFunc(self): self.assertIs(clean_ldap_name, import_func(clean_ldap_name)) self.assertIs(clean_ldap_name, import_func('django_python3_ldap.utils.clean_ldap_name')) self.assertTrue(callable(import_func('django.contrib.auth.get_user_model'))) self.assertRaises(AttributeError, import_func, 123) self.assertTrue(callable(import_func(settings.LDAP_AUTH_SYNC_USER_RELATIONS))) 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))) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/django_python3_ldap/utils.py0000644000175100001710000000670000000000000022357 0ustar00runnerdocker""" Some useful LDAP utilities. """ import re import binascii from django.utils.encoding import force_text from django.utils.module_loading import import_string from django_python3_ldap.conf import settings def import_func(func): if callable(func): return func elif isinstance(func, str): return import_string(func) raise AttributeError("Expected a function {0!r}".format(func)) def clean_ldap_name(name): """ Transforms the given name into a form that won't interfere with LDAP queries. """ 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), ) def convert_model_fields_to_ldap_fields(model_fields): """ Converts a set of model fields into a set of corresponding LDAP fields. """ return { settings.LDAP_AUTH_USER_FIELDS[field_name]: field_value for field_name, field_value in model_fields.items() } def format_search_filter(model_fields): """ Creates an LDAP search filter for the given set of model fields. """ ldap_fields = convert_model_fields_to_ldap_fields(model_fields) ldap_fields["objectClass"] = settings.LDAP_AUTH_OBJECT_CLASS search_filters = import_func(settings.LDAP_AUTH_FORMAT_SEARCH_FILTERS)(ldap_fields) return "(&{})".format("".join(search_filters)) def clean_user_data(model_fields): """ Transforms the user data loaded from LDAP into a form suitable for creating a user. """ return model_fields def format_username_openldap(model_fields): """ Formats a user identifier into a username suitable for binding to an OpenLDAP server. """ return "{user_identifier},{search_base}".format( user_identifier=",".join( "{attribute_name}={field_value}".format( attribute_name=clean_ldap_name(field_name), field_value=clean_ldap_name(field_value), ) for field_name, field_value in convert_model_fields_to_ldap_fields(model_fields).items() ), search_base=settings.LDAP_AUTH_SEARCH_BASE, ) def format_username_active_directory(model_fields): """ Formats a user identifier into a username suitable for binding to an Active Directory server. """ username = model_fields["username"] if settings.LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN: username = "{domain}\\{username}".format( domain=settings.LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN, username=username, ) return username def format_username_active_directory_principal(model_fields): """ Formats a user identifier into a username suitable for binding to an Active Directory server. """ username = model_fields["username"] if settings.LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN: username = "{username}@{domain}".format( username=username, domain=settings.LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN, ) return username def sync_user_relations(user, ldap_attributes, *, connection=None, dn=None): # do nothing by default pass def format_search_filters(ldap_fields): return [ "({attribute_name}={field_value})".format( attribute_name=clean_ldap_name(field_name), field_value=clean_ldap_name(field_value), ) for field_name, field_value in ldap_fields.items() ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1625834145.9746225 django-python3-ldap-0.12.0/django_python3_ldap.egg-info/0000755000175100001710000000000000000000000022334 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834145.0 django-python3-ldap-0.12.0/django_python3_ldap.egg-info/PKG-INFO0000644000175100001710000000134300000000000023432 0ustar00runnerdockerMetadata-Version: 1.1 Name: django-python3-ldap Version: 0.12.0 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 Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Framework :: Django ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834145.0 django-python3-ldap-0.12.0/django_python3_ldap.egg-info/SOURCES.txt0000644000175100001710000000137300000000000024224 0ustar00runnerdockerCHANGELOG.rst LICENSE MANIFEST.in README.rst setup.cfg setup.py django_python3_ldap/__init__.py django_python3_ldap/auth.py django_python3_ldap/conf.py django_python3_ldap/ldap.py django_python3_ldap/tests.py django_python3_ldap/utils.py django_python3_ldap.egg-info/PKG-INFO django_python3_ldap.egg-info/SOURCES.txt django_python3_ldap.egg-info/dependency_links.txt 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_promote.py django_python3_ldap/management/commands/ldap_sync_users.py tests/manage.py tests/django_python3_ldap_test/__init__.py tests/django_python3_ldap_test/settings.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834145.0 django-python3-ldap-0.12.0/django_python3_ldap.egg-info/dependency_links.txt0000644000175100001710000000000100000000000026402 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834145.0 django-python3-ldap-0.12.0/django_python3_ldap.egg-info/requires.txt0000644000175100001710000000005600000000000024735 0ustar00runnerdockerdjango>=1.11 ldap3<3,>=2.5 pyasn1<0.5,>=0.4.6 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834145.0 django-python3-ldap-0.12.0/django_python3_ldap.egg-info/top_level.txt0000644000175100001710000000002400000000000025062 0ustar00runnerdockerdjango_python3_ldap ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1625834145.9786224 django-python3-ldap-0.12.0/setup.cfg0000644000175100001710000000014000000000000016530 0ustar00runnerdocker[flake8] max-line-length = 120 exclude = venv,migrations [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/setup.py0000644000175100001710000000200500000000000016423 0ustar00runnerdockerfrom setuptools import setup, find_packages from django_python3_ldap import __version__ version_str = ".".join(str(n) for n in __version__) setup( name="django-python3-ldap", version=version_str, license="BSD", description="Django LDAP user authentication backend for Python 3.", author="Dave Hall", author_email="dave@etianen.com", url="https://github.com/etianen/django-python3-ldap", packages=find_packages(), install_requires=[ "django>=1.11", "ldap3>=2.5,<3", "pyasn1>=0.4.6,<0.5", ], classifiers=[ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Framework :: Django", ], ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1625834145.9746225 django-python3-ldap-0.12.0/tests/0000755000175100001710000000000000000000000016056 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1625834145.9746225 django-python3-ldap-0.12.0/tests/django_python3_ldap_test/0000755000175100001710000000000000000000000023043 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/tests/django_python3_ldap_test/__init__.py0000644000175100001710000000000000000000000025142 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/tests/django_python3_ldap_test/settings.py0000644000175100001710000000526100000000000025261 0ustar00runnerdocker""" Django settings for django_python3_ldap_test project. For more information on this file, see https://docs.djangoproject.com/en/1.7/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.7/ref/settings/ """ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '^vxc5sw2w^%02!vrk(#)am3ly#b$ykb!eu$bx*pto@v)&q8@9#' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True TEMPLATE_DEBUG = True ALLOWED_HOSTS = [] # LDAP auth settings. LDAP_AUTH_URL = "ldap://ldap.forumsys.com:389" LDAP_AUTH_SEARCH_BASE = "dc=example,dc=com" AUTHENTICATION_BACKENDS = ( "django_python3_ldap.auth.LDAPBackend", ) # LDAP auth test settings. LDAP_AUTH_TEST_USER_USERNAME = "tesla" LDAP_AUTH_TEST_USER_PASSWORD = "password" # Application definition INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django_python3_ldap', ) MIDDLEWARE = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) TEMPLATES = [{ 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }] # Database # https://docs.djangoproject.com/en/1.7/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ STATIC_URL = '/static/' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1625834137.0 django-python3-ldap-0.12.0/tests/manage.py0000755000175100001710000000041300000000000017661 0ustar00runnerdocker#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_python3_ldap_test.settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv)