[CalendarServer-changes] [9553] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Fri Aug 10 14:24:37 PDT 2012
Revision: 9553
http://trac.macosforge.org/projects/calendarserver/changeset/9553
Author: sagen at apple.com
Date: 2012-08-10 14:24:34 -0700 (Fri, 10 Aug 2012)
Log Message:
-----------
New calendar-user-search REPORT
Modified Paths:
--------------
CalendarServer/trunk/calendarserver/tools/principals.py
CalendarServer/trunk/calendarserver/webadmin/resource.py
CalendarServer/trunk/twistedcaldav/customxml.py
CalendarServer/trunk/twistedcaldav/directory/aggregate.py
CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
CalendarServer/trunk/twistedcaldav/directory/directory.py
CalendarServer/trunk/twistedcaldav/directory/idirectory.py
CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py
CalendarServer/trunk/twistedcaldav/directory/principal.py
CalendarServer/trunk/twistedcaldav/directory/test/test_buildquery.py
CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py
CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py
CalendarServer/trunk/twistedcaldav/extensions.py
CalendarServer/trunk/twistedcaldav/test/test_extensions.py
Modified: CalendarServer/trunk/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/principals.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/calendarserver/tools/principals.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -390,7 +390,7 @@
for fieldName in ("fullName", "firstName", "lastName", "emailAddresses"):
fields.append((fieldName, searchTerm, True, "contains"))
- records = list((yield config.directory.recordsMatchingFields(fields)))
+ records = list((yield config.directory.recordsMatchingTokens(searchTerm.strip().split())))
if records:
records.sort(key=operator.attrgetter('fullName'))
print "%d matches found:" % (len(records),)
Modified: CalendarServer/trunk/calendarserver/webadmin/resource.py
===================================================================
--- CalendarServer/trunk/calendarserver/webadmin/resource.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/calendarserver/webadmin/resource.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -691,4 +691,5 @@
fields.append((fieldName, searchStr, True, "contains"))
records = list((yield self.directory.recordsMatchingFields(fields)))
+ # records = list((yield self.directory.recordsMatchingTokens(searchStr.strip().split())))
returnValue(records)
Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/twistedcaldav/customxml.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -1349,6 +1349,32 @@
namespace = "http://apple.com/ns/ical/"
name = "calendar-color"
+#
+# Calendar-user-search REPORT
+#
+
+ at registerElement
+class CalendarUserSearchToken (WebDAVTextElement):
+ """
+ Contains a search token.
+ """
+ namespace = calendarserver_namespace
+ name = "search-token"
+
+ at registerElement
+class CalendarUserSearch (WebDAVElement):
+
+ namespace = calendarserver_namespace
+ name = "calendar-user-search"
+
+ allowed_children = {
+ (calendarserver_namespace, "search-token" ): (1, None),
+ (calendarserver_namespace, "limit" ): (0, 1),
+ (dav_namespace, "prop" ): (1, 1),
+ (dav_namespace, "apply-to-principal-collection-set"): (0, 1),
+ }
+ allowed_attributes = { "context" : False }
+
##
# Extensions to ResourceType
##
Modified: CalendarServer/trunk/twistedcaldav/directory/aggregate.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/aggregate.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/twistedcaldav/directory/aggregate.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -164,6 +164,41 @@
returnValue(itertools.chain(*generators))
+
+ @inlineCallbacks
+ def recordsMatchingTokens(self, tokens, context=None):
+ """
+ @param tokens: The tokens to search on
+ @type tokens: C{list} of C{str} (utf-8 bytes)
+ @param context: An indication of what the end user is searching
+ for, e.g. "attendee", "location"
+ @type context: C{str}
+ @return: a deferred sequence of L{IDirectoryRecord}s which
+ match the given tokens and optional context.
+
+ Each token is searched for within each record's full name and
+ email address; if each token is found within a record that
+ record is returned in the results.
+
+ If context is None, all record types are considered. If
+ context is "location", only locations are considered. If
+ context is "attendee", only users, groups, and resources
+ are considered.
+
+ Combine the results from the sub-services.
+ """
+
+ services = set(self._recordTypes.values())
+
+ generators = []
+ for service in services:
+ generator = (yield service.recordsMatchingTokens(tokens,
+ context=context))
+ generators.append(generator)
+
+ returnValue(itertools.chain(*generators))
+
+
def getGroups(self, guids):
"""
Returns a set of group records for the list of guids passed in. For
Modified: CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -530,6 +530,194 @@
else:
return ()
+ def recordsMatchingTokens(self, tokens, context=None, lookupMethod=None):
+ """
+ @param tokens: The tokens to search on
+ @type tokens: C{list} of C{str} (utf-8 bytes)
+ @param context: An indication of what the end user is searching
+ for, e.g. "attendee", "location"
+ @type context: C{str}
+ @return: a deferred sequence of L{IDirectoryRecord}s which
+ match the given tokens and optional context.
+
+ Each token is searched for within each record's full name and
+ email address; if each token is found within a record that
+ record is returned in the results.
+
+ If context is None, all record types are considered. If
+ context is "location", only locations are considered. If
+ context is "attendee", only users, groups, and resources
+ are considered.
+ """
+
+ if lookupMethod is None:
+ lookupMethod=self.odModule.queryRecordsWithAttributes_list
+
+ def collectResults(results):
+ self.log_debug("Got back %d records from OD" % (len(results),))
+ for key, value in results:
+ self.log_debug("OD result: %s %s" % (key, value))
+ try:
+ recordNodeName = value.get(
+ dsattributes.kDSNAttrMetaNodeLocation)
+ recordShortNames = self._uniqueTupleFromAttribute(
+ value.get(dsattributes.kDSNAttrRecordName))
+
+ recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
+
+ recordType = value.get(dsattributes.kDSNAttrRecordType)
+ if isinstance(recordType, list):
+ recordType = recordType[0]
+ if not recordType:
+ continue
+ recordType = self._fromODRecordTypes[recordType]
+
+ # Skip if group restriction is in place and guid is not
+ # a member (but don't skip any groups)
+ if (recordType != self.recordType_groups and
+ self.restrictedGUIDs is not None):
+ if str(recordGUID) not in self.restrictedGUIDs:
+ continue
+
+ recordAuthIDs = self._setFromAttribute(
+ value.get(dsattributes.kDSNAttrAltSecurityIdentities))
+ recordFullName = value.get(
+ dsattributes.kDS1AttrDistinguishedName)
+ recordFirstName = value.get(dsattributes.kDS1AttrFirstName)
+ recordLastName = value.get(dsattributes.kDS1AttrLastName)
+ recordEmailAddresses = self._setFromAttribute(
+ value.get(dsattributes.kDSNAttrEMailAddress),
+ lower=True)
+
+ # 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:
+ nestedGUIDs = ()
+ else:
+ nestedGUIDs = ()
+ memberGUIDs = ()
+
+ # Create records but don't store them in our index or
+ # send them to memcached, because these are transient,
+ # existing only so we can create principal resource
+ # objects that are used to generate the REPORT result.
+
+ record = OpenDirectoryRecord(
+ service = self,
+ recordType = recordType,
+ guid = recordGUID,
+ nodeName = recordNodeName,
+ shortNames = recordShortNames,
+ authIDs = recordAuthIDs,
+ fullName = recordFullName,
+ firstName = recordFirstName,
+ lastName = recordLastName,
+ emailAddresses = recordEmailAddresses,
+ memberGUIDs = memberGUIDs,
+ nestedGUIDs = nestedGUIDs,
+ extProxies = (),
+ extReadOnlyProxies = (),
+ )
+
+ # (Copied from below)
+ # Look up augment information
+ # TODO: this needs to be deferred but for now we hard code
+ # the deferred result because we know it is completing
+ # immediately.
+ if self.augmentService is not None:
+ d = self.augmentService.getAugmentRecord(record.guid,
+ recordType)
+ d.addCallback(lambda x:record.addAugmentInformation(x))
+
+ yield record
+
+ except KeyError:
+ pass
+
+ def multiQuery(directory, queries, recordTypes, attrs):
+ byGUID = { }
+ sets = []
+
+ caseInsensitive = True
+ for compound in queries:
+ compound = compound.generate()
+
+ self.log_debug("Calling OD: Types %s, Query %s" %
+ (recordTypes, compound))
+
+ try:
+ queryResults = lookupMethod(
+ directory,
+ compound,
+ caseInsensitive,
+ recordTypes,
+ attrs,
+ )
+
+ newSet = set()
+ for recordName, data in queryResults:
+ guid = data.get(dsattributes.kDS1AttrGeneratedUID, None)
+ if guid:
+ byGUID[guid] = (recordName, data)
+ newSet.add(guid)
+
+ sets.append(newSet)
+
+ except self.odModule.ODError, e:
+ self.log_error("Ignoring OD Error: %d %s" %
+ (e.message[1], e.message[0]))
+ continue
+
+ results = []
+ for guid in set.intersection(*sets):
+ recordName, data = byGUID.get(guid, None)
+ if data is not None:
+ results.append((data[dsattributes.kDSNAttrRecordName], data))
+ return results
+
+ queries = buildQueriesFromTokens(tokens, self._ODFields)
+
+ # Starting with the record types corresponding to the context...
+ recordTypes = self.recordTypesForSearchContext(context)
+ # ...limit to the types this service supports...
+ recordTypes = [r for r in recordTypes if r in self.recordTypes()]
+ # ...and map those to OD representations...
+ recordTypes = [self._toODRecordTypes[r] for r in recordTypes]
+
+ if recordTypes:
+ results = multiQuery(
+ self.directory,
+ queries,
+ recordTypes,
+ [
+ dsattributes.kDS1AttrGeneratedUID,
+ dsattributes.kDSNAttrRecordName,
+ dsattributes.kDSNAttrAltSecurityIdentities,
+ dsattributes.kDSNAttrRecordType,
+ dsattributes.kDS1AttrDistinguishedName,
+ dsattributes.kDS1AttrFirstName,
+ dsattributes.kDS1AttrLastName,
+ dsattributes.kDSNAttrEMailAddress,
+ dsattributes.kDSNAttrMetaNodeLocation,
+ dsattributes.kDSNAttrGroupMembers,
+ dsattributes.kDSNAttrNestedGroups,
+ ]
+ )
+ return succeed(collectResults(results))
+ else:
+ return succeed([])
+
+
def recordsMatchingFields(self, fields, operand="or", recordType=None,
lookupMethod=None):
@@ -1101,7 +1289,36 @@
return queries
+def buildQueriesFromTokens(tokens, mapping):
+ """
+ OD /Local doesn't support nested complex queries, so create a list of
+ complex queries that will be ANDed together in recordsMatchingTokens()
+ @param tokens: The tokens to search on
+ @type tokens: C{list} of C{str}
+ @param mapping: The mapping of DirectoryRecord attributes to OD attributes
+ @type mapping: C{dict}
+ @return: A list of expression objects
+ @type: C{list}
+ """
+
+ if len(tokens) == 0:
+ return None
+
+ fields = ["fullName", "emailAddresses"]
+
+ results = []
+ for token in tokens:
+ queries = []
+ for field in fields:
+ ODField = mapping[field]['odField']
+ query = dsquery.match(ODField, token, "contains")
+ queries.append(query)
+ results.append(dsquery.expression(dsquery.expression.OR, queries))
+ return results
+
+
+
class OpenDirectoryRecord(CachingDirectoryRecord):
"""
OpenDirectory implementation of L{IDirectoryRecord}.
Modified: CalendarServer/trunk/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/twistedcaldav/directory/directory.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -77,6 +77,9 @@
recordType_groups = "groups"
recordType_locations = "locations"
recordType_resources = "resources"
+
+ searchContext_location = "location"
+ searchContext_attendee = "attendee"
def _generatedGUID(self):
if not hasattr(self, "_guid"):
@@ -225,7 +228,96 @@
return self.recordsMatchingFields(fields, operand=operand,
recordType=recordType)
+ def recordTypesForSearchContext(self, context):
+ """
+ Map calendar-user-search REPORT context value to applicable record types
+ @param context: The context value to map (either "location" or "attendee")
+ @type context: C{str}
+ @returns: The list of record types the context maps to
+ @rtype: C{list} of C{str}
+ """
+ if context == self.searchContext_location:
+ recordTypes = [self.recordType_locations]
+ elif context == self.searchContext_attendee:
+ recordTypes = [self.recordType_users, self.recordType_groups,
+ self.recordType_resources]
+ else:
+ recordTypes = list(self.recordTypes())
+ return recordTypes
+
+
+ def recordsMatchingTokens(self, tokens, context=None):
+ """
+ @param tokens: The tokens to search on
+ @type tokens: C{list} of C{str} (utf-8 bytes)
+ @param context: An indication of what the end user is searching
+ for, e.g. "attendee", "location"
+ @type context: C{str}
+ @return: a deferred sequence of L{IDirectoryRecord}s which
+ match the given tokens and optional context.
+
+ Each token is searched for within each record's full name and
+ email address; if each token is found within a record that
+ record is returned in the results.
+
+ If context is None, all record types are considered. If
+ context is "location", only locations are considered. If
+ context is "attendee", only users, groups, and resources
+ are considered.
+ """
+
+ # Default, bruteforce method; override with one optimized for each
+ # service
+
+ def fieldMatches(fieldValue, value):
+ if fieldValue is None:
+ return False
+ elif type(fieldValue) in types.StringTypes:
+ fieldValue = (fieldValue,)
+
+ for testValue in fieldValue:
+ testValue = testValue.lower()
+ value = value.lower()
+
+ try:
+ testValue.index(value)
+ return True
+ except ValueError:
+ pass
+
+ return False
+
+ def recordMatches(record):
+ for token in tokens:
+ for fieldName in ["fullName", "emailAddresses"]:
+ try:
+ fieldValue = getattr(record, fieldName)
+ if fieldMatches(fieldValue, token):
+ break
+ except AttributeError:
+ # No value
+ pass
+ else:
+ return False
+ return True
+
+
+ def yieldMatches(recordTypes):
+ try:
+ for recordType in [r for r in recordTypes if r in self.recordTypes()]:
+ for record in self.listRecords(recordType):
+ if recordMatches(record):
+ yield record
+
+ except UnknownRecordTypeError:
+ # Skip this service since it doesn't understand this record type
+ pass
+
+ recordTypes = self.recordTypesForSearchContext(context)
+ return succeed(yieldMatches(recordTypes))
+
+
def recordsMatchingFields(self, fields, operand="or", recordType=None):
# Default, bruteforce method; override with one optimized for each
# service
Modified: CalendarServer/trunk/twistedcaldav/directory/idirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/idirectory.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/twistedcaldav/directory/idirectory.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -87,6 +87,27 @@
match the given fields.
"""
+ def recordsMatchingTokens(tokens, context=None):
+ """
+ @param tokens: The tokens to search on
+ @type tokens: C{list} of C{str} (utf-8 bytes)
+ @param context: An indication of what the end user is searching
+ for, e.g. "attendee", "location"
+ @type context: C{str}
+ @return: a deferred sequence of L{IDirectoryRecord}s which
+ match the given tokens and optional context.
+
+ Each token is searched for within each record's full name and
+ email address; if each token is found within a record that
+ record is returned in the results.
+
+ If context is None, all record types are considered. If
+ context is "location", only locations are considered. If
+ context is "attendee", only users, groups, and resources
+ are considered.
+ """
+
+
def setRealm(realmName):
"""
Set a new realm name for this (and nested services if any)
Modified: CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -974,6 +974,89 @@
% (recordTypes, indexType, indexKey))
+ def recordsMatchingTokens(self, tokens, context=None):
+ """
+ @param tokens: The tokens to search on
+ @type tokens: C{list} of C{str} (utf-8 bytes)
+ @param context: An indication of what the end user is searching
+ for, e.g. "attendee", "location"
+ @type context: C{str}
+ @return: a deferred sequence of L{IDirectoryRecord}s which
+ match the given tokens and optional context.
+
+ Each token is searched for within each record's full name and
+ email address; if each token is found within a record that
+ record is returned in the results.
+
+ If context is None, all record types are considered. If
+ context is "location", only locations are considered. If
+ context is "attendee", only users, groups, and resources
+ are considered.
+ """
+ self.log_debug("Peforming calendar user search for %s (%s)" % (tokens, context))
+
+ records = []
+ recordTypes = self.recordTypesForSearchContext(context)
+ recordTypes = [r for r in recordTypes if r in self.recordTypes()]
+ guidAttr = self.rdnSchema["guidAttr"]
+
+ for recordType in recordTypes:
+ base = self.typeDNs[recordType]
+ scope = ldap.SCOPE_SUBTREE
+ filterstr = buildFilterFromTokens(self.rdnSchema[recordType]["mapping"],
+ tokens)
+
+ if filterstr is not None:
+ # Query the LDAP server
+ self.log_debug("LDAP search %s %s %s" %
+ (ldap.dn.dn2str(base), scope, filterstr))
+ results = self.timedSearch(ldap.dn.dn2str(base), scope,
+ filterstr=filterstr, attrlist=self.attrlist,
+ timeoutSeconds=self.requestTimeoutSeconds,
+ resultLimit=self.requestResultsLimit)
+ self.log_debug("LDAP search returned %d results" % (len(results),))
+ numMissingGuids = 0
+ numMissingRecordNames = 0
+ for dn, attrs in results:
+ dn = normalizeDNstr(dn)
+ # Skip if group restriction is in place and guid is not
+ # a member
+ if (recordType != self.recordType_groups and
+ self.restrictedGUIDs is not None):
+ if guidAttr:
+ guid = self._getUniqueLdapAttribute(attrs, guidAttr)
+ if guid not in self.restrictedGUIDs:
+ continue
+
+ try:
+ record = self._ldapResultToRecord(dn, attrs, recordType)
+
+ # For non-group records, if not enabled for calendaring do
+ # not include in principal property search results
+ if (recordType != self.recordType_groups):
+ if not record.enabledForCalendaring:
+ continue
+
+ records.append(record)
+
+ except MissingGuidException:
+ numMissingGuids += 1
+
+ except MissingRecordNameException:
+ numMissingRecordNames += 1
+
+ if numMissingGuids:
+ self.log_warn("%d %s records are missing %s" %
+ (numMissingGuids, recordType, guidAttr))
+
+ if numMissingRecordNames:
+ self.log_warn("%d %s records are missing record name" %
+ (numMissingRecordNames, recordType))
+
+ self.log_debug("Calendar user search matched %d records" % (len(records),))
+ return succeed(records)
+
+
def recordsMatchingFields(self, fields, operand="or", recordType=None):
"""
Carries out the work of a principal-property-search against LDAP
@@ -1248,6 +1331,58 @@
return filterstr
+def buildFilterFromTokens(mapping, tokens):
+ """
+ Create an LDAP filter string from a list of query tokens. Each token is
+ searched for in each LDAP attribute corresponding to "fullName" and
+ "emailAddresses" (could be multiple LDAP fields for either).
+
+ @param mapping: A dict mapping internal directory attribute names to ldap names.
+ @type mapping: C{dict}
+ @param tokens: The list of tokens to search for
+ @type tokens: C{list}
+ @return: An LDAP filterstr
+ @rtype: C{str}
+ """
+
+ filterStr = None
+ tokens = [ldapEsc(t) for t in tokens if len(t) > 2]
+ if len(tokens) == 0:
+ return None
+
+ attributes = ["fullName", "emailAddresses"]
+
+ ldapFields = []
+ for attribute in attributes:
+ ldapField = mapping.get(attribute, None)
+ if ldapField:
+ if isinstance(ldapField, str):
+ ldapFields.append(ldapField)
+ else:
+ ldapFields.extend(ldapField)
+
+ if len(ldapFields) == 0:
+ return None
+
+ tokenFragments = []
+ for token in tokens:
+ fragments = []
+ for ldapField in ldapFields:
+ fragments.append("(%s=*%s*)" % (ldapField, token))
+ if len(fragments) == 1:
+ tokenFragment = fragments[0]
+ else:
+ tokenFragment = "(|%s)" % ("".join(fragments),)
+ tokenFragments.append(tokenFragment)
+
+ if len(tokenFragments) == 1:
+ filterStr = tokenFragments[0]
+ else:
+ filterStr = "(&%s)" % ("".join(tokenFragments),)
+
+ return filterStr
+
+
class LdapDirectoryRecord(CachingDirectoryRecord):
"""
LDAP implementation of L{IDirectoryRecord}.
Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -56,7 +56,7 @@
from twistedcaldav import caldavxml, customxml
from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin
from twistedcaldav.config import config
-from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.customxml import calendarserver_namespace, CalendarUserSearch
from twistedcaldav.directory.augment import allowedAutoScheduleModes
from twistedcaldav.directory.common import uidsResourceName
from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
@@ -364,6 +364,15 @@
return (self,)
+ def supportedReports(self):
+ """
+ Adds the CalendarUserSearch REPORT
+ """
+ result = super(DirectoryPrincipalProvisioningResource, self).supportedReports()
+ result.append(davxml.Report(CalendarUserSearch(),))
+ return result
+
+
class DirectoryPrincipalTypeProvisioningResource (DirectoryProvisioningResource):
"""
Collection resource which provisions directory principals of a
Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_buildquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_buildquery.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_buildquery.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -15,7 +15,7 @@
##
from twistedcaldav.test.util import TestCase
-from twistedcaldav.directory.appleopendirectory import buildQueries, OpenDirectoryService
+from twistedcaldav.directory.appleopendirectory import buildQueries, buildQueriesFromTokens, OpenDirectoryService
from calendarserver.platform.darwin.od import dsattributes
class BuildQueryTests(TestCase):
@@ -103,3 +103,23 @@
{
}
)
+
+ def test_buildQueryFromTokens(self):
+ results = buildQueriesFromTokens([], OpenDirectoryService._ODFields)
+ self.assertEquals(results, None)
+
+ results = buildQueriesFromTokens(["foo"], OpenDirectoryService._ODFields)
+ self.assertEquals(
+ results[0].generate(),
+ "(|(dsAttrTypeStandard:RealName=*foo*)(dsAttrTypeStandard:EMailAddress=*foo*))"
+ )
+
+ results = buildQueriesFromTokens(["foo", "bar"], OpenDirectoryService._ODFields)
+ self.assertEquals(
+ results[0].generate(),
+ "(|(dsAttrTypeStandard:RealName=*foo*)(dsAttrTypeStandard:EMailAddress=*foo*))"
+ )
+ self.assertEquals(
+ results[1].generate(),
+ "(|(dsAttrTypeStandard:RealName=*bar*)(dsAttrTypeStandard:EMailAddress=*bar*))"
+ )
Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -583,7 +583,62 @@
}
)
+class RecordsMatchingTokensTests(TestCase):
+ @inlineCallbacks
+ def setUp(self):
+ super(RecordsMatchingTokensTests, self).setUp()
+
+ self.directoryService = XMLDirectoryService(
+ {
+ 'xmlFile' : xmlFile,
+ 'augmentService' :
+ augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
+ }
+ )
+ calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
+
+ # Set up a principals hierarchy for each service we're testing with
+ self.principalRootResources = {}
+ name = self.directoryService.__class__.__name__
+ url = "/" + name + "/"
+
+ provisioningResource = DirectoryPrincipalProvisioningResource(url, self.directoryService)
+
+ self.site.resource.putChild(name, provisioningResource)
+
+ self.principalRootResources[self.directoryService.__class__.__name__] = provisioningResource
+
+ yield XMLCalendarUserProxyLoader(proxiesFile.path).updateProxyDB()
+
+ def tearDown(self):
+ """ Empty the proxy db between tests """
+ return calendaruserproxy.ProxyDBService.clean()
+
+ @inlineCallbacks
+ def test_recordsMatchingTokens(self):
+ """
+ Exercise the default recordsMatchingTokens implementation
+ """
+ records = list((yield self.directoryService.recordsMatchingTokens(["Use", "01"])))
+ self.assertEquals(len(records), 1)
+ self.assertEquals(records[0].shortNames[0], "user01")
+
+ records = list((yield self.directoryService.recordsMatchingTokens(['"quotey"'],
+ context=self.directoryService.searchContext_attendee)))
+ self.assertEquals(len(records), 1)
+ self.assertEquals(records[0].shortNames[0], "doublequotes")
+
+ records = list((yield self.directoryService.recordsMatchingTokens(["coast"])))
+ self.assertEquals(len(records), 5)
+
+ records = list((yield self.directoryService.recordsMatchingTokens(["poll"],
+ context=self.directoryService.searchContext_location)))
+ self.assertEquals(len(records), 1)
+ self.assertEquals(records[0].shortNames[0], "apollo")
+
+
+
class GUIDTests(TestCase):
def setUp(self):
Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -16,7 +16,7 @@
try:
from twistedcaldav.directory.ldapdirectory import (
- buildFilter, LdapDirectoryService,
+ buildFilter, buildFilterFromTokens, LdapDirectoryService,
MissingGuidException, MissingRecordNameException,
normalizeDNstr, dnContainedIn
)
@@ -193,6 +193,79 @@
entry["expected"]
)
+
+ class BuildFilterFromTokensTestCase(TestCase):
+
+ def test_buildFilterFromTokens(self):
+
+ entries = [
+ {
+ "tokens" : ["foo"],
+ "mapping" : {
+ "fullName" : "cn",
+ "emailAddresses" : "mail",
+ },
+ "expected" : "(|(cn=*foo*)(mail=*foo*))",
+ },
+ {
+ "tokens" : ["foo"],
+ "mapping" : {
+ "fullName" : "cn",
+ "emailAddresses" : ["mail", "mailAliases"],
+ },
+ "expected" : "(|(cn=*foo*)(mail=*foo*)(mailAliases=*foo*))",
+ },
+ {
+ "tokens" : [],
+ "mapping" : {
+ "fullName" : "cn",
+ "emailAddresses" : "mail",
+ },
+ "expected" : None,
+ },
+ {
+ "tokens" : ["foo", "bar"],
+ "mapping" : { },
+ "expected" : None,
+ },
+ {
+ "tokens" : ["foo", "bar"],
+ "mapping" : {
+ "emailAddresses" : "mail",
+ },
+ "expected" : "(&(mail=*foo*)(mail=*bar*))",
+ },
+ {
+ "tokens" : ["foo", "bar"],
+ "mapping" : {
+ "fullName" : "cn",
+ "emailAddresses" : "mail",
+ },
+ "expected" : "(&(|(cn=*foo*)(mail=*foo*))(|(cn=*bar*)(mail=*bar*)))",
+ },
+ {
+ "tokens" : ["foo", "bar"],
+ "mapping" : {
+ "fullName" : "cn",
+ "emailAddresses" : ["mail", "mailAliases"],
+ },
+ "expected" : "(&(|(cn=*foo*)(mail=*foo*)(mailAliases=*foo*))(|(cn=*bar*)(mail=*bar*)(mailAliases=*bar*)))",
+ },
+ {
+ "tokens" : ["foo", "bar", "baz("],
+ "mapping" : {
+ "fullName" : "cn",
+ "emailAddresses" : "mail",
+ },
+ "expected" : "(&(|(cn=*foo*)(mail=*foo*))(|(cn=*bar*)(mail=*bar*))(|(cn=*baz\\28*)(mail=*baz\\28*)))",
+ },
+ ]
+ for entry in entries:
+ self.assertEquals(
+ buildFilterFromTokens(entry["mapping"], entry["tokens"]),
+ entry["expected"]
+ )
+
class StubList(object):
def __init__(self, wrapper):
self.ldap = wrapper
Modified: CalendarServer/trunk/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/extensions.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/twistedcaldav/extensions.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -249,7 +249,95 @@
returnValue(MultiStatusResponse(responses))
+ @inlineCallbacks
+ def report_http___calendarserver_org_ns__calendar_user_search(self, request,
+ calendar_user_search):
+ """
+ Generate a calendar-user-search REPORT.
+ @param request: Request object
+ @param calendar_user_search: CalendarUserSearch object
+ """
+
+ # Verify root element
+ if not isinstance(calendar_user_search, customxml.CalendarUserSearch):
+ msg = "%s expected as root element, not %s." % (customxml.CalendarUserSearch.sname(), calendar_user_search.sname())
+ log.warn(msg)
+ raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
+
+ # Only handle Depth: 0
+ depth = request.headers.getHeader("depth", "0")
+ if depth != "0":
+ log.err("Error in calendar-user-search REPORT, Depth set to %s" % (depth,))
+ raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,)))
+
+ tokens, context, applyTo, clientLimit, propElement = extractCalendarUserSearchData(calendar_user_search)
+
+ # Run report
+ resultsWereLimited = None
+ resources = []
+ if applyTo or not hasattr(self, "directory"):
+ for principalCollection in self.principalCollections():
+ uri = principalCollection.principalCollectionURL()
+ resource = (yield request.locateResource(uri))
+ if resource:
+ resources.append((resource, uri))
+ else:
+ resources.append((self, request.uri))
+
+ # We need to access a directory service
+ principalCollection = resources[0][0]
+ dir = principalCollection.directory
+
+ matchingResources = []
+ matchcount = 0
+
+ records = (yield dir.recordsMatchingTokens(tokens, context=context))
+
+ for record in records:
+ resource = principalCollection.principalForRecord(record)
+ if resource:
+ matchingResources.append(resource)
+
+ # We've determined this is a matching resource
+ matchcount += 1
+ if clientLimit is not None and matchcount >= clientLimit:
+ resultsWereLimited = ("client", matchcount)
+ break
+ if matchcount >= config.MaxPrincipalSearchReportResults:
+ resultsWereLimited = ("server", matchcount)
+ break
+
+ # Generate the response
+ responses = []
+ for resource in matchingResources:
+ url = resource.url()
+ yield prop_common.responseForHref(
+ request,
+ responses,
+ element.HRef.fromString(url),
+ resource,
+ prop_common.propertyListForResource,
+ propElement
+ )
+
+ if resultsWereLimited is not None:
+ if resultsWereLimited[0] == "server":
+ log.err("Too many matching resources in "
+ "calendar-user-search report")
+ responses.append(element.StatusResponse(
+ element.HRef.fromString(request.uri),
+ element.Status.fromResponseCode(
+ responsecode.INSUFFICIENT_STORAGE_SPACE
+ ),
+ element.Error(element.NumberOfMatchesWithinLimits()),
+ element.ResponseDescription("Results limited by %s at %d"
+ % resultsWereLimited),
+ ))
+ returnValue(MultiStatusResponse(responses))
+
+
+
class DirectoryElement(Element):
"""
A L{DirectoryElement} is an L{Element} for rendering the contents of a
@@ -868,3 +956,41 @@
)
return self._data
+def extractCalendarUserSearchData(doc):
+ """
+ Extract relevant info from a CalendarUserSearch document
+
+ @param doc: CalendarUserSearch object to extract info from
+ @return: A tuple containing:
+ the list of tokens
+ the context string
+ the applyTo boolean
+ the clientLimit integer
+ the propElement containing the properties to return
+ """
+ context = doc.attributes.get("context", None)
+ applyTo = False
+ tokens = []
+ clientLimit = None
+ for child in doc.children:
+ if child.qname() == (dav_namespace, "prop"):
+ propElement = child
+
+ elif child.qname() == (dav_namespace,
+ "apply-to-principal-collection-set"):
+ applyTo = True
+
+ elif child.qname() == (calendarserver_namespace, "search-token"):
+ tokens.append(str(child))
+
+ elif child.qname() == (calendarserver_namespace, "limit"):
+ try:
+ nresults = child.childOfType(customxml.NResults)
+ clientLimit = int(str(nresults))
+ except (TypeError, ValueError,):
+ msg = "Bad XML: unknown value for <limit> element"
+ log.warn(msg)
+ raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
+
+ return tokens, context, applyTo, clientLimit, propElement
+
Modified: CalendarServer/trunk/twistedcaldav/test/test_extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_extensions.py 2012-08-10 18:28:21 UTC (rev 9552)
+++ CalendarServer/trunk/twistedcaldav/test/test_extensions.py 2012-08-10 21:24:34 UTC (rev 9553)
@@ -17,6 +17,7 @@
from twext.python.filepath import CachingFilePath as FilePath
from txdav.xml.element import WebDAVElement, ResourceType
+from txdav.xml.parser import WebDAVDocument
from twext.web2.http_headers import MimeType
from twext.web2.static import MetaDataMixin
@@ -24,7 +25,7 @@
from twisted.trial.unittest import TestCase
from twisted.web.microdom import parseString
-from twistedcaldav.extensions import DAVFile, DAVResourceWithChildrenMixin
+from twistedcaldav.extensions import DAVFile, DAVResourceWithChildrenMixin, extractCalendarUserSearchData
from xml.etree.cElementTree import XML
@@ -219,3 +220,46 @@
self.assertEquals(len(result), 2)
self.assertEquals(result[0].name, 'cheese')
self.assertEquals(result[1], ['burger'])
+
+
+class CalendarUserSearchTests(TestCase):
+ def test_extractCalendarUserSearchData(self):
+ """
+ Exercise the parser for calendar-user-search documents
+ """
+
+ data = """<B:calendar-user-search xmlns:A="DAV:" xmlns:B="http://calendarserver.org/ns/" context="attendee">
+ <B:search-token>morgen</B:search-token>
+ <A:prop>
+ <A:principal-URL/>
+ <A:displayname/>
+ </A:prop>
+</B:calendar-user-search>
+"""
+ doc = WebDAVDocument.fromString(data)
+ tokens, context, applyTo, clientLimit, propElement = extractCalendarUserSearchData(doc.root_element)
+ self.assertEquals(tokens, ["morgen"])
+ self.assertEquals(context, "attendee")
+ self.assertFalse(applyTo)
+ self.assertEquals(clientLimit, None)
+
+
+ data = """<B:calendar-user-search xmlns:A="DAV:" xmlns:B="http://calendarserver.org/ns/">
+ <B:search-token>morgen</B:search-token>
+ <B:search-token>sagen</B:search-token>
+ <B:limit>
+ <B:nresults>42</B:nresults>
+ </B:limit>
+ <A:prop>
+ <A:principal-URL/>
+ <A:displayname/>
+ </A:prop>
+ <A:apply-to-principal-collection-set/>
+</B:calendar-user-search>
+"""
+ doc = WebDAVDocument.fromString(data)
+ tokens, context, applyTo, clientLimit, propElement = extractCalendarUserSearchData(doc.root_element)
+ self.assertEquals(tokens, ["morgen", "sagen"])
+ self.assertEquals(context, None)
+ self.assertTrue(applyTo)
+ self.assertEquals(clientLimit, 42)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120810/386ca313/attachment-0001.html>
More information about the calendarserver-changes
mailing list