[CalendarServer-changes] [4229] CalendarServer/branches/release/CalendarServer-2.2-dev

source_changes at macosforge.org source_changes at macosforge.org
Mon May 11 14:38:13 PDT 2009


Revision: 4229
          http://trac.macosforge.org/projects/calendarserver/changeset/4229
Author:   wsanchez at apple.com
Date:     2009-05-11 14:38:13 -0700 (Mon, 11 May 2009)
Log Message:
-----------
Pulled up r4198 r4199 r4200 r4201 r4202 r4209 from trunk.

Modified Paths:
--------------
    CalendarServer/branches/release/CalendarServer-2.2-dev/conf/caldavd-apple.plist
    CalendarServer/branches/release/CalendarServer-2.2-dev/conf/caldavd-test.plist
    CalendarServer/branches/release/CalendarServer-2.2-dev/conf/caldavd.plist
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/cachingdirectory.py
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_apache.py
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_buildquery.py
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_proxyprincipalmembers.py
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_xmlfile.py
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/util.py

Added Paths:
-----------
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/oldappleopendirectory.py
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/oldxmlfile.py
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/xmlfile.py

Removed Paths:
-------------
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/cachingappleopendirectory.py
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/cachingxmlfile.py
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_opendirectoryrecords.py
    CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/xmlfile.py

Property Changed:
----------------
    CalendarServer/branches/release/CalendarServer-2.2-dev/
    CalendarServer/branches/release/CalendarServer-2.2-dev/doc/Extensions/caldav-privatecomments.txt
    CalendarServer/branches/release/CalendarServer-2.2-dev/doc/Extensions/caldav-privatecomments.xml
    CalendarServer/branches/release/CalendarServer-2.2-dev/doc/Extensions/caldav-schedulingchanges.txt
    CalendarServer/branches/release/CalendarServer-2.2-dev/doc/Extensions/caldav-schedulingchanges.xml


Property changes on: CalendarServer/branches/release/CalendarServer-2.2-dev
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/trunk:4105-4107,4113-4116,4121-4124,4137-4139,4141-4144,4154-4159,4163-4167,4172,4174-4176,4178-4180,4191,4194,4197,4213
   + /CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/trunk:4105-4107,4113-4116,4121-4124,4137-4139,4141-4144,4154-4159,4163-4167,4172,4174-4176,4178-4180,4191,4194,4197-4202,4209,4213

Modified: CalendarServer/branches/release/CalendarServer-2.2-dev/conf/caldavd-apple.plist
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/conf/caldavd-apple.plist	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/conf/caldavd-apple.plist	2009-05-11 21:38:13 UTC (rev 4229)
@@ -143,7 +143,7 @@
     <key>DirectoryService</key>
     <dict>
       <key>type</key>
-      <string>twistedcaldav.directory.cachingappleopendirectory.OpenDirectoryService</string>
+      <string>twistedcaldav.directory.appleopendirectory.OpenDirectoryService</string>
       
       <key>params</key>
       <dict>

Modified: CalendarServer/branches/release/CalendarServer-2.2-dev/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/conf/caldavd-test.plist	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/conf/caldavd-test.plist	2009-05-11 21:38:13 UTC (rev 4229)
@@ -139,7 +139,7 @@
     <key>DirectoryService</key>
     <dict>
       <key>type</key>
-      <string>twistedcaldav.directory.cachingappleopendirectory.OpenDirectoryService</string>
+      <string>twistedcaldav.directory.appleopendirectory.OpenDirectoryService</string>
       
       <key>params</key>
       <dict>

Modified: CalendarServer/branches/release/CalendarServer-2.2-dev/conf/caldavd.plist
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/conf/caldavd.plist	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/conf/caldavd.plist	2009-05-11 21:38:13 UTC (rev 4229)
@@ -142,7 +142,7 @@
     <key>DirectoryService</key>
     <dict>
       <key>type</key>
-      <string>twistedcaldav.directory.cachingappleopendirectory.OpenDirectoryService</string>
+      <string>twistedcaldav.directory.appleopendirectory.OpenDirectoryService</string>
       
       <key>params</key>
       <dict>


Property changes on: CalendarServer/branches/release/CalendarServer-2.2-dev/doc/Extensions/caldav-privatecomments.txt
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/users/cdaboo/attendee-comments-2886/doc/Extensions/caldav-privatecomments-00.txt:2887-2910
/CalendarServer/branches/users/cdaboo/byebye-serviceslocator-2937/doc/Extensions/caldav-privatecomments-00.txt:2938-3097
/CalendarServer/branches/users/cdaboo/implicit-if-match-3306/doc/Extensions/caldav-privatecomments.txt:3307-3349
/CalendarServer/branches/users/cdaboo/implicitauto-2947/doc/Extensions/caldav-privatecomments-00.txt:2948-2989
/CalendarServer/branches/users/cdaboo/location-partial-accept-3573/doc/Extensions/caldav-privatecomments.txt:3574-3581
/CalendarServer/branches/users/sagen/resource-delegates-4038/doc/Extensions/caldav-privatecomments.txt:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/doc/Extensions/caldav-privatecomments.txt:4068-4075
/CalendarServer/trunk/doc/Extensions/caldav-privatecomments.txt:4105-4107,4113-4116,4121-4124,4137-4139,4141-4144,4154-4159,4163-4167,4172,4174-4176,4178-4180,4191,4194,4197,4213
   + /CalendarServer/branches/users/cdaboo/attendee-comments-2886/doc/Extensions/caldav-privatecomments-00.txt:2887-2910
/CalendarServer/branches/users/cdaboo/byebye-serviceslocator-2937/doc/Extensions/caldav-privatecomments-00.txt:2938-3097
/CalendarServer/branches/users/cdaboo/implicit-if-match-3306/doc/Extensions/caldav-privatecomments.txt:3307-3349
/CalendarServer/branches/users/cdaboo/implicitauto-2947/doc/Extensions/caldav-privatecomments-00.txt:2948-2989
/CalendarServer/branches/users/cdaboo/location-partial-accept-3573/doc/Extensions/caldav-privatecomments.txt:3574-3581
/CalendarServer/branches/users/sagen/resource-delegates-4038/doc/Extensions/caldav-privatecomments.txt:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/doc/Extensions/caldav-privatecomments.txt:4068-4075
/CalendarServer/trunk/doc/Extensions/caldav-privatecomments.txt:4105-4107,4113-4116,4121-4124,4137-4139,4141-4144,4154-4159,4163-4167,4172,4174-4176,4178-4180,4191,4194,4197-4202,4209,4213


Property changes on: CalendarServer/branches/release/CalendarServer-2.2-dev/doc/Extensions/caldav-privatecomments.xml
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/users/cdaboo/attendee-comments-2886/doc/Extensions/caldav-privatecomments-00.xml:2887-2910
/CalendarServer/branches/users/cdaboo/byebye-serviceslocator-2937/doc/Extensions/caldav-privatecomments-00.xml:2938-3097
/CalendarServer/branches/users/cdaboo/implicit-if-match-3306/doc/Extensions/caldav-privatecomments.xml:3307-3349
/CalendarServer/branches/users/cdaboo/implicitauto-2947/doc/Extensions/caldav-privatecomments-00.xml:2948-2989
/CalendarServer/branches/users/cdaboo/location-partial-accept-3573/doc/Extensions/caldav-privatecomments.xml:3574-3581
/CalendarServer/branches/users/sagen/resource-delegates-4038/doc/Extensions/caldav-privatecomments.xml:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/doc/Extensions/caldav-privatecomments.xml:4068-4075
/CalendarServer/trunk/doc/Extensions/caldav-privatecomments.xml:4105-4107,4113-4116,4121-4124,4137-4139,4141-4144,4154-4159,4163-4167,4172,4174-4176,4178-4180,4191,4194,4197,4213
   + /CalendarServer/branches/users/cdaboo/attendee-comments-2886/doc/Extensions/caldav-privatecomments-00.xml:2887-2910
/CalendarServer/branches/users/cdaboo/byebye-serviceslocator-2937/doc/Extensions/caldav-privatecomments-00.xml:2938-3097
/CalendarServer/branches/users/cdaboo/implicit-if-match-3306/doc/Extensions/caldav-privatecomments.xml:3307-3349
/CalendarServer/branches/users/cdaboo/implicitauto-2947/doc/Extensions/caldav-privatecomments-00.xml:2948-2989
/CalendarServer/branches/users/cdaboo/location-partial-accept-3573/doc/Extensions/caldav-privatecomments.xml:3574-3581
/CalendarServer/branches/users/sagen/resource-delegates-4038/doc/Extensions/caldav-privatecomments.xml:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/doc/Extensions/caldav-privatecomments.xml:4068-4075
/CalendarServer/trunk/doc/Extensions/caldav-privatecomments.xml:4105-4107,4113-4116,4121-4124,4137-4139,4141-4144,4154-4159,4163-4167,4172,4174-4176,4178-4180,4191,4194,4197-4202,4209,4213


Property changes on: CalendarServer/branches/release/CalendarServer-2.2-dev/doc/Extensions/caldav-schedulingchanges.txt
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/users/cdaboo/attendee-comments-2886/doc/Extensions/caldav-schedulingchanges-01.txt:2887-2910
/CalendarServer/branches/users/cdaboo/byebye-serviceslocator-2937/doc/Extensions/caldav-schedulingchanges-01.txt:2938-3097
/CalendarServer/branches/users/cdaboo/implicit-if-match-3306/doc/Extensions/caldav-schedulingchanges.txt:3307-3349
/CalendarServer/branches/users/cdaboo/implicitauto-2947/doc/Extensions/caldav-schedulingchanges-01.txt:2948-2989
/CalendarServer/branches/users/cdaboo/location-partial-accept-3573/doc/Extensions/caldav-schedulingchanges.txt:3574-3581
/CalendarServer/branches/users/sagen/resource-delegates-4038/doc/Extensions/caldav-schedulingchanges.txt:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/doc/Extensions/caldav-schedulingchanges.txt:4068-4075
/CalendarServer/trunk/doc/Extensions/caldav-schedulingchanges.txt:4105-4107,4113-4116,4121-4124,4137-4139,4141-4144,4154-4159,4163-4167,4172,4174-4176,4178-4180,4191,4194,4197,4213
   + /CalendarServer/branches/users/cdaboo/attendee-comments-2886/doc/Extensions/caldav-schedulingchanges-01.txt:2887-2910
/CalendarServer/branches/users/cdaboo/byebye-serviceslocator-2937/doc/Extensions/caldav-schedulingchanges-01.txt:2938-3097
/CalendarServer/branches/users/cdaboo/implicit-if-match-3306/doc/Extensions/caldav-schedulingchanges.txt:3307-3349
/CalendarServer/branches/users/cdaboo/implicitauto-2947/doc/Extensions/caldav-schedulingchanges-01.txt:2948-2989
/CalendarServer/branches/users/cdaboo/location-partial-accept-3573/doc/Extensions/caldav-schedulingchanges.txt:3574-3581
/CalendarServer/branches/users/sagen/resource-delegates-4038/doc/Extensions/caldav-schedulingchanges.txt:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/doc/Extensions/caldav-schedulingchanges.txt:4068-4075
/CalendarServer/trunk/doc/Extensions/caldav-schedulingchanges.txt:4105-4107,4113-4116,4121-4124,4137-4139,4141-4144,4154-4159,4163-4167,4172,4174-4176,4178-4180,4191,4194,4197-4202,4209,4213


Property changes on: CalendarServer/branches/release/CalendarServer-2.2-dev/doc/Extensions/caldav-schedulingchanges.xml
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/users/cdaboo/attendee-comments-2886/doc/Extensions/caldav-schedulingchanges-01.xml:2887-2910
/CalendarServer/branches/users/cdaboo/byebye-serviceslocator-2937/doc/Extensions/caldav-schedulingchanges-01.xml:2938-3097
/CalendarServer/branches/users/cdaboo/implicit-if-match-3306/doc/Extensions/caldav-schedulingchanges.xml:3307-3349
/CalendarServer/branches/users/cdaboo/implicitauto-2947/doc/Extensions/caldav-schedulingchanges-01.xml:2948-2989
/CalendarServer/branches/users/cdaboo/location-partial-accept-3573/doc/Extensions/caldav-schedulingchanges.xml:3574-3581
/CalendarServer/branches/users/sagen/resource-delegates-4038/doc/Extensions/caldav-schedulingchanges.xml:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/doc/Extensions/caldav-schedulingchanges.xml:4068-4075
/CalendarServer/trunk/doc/Extensions/caldav-schedulingchanges.xml:4105-4107,4113-4116,4121-4124,4137-4139,4141-4144,4154-4159,4163-4167,4172,4174-4176,4178-4180,4191,4194,4197,4213
   + /CalendarServer/branches/users/cdaboo/attendee-comments-2886/doc/Extensions/caldav-schedulingchanges-01.xml:2887-2910
/CalendarServer/branches/users/cdaboo/byebye-serviceslocator-2937/doc/Extensions/caldav-schedulingchanges-01.xml:2938-3097
/CalendarServer/branches/users/cdaboo/implicit-if-match-3306/doc/Extensions/caldav-schedulingchanges.xml:3307-3349
/CalendarServer/branches/users/cdaboo/implicitauto-2947/doc/Extensions/caldav-schedulingchanges-01.xml:2948-2989
/CalendarServer/branches/users/cdaboo/location-partial-accept-3573/doc/Extensions/caldav-schedulingchanges.xml:3574-3581
/CalendarServer/branches/users/sagen/resource-delegates-4038/doc/Extensions/caldav-schedulingchanges.xml:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/doc/Extensions/caldav-schedulingchanges.xml:4068-4075
/CalendarServer/trunk/doc/Extensions/caldav-schedulingchanges.xml:4105-4107,4113-4116,4121-4124,4137-4139,4141-4144,4154-4159,4163-4167,4172,4174-4176,4178-4180,4191,4194,4197-4202,4209,4213

Deleted: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/appleopendirectory.py	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/appleopendirectory.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -1,1173 +0,0 @@
-##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Apple Open Directory directory service implementation.
-"""
-
-__all__ = [
-    "OpenDirectoryService",
-    "OpenDirectoryInitError",
-]
-
-import sys
-from random import random
-from uuid import UUID
-
-from twext.python.plistlib import readPlistFromString
-
-from xml.parsers.expat import ExpatError
-
-import opendirectory
-import dsattributes
-import dsquery
-
-from twisted.internet.reactor import callLater
-from twisted.internet.threads import deferToThread
-from twisted.cred.credentials import UsernamePassword
-from twisted.web2.auth.digest import DigestedCredentials
-
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.directory.directory import DirectoryError, UnknownRecordTypeError
-from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
-
-class OpenDirectoryService(DirectoryService):
-    """
-    Open Directory implementation of L{IDirectoryService}.
-    """
-    baseGUID = "891F8321-ED02-424C-BA72-89C32F215C1E"
-
-    def __repr__(self):
-        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.node)
-
-    def __init__(
-        self,
-        node="/Search",
-        restrictEnabledRecords=False,
-        restrictToGroup="",
-        dosetup=True,
-        cacheTimeout=30
-    ):
-        """
-        @param node: an OpenDirectory node name to bind to.
-        @param restrictEnabledRecords: C{True} if a group in the directory is to be used to determine
-            which calendar users are enabled.
-        @param restrictToGroup: C{str} guid or name of group used to restrict enabled users.
-        @param dosetup: if C{True} then the directory records are initialized,
-                        if C{False} they are not.
-                        This should only be set to C{False} when doing unit tests.
-        @param cacheTimeout: C{int} number of minutes before cache is invalidated.
-        """
-        try:
-            directory = opendirectory.odInit(node)
-        except opendirectory.ODError, e:
-            self.log_error("Open Directory (node=%s) Initialization error: %s" % (node, e))
-            raise
-
-        self.realmName = node
-        self.directory = directory
-        self.node = node
-        self.restrictEnabledRecords = restrictEnabledRecords
-        self.restrictToGroup = restrictToGroup
-        try:
-            UUID(self.restrictToGroup)
-        except:
-            self.restrictToGUID = False
-        else:
-            self.restrictToGUID = True
-        self.restrictedGUIDs = None
-        self.cacheTimeout = cacheTimeout
-        self._records = {}
-        self._delayedCalls = set()
-
-        if dosetup:
-            for recordType in self.recordTypes():
-                self.recordsForType(recordType)
-
-    def __cmp__(self, other):
-        if not isinstance(other, DirectoryRecord):
-            return super(DirectoryRecord, self).__eq__(other)
-
-        for attr in ("directory", "node"):
-            diff = cmp(getattr(self, attr), getattr(other, attr))
-            if diff != 0:
-                return diff
-        return 0
-
-    def __hash__(self):
-        h = hash(self.__class__)
-        for attr in ("directory", "node"):
-            h = (h + hash(getattr(self, attr))) & sys.maxint
-        return h
-
-    def _expandGroupMembership(self, members, nestedGroups, processedGUIDs=None, returnGroups=False):
-
-        if processedGUIDs is None:
-            processedGUIDs = set()
-
-        if isinstance(members, str):
-            members = [members]
-
-        if isinstance(nestedGroups, str):
-            nestedGroups = [nestedGroups]
-
-        for memberGUID in members:
-            if memberGUID not in processedGUIDs:
-                processedGUIDs.add(memberGUID)
-                yield memberGUID
-
-        for groupGUID in nestedGroups:
-            if groupGUID in processedGUIDs:
-                continue
-
-            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                self.directory,
-                dsattributes.kDS1AttrGeneratedUID,
-                groupGUID,
-                dsattributes.eDSExact,
-                False,
-                dsattributes.kDSStdRecordTypeGroups,
-                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
-            ))
-            result = opendirectory.queryRecordsWithAttribute_list(
-                self.directory,
-                dsattributes.kDS1AttrGeneratedUID,
-                groupGUID,
-                dsattributes.eDSExact,
-                False,
-                dsattributes.kDSStdRecordTypeGroups,
-                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
-            )
-
-            if not result:
-                self.log_error("Couldn't find group %s when trying to expand nested groups."
-                             % (groupGUID,))
-                continue
-
-            group = result[0][1]
-
-            processedGUIDs.add(groupGUID)
-            if returnGroups:
-                yield groupGUID
-
-            for GUID in self._expandGroupMembership(
-                group.get(dsattributes.kDSNAttrGroupMembers, []),
-                group.get(dsattributes.kDSNAttrNestedGroups, []),
-                processedGUIDs,
-                returnGroups,
-            ):
-                yield GUID
-
-    def _calendarUserAddresses(self, recordType, recordData):
-        """
-        Extract specific attributes from the directory record for use as calendar user address.
-        
-        @param recordData: a C{dict} containing the attributes retrieved from the directory.
-        @return: a C{set} of C{str} for each expanded calendar user address.
-        """
-        # Now get the addresses
-        result = set()
-        
-        # Add each email address as a mailto URI
-        emails = recordData.get(dsattributes.kDSNAttrEMailAddress)
-        if emails is not None:
-            if isinstance(emails, str):
-                emails = [emails]
-            for email in emails:
-                result.add("mailto:%s" % (email.lower(),))
-                
-        return result
-
-    def _parseResourceInfo(self, plist, guid, recordType, shortname):
-        """
-        Parse OD ResourceInfo attribute and extract information that the server needs.
-
-        @param plist: the plist that is the attribute value.
-        @type plist: str
-        @param guid: the directory GUID of the record being parsed.
-        @type guid: str
-        @param shortname: the record shortname of the record being parsed.
-        @type shortname: str
-        @return: a C{tuple} of C{bool} for auto-accept, C{str} for proxy GUID, C{str} for read-only proxy GUID.
-        """
-        try:
-            plist = readPlistFromString(plist)
-            wpframework = plist.get("com.apple.WhitePagesFramework", {})
-            autoaccept = wpframework.get("AutoAcceptsInvitation", False)
-            proxy = wpframework.get("CalendaringDelegate", None)
-            read_only_proxy = wpframework.get("ReadOnlyCalendaringDelegate", None)
-        except (ExpatError, AttributeError), e:
-            self.log_error(
-                "Failed to parse ResourceInfo attribute of record (%s)%s (guid=%s): %s\n%s" %
-                (recordType, shortname, guid, e, plist,)
-            )
-            raise ValueError("Invalid ResourceInfo")
-
-        return (autoaccept, proxy, read_only_proxy,)
-
-    def recordTypes(self):
-        return (
-            self.recordType_users,
-            self.recordType_groups,
-            self.recordType_locations,
-            self.recordType_resources,
-        )
-
-    def _storage(self, recordType):
-        try:
-            storage = self._records[recordType]
-        except KeyError:
-            self.reloadCache(recordType)
-            storage = self._records[recordType]
-        else:
-            if storage["status"] == "stale":
-                storage["status"] = "loading"
-
-                def onError(f):
-                    storage["status"] = "stale" # Keep trying
-                    self.log_error(
-                        "Unable to load records of type %s from OpenDirectory due to unexpected error: %s"
-                        % (recordType, f)
-                    )
-
-                # Reload the restricted access group details if reloading user records
-                if recordType == self.recordType_users:
-                    self.restrictedGUIDs = None
-
-                d = deferToThread(self.reloadCache, recordType)
-                d.addErrback(onError)
-
-        return storage
-
-    def recordsForType(self, recordType):
-        """
-        @param recordType: a record type
-        @return: a dictionary containing all records for the given record
-        type.  Keys are short names and values are the corresponding
-        OpenDirectoryRecord for the given record type.
-        """
-        return self._storage(recordType)["records"]
-
-    def listRecords(self, recordType):
-        return self.recordsForType(recordType).itervalues()
-
-    def recordWithShortName(self, recordType, shortName):
-        try:
-            return self.recordsForType(recordType)[shortName]
-        except KeyError:
-            # Check negative cache
-            if shortName in self._storage(recordType)["disabled names"]:
-                return None
-
-            # Cache miss; try looking the record up, in case it is new
-            # FIXME: This is a blocking call (hopefully it's a fast one)
-            self.reloadCache(recordType, lookup=("shortName", shortName,))
-            record = self.recordsForType(recordType).get(shortName, None)
-            if record is None:
-                # Add to negative cache
-                self._storage(recordType)["disabled names"].add(shortName)
-            return record
-
-    def recordWithCalendarUserAddress(self, address):
-        address = normalizeCUAddr(address)
-        record = None
-        if address.startswith("urn:uuid:"):
-            guid = address[9:]
-            record = self.recordWithGUID(guid)
-        elif address.startswith("mailto:"):
-            record = self._recordWithAttribute("cuas", "disabled cuas", "cua", address)
-
-        return record if record and record.enabledForCalendaring else None
-
-    def recordWithGUID(self, guid):
-        return self._recordWithAttribute("guids", "disabled guids", "guid", guid)
-
-    recordWithUID = recordWithGUID
-
-    def recordWithAuthID(self, authID):
-        return self._recordWithAttribute("authIDs", "disabled authIDs", "authID", authID)
-
-    def _recordWithAttribute(self, cacheKey, disabledKey, lookupKey, value):
-        def lookup():
-            for recordType in self.recordTypes():
-                record = self._storage(recordType)[cacheKey].get(value, None)
-                if record:
-                    return record
-            else:
-                return None
-
-        record = lookup()
-
-        if record is None:
-            # Cache miss; try looking the record up, in case it is new
-            for recordType in self.recordTypes():
-                # Check negative cache
-                if value in self._storage(recordType)[disabledKey]:
-                    continue
-
-                try:
-                    self.reloadCache(recordType, lookup=(lookupKey, value,))
-                    record = lookup()
-                except opendirectory.ODError, e:
-                    if e.message[1] == -14140 or e.message[1] == -14200:
-                        # Unsupported attribute on record - don't fail
-                        record = None
-                    else:
-                        raise
-
-                if record is None:
-                    self._storage(recordType)[disabledKey].add(value)
-                else:
-                    self.log_info("Faulted record with %s %s into %s record cache"
-                                  % (lookupKey, value, recordType))
-                    break
-            else:
-                # Nothing found; add to negative cache
-                self.log_info("Unable to find any record with %s %s" % (lookupKey, value,))
-
-        return record
-
-    def groupsForGUID(self, guid):
-        
-        # Lookup in index
-        try:
-            return self._storage(self.recordType_groups)["groupsForGUID"][guid]
-        except KeyError:
-            return ()
-
-    def proxiesForGUID(self, recordType, guid):
-        
-        # Lookup in index
-        try:
-            return self._storage(recordType)["proxiesForGUID"][guid]
-        except KeyError:
-            return ()
-
-    def readOnlyProxiesForGUID(self, recordType, guid):
-        
-        # Lookup in index
-        try:
-            return self._storage(recordType)["readOnlyProxiesForGUID"][guid]
-        except KeyError:
-            return ()
-
-    def _indexGroup(self, group, guids, index):
-        for guid in guids:
-            index.setdefault(guid, set()).add(group)
-
-    _ODFields = {
-        'fullName' : {
-            'odField' : dsattributes.kDS1AttrDistinguishedName,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-                dsattributes.kDSStdRecordTypeGroups,
-                dsattributes.kDSStdRecordTypePlaces,
-                dsattributes.kDSStdRecordTypeResources,
-            ]),
-        },
-        'firstName' : {
-            'odField' : dsattributes.kDS1AttrFirstName,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-            ]),
-        },
-        'lastName' : {
-            'odField' : dsattributes.kDS1AttrLastName,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-            ]),
-        },
-        'emailAddresses' : {
-            'odField' : dsattributes.kDSNAttrEMailAddress,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-                dsattributes.kDSStdRecordTypeGroups,
-            ]),
-        },
-        'recordName' : {
-            'odField' : dsattributes.kDSNAttrRecordName,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-                dsattributes.kDSStdRecordTypeGroups,
-                dsattributes.kDSStdRecordTypePlaces,
-                dsattributes.kDSStdRecordTypeResources,
-            ]),
-        },
-        'guid' : {
-            'odField' : dsattributes.kDS1AttrGeneratedUID,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-                dsattributes.kDSStdRecordTypeGroups,
-                dsattributes.kDSStdRecordTypePlaces,
-                dsattributes.kDSStdRecordTypeResources,
-            ]),
-        },
-    }
-
-    _toODRecordTypes = {
-        DirectoryService.recordType_users :
-            dsattributes.kDSStdRecordTypeUsers,
-        DirectoryService.recordType_locations :
-            dsattributes.kDSStdRecordTypePlaces,
-        DirectoryService.recordType_groups :
-            dsattributes.kDSStdRecordTypeGroups,
-        DirectoryService.recordType_resources :
-            dsattributes.kDSStdRecordTypeResources,
-    }
-
-    _fromODRecordTypes = dict([(b, a) for a, b in _toODRecordTypes.iteritems()])
-
-
-    def recordsMatchingFields(self, fields, operand="or", recordType=None):
-
-        # Note that OD applies case-sensitivity globally across the entire
-        # query, not per expression, so the current code uses whatever is
-        # specified in the last field in the fields list
-
-        def collectResults(results):
-            self.log_info("Got back %d records from OD" % (len(results),))
-            for key, val in results.iteritems():
-                self.log_debug("OD result: %s %s" % (key, val))
-                try:
-                    guid = val[dsattributes.kDS1AttrGeneratedUID]
-                    record = self.recordWithGUID(guid)
-                    if record:
-                        yield record
-                except KeyError:
-                    pass
-
-        def multiQuery(directory, queries, attrs, operand):
-            results = {}
-
-            for query, recordTypes in queries.iteritems():
-                if not query:
-                    continue
-
-                expressions = []
-                for ODField, value, caseless, matchType in query:
-                    if matchType == "starts-with":
-                        comparison = dsattributes.eDSStartsWith
-                    elif matchType == "contains":
-                        comparison = dsattributes.eDSContains
-                    else:
-                        comparison = dsattributes.eDSExact
-                    expressions.append(dsquery.match(ODField, value, comparison))
-
-                complexExpression = dsquery.expression(operand, expressions).generate()
-
-                self.log_info("Calling OD: Types %s, Operand %s, Caseless %s, %s" %
-                    (recordTypes, operand, caseless, complexExpression))
-
-                results.update(
-                    opendirectory.queryRecordsWithAttributes(
-                        directory,
-                        complexExpression,
-                        caseless,
-                        recordTypes,
-                        attrs,
-                    )
-                )
-
-            return results
-
-
-        operand = (dsquery.expression.OR if operand == "or"
-            else dsquery.expression.AND)
-
-        if recordType is None:
-            # The client is looking for records in any of the four types
-            recordTypes = set(self._toODRecordTypes.values())
-        else:
-            # The client is after only one recordType
-            recordTypes = [self._toODRecordTypes[recordType]]
-
-        queries = buildQueries(recordTypes, fields, self._ODFields)
-
-        deferred = deferToThread(
-            multiQuery,
-            self.directory,
-            queries,
-            [ dsattributes.kDS1AttrGeneratedUID ],
-            operand
-        )
-        deferred.addCallback(collectResults)
-        return deferred
-
-
-    def reloadCache(self, recordType, lookup=None):
-        if lookup is not None:
-            self.log_info("Faulting record with %s %s into %s record cache" % (lookup[0], lookup[1], recordType))
-        else:
-            self.log_info("Reloading %s record cache" % (recordType,))
-
-        results = self._queryDirectory(recordType, lookup=lookup)
-
-        if lookup is None:
-            records = {}
-            guids   = {}
-            authIDs = {}
-            cuas  = {}
-
-            disabledNames   = set()
-            disabledGUIDs   = set()
-            disabledAuthIDs = set()
-            disabledCUAs    = set()
-
-            if recordType == self.recordType_groups:
-                groupsForGUID = {}
-            elif recordType in (self.recordType_resources, self.recordType_locations):
-                proxiesForGUID = {}
-                readOnlyProxiesForGUID = {}
-        else:
-            storage = self._records[recordType]
-
-            records = storage["records"]
-            guids   = storage["guids"]
-            authIDs = storage["authIDs"]
-            cuas    = storage["cuas"]
-
-            disabledNames   = storage["disabled names"]
-            disabledGUIDs   = storage["disabled guids"]
-            disabledAuthIDs = storage["disabled authIDs"]
-            disabledCUAs    = storage["disabled cuas"]
-
-            if recordType == self.recordType_groups:
-                groupsForGUID = storage["groupsForGUID"]
-            elif recordType in (self.recordType_resources, self.recordType_locations):
-                proxiesForGUID = storage["proxiesForGUID"]
-                readOnlyProxiesForGUID = storage["readOnlyProxiesForGUID"]
-
-        enabled_count = 0
-        
-        def _uniqueTupleFromAttribute(attribute):
-            if attribute:
-                if isinstance(attribute, str):
-                    return (attribute,)
-                else:
-                    s = set()
-                    return tuple([(s.add(x), x)[1] for x in attribute if x not in s])
-            else:
-                return ()
-            
-        def _setFromAttribute(attribute, lower=False):
-            if attribute:
-                if isinstance(attribute, str):
-                    return set((attribute.lower() if lower else attribute,))
-                else:
-                    return set([item.lower() if lower else item for item in attribute])
-            else:
-                return ()
-            
-        for (recordShortName, value) in results:
-
-            # Now get useful record info.
-            recordGUID           = value.get(dsattributes.kDS1AttrGeneratedUID)
-            recordShortNames     = _uniqueTupleFromAttribute(value.get(dsattributes.kDSNAttrRecordName))
-            recordAuthIDs        = _setFromAttribute(value.get(dsattributes.kDSNAttrAltSecurityIdentities))
-            recordFullName       = value.get(dsattributes.kDS1AttrDistinguishedName)
-            recordFirstName      = value.get(dsattributes.kDS1AttrFirstName)
-            recordLastName       = value.get(dsattributes.kDS1AttrLastName)
-            recordEmailAddresses = _setFromAttribute(value.get(dsattributes.kDSNAttrEMailAddress), lower=True)
-            recordNodeName       = value.get(dsattributes.kDSNAttrMetaNodeLocation)
-
-            if not recordGUID:
-                self.log_debug("Record (%s)%s in node %s has no GUID; ignoring."
-                               % (recordType, recordShortName, recordNodeName))
-                continue
-
-            if recordGUID.lower().startswith("ffffeeee-dddd-cccc-bbbb-aaaa"):
-                self.log_debug("Ignoring system record (%s)%s in node %s."
-                               % (recordType, recordShortName, recordNodeName))
-                continue
-
-            # Determine enabled state
-            if recordType == self.recordType_groups:
-                enabledForCalendaring = False
-            else:
-                if self.restrictEnabledRecords and self.restrictedGUIDs is not None:
-                    enabledForCalendaring = recordGUID in self.restrictedGUIDs
-                else:
-                    enabledForCalendaring = True
-
-            if enabledForCalendaring:
-                enabled_count += 1
-                calendarUserAddresses = self._calendarUserAddresses(recordType, value)
-            else:
-                # Some records we want to keep even though they are not enabled for calendaring.
-                # Others we discard.
-                if recordType not in (
-                    self.recordType_users,
-                    self.recordType_groups,
-                ):
-                    self.log_debug(
-                        "Record (%s) %s is not enabled for calendaring"
-                        % (recordType, recordShortName)
-                    )
-                    continue
-
-                self.log_debug(
-                    "Record (%s) %s is not enabled for calendaring but may be used in ACLs"
-                    % (recordType, recordShortName)
-                )
-                calendarUserAddresses = ()
-
-            # Special case for groups, which have members.
-            if recordType == self.recordType_groups:
-                memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
-                if memberGUIDs is None:
-                    memberGUIDs = ()
-                elif type(memberGUIDs) is str:
-                    memberGUIDs = (memberGUIDs,)
-                nestedGUIDs = value.get(dsattributes.kDSNAttrNestedGroups)
-                if nestedGUIDs:
-                    if type(nestedGUIDs) is str:
-                        nestedGUIDs = (nestedGUIDs,)
-                    memberGUIDs += tuple(nestedGUIDs)
-            else:
-                memberGUIDs = ()
-
-            record = OpenDirectoryRecord(
-                service               = self,
-                recordType            = recordType,
-                guid                  = recordGUID,
-                nodeName              = recordNodeName,
-                shortNames            = recordShortNames,
-                authIDs               = recordAuthIDs,
-                fullName              = recordFullName,
-                firstName             = recordFirstName,
-                lastName              = recordLastName,
-                emailAddresses        = recordEmailAddresses,
-                calendarUserAddresses = calendarUserAddresses,
-                enabledForCalendaring = enabledForCalendaring,
-                memberGUIDs           = memberGUIDs,
-            )
-
-            def disableGUID(guid, record):
-                """
-                Disable the record by removing it from all indexes.
-                """
-
-                self.log_warn("GUID %s disabled due to conflict for record: %s"
-                              % (guid, record))
-
-                disabledGUIDs.add(guid)
-                disabledNames.update(record.shortNames)
-                disabledAuthIDs.update(record.authIDs)
-                disabledCUAs.update(record.calendarUserAddresses)
-
-                if guid in guids:
-                    try:
-                        del guids[guid]
-                    except KeyError:
-                        pass
-                for shortName in record.shortNames:
-                    try:
-                        del records[shortName]
-                    except KeyError:
-                        pass
-                for authID in record.authIDs:
-                    try:
-                        del authIDs[authID]
-                    except KeyError:
-                        pass
-                for cua in record.calendarUserAddresses:
-                    try:
-                        del cuas[cua]
-                    except KeyError:
-                        pass
-
-            if record.guid in disabledGUIDs:
-                disableGUID(record.guid, record)
-            else:
-                # Check for duplicates
-                existing_record = guids.get(record.guid)
-                if existing_record is not None:
-                    if existing_record.shortNames != record.shortNames:
-                        disableGUID(record.guid, record)
-                        disableGUID(record.guid, existing_record)
-                        if existing_record.enabledForCalendaring:
-                            enabled_count -= 1
-                else:
-                    guids[record.guid] = record
-                    self.log_debug("Added record %s to OD record cache" % (record,))
-                    if record.enabledForCalendaring:
-                        enabled_count += 1
-        
-                    # Do group indexing if needed
-                    if recordType == self.recordType_groups:
-                        self._indexGroup(record, record._memberGUIDs, groupsForGUID)
-
-                    # Index non-duplicate shortNames
-                    def disableName(shortName, record):
-                        self.log_warn("Short name %s disabled due to conflict for record: %s"
-                                      % (shortName, record))
-        
-                        record.shortNames = tuple([item for item in record.shortNames if item != shortName])
-                        disabledNames.add(shortName)
-        
-                        if shortName in records:
-                            del records[shortName]
-        
-                    for shortName in tuple(record.shortNames):
-                        if shortName in disabledNames:
-                            disableName(shortName, record)
-                        else:
-                            # Check for duplicates
-                            existing_record = records.get(shortName)
-                            if existing_record is not None and existing_record != record:
-                                disableName(shortName, record)
-                                disableName(shortName, existing_record)
-                            else:
-                                records[shortName] = record
-        
-                    # Index non-duplicate authIDs
-                    def disableAuthIDs(authID, record):
-                        self.log_warn("Auth ID %s disabled due to conflict for record: %s"
-                                      % (authID, record))
-        
-                        record.authIDs.remove(authID)
-                        disabledAuthIDs.add(authID)
-        
-                        if authID in authIDs:
-                            del authIDs[authID]
-        
-                    for authID in frozenset(recordAuthIDs):
-                        if authID in disabledAuthIDs:
-                            disableAuthIDs(authID, record)
-                        else:
-                            # Check for duplicates
-                            existing_record = authIDs.get(authID)
-                            if existing_record is not None:
-                                disableAuthIDs(authID, record)
-                                disableAuthIDs(authID, existing_record)
-                            else:
-                                authIDs[authID] = record
-        
-                    # Index non-duplicate CUAs
-                    def disableCUA(cua, record):
-                        self.log_warn("CUA %s disabled due to conflict for record: %s"
-                                      % (cua, record))
-        
-                        record.calendarUserAddresses.remove(cua)
-                        disabledCUAs.add(cua)
-        
-                        if cua in cuas:
-                            del cuas[cua]
-
-                        if cua in records:
-                            del records[cua]
-        
-                    for cua in frozenset(calendarUserAddresses):
-                        if cua in disabledCUAs:
-                            disableCUA(cua, record)
-                        else:
-                            # Check for duplicates
-                            existing_record = cuas.get(cua)
-                            if existing_record is not None:
-                                disableCUA(cua, record)
-                                disableCUA(cua, existing_record)
-                            else:
-                                cuas[cua] = record
-
-        if lookup is None:
-            #
-            # Replace the entire cache
-            #
-            storage = {
-                "status"           : "new",
-                "records"          : records,
-                "guids"            : guids,
-                "authIDs"          : authIDs,
-                "cuas"             : cuas,
-                "disabled names"   : disabledNames,
-                "disabled guids"   : disabledGUIDs,
-                "disabled authIDs" : disabledAuthIDs,
-                "disabled cuas"    : disabledCUAs,
-            }
-
-            # Add group indexing if needed
-            if recordType == self.recordType_groups:
-                storage["groupsForGUID"] = groupsForGUID
-
-            # Add proxy indexing if needed
-            elif recordType in (self.recordType_resources, self.recordType_locations):
-                storage["proxiesForGUID"] = proxiesForGUID
-                storage["readOnlyProxiesForGUID"] = readOnlyProxiesForGUID
-
-            def rot():
-                storage["status"] = "stale"
-                removals = set()
-                for call in self._delayedCalls:
-                    if not call.active():
-                        removals.add(call)
-                for item in removals:
-                    self._delayedCalls.remove(item)
-
-            #
-            # Add jitter/fuzz factor to avoid stampede for large OD query
-            # Max out the jitter at 60 minutes
-            #
-            cacheTimeout = min(self.cacheTimeout, 60) * 60
-            cacheTimeout = (cacheTimeout * random()) - (cacheTimeout / 2)
-            cacheTimeout += self.cacheTimeout * 60
-            self._delayedCalls.add(callLater(cacheTimeout, rot))
-
-            self._records[recordType] = storage
-
-            self.log_info(
-                "Added %d (%d enabled) records to %s OD record cache; expires in %d seconds"
-                % (len(self._records[recordType]["guids"]), enabled_count, recordType, cacheTimeout)
-            )
-
-    def _queryDirectory(self, recordType, lookup=None):
-        attrs = [
-            dsattributes.kDS1AttrGeneratedUID,
-            dsattributes.kDSNAttrRecordName,
-            dsattributes.kDSNAttrAltSecurityIdentities,
-            dsattributes.kDS1AttrDistinguishedName,
-            dsattributes.kDS1AttrFirstName,
-            dsattributes.kDS1AttrLastName,
-            dsattributes.kDSNAttrEMailAddress,
-            dsattributes.kDSNAttrMetaNodeLocation,
-        ]
-
-        if recordType == self.recordType_users:
-            listRecordType = dsattributes.kDSStdRecordTypeUsers
-
-        elif recordType == self.recordType_groups:
-            listRecordType = dsattributes.kDSStdRecordTypeGroups
-            attrs.append(dsattributes.kDSNAttrGroupMembers)
-            attrs.append(dsattributes.kDSNAttrNestedGroups)
-
-        elif recordType == self.recordType_locations:
-            listRecordType = dsattributes.kDSStdRecordTypePlaces
-            attrs.append(dsattributes.kDSNAttrResourceInfo)
-        
-        elif recordType == self.recordType_resources:
-            listRecordType = dsattributes.kDSStdRecordTypeResources
-            attrs.append(dsattributes.kDSNAttrResourceInfo)
-        
-        else:
-            raise UnknownRecordTypeError("Unknown Open Directory record type: %s" % (recordType))
-
-        # If restricting enabled records, then make sure the restricted group member
-        # details are loaded. Do nested group expansion and include the nested groups
-        # as enabled records too.
-        if self.restrictEnabledRecords and self.restrictedGUIDs is None:
-
-            attributeToMatch = dsattributes.kDS1AttrGeneratedUID if self.restrictToGUID else dsattributes.kDSNAttrRecordName 
-            valueToMatch = self.restrictToGroup
-
-            self.log_debug("Doing restricted group membership check")
-            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                self.directory,
-                attributeToMatch,
-                valueToMatch,
-                dsattributes.eDSExact,
-                False,
-                dsattributes.kDSStdRecordTypeGroups,
-                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups,],
-            ))
-            results = opendirectory.queryRecordsWithAttribute_list(
-                self.directory,
-                attributeToMatch,
-                valueToMatch,
-                dsattributes.eDSExact,
-                False,
-                dsattributes.kDSStdRecordTypeGroups,
-                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups,],
-            )
-    
-            if len(results) == 1:
-                members      = results[0][1].get(dsattributes.kDSNAttrGroupMembers, [])
-                nestedGroups = results[0][1].get(dsattributes.kDSNAttrNestedGroups, [])
-            else:
-                members = []
-                nestedGroups = []
-
-            self.restrictedGUIDs = set(self._expandGroupMembership(members, nestedGroups, returnGroups=True))
-            self.log_debug("Got %d restricted group members" % (len(self.restrictedGUIDs),))
-
-        query = None
-        if lookup is not None:
-            indexType, indexKey = lookup
-            origIndexKey = indexKey
-
-            if indexType == "cua":
-                # The directory doesn't contain CUAs, so we need to convert
-                # the CUA to the appropriate field name and value:
-                queryattr, indexKey = cuAddressConverter(indexKey)
-                # queryattr will be one of:
-                # guid, emailAddresses, or recordName
-                # ...which will need to be mapped to DS
-                queryattr = self._ODFields[queryattr]['odField']
-
-            else:
-                queryattr = {
-                    "shortName" : dsattributes.kDSNAttrRecordName,
-                    "guid"      : dsattributes.kDS1AttrGeneratedUID,
-                    "authID"    : dsattributes.kDSNAttrAltSecurityIdentities,
-                }.get(indexType)
-                assert queryattr is not None, "Invalid type for record faulting query"
-
-            query = dsquery.match(queryattr, indexKey, dsattributes.eDSExact)
-
-        try:
-            if query:
-                self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                    self.directory,
-                    query.attribute,
-                    query.value,
-                    query.matchType,
-                    False,
-                    listRecordType,
-                    attrs,
-                ))
-                results = opendirectory.queryRecordsWithAttribute_list(
-                    self.directory,
-                    query.attribute,
-                    query.value,
-                    query.matchType,
-                    False,
-                    listRecordType,
-                    attrs,
-                )
-            else:
-                self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
-                    self.directory,
-                    listRecordType,
-                    attrs,
-                ))
-                results = opendirectory.listAllRecordsWithAttributes_list(
-                    self.directory,
-                    listRecordType,
-                    attrs,
-                )
-        except opendirectory.ODError, ex:
-            self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
-            raise
-
-        return results
-
-    def getResourceInfo(self):
-        """
-        Resource information including proxy assignments for resource and
-        locations, as well as auto-schedule settings, used to live in the
-        directory.  This method fetches old resource info for migration
-        purposes.
-        """
-        attrs = [
-            dsattributes.kDS1AttrGeneratedUID,
-            dsattributes.kDSNAttrResourceInfo,
-        ]
-
-        for recordType in (dsattributes.kDSStdRecordTypePlaces, dsattributes.kDSStdRecordTypeResources):
-            try:
-                self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
-                    self.directory,
-                    recordType,
-                    attrs,
-                ))
-                results = opendirectory.listAllRecordsWithAttributes_list(
-                    self.directory,
-                    recordType,
-                    attrs,
-                )
-            except opendirectory.ODError, ex:
-                self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
-                raise
-
-            for (recordShortName, value) in results:
-                recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
-                resourceInfo = value.get(dsattributes.kDSNAttrResourceInfo)
-                if resourceInfo is not None:
-                    try:
-                        autoSchedule, proxy, readOnlyProxy = self._parseResourceInfo(resourceInfo,
-                            recordGUID, recordType, recordShortName)
-                    except ValueError:
-                        continue
-                    yield recordGUID, autoSchedule, proxy, readOnlyProxy
-
-
-def buildQueries(recordTypes, fields, mapping):
-    """
-    Determine how many queries need to be performed in order to work around opendirectory
-    quirks, where searching on fields that don't apply to a given recordType returns incorrect
-    results (either none, or all records).
-    """
-
-    fieldLists = {}
-    for recordType in recordTypes:
-        fieldLists[recordType] = []
-        for field, value, caseless, matchType in fields:
-            if field in mapping:
-                if recordType in mapping[field]['appliesTo']:
-                    ODField = mapping[field]['odField']
-                    fieldLists[recordType].append((ODField, value, caseless, matchType))
-
-    queries = {}
-    for recordType, fieldList in fieldLists.iteritems():
-        key = tuple(fieldList)
-        queries.setdefault(key, []).append(recordType)
-    return queries
-
-
-class OpenDirectoryRecord(DirectoryRecord):
-    """
-    Open Directory implementation of L{IDirectoryRecord}.
-    """
-    def __init__(
-        self, service, recordType, guid, nodeName, shortNames, authIDs, fullName,
-        firstName, lastName, emailAddresses,
-        calendarUserAddresses, enabledForCalendaring,
-        memberGUIDs,
-    ):
-        super(OpenDirectoryRecord, self).__init__(
-            service               = service,
-            recordType            = recordType,
-            guid                  = guid,
-            shortNames            = shortNames,
-            authIDs               = authIDs,
-            fullName              = fullName,
-            firstName             = firstName,
-            lastName              = lastName,
-            emailAddresses        = emailAddresses,
-            calendarUserAddresses = calendarUserAddresses,
-            enabledForCalendaring = enabledForCalendaring,
-        )
-        self.nodeName = nodeName
-        self._memberGUIDs = tuple(memberGUIDs)
-
-    def __repr__(self):
-        if self.service.realmName == self.nodeName:
-            location = self.nodeName
-        else:
-            location = "%s->%s" % (self.service.realmName, self.nodeName)
-
-        return "<%s[%s@%s(%s)] %s(%s) %r>" % (
-            self.__class__.__name__,
-            self.recordType,
-            self.service.guid,
-            location,
-            self.guid,
-            ",".join(self.shortNames),
-            self.fullName
-        )
-
-    def members(self):
-        if self.recordType != self.service.recordType_groups:
-            return
-
-        for guid in self._memberGUIDs:
-            userRecord = self.service.recordWithGUID(guid)
-            if userRecord is not None:
-                yield userRecord
-
-    def groups(self):
-        return self.service.groupsForGUID(self.guid)
-
-    def verifyCredentials(self, credentials):
-        if isinstance(credentials, UsernamePassword):
-            # Check cached password
-            try:
-                if credentials.password == self.password:
-                    return True
-            except AttributeError:
-                pass
-
-            # Check with directory services
-            try:
-                if opendirectory.authenticateUserBasic(self.service.directory, self.nodeName, self.shortNames[0], credentials.password):
-                    # Cache the password to avoid future DS queries
-                    self.password = credentials.password
-                    return True
-            except opendirectory.ODError, e:
-                self.log_error("Open Directory (node=%s) error while performing basic authentication for user %s: %s"
-                            % (self.service.realmName, self.shortNames[0], e))
-
-            return False
-
-        elif isinstance(credentials, DigestedCredentials):
-            #
-            # We need a special format for the "challenge" and "response" strings passed into open directory, as it is
-            # picky about exactly what it receives.
-            #
-            try:
-                challenge = 'Digest realm="%(realm)s", nonce="%(nonce)s", algorithm=%(algorithm)s' % credentials.fields
-                response = (
-                    'Digest username="%(username)s", '
-                    'realm="%(realm)s", '
-                    'nonce="%(nonce)s", '
-                    'uri="%(uri)s", '
-                    'response="%(response)s",'
-                    'algorithm=%(algorithm)s'
-                ) % credentials.fields
-            except KeyError, e:
-                self.log_error(
-                    "Open Directory (node=%s) error while performing digest authentication for user %s: "
-                    "missing digest response field: %s in: %s"
-                    % (self.service.realmName, self.shortNames[0], e, credentials.fields)
-                )
-                return False
-
-            try:
-                if self.digestcache[credentials.fields["uri"]] == response:
-                    return True
-            except (AttributeError, KeyError):
-                pass
-
-            try:
-                if opendirectory.authenticateUserDigest(
-                    self.service.directory,
-                    self.nodeName,
-                    self.shortNames[0],
-                    challenge,
-                    response,
-                    credentials.method
-                ):
-                    try:
-                        cache = self.digestcache
-                    except AttributeError:
-                        cache = self.digestcache = {}
-
-                    cache[credentials.fields["uri"]] = response
-
-                    return True
-                else:
-                    self.log_debug(
-"""Open Directory digest authentication failed with:
-    Nodename:  %s
-    Username:  %s
-    Challenge: %s
-    Response:  %s
-    Method:    %s
-""" % (self.nodeName, self.shortNames[0], challenge, response, credentials.method))
-
-            except opendirectory.ODError, e:
-                self.log_error(
-                    "Open Directory (node=%s) error while performing digest authentication for user %s: %s"
-                    % (self.service.realmName, self.shortNames[0], e)
-                )
-                return False
-
-            return False
-
-        return super(OpenDirectoryRecord, self).verifyCredentials(credentials)
-
-class OpenDirectoryInitError(DirectoryError):
-    """
-    OpenDirectory initialization error.
-    """

Copied: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/appleopendirectory.py (from rev 4201, CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py)
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/appleopendirectory.py	                        (rev 0)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/appleopendirectory.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -0,0 +1,932 @@
+##
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Apple Open Directory directory service implementation.
+"""
+
+__all__ = [
+    "OpenDirectoryService",
+    "OpenDirectoryInitError",
+]
+
+import sys
+import time
+from uuid import UUID
+
+from twext.python.plistlib import readPlistFromString
+
+from xml.parsers.expat import ExpatError
+
+import opendirectory
+import dsattributes
+import dsquery
+
+from twisted.internet.threads import deferToThread
+from twisted.cred.credentials import UsernamePassword
+from twisted.web2.auth.digest import DigestedCredentials
+from twistedcaldav.config import config
+
+from twistedcaldav.directory.cachingdirectory import CachingDirectoryService,\
+    CachingDirectoryRecord
+from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+from twistedcaldav.directory.directory import DirectoryError, UnknownRecordTypeError
+from twistedcaldav.directory.principal import cuAddressConverter
+
+class OpenDirectoryService(CachingDirectoryService):
+    """
+    Open Directory implementation of L{IDirectoryService}.
+    """
+    baseGUID = "891F8321-ED02-424C-BA72-89C32F215C1E"
+
+    def __repr__(self):
+        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.node)
+
+    def __init__(
+        self,
+        node="/Search",
+        restrictEnabledRecords=False,
+        restrictToGroup="",
+        dosetup=True,
+        cacheTimeout=30
+    ):
+        """
+        @param node: an OpenDirectory node name to bind to.
+        @param restrictEnabledRecords: C{True} if a group in the directory is to be used to determine
+            which calendar users are enabled.
+        @param restrictToGroup: C{str} guid or name of group used to restrict enabled users.
+        @param dosetup: if C{True} then the directory records are initialized,
+                        if C{False} they are not.
+                        This should only be set to C{False} when doing unit tests.
+        @param cacheTimeout: C{int} number of minutes before cache is invalidated.
+        """
+
+        super(OpenDirectoryService, self).__init__(cacheTimeout)
+
+        try:
+            directory = opendirectory.odInit(node)
+        except opendirectory.ODError, e:
+            self.log_error("Open Directory (node=%s) Initialization error: %s" % (node, e))
+            raise
+
+        self.realmName = node
+        self.directory = directory
+        self.node = node
+        self.restrictEnabledRecords = restrictEnabledRecords
+        self.restrictToGroup = restrictToGroup
+        try:
+            UUID(self.restrictToGroup)
+        except:
+            self.restrictToGUID = False
+        else:
+            self.restrictToGUID = True
+        self.restrictedGUIDs = None
+        self.restrictedTimestamp = 0
+        self._records = {}
+        self._delayedCalls = set()
+
+    def __cmp__(self, other):
+        if not isinstance(other, DirectoryRecord):
+            return super(DirectoryRecord, self).__eq__(other)
+
+        for attr in ("directory", "node"):
+            diff = cmp(getattr(self, attr), getattr(other, attr))
+            if diff != 0:
+                return diff
+        return 0
+
+    def __hash__(self):
+        h = hash(self.__class__)
+        for attr in ("directory", "node"):
+            h = (h + hash(getattr(self, attr))) & sys.maxint
+        return h
+
+    def _expandGroupMembership(self, members, nestedGroups, processedGUIDs=None, returnGroups=False):
+
+        if processedGUIDs is None:
+            processedGUIDs = set()
+
+        if isinstance(members, str):
+            members = [members]
+
+        if isinstance(nestedGroups, str):
+            nestedGroups = [nestedGroups]
+
+        for memberGUID in members:
+            if memberGUID not in processedGUIDs:
+                processedGUIDs.add(memberGUID)
+                yield memberGUID
+
+        for groupGUID in nestedGroups:
+            if groupGUID in processedGUIDs:
+                continue
+
+            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+                self.directory,
+                dsattributes.kDS1AttrGeneratedUID,
+                groupGUID,
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
+            ))
+            result = opendirectory.queryRecordsWithAttribute_list(
+                self.directory,
+                dsattributes.kDS1AttrGeneratedUID,
+                groupGUID,
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
+            )
+
+            if not result:
+                self.log_error("Couldn't find group %s when trying to expand nested groups."
+                             % (groupGUID,))
+                continue
+
+            group = result[0][1]
+
+            processedGUIDs.add(groupGUID)
+            if returnGroups:
+                yield groupGUID
+
+            for GUID in self._expandGroupMembership(
+                group.get(dsattributes.kDSNAttrGroupMembers, []),
+                group.get(dsattributes.kDSNAttrNestedGroups, []),
+                processedGUIDs,
+                returnGroups,
+            ):
+                yield GUID
+
+    def _calendarUserAddresses(self, recordType, recordData):
+        """
+        Extract specific attributes from the directory record for use as calendar user address.
+        
+        @param recordData: a C{dict} containing the attributes retrieved from the directory.
+        @return: a C{set} of C{str} for each expanded calendar user address.
+        """
+        # Now get the addresses
+        result = set()
+        
+        # Add each email address as a mailto URI
+        emails = recordData.get(dsattributes.kDSNAttrEMailAddress)
+        if emails is not None:
+            if isinstance(emails, str):
+                emails = [emails]
+            for email in emails:
+                result.add("mailto:%s" % (email.lower(),))
+                
+        return result
+
+    def recordTypes(self):
+        return (
+            self.recordType_users,
+            self.recordType_groups,
+            self.recordType_locations,
+            self.recordType_resources,
+        )
+
+    def groupsForGUID(self, guid):
+        
+        attrs = [
+            dsattributes.kDS1AttrGeneratedUID,
+        ]
+
+        recordType = dsattributes.kDSStdRecordTypeGroups
+
+        guids = set()
+
+        query = dsquery.match(dsattributes.kDSNAttrGroupMembers, guid, dsattributes.eDSExact)
+        try:
+            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+                self.directory,
+                query.attribute,
+                query.value,
+                query.matchType,
+                False,
+                recordType,
+                attrs,
+            ))
+            results = opendirectory.queryRecordsWithAttribute_list(
+                self.directory,
+                query.attribute,
+                query.value,
+                query.matchType,
+                False,
+                recordType,
+                attrs,
+            )
+        except opendirectory.ODError, ex:
+            self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
+            raise
+
+        for (_ignore_recordShortName, value) in results:
+
+            # Now get useful record info.
+            recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
+            if recordGUID:
+                guids.add(recordGUID)
+
+        query = dsquery.match(dsattributes.kDSNAttrNestedGroups, guid, dsattributes.eDSExact)
+        try:
+            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+                self.directory,
+                query.attribute,
+                query.value,
+                query.matchType,
+                False,
+                recordType,
+                attrs,
+            ))
+            results = opendirectory.queryRecordsWithAttribute_list(
+                self.directory,
+                query.attribute,
+                query.value,
+                query.matchType,
+                False,
+                recordType,
+                attrs,
+            )
+        except opendirectory.ODError, ex:
+            self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
+            raise
+
+        for (_ignore_recordShortName, value) in results:
+
+            # Now get useful record info.
+            recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
+            if recordGUID:
+                guids.add(recordGUID)
+
+        return guids
+
+    def proxiesForGUID(self, recordType, guid):
+        
+        # Lookup in index
+        try:
+            # TODO:
+            return ()
+        except KeyError:
+            return ()
+
+    def readOnlyProxiesForGUID(self, recordType, guid):
+        
+        # Lookup in index
+        try:
+            # TODO:
+            return ()
+        except KeyError:
+            return ()
+
+    _ODFields = {
+        'fullName' : {
+            'odField' : dsattributes.kDS1AttrDistinguishedName,
+            'appliesTo' : set([
+                dsattributes.kDSStdRecordTypeUsers,
+                dsattributes.kDSStdRecordTypeGroups,
+                dsattributes.kDSStdRecordTypePlaces,
+                dsattributes.kDSStdRecordTypeResources,
+            ]),
+        },
+        'firstName' : {
+            'odField' : dsattributes.kDS1AttrFirstName,
+            'appliesTo' : set([
+                dsattributes.kDSStdRecordTypeUsers,
+            ]),
+        },
+        'lastName' : {
+            'odField' : dsattributes.kDS1AttrLastName,
+            'appliesTo' : set([
+                dsattributes.kDSStdRecordTypeUsers,
+            ]),
+        },
+        'emailAddresses' : {
+            'odField' : dsattributes.kDSNAttrEMailAddress,
+            'appliesTo' : set([
+                dsattributes.kDSStdRecordTypeUsers,
+                dsattributes.kDSStdRecordTypeGroups,
+            ]),
+        },
+        'recordName' : {
+            'odField' : dsattributes.kDSNAttrRecordName,
+            'appliesTo' : set([
+                dsattributes.kDSStdRecordTypeUsers,
+                dsattributes.kDSStdRecordTypeGroups,
+                dsattributes.kDSStdRecordTypePlaces,
+                dsattributes.kDSStdRecordTypeResources,
+            ]),
+        },
+        'guid' : {
+            'odField' : dsattributes.kDS1AttrGeneratedUID,
+            'appliesTo' : set([
+                dsattributes.kDSStdRecordTypeUsers,
+                dsattributes.kDSStdRecordTypeGroups,
+                dsattributes.kDSStdRecordTypePlaces,
+                dsattributes.kDSStdRecordTypeResources,
+            ]),
+        },
+    }
+
+    _toODRecordTypes = {
+        DirectoryService.recordType_users :
+            dsattributes.kDSStdRecordTypeUsers,
+        DirectoryService.recordType_locations :
+            dsattributes.kDSStdRecordTypePlaces,
+        DirectoryService.recordType_groups :
+            dsattributes.kDSStdRecordTypeGroups,
+        DirectoryService.recordType_resources :
+            dsattributes.kDSStdRecordTypeResources,
+    }
+
+    _fromODRecordTypes = dict([(b, a) for a, b in _toODRecordTypes.iteritems()])
+
+
+    def recordsMatchingFields(self, fields, operand="or", recordType=None):
+
+        # Note that OD applies case-sensitivity globally across the entire
+        # query, not per expression, so the current code uses whatever is
+        # specified in the last field in the fields list
+
+        def collectResults(results):
+            self.log_info("Got back %d records from OD" % (len(results),))
+            for key, val in results.iteritems():
+                self.log_debug("OD result: %s %s" % (key, val))
+                try:
+                    guid = val[dsattributes.kDS1AttrGeneratedUID]
+                    record = self.recordWithGUID(guid)
+                    if record:
+                        yield record
+                except KeyError:
+                    pass
+
+        def multiQuery(directory, queries, attrs, operand):
+            results = {}
+
+            for query, recordTypes in queries.iteritems():
+                if not query:
+                    continue
+
+                expressions = []
+                for ODField, value, caseless, matchType in query:
+                    if matchType == "starts-with":
+                        comparison = dsattributes.eDSStartsWith
+                    elif matchType == "contains":
+                        comparison = dsattributes.eDSContains
+                    else:
+                        comparison = dsattributes.eDSExact
+                    expressions.append(dsquery.match(ODField, value, comparison))
+
+                complexExpression = dsquery.expression(operand, expressions).generate()
+
+                self.log_info("Calling OD: Types %s, Operand %s, Caseless %s, %s" %
+                    (recordTypes, operand, caseless, complexExpression))
+
+                results.update(
+                    opendirectory.queryRecordsWithAttributes(
+                        directory,
+                        complexExpression,
+                        caseless,
+                        recordTypes,
+                        attrs,
+                    )
+                )
+
+            return results
+
+
+        operand = (dsquery.expression.OR if operand == "or"
+            else dsquery.expression.AND)
+
+        if recordType is None:
+            # The client is looking for records in any of the four types
+            recordTypes = set(self._toODRecordTypes.values())
+        else:
+            # The client is after only one recordType
+            recordTypes = [self._toODRecordTypes[recordType]]
+
+        queries = buildQueries(recordTypes, fields, self._ODFields)
+
+        deferred = deferToThread(
+            multiQuery,
+            self.directory,
+            queries,
+            [ dsattributes.kDS1AttrGeneratedUID ],
+            operand
+        )
+        deferred.addCallback(collectResults)
+        return deferred
+
+
+    def queryDirectory(self, recordTypes, indexType, indexKey):
+        
+        attrs = [
+            dsattributes.kDS1AttrGeneratedUID,
+            dsattributes.kDSNAttrRecordName,
+            dsattributes.kDSNAttrAltSecurityIdentities,
+            dsattributes.kDSNAttrRecordType,
+            dsattributes.kDS1AttrDistinguishedName,
+            dsattributes.kDS1AttrFirstName,
+            dsattributes.kDS1AttrLastName,
+            dsattributes.kDSNAttrEMailAddress,
+            dsattributes.kDSNAttrMetaNodeLocation,
+        ]
+
+        origIndexKey = indexKey
+        if indexType == self.INDEX_TYPE_CUA:
+            # The directory doesn't contain CUAs, so we need to convert
+            # the CUA to the appropriate field name and value:
+            queryattr, indexKey = cuAddressConverter(indexKey)
+            # queryattr will be one of:
+            # guid, emailAddresses, or recordName
+            # ...which will need to be mapped to DS
+            queryattr = self._ODFields[queryattr]['odField']
+
+        else:
+            queryattr = {
+                self.INDEX_TYPE_SHORTNAME : dsattributes.kDSNAttrRecordName,
+                self.INDEX_TYPE_GUID      : dsattributes.kDS1AttrGeneratedUID,
+            }.get(indexType)
+            assert queryattr is not None, "Invalid type for record faulting query"
+
+        query = dsquery.match(queryattr, indexKey, dsattributes.eDSExact)
+
+
+        listRecordTypes = []
+        for recordType in recordTypes:
+            if recordType == DirectoryService.recordType_users:
+                listRecordTypes.append(dsattributes.kDSStdRecordTypeUsers)
+    
+            elif recordType == DirectoryService.recordType_groups:
+                listRecordTypes.append(dsattributes.kDSStdRecordTypeGroups)
+                attrs.append(dsattributes.kDSNAttrGroupMembers)
+                attrs.append(dsattributes.kDSNAttrNestedGroups)
+    
+            elif recordType == DirectoryService.recordType_locations:
+                if queryattr != dsattributes.kDSNAttrEMailAddress:
+                    listRecordTypes.append(dsattributes.kDSStdRecordTypePlaces)
+                # MOR: possibly can be removed
+                attrs.append(dsattributes.kDSNAttrResourceInfo)
+            
+            elif recordType == DirectoryService.recordType_resources:
+                if queryattr != dsattributes.kDSNAttrEMailAddress:
+                    listRecordTypes.append(dsattributes.kDSStdRecordTypeResources)
+                # MOR: possibly can be removed
+                attrs.append(dsattributes.kDSNAttrResourceInfo)
+            
+            else:
+                raise UnknownRecordTypeError("Unknown Open Directory record type: %s" % (recordType))
+
+
+        try:
+            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+                self.directory,
+                query.attribute,
+                query.value,
+                query.matchType,
+                False,
+                listRecordTypes,
+                attrs,
+            ))
+            results = opendirectory.queryRecordsWithAttribute_list(
+                self.directory,
+                query.attribute,
+                query.value,
+                query.matchType,
+                False,
+                listRecordTypes,
+                attrs,
+            )
+            self.log_debug("opendirectory.queryRecordsWithAttribute_list matched records: %s" % (len(results),))
+
+        except opendirectory.ODError, ex:
+            if ex.message[1] == -14140 or ex.message[1] == -14200:
+                # Unsupported attribute on record - don't fail
+                return
+            else:
+                self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
+                raise
+
+        def _uniqueTupleFromAttribute(attribute):
+            if attribute:
+                if isinstance(attribute, str):
+                    return (attribute,)
+                else:
+                    s = set()
+                    return tuple([(s.add(x), x)[1] for x in attribute if x not in s])
+            else:
+                return ()
+
+        def _setFromAttribute(attribute, lower=False):
+            if attribute:
+                if isinstance(attribute, str):
+                    return set((attribute.lower() if lower else attribute,))
+                else:
+                    return set([item.lower() if lower else item for item in attribute])
+            else:
+                return ()
+
+        enabledRecords = []
+        disabledRecords = []
+
+        for (recordShortName, value) in results:
+
+            # Now get useful record info.
+            recordGUID           = value.get(dsattributes.kDS1AttrGeneratedUID)
+            recordShortNames     = _uniqueTupleFromAttribute(value.get(dsattributes.kDSNAttrRecordName))
+            recordType           = value.get(dsattributes.kDSNAttrRecordType)
+            if isinstance(recordType, list):
+                recordType = recordType[0]
+            recordAuthIDs        = _setFromAttribute(value.get(dsattributes.kDSNAttrAltSecurityIdentities))
+            recordFullName       = value.get(dsattributes.kDS1AttrDistinguishedName)
+            recordFirstName      = value.get(dsattributes.kDS1AttrFirstName)
+            recordLastName       = value.get(dsattributes.kDS1AttrLastName)
+            recordEmailAddresses = _setFromAttribute(value.get(dsattributes.kDSNAttrEMailAddress), lower=True)
+            recordNodeName       = value.get(dsattributes.kDSNAttrMetaNodeLocation)
+
+            if not recordType:
+                self.log_debug("Record (unknown)%s in node %s has no recordType; ignoring."
+                    % (recordShortName, recordNodeName))
+                continue
+
+            recordType = self._fromODRecordTypes[recordType]
+
+            if not recordGUID:
+                self.log_debug("Record (%s)%s in node %s has no GUID; ignoring."
+                               % (recordType, recordShortName, recordNodeName))
+                continue
+
+            if recordGUID.lower().startswith("ffffeeee-dddd-cccc-bbbb-aaaa"):
+                self.log_debug("Ignoring system record (%s)%s in node %s."
+                               % (recordType, recordShortName, recordNodeName))
+                continue
+
+            # Determine enabled state
+            if recordType == self.recordType_groups:
+                enabledForCalendaring = False
+            else:
+                if (
+                    self.restrictEnabledRecords and
+                    config.Scheduling.iMIP.Username != recordShortName
+                ):
+                    if time.time() - self.restrictedTimestamp > self.cacheTimeout:
+                        attributeToMatch = dsattributes.kDS1AttrGeneratedUID if self.restrictToGUID else dsattributes.kDSNAttrRecordName
+                        valueToMatch = self.restrictToGroup
+                        self.log_debug("Doing restricted group membership check")
+                        self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+                            self.directory,
+                            attributeToMatch,
+                            valueToMatch,
+                            dsattributes.eDSExact,
+                            False,
+                            dsattributes.kDSStdRecordTypeGroups,
+                            [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups,],
+                        ))
+                        results = opendirectory.queryRecordsWithAttribute_list(
+                            self.directory,
+                            attributeToMatch,
+                            valueToMatch,
+                            dsattributes.eDSExact,
+                            False,
+                            dsattributes.kDSStdRecordTypeGroups,
+                            [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups,],
+                        )
+
+                        if len(results) == 1:
+                            members = results[0][1].get(dsattributes.kDSNAttrGroupMembers, [])
+                            nestedGroups = results[0][1].get(dsattributes.kDSNAttrNestedGroups, [])
+                        else:
+                            members = []
+                            nestedGroups = []
+                        self.restrictedGUIDs = set(self._expandGroupMembership(members, nestedGroups, returnGroups=True))
+                        self.log_debug("Got %d restricted group members" % (len(self.restrictedGUIDs),))
+                        self.restrictedTimestamp = time.time()
+
+                    enabledForCalendaring = recordGUID in self.restrictedGUIDs
+                else:
+                    enabledForCalendaring = True
+
+            if enabledForCalendaring:
+                calendarUserAddresses = self._calendarUserAddresses(recordType, value)
+            else:
+                # Some records we want to keep even though they are not enabled for calendaring.
+                # Others we discard.
+                if recordType not in (
+                    self.recordType_users,
+                    self.recordType_groups,
+                ):
+                    self.log_debug(
+                        "Record (%s) %s is not enabled for calendaring"
+                        % (recordType, recordShortName)
+                    )
+                    continue
+
+                self.log_debug(
+                    "Record (%s) %s is not enabled for calendaring but may be used in ACLs"
+                    % (recordType, recordShortName)
+                )
+                calendarUserAddresses = ()
+
+            # Special case for groups, which have members.
+            if recordType == self.recordType_groups:
+                memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
+                if memberGUIDs is None:
+                    memberGUIDs = ()
+                elif type(memberGUIDs) is str:
+                    memberGUIDs = (memberGUIDs,)
+                nestedGUIDs = value.get(dsattributes.kDSNAttrNestedGroups)
+                if nestedGUIDs:
+                    if type(nestedGUIDs) is str:
+                        nestedGUIDs = (nestedGUIDs,)
+                    memberGUIDs += tuple(nestedGUIDs)
+            else:
+                memberGUIDs = ()
+
+            record = OpenDirectoryRecord(
+                service               = self,
+                recordType            = recordType,
+                guid                  = recordGUID,
+                nodeName              = recordNodeName,
+                shortNames            = recordShortNames,
+                authIDs               = recordAuthIDs,
+                fullName              = recordFullName,
+                firstName             = recordFirstName,
+                lastName              = recordLastName,
+                emailAddresses        = recordEmailAddresses,
+                calendarUserAddresses = calendarUserAddresses,
+                enabledForCalendaring = enabledForCalendaring,
+                memberGUIDs           = memberGUIDs,
+            )
+            if enabledForCalendaring:
+                enabledRecords.append(record)
+            else:
+                disabledRecords.append(record)
+
+        record = None
+        if len(enabledRecords) == 1:
+            record = enabledRecords[0]
+        elif len(enabledRecords) == 0 and len(disabledRecords) == 1:
+            record = disabledRecords[0]
+
+        if record:
+            self.log_debug("Storing (%s %s) %s in internal cache" % (indexType, origIndexKey, record))
+            self.recordCacheForType(recordType).addRecord(record, indexType, origIndexKey)
+
+    def _parseResourceInfo(self, plist, guid, recordType, shortname):
+        """
+        Parse OD ResourceInfo attribute and extract information that the server needs.
+
+        @param plist: the plist that is the attribute value.
+        @type plist: str
+        @param guid: the directory GUID of the record being parsed.
+        @type guid: str
+        @param shortname: the record shortname of the record being parsed.
+        @type shortname: str
+        @return: a C{tuple} of C{bool} for auto-accept, C{str} for proxy GUID, C{str} for read-only proxy GUID.
+        """
+        try:
+            plist = readPlistFromString(plist)
+            wpframework = plist.get("com.apple.WhitePagesFramework", {})
+            autoaccept = wpframework.get("AutoAcceptsInvitation", False)
+            proxy = wpframework.get("CalendaringDelegate", None)
+            read_only_proxy = wpframework.get("ReadOnlyCalendaringDelegate", None)
+        except (ExpatError, AttributeError), e:
+            self.log_error(
+                "Failed to parse ResourceInfo attribute of record (%s)%s (guid=%s): %s\n%s" %
+                (recordType, shortname, guid, e, plist,)
+            )
+            raise ValueError("Invalid ResourceInfo")
+
+        return (autoaccept, proxy, read_only_proxy,)
+
+    def getResourceInfo(self):
+        """
+        Resource information including proxy assignments for resource and
+        locations, as well as auto-schedule settings, used to live in the
+        directory.  This method fetches old resource info for migration
+        purposes.
+        """
+        attrs = [
+            dsattributes.kDS1AttrGeneratedUID,
+            dsattributes.kDSNAttrResourceInfo,
+        ]
+
+        for recordType in (dsattributes.kDSStdRecordTypePlaces, dsattributes.kDSStdRecordTypeResources):
+            try:
+                self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
+                    self.directory,
+                    recordType,
+                    attrs,
+                ))
+                results = opendirectory.listAllRecordsWithAttributes_list(
+                    self.directory,
+                    recordType,
+                    attrs,
+                )
+            except opendirectory.ODError, ex:
+                self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
+                raise
+
+            for (recordShortName, value) in results:
+                recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
+                resourceInfo = value.get(dsattributes.kDSNAttrResourceInfo)
+                if resourceInfo is not None:
+                    try:
+                        autoSchedule, proxy, readOnlyProxy = self._parseResourceInfo(resourceInfo,
+                            recordGUID, recordType, recordShortName)
+                    except ValueError:
+                        continue
+                    yield recordGUID, autoSchedule, proxy, readOnlyProxy
+
+
+def buildQueries(recordTypes, fields, mapping):
+    """
+    Determine how many queries need to be performed in order to work around opendirectory
+    quirks, where searching on fields that don't apply to a given recordType returns incorrect
+    results (either none, or all records).
+    """
+
+    fieldLists = {}
+    for recordType in recordTypes:
+        fieldLists[recordType] = []
+        for field, value, caseless, matchType in fields:
+            if field in mapping:
+                if recordType in mapping[field]['appliesTo']:
+                    ODField = mapping[field]['odField']
+                    fieldLists[recordType].append((ODField, value, caseless, matchType))
+
+    queries = {}
+    for recordType, fieldList in fieldLists.iteritems():
+        key = tuple(fieldList)
+        queries.setdefault(key, []).append(recordType)
+    return queries
+
+
+class OpenDirectoryRecord(CachingDirectoryRecord):
+    """
+    Open Directory implementation of L{IDirectoryRecord}.
+    """
+    def __init__(
+        self, service, recordType, guid, nodeName, shortNames, authIDs,
+        fullName, firstName, lastName, emailAddresses,
+        calendarUserAddresses,
+        enabledForCalendaring,
+        memberGUIDs,
+    ):
+        super(OpenDirectoryRecord, self).__init__(
+            service               = service,
+            recordType            = recordType,
+            guid                  = guid,
+            shortNames            = shortNames,
+            authIDs               = authIDs,
+            fullName              = fullName,
+            firstName             = firstName,
+            lastName              = lastName,
+            emailAddresses        = emailAddresses,
+            calendarUserAddresses = calendarUserAddresses,
+            enabledForCalendaring = enabledForCalendaring,
+        )
+        self.nodeName = nodeName
+        self._memberGUIDs = tuple(memberGUIDs)
+        
+        self._groupMembershipGUIDs = None
+
+    def __repr__(self):
+        if self.service.realmName == self.nodeName:
+            location = self.nodeName
+        else:
+            location = "%s->%s" % (self.service.realmName, self.nodeName)
+
+        return "<%s[%s@%s(%s)] %s(%s) %r>" % (
+            self.__class__.__name__,
+            self.recordType,
+            self.service.guid,
+            location,
+            self.guid,
+            ",".join(self.shortNames),
+            self.fullName
+        )
+
+    def members(self):
+        if self.recordType != self.service.recordType_groups:
+            return
+
+        for guid in self._memberGUIDs:
+            userRecord = self.service.recordWithGUID(guid)
+            if userRecord is not None:
+                yield userRecord
+
+    def groups(self):
+        if self._groupMembershipGUIDs is None:
+            self._groupMembershipGUIDs = self.service.groupsForGUID(self.guid)
+
+        for guid in self._groupMembershipGUIDs:
+            record = self.service.recordWithGUID(guid)
+            if record:
+                yield record
+
+    def verifyCredentials(self, credentials):
+        if isinstance(credentials, UsernamePassword):
+            # Check cached password
+            try:
+                if credentials.password == self.password:
+                    return True
+            except AttributeError:
+                pass
+
+            # Check with directory services
+            try:
+                if opendirectory.authenticateUserBasic(self.service.directory, self.nodeName, self.shortNames[0], credentials.password):
+                    # Cache the password to avoid future DS queries
+                    self.password = credentials.password
+                    return True
+            except opendirectory.ODError, e:
+                self.log_error("Open Directory (node=%s) error while performing basic authentication for user %s: %s"
+                            % (self.service.realmName, self.shortNames[0], e))
+
+            return False
+
+        elif isinstance(credentials, DigestedCredentials):
+            #
+            # We need a special format for the "challenge" and "response" strings passed into open directory, as it is
+            # picky about exactly what it receives.
+            #
+            try:
+                challenge = 'Digest realm="%(realm)s", nonce="%(nonce)s", algorithm=%(algorithm)s' % credentials.fields
+                response = (
+                    'Digest username="%(username)s", '
+                    'realm="%(realm)s", '
+                    'nonce="%(nonce)s", '
+                    'uri="%(uri)s", '
+                    'response="%(response)s",'
+                    'algorithm=%(algorithm)s'
+                ) % credentials.fields
+            except KeyError, e:
+                self.log_error(
+                    "Open Directory (node=%s) error while performing digest authentication for user %s: "
+                    "missing digest response field: %s in: %s"
+                    % (self.service.realmName, self.shortNames[0], e, credentials.fields)
+                )
+                return False
+
+            try:
+                if self.digestcache[credentials.fields["uri"]] == response:
+                    return True
+            except (AttributeError, KeyError):
+                pass
+
+            try:
+                if opendirectory.authenticateUserDigest(
+                    self.service.directory,
+                    self.nodeName,
+                    self.shortNames[0],
+                    challenge,
+                    response,
+                    credentials.method
+                ):
+                    try:
+                        cache = self.digestcache
+                    except AttributeError:
+                        cache = self.digestcache = {}
+
+                    cache[credentials.fields["uri"]] = response
+
+                    return True
+                else:
+                    self.log_debug(
+"""Open Directory digest authentication failed with:
+    Nodename:  %s
+    Username:  %s
+    Challenge: %s
+    Response:  %s
+    Method:    %s
+""" % (self.nodeName, self.shortNames[0], challenge, response, credentials.method))
+
+            except opendirectory.ODError, e:
+                self.log_error(
+                    "Open Directory (node=%s) error while performing digest authentication for user %s: %s"
+                    % (self.service.realmName, self.shortNames[0], e)
+                )
+                return False
+
+            return False
+
+        return super(OpenDirectoryRecord, self).verifyCredentials(credentials)
+
+class OpenDirectoryInitError(DirectoryError):
+    """
+    OpenDirectory initialization error.
+    """

Deleted: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/cachingappleopendirectory.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/cachingappleopendirectory.py	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/cachingappleopendirectory.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -1,932 +0,0 @@
-##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Apple Open Directory directory service implementation.
-"""
-
-__all__ = [
-    "OpenDirectoryService",
-    "OpenDirectoryInitError",
-]
-
-import sys
-import time
-from uuid import UUID
-
-from twext.python.plistlib import readPlistFromString
-
-from xml.parsers.expat import ExpatError
-
-import opendirectory
-import dsattributes
-import dsquery
-
-from twisted.internet.threads import deferToThread
-from twisted.cred.credentials import UsernamePassword
-from twisted.web2.auth.digest import DigestedCredentials
-from twistedcaldav.config import config
-
-from twistedcaldav.directory.cachingdirectory import CachingDirectoryService,\
-    CachingDirectoryRecord
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.directory.directory import DirectoryError, UnknownRecordTypeError
-from twistedcaldav.directory.principal import cuAddressConverter
-
-class OpenDirectoryService(CachingDirectoryService):
-    """
-    Open Directory implementation of L{IDirectoryService}.
-    """
-    baseGUID = "891F8321-ED02-424C-BA72-89C32F215C1E"
-
-    def __repr__(self):
-        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.node)
-
-    def __init__(
-        self,
-        node="/Search",
-        restrictEnabledRecords=False,
-        restrictToGroup="",
-        dosetup=True,
-        cacheTimeout=30
-    ):
-        """
-        @param node: an OpenDirectory node name to bind to.
-        @param restrictEnabledRecords: C{True} if a group in the directory is to be used to determine
-            which calendar users are enabled.
-        @param restrictToGroup: C{str} guid or name of group used to restrict enabled users.
-        @param dosetup: if C{True} then the directory records are initialized,
-                        if C{False} they are not.
-                        This should only be set to C{False} when doing unit tests.
-        @param cacheTimeout: C{int} number of minutes before cache is invalidated.
-        """
-
-        super(OpenDirectoryService, self).__init__(cacheTimeout)
-
-        try:
-            directory = opendirectory.odInit(node)
-        except opendirectory.ODError, e:
-            self.log_error("Open Directory (node=%s) Initialization error: %s" % (node, e))
-            raise
-
-        self.realmName = node
-        self.directory = directory
-        self.node = node
-        self.restrictEnabledRecords = restrictEnabledRecords
-        self.restrictToGroup = restrictToGroup
-        try:
-            UUID(self.restrictToGroup)
-        except:
-            self.restrictToGUID = False
-        else:
-            self.restrictToGUID = True
-        self.restrictedGUIDs = None
-        self.restrictedTimestamp = 0
-        self._records = {}
-        self._delayedCalls = set()
-
-    def __cmp__(self, other):
-        if not isinstance(other, DirectoryRecord):
-            return super(DirectoryRecord, self).__eq__(other)
-
-        for attr in ("directory", "node"):
-            diff = cmp(getattr(self, attr), getattr(other, attr))
-            if diff != 0:
-                return diff
-        return 0
-
-    def __hash__(self):
-        h = hash(self.__class__)
-        for attr in ("directory", "node"):
-            h = (h + hash(getattr(self, attr))) & sys.maxint
-        return h
-
-    def _expandGroupMembership(self, members, nestedGroups, processedGUIDs=None, returnGroups=False):
-
-        if processedGUIDs is None:
-            processedGUIDs = set()
-
-        if isinstance(members, str):
-            members = [members]
-
-        if isinstance(nestedGroups, str):
-            nestedGroups = [nestedGroups]
-
-        for memberGUID in members:
-            if memberGUID not in processedGUIDs:
-                processedGUIDs.add(memberGUID)
-                yield memberGUID
-
-        for groupGUID in nestedGroups:
-            if groupGUID in processedGUIDs:
-                continue
-
-            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                self.directory,
-                dsattributes.kDS1AttrGeneratedUID,
-                groupGUID,
-                dsattributes.eDSExact,
-                False,
-                dsattributes.kDSStdRecordTypeGroups,
-                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
-            ))
-            result = opendirectory.queryRecordsWithAttribute_list(
-                self.directory,
-                dsattributes.kDS1AttrGeneratedUID,
-                groupGUID,
-                dsattributes.eDSExact,
-                False,
-                dsattributes.kDSStdRecordTypeGroups,
-                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
-            )
-
-            if not result:
-                self.log_error("Couldn't find group %s when trying to expand nested groups."
-                             % (groupGUID,))
-                continue
-
-            group = result[0][1]
-
-            processedGUIDs.add(groupGUID)
-            if returnGroups:
-                yield groupGUID
-
-            for GUID in self._expandGroupMembership(
-                group.get(dsattributes.kDSNAttrGroupMembers, []),
-                group.get(dsattributes.kDSNAttrNestedGroups, []),
-                processedGUIDs,
-                returnGroups,
-            ):
-                yield GUID
-
-    def _calendarUserAddresses(self, recordType, recordData):
-        """
-        Extract specific attributes from the directory record for use as calendar user address.
-        
-        @param recordData: a C{dict} containing the attributes retrieved from the directory.
-        @return: a C{set} of C{str} for each expanded calendar user address.
-        """
-        # Now get the addresses
-        result = set()
-        
-        # Add each email address as a mailto URI
-        emails = recordData.get(dsattributes.kDSNAttrEMailAddress)
-        if emails is not None:
-            if isinstance(emails, str):
-                emails = [emails]
-            for email in emails:
-                result.add("mailto:%s" % (email.lower(),))
-                
-        return result
-
-    def recordTypes(self):
-        return (
-            self.recordType_users,
-            self.recordType_groups,
-            self.recordType_locations,
-            self.recordType_resources,
-        )
-
-    def groupsForGUID(self, guid):
-        
-        attrs = [
-            dsattributes.kDS1AttrGeneratedUID,
-        ]
-
-        recordType = dsattributes.kDSStdRecordTypeGroups
-
-        guids = set()
-
-        query = dsquery.match(dsattributes.kDSNAttrGroupMembers, guid, dsattributes.eDSExact)
-        try:
-            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                self.directory,
-                query.attribute,
-                query.value,
-                query.matchType,
-                False,
-                recordType,
-                attrs,
-            ))
-            results = opendirectory.queryRecordsWithAttribute_list(
-                self.directory,
-                query.attribute,
-                query.value,
-                query.matchType,
-                False,
-                recordType,
-                attrs,
-            )
-        except opendirectory.ODError, ex:
-            self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
-            raise
-
-        for (_ignore_recordShortName, value) in results:
-
-            # Now get useful record info.
-            recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
-            if recordGUID:
-                guids.add(recordGUID)
-
-        query = dsquery.match(dsattributes.kDSNAttrNestedGroups, guid, dsattributes.eDSExact)
-        try:
-            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                self.directory,
-                query.attribute,
-                query.value,
-                query.matchType,
-                False,
-                recordType,
-                attrs,
-            ))
-            results = opendirectory.queryRecordsWithAttribute_list(
-                self.directory,
-                query.attribute,
-                query.value,
-                query.matchType,
-                False,
-                recordType,
-                attrs,
-            )
-        except opendirectory.ODError, ex:
-            self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
-            raise
-
-        for (_ignore_recordShortName, value) in results:
-
-            # Now get useful record info.
-            recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
-            if recordGUID:
-                guids.add(recordGUID)
-
-        return guids
-
-    def proxiesForGUID(self, recordType, guid):
-        
-        # Lookup in index
-        try:
-            # TODO:
-            return ()
-        except KeyError:
-            return ()
-
-    def readOnlyProxiesForGUID(self, recordType, guid):
-        
-        # Lookup in index
-        try:
-            # TODO:
-            return ()
-        except KeyError:
-            return ()
-
-    _ODFields = {
-        'fullName' : {
-            'odField' : dsattributes.kDS1AttrDistinguishedName,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-                dsattributes.kDSStdRecordTypeGroups,
-                dsattributes.kDSStdRecordTypePlaces,
-                dsattributes.kDSStdRecordTypeResources,
-            ]),
-        },
-        'firstName' : {
-            'odField' : dsattributes.kDS1AttrFirstName,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-            ]),
-        },
-        'lastName' : {
-            'odField' : dsattributes.kDS1AttrLastName,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-            ]),
-        },
-        'emailAddresses' : {
-            'odField' : dsattributes.kDSNAttrEMailAddress,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-                dsattributes.kDSStdRecordTypeGroups,
-            ]),
-        },
-        'recordName' : {
-            'odField' : dsattributes.kDSNAttrRecordName,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-                dsattributes.kDSStdRecordTypeGroups,
-                dsattributes.kDSStdRecordTypePlaces,
-                dsattributes.kDSStdRecordTypeResources,
-            ]),
-        },
-        'guid' : {
-            'odField' : dsattributes.kDS1AttrGeneratedUID,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-                dsattributes.kDSStdRecordTypeGroups,
-                dsattributes.kDSStdRecordTypePlaces,
-                dsattributes.kDSStdRecordTypeResources,
-            ]),
-        },
-    }
-
-    _toODRecordTypes = {
-        DirectoryService.recordType_users :
-            dsattributes.kDSStdRecordTypeUsers,
-        DirectoryService.recordType_locations :
-            dsattributes.kDSStdRecordTypePlaces,
-        DirectoryService.recordType_groups :
-            dsattributes.kDSStdRecordTypeGroups,
-        DirectoryService.recordType_resources :
-            dsattributes.kDSStdRecordTypeResources,
-    }
-
-    _fromODRecordTypes = dict([(b, a) for a, b in _toODRecordTypes.iteritems()])
-
-
-    def recordsMatchingFields(self, fields, operand="or", recordType=None):
-
-        # Note that OD applies case-sensitivity globally across the entire
-        # query, not per expression, so the current code uses whatever is
-        # specified in the last field in the fields list
-
-        def collectResults(results):
-            self.log_info("Got back %d records from OD" % (len(results),))
-            for key, val in results.iteritems():
-                self.log_debug("OD result: %s %s" % (key, val))
-                try:
-                    guid = val[dsattributes.kDS1AttrGeneratedUID]
-                    record = self.recordWithGUID(guid)
-                    if record:
-                        yield record
-                except KeyError:
-                    pass
-
-        def multiQuery(directory, queries, attrs, operand):
-            results = {}
-
-            for query, recordTypes in queries.iteritems():
-                if not query:
-                    continue
-
-                expressions = []
-                for ODField, value, caseless, matchType in query:
-                    if matchType == "starts-with":
-                        comparison = dsattributes.eDSStartsWith
-                    elif matchType == "contains":
-                        comparison = dsattributes.eDSContains
-                    else:
-                        comparison = dsattributes.eDSExact
-                    expressions.append(dsquery.match(ODField, value, comparison))
-
-                complexExpression = dsquery.expression(operand, expressions).generate()
-
-                self.log_info("Calling OD: Types %s, Operand %s, Caseless %s, %s" %
-                    (recordTypes, operand, caseless, complexExpression))
-
-                results.update(
-                    opendirectory.queryRecordsWithAttributes(
-                        directory,
-                        complexExpression,
-                        caseless,
-                        recordTypes,
-                        attrs,
-                    )
-                )
-
-            return results
-
-
-        operand = (dsquery.expression.OR if operand == "or"
-            else dsquery.expression.AND)
-
-        if recordType is None:
-            # The client is looking for records in any of the four types
-            recordTypes = set(self._toODRecordTypes.values())
-        else:
-            # The client is after only one recordType
-            recordTypes = [self._toODRecordTypes[recordType]]
-
-        queries = buildQueries(recordTypes, fields, self._ODFields)
-
-        deferred = deferToThread(
-            multiQuery,
-            self.directory,
-            queries,
-            [ dsattributes.kDS1AttrGeneratedUID ],
-            operand
-        )
-        deferred.addCallback(collectResults)
-        return deferred
-
-
-    def queryDirectory(self, recordTypes, indexType, indexKey):
-        
-        attrs = [
-            dsattributes.kDS1AttrGeneratedUID,
-            dsattributes.kDSNAttrRecordName,
-            dsattributes.kDSNAttrAltSecurityIdentities,
-            dsattributes.kDSNAttrRecordType,
-            dsattributes.kDS1AttrDistinguishedName,
-            dsattributes.kDS1AttrFirstName,
-            dsattributes.kDS1AttrLastName,
-            dsattributes.kDSNAttrEMailAddress,
-            dsattributes.kDSNAttrMetaNodeLocation,
-        ]
-
-        origIndexKey = indexKey
-        if indexType == self.INDEX_TYPE_CUA:
-            # The directory doesn't contain CUAs, so we need to convert
-            # the CUA to the appropriate field name and value:
-            queryattr, indexKey = cuAddressConverter(indexKey)
-            # queryattr will be one of:
-            # guid, emailAddresses, or recordName
-            # ...which will need to be mapped to DS
-            queryattr = self._ODFields[queryattr]['odField']
-
-        else:
-            queryattr = {
-                self.INDEX_TYPE_SHORTNAME : dsattributes.kDSNAttrRecordName,
-                self.INDEX_TYPE_GUID      : dsattributes.kDS1AttrGeneratedUID,
-            }.get(indexType)
-            assert queryattr is not None, "Invalid type for record faulting query"
-
-        query = dsquery.match(queryattr, indexKey, dsattributes.eDSExact)
-
-
-        listRecordTypes = []
-        for recordType in recordTypes:
-            if recordType == DirectoryService.recordType_users:
-                listRecordTypes.append(dsattributes.kDSStdRecordTypeUsers)
-    
-            elif recordType == DirectoryService.recordType_groups:
-                listRecordTypes.append(dsattributes.kDSStdRecordTypeGroups)
-                attrs.append(dsattributes.kDSNAttrGroupMembers)
-                attrs.append(dsattributes.kDSNAttrNestedGroups)
-    
-            elif recordType == DirectoryService.recordType_locations:
-                if queryattr != dsattributes.kDSNAttrEMailAddress:
-                    listRecordTypes.append(dsattributes.kDSStdRecordTypePlaces)
-                # MOR: possibly can be removed
-                attrs.append(dsattributes.kDSNAttrResourceInfo)
-            
-            elif recordType == DirectoryService.recordType_resources:
-                if queryattr != dsattributes.kDSNAttrEMailAddress:
-                    listRecordTypes.append(dsattributes.kDSStdRecordTypeResources)
-                # MOR: possibly can be removed
-                attrs.append(dsattributes.kDSNAttrResourceInfo)
-            
-            else:
-                raise UnknownRecordTypeError("Unknown Open Directory record type: %s" % (recordType))
-
-
-        try:
-            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                self.directory,
-                query.attribute,
-                query.value,
-                query.matchType,
-                False,
-                listRecordTypes,
-                attrs,
-            ))
-            results = opendirectory.queryRecordsWithAttribute_list(
-                self.directory,
-                query.attribute,
-                query.value,
-                query.matchType,
-                False,
-                listRecordTypes,
-                attrs,
-            )
-            self.log_debug("opendirectory.queryRecordsWithAttribute_list matched records: %s" % (len(results),))
-
-        except opendirectory.ODError, ex:
-            if ex.message[1] == -14140 or ex.message[1] == -14200:
-                # Unsupported attribute on record - don't fail
-                return
-            else:
-                self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
-                raise
-
-        def _uniqueTupleFromAttribute(attribute):
-            if attribute:
-                if isinstance(attribute, str):
-                    return (attribute,)
-                else:
-                    s = set()
-                    return tuple([(s.add(x), x)[1] for x in attribute if x not in s])
-            else:
-                return ()
-
-        def _setFromAttribute(attribute, lower=False):
-            if attribute:
-                if isinstance(attribute, str):
-                    return set((attribute.lower() if lower else attribute,))
-                else:
-                    return set([item.lower() if lower else item for item in attribute])
-            else:
-                return ()
-
-        enabledRecords = []
-        disabledRecords = []
-
-        for (recordShortName, value) in results:
-
-            # Now get useful record info.
-            recordGUID           = value.get(dsattributes.kDS1AttrGeneratedUID)
-            recordShortNames     = _uniqueTupleFromAttribute(value.get(dsattributes.kDSNAttrRecordName))
-            recordType           = value.get(dsattributes.kDSNAttrRecordType)
-            if isinstance(recordType, list):
-                recordType = recordType[0]
-            recordAuthIDs        = _setFromAttribute(value.get(dsattributes.kDSNAttrAltSecurityIdentities))
-            recordFullName       = value.get(dsattributes.kDS1AttrDistinguishedName)
-            recordFirstName      = value.get(dsattributes.kDS1AttrFirstName)
-            recordLastName       = value.get(dsattributes.kDS1AttrLastName)
-            recordEmailAddresses = _setFromAttribute(value.get(dsattributes.kDSNAttrEMailAddress), lower=True)
-            recordNodeName       = value.get(dsattributes.kDSNAttrMetaNodeLocation)
-
-            if not recordType:
-                self.log_debug("Record (unknown)%s in node %s has no recordType; ignoring."
-                    % (recordShortName, recordNodeName))
-                continue
-
-            recordType = self._fromODRecordTypes[recordType]
-
-            if not recordGUID:
-                self.log_debug("Record (%s)%s in node %s has no GUID; ignoring."
-                               % (recordType, recordShortName, recordNodeName))
-                continue
-
-            if recordGUID.lower().startswith("ffffeeee-dddd-cccc-bbbb-aaaa"):
-                self.log_debug("Ignoring system record (%s)%s in node %s."
-                               % (recordType, recordShortName, recordNodeName))
-                continue
-
-            # Determine enabled state
-            if recordType == self.recordType_groups:
-                enabledForCalendaring = False
-            else:
-                if (
-                    self.restrictEnabledRecords and
-                    config.Scheduling.iMIP.Username != recordShortName
-                ):
-                    if time.time() - self.restrictedTimestamp > self.cacheTimeout:
-                        attributeToMatch = dsattributes.kDS1AttrGeneratedUID if self.restrictToGUID else dsattributes.kDSNAttrRecordName
-                        valueToMatch = self.restrictToGroup
-                        self.log_debug("Doing restricted group membership check")
-                        self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                            self.directory,
-                            attributeToMatch,
-                            valueToMatch,
-                            dsattributes.eDSExact,
-                            False,
-                            dsattributes.kDSStdRecordTypeGroups,
-                            [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups,],
-                        ))
-                        results = opendirectory.queryRecordsWithAttribute_list(
-                            self.directory,
-                            attributeToMatch,
-                            valueToMatch,
-                            dsattributes.eDSExact,
-                            False,
-                            dsattributes.kDSStdRecordTypeGroups,
-                            [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups,],
-                        )
-
-                        if len(results) == 1:
-                            members = results[0][1].get(dsattributes.kDSNAttrGroupMembers, [])
-                            nestedGroups = results[0][1].get(dsattributes.kDSNAttrNestedGroups, [])
-                        else:
-                            members = []
-                            nestedGroups = []
-                        self.restrictedGUIDs = set(self._expandGroupMembership(members, nestedGroups, returnGroups=True))
-                        self.log_debug("Got %d restricted group members" % (len(self.restrictedGUIDs),))
-                        self.restrictedTimestamp = time.time()
-
-                    enabledForCalendaring = recordGUID in self.restrictedGUIDs
-                else:
-                    enabledForCalendaring = True
-
-            if enabledForCalendaring:
-                calendarUserAddresses = self._calendarUserAddresses(recordType, value)
-            else:
-                # Some records we want to keep even though they are not enabled for calendaring.
-                # Others we discard.
-                if recordType not in (
-                    self.recordType_users,
-                    self.recordType_groups,
-                ):
-                    self.log_debug(
-                        "Record (%s) %s is not enabled for calendaring"
-                        % (recordType, recordShortName)
-                    )
-                    continue
-
-                self.log_debug(
-                    "Record (%s) %s is not enabled for calendaring but may be used in ACLs"
-                    % (recordType, recordShortName)
-                )
-                calendarUserAddresses = ()
-
-            # Special case for groups, which have members.
-            if recordType == self.recordType_groups:
-                memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
-                if memberGUIDs is None:
-                    memberGUIDs = ()
-                elif type(memberGUIDs) is str:
-                    memberGUIDs = (memberGUIDs,)
-                nestedGUIDs = value.get(dsattributes.kDSNAttrNestedGroups)
-                if nestedGUIDs:
-                    if type(nestedGUIDs) is str:
-                        nestedGUIDs = (nestedGUIDs,)
-                    memberGUIDs += tuple(nestedGUIDs)
-            else:
-                memberGUIDs = ()
-
-            record = OpenDirectoryRecord(
-                service               = self,
-                recordType            = recordType,
-                guid                  = recordGUID,
-                nodeName              = recordNodeName,
-                shortNames            = recordShortNames,
-                authIDs               = recordAuthIDs,
-                fullName              = recordFullName,
-                firstName             = recordFirstName,
-                lastName              = recordLastName,
-                emailAddresses        = recordEmailAddresses,
-                calendarUserAddresses = calendarUserAddresses,
-                enabledForCalendaring = enabledForCalendaring,
-                memberGUIDs           = memberGUIDs,
-            )
-            if enabledForCalendaring:
-                enabledRecords.append(record)
-            else:
-                disabledRecords.append(record)
-
-        record = None
-        if len(enabledRecords) == 1:
-            record = enabledRecords[0]
-        elif len(enabledRecords) == 0 and len(disabledRecords) == 1:
-            record = disabledRecords[0]
-
-        if record:
-            self.log_debug("Storing (%s %s) %s in internal cache" % (indexType, origIndexKey, record))
-            self.recordCacheForType(recordType).addRecord(record, indexType, origIndexKey)
-
-    def _parseResourceInfo(self, plist, guid, recordType, shortname):
-        """
-        Parse OD ResourceInfo attribute and extract information that the server needs.
-
-        @param plist: the plist that is the attribute value.
-        @type plist: str
-        @param guid: the directory GUID of the record being parsed.
-        @type guid: str
-        @param shortname: the record shortname of the record being parsed.
-        @type shortname: str
-        @return: a C{tuple} of C{bool} for auto-accept, C{str} for proxy GUID, C{str} for read-only proxy GUID.
-        """
-        try:
-            plist = readPlistFromString(plist)
-            wpframework = plist.get("com.apple.WhitePagesFramework", {})
-            autoaccept = wpframework.get("AutoAcceptsInvitation", False)
-            proxy = wpframework.get("CalendaringDelegate", None)
-            read_only_proxy = wpframework.get("ReadOnlyCalendaringDelegate", None)
-        except (ExpatError, AttributeError), e:
-            self.log_error(
-                "Failed to parse ResourceInfo attribute of record (%s)%s (guid=%s): %s\n%s" %
-                (recordType, shortname, guid, e, plist,)
-            )
-            raise ValueError("Invalid ResourceInfo")
-
-        return (autoaccept, proxy, read_only_proxy,)
-
-    def getResourceInfo(self):
-        """
-        Resource information including proxy assignments for resource and
-        locations, as well as auto-schedule settings, used to live in the
-        directory.  This method fetches old resource info for migration
-        purposes.
-        """
-        attrs = [
-            dsattributes.kDS1AttrGeneratedUID,
-            dsattributes.kDSNAttrResourceInfo,
-        ]
-
-        for recordType in (dsattributes.kDSStdRecordTypePlaces, dsattributes.kDSStdRecordTypeResources):
-            try:
-                self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
-                    self.directory,
-                    recordType,
-                    attrs,
-                ))
-                results = opendirectory.listAllRecordsWithAttributes_list(
-                    self.directory,
-                    recordType,
-                    attrs,
-                )
-            except opendirectory.ODError, ex:
-                self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
-                raise
-
-            for (recordShortName, value) in results:
-                recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
-                resourceInfo = value.get(dsattributes.kDSNAttrResourceInfo)
-                if resourceInfo is not None:
-                    try:
-                        autoSchedule, proxy, readOnlyProxy = self._parseResourceInfo(resourceInfo,
-                            recordGUID, recordType, recordShortName)
-                    except ValueError:
-                        continue
-                    yield recordGUID, autoSchedule, proxy, readOnlyProxy
-
-
-def buildQueries(recordTypes, fields, mapping):
-    """
-    Determine how many queries need to be performed in order to work around opendirectory
-    quirks, where searching on fields that don't apply to a given recordType returns incorrect
-    results (either none, or all records).
-    """
-
-    fieldLists = {}
-    for recordType in recordTypes:
-        fieldLists[recordType] = []
-        for field, value, caseless, matchType in fields:
-            if field in mapping:
-                if recordType in mapping[field]['appliesTo']:
-                    ODField = mapping[field]['odField']
-                    fieldLists[recordType].append((ODField, value, caseless, matchType))
-
-    queries = {}
-    for recordType, fieldList in fieldLists.iteritems():
-        key = tuple(fieldList)
-        queries.setdefault(key, []).append(recordType)
-    return queries
-
-
-class OpenDirectoryRecord(CachingDirectoryRecord):
-    """
-    Open Directory implementation of L{IDirectoryRecord}.
-    """
-    def __init__(
-        self, service, recordType, guid, nodeName, shortNames, authIDs,
-        fullName, firstName, lastName, emailAddresses,
-        calendarUserAddresses,
-        enabledForCalendaring,
-        memberGUIDs,
-    ):
-        super(OpenDirectoryRecord, self).__init__(
-            service               = service,
-            recordType            = recordType,
-            guid                  = guid,
-            shortNames            = shortNames,
-            authIDs               = authIDs,
-            fullName              = fullName,
-            firstName             = firstName,
-            lastName              = lastName,
-            emailAddresses        = emailAddresses,
-            calendarUserAddresses = calendarUserAddresses,
-            enabledForCalendaring = enabledForCalendaring,
-        )
-        self.nodeName = nodeName
-        self._memberGUIDs = tuple(memberGUIDs)
-        
-        self._groupMembershipGUIDs = None
-
-    def __repr__(self):
-        if self.service.realmName == self.nodeName:
-            location = self.nodeName
-        else:
-            location = "%s->%s" % (self.service.realmName, self.nodeName)
-
-        return "<%s[%s@%s(%s)] %s(%s) %r>" % (
-            self.__class__.__name__,
-            self.recordType,
-            self.service.guid,
-            location,
-            self.guid,
-            ",".join(self.shortNames),
-            self.fullName
-        )
-
-    def members(self):
-        if self.recordType != self.service.recordType_groups:
-            return
-
-        for guid in self._memberGUIDs:
-            userRecord = self.service.recordWithGUID(guid)
-            if userRecord is not None:
-                yield userRecord
-
-    def groups(self):
-        if self._groupMembershipGUIDs is None:
-            self._groupMembershipGUIDs = self.service.groupsForGUID(self.guid)
-
-        for guid in self._groupMembershipGUIDs:
-            record = self.service.recordWithGUID(guid)
-            if record:
-                yield record
-
-    def verifyCredentials(self, credentials):
-        if isinstance(credentials, UsernamePassword):
-            # Check cached password
-            try:
-                if credentials.password == self.password:
-                    return True
-            except AttributeError:
-                pass
-
-            # Check with directory services
-            try:
-                if opendirectory.authenticateUserBasic(self.service.directory, self.nodeName, self.shortNames[0], credentials.password):
-                    # Cache the password to avoid future DS queries
-                    self.password = credentials.password
-                    return True
-            except opendirectory.ODError, e:
-                self.log_error("Open Directory (node=%s) error while performing basic authentication for user %s: %s"
-                            % (self.service.realmName, self.shortNames[0], e))
-
-            return False
-
-        elif isinstance(credentials, DigestedCredentials):
-            #
-            # We need a special format for the "challenge" and "response" strings passed into open directory, as it is
-            # picky about exactly what it receives.
-            #
-            try:
-                challenge = 'Digest realm="%(realm)s", nonce="%(nonce)s", algorithm=%(algorithm)s' % credentials.fields
-                response = (
-                    'Digest username="%(username)s", '
-                    'realm="%(realm)s", '
-                    'nonce="%(nonce)s", '
-                    'uri="%(uri)s", '
-                    'response="%(response)s",'
-                    'algorithm=%(algorithm)s'
-                ) % credentials.fields
-            except KeyError, e:
-                self.log_error(
-                    "Open Directory (node=%s) error while performing digest authentication for user %s: "
-                    "missing digest response field: %s in: %s"
-                    % (self.service.realmName, self.shortNames[0], e, credentials.fields)
-                )
-                return False
-
-            try:
-                if self.digestcache[credentials.fields["uri"]] == response:
-                    return True
-            except (AttributeError, KeyError):
-                pass
-
-            try:
-                if opendirectory.authenticateUserDigest(
-                    self.service.directory,
-                    self.nodeName,
-                    self.shortNames[0],
-                    challenge,
-                    response,
-                    credentials.method
-                ):
-                    try:
-                        cache = self.digestcache
-                    except AttributeError:
-                        cache = self.digestcache = {}
-
-                    cache[credentials.fields["uri"]] = response
-
-                    return True
-                else:
-                    self.log_debug(
-"""Open Directory digest authentication failed with:
-    Nodename:  %s
-    Username:  %s
-    Challenge: %s
-    Response:  %s
-    Method:    %s
-""" % (self.nodeName, self.shortNames[0], challenge, response, credentials.method))
-
-            except opendirectory.ODError, e:
-                self.log_error(
-                    "Open Directory (node=%s) error while performing digest authentication for user %s: %s"
-                    % (self.service.realmName, self.shortNames[0], e)
-                )
-                return False
-
-            return False
-
-        return super(OpenDirectoryRecord, self).verifyCredentials(credentials)
-
-class OpenDirectoryInitError(DirectoryError):
-    """
-    OpenDirectory initialization error.
-    """

Modified: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/cachingdirectory.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/cachingdirectory.py	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/cachingdirectory.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -97,7 +97,6 @@
 
 
     def removeRecord(self, record):
-        
         if record in self.records:
             self.records.remove(record)
             for indexType in self.directoryService.indexTypes():
@@ -245,13 +244,24 @@
     recordWithUID = recordWithGUID
 
     def _lookupRecord(self, recordTypes, indexType, indexKey, cacheOnMiss=True):
-        
+
         if recordTypes is None:
             recordTypes = self.recordTypes()
+        else:
+            # Only use recordTypes this service supports:
+            supportedRecordTypes = self.recordTypes()
+            typesToQuery = []
+            for recordType in recordTypes:
+                if recordType in supportedRecordTypes:
+                    typesToQuery.append(recordType)
+            if not typesToQuery:
+                return None
+            recordTypes = typesToQuery
 
         def lookup():
             for recordType in recordTypes:
                 record = self.recordCacheForType(recordType).findRecord(indexType, indexKey)
+
                 if record:
                     if (
                         record.cachedTime != 0 and

Deleted: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/cachingxmlfile.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/cachingxmlfile.py	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/cachingxmlfile.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -1,207 +0,0 @@
-##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-XML based user/group/resource directory service implementation.
-"""
-
-__all__ = [
-    "XMLDirectoryService",
-]
-
-from time import time
-import types
-
-from twisted.cred.credentials import UsernamePassword
-from twisted.web2.auth.digest import DigestedCredentials
-from twisted.python.filepath import FilePath
-
-from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.cachingdirectory import CachingDirectoryService,\
-    CachingDirectoryRecord
-from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
-
-class XMLDirectoryService(CachingDirectoryService):
-    """
-    XML based implementation of L{IDirectoryService}.
-    """
-    baseGUID = "9CA8DEC5-5A17-43A9-84A8-BE77C1FB9172"
-
-    realmName = None
-
-    def __repr__(self):
-        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.xmlFile)
-
-    def __init__(self, xmlFile, alwaysStat=False):
-        super(XMLDirectoryService, self).__init__()
-
-        if type(xmlFile) is str:
-            xmlFile = FilePath(xmlFile)
-
-        self.xmlFile = xmlFile
-        self._fileInfo = None
-        self._lastCheck = 0
-        self._alwaysStat = alwaysStat
-        self._accounts()
-
-    def recordTypes(self):
-        recordTypes = (
-            DirectoryService.recordType_users,
-            DirectoryService.recordType_groups,
-            DirectoryService.recordType_locations,
-            DirectoryService.recordType_resources
-        )
-        return recordTypes
-
-    def queryDirectory(self, recordTypes, indexType, indexKey):
-        
-        for recordType in recordTypes:
-            for xmlPrincipal in self._accounts()[recordType].itervalues():
-                
-                matched = False
-                if indexType == self.INDEX_TYPE_GUID:
-                    matched = indexKey == xmlPrincipal.guid
-                elif indexType == self.INDEX_TYPE_SHORTNAME:
-                    matched = indexKey in xmlPrincipal.shortNames
-                elif indexType == self.INDEX_TYPE_CUA:
-                    matched = indexKey in xmlPrincipal.calendarUserAddresses
-                
-                if matched:
-                    record = XMLDirectoryRecord(
-                        service       = self,
-                        recordType    = recordType,
-                        shortNames    = tuple(xmlPrincipal.shortNames),
-                        xmlPrincipal  = xmlPrincipal,
-                    )
-                    self.recordCacheForType(recordType).addRecord(record)
-            
-    def recordsMatchingFields(self, fields, operand="or", recordType=None):
-        # Default, brute force method search of underlying XML data
-
-        def fieldMatches(fieldValue, value, caseless, matchType):
-            if fieldValue is None:
-                return False
-            elif type(fieldValue) in types.StringTypes:
-                fieldValue = (fieldValue,)
-            
-            for testValue in fieldValue:
-                if caseless:
-                    testValue = testValue.lower()
-                    value = value.lower()
-    
-                if matchType == 'starts-with':
-                    if testValue.startswith(value):
-                        return True
-                elif matchType == 'contains':
-                    try:
-                        _ignore_discard = testValue.index(value)
-                        return True
-                    except ValueError:
-                        pass
-                else: # exact
-                    if testValue == value:
-                        return True
-                    
-            return False
-
-        def xmlPrincipalMatches(xmlPrincipal):
-            if operand == "and":
-                for fieldName, value, caseless, matchType in fields:
-                    try:
-                        fieldValue = getattr(xmlPrincipal, fieldName)
-                        if not fieldMatches(fieldValue, value, caseless, matchType):
-                            return False
-                    except AttributeError:
-                        # No property => no match
-                        return False
-                # we hit on every property
-                return True
-            else: # "or"
-                for fieldName, value, caseless, matchType in fields:
-                    try:
-                        fieldValue = getattr(xmlPrincipal, fieldName)
-                        if fieldMatches(fieldValue, value, caseless, matchType):
-                            return True
-                    except AttributeError:
-                        # No value
-                        pass
-                # we didn't hit any
-                return False
-
-        if recordType is None:
-            recordTypes = list(self.recordTypes())
-        else:
-            recordTypes = (recordType,)
-
-        for recordType in recordTypes:
-            for xmlPrincipal in self._accounts()[recordType].itervalues():
-                if xmlPrincipalMatches(xmlPrincipal):
-                    
-                    # Load/cache record from its GUID
-                    record = self.recordWithGUID(xmlPrincipal.guid)
-                    if record:
-                        yield record
-
-    def _accounts(self):
-        currentTime = time()
-        if self._alwaysStat or currentTime - self._lastCheck > 60:
-            self.xmlFile.restat()
-            self._lastCheck = currentTime
-            fileInfo = (self.xmlFile.getmtime(), self.xmlFile.getsize())
-            if fileInfo != self._fileInfo:
-                parser = XMLAccountsParser(self.xmlFile)
-                self._parsedAccounts = parser.items
-                self.realmName = parser.realm
-                self._fileInfo = fileInfo
-        return self._parsedAccounts
-
-class XMLDirectoryRecord(CachingDirectoryRecord):
-    """
-    XML based implementation implementation of L{IDirectoryRecord}.
-    """
-    def __init__(self, service, recordType, shortNames, xmlPrincipal):
-        super(XMLDirectoryRecord, self).__init__(
-            service               = service,
-            recordType            = recordType,
-            guid                  = xmlPrincipal.guid,
-            shortNames            = shortNames,
-            fullName              = xmlPrincipal.fullName,
-            firstName             = xmlPrincipal.firstName,
-            lastName              = xmlPrincipal.lastName,
-            emailAddresses        = xmlPrincipal.emailAddresses,
-            calendarUserAddresses = xmlPrincipal.calendarUserAddresses,
-            enabledForCalendaring = xmlPrincipal.enabledForCalendaring,
-        )
-
-        self.password          = xmlPrincipal.password
-        self._members          = xmlPrincipal.members
-        self._groups           = xmlPrincipal.groups
-
-    def members(self):
-        for recordType, shortName in self._members:
-            yield self.service.recordWithShortName(recordType, shortName)
-
-    def groups(self):
-        for shortName in self._groups:
-            yield self.service.recordWithShortName(DirectoryService.recordType_groups, shortName)
-
-    def verifyCredentials(self, credentials):
-        if isinstance(credentials, UsernamePassword):
-            return credentials.password == self.password
-        if isinstance(credentials, DigestedCredentials):
-            return credentials.checkPassword(self.password)
-
-        return super(XMLDirectoryRecord, self).verifyCredentials(credentials)

Copied: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/oldappleopendirectory.py (from rev 4198, CalendarServer/trunk/twistedcaldav/directory/oldappleopendirectory.py)
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/oldappleopendirectory.py	                        (rev 0)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/oldappleopendirectory.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -0,0 +1,1173 @@
+##
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Apple Open Directory directory service implementation.
+"""
+
+__all__ = [
+    "OpenDirectoryService",
+    "OpenDirectoryInitError",
+]
+
+import sys
+from random import random
+from uuid import UUID
+
+from twext.python.plistlib import readPlistFromString
+
+from xml.parsers.expat import ExpatError
+
+import opendirectory
+import dsattributes
+import dsquery
+
+from twisted.internet.reactor import callLater
+from twisted.internet.threads import deferToThread
+from twisted.cred.credentials import UsernamePassword
+from twisted.web2.auth.digest import DigestedCredentials
+
+from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+from twistedcaldav.directory.directory import DirectoryError, UnknownRecordTypeError
+from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
+
+class OpenDirectoryService(DirectoryService):
+    """
+    Open Directory implementation of L{IDirectoryService}.
+    """
+    baseGUID = "891F8321-ED02-424C-BA72-89C32F215C1E"
+
+    def __repr__(self):
+        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.node)
+
+    def __init__(
+        self,
+        node="/Search",
+        restrictEnabledRecords=False,
+        restrictToGroup="",
+        dosetup=True,
+        cacheTimeout=30
+    ):
+        """
+        @param node: an OpenDirectory node name to bind to.
+        @param restrictEnabledRecords: C{True} if a group in the directory is to be used to determine
+            which calendar users are enabled.
+        @param restrictToGroup: C{str} guid or name of group used to restrict enabled users.
+        @param dosetup: if C{True} then the directory records are initialized,
+                        if C{False} they are not.
+                        This should only be set to C{False} when doing unit tests.
+        @param cacheTimeout: C{int} number of minutes before cache is invalidated.
+        """
+        try:
+            directory = opendirectory.odInit(node)
+        except opendirectory.ODError, e:
+            self.log_error("Open Directory (node=%s) Initialization error: %s" % (node, e))
+            raise
+
+        self.realmName = node
+        self.directory = directory
+        self.node = node
+        self.restrictEnabledRecords = restrictEnabledRecords
+        self.restrictToGroup = restrictToGroup
+        try:
+            UUID(self.restrictToGroup)
+        except:
+            self.restrictToGUID = False
+        else:
+            self.restrictToGUID = True
+        self.restrictedGUIDs = None
+        self.cacheTimeout = cacheTimeout
+        self._records = {}
+        self._delayedCalls = set()
+
+        if dosetup:
+            for recordType in self.recordTypes():
+                self.recordsForType(recordType)
+
+    def __cmp__(self, other):
+        if not isinstance(other, DirectoryRecord):
+            return super(DirectoryRecord, self).__eq__(other)
+
+        for attr in ("directory", "node"):
+            diff = cmp(getattr(self, attr), getattr(other, attr))
+            if diff != 0:
+                return diff
+        return 0
+
+    def __hash__(self):
+        h = hash(self.__class__)
+        for attr in ("directory", "node"):
+            h = (h + hash(getattr(self, attr))) & sys.maxint
+        return h
+
+    def _expandGroupMembership(self, members, nestedGroups, processedGUIDs=None, returnGroups=False):
+
+        if processedGUIDs is None:
+            processedGUIDs = set()
+
+        if isinstance(members, str):
+            members = [members]
+
+        if isinstance(nestedGroups, str):
+            nestedGroups = [nestedGroups]
+
+        for memberGUID in members:
+            if memberGUID not in processedGUIDs:
+                processedGUIDs.add(memberGUID)
+                yield memberGUID
+
+        for groupGUID in nestedGroups:
+            if groupGUID in processedGUIDs:
+                continue
+
+            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+                self.directory,
+                dsattributes.kDS1AttrGeneratedUID,
+                groupGUID,
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
+            ))
+            result = opendirectory.queryRecordsWithAttribute_list(
+                self.directory,
+                dsattributes.kDS1AttrGeneratedUID,
+                groupGUID,
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
+            )
+
+            if not result:
+                self.log_error("Couldn't find group %s when trying to expand nested groups."
+                             % (groupGUID,))
+                continue
+
+            group = result[0][1]
+
+            processedGUIDs.add(groupGUID)
+            if returnGroups:
+                yield groupGUID
+
+            for GUID in self._expandGroupMembership(
+                group.get(dsattributes.kDSNAttrGroupMembers, []),
+                group.get(dsattributes.kDSNAttrNestedGroups, []),
+                processedGUIDs,
+                returnGroups,
+            ):
+                yield GUID
+
+    def _calendarUserAddresses(self, recordType, recordData):
+        """
+        Extract specific attributes from the directory record for use as calendar user address.
+        
+        @param recordData: a C{dict} containing the attributes retrieved from the directory.
+        @return: a C{set} of C{str} for each expanded calendar user address.
+        """
+        # Now get the addresses
+        result = set()
+        
+        # Add each email address as a mailto URI
+        emails = recordData.get(dsattributes.kDSNAttrEMailAddress)
+        if emails is not None:
+            if isinstance(emails, str):
+                emails = [emails]
+            for email in emails:
+                result.add("mailto:%s" % (email.lower(),))
+                
+        return result
+
+    def _parseResourceInfo(self, plist, guid, recordType, shortname):
+        """
+        Parse OD ResourceInfo attribute and extract information that the server needs.
+
+        @param plist: the plist that is the attribute value.
+        @type plist: str
+        @param guid: the directory GUID of the record being parsed.
+        @type guid: str
+        @param shortname: the record shortname of the record being parsed.
+        @type shortname: str
+        @return: a C{tuple} of C{bool} for auto-accept, C{str} for proxy GUID, C{str} for read-only proxy GUID.
+        """
+        try:
+            plist = readPlistFromString(plist)
+            wpframework = plist.get("com.apple.WhitePagesFramework", {})
+            autoaccept = wpframework.get("AutoAcceptsInvitation", False)
+            proxy = wpframework.get("CalendaringDelegate", None)
+            read_only_proxy = wpframework.get("ReadOnlyCalendaringDelegate", None)
+        except (ExpatError, AttributeError), e:
+            self.log_error(
+                "Failed to parse ResourceInfo attribute of record (%s)%s (guid=%s): %s\n%s" %
+                (recordType, shortname, guid, e, plist,)
+            )
+            raise ValueError("Invalid ResourceInfo")
+
+        return (autoaccept, proxy, read_only_proxy,)
+
+    def recordTypes(self):
+        return (
+            self.recordType_users,
+            self.recordType_groups,
+            self.recordType_locations,
+            self.recordType_resources,
+        )
+
+    def _storage(self, recordType):
+        try:
+            storage = self._records[recordType]
+        except KeyError:
+            self.reloadCache(recordType)
+            storage = self._records[recordType]
+        else:
+            if storage["status"] == "stale":
+                storage["status"] = "loading"
+
+                def onError(f):
+                    storage["status"] = "stale" # Keep trying
+                    self.log_error(
+                        "Unable to load records of type %s from OpenDirectory due to unexpected error: %s"
+                        % (recordType, f)
+                    )
+
+                # Reload the restricted access group details if reloading user records
+                if recordType == self.recordType_users:
+                    self.restrictedGUIDs = None
+
+                d = deferToThread(self.reloadCache, recordType)
+                d.addErrback(onError)
+
+        return storage
+
+    def recordsForType(self, recordType):
+        """
+        @param recordType: a record type
+        @return: a dictionary containing all records for the given record
+        type.  Keys are short names and values are the corresponding
+        OpenDirectoryRecord for the given record type.
+        """
+        return self._storage(recordType)["records"]
+
+    def listRecords(self, recordType):
+        return self.recordsForType(recordType).itervalues()
+
+    def recordWithShortName(self, recordType, shortName):
+        try:
+            return self.recordsForType(recordType)[shortName]
+        except KeyError:
+            # Check negative cache
+            if shortName in self._storage(recordType)["disabled names"]:
+                return None
+
+            # Cache miss; try looking the record up, in case it is new
+            # FIXME: This is a blocking call (hopefully it's a fast one)
+            self.reloadCache(recordType, lookup=("shortName", shortName,))
+            record = self.recordsForType(recordType).get(shortName, None)
+            if record is None:
+                # Add to negative cache
+                self._storage(recordType)["disabled names"].add(shortName)
+            return record
+
+    def recordWithCalendarUserAddress(self, address):
+        address = normalizeCUAddr(address)
+        record = None
+        if address.startswith("urn:uuid:"):
+            guid = address[9:]
+            record = self.recordWithGUID(guid)
+        elif address.startswith("mailto:"):
+            record = self._recordWithAttribute("cuas", "disabled cuas", "cua", address)
+
+        return record if record and record.enabledForCalendaring else None
+
+    def recordWithGUID(self, guid):
+        return self._recordWithAttribute("guids", "disabled guids", "guid", guid)
+
+    recordWithUID = recordWithGUID
+
+    def recordWithAuthID(self, authID):
+        return self._recordWithAttribute("authIDs", "disabled authIDs", "authID", authID)
+
+    def _recordWithAttribute(self, cacheKey, disabledKey, lookupKey, value):
+        def lookup():
+            for recordType in self.recordTypes():
+                record = self._storage(recordType)[cacheKey].get(value, None)
+                if record:
+                    return record
+            else:
+                return None
+
+        record = lookup()
+
+        if record is None:
+            # Cache miss; try looking the record up, in case it is new
+            for recordType in self.recordTypes():
+                # Check negative cache
+                if value in self._storage(recordType)[disabledKey]:
+                    continue
+
+                try:
+                    self.reloadCache(recordType, lookup=(lookupKey, value,))
+                    record = lookup()
+                except opendirectory.ODError, e:
+                    if e.message[1] == -14140 or e.message[1] == -14200:
+                        # Unsupported attribute on record - don't fail
+                        record = None
+                    else:
+                        raise
+
+                if record is None:
+                    self._storage(recordType)[disabledKey].add(value)
+                else:
+                    self.log_info("Faulted record with %s %s into %s record cache"
+                                  % (lookupKey, value, recordType))
+                    break
+            else:
+                # Nothing found; add to negative cache
+                self.log_info("Unable to find any record with %s %s" % (lookupKey, value,))
+
+        return record
+
+    def groupsForGUID(self, guid):
+        
+        # Lookup in index
+        try:
+            return self._storage(self.recordType_groups)["groupsForGUID"][guid]
+        except KeyError:
+            return ()
+
+    def proxiesForGUID(self, recordType, guid):
+        
+        # Lookup in index
+        try:
+            return self._storage(recordType)["proxiesForGUID"][guid]
+        except KeyError:
+            return ()
+
+    def readOnlyProxiesForGUID(self, recordType, guid):
+        
+        # Lookup in index
+        try:
+            return self._storage(recordType)["readOnlyProxiesForGUID"][guid]
+        except KeyError:
+            return ()
+
+    def _indexGroup(self, group, guids, index):
+        for guid in guids:
+            index.setdefault(guid, set()).add(group)
+
+    _ODFields = {
+        'fullName' : {
+            'odField' : dsattributes.kDS1AttrDistinguishedName,
+            'appliesTo' : set([
+                dsattributes.kDSStdRecordTypeUsers,
+                dsattributes.kDSStdRecordTypeGroups,
+                dsattributes.kDSStdRecordTypePlaces,
+                dsattributes.kDSStdRecordTypeResources,
+            ]),
+        },
+        'firstName' : {
+            'odField' : dsattributes.kDS1AttrFirstName,
+            'appliesTo' : set([
+                dsattributes.kDSStdRecordTypeUsers,
+            ]),
+        },
+        'lastName' : {
+            'odField' : dsattributes.kDS1AttrLastName,
+            'appliesTo' : set([
+                dsattributes.kDSStdRecordTypeUsers,
+            ]),
+        },
+        'emailAddresses' : {
+            'odField' : dsattributes.kDSNAttrEMailAddress,
+            'appliesTo' : set([
+                dsattributes.kDSStdRecordTypeUsers,
+                dsattributes.kDSStdRecordTypeGroups,
+            ]),
+        },
+        'recordName' : {
+            'odField' : dsattributes.kDSNAttrRecordName,
+            'appliesTo' : set([
+                dsattributes.kDSStdRecordTypeUsers,
+                dsattributes.kDSStdRecordTypeGroups,
+                dsattributes.kDSStdRecordTypePlaces,
+                dsattributes.kDSStdRecordTypeResources,
+            ]),
+        },
+        'guid' : {
+            'odField' : dsattributes.kDS1AttrGeneratedUID,
+            'appliesTo' : set([
+                dsattributes.kDSStdRecordTypeUsers,
+                dsattributes.kDSStdRecordTypeGroups,
+                dsattributes.kDSStdRecordTypePlaces,
+                dsattributes.kDSStdRecordTypeResources,
+            ]),
+        },
+    }
+
+    _toODRecordTypes = {
+        DirectoryService.recordType_users :
+            dsattributes.kDSStdRecordTypeUsers,
+        DirectoryService.recordType_locations :
+            dsattributes.kDSStdRecordTypePlaces,
+        DirectoryService.recordType_groups :
+            dsattributes.kDSStdRecordTypeGroups,
+        DirectoryService.recordType_resources :
+            dsattributes.kDSStdRecordTypeResources,
+    }
+
+    _fromODRecordTypes = dict([(b, a) for a, b in _toODRecordTypes.iteritems()])
+
+
+    def recordsMatchingFields(self, fields, operand="or", recordType=None):
+
+        # Note that OD applies case-sensitivity globally across the entire
+        # query, not per expression, so the current code uses whatever is
+        # specified in the last field in the fields list
+
+        def collectResults(results):
+            self.log_info("Got back %d records from OD" % (len(results),))
+            for key, val in results.iteritems():
+                self.log_debug("OD result: %s %s" % (key, val))
+                try:
+                    guid = val[dsattributes.kDS1AttrGeneratedUID]
+                    record = self.recordWithGUID(guid)
+                    if record:
+                        yield record
+                except KeyError:
+                    pass
+
+        def multiQuery(directory, queries, attrs, operand):
+            results = {}
+
+            for query, recordTypes in queries.iteritems():
+                if not query:
+                    continue
+
+                expressions = []
+                for ODField, value, caseless, matchType in query:
+                    if matchType == "starts-with":
+                        comparison = dsattributes.eDSStartsWith
+                    elif matchType == "contains":
+                        comparison = dsattributes.eDSContains
+                    else:
+                        comparison = dsattributes.eDSExact
+                    expressions.append(dsquery.match(ODField, value, comparison))
+
+                complexExpression = dsquery.expression(operand, expressions).generate()
+
+                self.log_info("Calling OD: Types %s, Operand %s, Caseless %s, %s" %
+                    (recordTypes, operand, caseless, complexExpression))
+
+                results.update(
+                    opendirectory.queryRecordsWithAttributes(
+                        directory,
+                        complexExpression,
+                        caseless,
+                        recordTypes,
+                        attrs,
+                    )
+                )
+
+            return results
+
+
+        operand = (dsquery.expression.OR if operand == "or"
+            else dsquery.expression.AND)
+
+        if recordType is None:
+            # The client is looking for records in any of the four types
+            recordTypes = set(self._toODRecordTypes.values())
+        else:
+            # The client is after only one recordType
+            recordTypes = [self._toODRecordTypes[recordType]]
+
+        queries = buildQueries(recordTypes, fields, self._ODFields)
+
+        deferred = deferToThread(
+            multiQuery,
+            self.directory,
+            queries,
+            [ dsattributes.kDS1AttrGeneratedUID ],
+            operand
+        )
+        deferred.addCallback(collectResults)
+        return deferred
+
+
+    def reloadCache(self, recordType, lookup=None):
+        if lookup is not None:
+            self.log_info("Faulting record with %s %s into %s record cache" % (lookup[0], lookup[1], recordType))
+        else:
+            self.log_info("Reloading %s record cache" % (recordType,))
+
+        results = self._queryDirectory(recordType, lookup=lookup)
+
+        if lookup is None:
+            records = {}
+            guids   = {}
+            authIDs = {}
+            cuas  = {}
+
+            disabledNames   = set()
+            disabledGUIDs   = set()
+            disabledAuthIDs = set()
+            disabledCUAs    = set()
+
+            if recordType == self.recordType_groups:
+                groupsForGUID = {}
+            elif recordType in (self.recordType_resources, self.recordType_locations):
+                proxiesForGUID = {}
+                readOnlyProxiesForGUID = {}
+        else:
+            storage = self._records[recordType]
+
+            records = storage["records"]
+            guids   = storage["guids"]
+            authIDs = storage["authIDs"]
+            cuas    = storage["cuas"]
+
+            disabledNames   = storage["disabled names"]
+            disabledGUIDs   = storage["disabled guids"]
+            disabledAuthIDs = storage["disabled authIDs"]
+            disabledCUAs    = storage["disabled cuas"]
+
+            if recordType == self.recordType_groups:
+                groupsForGUID = storage["groupsForGUID"]
+            elif recordType in (self.recordType_resources, self.recordType_locations):
+                proxiesForGUID = storage["proxiesForGUID"]
+                readOnlyProxiesForGUID = storage["readOnlyProxiesForGUID"]
+
+        enabled_count = 0
+        
+        def _uniqueTupleFromAttribute(attribute):
+            if attribute:
+                if isinstance(attribute, str):
+                    return (attribute,)
+                else:
+                    s = set()
+                    return tuple([(s.add(x), x)[1] for x in attribute if x not in s])
+            else:
+                return ()
+            
+        def _setFromAttribute(attribute, lower=False):
+            if attribute:
+                if isinstance(attribute, str):
+                    return set((attribute.lower() if lower else attribute,))
+                else:
+                    return set([item.lower() if lower else item for item in attribute])
+            else:
+                return ()
+            
+        for (recordShortName, value) in results:
+
+            # Now get useful record info.
+            recordGUID           = value.get(dsattributes.kDS1AttrGeneratedUID)
+            recordShortNames     = _uniqueTupleFromAttribute(value.get(dsattributes.kDSNAttrRecordName))
+            recordAuthIDs        = _setFromAttribute(value.get(dsattributes.kDSNAttrAltSecurityIdentities))
+            recordFullName       = value.get(dsattributes.kDS1AttrDistinguishedName)
+            recordFirstName      = value.get(dsattributes.kDS1AttrFirstName)
+            recordLastName       = value.get(dsattributes.kDS1AttrLastName)
+            recordEmailAddresses = _setFromAttribute(value.get(dsattributes.kDSNAttrEMailAddress), lower=True)
+            recordNodeName       = value.get(dsattributes.kDSNAttrMetaNodeLocation)
+
+            if not recordGUID:
+                self.log_debug("Record (%s)%s in node %s has no GUID; ignoring."
+                               % (recordType, recordShortName, recordNodeName))
+                continue
+
+            if recordGUID.lower().startswith("ffffeeee-dddd-cccc-bbbb-aaaa"):
+                self.log_debug("Ignoring system record (%s)%s in node %s."
+                               % (recordType, recordShortName, recordNodeName))
+                continue
+
+            # Determine enabled state
+            if recordType == self.recordType_groups:
+                enabledForCalendaring = False
+            else:
+                if self.restrictEnabledRecords and self.restrictedGUIDs is not None:
+                    enabledForCalendaring = recordGUID in self.restrictedGUIDs
+                else:
+                    enabledForCalendaring = True
+
+            if enabledForCalendaring:
+                enabled_count += 1
+                calendarUserAddresses = self._calendarUserAddresses(recordType, value)
+            else:
+                # Some records we want to keep even though they are not enabled for calendaring.
+                # Others we discard.
+                if recordType not in (
+                    self.recordType_users,
+                    self.recordType_groups,
+                ):
+                    self.log_debug(
+                        "Record (%s) %s is not enabled for calendaring"
+                        % (recordType, recordShortName)
+                    )
+                    continue
+
+                self.log_debug(
+                    "Record (%s) %s is not enabled for calendaring but may be used in ACLs"
+                    % (recordType, recordShortName)
+                )
+                calendarUserAddresses = ()
+
+            # Special case for groups, which have members.
+            if recordType == self.recordType_groups:
+                memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
+                if memberGUIDs is None:
+                    memberGUIDs = ()
+                elif type(memberGUIDs) is str:
+                    memberGUIDs = (memberGUIDs,)
+                nestedGUIDs = value.get(dsattributes.kDSNAttrNestedGroups)
+                if nestedGUIDs:
+                    if type(nestedGUIDs) is str:
+                        nestedGUIDs = (nestedGUIDs,)
+                    memberGUIDs += tuple(nestedGUIDs)
+            else:
+                memberGUIDs = ()
+
+            record = OpenDirectoryRecord(
+                service               = self,
+                recordType            = recordType,
+                guid                  = recordGUID,
+                nodeName              = recordNodeName,
+                shortNames            = recordShortNames,
+                authIDs               = recordAuthIDs,
+                fullName              = recordFullName,
+                firstName             = recordFirstName,
+                lastName              = recordLastName,
+                emailAddresses        = recordEmailAddresses,
+                calendarUserAddresses = calendarUserAddresses,
+                enabledForCalendaring = enabledForCalendaring,
+                memberGUIDs           = memberGUIDs,
+            )
+
+            def disableGUID(guid, record):
+                """
+                Disable the record by removing it from all indexes.
+                """
+
+                self.log_warn("GUID %s disabled due to conflict for record: %s"
+                              % (guid, record))
+
+                disabledGUIDs.add(guid)
+                disabledNames.update(record.shortNames)
+                disabledAuthIDs.update(record.authIDs)
+                disabledCUAs.update(record.calendarUserAddresses)
+
+                if guid in guids:
+                    try:
+                        del guids[guid]
+                    except KeyError:
+                        pass
+                for shortName in record.shortNames:
+                    try:
+                        del records[shortName]
+                    except KeyError:
+                        pass
+                for authID in record.authIDs:
+                    try:
+                        del authIDs[authID]
+                    except KeyError:
+                        pass
+                for cua in record.calendarUserAddresses:
+                    try:
+                        del cuas[cua]
+                    except KeyError:
+                        pass
+
+            if record.guid in disabledGUIDs:
+                disableGUID(record.guid, record)
+            else:
+                # Check for duplicates
+                existing_record = guids.get(record.guid)
+                if existing_record is not None:
+                    if existing_record.shortNames != record.shortNames:
+                        disableGUID(record.guid, record)
+                        disableGUID(record.guid, existing_record)
+                        if existing_record.enabledForCalendaring:
+                            enabled_count -= 1
+                else:
+                    guids[record.guid] = record
+                    self.log_debug("Added record %s to OD record cache" % (record,))
+                    if record.enabledForCalendaring:
+                        enabled_count += 1
+        
+                    # Do group indexing if needed
+                    if recordType == self.recordType_groups:
+                        self._indexGroup(record, record._memberGUIDs, groupsForGUID)
+
+                    # Index non-duplicate shortNames
+                    def disableName(shortName, record):
+                        self.log_warn("Short name %s disabled due to conflict for record: %s"
+                                      % (shortName, record))
+        
+                        record.shortNames = tuple([item for item in record.shortNames if item != shortName])
+                        disabledNames.add(shortName)
+        
+                        if shortName in records:
+                            del records[shortName]
+        
+                    for shortName in tuple(record.shortNames):
+                        if shortName in disabledNames:
+                            disableName(shortName, record)
+                        else:
+                            # Check for duplicates
+                            existing_record = records.get(shortName)
+                            if existing_record is not None and existing_record != record:
+                                disableName(shortName, record)
+                                disableName(shortName, existing_record)
+                            else:
+                                records[shortName] = record
+        
+                    # Index non-duplicate authIDs
+                    def disableAuthIDs(authID, record):
+                        self.log_warn("Auth ID %s disabled due to conflict for record: %s"
+                                      % (authID, record))
+        
+                        record.authIDs.remove(authID)
+                        disabledAuthIDs.add(authID)
+        
+                        if authID in authIDs:
+                            del authIDs[authID]
+        
+                    for authID in frozenset(recordAuthIDs):
+                        if authID in disabledAuthIDs:
+                            disableAuthIDs(authID, record)
+                        else:
+                            # Check for duplicates
+                            existing_record = authIDs.get(authID)
+                            if existing_record is not None:
+                                disableAuthIDs(authID, record)
+                                disableAuthIDs(authID, existing_record)
+                            else:
+                                authIDs[authID] = record
+        
+                    # Index non-duplicate CUAs
+                    def disableCUA(cua, record):
+                        self.log_warn("CUA %s disabled due to conflict for record: %s"
+                                      % (cua, record))
+        
+                        record.calendarUserAddresses.remove(cua)
+                        disabledCUAs.add(cua)
+        
+                        if cua in cuas:
+                            del cuas[cua]
+
+                        if cua in records:
+                            del records[cua]
+        
+                    for cua in frozenset(calendarUserAddresses):
+                        if cua in disabledCUAs:
+                            disableCUA(cua, record)
+                        else:
+                            # Check for duplicates
+                            existing_record = cuas.get(cua)
+                            if existing_record is not None:
+                                disableCUA(cua, record)
+                                disableCUA(cua, existing_record)
+                            else:
+                                cuas[cua] = record
+
+        if lookup is None:
+            #
+            # Replace the entire cache
+            #
+            storage = {
+                "status"           : "new",
+                "records"          : records,
+                "guids"            : guids,
+                "authIDs"          : authIDs,
+                "cuas"             : cuas,
+                "disabled names"   : disabledNames,
+                "disabled guids"   : disabledGUIDs,
+                "disabled authIDs" : disabledAuthIDs,
+                "disabled cuas"    : disabledCUAs,
+            }
+
+            # Add group indexing if needed
+            if recordType == self.recordType_groups:
+                storage["groupsForGUID"] = groupsForGUID
+
+            # Add proxy indexing if needed
+            elif recordType in (self.recordType_resources, self.recordType_locations):
+                storage["proxiesForGUID"] = proxiesForGUID
+                storage["readOnlyProxiesForGUID"] = readOnlyProxiesForGUID
+
+            def rot():
+                storage["status"] = "stale"
+                removals = set()
+                for call in self._delayedCalls:
+                    if not call.active():
+                        removals.add(call)
+                for item in removals:
+                    self._delayedCalls.remove(item)
+
+            #
+            # Add jitter/fuzz factor to avoid stampede for large OD query
+            # Max out the jitter at 60 minutes
+            #
+            cacheTimeout = min(self.cacheTimeout, 60) * 60
+            cacheTimeout = (cacheTimeout * random()) - (cacheTimeout / 2)
+            cacheTimeout += self.cacheTimeout * 60
+            self._delayedCalls.add(callLater(cacheTimeout, rot))
+
+            self._records[recordType] = storage
+
+            self.log_info(
+                "Added %d (%d enabled) records to %s OD record cache; expires in %d seconds"
+                % (len(self._records[recordType]["guids"]), enabled_count, recordType, cacheTimeout)
+            )
+
+    def _queryDirectory(self, recordType, lookup=None):
+        attrs = [
+            dsattributes.kDS1AttrGeneratedUID,
+            dsattributes.kDSNAttrRecordName,
+            dsattributes.kDSNAttrAltSecurityIdentities,
+            dsattributes.kDS1AttrDistinguishedName,
+            dsattributes.kDS1AttrFirstName,
+            dsattributes.kDS1AttrLastName,
+            dsattributes.kDSNAttrEMailAddress,
+            dsattributes.kDSNAttrMetaNodeLocation,
+        ]
+
+        if recordType == self.recordType_users:
+            listRecordType = dsattributes.kDSStdRecordTypeUsers
+
+        elif recordType == self.recordType_groups:
+            listRecordType = dsattributes.kDSStdRecordTypeGroups
+            attrs.append(dsattributes.kDSNAttrGroupMembers)
+            attrs.append(dsattributes.kDSNAttrNestedGroups)
+
+        elif recordType == self.recordType_locations:
+            listRecordType = dsattributes.kDSStdRecordTypePlaces
+            attrs.append(dsattributes.kDSNAttrResourceInfo)
+        
+        elif recordType == self.recordType_resources:
+            listRecordType = dsattributes.kDSStdRecordTypeResources
+            attrs.append(dsattributes.kDSNAttrResourceInfo)
+        
+        else:
+            raise UnknownRecordTypeError("Unknown Open Directory record type: %s" % (recordType))
+
+        # If restricting enabled records, then make sure the restricted group member
+        # details are loaded. Do nested group expansion and include the nested groups
+        # as enabled records too.
+        if self.restrictEnabledRecords and self.restrictedGUIDs is None:
+
+            attributeToMatch = dsattributes.kDS1AttrGeneratedUID if self.restrictToGUID else dsattributes.kDSNAttrRecordName 
+            valueToMatch = self.restrictToGroup
+
+            self.log_debug("Doing restricted group membership check")
+            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+                self.directory,
+                attributeToMatch,
+                valueToMatch,
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups,],
+            ))
+            results = opendirectory.queryRecordsWithAttribute_list(
+                self.directory,
+                attributeToMatch,
+                valueToMatch,
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups,],
+            )
+    
+            if len(results) == 1:
+                members      = results[0][1].get(dsattributes.kDSNAttrGroupMembers, [])
+                nestedGroups = results[0][1].get(dsattributes.kDSNAttrNestedGroups, [])
+            else:
+                members = []
+                nestedGroups = []
+
+            self.restrictedGUIDs = set(self._expandGroupMembership(members, nestedGroups, returnGroups=True))
+            self.log_debug("Got %d restricted group members" % (len(self.restrictedGUIDs),))
+
+        query = None
+        if lookup is not None:
+            indexType, indexKey = lookup
+            origIndexKey = indexKey
+
+            if indexType == "cua":
+                # The directory doesn't contain CUAs, so we need to convert
+                # the CUA to the appropriate field name and value:
+                queryattr, indexKey = cuAddressConverter(indexKey)
+                # queryattr will be one of:
+                # guid, emailAddresses, or recordName
+                # ...which will need to be mapped to DS
+                queryattr = self._ODFields[queryattr]['odField']
+
+            else:
+                queryattr = {
+                    "shortName" : dsattributes.kDSNAttrRecordName,
+                    "guid"      : dsattributes.kDS1AttrGeneratedUID,
+                    "authID"    : dsattributes.kDSNAttrAltSecurityIdentities,
+                }.get(indexType)
+                assert queryattr is not None, "Invalid type for record faulting query"
+
+            query = dsquery.match(queryattr, indexKey, dsattributes.eDSExact)
+
+        try:
+            if query:
+                self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+                    self.directory,
+                    query.attribute,
+                    query.value,
+                    query.matchType,
+                    False,
+                    listRecordType,
+                    attrs,
+                ))
+                results = opendirectory.queryRecordsWithAttribute_list(
+                    self.directory,
+                    query.attribute,
+                    query.value,
+                    query.matchType,
+                    False,
+                    listRecordType,
+                    attrs,
+                )
+            else:
+                self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
+                    self.directory,
+                    listRecordType,
+                    attrs,
+                ))
+                results = opendirectory.listAllRecordsWithAttributes_list(
+                    self.directory,
+                    listRecordType,
+                    attrs,
+                )
+        except opendirectory.ODError, ex:
+            self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
+            raise
+
+        return results
+
+    def getResourceInfo(self):
+        """
+        Resource information including proxy assignments for resource and
+        locations, as well as auto-schedule settings, used to live in the
+        directory.  This method fetches old resource info for migration
+        purposes.
+        """
+        attrs = [
+            dsattributes.kDS1AttrGeneratedUID,
+            dsattributes.kDSNAttrResourceInfo,
+        ]
+
+        for recordType in (dsattributes.kDSStdRecordTypePlaces, dsattributes.kDSStdRecordTypeResources):
+            try:
+                self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
+                    self.directory,
+                    recordType,
+                    attrs,
+                ))
+                results = opendirectory.listAllRecordsWithAttributes_list(
+                    self.directory,
+                    recordType,
+                    attrs,
+                )
+            except opendirectory.ODError, ex:
+                self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
+                raise
+
+            for (recordShortName, value) in results:
+                recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
+                resourceInfo = value.get(dsattributes.kDSNAttrResourceInfo)
+                if resourceInfo is not None:
+                    try:
+                        autoSchedule, proxy, readOnlyProxy = self._parseResourceInfo(resourceInfo,
+                            recordGUID, recordType, recordShortName)
+                    except ValueError:
+                        continue
+                    yield recordGUID, autoSchedule, proxy, readOnlyProxy
+
+
+def buildQueries(recordTypes, fields, mapping):
+    """
+    Determine how many queries need to be performed in order to work around opendirectory
+    quirks, where searching on fields that don't apply to a given recordType returns incorrect
+    results (either none, or all records).
+    """
+
+    fieldLists = {}
+    for recordType in recordTypes:
+        fieldLists[recordType] = []
+        for field, value, caseless, matchType in fields:
+            if field in mapping:
+                if recordType in mapping[field]['appliesTo']:
+                    ODField = mapping[field]['odField']
+                    fieldLists[recordType].append((ODField, value, caseless, matchType))
+
+    queries = {}
+    for recordType, fieldList in fieldLists.iteritems():
+        key = tuple(fieldList)
+        queries.setdefault(key, []).append(recordType)
+    return queries
+
+
+class OpenDirectoryRecord(DirectoryRecord):
+    """
+    Open Directory implementation of L{IDirectoryRecord}.
+    """
+    def __init__(
+        self, service, recordType, guid, nodeName, shortNames, authIDs, fullName,
+        firstName, lastName, emailAddresses,
+        calendarUserAddresses, enabledForCalendaring,
+        memberGUIDs,
+    ):
+        super(OpenDirectoryRecord, self).__init__(
+            service               = service,
+            recordType            = recordType,
+            guid                  = guid,
+            shortNames            = shortNames,
+            authIDs               = authIDs,
+            fullName              = fullName,
+            firstName             = firstName,
+            lastName              = lastName,
+            emailAddresses        = emailAddresses,
+            calendarUserAddresses = calendarUserAddresses,
+            enabledForCalendaring = enabledForCalendaring,
+        )
+        self.nodeName = nodeName
+        self._memberGUIDs = tuple(memberGUIDs)
+
+    def __repr__(self):
+        if self.service.realmName == self.nodeName:
+            location = self.nodeName
+        else:
+            location = "%s->%s" % (self.service.realmName, self.nodeName)
+
+        return "<%s[%s@%s(%s)] %s(%s) %r>" % (
+            self.__class__.__name__,
+            self.recordType,
+            self.service.guid,
+            location,
+            self.guid,
+            ",".join(self.shortNames),
+            self.fullName
+        )
+
+    def members(self):
+        if self.recordType != self.service.recordType_groups:
+            return
+
+        for guid in self._memberGUIDs:
+            userRecord = self.service.recordWithGUID(guid)
+            if userRecord is not None:
+                yield userRecord
+
+    def groups(self):
+        return self.service.groupsForGUID(self.guid)
+
+    def verifyCredentials(self, credentials):
+        if isinstance(credentials, UsernamePassword):
+            # Check cached password
+            try:
+                if credentials.password == self.password:
+                    return True
+            except AttributeError:
+                pass
+
+            # Check with directory services
+            try:
+                if opendirectory.authenticateUserBasic(self.service.directory, self.nodeName, self.shortNames[0], credentials.password):
+                    # Cache the password to avoid future DS queries
+                    self.password = credentials.password
+                    return True
+            except opendirectory.ODError, e:
+                self.log_error("Open Directory (node=%s) error while performing basic authentication for user %s: %s"
+                            % (self.service.realmName, self.shortNames[0], e))
+
+            return False
+
+        elif isinstance(credentials, DigestedCredentials):
+            #
+            # We need a special format for the "challenge" and "response" strings passed into open directory, as it is
+            # picky about exactly what it receives.
+            #
+            try:
+                challenge = 'Digest realm="%(realm)s", nonce="%(nonce)s", algorithm=%(algorithm)s' % credentials.fields
+                response = (
+                    'Digest username="%(username)s", '
+                    'realm="%(realm)s", '
+                    'nonce="%(nonce)s", '
+                    'uri="%(uri)s", '
+                    'response="%(response)s",'
+                    'algorithm=%(algorithm)s'
+                ) % credentials.fields
+            except KeyError, e:
+                self.log_error(
+                    "Open Directory (node=%s) error while performing digest authentication for user %s: "
+                    "missing digest response field: %s in: %s"
+                    % (self.service.realmName, self.shortNames[0], e, credentials.fields)
+                )
+                return False
+
+            try:
+                if self.digestcache[credentials.fields["uri"]] == response:
+                    return True
+            except (AttributeError, KeyError):
+                pass
+
+            try:
+                if opendirectory.authenticateUserDigest(
+                    self.service.directory,
+                    self.nodeName,
+                    self.shortNames[0],
+                    challenge,
+                    response,
+                    credentials.method
+                ):
+                    try:
+                        cache = self.digestcache
+                    except AttributeError:
+                        cache = self.digestcache = {}
+
+                    cache[credentials.fields["uri"]] = response
+
+                    return True
+                else:
+                    self.log_debug(
+"""Open Directory digest authentication failed with:
+    Nodename:  %s
+    Username:  %s
+    Challenge: %s
+    Response:  %s
+    Method:    %s
+""" % (self.nodeName, self.shortNames[0], challenge, response, credentials.method))
+
+            except opendirectory.ODError, e:
+                self.log_error(
+                    "Open Directory (node=%s) error while performing digest authentication for user %s: %s"
+                    % (self.service.realmName, self.shortNames[0], e)
+                )
+                return False
+
+            return False
+
+        return super(OpenDirectoryRecord, self).verifyCredentials(credentials)
+
+class OpenDirectoryInitError(DirectoryError):
+    """
+    OpenDirectory initialization error.
+    """

Copied: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/oldxmlfile.py (from rev 4198, CalendarServer/trunk/twistedcaldav/directory/oldxmlfile.py)
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/oldxmlfile.py	                        (rev 0)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/oldxmlfile.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -0,0 +1,144 @@
+##
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+XML based user/group/resource directory service implementation.
+"""
+
+__all__ = [
+    "XMLDirectoryService",
+]
+
+from time import time
+
+from twisted.cred.credentials import UsernamePassword
+from twisted.web2.auth.digest import DigestedCredentials
+from twisted.python.filepath import FilePath
+
+from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
+
+class XMLDirectoryService(DirectoryService):
+    """
+    XML based implementation of L{IDirectoryService}.
+    """
+    baseGUID = "9CA8DEC5-5A17-43A9-84A8-BE77C1FB9172"
+
+    realmName = None
+
+    def __repr__(self):
+        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.xmlFile)
+
+    def __init__(self, xmlFile, alwaysStat=False):
+        super(XMLDirectoryService, self).__init__()
+
+        if type(xmlFile) is str:
+            xmlFile = FilePath(xmlFile)
+
+        self.xmlFile = xmlFile
+        self._fileInfo = None
+        self._lastCheck = 0
+        self._alwaysStat = alwaysStat
+        self._accounts()
+
+    def recordTypes(self):
+        recordTypes = (
+            DirectoryService.recordType_users,
+            DirectoryService.recordType_groups,
+            DirectoryService.recordType_locations,
+            DirectoryService.recordType_resources
+        )
+        return recordTypes
+
+    def listRecords(self, recordType):
+        for _ignore_entryShortName, xmlPrincipal in self._entriesForRecordType(recordType):
+            yield XMLDirectoryRecord(
+                service       = self,
+                recordType    = recordType,
+                shortNames    = tuple(xmlPrincipal.shortNames),
+                xmlPrincipal  = xmlPrincipal,
+            )
+
+    def recordWithShortName(self, recordType, shortName):
+        for _ignore_entryShortName, xmlPrincipal in self._entriesForRecordType(recordType):
+            if shortName in xmlPrincipal.shortNames:
+                return XMLDirectoryRecord(
+                    service       = self,
+                    recordType    = recordType,
+                    shortNames    = tuple(xmlPrincipal.shortNames),
+                    xmlPrincipal  = xmlPrincipal,
+                )
+
+        return None
+
+    def _entriesForRecordType(self, recordType):
+        try:
+            for shortName, entry in sorted(self._accounts()[recordType].iteritems(), key=lambda x: x[0]):
+                yield shortName, entry
+        except KeyError:
+            return
+
+    def _accounts(self):
+        currentTime = time()
+        if self._alwaysStat or currentTime - self._lastCheck > 60:
+            self.xmlFile.restat()
+            self._lastCheck = currentTime
+            fileInfo = (self.xmlFile.getmtime(), self.xmlFile.getsize())
+            if fileInfo != self._fileInfo:
+                parser = XMLAccountsParser(self.xmlFile)
+                self._parsedAccounts = parser.items
+                self.realmName = parser.realm
+                self._fileInfo = fileInfo
+        return self._parsedAccounts
+
+
+class XMLDirectoryRecord(DirectoryRecord):
+    """
+    XML based implementation implementation of L{IDirectoryRecord}.
+    """
+    def __init__(self, service, recordType, shortNames, xmlPrincipal):
+        super(XMLDirectoryRecord, self).__init__(
+            service               = service,
+            recordType            = recordType,
+            guid                  = xmlPrincipal.guid,
+            shortNames            = shortNames,
+            fullName              = xmlPrincipal.fullName,
+            firstName             = xmlPrincipal.firstName,
+            lastName              = xmlPrincipal.lastName,
+            emailAddresses        = xmlPrincipal.emailAddresses,
+            calendarUserAddresses = xmlPrincipal.calendarUserAddresses,
+            enabledForCalendaring = xmlPrincipal.enabledForCalendaring,
+        )
+
+        self.password          = xmlPrincipal.password
+        self._members          = xmlPrincipal.members
+        self._groups           = xmlPrincipal.groups
+
+    def members(self):
+        for recordType, shortName in self._members:
+            yield self.service.recordWithShortName(recordType, shortName)
+
+    def groups(self):
+        for shortName in self._groups:
+            yield self.service.recordWithShortName(DirectoryService.recordType_groups, shortName)
+
+    def verifyCredentials(self, credentials):
+        if isinstance(credentials, UsernamePassword):
+            return credentials.password == self.password
+        if isinstance(credentials, DigestedCredentials):
+            return credentials.checkPassword(self.password)
+
+        return super(XMLDirectoryRecord, self).verifyCredentials(credentials)

Modified: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_apache.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_apache.py	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_apache.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -97,7 +97,8 @@
         self.userFile().open("w").write(self.userEntry[1])
         self.assertEquals(self.recordNames(DirectoryService.recordType_users), set((self.userEntry[0],)))
 
-class Basic (Apache, twistedcaldav.directory.test.util.BasicTestCase):
+class Basic (Apache, twistedcaldav.directory.test.util.BasicTestCase,
+    twistedcaldav.directory.test.util.NonCachingTestCase):
     """
     Test Apache-Compatible UserFile/GroupFile directory implementation.
     """
@@ -106,7 +107,8 @@
     userFileName = basicUserFile
     userEntry = ("wsanchez", "wsanchez:Cytm0Bwm7CPJs\n")
 
-class Digest (Apache, twistedcaldav.directory.test.util.DigestTestCase):
+class Digest (Apache, twistedcaldav.directory.test.util.DigestTestCase,
+    twistedcaldav.directory.test.util.NonCachingTestCase):
     """
     Test Apache-Compatible DigestFile/GroupFile directory implementation.
     """

Modified: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_buildquery.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_buildquery.py	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_buildquery.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -15,7 +15,7 @@
 ##
 
 from twistedcaldav.test.util import TestCase
-from twistedcaldav.directory.cachingappleopendirectory import buildQueries, OpenDirectoryService
+from twistedcaldav.directory.appleopendirectory import buildQueries, OpenDirectoryService
 import dsattributes
 
 class BuildQueryTests(TestCase):

Deleted: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_opendirectoryrecords.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_opendirectoryrecords.py	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_opendirectoryrecords.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -1,731 +0,0 @@
-##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from uuid import uuid4
-from twistedcaldav.test.util import TestCase
-
-try:
-    from twistedcaldav.directory.appleopendirectory import OpenDirectoryService as RealOpenDirectoryService
-    import dsattributes
-except ImportError:
-    pass
-else:
-    from twistedcaldav.directory.directory import DirectoryService
-    from twistedcaldav.directory.util import uuidFromName
-
-    class OpenDirectoryService (RealOpenDirectoryService):
-        def _queryDirectory(self, recordType, lookup=None):
-            self._didQuery = True
-            if lookup is None:
-                return self.fakerecords[recordType]
-
-            if lookup[0] == "guid":
-                lookup = (lookup[0], lookup[1].lower(),)
-
-            records = []
-
-            for name, record in self.fakerecords[recordType]:
-                if lookup[0] == "shortName":
-                    if name == lookup[1]:
-                        records.append((name, record))
-                elif lookup[0] == "guid":
-                    if record[dsattributes.kDS1AttrGeneratedUID] == lookup[1]:
-                        records.append((name, record))
-                elif lookup[0] == "email":
-                    if record[dsattributes.kDSNAttrEMailAddress] == lookup[1]:
-                        records.append((name, record))
-
-            return tuple(records)
-    
-    class ReloadCache(TestCase):
-        def setUp(self):
-            super(ReloadCache, self).setUp()
-            self.service = OpenDirectoryService(node="/Search", dosetup=False)
-            
-        def tearDown(self):
-            for call in self.service._delayedCalls:
-                call.cancel()
-
-        def loadRecords(self, records):
-            self.service.fakerecords = records
-
-            for recordType in self.service.recordTypes():
-                if recordType not in records:
-                    self.service.fakerecords[recordType] = []
-                self.service.reloadCache(recordType)
-
-        def verifyRecords(self, recordType, expected, key="records"):
-            expected = set(expected)
-            found = set(self.service._records[recordType][key].keys())
-            
-            missing = expected.difference(found)
-            extras = found.difference(expected)
-
-            self.assertTrue(len(missing) == 0, msg="Directory records not found: %s" % (missing,))
-            self.assertTrue(len(extras) == 0, msg="Directory records not expected: %s" % (extras,))
-                
-        def verifyRecordsCheckEnabled(self, recordType, expected, enabled):
-            expected = set(expected)
-            found = set((
-                item for item in self.service._records[recordType]["records"].iterkeys()
-                if self.service._records[recordType]["records"][item].enabledForCalendaring == enabled
-            ))
-            
-            missing = expected.difference(found)
-            extras = found.difference(expected)
-
-            self.assertTrue(len(missing) == 0, msg="Directory records not found: %s" % (missing,))
-            self.assertTrue(len(extras) == 0, msg="Directory records not expected: %s" % (extras,))
-                
-        def verifyDisabledRecords(self, recordType, expectedNames, expectedGUIDs):
-            def check(disabledType, expected):
-                expected = set(expected)
-                found = self.service._records[recordType][disabledType]
-            
-                missing = expected.difference(found)
-                extras = found.difference(expected)
-
-                self.assertTrue(len(missing) == 0, msg="Disabled directory records not found: %s" % (missing,))
-                self.assertTrue(len(extras) == 0, msg="Disabled directory records not expected: %s" % (extras,))
-
-            check("disabled names", expectedNames)
-            check("disabled guids", (guid.lower() for guid in expectedGUIDs))
-
-        def verifyDisabledNames(self, recordType, expectedNames):
-            def check(disabledType, expected):
-                expected = set(expected)
-                found = self.service._records[recordType][disabledType]
-            
-                missing = expected.difference(found)
-                extras = found.difference(expected)
-
-                self.assertTrue(len(missing) == 0, msg="Disabled directory records not found: %s" % (missing,))
-                self.assertTrue(len(extras) == 0, msg="Disabled directory records not expected: %s" % (extras,))
-
-            check("disabled names", expectedNames)
-
-        def verifyQuery(self, f, *args):
-            try:
-                delattr(self.service, "_didQuery")
-            except AttributeError:
-                pass
-            self.assertFalse(f(*args))
-            self.assertTrue(hasattr(self.service, "_didQuery"))
-
-        def verifyNoQuery(self, f, *args):
-            try:
-                delattr(self.service, "_didQuery")
-            except AttributeError:
-                pass
-            self.assertFalse(f(*args))
-            self.assertFalse(hasattr(self.service, "_didQuery"))
-
-        def test_restrictionGroupName(self):
-            service = OpenDirectoryService(
-                node="/Search",
-                restrictEnabledRecords=True,
-                restrictToGroup="group_name",
-                dosetup=False)
-            self.assertTrue(service.restrictEnabledRecords)
-            self.assertEqual(service.restrictToGroup, "group_name")
-            self.assertFalse(service.restrictToGUID)
-
-        def test_restrictionGroupGUID(self):
-            guid = str(uuid4())
-            service = OpenDirectoryService(
-                node="/Search",
-                restrictEnabledRecords=True,
-                restrictToGroup=guid,
-                dosetup=False)
-            self.assertTrue(service.restrictEnabledRecords)
-            self.assertEqual(service.restrictToGroup, guid)
-            self.assertTrue(service.restrictToGUID)
-
-        def test_normal(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                ],
-                DirectoryService.recordType_groups: [
-                    fakeODRecord("Group 01"),
-                    fakeODRecord("Group 02"),
-                ],
-                DirectoryService.recordType_resources: [
-                    fakeODRecord("Resource 01"),
-                    fakeODRecord("Resource 02"),
-                ],
-                DirectoryService.recordType_locations: [
-                    fakeODRecord("Location 01"),
-                    fakeODRecord("Location 02"),
-                ],
-            })
-
-            self.verifyRecords(DirectoryService.recordType_users, ("user01", "user02"))
-            self.verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
-            self.verifyRecords(DirectoryService.recordType_groups, ("group01", "group02"))
-            self.verifyDisabledRecords(DirectoryService.recordType_groups, (), ())
-
-            self.verifyRecords(DirectoryService.recordType_resources, ("resource01", "resource02"))
-            self.verifyDisabledRecords(DirectoryService.recordType_resources, (), ())
-
-            self.verifyRecords(DirectoryService.recordType_locations, ("location01", "location02"))
-            self.verifyDisabledRecords(DirectoryService.recordType_locations, (), ())
-
-        def test_normalDisabledUsers(self):
-            self.service.restrictEnabledRecords = True
-            self.service.restrictToGroup = "restrictedaccess"
-
-            self.service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 03"),
-                    fakeODRecord("User 04"),
-                ],
-                DirectoryService.recordType_groups: [
-                    fakeODRecord("Group 01"),
-                    fakeODRecord("Group 02"),
-                    fakeODRecord("Group 03"),
-                    fakeODRecord("Group 04"),
-                ],
-                DirectoryService.recordType_resources: [
-                    fakeODRecord("Resource 01"),
-                    fakeODRecord("Resource 02"),
-                    fakeODRecord("Resource 03"),
-                    fakeODRecord("Resource 04"),
-                ],
-                DirectoryService.recordType_locations: [
-                    fakeODRecord("Location 01"),
-                    fakeODRecord("Location 02"),
-                    fakeODRecord("Location 03"),
-                    fakeODRecord("Location 04"),
-                ],
-            }
-
-            # Disable certain records
-            self.service.restrictedGUIDs = set((
-                self.service.fakerecords[DirectoryService.recordType_users    ][0][1][dsattributes.kDS1AttrGeneratedUID],
-                self.service.fakerecords[DirectoryService.recordType_users    ][1][1][dsattributes.kDS1AttrGeneratedUID],
-                self.service.fakerecords[DirectoryService.recordType_resources][0][1][dsattributes.kDS1AttrGeneratedUID],
-                self.service.fakerecords[DirectoryService.recordType_resources][1][1][dsattributes.kDS1AttrGeneratedUID],
-                self.service.fakerecords[DirectoryService.recordType_locations][0][1][dsattributes.kDS1AttrGeneratedUID],
-                self.service.fakerecords[DirectoryService.recordType_locations][1][1][dsattributes.kDS1AttrGeneratedUID],
-                self.service.fakerecords[DirectoryService.recordType_groups   ][0][1][dsattributes.kDS1AttrGeneratedUID],
-                self.service.fakerecords[DirectoryService.recordType_groups   ][1][1][dsattributes.kDS1AttrGeneratedUID],
-            ))
-
-            self.service.reloadCache(DirectoryService.recordType_users)
-            self.service.reloadCache(DirectoryService.recordType_groups)
-            self.service.reloadCache(DirectoryService.recordType_resources)
-            self.service.reloadCache(DirectoryService.recordType_locations)
-
-            self.verifyRecordsCheckEnabled(DirectoryService.recordType_users, ("user01", "user02"), True)
-            self.verifyRecordsCheckEnabled(DirectoryService.recordType_users, ("user03", "user04"), False)
-
-            # Groups are always disabled
-            #self.verifyRecordsCheckEnabled(DirectoryService.recordType_groups, ("group01", "group02"), True)
-            #self.verifyRecordsCheckEnabled(DirectoryService.recordType_groups, ("group03", "group04"), False)
-
-            self.verifyRecordsCheckEnabled(DirectoryService.recordType_resources, ("resource01", "resource02"), True)
-            self.verifyRecordsCheckEnabled(DirectoryService.recordType_resources, (), False)
-
-            self.verifyRecordsCheckEnabled(DirectoryService.recordType_locations, ("location01", "location02"), True)
-            self.verifyRecordsCheckEnabled(DirectoryService.recordType_locations, (), False)
-
-        def test_normalCacheMiss(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                ],
-            })
-
-            self.verifyRecords(DirectoryService.recordType_users, ("user01",))
-            self.verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
-            self.service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 03", guid="D10F3EE0-5014-41D3-8488-3819D3EF3B2A"),
-                ],
-            }
-
-            self.service.reloadCache(DirectoryService.recordType_users, lookup=("shortName", "user02",))
-            self.service.reloadCache(DirectoryService.recordType_users, lookup=("guid", "D10F3EE0-5014-41D3-8488-3819D3EF3B2A",))
-
-            self.verifyRecords(DirectoryService.recordType_users, ("user01", "user02", "user03"))
-            self.verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
-        def test_noGUID(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01", guid=""),
-                ],
-            })
-
-            self.verifyRecords(DirectoryService.recordType_users, ())
-
-        def test_systemRecord(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("root",   guid="FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000"),
-                    fakeODRecord("daemon", guid="FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000001"),
-                    fakeODRecord("uucp",   guid="ffffeeee-dddd-cccc-bbbb-aaaa00000004"), # Try lowercase also
-                    fakeODRecord("nobody", guid="ffffeeee-dddd-cccc-bbbb-aaaafffffffe"),
-                ],
-            })
-
-            self.verifyRecords(DirectoryService.recordType_users, ())
-
-        def test_duplicateAuthIDs(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02", email="shared at example.com"),
-                    fakeODRecord("User 03", email="shared at example.com"),
-                ],
-            })
-
-            self.verifyRecords(DirectoryService.recordType_users, ("user01", "user02", "user03"))
-            self.verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
-            self.assertTrue (self.service.recordWithShortName(DirectoryService.recordType_users, "user01").authIDs)
-            self.assertFalse(self.service.recordWithShortName(DirectoryService.recordType_users, "user02").authIDs)
-            self.assertFalse(self.service.recordWithShortName(DirectoryService.recordType_users, "user03").authIDs)
-
-        def test_duplicateCUAs(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02", email="shared at example.com"),
-                    fakeODRecord("User 03", email="shared at example.com"),
-                ],
-            })
-
-            self.verifyRecords(DirectoryService.recordType_users, ("user01", "user02", "user03"))
-            self.verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
-            self.assertTrue (self.service.recordWithShortName(DirectoryService.recordType_users, "user01").calendarUserAddresses)
-            self.assertFalse("mailto:shared at example.com" in self.service.recordWithShortName(DirectoryService.recordType_users, "user02").calendarUserAddresses)
-            self.assertFalse("mailto:shared at example.com" in self.service.recordWithShortName(DirectoryService.recordType_users, "user03").calendarUserAddresses)
-
-        def test_duplicateRecords(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 02"),
-                ],
-            })
-
-            self.verifyRecords(DirectoryService.recordType_users, ("user01", "user02"))
-            self.verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-            self.verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
-        def test_duplicateName(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02", guid="A25775BB-1281-4606-98C6-2893B2D5CCD7"),
-                    fakeODRecord("User 02", guid="30CA2BB9-C935-4A5D-80E2-79266BCB0255"),
-                ],
-            })
-
-            self.verifyRecords(DirectoryService.recordType_users, ("user01",))
-            self.verifyRecords(
-                DirectoryService.recordType_users,
-                (
-                    guidForShortName("user01"),
-                    "A25775BB-1281-4606-98C6-2893B2D5CCD7".lower(),
-                    "30CA2BB9-C935-4A5D-80E2-79266BCB0255".lower(),
-                ),
-                key="guids",
-            )
-            self.verifyDisabledNames(
-                DirectoryService.recordType_users,
-                ("user02",),
-            )
-
-        def test_duplicateGUID(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02", guid="113D7F74-F84A-4F17-8C96-CE8F10D68EF8"),
-                    fakeODRecord("User 03", guid="113D7F74-F84A-4F17-8C96-CE8F10D68EF8"),
-                ],
-            })
-
-            self.verifyRecords(DirectoryService.recordType_users, ("user01",))
-            self.verifyDisabledRecords(
-                DirectoryService.recordType_users,
-                ("user02", "user03"),
-                ("113D7F74-F84A-4F17-8C96-CE8F10D68EF8",),
-            )
-
-        def test_duplicateCombo(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02", guid="113D7F74-F84A-4F17-8C96-CE8F10D68EF8"),
-                    fakeODRecord("User 02", guid="113D7F74-F84A-4F17-8C96-CE8F10D68EF8", shortName="user03"),
-                    fakeODRecord("User 02", guid="136E369F-DB40-4135-878D-B75D38242D39"),
-                ],
-            })
-
-            self.verifyRecords(DirectoryService.recordType_users, ("user01",))
-            self.verifyDisabledRecords(
-                DirectoryService.recordType_users,
-                ("user02", "user03"),
-                ("113D7F74-F84A-4F17-8C96-CE8F10D68EF8",),
-            )
-
-        def test_duplicateGUIDCacheMiss(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02", guid="EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E"),
-                    fakeODRecord("User 03", guid="D10F3EE0-5014-41D3-8488-3819D3EF3B2A"),
-                ],
-            })
-
-            self.verifyRecords(DirectoryService.recordType_users, ("user01", "user02", "user03"))
-            self.verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-            
-            self.service.fakerecords = {
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02", guid="EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E"),
-                    fakeODRecord("User 02", guid="EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E", shortName="user04"),
-                    fakeODRecord("User 03", guid="62368DDF-0C62-4C97-9A58-DE9FD46131A0"),
-                    fakeODRecord("User 03", guid="62368DDF-0C62-4C97-9A58-DE9FD46131A0", shortName="user05"),
-                ],
-            }
-
-            self.service.reloadCache(DirectoryService.recordType_users, lookup=("shortName", "user04",))
-            self.service.reloadCache(DirectoryService.recordType_users, lookup=("guid", "62368DDF-0C62-4C97-9A58-DE9FD46131A0",))
-
-            self.verifyRecords(DirectoryService.recordType_users, ("user01",))
-            self.verifyDisabledRecords(
-                DirectoryService.recordType_users,
-                ("user02", "user03", "user04", "user05"),
-                ("EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E", "62368DDF-0C62-4C97-9A58-DE9FD46131A0",),
-            )
-
-        def test_groupMembers(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                ],
-                DirectoryService.recordType_groups: [
-                    fakeODRecord("Group 01", members=[
-                        guidForShortName("user01"),
-                        guidForShortName("user02"),
-                    ]),
-                    fakeODRecord("Group 02", members=[
-                        guidForShortName("resource01"),
-                        guidForShortName("user02"),
-                    ]),
-                ],
-                DirectoryService.recordType_resources: [
-                    fakeODRecord("Resource 01"),
-                    fakeODRecord("Resource 02"),
-                ],
-                DirectoryService.recordType_locations: [
-                    fakeODRecord("Location 01"),
-                    fakeODRecord("Location 02"),
-                ],
-            })
-
-            group1 = self.service.recordWithShortName(DirectoryService.recordType_groups, "group01")
-            self.assertTrue(group1 is not None)
-
-            group2 = self.service.recordWithShortName(DirectoryService.recordType_groups, "group02")
-            self.assertTrue(group2 is not None)
-
-            user1 = self.service.recordWithShortName(DirectoryService.recordType_users, "user01")
-            self.assertTrue(user1 is not None)
-            self.assertEqual(set((group1,)), user1.groups()) 
-            
-            user2 = self.service.recordWithShortName(DirectoryService.recordType_users, "user02")
-            self.assertTrue(user2 is not None)
-            self.assertEqual(set((group1, group2)), user2.groups()) 
-            
-            self.service.fakerecords[DirectoryService.recordType_groups] = [
-                fakeODRecord("Group 01", members=[
-                    guidForShortName("user01"),
-                ]),
-                fakeODRecord("Group 02", members=[
-                    guidForShortName("resource01"),
-                    guidForShortName("user02"),
-                ]),
-            ]
-            self.service.reloadCache(DirectoryService.recordType_groups)
-
-            group1 = self.service.recordWithShortName(DirectoryService.recordType_groups, "group01")
-            self.assertTrue(group1 is not None)
-
-            group2 = self.service.recordWithShortName(DirectoryService.recordType_groups, "group02")
-            self.assertTrue(group2 is not None)
-
-            user1 = self.service.recordWithShortName(DirectoryService.recordType_users, "user01")
-            self.assertTrue(user1 is not None)
-            self.assertEqual(set((group1,)), user1.groups()) 
-            
-            user2 = self.service.recordWithShortName(DirectoryService.recordType_users, "user02")
-            self.assertTrue(user2 is not None)
-            self.assertEqual(set((group2,)), user2.groups()) 
-            
-            self.service.fakerecords[DirectoryService.recordType_groups] = [
-                fakeODRecord("Group 03", members=[
-                    guidForShortName("user01"),
-                    guidForShortName("user02"),
-                ]),
-            ]
-            self.service.reloadCache(DirectoryService.recordType_groups, lookup=("guid", guidForShortName("group03"),))
-
-            group1 = self.service.recordWithShortName(DirectoryService.recordType_groups, "group01")
-            self.assertTrue(group1 is not None)
-
-            group2 = self.service.recordWithShortName(DirectoryService.recordType_groups, "group02")
-            self.assertTrue(group2 is not None)
-
-            group3 = self.service.recordWithShortName(DirectoryService.recordType_groups, "group03")
-            self.assertTrue(group2 is not None)
-
-            user1 = self.service.recordWithShortName(DirectoryService.recordType_users, "user01")
-            self.assertTrue(user1 is not None)
-            self.assertEqual(set((group1, group3)), user1.groups()) 
-            
-            user2 = self.service.recordWithShortName(DirectoryService.recordType_users, "user02")
-            self.assertTrue(user2 is not None)
-            self.assertEqual(set((group2, group3)), user2.groups()) 
-
-        def test_negativeCacheShortname(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 03"),
-                    fakeODRecord("User 04"),
-                ],
-                DirectoryService.recordType_groups: [
-                    fakeODRecord("Group 01"),
-                    fakeODRecord("Group 02"),
-                    fakeODRecord("Group 03"),
-                    fakeODRecord("Group 04"),
-                ],
-                DirectoryService.recordType_resources: [
-                    fakeODRecord("Resource 01"),
-                    fakeODRecord("Resource 02"),
-                    fakeODRecord("Resource 03"),
-                    fakeODRecord("Resource 04"),
-                ],
-                DirectoryService.recordType_locations: [
-                    fakeODRecord("Location 01"),
-                    fakeODRecord("Location 02"),
-                    fakeODRecord("Location 03"),
-                    fakeODRecord("Location 04"),
-                ],
-            })
-
-            self.assertTrue(self.service.recordWithShortName(DirectoryService.recordType_users, "user01"))
-            self.verifyQuery(self.service.recordWithShortName, DirectoryService.recordType_users, "user05")
-            self.verifyNoQuery(self.service.recordWithShortName, DirectoryService.recordType_users, "user05")
-
-            self.assertTrue(self.service.recordWithShortName(DirectoryService.recordType_groups, "group01"))
-            self.verifyQuery(self.service.recordWithShortName, DirectoryService.recordType_groups, "group05")
-            self.verifyNoQuery(self.service.recordWithShortName, DirectoryService.recordType_groups, "group05")
-
-            self.assertTrue(self.service.recordWithShortName(DirectoryService.recordType_resources, "resource01"))
-            self.verifyQuery(self.service.recordWithShortName, DirectoryService.recordType_resources, "resource05")
-            self.verifyNoQuery(self.service.recordWithShortName, DirectoryService.recordType_resources, "resource05")
-
-            self.assertTrue(self.service.recordWithShortName(DirectoryService.recordType_locations, "location01"))
-            self.verifyQuery(self.service.recordWithShortName, DirectoryService.recordType_locations, "location05")
-            self.verifyNoQuery(self.service.recordWithShortName, DirectoryService.recordType_locations, "location05")
-
-        def test_negativeCacheGUID(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 03"),
-                    fakeODRecord("User 04"),
-                ],
-                DirectoryService.recordType_groups: [
-                    fakeODRecord("Group 01"),
-                    fakeODRecord("Group 02"),
-                    fakeODRecord("Group 03"),
-                    fakeODRecord("Group 04"),
-                ],
-                DirectoryService.recordType_resources: [
-                    fakeODRecord("Resource 01"),
-                    fakeODRecord("Resource 02"),
-                    fakeODRecord("Resource 03"),
-                    fakeODRecord("Resource 04"),
-                ],
-                DirectoryService.recordType_locations: [
-                    fakeODRecord("Location 01"),
-                    fakeODRecord("Location 02"),
-                    fakeODRecord("Location 03"),
-                    fakeODRecord("Location 04"),
-                ],
-            })
-
-            self.assertTrue(self.service.recordWithGUID(guidForShortName("user01")))
-            self.verifyQuery(self.service.recordWithGUID, guidForShortName("user05"))
-            self.verifyNoQuery(self.service.recordWithGUID, guidForShortName("user05"))
-
-            self.assertTrue(self.service.recordWithGUID(guidForShortName("group01")))
-            self.verifyQuery(self.service.recordWithGUID, guidForShortName("group05"))
-            self.verifyNoQuery(self.service.recordWithGUID, guidForShortName("group05"))
-
-            self.assertTrue(self.service.recordWithGUID(guidForShortName("resource01")))
-            self.verifyQuery(self.service.recordWithGUID, guidForShortName("resource05"))
-            self.verifyNoQuery(self.service.recordWithGUID, guidForShortName("resource05"))
-
-            self.assertTrue(self.service.recordWithGUID(guidForShortName("location01")))
-            self.verifyQuery(self.service.recordWithGUID, guidForShortName("location05"))
-            self.verifyNoQuery(self.service.recordWithGUID, guidForShortName("location05"))
-
-        def test_negativeCacheAuthID(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 03"),
-                    fakeODRecord("User 04"),
-                ],
-                DirectoryService.recordType_groups: [
-                    fakeODRecord("Group 01"),
-                    fakeODRecord("Group 02"),
-                    fakeODRecord("Group 03"),
-                    fakeODRecord("Group 04"),
-                ],
-                DirectoryService.recordType_resources: [
-                    fakeODRecord("Resource 01"),
-                    fakeODRecord("Resource 02"),
-                    fakeODRecord("Resource 03"),
-                    fakeODRecord("Resource 04"),
-                ],
-                DirectoryService.recordType_locations: [
-                    fakeODRecord("Location 01"),
-                    fakeODRecord("Location 02"),
-                    fakeODRecord("Location 03"),
-                    fakeODRecord("Location 04"),
-                ],
-            })
-
-            self.assertTrue(self.service.recordWithAuthID("Kerberos:user01 at example.com"))
-            self.verifyQuery(self.service.recordWithAuthID, "Kerberos:user05 at example.com")
-            self.verifyNoQuery(self.service.recordWithAuthID, "Kerberos:user05 at example.com")
-
-            self.assertTrue(self.service.recordWithAuthID("Kerberos:group01 at example.com"))
-            self.verifyQuery(self.service.recordWithAuthID, "Kerberos:group05 at example.com")
-            self.verifyNoQuery(self.service.recordWithAuthID, "Kerberos:group05 at example.com")
-
-            self.assertTrue(self.service.recordWithAuthID("Kerberos:resource01 at example.com"))
-            self.verifyQuery(self.service.recordWithAuthID, "Kerberos:resource05 at example.com")
-            self.verifyNoQuery(self.service.recordWithAuthID, "Kerberos:resource05 at example.com")
-
-            self.assertTrue(self.service.recordWithAuthID("Kerberos:location01 at example.com"))
-            self.verifyQuery(self.service.recordWithAuthID, "Kerberos:location05 at example.com")
-            self.verifyNoQuery(self.service.recordWithAuthID, "Kerberos:location05 at example.com")
-
-        def test_negativeCacheCalendarUserAddress(self):
-            self.loadRecords({
-                DirectoryService.recordType_users: [
-                    fakeODRecord("User 01"),
-                    fakeODRecord("User 02"),
-                    fakeODRecord("User 03"),
-                    fakeODRecord("User 04"),
-                ],
-                DirectoryService.recordType_groups: [
-                    fakeODRecord("Group 01"),
-                    fakeODRecord("Group 02"),
-                    fakeODRecord("Group 03"),
-                    fakeODRecord("Group 04"),
-                ],
-                DirectoryService.recordType_resources: [
-                    fakeODRecord("Resource 01"),
-                    fakeODRecord("Resource 02"),
-                    fakeODRecord("Resource 03"),
-                    fakeODRecord("Resource 04"),
-                ],
-                DirectoryService.recordType_locations: [
-                    fakeODRecord("Location 01"),
-                    fakeODRecord("Location 02"),
-                    fakeODRecord("Location 03"),
-                    fakeODRecord("Location 04"),
-                ],
-            })
-
-            self.assertTrue(self.service.recordWithCalendarUserAddress("mailto:user01 at example.com"))
-            self.verifyQuery(self.service.recordWithCalendarUserAddress, "mailto:user05 at example.com")
-            self.verifyNoQuery(self.service.recordWithCalendarUserAddress, "mailto:user05 at example.com")
-
-            # Groups don't have CUAs
-            # self.assertTrue(self.service.recordWithCalendarUserAddress("mailto:group01 at example.com"))
-            # self.verifyQuery(self.service.recordWithCalendarUserAddress, "mailto:group05 at example.com")
-            # self.verifyNoQuery(self.service.recordWithCalendarUserAddress, "mailto:group05 at example.com")
-
-            self.assertTrue(self.service.recordWithCalendarUserAddress("mailto:resource01 at example.com"))
-            self.verifyQuery(self.service.recordWithCalendarUserAddress, "mailto:resource05 at example.com")
-            self.verifyNoQuery(self.service.recordWithCalendarUserAddress, "mailto:resource05 at example.com")
-
-            self.assertTrue(self.service.recordWithCalendarUserAddress("mailto:location01 at example.com"))
-            self.verifyQuery(self.service.recordWithCalendarUserAddress, "mailto:location05 at example.com")
-            self.verifyNoQuery(self.service.recordWithCalendarUserAddress, "mailto:location05 at example.com")
-
-
-
-def fakeODRecord(fullName, shortName=None, guid=None, email=None, members=None, resourceInfo=None):
-    if shortName is None:
-        shortName = shortNameForFullName(fullName)
-
-    if guid is None:
-        guid = guidForShortName(shortName)
-    else:
-        guid = guid.lower()
-
-    if email is None:
-        email = "%s at example.com" % (shortName,)
-
-    attrs = {
-        dsattributes.kDS1AttrDistinguishedName: fullName,
-        dsattributes.kDS1AttrGeneratedUID: guid,
-        dsattributes.kDSNAttrRecordName: shortName,
-        dsattributes.kDSNAttrAltSecurityIdentities: "Kerberos:%s" % (email,),
-        dsattributes.kDSNAttrEMailAddress: email,
-        dsattributes.kDSNAttrMetaNodeLocation: "/LDAPv3/127.0.0.1",
-    }
-    
-    if members:
-        attrs[dsattributes.kDSNAttrGroupMembers] = members
-
-    if resourceInfo:
-        attrs[dsattributes.kDSNAttrResourceInfo] = resourceInfo
-
-    return [ shortName, attrs ]
-
-def shortNameForFullName(fullName):
-    return fullName.lower().replace(" ", "")
-
-def guidForShortName(shortName):
-    return uuidFromName(OpenDirectoryService.baseGUID, shortName)

Modified: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -420,10 +420,20 @@
                 self.assertEquals(len(members), 2)
 
                 # Remove the dreid user from the directory service
-                del self.directoryService._accounts()[DirectoryService.recordType_users]["dreid"]
 
+                delRec = self.directoryService.recordWithShortName(
+                    DirectoryService.recordType_users, "dreid")
+                for cache in self.directoryService._recordCaches.itervalues():
+                   cache.removeRecord(delRec)
+                del self.directoryService._accounts()[
+                    DirectoryService.recordType_users]["dreid"]
+
+
+                cacheTimeout = config.DirectoryService.params.get("cacheTimeout", 30) * 60 * 2
+
                 @inlineCallbacks
                 def _membershipTest():
+
                     uids = [p.principalUID() for p in (yield testPrincipal.groupMemberships())]
                     self.assertTrue("5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1#%s" % (proxyType,) not in uids)
 
@@ -454,6 +464,8 @@
                     # Restore removed user
                     parser = XMLAccountsParser(self.directoryService.xmlFile)
                     self.directoryService._parsedAccounts = parser.items
+                    self.directoryService.recordWithShortName(
+                        DirectoryService.recordType_users, "dreid")
 
                     # Trigger the proxy DB clean up, which will actually
                     # remove the deletion timer because the principal has been
@@ -465,13 +477,17 @@
                     self.assertEquals(result, None)
 
                     # Remove the dreid user from the directory service
-                    del self.directoryService._accounts()[DirectoryService.recordType_users]["dreid"]
+                    delRec = self.directoryService.recordWithShortName(
+                        DirectoryService.recordType_users, "dreid")
+                    for cache in self.directoryService._recordCaches.itervalues():
+                       cache.removeRecord(delRec)
+                    del self.directoryService._accounts()[
+                        DirectoryService.recordType_users]["dreid"]
 
                     # Trigger the proxy DB clean up, which won't actually
                     # remove anything because we haven't exceeded the timeout
                     yield proxyGroup.groupMembers()
 
-                    cacheTimeout = config.DirectoryService.params.get("cacheTimeout", 30) * 60 * 2
                     # Advance beyond the timeout
                     theTime += cacheTimeout
                     db._memcacher.theTime = theTime

Modified: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_xmlfile.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_xmlfile.py	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/test_xmlfile.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -121,6 +121,10 @@
             ( DirectoryService.recordType_locations , ()         ),
             ( DirectoryService.recordType_resources , ()         ),
         ):
+            # Fault records in
+            for name in expectedRecords:
+                service.recordWithShortName(recordType, name)
+
             self.assertEquals(
                 set(r.shortNames[0] for r in service.listRecords(recordType)),
                 set(expectedRecords)
@@ -150,6 +154,10 @@
             ( DirectoryService.recordType_locations , ("my office",) ),
             ( DirectoryService.recordType_resources , ()             ),
         ):
+            # Fault records in
+            for name in expectedRecords:
+                service.recordWithShortName(recordType, name)
+
             self.assertEquals(
                 set(r.shortNames[0] for r in service.listRecords(recordType)),
                 set(expectedRecords)
@@ -185,6 +193,10 @@
             ( DirectoryService.recordType_locations , ()                       ),
             ( DirectoryService.recordType_resources , ()                       ),
         ):
+            # Fault records in
+            for name in expectedRecords:
+                service.recordWithShortName(recordType, name)
+
             self.assertEquals(
                 set(r.shortNames[0] for r in service.listRecords(recordType)),
                 set(expectedRecords)
@@ -227,6 +239,10 @@
             ( DirectoryService.recordType_locations , ("my office",) ),
             ( DirectoryService.recordType_resources , ()             ),
         ):
+            # Fault records in
+            for name in expectedRecords:
+                service.recordWithShortName(recordType, name)
+
             self.assertEquals(
                 set(r.shortNames[0] for r in service.listRecords(recordType)),
                 set(expectedRecords)

Modified: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/util.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/util.py	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/test/util.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -65,42 +65,6 @@
 
         self.assertEquals(set(self.service().recordTypes()), self.recordTypes)
 
-    def test_listRecords_user(self):
-        """
-        IDirectoryService.listRecords(DirectoryService.recordType_users)
-        """
-        if not self.users:
-            raise SkipTest("No users")
-
-        self.assertEquals(self.recordNames(DirectoryService.recordType_users), set(self.users.keys()))
-
-    def test_listRecords_group(self):
-        """
-        IDirectoryService.listRecords(DirectoryService.recordType_groups)
-        """
-        if not self.groups:
-            raise SkipTest("No groups")
-
-        self.assertEquals(self.recordNames(DirectoryService.recordType_groups), set(self.groups.keys()))
-
-    def test_listRecords_locations(self):
-        """
-        IDirectoryService.listRecords("locations")
-        """
-        if not self.resources:
-            raise SkipTest("No locations")
-
-        self.assertEquals(self.recordNames(DirectoryService.recordType_locations), set(self.locations.keys()))
-
-    def test_listRecords_resources(self):
-        """
-        IDirectoryService.listRecords("resources")
-        """
-        if not self.resources:
-            raise SkipTest("No resources")
-
-        self.assertEquals(self.recordNames(DirectoryService.recordType_resources), set(self.resources.keys()))
-
     def test_recordWithShortName(self):
         """
         IDirectoryService.recordWithShortName()
@@ -254,6 +218,45 @@
         else:
             return ""
 
+class NonCachingTestCase (DirectoryTestCase):
+
+    def test_listRecords_user(self):
+        """
+        IDirectoryService.listRecords(DirectoryService.recordType_users)
+        """
+        if not self.users:
+            raise SkipTest("No users")
+
+        self.assertEquals(self.recordNames(DirectoryService.recordType_users), set(self.users.keys()))
+
+    def test_listRecords_group(self):
+        """
+        IDirectoryService.listRecords(DirectoryService.recordType_groups)
+        """
+        if not self.groups:
+            raise SkipTest("No groups")
+
+        self.assertEquals(self.recordNames(DirectoryService.recordType_groups), set(self.groups.keys()))
+
+    def test_listRecords_locations(self):
+        """
+        IDirectoryService.listRecords("locations")
+        """
+        if not self.resources:
+            raise SkipTest("No locations")
+
+        self.assertEquals(self.recordNames(DirectoryService.recordType_locations), set(self.locations.keys()))
+
+    def test_listRecords_resources(self):
+        """
+        IDirectoryService.listRecords("resources")
+        """
+        if not self.resources:
+            raise SkipTest("No resources")
+
+        self.assertEquals(self.recordNames(DirectoryService.recordType_resources), set(self.resources.keys()))
+
+
 class BasicTestCase (DirectoryTestCase):
     """
     Tests a directory implementation with basic auth.

Deleted: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/xmlfile.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/xmlfile.py	2009-05-11 21:11:02 UTC (rev 4228)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/xmlfile.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -1,144 +0,0 @@
-##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-XML based user/group/resource directory service implementation.
-"""
-
-__all__ = [
-    "XMLDirectoryService",
-]
-
-from time import time
-
-from twisted.cred.credentials import UsernamePassword
-from twisted.web2.auth.digest import DigestedCredentials
-from twisted.python.filepath import FilePath
-
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
-
-class XMLDirectoryService(DirectoryService):
-    """
-    XML based implementation of L{IDirectoryService}.
-    """
-    baseGUID = "9CA8DEC5-5A17-43A9-84A8-BE77C1FB9172"
-
-    realmName = None
-
-    def __repr__(self):
-        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.xmlFile)
-
-    def __init__(self, xmlFile, alwaysStat=False):
-        super(XMLDirectoryService, self).__init__()
-
-        if type(xmlFile) is str:
-            xmlFile = FilePath(xmlFile)
-
-        self.xmlFile = xmlFile
-        self._fileInfo = None
-        self._lastCheck = 0
-        self._alwaysStat = alwaysStat
-        self._accounts()
-
-    def recordTypes(self):
-        recordTypes = (
-            DirectoryService.recordType_users,
-            DirectoryService.recordType_groups,
-            DirectoryService.recordType_locations,
-            DirectoryService.recordType_resources
-        )
-        return recordTypes
-
-    def listRecords(self, recordType):
-        for _ignore_entryShortName, xmlPrincipal in self._entriesForRecordType(recordType):
-            yield XMLDirectoryRecord(
-                service       = self,
-                recordType    = recordType,
-                shortNames    = tuple(xmlPrincipal.shortNames),
-                xmlPrincipal  = xmlPrincipal,
-            )
-
-    def recordWithShortName(self, recordType, shortName):
-        for _ignore_entryShortName, xmlPrincipal in self._entriesForRecordType(recordType):
-            if shortName in xmlPrincipal.shortNames:
-                return XMLDirectoryRecord(
-                    service       = self,
-                    recordType    = recordType,
-                    shortNames    = tuple(xmlPrincipal.shortNames),
-                    xmlPrincipal  = xmlPrincipal,
-                )
-
-        return None
-
-    def _entriesForRecordType(self, recordType):
-        try:
-            for shortName, entry in sorted(self._accounts()[recordType].iteritems(), key=lambda x: x[0]):
-                yield shortName, entry
-        except KeyError:
-            return
-
-    def _accounts(self):
-        currentTime = time()
-        if self._alwaysStat or currentTime - self._lastCheck > 60:
-            self.xmlFile.restat()
-            self._lastCheck = currentTime
-            fileInfo = (self.xmlFile.getmtime(), self.xmlFile.getsize())
-            if fileInfo != self._fileInfo:
-                parser = XMLAccountsParser(self.xmlFile)
-                self._parsedAccounts = parser.items
-                self.realmName = parser.realm
-                self._fileInfo = fileInfo
-        return self._parsedAccounts
-
-
-class XMLDirectoryRecord(DirectoryRecord):
-    """
-    XML based implementation implementation of L{IDirectoryRecord}.
-    """
-    def __init__(self, service, recordType, shortNames, xmlPrincipal):
-        super(XMLDirectoryRecord, self).__init__(
-            service               = service,
-            recordType            = recordType,
-            guid                  = xmlPrincipal.guid,
-            shortNames            = shortNames,
-            fullName              = xmlPrincipal.fullName,
-            firstName             = xmlPrincipal.firstName,
-            lastName              = xmlPrincipal.lastName,
-            emailAddresses        = xmlPrincipal.emailAddresses,
-            calendarUserAddresses = xmlPrincipal.calendarUserAddresses,
-            enabledForCalendaring = xmlPrincipal.enabledForCalendaring,
-        )
-
-        self.password          = xmlPrincipal.password
-        self._members          = xmlPrincipal.members
-        self._groups           = xmlPrincipal.groups
-
-    def members(self):
-        for recordType, shortName in self._members:
-            yield self.service.recordWithShortName(recordType, shortName)
-
-    def groups(self):
-        for shortName in self._groups:
-            yield self.service.recordWithShortName(DirectoryService.recordType_groups, shortName)
-
-    def verifyCredentials(self, credentials):
-        if isinstance(credentials, UsernamePassword):
-            return credentials.password == self.password
-        if isinstance(credentials, DigestedCredentials):
-            return credentials.checkPassword(self.password)
-
-        return super(XMLDirectoryRecord, self).verifyCredentials(credentials)

Copied: CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/xmlfile.py (from rev 4199, CalendarServer/trunk/twistedcaldav/directory/xmlfile.py)
===================================================================
--- CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/xmlfile.py	                        (rev 0)
+++ CalendarServer/branches/release/CalendarServer-2.2-dev/twistedcaldav/directory/xmlfile.py	2009-05-11 21:38:13 UTC (rev 4229)
@@ -0,0 +1,208 @@
+##
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+XML based user/group/resource directory service implementation.
+"""
+
+__all__ = [
+    "XMLDirectoryService",
+]
+
+from time import time
+import types
+
+from twisted.cred.credentials import UsernamePassword
+from twisted.web2.auth.digest import DigestedCredentials
+from twisted.python.filepath import FilePath
+
+from twistedcaldav.directory.directory import DirectoryService
+from twistedcaldav.directory.cachingdirectory import CachingDirectoryService,\
+    CachingDirectoryRecord
+from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
+
+class XMLDirectoryService(CachingDirectoryService):
+    """
+    XML based implementation of L{IDirectoryService}.
+    """
+    baseGUID = "9CA8DEC5-5A17-43A9-84A8-BE77C1FB9172"
+
+    realmName = None
+
+    def __repr__(self):
+        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.xmlFile)
+
+    def __init__(self, xmlFile, alwaysStat=False):
+        super(XMLDirectoryService, self).__init__()
+
+        if type(xmlFile) is str:
+            xmlFile = FilePath(xmlFile)
+
+        self.xmlFile = xmlFile
+        self._fileInfo = None
+        self._lastCheck = 0
+        self._alwaysStat = alwaysStat
+        self._accounts()
+
+    def recordTypes(self):
+        recordTypes = (
+            DirectoryService.recordType_users,
+            DirectoryService.recordType_groups,
+            DirectoryService.recordType_locations,
+            DirectoryService.recordType_resources
+        )
+        return recordTypes
+
+    def queryDirectory(self, recordTypes, indexType, indexKey):
+        
+        for recordType in recordTypes:
+            for xmlPrincipal in self._accounts()[recordType].itervalues():
+                
+                matched = False
+                if indexType == self.INDEX_TYPE_GUID:
+                    matched = indexKey == xmlPrincipal.guid
+                elif indexType == self.INDEX_TYPE_SHORTNAME:
+                    matched = indexKey in xmlPrincipal.shortNames
+                elif indexType == self.INDEX_TYPE_CUA:
+                    matched = indexKey in xmlPrincipal.calendarUserAddresses
+                
+                if matched:
+                    record = XMLDirectoryRecord(
+                        service       = self,
+                        recordType    = recordType,
+                        shortNames    = tuple(xmlPrincipal.shortNames),
+                        xmlPrincipal  = xmlPrincipal,
+                    )
+                    self.recordCacheForType(recordType).addRecord(record,
+                        indexType, indexKey)
+            
+    def recordsMatchingFields(self, fields, operand="or", recordType=None):
+        # Default, brute force method search of underlying XML data
+
+        def fieldMatches(fieldValue, value, caseless, matchType):
+            if fieldValue is None:
+                return False
+            elif type(fieldValue) in types.StringTypes:
+                fieldValue = (fieldValue,)
+            
+            for testValue in fieldValue:
+                if caseless:
+                    testValue = testValue.lower()
+                    value = value.lower()
+    
+                if matchType == 'starts-with':
+                    if testValue.startswith(value):
+                        return True
+                elif matchType == 'contains':
+                    try:
+                        _ignore_discard = testValue.index(value)
+                        return True
+                    except ValueError:
+                        pass
+                else: # exact
+                    if testValue == value:
+                        return True
+                    
+            return False
+
+        def xmlPrincipalMatches(xmlPrincipal):
+            if operand == "and":
+                for fieldName, value, caseless, matchType in fields:
+                    try:
+                        fieldValue = getattr(xmlPrincipal, fieldName)
+                        if not fieldMatches(fieldValue, value, caseless, matchType):
+                            return False
+                    except AttributeError:
+                        # No property => no match
+                        return False
+                # we hit on every property
+                return True
+            else: # "or"
+                for fieldName, value, caseless, matchType in fields:
+                    try:
+                        fieldValue = getattr(xmlPrincipal, fieldName)
+                        if fieldMatches(fieldValue, value, caseless, matchType):
+                            return True
+                    except AttributeError:
+                        # No value
+                        pass
+                # we didn't hit any
+                return False
+
+        if recordType is None:
+            recordTypes = list(self.recordTypes())
+        else:
+            recordTypes = (recordType,)
+
+        for recordType in recordTypes:
+            for xmlPrincipal in self._accounts()[recordType].itervalues():
+                if xmlPrincipalMatches(xmlPrincipal):
+                    
+                    # Load/cache record from its GUID
+                    record = self.recordWithGUID(xmlPrincipal.guid)
+                    if record:
+                        yield record
+
+    def _accounts(self):
+        currentTime = time()
+        if self._alwaysStat or currentTime - self._lastCheck > 60:
+            self.xmlFile.restat()
+            self._lastCheck = currentTime
+            fileInfo = (self.xmlFile.getmtime(), self.xmlFile.getsize())
+            if fileInfo != self._fileInfo:
+                parser = XMLAccountsParser(self.xmlFile)
+                self._parsedAccounts = parser.items
+                self.realmName = parser.realm
+                self._fileInfo = fileInfo
+        return self._parsedAccounts
+
+class XMLDirectoryRecord(CachingDirectoryRecord):
+    """
+    XML based implementation implementation of L{IDirectoryRecord}.
+    """
+    def __init__(self, service, recordType, shortNames, xmlPrincipal):
+        super(XMLDirectoryRecord, self).__init__(
+            service               = service,
+            recordType            = recordType,
+            guid                  = xmlPrincipal.guid,
+            shortNames            = shortNames,
+            fullName              = xmlPrincipal.fullName,
+            firstName             = xmlPrincipal.firstName,
+            lastName              = xmlPrincipal.lastName,
+            emailAddresses        = xmlPrincipal.emailAddresses,
+            calendarUserAddresses = xmlPrincipal.calendarUserAddresses,
+            enabledForCalendaring = xmlPrincipal.enabledForCalendaring,
+        )
+
+        self.password          = xmlPrincipal.password
+        self._members          = xmlPrincipal.members
+        self._groups           = xmlPrincipal.groups
+
+    def members(self):
+        for recordType, shortName in self._members:
+            yield self.service.recordWithShortName(recordType, shortName)
+
+    def groups(self):
+        for shortName in self._groups:
+            yield self.service.recordWithShortName(DirectoryService.recordType_groups, shortName)
+
+    def verifyCredentials(self, credentials):
+        if isinstance(credentials, UsernamePassword):
+            return credentials.password == self.password
+        if isinstance(credentials, DigestedCredentials):
+            return credentials.checkPassword(self.password)
+
+        return super(XMLDirectoryRecord, self).verifyCredentials(credentials)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20090511/b1500811/attachment-0001.html>


More information about the calendarserver-changes mailing list