[CalendarServer-changes] [8035] CalendarServer/trunk/twistedcaldav/directory
source_changes at macosforge.org
source_changes at macosforge.org
Wed Aug 31 13:04:14 PDT 2011
Revision: 8035
http://trac.macosforge.org/projects/calendarserver/changeset/8035
Author: sagen at apple.com
Date: 2011-08-31 13:04:14 -0700 (Wed, 31 Aug 2011)
Log Message:
-----------
When precomputing which groups users are in, instead of downloading all the groups, we retrieve only the ones that have been delegated to, and their subgroups.
Modified Paths:
--------------
CalendarServer/trunk/twistedcaldav/directory/aggregate.py
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/aggregate.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/aggregate.py 2011-08-31 20:01:41 UTC (rev 8034)
+++ CalendarServer/trunk/twistedcaldav/directory/aggregate.py 2011-08-31 20:04:14 UTC (rev 8035)
@@ -164,6 +164,15 @@
returnValue(itertools.chain(*generators))
+ def getGroups(self, guids):
+ """
+ Returns a set of group records for the list of guids passed in. For
+ any group that also contains subgroups, those subgroups' records are
+ also returned, and so on.
+ """
+ recordType = self.recordType_groups
+ service = self.serviceForRecordType(recordType)
+ return service.getGroups(guids)
def serviceForRecordType(self, recordType):
Modified: CalendarServer/trunk/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory.py 2011-08-31 20:01:41 UTC (rev 8034)
+++ CalendarServer/trunk/twistedcaldav/directory/directory.py 2011-08-31 20:04:14 UTC (rev 8035)
@@ -298,6 +298,13 @@
return succeed(yieldMatches(recordType))
+ def getGroups(self, guids):
+ """
+ This implementation returns all groups, not just the ones specified
+ by guids
+ """
+ return succeed(self.listRecords(self.recordType_groups))
+
def getResourceInfo(self):
return ()
@@ -473,26 +480,40 @@
self.cache = cache
- def getGroups(self):
+ @inlineCallbacks
+ def getGroups(self, guids=None):
"""
Retrieve all groups and their member info (but don't actually fault in
- 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().
+ the records of the members), and return two dictionaries. The first
+ contains group records; the keys for this dictionary are the identifiers
+ used by the directory service to specify members. In OpenDirectory
+ these would be guids, but in LDAP these could be DNs, or some other
+ attribute. This attribute can be retrieved from a record using
+ record.cachedGroupsAlias().
+ The second dictionary returned maps that member attribute back to the
+ corresponding guid. These dictionaries are used to reverse-index the
+ groups that users are in by expandedMembers().
+
+ @param guids: if provided, retrieve only the groups corresponding to
+ these guids (including their sub groups)
+ @type guids: list of guid strings
"""
groups = {}
aliases = {}
- for record in self.directory.listRecords(self.directory.recordType_groups):
+
+ if guids is None: # get all group guids
+ records = self.directory.listRecords(self.directory.recordType_groups)
+ else: # get only the ones we know have been delegated to
+ records = (yield self.directory.getGroups(guids))
+
+ for record in records:
alias = record.cachedGroupsAlias()
groups[alias] = record.memberGUIDs()
aliases[record.guid] = alias
- return groups, aliases
+ returnValue((groups, aliases))
+
def expandedMembers(self, groups, guid, members=None, seen=None):
"""
Return the complete, flattened set of members of a group, including
@@ -618,10 +639,10 @@
# 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()
+ groups, aliases = (yield self.getGroups(guids=delegatedGUIDs))
groupGUIDs = set(aliases.keys())
- self.log_info("There are %d groups in the directory" %
- (len(groupGUIDs),))
+ self.log_info("%d groups retrieved from the directory" %
+ (len(groupGUIDs),))
delegatedGUIDs = delegatedGUIDs.intersection(groupGUIDs)
self.log_info("%d groups are proxies" % (len(delegatedGUIDs),))
Modified: CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py 2011-08-31 20:01:41 UTC (rev 8034)
+++ CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py 2011-08-31 20:04:14 UTC (rev 8035)
@@ -54,7 +54,7 @@
CachingDirectoryRecord)
from twistedcaldav.directory.directory import DirectoryConfigurationError
from twistedcaldav.directory.augment import AugmentRecord
-from twisted.internet.defer import succeed
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
from twext.web2.http import HTTPError, StatusResponse
from twext.web2 import responsecode
@@ -163,7 +163,7 @@
"groupSchema": {
"membersAttr": "member", # how members are specified
"nestedGroupsAttr": None, # how nested groups are specified
- "memberIdAttr": None, # which attribute the above refer to
+ "memberIdAttr": None, # which attribute the above refer to (None means use DN)
},
"resourceSchema": {
# Either set this attribute to retrieve the plist version
@@ -225,6 +225,9 @@
for attr in self.rdnSchema[recordType]["mapping"].values():
if attr:
attrSet.add(attr)
+ # Also put the guidAttr attribute into the mappings for each type
+ # so recordsMatchingFields can query on guid
+ self.rdnSchema[recordType]["mapping"]["guid"] = self.rdnSchema["guidAttr"]
if self.groupSchema["membersAttr"]:
attrSet.add(self.groupSchema["membersAttr"])
if self.groupSchema["nestedGroupsAttr"]:
@@ -245,7 +248,7 @@
attrSet.add(self.partitionSchema["serverIdAttr"])
if self.partitionSchema["partitionIdAttr"]:
attrSet.add(self.partitionSchema["partitionIdAttr"])
- self.attrList = list(attrSet)
+ self.attrlist = list(attrSet)
self.typeRDNs = {}
for recordType in self.recordTypes():
@@ -287,21 +290,21 @@
base = self.typeRDNs[recordType] + self.base
# Build filter
- filter = "(!(objectClass=organizationalUnit))"
+ filterstr = "(!(objectClass=organizationalUnit))"
typeFilter = self.rdnSchema[recordType]["filter"]
if typeFilter:
- filter = "(&%s%s)" % (filter, typeFilter)
+ filterstr = "(&%s%s)" % (filterstr, typeFilter)
# Query the LDAP server
self.log_debug("Querying ldap for records matching base %s and filter %s for attributes %s." %
- (ldap.dn.dn2str(base), filter, self.attrList))
+ (ldap.dn.dn2str(base), filterstr, self.attrlist))
# This takes a while, so if you don't want to have a "long request"
# warning logged, use this instead of timedSearch:
# results = self.ldap.search_s(ldap.dn.dn2str(base),
- # ldap.SCOPE_SUBTREE, filter, self.attrList)
+ # ldap.SCOPE_SUBTREE, filterstr=filterstr, attrlist=self.attrlist)
results = self.timedSearch(ldap.dn.dn2str(base),
- ldap.SCOPE_SUBTREE, filter, self.attrList)
+ ldap.SCOPE_SUBTREE, filterstr=filterstr, attrlist=self.attrlist)
records = []
numMissingGuids = 0
@@ -402,17 +405,24 @@
self.log_debug("Authentication succeeded for %s" % (dn,))
- def timedSearch(self, *args, **kwds):
+ def timedSearch(self, base, scope, filterstr="(objectClass=*)",
+ attrlist=None):
"""
Execute an ldap.search_s( ); if it takes longer than the configured
threshold, emit a log error.
"""
startTime = time.time()
- result = self.ldap.search_s(*args, **kwds)
+ try:
+ result = self.ldap.search_s(base, scope, filterstr=filterstr,
+ attrlist=attrlist)
+ except ldap.NO_SUCH_OBJECT:
+ result = []
totalTime = time.time() - startTime
if totalTime > self.warningThresholdSeconds:
- self.log_error("LDAP query exceeded threshold: %.2f seconds for %s %s (#results=%d)" %
- (totalTime, args, kwds, len(result)))
+ if filterstr and len(filterstr) > 100:
+ filterstr = "%s..." % (filterstr[:100],)
+ self.log_error("LDAP query exceeded threshold: %.2f seconds for %s %s %s (#results=%d)" %
+ (totalTime, base, filterstr, attrlist, len(result)))
return result
@@ -432,11 +442,11 @@
recordType = self.recordType_groups
base = self.typeRDNs[recordType] + self.base
- filter = "(cn=%s)" % (self.restrictToGroup,)
+ filterstr = "(cn=%s)" % (self.restrictToGroup,)
self.log_debug("Retrieving ldap record with base %s and filter %s." %
- (ldap.dn.dn2str(base), filter))
+ (ldap.dn.dn2str(base), filterstr))
result = self.ldap.search_s(ldap.dn.dn2str(base),
- ldap.SCOPE_SUBTREE, filter, self.attrList)
+ ldap.SCOPE_SUBTREE, filterstr=filterstr, attrlist=self.attrlist)
if len(result) == 1:
dn, attrs = result[0]
@@ -483,12 +493,12 @@
recordType = self.recordType_groups
base = self.typeRDNs[recordType] + self.base
- filter = "(%s=%s)" % (self.rdnSchema["guidAttr"], groupGUID)
+ filterstr = "(%s=%s)" % (self.rdnSchema["guidAttr"], groupGUID)
self.log_debug("Retrieving ldap record with base %s and filter %s." %
- (ldap.dn.dn2str(base), filter))
+ (ldap.dn.dn2str(base), filterstr))
result = self.timedSearch(ldap.dn.dn2str(base),
- ldap.SCOPE_SUBTREE, filter, self.attrList)
+ ldap.SCOPE_SUBTREE, filterstr=filterstr, attrlist=self.attrlist)
if len(result) == 0:
continue
@@ -742,21 +752,21 @@
base = self.typeRDNs[recordType] + self.base
# Build filter
- filter = "(!(objectClass=organizationalUnit))"
+ filterstr = "(!(objectClass=organizationalUnit))"
typeFilter = self.rdnSchema[recordType]["filter"]
if typeFilter:
- filter = "(&%s%s)" % (filter, typeFilter)
+ filterstr = "(&%s%s)" % (filterstr, typeFilter)
if indexType == self.INDEX_TYPE_GUID:
# Query on guid only works if guid attribute has been defined.
# Support for query on guid even if is auto-generated should
# be added.
if not guidAttr: return
- filter = "(&%s(%s=%s))" % (filter, guidAttr, indexKey)
+ filterstr = "(&%s(%s=%s))" % (filterstr, guidAttr, indexKey)
elif indexType == self.INDEX_TYPE_SHORTNAME:
- filter = "(&%s(%s=%s))" % (
- filter,
+ filterstr = "(&%s(%s=%s))" % (
+ filterstr,
self.rdnSchema[recordType]["mapping"]["recordName"],
indexKey
)
@@ -766,23 +776,23 @@
email = indexKey[7:] # strip "mailto:"
emailSuffix = self.rdnSchema[recordType]["emailSuffix"]
if emailSuffix is not None and email.partition("@")[2] == emailSuffix:
- filter = "(&%s(|(&(!(mail=*))(%s=%s))(mail=%s)))" % (
- filter,
+ filterstr = "(&%s(|(&(!(mail=*))(%s=%s))(mail=%s)))" % (
+ filterstr,
self.rdnSchema[recordType]["attr"],
email.partition("@")[0],
email
)
else:
- filter = "(&%s(mail=%s))" % (filter, email)
+ filterstr = "(&%s(mail=%s))" % (filterstr, email)
elif indexType == self.INDEX_TYPE_AUTHID:
return
# Query the LDAP server
self.log_debug("Retrieving ldap record with base %s and filter %s." %
- (ldap.dn.dn2str(base), filter))
+ (ldap.dn.dn2str(base), filterstr))
result = self.timedSearch(ldap.dn.dn2str(base),
- ldap.SCOPE_SUBTREE, filter, self.attrList)
+ ldap.SCOPE_SUBTREE, filterstr=filterstr, attrlist=self.attrlist)
if result:
dn, attrs = result.pop()
@@ -816,6 +826,7 @@
self.log_warn("Ignoring record missing guid attribute: recordType %s, indexType %s and indexKey %s"
% (recordTypes, indexType, indexKey))
+
def recordsMatchingFields(self, fields, operand="or", recordType=None):
"""
Carries out the work of a principal-property-search against LDAP
@@ -840,17 +851,26 @@
guidAttr = self.rdnSchema["guidAttr"]
for recordType in recordTypes:
- filter = buildFilter(self.rdnSchema[recordType]["mapping"], fields,
- operand=operand)
- if filter is not None:
+ base = self.typeRDNs[recordType] + self.base
+
+ if fields[0][0] == "dn":
+ # DN's are not an attribute that can be searched on by filter
+ scope = ldap.SCOPE_BASE
+ filterstr = "(objectClass=*)"
+ base = ldap.dn.str2dn(fields[0][1])
+
+ else:
+ scope = ldap.SCOPE_SUBTREE
+ filterstr = buildFilter(self.rdnSchema[recordType]["mapping"],
+ fields, operand=operand)
+
+ if filterstr is not None:
# Query the LDAP server
- base = self.typeRDNs[recordType] + self.base
-
- self.log_debug("LDAP search %s %s" %
- (ldap.dn.dn2str(base), filter))
- results = self.timedSearch(ldap.dn.dn2str(base),
- ldap.SCOPE_SUBTREE, filter, self.attrList)
+ self.log_debug("LDAP search %s %s %s" %
+ (ldap.dn.dn2str(base), scope, filterstr))
+ results = self.timedSearch(ldap.dn.dn2str(base), scope, filterstr=filterstr,
+ attrlist=self.attrlist)
self.log_debug("LDAP search returned %d results" % (len(results),))
numMissingGuids = 0
for dn, attrs in results:
@@ -885,6 +905,81 @@
return succeed(records)
+ @inlineCallbacks
+ def getGroups(self, guids):
+ """
+ Returns a set of group records for the list of guids passed in. For
+ any group that also contains subgroups, those subgroups' records are
+ also returned, and so on.
+ """
+
+ recordsByAlias = {}
+
+ groupsDN = self.typeRDNs[self.recordType_groups] + self.base
+ memberIdAttr = self.groupSchema["memberIdAttr"]
+
+ # First time through the loop we search using the attribute
+ # corresponding to guid, since that is what the proxydb uses.
+ # Subsequent iterations fault in groups via the attribute
+ # used to identify members.
+ attributeToSearch = "guid"
+ valuesToFetch = guids
+
+ while valuesToFetch:
+
+ if attributeToSearch == "dn":
+ # Since DN can't be searched on in a filter we have to call
+ # recordsMatchingFields for *each* DN.
+ results = []
+ for value in valuesToFetch:
+ fields = [["dn", value, False, "equals"]]
+ result = (yield self.recordsMatchingFields(fields,
+ recordType=self.recordType_groups))
+ results.extend(result)
+ else:
+ fields = []
+ for value in valuesToFetch:
+ fields.append([attributeToSearch, value, False, "equals"])
+ results = (yield self.recordsMatchingFields(fields,
+ recordType=self.recordType_groups))
+
+ # Reset values for next iteration
+ valuesToFetch = set()
+
+ for record in results:
+ alias = record.cachedGroupsAlias()
+ if alias not in recordsByAlias:
+ recordsByAlias[alias] = record
+
+ # record._memberIds contains the members of this group,
+ # but it might not be in guid form; it will be data from
+ # self.groupSchema["memberIdAttr"]
+ for memberAlias in record._memberIds:
+ if not memberIdAttr:
+ # Members are identified by dn so we can take a short
+ # cut: we know we only need to examine groups, and
+ # those will be children of the groups DN
+ if not dnContainedIn(memberAlias, groupsDN):
+ continue
+ if memberAlias not in recordsByAlias:
+ valuesToFetch.add(memberAlias)
+
+ # Switch to the LDAP attribute used for identifying members
+ # for subsequent iterations. If memberIdAttr is not specified
+ # in the config, we'll search using dn.
+ attributeToSearch = memberIdAttr if memberIdAttr else "dn"
+
+ returnValue(recordsByAlias.values())
+
+
+def dnContainedIn(child, parent):
+ """
+ Return True if child dn is contained within parent dn, otherwise False.
+ """
+ child = ldap.dn.str2dn(child)
+ return child[-len(parent):] == parent
+
+
def buildFilter(mapping, fields, operand="or"):
"""
Create an LDAP filter string from a list of tuples representing directory
@@ -908,14 +1003,14 @@
converted.append("(%s=%s)" % (ldapField, value))
if len(converted) == 0:
- filter = None
+ filterstr = None
elif len(converted) == 1:
- filter = converted[0]
+ filterstr = converted[0]
else:
operand = ("|" if operand == "or" else "&")
- filter = "(%s%s)" % (operand, "".join(converted))
+ filterstr = "(%s%s)" % (operand, "".join(converted))
- return filter
+ return filterstr
class LdapDirectoryRecord(CachingDirectoryRecord):
@@ -990,18 +1085,18 @@
if memberIdAttr:
base = self.service.base
- filter = "(%s=%s)" % (memberIdAttr, memberId)
+ filterstr = "(%s=%s)" % (memberIdAttr, memberId)
self.log_debug("Retrieving subtree of %s with filter %s" %
- (ldap.dn.dn2str(base), filter),
+ (ldap.dn.dn2str(base), filterstr),
system="LdapDirectoryService")
result = self.service.timedSearch(ldap.dn.dn2str(base),
- ldap.SCOPE_SUBTREE, filter, self.service.attrList)
+ ldap.SCOPE_SUBTREE, filterstr=filterstr, attrlist=self.service.attrlist)
else:
self.log_debug("Retrieving %s." % memberId,
system="LdapDirectoryService")
result = self.service.timedSearch(memberId,
- ldap.SCOPE_BASE, attrlist=self.service.attrList)
+ ldap.SCOPE_BASE, attrlist=self.service.attrlist)
if result:
# TODO: what about duplicates?
@@ -1049,9 +1144,9 @@
membersAttrs.append(self.service.groupSchema["nestedGroupsAttr"])
if len(membersAttrs) == 1:
- filter = "(%s=%s)" % (membersAttrs[0], self._memberId)
+ filterstr = "(%s=%s)" % (membersAttrs[0], self._memberId)
else:
- filter = "(|%s)" % ( "".join(
+ filterstr = "(|%s)" % ( "".join(
["(%s=%s)" % (a, self._memberId) for a in membersAttrs]
),
)
@@ -1060,7 +1155,7 @@
try:
results = self.service.timedSearch(ldap.dn.dn2str(base),
- ldap.SCOPE_SUBTREE, filter, self.service.attrList)
+ ldap.SCOPE_SUBTREE, filterstr=filterstr, attrlist=self.service.attrlist)
for dn, attrs in results:
shortName = self.service._getUniqueLdapAttribute(attrs, "cn")
Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py 2011-08-31 20:01:41 UTC (rev 8034)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py 2011-08-31 20:04:14 UTC (rev 8035)
@@ -194,7 +194,7 @@
cache=cache, useExternalProxies=False)
# Exercise getGroups()
- groups, aliases = updater.getGroups()
+ groups, aliases = (yield updater.getGroups())
self.assertEquals(
groups,
{
Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py 2011-08-31 20:01:41 UTC (rev 8034)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py 2011-08-31 20:04:14 UTC (rev 8035)
@@ -95,19 +95,19 @@
class LdapDirectoryTestWrapper(object):
"""
A test stub which replaces search_s( ) with a version that will return
- whatever you have previously called setTestResults( ) with.
+ whatever you have previously called addTestResults( ) with.
"""
def __init__(self, actual):
self.actual = actual
- self.testResults = None
+ self.testResults = []
- def setTestResults(self, results):
- self.testResults = results
+ def addTestResults(self, results):
+ self.testResults.insert(0, results)
- def search_s(self, base, scope, filter="(objectClass=*)",
- attrList=None):
- return self.testResults
+ def search_s(self, base, scope, filterstr="(objectClass=*)",
+ attrlist=None):
+ return self.testResults.pop()
class LdapDirectoryServiceTestCase(TestCase):
@@ -419,7 +419,7 @@
and turns the results into records
"""
- self.service.ldap.setTestResults([
+ self.service.ldap.addTestResults([
(
"uid=odtestamanda,cn=users,dc=example,dc=com",
{
@@ -483,11 +483,35 @@
self.service, 30, cache=cache, useExternalProxies=False)
# Fake LDAP results for the group listRecords performed within updateCache()
- self.service.ldap.setTestResults([
+
+ # Also include recursive groups to make sure we handle that situation
+ self.service.ldap.addTestResults([
(
- "cn=bothcoasts,cn=groups,dc=example,dc=com",
+ "cn=recursive1_coasts,cn=groups,dc=example,dc=com",
{
- 'cn': ['topgroup'],
+ 'cn': ['recursive1_coasts'],
+ 'apple-generateduid': ['recursive1_coasts'],
+ 'uniqueMember': [
+ 'cn=recursive2_coasts,cn=groups,dc=example,dc=com',
+ 'uid=wsanchez,cn=users,dc=example,dc=com',
+ ],
+ }
+ ),
+ (
+ "cn=recursive2_coasts,cn=groups,dc=example,dc=com",
+ {
+ 'cn': ['recursive2_coasts'],
+ 'apple-generateduid': ['recursive2_coasts'],
+ 'uniqueMember': [
+ 'cn=recursive1_coasts,cn=groups,dc=example,dc=com',
+ 'uid=cdaboo,cn=users,dc=example,dc=com',
+ ],
+ }
+ ),
+ (
+ 'cn=both_coasts,cn=groups,dc=example,dc=com',
+ {
+ 'cn': ['both_coasts'],
'apple-generateduid': ['both_coasts'],
'uniqueMember': [
'cn=right_coast,cn=groups,dc=example,dc=com',
@@ -518,18 +542,57 @@
}
),
])
+ self.service.ldap.addTestResults([
+ (
+ "cn=recursive2_coasts,cn=groups,dc=example,dc=com",
+ {
+ 'cn': ['recursive2_coasts'],
+ 'apple-generateduid': ['recursive2_coasts'],
+ 'uniqueMember': [
+ 'cn=recursive1_coasts,cn=groups,dc=example,dc=com',
+ 'uid=cdaboo,cn=users,dc=example,dc=com',
+ ],
+ }
+ ),
+ ])
+ self.service.ldap.addTestResults([
+ (
+ '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.service.ldap.addTestResults([
+ (
+ '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',
+ ],
+ }
+ ),
+ ])
- self.assertEquals((False, 6), (yield updater.updateCache()))
+ self.assertEquals((False, 8), (yield updater.updateCache()))
users = self.service.recordType_users
for shortName, groups in [
- ("cdaboo", set(["both_coasts"])),
- ("wsanchez", set(["both_coasts", "left_coast"])),
+ ("cdaboo", set(["both_coasts", "recursive1_coasts", "recursive2_coasts"])),
+ ("wsanchez", set(["both_coasts", "left_coast", "recursive1_coasts", "recursive2_coasts"])),
]:
# Fake LDAP results for the record lookup
- self.service.ldap.setTestResults([
+ self.service.ldap.addTestResults([
(
"uid=%s,cn=users,dc=example,dc=com" % (shortName,),
{
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110831/87f7be18/attachment-0001.html>
More information about the calendarserver-changes
mailing list