pax_global_header00006660000000000000000000000064122417615330014516gustar00rootroot0000000000000052 comment=4b086d338334e8fcb0ef86258600af5bad0e1e71 django-ldapdb-0.2.0/000077500000000000000000000000001224176153300142035ustar00rootroot00000000000000django-ldapdb-0.2.0/.addcopyright000066400000000000000000000030451224176153300166670ustar00rootroot00000000000000{ 'directories': [ 'examples', 'ldapdb', 'ldapdb/backends', 'ldapdb/backends/ldap', 'ldapdb/models', ], 'license': """django-ldapdb Copyright (c) 2009-2011, Bolloré telecom Copyright (c) 2013, Jeremy Lainé All rights reserved. See AUTHORS file for a full list of contributors. 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. 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-ldapdb-0.2.0/.gitignore000066400000000000000000000000271224176153300161720ustar00rootroot00000000000000*.egg *.egg-info *.pyc django-ldapdb-0.2.0/.travis.yml000066400000000000000000000004311224176153300163120ustar00rootroot00000000000000language: python python: - "2.7" env: - DJANGO=1.2.7 - DJANGO=1.3.7 - DJANGO=1.4.6 - DJANGO=1.5.2 - DJANGO=1.6 install: - pip install -q Django==$DJANGO --use-mirrors - pip install -q python-ldap mockldap --use-mirrors script: python manage.py test ldapdb examples django-ldapdb-0.2.0/AUTHORS000066400000000000000000000001151224176153300152500ustar00rootroot00000000000000Jeremy Lainé * Original developer of django-ldapdb django-ldapdb-0.2.0/LICENSE000066400000000000000000000025301224176153300152100ustar00rootroot00000000000000Copyright (c) 2009-2011, Bolloré telecom Copyright (c) 2013, Jeremy Lainé 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. 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-ldapdb-0.2.0/README.md000066400000000000000000000044271224176153300154710ustar00rootroot00000000000000django-ldapdb - support for django models over LDAP Copyright (c) 2009-2011, Bolloré Telecom Copyright (c) 2013, Jeremy Lainé [![Build Status](https://travis-ci.org/jlaine/django-ldapdb.png)](https://travis-ci.org/jlaine/django-ldapdb) About ----- _django-ldapdb_ is an LDAP database backend for Django. It allows you to manipulate LDAP entries using Django's models. Declaring models using the LDAP backend is very staightforward, you simply inherit from _ldapdb.models.Model_ and declare the fields in the same way as for regular models. You can even edit the LDAP entries using Django's admin interface. _django-ldapdb_ requires Django version 1.2.x, 1.3.x, 1.4.x, 1.5.x or 1.6.x. _django-ldapdb_ is distributed under the BSD license, see the LICENSE file for details. See AUTHORS file for a full list of contributors. Using django-ldapdb ------------------- Add the following to your _settings.py_: DATABASES = { ... 'ldap': { 'ENGINE': 'ldapdb.backends.ldap', 'NAME': 'ldap://ldap.nodomain.org/', 'USER': 'cn=admin,dc=nodomain,dc=org', 'PASSWORD': 'some_secret_password', } } DATABASE_ROUTERS = ['ldapdb.router.Router'] If you want to access posixGroup entries in your application, you can add something like this to your _models.py_: from ldapdb.models.fields import CharField, IntegerField, ListField import ldapdb.models class LdapGroup(ldapdb.models.Model): """ Class for representing an LDAP group entry. """ # LDAP meta-data base_dn = "ou=groups,dc=nodomain,dc=org" object_classes = ['posixGroup'] # posixGroup attributes gid = IntegerField(db_column='gidNumber', unique=True) name = CharField(db_column='cn', max_length=200, primary_key=True) members = ListField(db_column='memberUid') def __str__(self): return self.name def __unicode__(self): return self.name _Important note_ : you _must_ declare an attribute to be used as the primary key. This attribute will play a special role, as it will be used to build the Relative Distinguished Name of the entry. For instance in the example above, a group whose cn is _foo_ will have the DN _cn=foo,ou=groups,dc=nodomain,dc=org_. django-ldapdb-0.2.0/__init__.py000066400000000000000000000000001224176153300163020ustar00rootroot00000000000000django-ldapdb-0.2.0/examples/000077500000000000000000000000001224176153300160215ustar00rootroot00000000000000django-ldapdb-0.2.0/examples/__init__.py000066400000000000000000000000001224176153300201200ustar00rootroot00000000000000django-ldapdb-0.2.0/examples/admin.py000066400000000000000000000040041224176153300174610ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-ldapdb # Copyright (c) 2009-2011, Bolloré telecom # Copyright (c) 2013, Jeremy Lainé # All rights reserved. # # See AUTHORS file for a full list of contributors. # # 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. # # 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. # from django.contrib import admin from examples.models import LdapGroup, LdapUser class LdapGroupAdmin(admin.ModelAdmin): exclude = ['dn', 'usernames'] list_display = ['name', 'gid'] search_fields = ['name'] class LdapUserAdmin(admin.ModelAdmin): exclude = ['dn', 'password', 'photo'] list_display = ['username', 'first_name', 'last_name', 'email', 'uid'] search_fields = ['first_name', 'last_name', 'full_name', 'username'] admin.site.register(LdapGroup, LdapGroupAdmin) admin.site.register(LdapUser, LdapUserAdmin) django-ldapdb-0.2.0/examples/fixtures/000077500000000000000000000000001224176153300176725ustar00rootroot00000000000000django-ldapdb-0.2.0/examples/fixtures/test_users.json000066400000000000000000000011031224176153300227600ustar00rootroot00000000000000[ { "pk": "1", "model": "auth.user", "fields": { "username": "test_user", "first_name": "Test", "last_name": "User", "is_active": true, "is_superuser": true, "is_staff": true, "last_login": "2006-12-17 07:03:31", "groups": [], "user_permissions": [], "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", "email": "test_user@example.com", "date_joined": "2006-12-17 07:03:31" } } ] django-ldapdb-0.2.0/examples/models.py000066400000000000000000000065341224176153300176660ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-ldapdb # Copyright (c) 2009-2011, Bolloré telecom # Copyright (c) 2013, Jeremy Lainé # All rights reserved. # # See AUTHORS file for a full list of contributors. # # 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. # # 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. # from ldapdb.models.fields import (CharField, DateField, ImageField, ListField, IntegerField, FloatField) import ldapdb.models class LdapUser(ldapdb.models.Model): """ Class for representing an LDAP user entry. """ # LDAP meta-data base_dn = "ou=people,dc=nodomain" object_classes = ['posixAccount', 'shadowAccount', 'inetOrgPerson'] # inetOrgPerson first_name = CharField(db_column='givenName') last_name = CharField(db_column='sn') full_name = CharField(db_column='cn') email = CharField(db_column='mail') phone = CharField(db_column='telephoneNumber', blank=True) mobile_phone = CharField(db_column='mobile', blank=True) photo = ImageField(db_column='jpegPhoto') # posixAccount uid = IntegerField(db_column='uidNumber', unique=True) group = IntegerField(db_column='gidNumber') gecos = CharField(db_column='gecos') home_directory = CharField(db_column='homeDirectory') login_shell = CharField(db_column='loginShell', default='/bin/bash') username = CharField(db_column='uid', primary_key=True) password = CharField(db_column='userPassword') date_of_birth = DateField(db_column='birthday', blank=True) latitude = FloatField(db_column='latitude', blank=True) def __str__(self): return self.username def __unicode__(self): return self.full_name class LdapGroup(ldapdb.models.Model): """ Class for representing an LDAP group entry. """ # LDAP meta-data base_dn = "ou=groups,dc=nodomain" object_classes = ['posixGroup'] # posixGroup attributes gid = IntegerField(db_column='gidNumber', unique=True) name = CharField(db_column='cn', max_length=200, primary_key=True) usernames = ListField(db_column='memberUid') def __str__(self): return self.name def __unicode__(self): return self.name django-ldapdb-0.2.0/examples/tests.py000066400000000000000000000545101224176153300175420ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-ldapdb # Copyright (c) 2009-2011, Bolloré telecom # Copyright (c) 2013, Jeremy Lainé # All rights reserved. # # See AUTHORS file for a full list of contributors. # # 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. # # 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. # import datetime import ldap from django.conf import settings from django.db.models import Q from django.test import TestCase from ldapdb.backends.ldap.compiler import query_as_ldap from examples.models import LdapUser, LdapGroup from mockldap import MockLdap admin = ('cn=admin,dc=nodomain', {'userPassword': ['test']}) groups = ('ou=groups,dc=nodomain', { 'objectClass': ['top', 'organizationalUnit'], 'ou': ['groups']}) people = ('ou=people,dc=nodomain', { 'objectClass': ['top', 'organizationalUnit'], 'ou': ['groups']}) contacts = ('ou=contacts,ou=groups,dc=nodomain', { 'objectClass': ['top', 'organizationalUnit'], 'ou': ['groups']}) foogroup = ('cn=foogroup,ou=groups,dc=nodomain', { 'objectClass': ['posixGroup'], 'memberUid': ['foouser', 'baruser'], 'gidNumber': ['1000'], 'cn': ['foogroup']}) bargroup = ('cn=bargroup,ou=groups,dc=nodomain', { 'objectClass': ['posixGroup'], 'memberUid': ['zoouser', 'baruser'], 'gidNumber': ['1001'], 'cn': ['bargroup']}) wizgroup = ('cn=wizgroup,ou=groups,dc=nodomain', { 'objectClass': ['posixGroup'], 'memberUid': ['wizuser', 'baruser'], 'gidNumber': ['1002'], 'cn': ['wizgroup']}) foouser = ('uid=foouser,ou=people,dc=nodomain', { 'cn': ['F\xc3\xb4o Us\xc3\xa9r'], 'objectClass': ['posixAccount', 'shadowAccount', 'inetOrgPerson'], 'loginShell': ['/bin/bash'], 'jpegPhoto': [ '\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xff' '\xfe\x00\x1cCreated with GIMP on a Mac\xff\xdb\x00C\x00\x05\x03\x04' '\x04\x04\x03\x05\x04\x04\x04\x05\x05\x05\x06\x07\x0c\x08\x07\x07\x07' '\x07\x0f\x0b\x0b\t\x0c\x11\x0f\x12\x12\x11\x0f\x11\x11\x13\x16\x1c' '\x17\x13\x14\x1a\x15\x11\x11\x18!\x18\x1a\x1d\x1d\x1f\x1f\x1f\x13' '\x17"$"\x1e$\x1c\x1e\x1f\x1e\xff\xdb\x00C\x01\x05\x05\x05\x07\x06\x07' '\x0e\x08\x08\x0e\x1e\x14\x11\x14\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' '\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' '\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' '\x1e\x1e\x1e\x1e\x1e\x1e\x1e\xff\xc0\x00\x11\x08\x00\x08\x00\x08\x03' '\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x15\x00\x01\x01\x00\x00' '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00' '\x19\x10\x00\x03\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' '\x00\x00\x01\x02\x06\x11A\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00' '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x14\x11\x01' '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff' '\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\x9d\xf29wU5Q\xd6' '\xfd\x00\x01\xff\xd9'], 'uidNumber': ['2000'], 'gidNumber': ['1000'], 'sn': ['Us\xc3\xa9r'], 'homeDirectory': ['/home/foouser'], 'givenName': ['F\xc3\xb4o'], 'uid': ['foouser'], 'birthday': ['1982-06-12'], 'latitude': ['3.14']}) class ConnectionTestCase(TestCase): directory = dict([admin, people, foouser]) @classmethod def setUpClass(cls): cls.mockldap = MockLdap(cls.directory) @classmethod def tearDownClass(cls): del cls.mockldap def setUp(self): self.mockldap.start() self.ldapobj = self.mockldap[settings.DATABASES['ldap']['NAME']] def tearDown(self): self.mockldap.stop() del self.ldapobj def test_connection_options(self): LdapUser.objects.get(username='foouser') self.assertEqual(self.ldapobj.get_option(ldap.OPT_X_TLS_DEMAND), True) def test_start_tls(self): self.assertFalse(self.ldapobj.tls_enabled) LdapUser.objects.get(username='foouser') self.assertTrue(self.ldapobj.tls_enabled) def test_dont_start_tls(self): settings.DATABASES['ldap']['TLS'] = False self.assertFalse(self.ldapobj.tls_enabled) LdapUser.objects.get(username='foouser') self.assertFalse(self.ldapobj.tls_enabled) settings.DATABASES['ldap']['TLS'] = True def test_bound_as_admin(self): LdapUser.objects.get(username='foouser') self.assertEqual(self.ldapobj.bound_as, admin[0]) class GroupTestCase(TestCase): directory = dict([admin, groups, foogroup, bargroup, wizgroup, foouser]) @classmethod def setUpClass(cls): cls.mockldap = MockLdap(cls.directory) @classmethod def tearDownClass(cls): del cls.mockldap def setUp(self): self.mockldap.start() self.ldapobj = self.mockldap[settings.DATABASES['ldap']['NAME']] def tearDown(self): self.mockldap.stop() del self.ldapobj def test_count(self): # empty query qs = LdapGroup.objects.none() self.assertEquals(qs.count(), 0) qs = LdapGroup.objects.none() self.assertEquals(len(qs), 0) # all query qs = LdapGroup.objects.all() self.assertEquals(qs.count(), 3) qs = LdapGroup.objects.all() self.assertEquals(len(qs), 3) def test_ldap_filter(self): # single filter qs = LdapGroup.objects.filter(name='foogroup') self.assertEquals(query_as_ldap(qs.query), '(&(objectClass=posixGroup)(cn=foogroup))') qs = LdapGroup.objects.filter(Q(name='foogroup')) self.assertEquals(query_as_ldap(qs.query), '(&(objectClass=posixGroup)(cn=foogroup))') # AND filter qs = LdapGroup.objects.filter(gid=1000, name='foogroup') self.assertEquals(query_as_ldap(qs.query), '(&(objectClass=posixGroup)(&(gidNumber=1000)' '(cn=foogroup)))') qs = LdapGroup.objects.filter(Q(gid=1000) & Q(name='foogroup')) self.assertEquals(query_as_ldap(qs.query), '(&(objectClass=posixGroup)(&(gidNumber=1000)' '(cn=foogroup)))') # OR filter qs = LdapGroup.objects.filter(Q(gid=1000) | Q(name='foogroup')) self.assertEquals(query_as_ldap(qs.query), '(&(objectClass=posixGroup)(|(gidNumber=1000)' '(cn=foogroup)))') # single exclusion qs = LdapGroup.objects.exclude(name='foogroup') self.assertEquals(query_as_ldap(qs.query), '(&(objectClass=posixGroup)(!(cn=foogroup)))') qs = LdapGroup.objects.filter(~Q(name='foogroup')) self.assertEquals(query_as_ldap(qs.query), '(&(objectClass=posixGroup)(!(cn=foogroup)))') # multiple exclusion qs = LdapGroup.objects.exclude(name='foogroup', gid=1000) self.assertEquals(query_as_ldap(qs.query), '(&(objectClass=posixGroup)(!(&(gidNumber=1000)' '(cn=foogroup))))') qs = LdapGroup.objects.filter(name='foogroup').exclude(gid=1000) self.assertEquals(query_as_ldap(qs.query), '(&(objectClass=posixGroup)(&(cn=foogroup)' '(!(gidNumber=1000))))') def test_filter(self): qs = LdapGroup.objects.filter(name='foogroup') self.assertEquals(qs.count(), 1) qs = LdapGroup.objects.filter(name='foogroup') self.assertEquals(len(qs), 1) g = qs[0] self.assertEquals(g.dn, 'cn=foogroup,%s' % LdapGroup.base_dn) self.assertEquals(g.name, 'foogroup') self.assertEquals(g.gid, 1000) self.assertEquals(g.usernames, ['foouser', 'baruser']) # try to filter non-existent entries qs = LdapGroup.objects.filter(name='does_not_exist') self.assertEquals(qs.count(), 0) qs = LdapGroup.objects.filter(name='does_not_exist') self.assertEquals(len(qs), 0) def test_get(self): g = LdapGroup.objects.get(name='foogroup') self.assertEquals(g.dn, 'cn=foogroup,%s' % LdapGroup.base_dn) self.assertEquals(g.name, 'foogroup') self.assertEquals(g.gid, 1000) self.assertEquals(g.usernames, ['foouser', 'baruser']) # try to get a non-existent entry self.assertRaises(LdapGroup.DoesNotExist, LdapGroup.objects.get, name='does_not_exist') def test_order_by(self): # ascending name qs = LdapGroup.objects.order_by('name') self.assertEquals(len(qs), 3) self.assertEquals(qs[0].name, 'bargroup') self.assertEquals(qs[1].name, 'foogroup') self.assertEquals(qs[2].name, 'wizgroup') # descending name qs = LdapGroup.objects.order_by('-name') self.assertEquals(len(qs), 3) self.assertEquals(qs[0].name, 'wizgroup') self.assertEquals(qs[1].name, 'foogroup') self.assertEquals(qs[2].name, 'bargroup') # ascending gid qs = LdapGroup.objects.order_by('gid') self.assertEquals(len(qs), 3) self.assertEquals(qs[0].gid, 1000) self.assertEquals(qs[1].gid, 1001) self.assertEquals(qs[2].gid, 1002) # descending gid qs = LdapGroup.objects.order_by('-gid') self.assertEquals(len(qs), 3) self.assertEquals(qs[0].gid, 1002) self.assertEquals(qs[1].gid, 1001) self.assertEquals(qs[2].gid, 1000) # ascending pk qs = LdapGroup.objects.order_by('pk') self.assertEquals(len(qs), 3) self.assertEquals(qs[0].name, 'bargroup') self.assertEquals(qs[1].name, 'foogroup') self.assertEquals(qs[2].name, 'wizgroup') # descending pk qs = LdapGroup.objects.order_by('-pk') self.assertEquals(len(qs), 3) self.assertEquals(qs[0].name, 'wizgroup') self.assertEquals(qs[1].name, 'foogroup') self.assertEquals(qs[2].name, 'bargroup') def test_bulk_delete(self): LdapGroup.objects.all().delete() qs = LdapGroup.objects.all() self.assertEquals(len(qs), 0) def test_bulk_delete_none(self): LdapGroup.objects.none().delete() qs = LdapGroup.objects.all() self.assertEquals(len(qs), 3) def test_slice(self): qs = LdapGroup.objects.order_by('gid') objs = list(qs) self.assertEquals(len(objs), 3) self.assertEquals(objs[0].gid, 1000) self.assertEquals(objs[1].gid, 1001) self.assertEquals(objs[2].gid, 1002) # limit only qs = LdapGroup.objects.order_by('gid') objs = qs[:2] self.assertEquals(objs.count(), 2) objs = qs[:2] self.assertEquals(len(objs), 2) self.assertEquals(objs[0].gid, 1000) self.assertEquals(objs[1].gid, 1001) # offset only qs = LdapGroup.objects.order_by('gid') objs = qs[1:] self.assertEquals(objs.count(), 2) objs = qs[1:] self.assertEquals(len(objs), 2) self.assertEquals(objs[0].gid, 1001) self.assertEquals(objs[1].gid, 1002) # offset and limit qs = LdapGroup.objects.order_by('gid') objs = qs[1:2] self.assertEquals(objs.count(), 1) objs = qs[1:2] self.assertEquals(len(objs), 1) self.assertEquals(objs[0].gid, 1001) def test_update(self): g = LdapGroup.objects.get(name='foogroup') g.gid = 1002 g.usernames = ['foouser2', 'baruser2'] g.save() # make sure DN gets updated if we change the pk g.name = 'foogroup2' g.save() self.assertEquals(g.dn, 'cn=foogroup2,%s' % LdapGroup.base_dn) def test_values(self): qs = sorted(LdapGroup.objects.values('name')) self.assertEquals(len(qs), 3) self.assertEquals(qs[0], {'name': 'bargroup'}) self.assertEquals(qs[1], {'name': 'foogroup'}) self.assertEquals(qs[2], {'name': 'wizgroup'}) def test_values_list(self): qs = sorted(LdapGroup.objects.values_list('name')) self.assertEquals(len(qs), 3) self.assertEquals(qs[0], ('bargroup',)) self.assertEquals(qs[1], ('foogroup',)) self.assertEquals(qs[2], ('wizgroup',)) def test_delete(self): g = LdapGroup.objects.get(name='foogroup') g.delete() qs = LdapGroup.objects.all() self.assertEquals(len(qs), 2) def test_save(self): g = LdapGroup() g.name = 'newgroup' g.gid = 1010 g.usernames = ['someuser', 'foouser'] g.save() new = LdapGroup.objects.get(name='newgroup') self.assertEquals(new.name, 'newgroup') self.assertEquals(new.gid, 1010) self.assertEquals(new.usernames, ['someuser', 'foouser']) class UserTestCase(TestCase): directory = dict([admin, groups, people, foouser]) @classmethod def setUpClass(cls): cls.mockldap = MockLdap(cls.directory) @classmethod def tearDownClass(cls): del cls.mockldap def setUp(self): self.mockldap.start() self.ldapobj = self.mockldap[settings.DATABASES['ldap']['NAME']] def tearDown(self): self.mockldap.stop() del self.ldapobj def test_get(self): u = LdapUser.objects.get(username='foouser') self.assertEquals(u.first_name, u'Fôo') self.assertEquals(u.last_name, u'Usér') self.assertEquals(u.full_name, u'Fôo Usér') self.assertEquals(u.group, 1000) self.assertEquals(u.home_directory, '/home/foouser') self.assertEquals(u.uid, 2000) self.assertEquals(u.username, 'foouser') self.assertEquals(u.photo, '\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01' '\x01\x00H\x00H\x00\x00\xff\xfe\x00\x1cCreated with ' 'GIMP on a Mac\xff\xdb\x00C\x00\x05\x03\x04\x04\x04' '\x03\x05\x04\x04\x04\x05\x05\x05\x06\x07\x0c\x08' '\x07\x07\x07\x07\x0f\x0b\x0b\t\x0c\x11\x0f\x12\x12' '\x11\x0f\x11\x11\x13\x16\x1c\x17\x13\x14\x1a\x15' '\x11\x11\x18!\x18\x1a\x1d\x1d\x1f\x1f\x1f\x13\x17' '"$"\x1e$\x1c\x1e\x1f\x1e\xff\xdb\x00C\x01\x05\x05' '\x05\x07\x06\x07\x0e\x08\x08\x0e\x1e\x14\x11\x14' '\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' '\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' '\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' '\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e' '\x1e\x1e\xff\xc0\x00\x11\x08\x00\x08\x00\x08\x03' '\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x15' '\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' '\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x19\x10' '\x00\x03\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00' '\x00\x00\x00\x00\x00\x01\x02\x06\x11A\xff\xc4\x00' '\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' '\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x14\x11' '\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' '\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00' '\x02\x11\x03\x11\x00?\x00\x9d\xf29wU5Q\xd6\xfd\x00' '\x01\xff\xd9') self.assertEquals(u.date_of_birth, datetime.date(1982, 6, 12)) self.assertEquals(u.latitude, 3.14) self.assertRaises(LdapUser.DoesNotExist, LdapUser.objects.get, username='does_not_exist') def test_update(self): u = LdapUser.objects.get(username='foouser') u.first_name = u'Fôo2' u.save() # make sure DN gets updated if we change the pk u.username = 'foouser2' u.save() self.assertEquals(u.dn, 'uid=foouser2,%s' % LdapUser.base_dn) class ScopedTestCase(TestCase): directory = dict([admin, groups, people, foogroup, contacts]) @classmethod def setUpClass(cls): cls.mockldap = MockLdap(cls.directory) @classmethod def tearDownClass(cls): del cls.mockldap def setUp(self): self.mockldap.start() self.ldapobj = self.mockldap[settings.DATABASES['ldap']['NAME']] self.scoped_model = LdapGroup.scoped("ou=contacts,%s" % LdapGroup.base_dn) def tearDown(self): self.mockldap.stop() del self.ldapobj def test_scope(self): ScopedGroup = self.scoped_model qs = LdapGroup.objects.all() self.assertEquals(qs.count(), 1) qs = ScopedGroup.objects.all() self.assertEquals(qs.count(), 0) # create scoped group g2 = ScopedGroup() g2.name = "scopedgroup" g2.gid = 5000 g2.save() qs = LdapGroup.objects.all() self.assertEquals(qs.count(), 2) qs = ScopedGroup.objects.all() self.assertEquals(qs.count(), 1) g2 = ScopedGroup.objects.get(name="scopedgroup") self.assertEquals(g2.name, u'scopedgroup') self.assertEquals(g2.gid, 5000) class AdminTestCase(TestCase): fixtures = ['test_users.json'] directory = dict([admin, groups, people, foouser, foogroup, bargroup]) @classmethod def setUpClass(cls): cls.mockldap = MockLdap(cls.directory) @classmethod def tearDownClass(cls): del cls.mockldap def setUp(self): self.mockldap.start() self.ldapobj = self.mockldap[settings.DATABASES['ldap']['NAME']] self.client.login(username="test_user", password="password") def tearDown(self): self.mockldap.stop() del self.ldapobj def test_index(self): response = self.client.get('/admin/examples/') self.assertContains(response, "Ldap groups") self.assertContains(response, "Ldap users") def test_group_list(self): response = self.client.get('/admin/examples/ldapgroup/') self.assertContains(response, "Ldap groups") self.assertContains(response, "foogroup") self.assertContains(response, "1000") # order by name response = self.client.get('/admin/examples/ldapgroup/?o=1') self.assertContains(response, "Ldap groups") self.assertContains(response, "foogroup") self.assertContains(response, "1000") # order by gid response = self.client.get('/admin/examples/ldapgroup/?o=2') self.assertContains(response, "Ldap groups") self.assertContains(response, "foogroup") self.assertContains(response, "1000") def test_group_detail(self): response = self.client.get('/admin/examples/ldapgroup/foogroup/') self.assertContains(response, "foogroup") self.assertContains(response, "1000") def test_group_add(self): response = self.client.post('/admin/examples/ldapgroup/add/', {'gid': '1002', 'name': 'wizgroup'}) self.assertRedirects(response, '/admin/examples/ldapgroup/') qs = LdapGroup.objects.all() self.assertEquals(qs.count(), 3) def test_group_delete(self): response = self.client.post( '/admin/examples/ldapgroup/foogroup/delete/', {'yes': 'post'}) self.assertRedirects(response, '/admin/examples/ldapgroup/') qs = LdapGroup.objects.all() self.assertEquals(qs.count(), 1) def test_group_search(self): self.ldapobj.search_s.seed( "ou=groups,dc=nodomain", 2, "(&(objectClass=posixGroup)(cn=*foo*))", ['dn'])([foogroup]) self.ldapobj.search_s.seed( "ou=groups,dc=nodomain", 2, "(&(objectClass=posixGroup)(cn=*foo*))", ['gidNumber', 'cn', 'memberUid'])([foogroup]) response = self.client.get('/admin/examples/ldapgroup/?q=foo') self.assertContains(response, "Ldap groups") self.assertContains(response, "foogroup") self.assertContains(response, "1000") def test_user_list(self): response = self.client.get('/admin/examples/ldapuser/') self.assertContains(response, "Ldap users") self.assertContains(response, "foouser") self.assertContains(response, "2000") # order by username response = self.client.get('/admin/examples/ldapuser/?o=1') self.assertContains(response, "Ldap users") self.assertContains(response, "foouser") self.assertContains(response, "2000") # order by uid response = self.client.get('/admin/examples/ldapuser/?o=2') self.assertContains(response, "Ldap users") self.assertContains(response, "foouser") self.assertContains(response, "2000") def test_user_detail(self): response = self.client.get('/admin/examples/ldapuser/foouser/') self.assertContains(response, "foouser") self.assertContains(response, "2000") def test_user_delete(self): response = self.client.post('/admin/examples/ldapuser/foouser/delete/', {'yes': 'post'}) self.assertRedirects(response, '/admin/examples/ldapuser/') django-ldapdb-0.2.0/ldapdb/000077500000000000000000000000001224176153300154315ustar00rootroot00000000000000django-ldapdb-0.2.0/ldapdb/__init__.py000066400000000000000000000043071224176153300175460ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-ldapdb # Copyright (c) 2009-2011, Bolloré telecom # Copyright (c) 2013, Jeremy Lainé # All rights reserved. # # See AUTHORS file for a full list of contributors. # # 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. # # 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. # from django.conf import settings def escape_ldap_filter(value): value = unicode(value) return value.replace('\\', '\\5c') \ .replace('*', '\\2a') \ .replace('(', '\\28') \ .replace(')', '\\29') \ .replace('\0', '\\00') # Legacy single database support if hasattr(settings, 'LDAPDB_SERVER_URI'): from django import db from ldapdb.router import Router # Add the LDAP backend settings.DATABASES['ldap'] = { 'ENGINE': 'ldapdb.backends.ldap', 'NAME': settings.LDAPDB_SERVER_URI, 'USER': settings.LDAPDB_BIND_DN, 'PASSWORD': settings.LDAPDB_BIND_PASSWORD} # Add the LDAP router db.router.routers.append(Router()) django-ldapdb-0.2.0/ldapdb/backends/000077500000000000000000000000001224176153300172035ustar00rootroot00000000000000django-ldapdb-0.2.0/ldapdb/backends/__init__.py000066400000000000000000000000001224176153300213020ustar00rootroot00000000000000django-ldapdb-0.2.0/ldapdb/backends/ldap/000077500000000000000000000000001224176153300201235ustar00rootroot00000000000000django-ldapdb-0.2.0/ldapdb/backends/ldap/__init__.py000066400000000000000000000000001224176153300222220ustar00rootroot00000000000000django-ldapdb-0.2.0/ldapdb/backends/ldap/base.py000066400000000000000000000122321224176153300214070ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-ldapdb # Copyright (c) 2009-2011, Bolloré telecom # Copyright (c) 2013, Jeremy Lainé # All rights reserved. # # See AUTHORS file for a full list of contributors. # # 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. # # 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. # import ldap import django from django.db.backends import (BaseDatabaseFeatures, BaseDatabaseOperations, BaseDatabaseWrapper) from django.db.backends.creation import BaseDatabaseCreation class DatabaseCreation(BaseDatabaseCreation): def create_test_db(self, verbosity=1, autoclobber=False): """ Creates a test database, prompting the user for confirmation if the database already exists. Returns the name of the test database created. """ pass def destroy_test_db(self, old_database_name, verbosity=1): """ Destroy a test database, prompting the user for confirmation if the database already exists. Returns the name of the test database created. """ pass class DatabaseCursor(object): def __init__(self, ldap_connection): self.connection = ldap_connection class DatabaseFeatures(BaseDatabaseFeatures): def __init__(self, connection): self.connection = connection self.supports_transactions = False class DatabaseOperations(BaseDatabaseOperations): compiler_module = "ldapdb.backends.ldap.compiler" def quote_name(self, name): return name class DatabaseWrapper(BaseDatabaseWrapper): def __init__(self, *args, **kwargs): super(DatabaseWrapper, self).__init__(*args, **kwargs) self.charset = "utf-8" self.creation = DatabaseCreation(self) self.features = DatabaseFeatures(self) if django.VERSION > (1, 4): self.ops = DatabaseOperations(self) else: self.ops = DatabaseOperations() self.settings_dict['SUPPORTS_TRANSACTIONS'] = False def close(self): if hasattr(self, 'validate_thread_sharing'): # django >= 1.4 self.validate_thread_sharing() if self.connection is not None: self.connection.unbind_s() self.connection = None def ensure_connection(self): if self.connection is None: self.connection = ldap.initialize(self.settings_dict['NAME']) options = self.settings_dict.get('CONNECTION_OPTIONS', {}) for opt, value in options.items(): self.connection.set_option(opt, value) if self.settings_dict.get('TLS', False): self.connection.start_tls_s() self.connection.simple_bind_s( self.settings_dict['USER'], self.settings_dict['PASSWORD']) def _commit(self): pass def _cursor(self): self.ensure_connection() return DatabaseCursor(self.connection) def _rollback(self): pass def add_s(self, dn, modlist): cursor = self._cursor() return cursor.connection.add_s(dn.encode(self.charset), modlist) def delete_s(self, dn): cursor = self._cursor() return cursor.connection.delete_s(dn.encode(self.charset)) def modify_s(self, dn, modlist): cursor = self._cursor() return cursor.connection.modify_s(dn.encode(self.charset), modlist) def rename_s(self, dn, newrdn): cursor = self._cursor() return cursor.connection.rename_s(dn.encode(self.charset), newrdn.encode(self.charset)) def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None): cursor = self._cursor() results = cursor.connection.search_s(base, scope, filterstr.encode(self.charset), attrlist) output = [] for dn, attrs in results: output.append((dn.decode(self.charset), attrs)) return output django-ldapdb-0.2.0/ldapdb/backends/ldap/compiler.py000066400000000000000000000212001224176153300223020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-ldapdb # Copyright (c) 2009-2011, Bolloré telecom # Copyright (c) 2013, Jeremy Lainé # All rights reserved. # # See AUTHORS file for a full list of contributors. # # 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. # # 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. # import ldap from django.db.models.sql import aggregates, compiler from django.db.models.sql.where import AND, OR def get_lookup_operator(lookup_type): if lookup_type == 'gte': return '>=' elif lookup_type == 'lte': return '<=' else: return '=' def query_as_ldap(query): # starting with django 1.6 we can receive empty querysets if hasattr(query, 'is_empty') and query.is_empty(): return filterstr = ''.join(['(objectClass=%s)' % cls for cls in query.model.object_classes]) sql, params = where_as_ldap(query.where) filterstr += sql return '(&%s)' % filterstr def where_as_ldap(self): bits = [] for item in self.children: if hasattr(item, 'as_sql'): sql, params = where_as_ldap(item) bits.append(sql) continue constraint, lookup_type, y, values = item comp = get_lookup_operator(lookup_type) if lookup_type == 'in': equal_bits = ["(%s%s%s)" % (constraint.col, comp, value) for value in values] clause = '(|%s)' % ''.join(equal_bits) else: clause = "(%s%s%s)" % (constraint.col, comp, values) bits.append(clause) if not len(bits): return '', [] if len(bits) == 1: sql_string = bits[0] elif self.connector == AND: sql_string = '(&%s)' % ''.join(bits) elif self.connector == OR: sql_string = '(|%s)' % ''.join(bits) else: raise Exception("Unhandled WHERE connector: %s" % self.connector) if self.negated: sql_string = ('(!%s)' % sql_string) return sql_string, [] class SQLCompiler(object): def __init__(self, query, connection, using): self.query = query self.connection = connection self.using = using def execute_sql(self, result_type=compiler.MULTI): if result_type != compiler.SINGLE: raise Exception("LDAP does not support MULTI queries") for key, aggregate in self.query.aggregate_select.items(): if not isinstance(aggregate, aggregates.Count): raise Exception("Unsupported aggregate %s" % aggregate) filterstr = query_as_ldap(self.query) if not filterstr: return try: vals = self.connection.search_s( self.query.model.base_dn, self.query.model.search_scope, filterstr=filterstr, attrlist=['dn'], ) except ldap.NO_SUCH_OBJECT: vals = [] if not vals: return None output = [] for alias, col in self.query.extra_select.iteritems(): output.append(col[0]) for key, aggregate in self.query.aggregate_select.items(): if isinstance(aggregate, aggregates.Count): output.append(len(vals)) else: output.append(None) return output def results_iter(self): filterstr = query_as_ldap(self.query) if not filterstr: return if hasattr(self.query, 'select_fields') and len(self.query.select_fields): # django < 1.6 fields = self.query.select_fields elif len(self.query.select): # django >= 1.6 fields = [x.field for x in self.query.select] else: fields = self.query.model._meta.fields attrlist = [x.db_column for x in fields if x.db_column] try: vals = self.connection.search_s( self.query.model.base_dn, self.query.model.search_scope, filterstr=filterstr, attrlist=attrlist, ) except ldap.NO_SUCH_OBJECT: return # perform sorting if self.query.extra_order_by: ordering = self.query.extra_order_by elif not self.query.default_ordering: ordering = self.query.order_by else: ordering = self.query.order_by or self.query.model._meta.ordering def cmpvals(x, y): for fieldname in ordering: if fieldname.startswith('-'): fieldname = fieldname[1:] negate = True else: negate = False if fieldname == 'pk': fieldname = self.query.model._meta.pk.name field = self.query.model._meta.get_field(fieldname) attr_x = field.from_ldap(x[1].get(field.db_column, []), connection=self.connection) attr_y = field.from_ldap(y[1].get(field.db_column, []), connection=self.connection) # perform case insensitive comparison if hasattr(attr_x, 'lower'): attr_x = attr_x.lower() if hasattr(attr_y, 'lower'): attr_y = attr_y.lower() val = negate and cmp(attr_y, attr_x) or cmp(attr_x, attr_y) if val: return val return 0 vals = sorted(vals, cmp=cmpvals) # process results pos = 0 results = [] for dn, attrs in vals: # FIXME : This is not optimal, we retrieve more results than we # need but there is probably no other options as we can't perform # ordering server side. if (self.query.low_mark and pos < self.query.low_mark) or \ (self.query.high_mark is not None and pos >= self.query.high_mark): pos += 1 continue row = [] for field in iter(fields): if field.attname == 'dn': row.append(dn) elif hasattr(field, 'from_ldap'): row.append(field.from_ldap(attrs.get(field.db_column, []), connection=self.connection)) else: row.append(None) if self.query.distinct: if row in results: continue else: results.append(row) yield row pos += 1 class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler): pass class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler): def execute_sql(self, result_type=compiler.MULTI): filterstr = query_as_ldap(self.query) if not filterstr: return try: vals = self.connection.search_s( self.query.model.base_dn, self.query.model.search_scope, filterstr=filterstr, attrlist=['dn'], ) except ldap.NO_SUCH_OBJECT: return # FIXME : there is probably a more efficient way to do this for dn, attrs in vals: self.connection.delete_s(dn) class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler): pass class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler): pass class SQLDateCompiler(compiler.SQLDateCompiler, SQLCompiler): pass django-ldapdb-0.2.0/ldapdb/models/000077500000000000000000000000001224176153300167145ustar00rootroot00000000000000django-ldapdb-0.2.0/ldapdb/models/__init__.py000066400000000000000000000030201224176153300210200ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-ldapdb # Copyright (c) 2009-2011, Bolloré telecom # Copyright (c) 2013, Jeremy Lainé # All rights reserved. # # See AUTHORS file for a full list of contributors. # # 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. # # 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. # from ldapdb.models.base import Model # noqa django-ldapdb-0.2.0/ldapdb/models/base.py000066400000000000000000000142651224176153300202100ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-ldapdb # Copyright (c) 2009-2011, Bolloré telecom # Copyright (c) 2013, Jeremy Lainé # All rights reserved. # # See AUTHORS file for a full list of contributors. # # 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. # # 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. # import ldap import logging import django.db.models from django.db import connections, router from django.db.models import signals import ldapdb # noqa logger = logging.getLogger('ldapdb') class Model(django.db.models.base.Model): """ Base class for all LDAP models. """ dn = django.db.models.fields.CharField(max_length=200) # meta-data base_dn = None search_scope = ldap.SCOPE_SUBTREE object_classes = ['top'] def __init__(self, *args, **kwargs): super(Model, self).__init__(*args, **kwargs) self.saved_pk = self.pk def build_rdn(self): """ Build the Relative Distinguished Name for this entry. """ bits = [] for field in self._meta.fields: if field.db_column and field.primary_key: bits.append("%s=%s" % (field.db_column, getattr(self, field.name))) if not len(bits): raise Exception("Could not build Distinguished Name") return '+'.join(bits) def build_dn(self): """ Build the Distinguished Name for this entry. """ return "%s,%s" % (self.build_rdn(), self.base_dn) raise Exception("Could not build Distinguished Name") def delete(self, using=None): """ Delete this entry. """ using = using or router.db_for_write(self.__class__, instance=self) connection = connections[using] logger.debug("Deleting LDAP entry %s" % self.dn) connection.delete_s(self.dn) signals.post_delete.send(sender=self.__class__, instance=self) def save(self, using=None): """ Saves the current instance. """ using = using or router.db_for_write(self.__class__, instance=self) connection = connections[using] if not self.dn: # create a new entry record_exists = False entry = [('objectClass', self.object_classes)] new_dn = self.build_dn() for field in self._meta.fields: if not field.db_column: continue value = getattr(self, field.name) if value: entry.append((field.db_column, field.get_db_prep_save( value, connection=connection))) logger.debug("Creating new LDAP entry %s" % new_dn) connection.add_s(new_dn, entry) # update object self.dn = new_dn else: # update an existing entry record_exists = True modlist = [] orig = self.__class__.objects.get(pk=self.saved_pk) for field in self._meta.fields: if not field.db_column: continue old_value = getattr(orig, field.name, None) new_value = getattr(self, field.name, None) if old_value != new_value: if new_value: modlist.append( (ldap.MOD_REPLACE, field.db_column, field.get_db_prep_save(new_value, connection=connection))) elif old_value: modlist.append((ldap.MOD_DELETE, field.db_column, None)) if len(modlist): # handle renaming new_dn = self.build_dn() if new_dn != self.dn: logger.debug("Renaming LDAP entry %s to %s" % (self.dn, new_dn)) connection.rename_s(self.dn, self.build_rdn()) self.dn = new_dn logger.debug("Modifying existing LDAP entry %s" % self.dn) connection.modify_s(self.dn, modlist) else: logger.debug("No changes to be saved to LDAP entry %s" % self.dn) # done self.saved_pk = self.pk signals.post_save.send(sender=self.__class__, instance=self, created=(not record_exists)) @classmethod def scoped(base_class, base_dn): """ Returns a copy of the current class with a different base_dn. """ class Meta: proxy = True import re suffix = re.sub('[=,]', '_', base_dn) name = "%s_%s" % (base_class.__name__, str(suffix)) new_class = type(name, (base_class,), { 'base_dn': base_dn, '__module__': base_class.__module__, 'Meta': Meta}) return new_class class Meta: abstract = True django-ldapdb-0.2.0/ldapdb/models/fields.py000066400000000000000000000200121224176153300205270ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-ldapdb # Copyright (c) 2009-2011, Bolloré telecom # Copyright (c) 2013, Jeremy Lainé # All rights reserved. # # See AUTHORS file for a full list of contributors. # # 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. # # 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. # from django.db.models import fields, SubfieldBase from ldapdb import escape_ldap_filter import datetime class CharField(fields.CharField): def __init__(self, *args, **kwargs): kwargs['max_length'] = 200 super(CharField, self).__init__(*args, **kwargs) def from_ldap(self, value, connection): if len(value) == 0: return '' else: return value[0].decode(connection.charset) def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): "Returns field's value prepared for database lookup." if lookup_type == 'endswith': return ["*%s" % escape_ldap_filter(value)] elif lookup_type == 'startswith': return ["%s*" % escape_ldap_filter(value)] elif lookup_type in ['contains', 'icontains']: return ["*%s*" % escape_ldap_filter(value)] elif lookup_type == 'exact': return [escape_ldap_filter(value)] elif lookup_type == 'in': return [escape_ldap_filter(v) for v in value] raise TypeError("CharField has invalid lookup: %s" % lookup_type) def get_db_prep_save(self, value, connection): return [value.encode(connection.charset)] def get_prep_lookup(self, lookup_type, value): "Perform preliminary non-db specific lookup checks and conversions" if lookup_type == 'endswith': return "*%s" % escape_ldap_filter(value) elif lookup_type == 'startswith': return "%s*" % escape_ldap_filter(value) elif lookup_type in ['contains', 'icontains']: return "*%s*" % escape_ldap_filter(value) elif lookup_type == 'exact': return escape_ldap_filter(value) elif lookup_type == 'in': return [escape_ldap_filter(v) for v in value] raise TypeError("CharField has invalid lookup: %s" % lookup_type) class ImageField(fields.Field): def from_ldap(self, value, connection): if len(value) == 0: return '' else: return value[0] def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): "Returns field's value prepared for database lookup." return [self.get_prep_lookup(lookup_type, value)] def get_db_prep_save(self, value, connection): return [value] def get_prep_lookup(self, lookup_type, value): "Perform preliminary non-db specific lookup checks and conversions" raise TypeError("ImageField has invalid lookup: %s" % lookup_type) class IntegerField(fields.IntegerField): def from_ldap(self, value, connection): if len(value) == 0: return 0 else: return int(value[0]) def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): "Returns field's value prepared for database lookup." return [self.get_prep_lookup(lookup_type, value)] def get_db_prep_save(self, value, connection): return [str(value)] def get_prep_lookup(self, lookup_type, value): "Perform preliminary non-db specific lookup checks and conversions" if lookup_type in ('exact', 'gte', 'lte'): return value raise TypeError("IntegerField has invalid lookup: %s" % lookup_type) class FloatField(fields.FloatField): def from_ldap(self, value, connection): if len(value) == 0: return 0.0 else: return float(value[0]) def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): "Returns field's value prepared for database lookup." return [self.get_prep_lookup(lookup_type, value)] def get_db_prep_save(self, value, connection): return [str(value)] def get_prep_lookup(self, lookup_type, value): "Perform preliminary non-db specific lookup checks and conversions" if lookup_type in ('exact', 'gte', 'lte'): return value raise TypeError("FloatField has invalid lookup: %s" % lookup_type) class ListField(fields.Field): __metaclass__ = SubfieldBase def from_ldap(self, value, connection): return value def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): "Returns field's value prepared for database lookup." return [self.get_prep_lookup(lookup_type, value)] def get_db_prep_save(self, value, connection): return [x.encode(connection.charset) for x in value] def get_prep_lookup(self, lookup_type, value): "Perform preliminary non-db specific lookup checks and conversions" if lookup_type == 'contains': return escape_ldap_filter(value) raise TypeError("ListField has invalid lookup: %s" % lookup_type) def to_python(self, value): if not value: return [] return value class DateField(fields.DateField): """ A text field containing date, in specified format. The format can be specified as 'format' argument, as strptime() format string. It defaults to ISO8601 (%Y-%m-%d). Note: 'lte' and 'gte' lookups are done string-wise. Therefore, they will onlywork correctly on Y-m-d dates with constant component widths. """ def __init__(self, *args, **kwargs): if 'format' in kwargs: self._date_format = kwargs.pop('format') else: self._date_format = '%Y-%m-%d' super(DateField, self).__init__(*args, **kwargs) def from_ldap(self, value, connection): if len(value) == 0: return None else: return datetime.datetime.strptime(value[0], self._date_format).date() def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): "Returns field's value prepared for database lookup." return [self.get_prep_lookup(lookup_type, value)] def get_db_prep_save(self, value, connection): if not isinstance(value, datetime.date) \ and not isinstance(value, datetime.datetime): raise ValueError( 'DateField can be only set to a datetime.date instance') return [value.strftime(self._date_format)] def get_prep_lookup(self, lookup_type, value): "Perform preliminary non-db specific lookup checks and conversions" if lookup_type in ('exact',): return value raise TypeError("DateField has invalid lookup: %s" % lookup_type) django-ldapdb-0.2.0/ldapdb/router.py000066400000000000000000000053251224176153300173300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-ldapdb # Copyright (c) 2009-2011, Bolloré telecom # Copyright (c) 2013, Jeremy Lainé # All rights reserved. # # See AUTHORS file for a full list of contributors. # # 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. # # 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. # def is_ldap_model(model): # FIXME: there is probably a better check than testing 'base_dn' return hasattr(model, 'base_dn') class Router(object): """ A router to point database operations on LDAP models to the LDAP database. NOTE: if you have more than one LDAP database, you will need to write your own router. """ def __init__(self): "Find the name of the LDAP database" from django.conf import settings self.ldap_alias = None for alias, settings_dict in settings.DATABASES.items(): if settings_dict['ENGINE'] == 'ldapdb.backends.ldap': self.ldap_alias = alias break def allow_syncdb(self, db, model): "Do not create tables for LDAP models" if is_ldap_model(model): return db == self.ldap_alias return None def db_for_read(self, model, **hints): "Point all operations on LDAP models to the LDAP database" if is_ldap_model(model): return self.ldap_alias return None def db_for_write(self, model, **hints): "Point all operations on LDAP models to the LDAP database" if is_ldap_model(model): return self.ldap_alias return None django-ldapdb-0.2.0/ldapdb/tests.py000066400000000000000000000161251224176153300171520ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-ldapdb # Copyright (c) 2009-2011, Bolloré telecom # Copyright (c) 2013, Jeremy Lainé # All rights reserved. # # See AUTHORS file for a full list of contributors. # # 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. # # 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. # from django.test import TestCase from django.db.models.sql.where import Constraint, AND, OR, WhereNode from ldapdb import escape_ldap_filter from ldapdb.backends.ldap.compiler import where_as_ldap from ldapdb.models.fields import (CharField, IntegerField, FloatField, ListField, DateField) class WhereTestCase(TestCase): def test_escape(self): self.assertEquals(escape_ldap_filter(u'fôöbàr'), u'fôöbàr') self.assertEquals(escape_ldap_filter('foo*bar'), 'foo\\2abar') self.assertEquals(escape_ldap_filter('foo(bar'), 'foo\\28bar') self.assertEquals(escape_ldap_filter('foo)bar'), 'foo\\29bar') self.assertEquals(escape_ldap_filter('foo\\bar'), 'foo\\5cbar') self.assertEquals(escape_ldap_filter('foo\\bar*wiz'), 'foo\\5cbar\\2awiz') def test_char_field_exact(self): where = WhereNode() where.add((Constraint("cn", "cn", CharField()), 'exact', "test"), AND) self.assertEquals(where_as_ldap(where), ("(cn=test)", [])) where = WhereNode() where.add((Constraint("cn", "cn", CharField()), 'exact', "(test)"), AND) self.assertEquals(where_as_ldap(where), ("(cn=\\28test\\29)", [])) def test_char_field_in(self): where = WhereNode() where.add((Constraint("cn", "cn", CharField()), 'in', ["foo", "bar"]), AND) self.assertEquals(where_as_ldap(where), ("(|(cn=foo)(cn=bar))", [])) where = WhereNode() where.add((Constraint("cn", "cn", CharField()), 'in', ["(foo)", "(bar)"]), AND) self.assertEquals(where_as_ldap(where), ("(|(cn=\\28foo\\29)(cn=\\28bar\\29))", [])) def test_char_field_startswith(self): where = WhereNode() where.add((Constraint("cn", "cn", CharField()), 'startswith', "test"), AND) self.assertEquals(where_as_ldap(where), ("(cn=test*)", [])) where = WhereNode() where.add((Constraint("cn", "cn", CharField()), 'startswith', "te*st"), AND) self.assertEquals(where_as_ldap(where), ("(cn=te\\2ast*)", [])) def test_char_field_endswith(self): where = WhereNode() where.add((Constraint("cn", "cn", CharField()), 'endswith', "test"), AND) self.assertEquals(where_as_ldap(where), ("(cn=*test)", [])) where = WhereNode() where.add((Constraint("cn", "cn", CharField()), 'endswith', "te*st"), AND) self.assertEquals(where_as_ldap(where), ("(cn=*te\\2ast)", [])) def test_char_field_contains(self): where = WhereNode() where.add((Constraint("cn", "cn", CharField()), 'contains', "test"), AND) self.assertEquals(where_as_ldap(where), ("(cn=*test*)", [])) where = WhereNode() where.add((Constraint("cn", "cn", CharField()), 'contains', "te*st"), AND) self.assertEquals(where_as_ldap(where), ("(cn=*te\\2ast*)", [])) def test_integer_field(self): where = WhereNode() where.add((Constraint("uid", "uid", IntegerField()), 'exact', 1), AND) self.assertEquals(where_as_ldap(where), ("(uid=1)", [])) where = WhereNode() where.add((Constraint("uid", "uid", IntegerField()), 'gte', 1), AND) self.assertEquals(where_as_ldap(where), ("(uid>=1)", [])) where = WhereNode() where.add((Constraint("uid", "uid", IntegerField()), 'lte', 1), AND) self.assertEquals(where_as_ldap(where), ("(uid<=1)", [])) def test_float_field(self): where = WhereNode() where.add((Constraint("uid", "uid", FloatField()), 'exact', 1.2), AND) self.assertEquals(where_as_ldap(where), ("(uid=1.2)", [])) where = WhereNode() where.add((Constraint("uid", "uid", FloatField()), 'gte', 1.2), AND) self.assertEquals(where_as_ldap(where), ("(uid>=1.2)", [])) where = WhereNode() where.add((Constraint("uid", "uid", FloatField()), 'lte', 1.2), AND) self.assertEquals(where_as_ldap(where), ("(uid<=1.2)", [])) def test_list_field_contains(self): where = WhereNode() where.add((Constraint("memberUid", "memberUid", ListField()), 'contains', 'foouser'), AND) self.assertEquals(where_as_ldap(where), ("(memberUid=foouser)", [])) where = WhereNode() where.add((Constraint("memberUid", "memberUid", ListField()), 'contains', '(foouser)'), AND) self.assertEquals(where_as_ldap(where), ("(memberUid=\\28foouser\\29)", [])) def test_date_field(self): where = WhereNode() where.add((Constraint("birthday", "birthday", DateField()), 'exact', '2013-09-03'), AND) self.assertEquals(where_as_ldap(where), ("(birthday=2013-09-03)", [])) def test_and(self): where = WhereNode() where.add((Constraint("cn", "cn", CharField()), 'exact', "foo"), AND) where.add((Constraint("givenName", "givenName", CharField()), 'exact', "bar"), AND) self.assertEquals(where_as_ldap(where), ("(&(cn=foo)(givenName=bar))", [])) def test_or(self): where = WhereNode() where.add((Constraint("cn", "cn", CharField()), 'exact', "foo"), AND) where.add((Constraint("givenName", "givenName", CharField()), 'exact', "bar"), OR) self.assertEquals(where_as_ldap(where), ("(|(cn=foo)(givenName=bar))", [])) django-ldapdb-0.2.0/manage.py000077500000000000000000000003621224176153300160110ustar00rootroot00000000000000#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) django-ldapdb-0.2.0/settings.py000066400000000000000000000062231224176153300164200ustar00rootroot00000000000000# Django settings for django-ldapdb project. import ldap DEBUG = True TEMPLATE_DEBUG = DEBUG ADMINS = ( # ('Your Name', 'your_email@domain.com'), ) MANAGERS = ADMINS DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'ldapdb.db', 'USER': '', 'PASSWORD': '', 'HOST': '', 'PORT': '', }, 'ldap': { 'ENGINE': 'ldapdb.backends.ldap', 'NAME': 'ldap://localhost', 'USER': 'cn=admin,dc=nodomain', 'PASSWORD': 'test', 'TLS': True, 'CONNECTION_OPTIONS': { ldap.OPT_X_TLS_DEMAND: True, } } } DATABASE_ROUTERS = ['ldapdb.router.Router'] # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. # On Unix systems, a value of None will cause Django to use the same # timezone as the operating system. # If running in a Windows environment this must be set to the same as your # system time zone. TIME_ZONE = 'America/Chicago' # Language code for this installation. All choices can be found here: # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en-us' SITE_ID = 1 # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. USE_I18N = True # If you set this to False, Django will not format dates, numbers and # calendars according to the current locale USE_L10N = True # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: "/home/media/media.lawrence.com/" MEDIA_ROOT = '' # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash if there is a path component (optional in other cases). # Examples: "http://media.lawrence.com", "http://example.com/media/" MEDIA_URL = '' # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". ADMIN_MEDIA_PREFIX = '/media/' # Make this unique, and don't share it with anybody. SECRET_KEY = 'some_random_secret_key' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', # 'django.template.loaders.eggs.Loader', ) MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', ) ROOT_URLCONF = 'urls' STATIC_URL = '/static/' TEMPLATE_DIRS = ( # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. ) INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'ldapdb', 'examples', 'django.contrib.admin', ) django-ldapdb-0.2.0/setup.py000077500000000000000000000021711224176153300157210ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup setup( name="django-ldapdb", version="0.2.0", description=u"An LDAP database backend for Django", long_description=open('README.md').read(), url="https://github.com/jlaine/django-ldapdb", author="Jeremy Laine", author_email="jeremy.laine@m4x.org", packages=['ldapdb', 'ldapdb.backends', 'ldapdb.backends.ldap', 'ldapdb.models'], classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Programming Language :: Python", "Framework :: Django", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Topic :: Internet :: WWW/HTTP", "Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP", "Topic :: Software Development :: Libraries :: Python Modules", ], keywords=['django', 'ldap', 'database'], install_requires=[ 'django>=1.2', 'python-ldap>=2.0', ], setup_requires=[ 'setuptools>=0.6c11', ], tests_require=[ 'mockldap>=0.1', ] ) django-ldapdb-0.2.0/urls.py000066400000000000000000000005361224176153300155460ustar00rootroot00000000000000try: from django.conf.urls import include, patterns, url except ImportError: from django.conf.urls.defaults import include, patterns, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns( '', url(r'^admin/doc/', include('django.contrib.admindocs.urls')), url(r'^admin/', include(admin.site.urls)), )