[CalendarServer-changes] [3899] CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/ twistedcaldav/directory
source_changes at macosforge.org
source_changes at macosforge.org
Thu Mar 19 13:45:50 PDT 2009
Revision: 3899
http://trac.macosforge.org/projects/calendarserver/changeset/3899
Author: sagen at apple.com
Date: 2009-03-19 13:45:50 -0700 (Thu, 19 Mar 2009)
Log Message:
-----------
Merging Cyrus' on-demand directory work forward
Modified Paths:
--------------
CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/sqldb.py
CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlaccountsparser.py
CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlfile.py
Added Paths:
-----------
CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingappleopendirectory.py
CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingdirectory.py
CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingxmlfile.py
CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/test/test_cachedirectory.py
Copied: CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingappleopendirectory.py (from rev 3898, CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3672/twistedcaldav/directory/cachingappleopendirectory.py)
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingappleopendirectory.py (rev 0)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingappleopendirectory.py 2009-03-19 20:45:50 UTC (rev 3899)
@@ -0,0 +1,778 @@
+##
+# 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 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.directory.cachingdirectory import CachingDirectoryService,\
+ CachingDirectoryRecord
+from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+from twistedcaldav.directory.directory import DirectoryError, UnknownRecordTypeError
+
+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.cacheTimeout = cacheTimeout
+ 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 _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 (
+ DirectoryService.recordType_users,
+ DirectoryService.recordType_groups,
+ DirectoryService.recordType_locations,
+ DirectoryService.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' : dsattributes.kDS1AttrDistinguishedName,
+ 'firstName' : dsattributes.kDS1AttrFirstName,
+ 'lastName' : dsattributes.kDS1AttrLastName,
+ 'emailAddresses' : dsattributes.kDSNAttrEMailAddress,
+ 'recordName' : dsattributes.kDSNAttrRecordName,
+ 'guid' : dsattributes.kDS1AttrGeneratedUID,
+ }
+
+ _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
+
+
+ operand = (dsquery.expression.OR if operand == "or"
+ else dsquery.expression.AND)
+
+ expressions = []
+ for field, value, caseless, matchType in fields:
+ if field in self._ODFields:
+ ODField = self._ODFields[field]
+ if matchType == "starts-with":
+ comparison = dsattributes.eDSStartsWith
+ elif matchType == "contains":
+ comparison = dsattributes.eDSContains
+ else:
+ comparison = dsattributes.eDSExact
+ expressions.append(dsquery.match(ODField, value, comparison))
+
+
+ if recordType is None:
+ recordTypes = self._toODRecordTypes.values()
+ else:
+ recordTypes = (self._toODRecordTypes[recordType],)
+
+ self.log_info("Calling OD: Types %s, Operand %s, Caseless %s, %s" % (recordTypes, operand, caseless, fields))
+ deferred = deferToThread(
+ opendirectory.queryRecordsWithAttributes,
+ self.directory,
+ dsquery.expression(operand, expressions).generate(),
+ caseless,
+ recordTypes,
+ [ dsattributes.kDS1AttrGeneratedUID ]
+ )
+ deferred.addCallback(collectResults)
+ return deferred
+
+ def queryDirectory(self, recordTypes, indexType, indexKey):
+
+ attrs = [
+ dsattributes.kDS1AttrGeneratedUID,
+ dsattributes.kDSNAttrRecordName,
+ dsattributes.kDSNAttrRecordType,
+ dsattributes.kDS1AttrDistinguishedName,
+ dsattributes.kDS1AttrFirstName,
+ dsattributes.kDS1AttrLastName,
+ dsattributes.kDSNAttrEMailAddress,
+ dsattributes.kDSNAttrMetaNodeLocation,
+ ]
+
+ 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:
+ listRecordTypes.append(dsattributes.kDSStdRecordTypePlaces)
+ attrs.append(dsattributes.kDSNAttrResourceInfo)
+
+ elif recordType == DirectoryService.recordType_resources:
+ listRecordTypes.append(dsattributes.kDSStdRecordTypeResources)
+ attrs.append(dsattributes.kDSNAttrResourceInfo)
+
+ else:
+ raise UnknownRecordTypeError("Unknown Open Directory record type: %s" % (recordType))
+
+ queryattr = {
+ self.INDEX_TYPE_SHORTNAME : dsattributes.kDSNAttrRecordName,
+ self.INDEX_TYPE_GUID : dsattributes.kDS1AttrGeneratedUID,
+ self.INDEX_TYPE_EMAIL : dsattributes.kDSNAttrEMailAddress,
+ }.get(indexType)
+ assert queryattr is not None, "Invalid type for record faulting query"
+ query = dsquery.match(queryattr, indexKey, 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,
+ 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:
+ self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
+ raise
+
+ for (recordShortName, value) in results:
+
+ # Now get useful record info.
+ recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
+ recordShortNames = value.get(dsattributes.kDSNAttrRecordName)
+ recordType = value.get(dsattributes.kDSNAttrRecordType)
+ if isinstance(recordType, list):
+ recordType = recordType[0]
+ if isinstance(recordShortNames, str):
+ recordShortNames = (recordShortNames,)
+ else:
+ recordShortNames = tuple(recordShortNames) if recordShortNames else ()
+ recordFullName = value.get(dsattributes.kDS1AttrDistinguishedName)
+ recordFirstName = value.get(dsattributes.kDS1AttrFirstName)
+ recordLastName = value.get(dsattributes.kDS1AttrLastName)
+ recordEmailAddress = value.get(dsattributes.kDSNAttrEMailAddress)
+ 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
+ enabledForCalendaring = True
+
+ if self.restrictEnabledRecords and self.restrictedGUIDs is not None:
+ enabledForCalendaring = recordGUID in self.restrictedGUIDs
+
+ if not enabledForCalendaring:
+ # Some records we want to keep even though they are not enabled for calendaring.
+ # Others we discard.
+ if recordType in (
+ DirectoryService.recordType_users,
+ DirectoryService.recordType_groups,
+ ):
+ self.log_debug(
+ "Record (%s) %s is not enabled for calendaring but may be used in ACLs"
+ % (recordType, recordShortName)
+ )
+ else:
+ self.log_debug(
+ "Record (%s) %s is not enabled for calendaring"
+ % (recordType, recordShortName)
+ )
+ continue
+
+ # Get calendar user addresses from directory record.
+ if enabledForCalendaring:
+ calendarUserAddresses = self._calendarUserAddresses(recordType, value)
+ else:
+ calendarUserAddresses = ()
+
+ # Get email address from directory record
+ recordEmailAddresses = set()
+ if isinstance(recordEmailAddress, str):
+ recordEmailAddresses.add(recordEmailAddress.lower())
+ elif isinstance(recordEmailAddress, list):
+ for addr in recordEmailAddresses:
+ recordEmailAddresses.add(addr.lower())
+
+ # Special case for groups, which have members.
+ if recordType == DirectoryService.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 = ()
+
+ # Special case for resources and locations
+ autoSchedule = False
+ proxyGUIDs = ()
+ readOnlyProxyGUIDs = ()
+ if recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
+ resourceInfo = value.get(dsattributes.kDSNAttrResourceInfo)
+ if resourceInfo is not None:
+ try:
+ autoSchedule, proxy, read_only_proxy = self._parseResourceInfo(resourceInfo, recordGUID, recordType, recordShortName)
+ except ValueError:
+ continue
+ if proxy:
+ proxyGUIDs = (proxy,)
+ if read_only_proxy:
+ readOnlyProxyGUIDs = (read_only_proxy,)
+
+ record = OpenDirectoryRecord(
+ service = self,
+ recordType = recordType,
+ guid = recordGUID,
+ nodeName = recordNodeName,
+ shortNames = recordShortNames,
+ fullName = recordFullName,
+ firstName = recordFirstName,
+ lastName = recordLastName,
+ emailAddresses = recordEmailAddresses,
+ calendarUserAddresses = calendarUserAddresses,
+ autoSchedule = autoSchedule,
+ enabledForCalendaring = enabledForCalendaring,
+ memberGUIDs = memberGUIDs,
+ proxyGUIDs = proxyGUIDs,
+ readOnlyProxyGUIDs = readOnlyProxyGUIDs,
+ )
+ self.recordCacheForType(recordType).addRecord(record)
+
+class OpenDirectoryRecord(CachingDirectoryRecord):
+ """
+ Open Directory implementation of L{IDirectoryRecord}.
+ """
+ def __init__(
+ self, service, recordType, guid, nodeName, shortNames, fullName,
+ firstName, lastName, emailAddresses,
+ calendarUserAddresses, autoSchedule, enabledForCalendaring,
+ memberGUIDs, proxyGUIDs, readOnlyProxyGUIDs,
+ ):
+ super(OpenDirectoryRecord, self).__init__(
+ service = service,
+ recordType = recordType,
+ guid = guid,
+ shortNames = shortNames,
+ fullName = fullName,
+ firstName = firstName,
+ lastName = lastName,
+ emailAddresses = emailAddresses,
+ calendarUserAddresses = calendarUserAddresses,
+ autoSchedule = autoSchedule,
+ enabledForCalendaring = enabledForCalendaring,
+ )
+ self.nodeName = nodeName
+ self._memberGUIDs = tuple(memberGUIDs)
+ self._proxyGUIDs = tuple(proxyGUIDs)
+ self._readOnlyProxyGUIDs = tuple(readOnlyProxyGUIDs)
+
+ 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 != DirectoryService.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 proxies(self):
+ if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
+ return
+
+ for guid in self._proxyGUIDs:
+ proxyRecord = self.service.recordWithGUID(guid)
+ if proxyRecord is None:
+ self.log_error("No record for proxy in (%s)%s with GUID %s" % (
+ self.recordType,
+ self.shortNames[0],
+ guid,
+ ))
+ else:
+ yield proxyRecord
+
+ def proxyFor(self):
+ result = set()
+ result.update(self.service.proxiesForGUID(DirectoryService.recordType_resources, self.guid))
+ result.update(self.service.proxiesForGUID(DirectoryService.recordType_locations, self.guid))
+ return result
+
+ def readOnlyProxies(self):
+ if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
+ return
+
+ for guid in self._readOnlyProxyGUIDs:
+ proxyRecord = self.service.recordWithGUID(guid)
+ if proxyRecord is None:
+ self.log_error("No record for proxy in (%s)%s with GUID %s" % (
+ self.recordType,
+ self.shortNames[0],
+ guid,
+ ))
+ else:
+ yield proxyRecord
+
+ def readOnlyProxyFor(self):
+ result = set()
+ result.update(self.service.readOnlyProxiesForGUID(DirectoryService.recordType_resources, self.guid))
+ result.update(self.service.readOnlyProxiesForGUID(DirectoryService.recordType_locations, self.guid))
+ return result
+
+ 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/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingdirectory.py (from rev 3898, CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3672/twistedcaldav/directory/cachingdirectory.py)
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingdirectory.py (rev 0)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingdirectory.py 2009-03-19 20:45:50 UTC (rev 3899)
@@ -0,0 +1,244 @@
+##
+# Copyright (c) 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 twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+import time
+import types
+
+"""
+Caching directory service implementation.
+"""
+
+__all__ = [
+ "CachingDirectoryService",
+ "CachingDirectoryRecord",
+ "DictRecordTypeCache",
+]
+
+class RecordTypeCache(object):
+ """
+ Abstract class for a record type cache. We will likely have dict and memcache implementations of this.
+ """
+
+ def __init__(self, directoryService, recordType):
+
+ self.directoryService = directoryService
+ self.recordType = recordType
+
+ def addRecord(self, record):
+ raise NotImplementedError()
+
+ def removeRecord(self, record):
+ raise NotImplementedError()
+
+ def replaceRecord(self, oldRecord, newRecord):
+ raise NotImplementedError()
+
+ def findRecord(self, indexType, indexKey):
+ raise NotImplementedError()
+
+class DictRecordTypeCache(RecordTypeCache):
+ """
+ Cache implementation using a dict. Does not share the cache with other instances.
+ """
+
+ def __init__(self, directoryService, recordType):
+
+ super(DictRecordTypeCache, self).__init__(directoryService, recordType)
+ self.records = set()
+ self.recordsIndexedBy = {
+ CachingDirectoryService.INDEX_TYPE_GUID : {},
+ CachingDirectoryService.INDEX_TYPE_SHORTNAME: {},
+ CachingDirectoryService.INDEX_TYPE_EMAIL : {},
+ }
+
+ def addRecord(self, record):
+
+ self.records.add(record)
+ for indexType in self.directoryService.indexTypes():
+ try:
+ indexData = getattr(record, CachingDirectoryService.indexTypeToRecordAttribute[indexType])
+ except AttributeError:
+ continue
+ if isinstance(indexData, str):
+ indexData = (indexData,)
+ if type(indexData) in (types.ListType, types.TupleType, set):
+ for item in indexData:
+ self.recordsIndexedBy[indexType][item] = record
+ elif indexData is None:
+ pass
+ else:
+ raise AssertionError("Data from record attribute must be str, list or tuple")
+
+ def removeRecord(self, record):
+
+ if record in self.records:
+ self.records.remove(record)
+ for indexType in self.directoryService.indexTypes():
+ try:
+ indexData = getattr(record, CachingDirectoryService.indexTypeToRecordAttribute[indexType])
+ except AttributeError:
+ continue
+ if isinstance(indexData, str):
+ indexData = (indexData,)
+ if type(indexData) in (types.ListType, types.TupleType):
+ for item in indexData:
+ try:
+ del self.recordsIndexedBy[indexType][item]
+ except KeyError:
+ raise AssertionError("Missing record index item")
+ else:
+ raise AssertionError("Data from record attribute must be str, list or tuple")
+
+ def replaceRecord(self, oldRecord, newRecord):
+ self.removeRecord(oldRecord)
+ self.addRecord(newRecord)
+
+ def findRecord(self, indexType, indexKey):
+ return self.recordsIndexedBy[indexType].get(indexKey)
+
+class CachingDirectoryService(DirectoryService):
+ """
+ Caching Directory implementation of L{IDirectoryService}.
+
+ This is class must be overridden to provide a concrete implementation.
+ """
+
+ INDEX_TYPE_GUID = "guid"
+ INDEX_TYPE_SHORTNAME = "shortname"
+ INDEX_TYPE_EMAIL = "email"
+
+ indexTypeToRecordAttribute = {
+ "guid" : "guid",
+ "shortname": "shortNames",
+ "email" : "emailAddresses",
+ }
+
+ def __init__(
+ self,
+ cacheTimeout=30,
+ cacheClass=DictRecordTypeCache,
+ ):
+ """
+ @param cacheTimeout: C{int} number of minutes before cache is invalidated.
+ """
+
+ self.cacheTimeout = cacheTimeout * 60
+
+ self._initCaches(cacheClass)
+
+ def _initCaches(self, cacheClass):
+ self._recordCaches = dict([
+ (recordType, cacheClass(self, recordType))
+ for recordType in self.recordTypes()
+ ])
+
+ self._disabledKeys = dict([(indexType, dict()) for indexType in self.indexTypes()])
+
+ def indexTypes(self):
+
+ return (
+ CachingDirectoryService.INDEX_TYPE_GUID,
+ CachingDirectoryService.INDEX_TYPE_SHORTNAME,
+ CachingDirectoryService.INDEX_TYPE_EMAIL,
+ )
+
+ def recordCacheForType(self, recordType):
+ return self._recordCaches[recordType]
+
+ def listRecords(self, recordType):
+ return self.recordCacheForType(recordType).records
+
+ def recordWithShortName(self, recordType, shortName):
+ return self._lookupRecord((recordType,), CachingDirectoryService.INDEX_TYPE_SHORTNAME, shortName)
+
+ def recordWithEmailAddress(self, emailAddress):
+ return self._lookupRecord(None, CachingDirectoryService.INDEX_TYPE_EMAIL, emailAddress)
+
+ def recordWithGUID(self, guid):
+ return self._lookupRecord(None, CachingDirectoryService.INDEX_TYPE_GUID, guid)
+
+ recordWithUID = recordWithGUID
+
+ def _lookupRecord(self, recordTypes, indexType, indexKey, cacheOnMiss=True):
+
+ if recordTypes is None:
+ recordTypes = self.recordTypes()
+
+ def lookup():
+ for recordType in recordTypes:
+ record = self.recordCacheForType(recordType).findRecord(indexType, indexKey)
+ if record:
+ return record
+ else:
+ return None
+
+ record = lookup()
+ if record:
+ return record
+
+ if cacheOnMiss:
+
+ # Check negative cache (take cache entry timeout into account)
+ try:
+ disabledTime = self._disabledKeys[indexType][indexKey]
+ if time.time() - disabledTime < self.cacheTimeout:
+ return None
+ except KeyError:
+ pass
+
+ # Try query
+ self.log_debug("Faulting record for attribute '%s' with value '%s'" % (indexType, indexKey,))
+ self.queryDirectory(recordTypes, indexType, indexKey)
+
+ # Now try again from cache
+ record = lookup()
+ if record:
+ self.log_debug("Found record for attribute '%s' with value '%s'" % (indexType, indexKey,))
+ return record
+
+ # Add to negative cache with timestamp
+ self.log_debug("Failed to fault record for attribute '%s' with value '%s'" % (indexType, indexKey,))
+ self._disabledKeys[indexType][indexKey] = time.time()
+
+ return None
+
+ def queryDirectory(self, recordTypes, indexType, indexKey):
+ raise NotImplementedError()
+
+class CachingDirectoryRecord(DirectoryRecord):
+
+ def __init__(
+ self, service, recordType, guid, shortNames, fullName,
+ firstName, lastName, emailAddresses,
+ calendarUserAddresses, autoSchedule, enabledForCalendaring=True,
+ uid=None,
+ ):
+ super(CachingDirectoryRecord, self).__init__(
+ service = service,
+ recordType = recordType,
+ guid = guid,
+ shortNames = shortNames,
+ fullName = fullName,
+ firstName = firstName,
+ lastName = lastName,
+ emailAddresses = emailAddresses,
+ calendarUserAddresses = calendarUserAddresses,
+ autoSchedule = autoSchedule,
+ enabledForCalendaring = enabledForCalendaring,
+ uid = uid,
+ )
+
+ self.cachedTime = time.time()
Copied: CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingxmlfile.py (from rev 3898, CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3672/twistedcaldav/directory/cachingxmlfile.py)
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingxmlfile.py (rev 0)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingxmlfile.py 2009-03-19 20:45:50 UTC (rev 3899)
@@ -0,0 +1,228 @@
+##
+# 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_EMAIL:
+ matched = indexKey in xmlPrincipal.emailAddresses
+
+ 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,
+ autoSchedule = xmlPrincipal.autoSchedule,
+ enabledForCalendaring = xmlPrincipal.enabledForCalendaring,
+ )
+
+ self.password = xmlPrincipal.password
+ self._members = xmlPrincipal.members
+ self._groups = xmlPrincipal.groups
+ self._proxies = xmlPrincipal.proxies
+ self._proxyFor = xmlPrincipal.proxyFor
+ self._readOnlyProxies = xmlPrincipal.readOnlyProxies
+ self._readOnlyProxyFor = xmlPrincipal.readOnlyProxyFor
+
+ 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 proxies(self):
+ for recordType, shortName in self._proxies:
+ yield self.service.recordWithShortName(recordType, shortName)
+
+ def proxyFor(self, read_write=True):
+ for recordType, shortName in self._proxyFor:
+ yield self.service.recordWithShortName(recordType, shortName)
+
+ def readOnlyProxies(self):
+ for recordType, shortName in self._readOnlyProxies:
+ yield self.service.recordWithShortName(recordType, shortName)
+
+ def readOnlyProxyFor(self, read_write=True):
+ for recordType, shortName in self._readOnlyProxyFor:
+ yield self.service.recordWithShortName(recordType, 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/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/sqldb.py
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/sqldb.py 2009-03-19 20:44:02 UTC (rev 3898)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/sqldb.py 2009-03-19 20:45:50 UTC (rev 3899)
@@ -176,7 +176,7 @@
shortName = record.shortNames[0]
guid = record.guid
password = record.password
- name = record.name
+ name = record.fullName
self._db_execute(
"""
Copied: CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/test/test_cachedirectory.py (from rev 3898, CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3672/twistedcaldav/directory/test/test_cachedirectory.py)
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/test/test_cachedirectory.py (rev 0)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/test/test_cachedirectory.py 2009-03-19 20:45:50 UTC (rev 3899)
@@ -0,0 +1,219 @@
+#
+# Copyright (c) 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 twistedcaldav.directory.cachingdirectory import CachingDirectoryService,\
+ CachingDirectoryRecord, DictRecordTypeCache
+from twistedcaldav.test.util import TestCase
+from twistedcaldav.directory.directory import DirectoryService
+from twistedcaldav.directory.util import uuidFromName
+from uuid import uuid4
+
+class TestDirectoryService (CachingDirectoryService):
+
+ realmName = "Dummy Realm"
+
+ def recordTypes(self):
+ return (
+ DirectoryService.recordType_users,
+ DirectoryService.recordType_groups,
+ DirectoryService.recordType_locations,
+ DirectoryService.recordType_resources,
+ )
+
+ def queryDirectory(self, recordTypes, indexType, indexKey):
+
+ self.queried = True
+
+ for recordType in recordTypes:
+ for record in self.fakerecords[recordType]:
+ cacheIt = False
+ if indexType in (
+ CachingDirectoryService.INDEX_TYPE_SHORTNAME,
+ CachingDirectoryService.INDEX_TYPE_EMAIL
+ ):
+ if indexKey in record[indexType]:
+ cacheIt = True
+ else:
+ if indexKey == record[indexType]:
+ cacheIt = True
+
+ if cacheIt:
+ cacheRecord = CachingDirectoryRecord(
+ service = self,
+ recordType = recordType,
+ guid = record.get("guid"),
+ shortNames = record.get("shortname"),
+ fullName = record.get("fullName"),
+ firstName = "",
+ lastName = "",
+ emailAddresses = record.get("email"),
+ calendarUserAddresses = record.get("email"),
+ autoSchedule = False,
+ enabledForCalendaring = True,
+ )
+ self.recordCacheForType(recordType).addRecord(cacheRecord)
+
+class CachingDirectoryTest(TestCase):
+
+ baseGUID = str(uuid4())
+
+ def setUp(self):
+ super(CachingDirectoryTest, self).setUp()
+ self.service = TestDirectoryService()
+ self.service.queried = False
+
+ def loadRecords(self, records):
+ self.service._initCaches(DictRecordTypeCache)
+ self.service.fakerecords = records
+ self.service.queried = False
+
+ def fakeRecord(
+ self,
+ fullName,
+ shortNames=None,
+ guid=None,
+ emails=None,
+ members=None,
+ resourceInfo=None,
+ multinames=False
+ ):
+ if shortNames is None:
+ shortNames = (self.shortNameForFullName(fullName),)
+ if multinames:
+ shortNames += (fullName,)
+
+ if guid is None:
+ guid = self.guidForShortName(shortNames[0])
+ else:
+ guid = guid.lower()
+
+ if emails is None:
+ emails = ("%s at example.com" % (shortNames[0],),)
+
+ attrs = {
+ "fullName": fullName,
+ "guid": guid,
+ "shortname": shortNames,
+ "email": emails,
+ }
+
+ if members:
+ attrs["members"] = members
+
+ if resourceInfo:
+ attrs["resourceInfo"] = resourceInfo
+
+ return attrs
+
+ def shortNameForFullName(self, fullName):
+ return fullName.lower().replace(" ", "")
+
+ def guidForShortName(self, shortName):
+ return uuidFromName(self.baseGUID, shortName)
+
+ def dummyRecords(self):
+ SIZE = 10
+ self.loadRecords({
+ DirectoryService.recordType_users: [
+ self.fakeRecord("User %02d" % x, multinames=(x>5)) for x in range(1,SIZE+1)
+ ],
+ DirectoryService.recordType_groups: [
+ self.fakeRecord("Group %02d" % x) for x in range(1,SIZE+1)
+ ],
+ DirectoryService.recordType_resources: [
+ self.fakeRecord("Resource %02d" % x) for x in range(1,SIZE+1)
+ ],
+ DirectoryService.recordType_locations: [
+ self.fakeRecord("Location %02d" % x) for x in range(1,SIZE+1)
+ ],
+ })
+
+ def verifyRecords(self, recordType, expectedGUIDs):
+
+ records = self.service.listRecords(recordType)
+ recordGUIDs = set([record.guid for record in records])
+ self.assertEqual(recordGUIDs, expectedGUIDs)
+
+class GUIDLookups(CachingDirectoryTest):
+
+ def test_emptylist(self):
+ self.dummyRecords()
+
+ self.verifyRecords(DirectoryService.recordType_users, set())
+ self.verifyRecords(DirectoryService.recordType_groups, set())
+ self.verifyRecords(DirectoryService.recordType_resources, set())
+ self.verifyRecords(DirectoryService.recordType_locations, set())
+
+ def test_cacheoneguid(self):
+ self.dummyRecords()
+
+ self.assertTrue(self.service.recordWithGUID(self.guidForShortName("user01")) is not None)
+ self.assertTrue(self.service.queried)
+ self.verifyRecords(DirectoryService.recordType_users, set((
+ self.guidForShortName("user01"),
+ )))
+ self.verifyRecords(DirectoryService.recordType_groups, set())
+ self.verifyRecords(DirectoryService.recordType_resources, set())
+ self.verifyRecords(DirectoryService.recordType_locations, set())
+
+ # Make sure it really is cached and won't cause another query
+ self.service.queried = False
+ self.assertTrue(self.service.recordWithGUID(self.guidForShortName("user01")) is not None)
+ self.assertFalse(self.service.queried)
+
+ def test_cacheoneshortname(self):
+ self.dummyRecords()
+
+ self.assertTrue(self.service.recordWithShortName(
+ DirectoryService.recordType_users,
+ "user02"
+ ) is not None)
+ self.assertTrue(self.service.queried)
+ self.verifyRecords(DirectoryService.recordType_users, set((
+ self.guidForShortName("user02"),
+ )))
+ self.verifyRecords(DirectoryService.recordType_groups, set())
+ self.verifyRecords(DirectoryService.recordType_resources, set())
+ self.verifyRecords(DirectoryService.recordType_locations, set())
+
+ # Make sure it really is cached and won't cause another query
+ self.service.queried = False
+ self.assertTrue(self.service.recordWithShortName(
+ DirectoryService.recordType_users,
+ "user02"
+ ) is not None)
+ self.assertFalse(self.service.queried)
+
+ def test_cacheoneemail(self):
+ self.dummyRecords()
+
+ self.assertTrue(self.service.recordWithEmailAddress(
+ "user03 at example.com"
+ ) is not None)
+ self.assertTrue(self.service.queried)
+ self.verifyRecords(DirectoryService.recordType_users, set((
+ self.guidForShortName("user03"),
+ )))
+ self.verifyRecords(DirectoryService.recordType_groups, set())
+ self.verifyRecords(DirectoryService.recordType_resources, set())
+ self.verifyRecords(DirectoryService.recordType_locations, set())
+
+ # Make sure it really is cached and won't cause another query
+ self.service.queried = False
+ self.assertTrue(self.service.recordWithEmailAddress(
+ "user03 at example.com"
+ ) is not None)
+ self.assertFalse(self.service.queried)
Modified: CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlaccountsparser.py
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlaccountsparser.py 2009-03-19 20:44:02 UTC (rev 3898)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlaccountsparser.py 2009-03-19 20:45:50 UTC (rev 3899)
@@ -165,7 +165,7 @@
self.shortNames = []
self.guid = None
self.password = None
- self.name = None
+ self.fullName = None
self.firstName = None
self.lastName = None
self.emailAddresses = set()
@@ -202,10 +202,10 @@
password = self.password % ctr
else:
password = self.password
- if self.name.find("%") != -1:
- name = self.name % ctr
+ if self.fullName.find("%") != -1:
+ fullName = self.fullName % ctr
else:
- name = self.name
+ fullName = self.fullName
if self.firstName and self.firstName.find("%") != -1:
firstName = self.firstName % ctr
else:
@@ -231,7 +231,7 @@
result.shortNames = shortNames
result.guid = guid
result.password = password
- result.name = name
+ result.fullName = fullName
result.firstName = firstName
result.lastName = lastName
result.emailAddresses = emailAddresses
@@ -264,7 +264,7 @@
self.password = child.firstChild.data.encode("utf-8")
elif child_name == ELEMENT_NAME:
if child.firstChild is not None:
- self.name = child.firstChild.data.encode("utf-8")
+ self.fullName = child.firstChild.data.encode("utf-8")
elif child_name == ELEMENT_FIRST_NAME:
if child.firstChild is not None:
self.firstName = child.firstChild.data.encode("utf-8")
Modified: CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlfile.py
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlfile.py 2009-03-19 20:44:02 UTC (rev 3898)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlfile.py 2009-03-19 20:45:50 UTC (rev 3899)
@@ -114,7 +114,7 @@
recordType = recordType,
guid = xmlPrincipal.guid,
shortNames = shortNames,
- fullName = xmlPrincipal.name,
+ fullName = xmlPrincipal.fullName,
firstName = xmlPrincipal.firstName,
lastName = xmlPrincipal.lastName,
emailAddresses = xmlPrincipal.emailAddresses,
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20090319/3eb65dfc/attachment-0001.html>
More information about the calendarserver-changes
mailing list