django-python3-ldap-0.11.1/0000755000076500000240000000000013171604154015420 5ustar davestaff00000000000000django-python3-ldap-0.11.1/CHANGELOG.rst0000644000076500000240000000535613171604051017446 0ustar davestaff00000000000000django-python3-ldap changelog ============================= 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. django-python3-ldap-0.11.1/django_python3_ldap/0000755000076500000240000000000013171604154021346 5ustar davestaff00000000000000django-python3-ldap-0.11.1/django_python3_ldap/__init__.py0000644000076500000240000000013113171604112023444 0ustar davestaff00000000000000""" Django LDAP user authentication backend for Python 3. """ __version__ = (0, 11, 1) django-python3-ldap-0.11.1/django_python3_ldap/auth.py0000644000076500000240000000076012455531455022673 0ustar davestaff00000000000000""" 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) django-python3-ldap-0.11.1/django_python3_ldap/conf.py0000644000076500000240000000577113171603631022656 0ustar davestaff00000000000000""" 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) django-python3-ldap-0.11.1/django_python3_ldap/ldap.py0000644000076500000240000001500513171603631022640 0ustar davestaff00000000000000""" Low-level LDAP hooks. """ import ldap3 from ldap3.core.exceptions import LDAPException import logging 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 import_func(settings.LDAP_AUTH_SYNC_USER_RELATIONS)(user, attributes) # 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) # Configure the connection. if settings.LDAP_AUTH_USE_TLS: auto_bind = ldap3.AUTO_BIND_TLS_BEFORE_BIND else: auto_bind = ldap3.AUTO_BIND_NO_TLS # 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=auto_bind, 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 # 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() try: c.rebind( user=format_username({User.USERNAME_FIELD: settings.LDAP_AUTH_CONNECTION_USERNAME}), password=settings.LDAP_AUTH_CONNECTION_PASSWORD, ) except LDAPException as ex: logger.warning("LDAP rebind failed: {ex}".format(ex=ex)) yield None return # Return the connection. logger.info("LDAP connect succeeded") try: yield Connection(c) 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") # 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) django-python3-ldap-0.11.1/django_python3_ldap/management/0000755000076500000240000000000013171604154023462 5ustar davestaff00000000000000django-python3-ldap-0.11.1/django_python3_ldap/management/__init__.py0000644000076500000240000000000012455531455025570 0ustar davestaff00000000000000django-python3-ldap-0.11.1/django_python3_ldap/management/commands/0000755000076500000240000000000013171604154025263 5ustar davestaff00000000000000django-python3-ldap-0.11.1/django_python3_ldap/management/commands/__init__.py0000644000076500000240000000000012455531455027371 0ustar davestaff00000000000000django-python3-ldap-0.11.1/django_python3_ldap/management/commands/ldap_promote.py0000664000076500000240000000233713044102637030327 0ustar davestaff00000000000000from 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, )) django-python3-ldap-0.11.1/django_python3_ldap/management/commands/ldap_sync_users.py0000644000076500000240000000203413131125233031021 0ustar davestaff00000000000000from 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, )) django-python3-ldap-0.11.1/django_python3_ldap/tests.py0000644000076500000240000001776013073375622023103 0ustar davestaff00000000000000# 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 @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) 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_USE_TLS=True, 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_USE_TLS=True, 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() 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"): 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) 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))) django-python3-ldap-0.11.1/django_python3_ldap/utils.py0000644000076500000240000000671313113507266023071 0ustar davestaff00000000000000""" 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.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. """ 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): # 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() ] django-python3-ldap-0.11.1/django_python3_ldap.egg-info/0000755000076500000240000000000013171604154023040 5ustar davestaff00000000000000django-python3-ldap-0.11.1/django_python3_ldap.egg-info/dependency_links.txt0000644000076500000240000000000113171604153027105 0ustar davestaff00000000000000 django-python3-ldap-0.11.1/django_python3_ldap.egg-info/PKG-INFO0000644000076500000240000000140513171604153024134 0ustar davestaff00000000000000Metadata-Version: 1.1 Name: django-python3-ldap Version: 0.11.1 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-Content-Type: UNKNOWN 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.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Framework :: Django django-python3-ldap-0.11.1/django_python3_ldap.egg-info/requires.txt0000644000076500000240000000004513171604153025436 0ustar davestaff00000000000000django>=1.8 ldap3==2.3 pyasn1==0.3.2 django-python3-ldap-0.11.1/django_python3_ldap.egg-info/SOURCES.txt0000644000076500000240000000136113171604153024724 0ustar davestaff00000000000000CHANGELOG.rst 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 tests/manage.py tests/django_python3_ldap_test/__init__.py tests/django_python3_ldap_test/settings.pydjango-python3-ldap-0.11.1/django_python3_ldap.egg-info/top_level.txt0000644000076500000240000000002413171604153025565 0ustar davestaff00000000000000django_python3_ldap django-python3-ldap-0.11.1/LICENSE0000664000076500000240000000276412455527251016446 0ustar davestaff00000000000000Copyright (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. django-python3-ldap-0.11.1/MANIFEST.in0000644000076500000240000000012613066164030017152 0ustar davestaff00000000000000include LICENSE include README.rst include CHANGELOG.rst recursive-include tests *.py django-python3-ldap-0.11.1/PKG-INFO0000644000076500000240000000140513171604154016515 0ustar davestaff00000000000000Metadata-Version: 1.1 Name: django-python3-ldap Version: 0.11.1 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-Content-Type: UNKNOWN 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.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Framework :: Django django-python3-ldap-0.11.1/README.rst0000644000076500000240000001703113171603631017110 0ustar davestaff00000000000000django-python3-ldap =================== **django-python3-ldap** provides a Django LDAP user authentication backend for Python 2 and 3. Features -------- - Authenticate users with an LDAP server. - Sync LDAP users with a local Django database. - Supports custom Django user models. - Works in Python 2 and 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. 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 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 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" 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 `_ django-python3-ldap-0.11.1/setup.cfg0000644000076500000240000000004613171604154017241 0ustar davestaff00000000000000[egg_info] tag_build = tag_date = 0 django-python3-ldap-0.11.1/setup.py0000644000076500000240000000177413142033012017126 0ustar davestaff00000000000000from 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.8", "ldap3==2.3", "pyasn1==0.3.2", ], 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 :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Framework :: Django", ], ) django-python3-ldap-0.11.1/tests/0000755000076500000240000000000013171604154016562 5ustar davestaff00000000000000django-python3-ldap-0.11.1/tests/django_python3_ldap_test/0000755000076500000240000000000013171604154023547 5ustar davestaff00000000000000django-python3-ldap-0.11.1/tests/django_python3_ldap_test/__init__.py0000644000076500000240000000000012455513744025656 0ustar davestaff00000000000000django-python3-ldap-0.11.1/tests/django_python3_ldap_test/settings.py0000644000076500000240000000461713044103161025760 0ustar davestaff00000000000000""" 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 = os.environ.get("LDAP_AUTH_URL", "ldap://localhost:389") LDAP_AUTH_SEARCH_BASE = os.environ.get("LDAP_AUTH_SEARCH_BASE", "") AUTHENTICATION_BACKENDS = ( "django_python3_ldap.auth.LDAPBackend", ) # LDAP auth test settings. LDAP_AUTH_TEST_USER_USERNAME = os.environ.get("LDAP_AUTH_TEST_USER_USERNAME", "") LDAP_AUTH_TEST_USER_PASSWORD = os.environ.get("LDAP_AUTH_TEST_USER_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_CLASSES = ( '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', ) # 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/' django-python3-ldap-0.11.1/tests/manage.py0000755000076500000240000000041312455513744020375 0ustar davestaff00000000000000#!/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)