[Python-modules-commits] [django-python3-ldap] 01/03: importing django-python3-ldap_0.9.8.orig.tar.gz

Michael Fladischer fladi at moszumanska.debian.org
Tue Jan 5 12:39:37 UTC 2016


This is an automated email from the git hooks/post-receive script.

fladi pushed a commit to branch master
in repository django-python3-ldap.

commit e366734e58f3dffa63ea86e08b93dd464b1d59d1
Author: Michael Fladischer <FladischerMichael at fladi.at>
Date:   Mon Jan 4 19:10:50 2016 +0100

    importing django-python3-ldap_0.9.8.orig.tar.gz
---
 LICENSE                                            |  27 ++++
 MANIFEST.in                                        |   2 +
 PKG-INFO                                           |  21 +++
 README.rst                                         | 168 +++++++++++++++++++
 django_python3_ldap.egg-info/PKG-INFO              |  21 +++
 django_python3_ldap.egg-info/SOURCES.txt           |  19 +++
 django_python3_ldap.egg-info/dependency_links.txt  |   1 +
 django_python3_ldap.egg-info/requires.txt          |   2 +
 django_python3_ldap.egg-info/top_level.txt         |   1 +
 django_python3_ldap/__init__.py                    |   6 +
 django_python3_ldap/auth.py                        |  23 +++
 django_python3_ldap/conf.py                        | 119 ++++++++++++++
 django_python3_ldap/ldap.py                        | 145 +++++++++++++++++
 django_python3_ldap/management/__init__.py         |   0
 .../management/commands/__init__.py                |   0
 .../management/commands/ldap_promote.py            |  30 ++++
 .../management/commands/ldap_sync_users.py         |  20 +++
 django_python3_ldap/tests.py                       | 178 +++++++++++++++++++++
 django_python3_ldap/utils.py                       | 109 +++++++++++++
 setup.cfg                                          |   5 +
 setup.py                                           |  35 ++++
 21 files changed, 932 insertions(+)

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..711d010
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright (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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..9d5d250
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include LICENSE
+include README.rst
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..785fdd4
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,21 @@
+Metadata-Version: 1.1
+Name: django-python3-ldap
+Version: 0.9.8
+Summary: Django LDAP user authentication backend for Python 3.
+Home-page: https://github.com/etianen/django-python3-ldap
+Author: Dave Hall
+Author-email: dave at 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 :: 2.7
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Framework :: Django
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..69df588
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,168 @@
+django-python3-ldap
+===================
+
+**django-python3-ldap** provides a Django LDAP user authentication backend for Python 3.
+
+
+Features
+--------
+
+- Authenticate users with an LDAP server.
+- Sync LDAP users with a local Django database.
+- Supports custom Django user models.
+- Works in Python 3!
+
+
+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.
+
+
+Available settings
+------------------
+
+.. 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 and a dict of {ldap_field_name: [value]},
+    # 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 authenticating the `ldap_sync_users`
+    # management command. Set to None if you allow anonymous queries.
+    LDAP_AUTH_CONNECTION_USERNAME = None
+    LDAP_AUTH_CONNECTION_PASSWORD = None
+
+
+Microsoft Active Directory support
+----------------------------------
+
+django-python3-ldap is configured by default to support login via OpenLDAP. To connect to
+a Microsoft Active Directory, add the following line to your settings file.
+
+.. code:: python
+
+    LDAP_AUTH_FORMAT_USERNAME = "django_python3_ldap.utils.format_username_active_directory"
+
+If your Active Directory server requires a domain to be supplied with the username,
+then also specify:
+
+.. code:: python
+
+    LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN = "your_domain"
+
+
+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"
+
+    # pay/to/your.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 <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/PKG-INFO b/django_python3_ldap.egg-info/PKG-INFO
new file mode 100644
index 0000000..785fdd4
--- /dev/null
+++ b/django_python3_ldap.egg-info/PKG-INFO
@@ -0,0 +1,21 @@
+Metadata-Version: 1.1
+Name: django-python3-ldap
+Version: 0.9.8
+Summary: Django LDAP user authentication backend for Python 3.
+Home-page: https://github.com/etianen/django-python3-ldap
+Author: Dave Hall
+Author-email: dave at 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 :: 2.7
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Framework :: Django
diff --git a/django_python3_ldap.egg-info/SOURCES.txt b/django_python3_ldap.egg-info/SOURCES.txt
new file mode 100644
index 0000000..ea2ae84
--- /dev/null
+++ b/django_python3_ldap.egg-info/SOURCES.txt
@@ -0,0 +1,19 @@
+LICENSE
+MANIFEST.in
+README.rst
+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
\ No newline at end of file
diff --git a/django_python3_ldap.egg-info/dependency_links.txt b/django_python3_ldap.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/django_python3_ldap.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/django_python3_ldap.egg-info/requires.txt b/django_python3_ldap.egg-info/requires.txt
new file mode 100644
index 0000000..b1c41e9
--- /dev/null
+++ b/django_python3_ldap.egg-info/requires.txt
@@ -0,0 +1,2 @@
+django>=1.7
+ldap3>=0.9.8.4,!=0.9.9.2
diff --git a/django_python3_ldap.egg-info/top_level.txt b/django_python3_ldap.egg-info/top_level.txt
new file mode 100644
index 0000000..ceea820
--- /dev/null
+++ b/django_python3_ldap.egg-info/top_level.txt
@@ -0,0 +1 @@
+django_python3_ldap
diff --git a/django_python3_ldap/__init__.py b/django_python3_ldap/__init__.py
new file mode 100644
index 0000000..cb8dc7f
--- /dev/null
+++ b/django_python3_ldap/__init__.py
@@ -0,0 +1,6 @@
+"""
+Django LDAP user authentication backend for Python 3.
+"""
+
+
+__version__ = (0, 9, 8)
diff --git a/django_python3_ldap/auth.py b/django_python3_ldap/auth.py
new file mode 100644
index 0000000..cba93fd
--- /dev/null
+++ b/django_python3_ldap/auth.py
@@ -0,0 +1,23 @@
+"""
+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)
diff --git a/django_python3_ldap/conf.py b/django_python3_ldap/conf.py
new file mode 100644
index 0000000..e1e3469
--- /dev/null
+++ b/django_python3_ldap/conf.py
@@ -0,0 +1,119 @@
+"""
+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,
+    )
+
+
+settings = LazySettings(settings)
diff --git a/django_python3_ldap/ldap.py b/django_python3_ldap/ldap.py
new file mode 100644
index 0000000..8841a2e
--- /dev/null
+++ b/django_python3_ldap/ldap.py
@@ -0,0 +1,145 @@
+"""
+Low-level LDAP hooks.
+"""
+
+import ldap3
+
+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
+
+
+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.
+        """
+        User = get_user_model()
+        attributes = user_data["attributes"]
+        # Create the user data.
+        user_fields = {
+            field_name: attributes.get(attribute_name, ("",))[0]
+            for field_name, attribute_name
+            in settings.LDAP_AUTH_USER_FIELDS.items()
+        }
+        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
+        )
+        # Update relations
+        import_func(settings.LDAP_AUTH_SYNC_USER_RELATIONS)(user, attributes)
+        # All done!
+        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.SEARCH_SCOPE_WHOLE_SUBTREE,
+            attributes = ldap3.ALL_ATTRIBUTES,
+            paged_size = 30,
+        )
+        return (
+            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.SEARCH_SCOPE_WHOLE_SUBTREE,
+            attributes = ldap3.ALL_ATTRIBUTES,
+            get_operational_attributes = True,
+            size_limit = 1,
+        ):
+            return self._get_or_create_user(self._connection.response[0])
+        return None
+
+
+ at 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.
+    username = None
+    password = None
+    if kwargs:
+        password = kwargs.pop("password")
+        username = import_func(settings.LDAP_AUTH_FORMAT_USERNAME)(kwargs)
+    # Make the connection.
+    if username or password:
+        if settings.LDAP_AUTH_USE_TLS:
+            auto_bind = ldap3.AUTO_BIND_TLS_BEFORE_BIND
+        else:
+            auto_bind = ldap3.AUTO_BIND_NO_TLS
+    else:
+        auto_bind = ldap3.AUTO_BIND_NONE
+    try:
+        with ldap3.Connection(ldap3.Server(settings.LDAP_AUTH_URL), user=username, password=password, auto_bind=auto_bind) as c:
+            yield Connection(c)
+    except (ldap3.LDAPBindError, ldap3.LDAPSASLPrepError):
+        yield None
+
+
+def authenticate(**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")
+    # 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)
diff --git a/django_python3_ldap/management/__init__.py b/django_python3_ldap/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django_python3_ldap/management/commands/__init__.py b/django_python3_ldap/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django_python3_ldap/management/commands/ldap_promote.py b/django_python3_ldap/management/commands/ldap_promote.py
new file mode 100644
index 0000000..17cbd64
--- /dev/null
+++ b/django_python3_ldap/management/commands/ldap_promote.py
@@ -0,0 +1,30 @@
+from 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."
+
+    args = "[username, ...]"
+
+    @transaction.atomic()
+    def handle(self, *usernames, **kwargs):
+        verbosity = int(kwargs.get("verbosity", 1))
+        User = get_user_model()
+        for username in 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,
+                    ))
diff --git a/django_python3_ldap/management/commands/ldap_sync_users.py b/django_python3_ldap/management/commands/ldap_sync_users.py
new file mode 100644
index 0000000..a51cf3a
--- /dev/null
+++ b/django_python3_ldap/management/commands/ldap_sync_users.py
@@ -0,0 +1,20 @@
+from django.core.management.base import NoArgsCommand
+from django.db import transaction
+
+from django_python3_ldap import ldap
+from django_python3_ldap.conf import settings
+
+
+class Command(NoArgsCommand):
+
+    help = "Creates local user models for all users found in the remote LDAP authentication server."
+
+    @transaction.atomic()
+    def handle_noargs(self, **kwargs):
+        verbosity = int(kwargs.get("verbosity", 1))
+        with ldap.connection(username=settings.LDAP_AUTH_CONNECTION_USERNAME, password=settings.LDAP_AUTH_CONNECTION_PASSWORD) as connection:
+            for user in connection.iter_users():
+                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
new file mode 100644
index 0000000..124e00c
--- /dev/null
+++ b/django_python3_ldap/tests.py
@@ -0,0 +1,178 @@
+# encoding=utf-8
+from __future__ import unicode_literals
+
+from unittest import skipUnless
+from io import StringIO, BytesIO
+
+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.utils import six
+
+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
+
+
+ at skipUnless(settings.LDAP_AUTH_TEST_USER_USERNAME, "No settings.LDAP_AUTH_TEST_USER_USERNAME supplied.")
+ at skipUnless(settings.LDAP_AUTH_TEST_USER_PASSWORD, "No settings.LDAP_AUTH_TEST_USER_PASSWORD supplied.")
+ at skipUnless(settings.LDAP_AUTH_USER_LOOKUP_FIELDS == ("username",), "Cannot test using custom lookup fields.")
+ at 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 at bar.com"), r'foo at 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)
+
+    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)
+
+    # User syncronisation.
+
+    def testSyncUsersCreatesUsers(self):
+        call_command("ldap_sync_users", verbosity=0)
+        self.assertGreater(User.objects.count(), 0)
+
+    def testSyncUsersCommandOutput(self):
+        out = StringIO() if six.PY3 else BytesIO()
+        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:
+            six.assertRegex(self, row, r'^Synced [^\s]+$')
+
+    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() if six.PY3 else BytesIO())
+        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") as cm:
+            call_command("ldap_promote", "missing_user", verbosity=0)
+
+    def testSyncUserRelations(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)
+            groups = list()
+            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)))
diff --git a/django_python3_ldap/utils.py b/django_python3_ldap/utils.py
new file mode 100644
index 0000000..24029b6
--- /dev/null
+++ b/django_python3_ldap/utils.py
@@ -0,0 +1,109 @@
+"""
+Some useful LDAP utilities.
+"""
+
+import re, binascii
+
+from django.contrib.auth.hashers import make_password
+from django.utils.encoding import force_text
+from django.utils.module_loading import import_string
+from django.utils import six
+
+from django_python3_ldap.conf import settings
+
+
+def import_func(func):
+    if callable(func):
+        return func
+    elif isinstance(func, six.string_types):
+        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.
+    """
+    # Create an unusable password for the user.
+    model_fields["password"] = make_password(None)
+    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,
+    )
... 82 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/django-python3-ldap.git



More information about the Python-modules-commits mailing list