[Python-modules-commits] [django-auth-ldap] 01/08: Import django-auth-ldap_1.2.12+dfsg.orig.tar.gz

Michael Fladischer fladi at moszumanska.debian.org
Sun May 28 09:26:20 UTC 2017


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

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

commit eb4338e4d7a8bd0af8b130f75a9d3ad09cca4612
Author: Michael Fladischer <FladischerMichael at fladi.at>
Date:   Wed May 24 10:45:58 2017 +0200

    Import django-auth-ldap_1.2.12+dfsg.orig.tar.gz
---
 CHANGES                            |   7 ++
 PKG-INFO                           |   2 +-
 django_auth_ldap.egg-info/PKG-INFO |   2 +-
 django_auth_ldap/__init__.py       |   2 +-
 django_auth_ldap/backend.py        |  63 +++++++++++++----
 django_auth_ldap/config.py         |  84 +++++++++++++++++++++++
 django_auth_ldap/tests.py          | 137 +++++++++++++++++++++++++++++++++++--
 docs/source/conf.py                |   2 +-
 docs/source/groups.rst             |  23 +++++++
 docs/source/reference.rst          |  59 +++++++++++++---
 docs/source/users.rst              |  11 +--
 setup.py                           |   2 +-
 test/.coveragerc                   |   1 +
 13 files changed, 358 insertions(+), 37 deletions(-)

diff --git a/CHANGES b/CHANGES
index fe813c1..db447f4 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,10 @@
+v1.2.12 - 2017-05-20 - Complex group queries
+--------------------------------------------
+
+- Support for complex group queries via
+  :class:`~django_auth_ldap.config.LDAPGroupQuery`.
+
+
 v1.2.11 - 2017-04-22 - Testing and debugging cleanup
 ----------------------------------------------------
 
diff --git a/PKG-INFO b/PKG-INFO
index 9d03581..8f0b616 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: django-auth-ldap
-Version: 1.2.11
+Version: 1.2.12
 Summary: Django LDAP authentication backend
 Home-page: http://bitbucket.org/psagers/django-auth-ldap/
 Author: Peter Sagerson
diff --git a/django_auth_ldap.egg-info/PKG-INFO b/django_auth_ldap.egg-info/PKG-INFO
index 9d03581..8f0b616 100644
--- a/django_auth_ldap.egg-info/PKG-INFO
+++ b/django_auth_ldap.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: django-auth-ldap
-Version: 1.2.11
+Version: 1.2.12
 Summary: Django LDAP authentication backend
 Home-page: http://bitbucket.org/psagers/django-auth-ldap/
 Author: Peter Sagerson
diff --git a/django_auth_ldap/__init__.py b/django_auth_ldap/__init__.py
index a158771..4bdfabd 100644
--- a/django_auth_ldap/__init__.py
+++ b/django_auth_ldap/__init__.py
@@ -1,2 +1,2 @@
-version = (1, 2, 11)
+version = (1, 2, 12)
 version_string = '.'.join(map(str, version))
diff --git a/django_auth_ldap/backend.py b/django_auth_ldap/backend.py
index 81ad4ae..4c6d698 100644
--- a/django_auth_ldap/backend.py
+++ b/django_auth_ldap/backend.py
@@ -45,11 +45,13 @@ information will be user_dn or user_info.
 Additional classes can be found in the config module next to this one.
 """
 
+import copy
+from functools import reduce
 import ldap
+import operator
+import pprint
 import sys
 import traceback
-import pprint
-import copy
 
 from django.contrib.auth.models import User, Group, Permission
 import django.conf
@@ -86,7 +88,7 @@ try:
 except NameError:
     basestring = str
 
-from django_auth_ldap.config import _LDAPConfig, LDAPSearch
+from django_auth_ldap.config import _LDAPConfig, LDAPGroupQuery, LDAPSearch
 
 
 logger = _LDAPConfig.get_logger()
@@ -516,9 +518,11 @@ class _LDAPUser(object):
         required_group_dn = self.settings.REQUIRE_GROUP
 
         if required_group_dn is not None:
-            is_member = self._get_groups().is_member_of(required_group_dn)
-            if not is_member:
-                raise self.AuthenticationFailed("user is not a member of AUTH_LDAP_REQUIRE_GROUP")
+            if not isinstance(required_group_dn, LDAPGroupQuery):
+                required_group_dn = LDAPGroupQuery(required_group_dn)
+            result = required_group_dn.resolve(self)
+            if not result:
+                raise self.AuthenticationFailed("user does not satisfy AUTH_LDAP_REQUIRE_GROUP")
 
         return True
 
@@ -532,7 +536,7 @@ class _LDAPUser(object):
         if denied_group_dn is not None:
             is_member = self._get_groups().is_member_of(denied_group_dn)
             if is_member:
-                raise self.AuthenticationFailed("user is a member of AUTH_LDAP_DENY_GROUP")
+                raise self.AuthenticationFailed("user does not satisfy AUTH_LDAP_DENY_GROUP")
 
         return True
 
@@ -601,9 +605,12 @@ class _LDAPUser(object):
 
     def _populate_user_from_group_memberships(self):
         for field, group_dns in self.settings.USER_FLAGS_BY_GROUP.items():
-            if isinstance(group_dns, basestring):
-                group_dns = [group_dns]
-            value = any(self._get_groups().is_member_of(dn) for dn in group_dns)
+            try:
+                query = self._normalize_group_dns(group_dns)
+            except ValueError as e:
+                raise ImproperlyConfigured("{}: {}", self.settings._name('USER_FLAGS_BY_GROUP'), e)
+
+            value = query.resolve(self)
             setattr(self._user, field, value)
 
     def _should_populate_profile(self):
@@ -658,14 +665,37 @@ class _LDAPUser(object):
         save_profile = False
 
         for field, group_dns in self.settings.PROFILE_FLAGS_BY_GROUP.items():
-            if isinstance(group_dns, basestring):
-                group_dns = [group_dns]
-            value = any(self._get_groups().is_member_of(dn) for dn in group_dns)
+            try:
+                query = self._normalize_group_dns(group_dns)
+            except ValueError as e:
+                raise ImproperlyConfigured("{}: {}", self.settings._name('PROFILE_FLAGS_BY_GROUP'), e)
+
+            value = query.resolve(self)
             setattr(profile, field, value)
             save_profile = True
 
         return save_profile
 
+    def _normalize_group_dns(self, group_dns):
+        """
+        Converts one or more group DNs to an LDAPGroupQuery.
+
+        group_dns may be a string, a non-empty list or tuple of strings, or an
+        LDAPGroupQuery. The result will be an LDAPGroupQuery. A list or tuple
+        will be joined with the | operator.
+
+        """
+        if isinstance(group_dns, LDAPGroupQuery):
+            query = group_dns
+        elif isinstance(group_dns, basestring):
+            query = LDAPGroupQuery(group_dns)
+        elif isinstance(group_dns, (list, tuple)) and len(group_dns) > 0:
+            query = reduce(operator.or_, map(LDAPGroupQuery, group_dns))
+        else:
+            raise ValueError(group_dns)
+
+        return query
+
     def _mirror_groups(self):
         """
         Mirrors the user's LDAP groups in the Django database and updates the
@@ -875,6 +905,8 @@ class LDAPSettings(object):
     instance will contain all of our settings as attributes, with default values
     if they are not specified by the configuration.
     """
+    _prefix = 'AUTH_LDAP_'
+
     defaults = {
         'ALWAYS_UPDATE_USER': True,
         'AUTHORIZE_ALL_USERS': False,
@@ -907,8 +939,13 @@ class LDAPSettings(object):
         Loads our settings from django.conf.settings, applying defaults for any
         that are omitted.
         """
+        self._prefix = prefix
+
         defaults = dict(self.defaults, **defaults)
 
         for name, default in defaults.items():
             value = getattr(django.conf.settings, prefix + name, default)
             setattr(self, name, value)
+
+    def _name(self, suffix):
+        return (self._prefix + suffix)
diff --git a/django_auth_ldap/config.py b/django_auth_ldap/config.py
index b57f052..58c612e 100644
--- a/django_auth_ldap/config.py
+++ b/django_auth_ldap/config.py
@@ -34,6 +34,7 @@ import ldap
 import logging
 import pprint
 
+from django.utils.tree import Node
 try:
     from django.utils.encoding import force_str
 except ImportError:  # Django < 1.5
@@ -635,3 +636,86 @@ class NestedOrganizationalRoleGroupType(NestedMemberDNGroupType):
     """
     def __init__(self, name_attr='cn'):
         super(NestedOrganizationalRoleGroupType, self).__init__('roleOccupant', name_attr)
+
+
+class LDAPGroupQuery(Node):
+    """
+    Represents a compound query for group membership.
+
+    This can be used to construct an arbitrarily complex group membership query
+    with AND, OR, and NOT logical operators. Construct primitive queries with a
+    group DN as the only argument. These queries can then be combined with the
+    ``&``, ``|``, and ``~`` operators.
+
+    :param str group_dn: The DN of a group to test for membership.
+
+    """
+    # Connection types
+    AND = 'AND'
+    OR = 'OR'
+    default = AND
+
+    _CONNECTORS = [AND, OR]
+
+    def __init__(self, *args, **kwargs):
+        super(LDAPGroupQuery, self).__init__(children=list(args) + list(kwargs.items()))
+
+    def __and__(self, other):
+        return self._combine(other, self.AND)
+
+    def __or__(self, other):
+        return self._combine(other, self.OR)
+
+    def __invert__(self):
+        obj = type(self)()
+        obj.add(self, self.AND)
+        obj.negate()
+
+        return obj
+
+    def _combine(self, other, conn):
+        if not isinstance(other, LDAPGroupQuery):
+            raise TypeError(other)
+        if conn not in self._CONNECTORS:
+            raise ValueError(conn)
+
+        obj = type(self)()
+        obj.connector = conn
+        obj.add(self, conn)
+        obj.add(other, conn)
+
+        return obj
+
+    def resolve(self, ldap_user, groups=None):
+        if groups is None:
+            groups = ldap_user._get_groups()
+
+        result = self.aggregator(self._resolve_children(ldap_user, groups))
+        if self.negated:
+            result = not result
+
+        return result
+
+    @property
+    def aggregator(self):
+        """
+        Returns a function for aggregating a sequence of sub-results.
+        """
+        if self.connector == self.AND:
+            aggregator = all
+        elif self.connector == self.OR:
+            aggregator = any
+        else:
+            raise ValueError(self.connector)
+
+        return aggregator
+
+    def _resolve_children(self, ldap_user, groups):
+        """
+        Generates the query result for each child.
+        """
+        for child in self.children:
+            if isinstance(child, LDAPGroupQuery):
+                yield child.resolve(ldap_user, groups)
+            else:
+                yield groups.is_member_of(child)
diff --git a/django_auth_ldap/tests.py b/django_auth_ldap/tests.py
index 61f2fcc..8661808 100644
--- a/django_auth_ldap/tests.py
+++ b/django_auth_ldap/tests.py
@@ -38,8 +38,9 @@ except ImportError:
 
 import django
 from django.conf import settings
-import django.db.models.signals
 from django.contrib.auth.models import User, Permission, Group
+from django.core.exceptions import ImproperlyConfigured
+import django.db.models.signals
 from django.test import TestCase
 
 try:
@@ -60,6 +61,7 @@ except ImportError:
 
 from django_auth_ldap.models import TestUser, TestProfile
 from django_auth_ldap import backend
+from django_auth_ldap.config import LDAPGroupQuery
 from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
 from django_auth_ldap.config import PosixGroupType, MemberDNGroupType, NestedMemberDNGroupType, NISGroupType
 from django_auth_ldap.config import GroupOfNamesType
@@ -164,6 +166,23 @@ class LDAPTest(TestCase):
         "member": ["uid=bob,ou=people,o=test"]
     })
 
+    # grouOfNames objects for LDAPGroupQuery testing
+    alice_gon = ("cn=alice_gon,ou=query_groups,o=test", {
+        "cn": ["alice_gon"],
+        "objectClass": ["groupOfNames"],
+        "member": ["uid=alice,ou=people,o=test"]
+    })
+    mutual_gon = ("cn=mutual_gon,ou=query_groups,o=test", {
+        "cn": ["mutual_gon"],
+        "objectClass": ["groupOfNames"],
+        "member": ["uid=alice,ou=people,o=test", "uid=bob,ou=people,o=test"]
+    })
+    bob_gon = ("cn=bob_gon,ou=query_groups,o=test", {
+        "cn": ["bob_gon"],
+        "objectClass": ["groupOfNames"],
+        "member": ["uid=bob,ou=people,o=test"]
+    })
+
     # nisGroup objects
     active_nis = ("cn=active_nis,ou=groups,o=test", {
         "cn": ["active_nis"],
@@ -204,6 +223,7 @@ class LDAPTest(TestCase):
     directory = dict([top, people, groups, moregroups, alice, bob, dressler,
                       nobody, active_px, staff_px, superuser_px, empty_gon,
                       active_gon, staff_gon, superuser_gon, other_gon,
+                      alice_gon, mutual_gon, bob_gon,
                       active_nis, staff_nis, superuser_nis,
                       parent_gon, nested_gon, circular_gon])
 
@@ -224,7 +244,9 @@ class LDAPTest(TestCase):
         cls.configure_logger()
         cls.mockldap = mockldap.MockLdap(cls.directory)
 
-        warnings.filterwarnings('ignore', message='.*?AUTH_PROFILE_MODULE', category=DeprecationWarning, module='django_auth_ldap')
+        warnings.filterwarnings(
+            'ignore', message='.*?AUTH_PROFILE_MODULE', category=DeprecationWarning, module='django_auth_ldap'
+        )
 
     @classmethod
     def tearDownClass(cls):
@@ -753,6 +775,95 @@ class LDAPTest(TestCase):
              'initialize', 'simple_bind_s', 'simple_bind_s', 'compare_s']
         )
 
+    def test_simple_group_query(self):
+        self._init_settings(
+            USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            GROUP_SEARCH=LDAPSearch('ou=query_groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)'),
+            GROUP_TYPE=MemberDNGroupType(member_attr='member'),
+        )
+        alice = self.backend.authenticate(username='alice', password='password')
+        query = LDAPGroupQuery('cn=alice_gon,ou=query_groups,o=test')
+        self.assertTrue(query.resolve(alice.ldap_user))
+
+    def test_negated_group_query(self):
+        self._init_settings(
+            USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            GROUP_SEARCH=LDAPSearch('ou=query_groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)'),
+            GROUP_TYPE=MemberDNGroupType(member_attr='member'),
+        )
+        alice = self.backend.authenticate(username='alice', password='password')
+        query = ~LDAPGroupQuery('cn=alice_gon,ou=query_groups,o=test')
+        self.assertFalse(query.resolve(alice.ldap_user))
+
+    def test_or_group_query(self):
+        self._init_settings(
+            USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            GROUP_SEARCH=LDAPSearch('ou=query_groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)'),
+            GROUP_TYPE=MemberDNGroupType(member_attr='member'),
+        )
+        alice = self.backend.authenticate(username='alice', password='password')
+        bob = self.backend.authenticate(username='bob', password='password')
+
+        query = (
+            LDAPGroupQuery('cn=alice_gon,ou=query_groups,o=test') |
+            LDAPGroupQuery('cn=bob_gon,ou=query_groups,o=test')
+        )
+        self.assertTrue(query.resolve(alice.ldap_user))
+        self.assertTrue(query.resolve(bob.ldap_user))
+
+    def test_and_group_query(self):
+        self._init_settings(
+            USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            GROUP_SEARCH=LDAPSearch('ou=query_groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)'),
+            GROUP_TYPE=MemberDNGroupType(member_attr='member'),
+        )
+        alice = self.backend.authenticate(username='alice', password='password')
+        bob = self.backend.authenticate(username='bob', password='password')
+
+        query = (
+            LDAPGroupQuery('cn=alice_gon,ou=query_groups,o=test') &
+            LDAPGroupQuery('cn=mutual_gon,ou=query_groups,o=test')
+        )
+        self.assertTrue(query.resolve(alice.ldap_user))
+        self.assertFalse(query.resolve(bob.ldap_user))
+
+    def test_nested_group_query(self):
+        self._init_settings(
+            USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            GROUP_SEARCH=LDAPSearch('ou=query_groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)'),
+            GROUP_TYPE=MemberDNGroupType(member_attr='member'),
+        )
+        alice = self.backend.authenticate(username='alice', password='password')
+        bob = self.backend.authenticate(username='bob', password='password')
+
+        query = (
+            (
+                LDAPGroupQuery('cn=alice_gon,ou=query_groups,o=test') &
+                LDAPGroupQuery('cn=mutual_gon,ou=query_groups,o=test')
+            ) |
+            LDAPGroupQuery('cn=bob_gon,ou=query_groups,o=test')
+        )
+        self.assertTrue(query.resolve(alice.ldap_user))
+        self.assertTrue(query.resolve(bob.ldap_user))
+
+    def test_require_group_as_group_query(self):
+        query = (
+            LDAPGroupQuery('cn=alice_gon,ou=query_groups,o=test') &
+            LDAPGroupQuery('cn=mutual_gon,ou=query_groups,o=test')
+        )
+        self._init_settings(
+            USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            GROUP_SEARCH=LDAPSearch('ou=query_groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)'),
+            GROUP_TYPE=MemberDNGroupType(member_attr='member'),
+            REQUIRE_GROUP=query
+        )
+
+        alice = self.backend.authenticate(username='alice', password='password')
+        bob = self.backend.authenticate(username='bob', password='password')
+
+        self.assertTrue(alice is not None)
+        self.assertTrue(bob is None)
+
     def test_group_union(self):
         self._init_settings(
             USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
@@ -816,7 +927,10 @@ class LDAPTest(TestCase):
         )
         alice = self.backend.authenticate(username='alice', password='password')
 
-        self.assertEqual(alice.ldap_user.group_dns, set((g[0].lower() for g in [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon])))
+        self.assertEqual(
+            alice.ldap_user.group_dns,
+            set((g[0].lower() for g in [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon]))
+        )
 
     def test_group_names(self):
         self._init_settings(
@@ -834,7 +948,7 @@ class LDAPTest(TestCase):
             GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE),
             GROUP_TYPE=MemberDNGroupType(member_attr='member'),
             USER_FLAGS_BY_GROUP={
-                'is_active': "cn=active_gon,ou=groups,o=test",
+                'is_active': LDAPGroupQuery("cn=active_gon,ou=groups,o=test"),
                 'is_staff': ["cn=empty_gon,ou=groups,o=test",
                              "cn=staff_gon,ou=groups,o=test"],
                 'is_superuser': "cn=superuser_gon,ou=groups,o=test"
@@ -851,6 +965,21 @@ class LDAPTest(TestCase):
         self.assertTrue(not bob.is_staff)
         self.assertTrue(not bob.is_superuser)
 
+    def test_user_flags_misconfigured(self):
+        self._init_settings(
+            USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE),
+            GROUP_TYPE=MemberDNGroupType(member_attr='member'),
+            USER_FLAGS_BY_GROUP={
+                'is_active': LDAPGroupQuery("cn=active_gon,ou=groups,o=test"),
+                'is_staff': [],
+                'is_superuser': "cn=superuser_gon,ou=groups,o=test"
+            }
+        )
+
+        with self.assertRaises(ImproperlyConfigured):
+            self.backend.authenticate(username='alice', password='password')
+
     def test_posix_membership(self):
         self._init_settings(
             USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
diff --git a/docs/source/conf.py b/docs/source/conf.py
index a4aad5d..e0ac649 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -59,7 +59,7 @@ copyright = u'2009, Peter Sagerson'
 # The short X.Y version.
 version = '1.1'
 # The full version, including alpha/beta/rc tags.
-release = '1.2.11'
+release = '1.2.12'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/docs/source/groups.rst b/docs/source/groups.rst
index 5ec0664..e84be4d 100644
--- a/docs/source/groups.rst
+++ b/docs/source/groups.rst
@@ -58,6 +58,8 @@ configuration must be of this type and part of the search results.
     AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
 
 
+.. _limiting-access:
+
 Limiting Access
 ---------------
 
@@ -71,6 +73,27 @@ the reverse: if given, members of this group will be rejected.
     AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=groups,dc=example,dc=com"
     AUTH_LDAP_DENY_GROUP = "cn=disabled,ou=groups,dc=example,dc=com"
 
+However, these two settings alone may not be enough to satisfy your needs. In
+such cases, you can use the :class:`~django_auth_ldap.config.LDAPGroupQuery`
+object to perform more complex matches against a user's groups. For example:
+
+.. code-block:: python
+
+    from django_auth_ldap.config import LDAPGroupQuery
+
+    AUTH_LDAP_REQUIRE_GROUP = (
+        (
+            LDAPGroupQuery("cn=enabled,ou=groups,dc=example,dc=com") |
+            LDAPGroupQuery("cn=also_enabled,ou=groups,dc=example,dc=com")
+        ) &
+        ~LDAPGroupQuery("cn=disabled,ou=groups,dc=example,dc=com")
+    )
+
+It is important to note a couple features of the example above. First and foremost,
+this handles the case of both `AUTH_LDAP_REQUIRE_GROUP` and `AUTH_LDAP_DENY_GROUP`
+in one setting. Second, you can use three operators on these queries: ``&``, ``|``,
+and ``~``: ``and``, ``or``, and ``not``, respectively.
+
 When groups are configured, you can always get the list of a user's groups from
 ``user.ldap_user.group_dns`` or ``user.ldap_user.group_names``. More advanced
 uses of groups are covered in the next two sections.
diff --git a/docs/source/reference.rst b/docs/source/reference.rst
index 26db1ce..834f120 100644
--- a/docs/source/reference.rst
+++ b/docs/source/reference.rst
@@ -223,6 +223,10 @@ A mapping from boolean profile field names to distinguished names of LDAP
 groups. The corresponding field in a user's profile is set to ``True`` or
 ``False`` according to whether the user is a member of the group.
 
+Values may be strings for simple group membership tests or
+:class:`~django_auth_ldap.config.LDAPGroupQuery` instances for more complex
+cases.
+
 This is ignored in Django 1.7 and later.
 
 
@@ -234,7 +238,8 @@ AUTH_LDAP_REQUIRE_GROUP
 Default: ``None``
 
 The distinguished name of a group; authentication will fail for any user that
-does not belong to this group.
+does not belong to this group. This can also be an
+:class:`~django_auth_ldap.config.LDAPGroupQuery` instance.
 
 
 .. setting:: AUTH_LDAP_SERVER_URI
@@ -312,6 +317,10 @@ A mapping from boolean :class:`~django.contrib.auth.models.User` field names to
 distinguished names of LDAP groups. The corresponding field is set to ``True``
 or ``False`` according to whether the user is a member of the group.
 
+Values may be strings for simple group membership tests or
+:class:`~django_auth_ldap.config.LDAPGroupQuery` instances for more complex
+cases.
+
 
 .. setting:: AUTH_LDAP_USER_SEARCH
 
@@ -349,10 +358,12 @@ Configuration
 
     .. method:: __init__(base_dn, scope, filterstr='(objectClass=*)')
 
-        * ``base_dn``: The distinguished name of the search base.
-        * ``scope``: One of ``ldap.SCOPE_*``.
-        * ``filterstr``: An optional filter string (e.g. '(objectClass=person)').
-          In order to be valid, ``filterstr`` must be enclosed in parentheses.
+        :param str base_dn: The distinguished name of the search base.
+        :param int scope: One of ``ldap.SCOPE_*``.
+        :param str filterstr: An optional filter string (e.g.
+            '(objectClass=person)'). In order to be valid, ``filterstr`` must be
+            enclosed in parentheses.
+
 
 .. class:: LDAPSearchUnion
 
@@ -360,10 +371,12 @@ Configuration
 
     .. method:: __init__(\*searches)
 
-        * ``searches``: Zero or more LDAPSearch objects. The result of the
-          overall search is the union (by DN) of the results of the underlying
-          searches. The precedence of the underlying results and the ordering of
-          the final results are both undefined.
+        :param searches: Zero or more LDAPSearch objects. The result of the
+            overall search is the union (by DN) of the results of the underlying
+            searches. The precedence of the underlying results and the ordering
+            of the final results are both undefined.
+        :type searches: :class:`LDAPSearch`
+
 
 .. class:: LDAPGroupType
 
@@ -377,6 +390,7 @@ Configuration
         first value of the cn attribute. You can specify a different attribute
         with ``name_attr``.
 
+
 .. class:: PosixGroupType
 
     A concrete subclass of :class:`~django_auth_ldap.config.LDAPGroupType` that
@@ -385,6 +399,7 @@ Configuration
 
     .. method:: __init__(name_attr='cn')
 
+
 .. class:: NISGroupType
 
     A concrete subclass of :class:`~django_auth_ldap.config.LDAPGroupType` that
@@ -392,6 +407,7 @@ Configuration
 
     .. method:: __init__(name_attr='cn')
 
+
 .. class:: MemberDNGroupType
 
     A concrete subclass of
@@ -400,8 +416,9 @@ Configuration
 
     .. method:: __init__(member_attr, name_attr='cn')
 
-        * ``member_attr``: The attribute on the group object that contains a
-          list of member DNs. 'member' and 'uniqueMember' are common examples.
+        :param str member_attr: The attribute on the group object that contains
+            a list of member DNs. 'member' and 'uniqueMember' are common
+            examples.
 
 
 .. class:: NestedMemberDNGroupType
@@ -491,6 +508,26 @@ Configuration
     .. method:: __init__(name_attr='cn')
 
 
+.. class:: LDAPGroupQuery
+
+    Represents a compound query for group membership.
+
+    This can be used to construct an arbitrarily complex group membership query
+    with AND, OR, and NOT logical operators. Construct primitive queries with a
+    group DN as the only argument. These queries can then be combined with the
+    ``&``, ``|``, and ``~`` operators.
+
+    This is used by certain settings, including
+    :setting:`AUTH_LDAP_REQUIRE_GROUP` and
+    :setting:`AUTH_LDAP_USER_FLAGS_BY_GROUP`. An example is shown in
+    :ref:`limiting-access`.
+
+    .. method:: __init__(group_dn)
+
+        :param str group_dn: The distinguished name of a group to test for
+            membership.
+
+
 Backend
 -------
 
diff --git a/docs/source/users.rst b/docs/source/users.rst
index 792244a..a493ce4 100644
--- a/docs/source/users.rst
+++ b/docs/source/users.rst
@@ -84,8 +84,10 @@ group membership::
 
     AUTH_LDAP_USER_FLAGS_BY_GROUP = {
         "is_active": "cn=active,ou=groups,dc=example,dc=com",
-        "is_staff": ["cn=staff,ou=groups,dc=example,dc=com",
-                     "cn=admin,ou=groups,dc=example,dc=com"],
+        "is_staff": (
+            LDAPGroupQuery("cn=staff,ou=groups,dc=example,dc=com") |
+            LDAPGroupQuery("cn=admin,ou=groups,dc=example,dc=com")
+        ),
         "is_superuser": "cn=superuser,ou=groups,dc=example,dc=com"
     }
 
@@ -93,8 +95,9 @@ group membership::
         "is_awesome": ["cn=awesome,ou=groups,dc=example,dc=com"]
     }
 
-If a list of groups is given, the flag will be set if the user is a member of
-any group.
+Values in these dictionaries may be simple DNs (as strings), lists or tuples of
+DNs, or :class:`~django_auth_ldap.config.LDAPGroupQuery` instances. Lists are
+converted to queries joined by ``|``.
 
 .. note::
 
diff --git a/setup.py b/setup.py
index 7ce54de..c32174e 100644
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,7 @@ PY3 = (sys.version_info[0] == 3)
 
 setup(
     name="django-auth-ldap",
-    version="1.2.11",
+    version="1.2.12",
     description="Django LDAP authentication backend",
     long_description=open('README').read(),
     url="http://bitbucket.org/psagers/django-auth-ldap/",
diff --git a/test/.coveragerc b/test/.coveragerc
index b620ab8..2dea015 100644
--- a/test/.coveragerc
+++ b/test/.coveragerc
@@ -1,3 +1,4 @@
 [run]
+data_file = ../.coverage
 source = django_auth_ldap.config
          django_auth_ldap.backend

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



More information about the Python-modules-commits mailing list