[CalendarServer-changes] [7855] CalendarServer/trunk/twistedcaldav/directory

source_changes at macosforge.org source_changes at macosforge.org
Tue Aug 2 19:58:18 PDT 2011


Revision: 7855
          http://trac.macosforge.org/projects/calendarserver/changeset/7855
Author:   sagen at apple.com
Date:     2011-08-02 19:58:18 -0700 (Tue, 02 Aug 2011)
Log Message:
-----------
Deal with nested groups when members are referred to by an attribute other than guid; use pickle mode in GroupMembershipCache so that commas in DNs don't foul up lists of members

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/directory/directory.py
    CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py

Modified: CalendarServer/trunk/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory.py	2011-08-02 18:04:28 UTC (rev 7854)
+++ CalendarServer/trunk/twistedcaldav/directory/directory.py	2011-08-03 02:58:18 UTC (rev 7855)
@@ -406,7 +406,7 @@
 
     """
 
-    def __init__(self, namespace, pickle=False, no_invalidation=False,
+    def __init__(self, namespace, pickle=True, no_invalidation=False,
         key_normalization=True, expireSeconds=0):
 
         super(GroupMembershipCache, self).__init__(namespace, pickle=pickle,
@@ -418,14 +418,14 @@
     def setGroupsFor(self, guid, memberships):
         self.log_debug("set groups-for %s : %s" % (guid, memberships))
         return self.set("groups-for:%s" %
-            (str(guid)), str(",".join(memberships)),
+            (str(guid)), memberships,
             expireTime=self.expireSeconds)
 
     def getGroupsFor(self, guid):
         self.log_debug("get groups-for %s" % (guid,))
         def _value(value):
             if value:
-                return set(value.split(","))
+                return value
             else:
                 return set()
         d = self.get("groups-for:%s" % (str(guid),))
@@ -476,15 +476,23 @@
     def getGroups(self):
         """
         Retrieve all groups and their member info (but don't actually fault in
-        the records of the members), and return a dictionary of group-guid to
-        member-guids.  Ultimately this dictionary will be used to reverse-index
-        the groups that users are in by expandedMembers().
+        the records of the members), and return two dictionaries.  The first maps
+        group-guid to members.  The keys will be guids, the values are lists of
+        members usually specified by guid, but in a directory system like LDAP which
+        can use a different attribute to refer to members this could be a DN.  The
+        second dictionary returns maps that member attribute back to the corresponding
+        guid.  These dictionaries are used to reverse-index the groups that users are
+        in by expandedMembers().
         """
         groups = {}
+        aliases = {}
         for record in self.directory.listRecords(self.directory.recordType_groups):
-            groups[record.guid] = record.memberGUIDs()
-        return groups
+            alias = record.cachedGroupsAlias()
+            groups[alias] = record.memberGUIDs()
+            aliases[record.guid] = alias
+        return groups, aliases
 
+
     def expandedMembers(self, groups, guid, members=None, seen=None):
         """
         Return the complete, flattened set of members of a group, including
@@ -503,9 +511,9 @@
                 if groups.has_key(member): # it's a group then
                     self.expandedMembers(groups, member, members=members,
                                          seen=seen)
-
         return members
 
+
     @inlineCallbacks
     def updateCache(self, fast=False):
         """
@@ -525,6 +533,12 @@
         the (possibly modified) value for fast, and the number of members loaded
         into the cache (which can be zero if fast=True and isPopulated(), or
         fast=False and the cache is locked by someone else).
+
+        The pickled snapshot file is a dict whose keys represent a record and
+        the values are the guids of the groups that record is a member of.  The
+        keys are normally guids except in the case of a directory system like LDAP
+        where there can be a different attribute used for referring to members,
+        such as a DN.
         """
 
         # TODO: add memcached eviction protection
@@ -594,12 +608,18 @@
             # containing which delegated-to groups a user is a member of
 
             self.log_info("Retrieving list of all proxies")
+            # This is always a set of guids:
             delegatedGUIDs = set((yield self.proxyDB.getAllMembers()))
             self.log_info("There are %d proxies" % (len(delegatedGUIDs),))
+            self.log_info("Retrieving group hierarchy from directory")
 
-            self.log_info("Retrieving group hierarchy from directory")
-            groups = self.getGroups()
-            groupGUIDs = set(groups.keys())
+            # "groups" maps a group to its members; the keys and values consist
+            # of whatever directory attribute is used to refer to members.  The
+            # attribute value comes from record.cachedGroupsAlias().
+            # "aliases" maps the record.cachedGroupsAlias() value for a group
+            # back to the group's guid.
+            groups, aliases = self.getGroups()
+            groupGUIDs = set(aliases.keys())
             self.log_info("There are %d groups in the directory" %
                            (len(groupGUIDs),))
 
@@ -609,7 +629,8 @@
             # Reverse index the group membership from cache
             members = {}
             for groupGUID in delegatedGUIDs:
-                groupMembers = self.expandedMembers(groups, groupGUID)
+                groupMembers = self.expandedMembers(groups, aliases[groupGUID])
+                # groupMembers is in cachedGroupsAlias() format
                 for member in groupMembers:
                     memberships = members.setdefault(member, set())
                     memberships.add(groupGUID)
@@ -1034,8 +1055,19 @@
         Return the set of groups (guids) this record is a member of, based on
         the data cached by cacheGroupMembership( )
         """
-        return self.service.groupMembershipCache.getGroupsFor(self.guid)
+        return self.service.groupMembershipCache.getGroupsFor(self.cachedGroupsAlias())
 
+    def cachedGroupsAlias(self):
+        """
+        The GroupMembershipCache uses keys based on this value.  Normally it's
+        a record's guid but in a directory system like LDAP which can use a
+        different attribute to refer to group members, we need to be able to
+        look up an entry in the GroupMembershipCache by that attribute.
+        Subclasses which don't use record.guid to look up group membership
+        should override this method.
+        """
+        return self.guid
+
     def externalProxies(self):
         """
         Return the set of proxies defined in the directory service, as opposed

Modified: CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py	2011-08-02 18:04:28 UTC (rev 7854)
+++ CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py	2011-08-03 02:58:18 UTC (rev 7855)
@@ -1035,12 +1035,14 @@
 
         return groups
 
-    def cachedGroups(self):
+    def cachedGroupsAlias(self):
         """
-        Return the set of groups (guids) this record is a member of, based on
-        the data cached by cacheGroupMembership( )
+        See directory.py for full description
+
+        LDAP group members can be referred to by attributes other than guid.  _memberId
+        will be set to the appropriate value to look up group-membership with.
         """
-        return self.service.groupMembershipCache.getGroupsFor(self._memberId)
+        return self._memberId
 
     def memberGUIDs(self):
         return set(self._memberGUIDs)

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py	2011-08-02 18:04:28 UTC (rev 7854)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py	2011-08-03 02:58:18 UTC (rev 7855)
@@ -162,11 +162,11 @@
         """
         cache = GroupMembershipCache("ProxyDB", expireSeconds=10)
 
-        yield cache.setGroupsFor("a", ["b", "c", "d"]) # a is in b, c, d
+        yield cache.setGroupsFor("a", set(["b", "c", "d"])) # a is in b, c, d
         members = (yield cache.getGroupsFor("a"))
         self.assertEquals(members, set(["b", "c", "d"]))
 
-        yield cache.setGroupsFor("b", []) # b not in any groups
+        yield cache.setGroupsFor("b", set()) # b not in any groups
         members = (yield cache.getGroupsFor("b"))
         self.assertEquals(members, set())
 
@@ -194,7 +194,7 @@
             cache=cache, useExternalProxies=False)
 
         # Exercise getGroups()
-        groups = updater.getGroups()
+        groups, aliases = updater.getGroups()
         self.assertEquals(
             groups,
             {
@@ -225,6 +225,21 @@
                     set(['5A985493-EE2C-4665-94CF-4DFEA3A89500'])
             }
         )
+        self.assertEquals(
+            aliases,
+            {
+                '9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1':
+                    '9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1',
+                 'admin': 'admin',
+                 'both_coasts': 'both_coasts',
+                 'grunts': 'grunts',
+                 'left_coast': 'left_coast',
+                 'non_calendar_group': 'non_calendar_group',
+                 'recursive1_coasts': 'recursive1_coasts',
+                 'recursive2_coasts': 'recursive2_coasts',
+                 'right_coast': 'right_coast'
+            }
+        )
 
         # Exercise expandedMembers()
         self.assertEquals(

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py	2011-08-02 18:04:28 UTC (rev 7854)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py	2011-08-03 02:58:18 UTC (rev 7855)
@@ -18,6 +18,11 @@
     from twistedcaldav.directory.ldapdirectory import (
         buildFilter, LdapDirectoryService, MissingGuidException
     )
+    from twistedcaldav.test.util import proxiesFile
+    from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
+    from twistedcaldav.directory import calendaruserproxy
+    from twistedcaldav.directory.directory import GroupMembershipCache, GroupMembershipCacheUpdater
+    from twisted.internet.defer import inlineCallbacks
 except ImportError:
     print "Skipping because ldap module not installed"
 else:
@@ -108,6 +113,8 @@
     class LdapDirectoryServiceTestCase(TestCase):
 
         def setUp(self):
+            super(LdapDirectoryServiceTestCase, self).setUp()
+
             params = {
                 "augmentService" : None,
                 "groupMembershipCache" : None,
@@ -191,9 +198,9 @@
                     },
                 },
                 "groupSchema": {
-                    "membersAttr": "apple-group-memberguid", # how members are specified
-                    "nestedGroupsAttr": "apple-group-nestedgroup", # how nested groups are specified
-                    "memberIdAttr": "apple-generateduid", # which attribute the above refer to
+                    "membersAttr": "uniqueMember", # how members are specified
+                    "nestedGroupsAttr": "", # how nested groups are specified
+                    "memberIdAttr": "", # which attribute the above refer to
                 },
                 "resourceSchema": {
                     "resourceInfoAttr": "apple-resource-info", # contains location/resource info
@@ -296,13 +303,10 @@
             guid = '6C6CD280-E6E3-11DF-9492-0800200C9A66'
             attrs = {
                 'apple-generateduid': [guid],
-                'apple-group-memberguid':
+                'uniqueMember':
                     [
                         '9DC04A70-E6DD-11DF-9492-0800200C9A66',
-                        '9DC04A71-E6DD-11DF-9492-0800200C9A66'
-                    ],
-                'apple-group-nestedgroup':
-                    [
+                        '9DC04A71-E6DD-11DF-9492-0800200C9A66',
                         '6C6CD282-E6E3-11DF-9492-0800200C9A66'
                     ],
                 'cn': ['odtestgrouptop']
@@ -454,3 +458,85 @@
                 set([r.firstName for r in records]),
                 set(["Amanda", "Betty"]) # Carlene is skipped because no guid in LDAP
             )
+
+        @inlineCallbacks
+        def test_groupMembershipAliases(self):
+            """
+            Exercise a directory enviornment where group membership does not refer
+            to guids but instead uses LDAP DNs.  This example uses the LDAP attribute
+            "uniqueMember" to specify members of a group.  The value of this attribute
+            is each members' DN.  Even though the proxy database deals strictly in
+            guids, updateCache( ) is smart enough to map between guids and this
+            attribute which is referred to in the code as record.cachedGroupsAlias().
+            """
+
+            # Set up proxydb and preload it from xml
+            calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
+            yield XMLCalendarUserProxyLoader(proxiesFile.path).updateProxyDB()
+
+            # Set up the GroupMembershipCache
+            cache = GroupMembershipCache("ProxyDB", expireSeconds=60)
+            self.service.groupMembershipCache = cache
+            updater = GroupMembershipCacheUpdater(calendaruserproxy.ProxyDBService,
+                self.service, 30, cache=cache, useExternalProxies=False)
+
+            # Fake LDAP results for the group listRecords performed within updateCache()
+            self.service.ldap.setTestResults([
+                (
+                    "cn=bothcoasts,cn=groups,dc=example,dc=com",
+                    {
+                        'cn': ['topgroup'],
+                        'apple-generateduid': ['both_coasts'],
+                        'uniqueMember': [
+                            'cn=right_coast,cn=groups,dc=example,dc=com',
+                            'cn=left_coast,cn=groups,dc=example,dc=com',
+                        ],
+                    }
+                ),
+                (
+                    'cn=right_coast,cn=groups,dc=example,dc=com',
+                    {
+                        'cn': ['right_coast'],
+                        'apple-generateduid': ['right_coast'],
+                        'uniqueMember': [
+                            'uid=cdaboo,cn=users,dc=example,dc=com',
+                        ],
+                    }
+                ),
+                (
+                    'cn=left_coast,cn=groups,dc=example,dc=com',
+                    {
+                        'cn': ['left_coast'],
+                        'apple-generateduid': ['left_coast'],
+                        'uniqueMember': [
+                            'uid=wsanchez,cn=users,dc=example,dc=com',
+                            'uid=lecroy,cn=users,dc=example,dc=com',
+                            'uid=dreid,cn=users,dc=example,dc=com',
+                        ],
+                    }
+                ),
+            ])
+
+            self.assertEquals((False, 6), (yield updater.updateCache()))
+
+            users = self.service.recordType_users
+
+            for shortName, groups in [
+                ("cdaboo", set(["both_coasts"])),
+                ("wsanchez", set(["both_coasts", "left_coast"])),
+            ]:
+
+                # Fake LDAP results for the record lookup
+                self.service.ldap.setTestResults([
+                    (
+                        "uid=%s,cn=users,dc=example,dc=com" % (shortName,),
+                        {
+                            'uid': [shortName],
+                            'cn': [shortName],
+                            'apple-generateduid': [shortName],
+                        }
+                    ),
+                ])
+
+                record = self.service.recordWithShortName(users, shortName)
+                self.assertEquals(groups, (yield record.cachedGroups()))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110802/2ae4c036/attachment-0001.html>


More information about the calendarserver-changes mailing list