def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy): """ Generate a free-busy REPORT. (CalDAV-access-09, section 7.8) """ if not self.isCollection(): log.error("freebusy report is only allowed on collection resources {s!r}", s=self) raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Not a calendar collection")) if freebusy.qname() != (caldavxml.caldav_namespace, "free-busy-query"): raise ValueError("{CalDAV:}free-busy-query expected as root element, not %s." % (freebusy.sname(),)) timerange = freebusy.timerange if not timerange.valid(): raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid time-range specified")) fbset = [] accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes()) if accepted_type is None: raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type")) def getCalendarList(calresource, uri): # @UnusedVariable """ Store the calendars that match the query in L{fbset} which will then be used with the freebusy query. @param calresource: the L{CalDAVResource} for a calendar collection. @param uri: the uri for the calendar collection resource. """ fbset.append(calresource._newStoreObject) return succeed(True) # Run report taking depth into account depth = request.headers.getHeader("depth", "0") yield report_common.applyToCalendarCollections(self, request, request.uri, depth, getCalendarList, (caldavxml.ReadFreeBusy(),)) # Do the actual freebusy query against the set of matched calendars principal = yield self.resourceOwnerPrincipal(request) organizer = recipient = LocalCalendarUser(principal.canonicalCalendarUserAddress(), principal.record) timerange = Period(timerange.start, timerange.end) try: fbresult = yield FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange).generateAttendeeFreeBusyResponse(fbset=fbset, method=None) except NumberOfMatchesWithinLimits: log.error("Too many matching components in free-busy report") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits(), "Too many components" )) except TimeRangeLowerLimit, e: raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, caldavxml.MinDateTime(), "Time-range value too far in the past. Must be on or after %s." % (str(e.limit),) ))
# Run report taking depth into account try: depth = request.headers.getHeader("depth", "0") yield report_common.applyToCalendarCollections(self, request, request.uri, depth, doQuery, (davxml.Read(),)) except TooManyInstancesError, ex: log.error("Too many instances need to be computed in calendar-query report") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, MaxInstances.fromString(str(ex.max_allowed)), "Too many instances", )) except NumberOfMatchesWithinLimits: log.error("Too many matching components in calendar-query report") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits(), "Too many components", )) except TimeRangeLowerLimit, e: raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, caldavxml.MinDateTime(), "Time-range value too far in the past. Must be on or after %s." % (str(e.limit),) )) except TimeRangeUpperLimit, e: raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, caldavxml.MaxDateTime(), "Time-range value too far in the future. Must be on or before %s." % (str(e.limit),) ))
def report_DAV__principal_property_search(self, request, principal_property_search): """ Generate a principal-property-search REPORT. (RFC 3744, section 9.4) """ # Verify root element if not isinstance(principal_property_search, element.PrincipalPropertySearch): raise ValueError("%s expected as root element, not %s." % (element.PrincipalPropertySearch.sname(), principal_property_search.sname())) # Only handle Depth: 0 depth = request.headers.getHeader("depth", "0") if depth != "0": log.error("Error in prinicpal-property-search REPORT, Depth set to %s" % (depth,)) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,))) # Get a single DAV:prop element from the REPORT request body propertiesForResource = None propElement = None propertySearches = [] applyTo = False for child in principal_property_search.children: if child.qname() == (dav_namespace, "prop"): propertiesForResource = prop_common.propertyListForResource propElement = child elif child.qname() == (dav_namespace, "apply-to-principal-collection-set"): applyTo = True elif child.qname() == (dav_namespace, "property-search"): props = child.childOfType(element.PropertyContainer) props.removeWhitespaceNodes() match = child.childOfType(element.Match) propertySearches.append((props.children, str(match).lower())) def nodeMatch(node, match): """ See if the content of the supplied node matches the supplied text. Try to follow the matching guidance in rfc3744 section 9.4.1. @param prop: the property element to match. @param match: the text to match against. @return: True if the property matches, False otherwise. """ node.removeWhitespaceNodes() for child in node.children: if isinstance(child, PCDATAElement): comp = str(child).lower() if comp.find(match) != -1: return True else: return nodeMatch(child, match) else: return False def propertySearch(resource, request): """ Test the resource to see if it contains properties matching the property-search specification in this report. @param resource: the L{DAVFile} for the resource to test. @param request: the current request. @return: True if the resource has matching properties, False otherwise. """ for props, match in propertySearches: # Test each property for prop in props: try: propvalue = waitForDeferred(resource.readProperty(prop.qname(), request)) yield propvalue propvalue = propvalue.getResult() if propvalue and not nodeMatch(propvalue, match): yield False return except HTTPError: # No property => no match yield False return yield True propertySearch = deferredGenerator(propertySearch) # Run report try: resources = [] responses = [] matchcount = 0 if applyTo: for principalCollection in self.principalCollections(): uri = principalCollection.principalCollectionURL() resource = waitForDeferred(request.locateResource(uri)) yield resource resource = resource.getResult() if resource: resources.append((resource, uri)) else: resources.append((self, request.uri)) # Loop over all collections and principal resources within for resource, ruri in resources: # Do some optimisation of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = waitForDeferred(resource.inheritedACEsforChildren(request)) yield filteredaces filteredaces = filteredaces.getResult() children = [] d = waitForDeferred(resource.findChildren("infinity", request, lambda x, y: children.append((x,y)), privileges=(element.Read(),), inherited_aces=filteredaces)) yield d d.getResult() for child, uri in children: if isPrincipalResource(child): d = waitForDeferred(propertySearch(child, request)) yield d d = d.getResult() if d: # Check size of results is within limit matchcount += 1 if matchcount > max_number_of_matches: raise NumberOfMatchesWithinLimits(max_number_of_matches) d = waitForDeferred(prop_common.responseForHref( request, responses, element.HRef.fromString(uri), child, propertiesForResource, propElement )) yield d d.getResult() except NumberOfMatchesWithinLimits: log.error("Too many matching components in prinicpal-property-search report") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, element.NumberOfMatchesWithinLimits() )) yield MultiStatusResponse(responses)
def report_urn_ietf_params_xml_ns_caldav_free_busy_query( self, request, freebusy): #@UnusedVariable """ Generate a free-busy REPORT. (CalDAV-access-09, section 7.8) """ if not self.isCollection(): log.error( "freebusy report is only allowed on collection resources %s" % (self, )) raise HTTPError( StatusResponse(responsecode.FORBIDDEN, "Not a calendar collection")) if freebusy.qname() != (caldavxml.caldav_namespace, "free-busy-query"): raise ValueError( "{CalDAV:}free-busy-query expected as root element, not %s." % (freebusy.sname(), )) timerange = freebusy.timerange if not timerange.valid(): raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, "Invalid time-range specified")) # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE fbinfo = ([], [], []) matchcount = [0] accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes()) if accepted_type is None: raise HTTPError( StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type")) def generateFreeBusyInfo(calresource, uri): #@UnusedVariable """ Run a free busy report on the specified calendar collection accumulating the free busy info for later processing. @param calresource: the L{CalDAVResource} for a calendar collection. @param uri: the uri for the calendar collecton resource. """ def _gotResult(result): matchcount[0] = result return True d = report_common.generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchcount[0]) d.addCallback(_gotResult) return d # Run report taking depth into account try: depth = request.headers.getHeader("depth", "0") yield report_common.applyToCalendarCollections( self, request, request.uri, depth, generateFreeBusyInfo, (caldavxml.ReadFreeBusy(), )) except NumberOfMatchesWithinLimits: log.error("Too many matching components in free-busy report") raise HTTPError( ErrorResponse(responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits(), "Too many components")) except TimeRangeLowerLimit, e: raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, caldavxml.MinDateTime(), "Time-range value too far in the past. Must be on or after %s." % (str(e.limit), )))
def report_DAV__acl_principal_prop_set(self, request, acl_prinicpal_prop_set): """ Generate an acl-prinicpal-prop-set REPORT. (RFC 3744, section 9.2) """ # Verify root element if not isinstance(acl_prinicpal_prop_set, davxml.ACLPrincipalPropSet): raise ValueError("%s expected as root element, not %s." % (davxml.ACLPrincipalPropSet.sname(), acl_prinicpal_prop_set.sname())) # Depth must be "0" depth = request.headers.getHeader("depth", "0") if depth != "0": log.error("Error in prinicpal-prop-set REPORT, Depth set to %s" % (depth, )) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth, ))) # # Check authentication and access controls # x = waitForDeferred(self.authorize(request, (davxml.ReadACL(), ))) yield x x.getResult() # Get a single DAV:prop element from the REPORT request body propertiesForResource = None propElement = None for child in acl_prinicpal_prop_set.children: if child.qname() == ("DAV:", "prop"): if propertiesForResource is not None: log.error("Only one DAV:prop element allowed") raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, "Only one DAV:prop element allowed")) propertiesForResource = prop_common.propertyListForResource propElement = child if propertiesForResource is None: log.error( "Error in acl-principal-prop-set REPORT, no DAV:prop element") raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, "No DAV:prop element")) # Enumerate principals on ACL in current resource principals = [] acl = waitForDeferred(self.accessControlList(request)) yield acl acl = acl.getResult() for ace in acl.children: resolved = waitForDeferred( self.resolvePrincipal(ace.principal.children[0], request)) yield resolved resolved = resolved.getResult() if resolved is not None and resolved not in principals: principals.append(resolved) # Run report for each referenced principal try: responses = [] matchcount = 0 for principal in principals: # Check size of results is within limit matchcount += 1 if matchcount > max_number_of_matches: raise NumberOfMatchesWithinLimits(max_number_of_matches) resource = waitForDeferred(request.locateResource(str(principal))) yield resource resource = resource.getResult() if resource is not None: # # Check authentication and access controls # x = waitForDeferred( resource.authorize(request, (davxml.Read(), ))) yield x try: x.getResult() except HTTPError: responses.append( davxml.StatusResponse( principal, davxml.Status.fromResponseCode( responsecode.FORBIDDEN))) else: d = waitForDeferred( prop_common.responseForHref(request, responses, principal, resource, propertiesForResource, propElement)) yield d d.getResult() else: log.error("Requested principal resource not found: %s" % (str(principal), )) responses.append( davxml.StatusResponse( principal, davxml.Status.fromResponseCode( responsecode.NOT_FOUND))) except NumberOfMatchesWithinLimits: log.error("Too many matching components") raise HTTPError( ErrorResponse(responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits())) yield MultiStatusResponse(responses)
class DirectoryPrincipalPropertySearchMixIn(object): @inlineCallbacks def report_DAV__principal_property_search(self, request, principal_property_search): """ Generate a principal-property-search REPORT. (RFC 3744, section 9.4) Overrides twisted implementation, targeting only directory-enabled searching. """ # Verify root element if not isinstance(principal_property_search, element.PrincipalPropertySearch): msg = "%s expected as root element, not %s." % ( element.PrincipalPropertySearch.sname(), principal_property_search.sname()) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # Should we AND (the default) or OR (if test="anyof")? testMode = principal_property_search.attributes.get("test", "allof") if testMode not in ("allof", "anyof"): msg = "Bad XML: unknown value for test attribute: %s" % ( testMode, ) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) operand = Operand.AND if testMode == "allof" else Operand.OR # Are we narrowing results down to a single CUTYPE? cuType = principal_property_search.attributes.get("type", None) if cuType not in ("INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", None): msg = "Bad XML: unknown value for type attribute: %s" % (cuType, ) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # Only handle Depth: 0 depth = request.headers.getHeader("depth", "0") if depth != "0": log.error( "Error in principal-property-search REPORT, Depth set to %s" % (depth, )) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth, ))) # Get any limit value from xml clientLimit = None # Get a single DAV:prop element from the REPORT request body propertiesForResource = None propElement = None propertySearches = [] applyTo = False for child in principal_property_search.children: if child.qname() == (dav_namespace, "prop"): propertiesForResource = prop_common.propertyListForResource propElement = child elif child.qname() == (dav_namespace, "apply-to-principal-collection-set"): applyTo = True elif child.qname() == (dav_namespace, "property-search"): props = child.childOfType(element.PropertyContainer) props.removeWhitespaceNodes() match = child.childOfType(element.Match) caseless = match.attributes.get("caseless", "yes") if caseless not in ("yes", "no"): msg = "Bad XML: unknown value for caseless attribute: %s" % ( caseless, ) log.warn(msg) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, msg)) caseless = (caseless == "yes") matchType = match.attributes.get("match-type", u"contains").encode("utf-8") if matchType not in ("starts-with", "contains", "equals"): msg = "Bad XML: unknown value for match-type attribute: %s" % ( matchType, ) log.warn(msg) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, msg)) # Convert to twext.who.expression form matchType = { "starts-with": MatchType.startsWith, "contains": MatchType.contains, "equals": MatchType.equals }.get(matchType) matchFlags = MatchFlags.caseInsensitive if caseless else MatchFlags.none # Ignore any query strings under three letters matchText = match.toString() # gives us unicode if len(matchText) >= 3: propertySearches.append( (props.children, matchText, matchFlags, matchType)) 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)) # 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] if not hasattr(principalCollection, "directory"): # Use Twisted's implementation instead in this case result = (yield super(DirectoryPrincipalPropertySearchMixIn, self).report_DAV__principal_property_search( request, principal_property_search)) returnValue(result) dir = principalCollection.directory # See if we can take advantage of the directory fields = [] nonDirectorySearches = [] for props, match, matchFlags, matchType in propertySearches: nonDirectoryProps = [] for prop in props: try: fieldName, match = principalCollection.propertyToField( prop, match) except ValueError, e: raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, str(e))) if fieldName: fields.append((fieldName, match, matchFlags, matchType)) else: nonDirectoryProps.append(prop) if nonDirectoryProps: nonDirectorySearches.append( (nonDirectoryProps, match, matchFlags, matchType)) matchingResources = [] matchcount = 0 # nonDirectorySearches are ignored if fields: records = (yield dir.recordsMatchingFieldsWithCUType(fields, operand=operand, cuType=cuType)) for record in records: resource = yield 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, propertiesForResource, propElement) if resultsWereLimited is not None: if resultsWereLimited[0] == "server": log.error( "Too many matching resources in principal-property-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))
def report_http___calendarserver_org_ns__calendarserver_principal_search( self, request, calendarserver_principal_search): """ Generate a calendarserver-principal-search REPORT. @param request: Request object @param calendarserver_principal_search: CalendarServerPrincipalSearch object """ # Verify root element if not isinstance(calendarserver_principal_search, customxml.CalendarServerPrincipalSearch): msg = "%s expected as root element, not %s." % ( customxml.CalendarServerPrincipalSearch.sname(), calendarserver_principal_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.error( "Error in calendarserver-principal-search REPORT, Depth set to %s" % (depth, )) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth, ))) tokens, context, applyTo, clientLimit, propElement = extractCalendarServerPrincipalSearchData( calendarserver_principal_search) if not validateTokens(tokens): raise HTTPError( StatusResponse(responsecode.FORBIDDEN, "Insufficient search token length")) # 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 = yield 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.error( "Too many matching resources in calendarserver-principal-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))
def report_DAV__sync_collection(self, request, sync_collection): """ Generate a sync-collection REPORT. """ # These resource support the report if not config.EnableSyncReport or element.Report(element.SyncCollection(),) not in self.supportedReports(): log.error("sync-collection report is only allowed on calendar/inbox/addressbook/notification collection resources {s!r}", s=self) raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, element.SupportedReport(), "Report not supported on this resource", )) responses = [] # Do not support limit if sync_collection.sync_limit is not None: raise HTTPError(ErrorResponse( responsecode.INSUFFICIENT_STORAGE_SPACE, element.NumberOfMatchesWithinLimits(), "Report limit not supported", )) # Process Depth and sync-level for backwards compatibility # Use sync-level if present and ignore Depth, else use Depth if sync_collection.sync_level: depth = sync_collection.sync_level if depth == "infinite": depth = "infinity" descriptor = "DAV:sync-level" else: depth = request.headers.getHeader("depth", None) descriptor = "Depth header without DAV:sync-level" if depth not in ("1", "infinity"): log.error("sync-collection report with invalid depth header: {d}", d=depth) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid %s value" % (descriptor,))) propertyreq = sync_collection.property.children if sync_collection.property else None # Do some optimization of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = (yield self.inheritedACEsforChildren(request)) changed, removed, notallowed, newtoken, resourceChanged = yield self.whatchanged(sync_collection.sync_token, depth) # Now determine which valid resources are readable and which are not ok_resources = [] forbidden_resources = [] if changed: yield self.findChildrenFaster( depth, request, lambda x, y: ok_resources.append((x, y)), lambda x, y: forbidden_resources.append((x, y)), None, None, changed, (element.Read(),), inherited_aces=filteredaces ) if resourceChanged: ok_resources.append((self, request.uri)) for child, child_uri in ok_resources: href = element.HRef.fromString(child_uri) try: if propertyreq: yield responseForHref( request, responses, href, child, functools.partial(_namedPropertiesForResource, dataAllowed=False, forbidden=False), propertyreq) else: responses.append(element.StatusResponse(element.HRef.fromString(href), element.Status.fromResponseCode(responsecode.OK))) except ConcurrentModification: # This can happen because of a race-condition between the # time we determine which resources exist and the deletion # of one of these resources in another request. In this # case, we ignore the now missing resource rather # than raise an error for the entire report. log.error("Missing resource during sync: {h}", h=href) for child, child_uri in forbidden_resources: href = element.HRef.fromString(child_uri) try: if propertyreq: yield responseForHref( request, responses, href, child, functools.partial(_namedPropertiesForResource, dataAllowed=False, forbidden=True), propertyreq) else: responses.append(element.StatusResponse(element.HRef.fromString(href), element.Status.fromResponseCode(responsecode.OK))) except ConcurrentModification: # This can happen because of a race-condition between the # time we determine which resources exist and the deletion # of one of these resources in another request. In this # case, we ignore the now missing resource rather # than raise an error for the entire report. log.error("Missing resource during sync: {h}", h=href) for name in removed: href = element.HRef.fromString(joinURL(request.uri, name)) responses.append(element.StatusResponse(element.HRef.fromString(href), element.Status.fromResponseCode(responsecode.NOT_FOUND))) for name in notallowed: href = element.HRef.fromString(joinURL(request.uri, name)) responses.append(element.StatusResponse(element.HRef.fromString(href), element.Status.fromResponseCode(responsecode.NOT_ALLOWED))) if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems["responses"] = len(responses) responses.append(element.SyncToken.fromString(newtoken)) returnValue(MultiStatusResponse(responses))
def report_urn_ietf_params_xml_ns_carddav_addressbook_query( self, request, addressbook_query): """ Generate an addressbook-query REPORT. (CardDAV, section 8.6) """ # Verify root element if addressbook_query.qname() != (carddav_namespace, "addressbook-query"): raise ValueError( "{CardDAV:}addressbook-query expected as root element, not {elementName}." .format(elementName=addressbook_query.sname())) if not self.isCollection(): parent = (yield self.locateParent(request, request.uri)) if not parent.isAddressBookCollection(): log.error( "addressbook-query report is not allowed on a resource outside of an address book collection {parent}", parent=self) raise HTTPError( StatusResponse( responsecode.FORBIDDEN, "Must be address book collection or address book resource") ) responses = [] xmlfilter = addressbook_query.filter filter = Filter(xmlfilter) query = addressbook_query.props limit = addressbook_query.limit assert query is not None if query.qname() == ("DAV:", "allprop"): propertiesForResource = report_common.allPropertiesForResource generate_address_data = False elif query.qname() == ("DAV:", "propname"): propertiesForResource = report_common.propertyNamesForResource generate_address_data = False elif query.qname() == ("DAV:", "prop"): propertiesForResource = report_common.propertyListForResource # Verify that any address-data element matches what we can handle result, message, generate_address_data = report_common.validPropertyListAddressDataTypeVersion( query) if not result: log.error(message) raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (carddav_namespace, "supported-address-data"), "Invalid address-data", )) else: raise AssertionError("We shouldn't be here") # Verify that the filter element is valid if (filter is None) or not filter.valid(): log.error("Invalid filter element: %r" % (filter, )) raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (carddav_namespace, "valid-filter"), "Invalid filter element", )) matchcount = [ 0, ] max_number_of_results = [ config.MaxQueryWithDataResults if generate_address_data else None, ] limited = [ False, ] if limit: clientLimit = int(str(limit.childOfType(NResults))) if max_number_of_results[ 0] is None or clientLimit < max_number_of_results[0]: max_number_of_results[0] = clientLimit @inlineCallbacks def doQuery(addrresource, uri): """ Run a query on the specified address book collection accumulating the query responses. @param addrresource: the L{CalDAVResource} for an address book collection. @param uri: the uri for the address book collecton resource. """ def checkMaxResults(): matchcount[0] += 1 if max_number_of_results[0] is not None and matchcount[ 0] > max_number_of_results[0]: raise NumberOfMatchesWithinLimits(max_number_of_results[0]) @inlineCallbacks def queryAddressBookObjectResource(resource, uri, name, vcard, query_ok=False): """ Run a query on the specified vcard. @param resource: the L{CalDAVResource} for the vcard. @param uri: the uri of the resource. @param name: the name of the resource. @param vcard: the L{Component} vcard read from the resource. """ if query_ok or filter.match(vcard): # Check size of results is within limit checkMaxResults() if name: href = davxml.HRef.fromString(joinURL(uri, name)) else: href = davxml.HRef.fromString(uri) try: yield report_common.responseForHref(request, responses, href, resource, propertiesForResource, query, vcard=vcard) except ConcurrentModification: # This can happen because of a race-condition between the # time we determine which resources exist and the deletion # of one of these resources in another request. In this # case, we ignore the now missing resource rather # than raise an error for the entire report. log.error("Missing resource during sync: {href}", href=href) @inlineCallbacks def queryDirectoryBackedAddressBook(directoryBackedAddressBook, addressBookFilter): """ """ results, limited[ 0] = yield directoryBackedAddressBook.doAddressBookDirectoryQuery( addressBookFilter, query, max_number_of_results[0]) for vCardResult in results: # match against original filter if different from addressBookFilter if addressBookFilter is filter or filter.match( (yield vCardResult.vCard())): # Check size of results is within limit checkMaxResults() try: yield report_common.responseForHref( request, responses, vCardResult.hRef(), vCardResult, propertiesForResource, query, vcard=(yield vCardResult.vCard())) except ConcurrentModification: # This can happen because of a race-condition between the # time we determine which resources exist and the deletion # of one of these resources in another request. In this # case, we ignore the now missing resource rather # than raise an error for the entire report. log.error("Missing resource during sync: {href}", href=vCardResult.hRef()) if not addrresource.isAddressBookCollection(): #do UID lookup on last part of uri resource_name = urllib.unquote(uri[uri.rfind("/") + 1:]) if resource_name.endswith(".vcf") and len(resource_name) > 4: # see if parent is directory backed address book parent = (yield addrresource.locateParent(request, uri)) # Check whether supplied resource is an address book or an address book object resource if addrresource.isAddressBookCollection(): if addrresource.isDirectoryBackedAddressBookCollection(): yield maybeDeferred(queryDirectoryBackedAddressBook, addrresource, filter) else: # Do some optimisation of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = (yield addrresource.inheritedACEsforChildren(request)) # Check for disabled access if filteredaces is not None: index_query_ok = True try: # Get list of children that match the search and have read access names = [ name for name, ignore_uid in ( yield addrresource.search(filter)) ] #@UnusedVariable except IndexedSearchException: names = yield addrresource.listChildren() index_query_ok = False if not names: return # Now determine which valid resources are readable and which are not ok_resources = [] yield addrresource.findChildrenFaster( "1", request, lambda x, y: ok_resources.append((x, y)), None, None, None, names, (davxml.Read(), ), inherited_aces=filteredaces) for child, child_uri in ok_resources: child_uri_name = child_uri[child_uri.rfind("/") + 1:] if generate_address_data or not index_query_ok: vcard = yield child.vCard() assert vcard is not None, "vCard {name} is missing from address book collection {collection!r}".format( name=child_uri_name, collection=self) else: vcard = None yield queryAddressBookObjectResource( child, uri, child_uri_name, vcard, query_ok=index_query_ok) else: handled = False resource_name = urllib.unquote(uri[uri.rfind("/") + 1:]) if resource_name.endswith(".vcf") and len(resource_name) > 4: # see if parent is directory backed address book parent = (yield addrresource.locateParent(request, uri)) if parent.isDirectoryBackedAddressBookCollection(): vCardFilter = carddavxml.Filter(*[ carddavxml.PropertyFilter( carddavxml.TextMatch.fromString( resource_name[:-4]), name="UID", # attributes ), ]) vCardFilter = Filter(vCardFilter) yield maybeDeferred(queryDirectoryBackedAddressBook, parent, vCardFilter) handled = True if not handled: vcard = yield addrresource.vCard() yield queryAddressBookObjectResource(addrresource, uri, None, vcard) if limited[0]: raise NumberOfMatchesWithinLimits(matchcount[0]) # Run report taking depth into account try: depth = request.headers.getHeader("depth", "0") yield report_common.applyToAddressBookCollections( self, request, request.uri, depth, doQuery, (davxml.Read(), )) except NumberOfMatchesWithinLimits, e: self.log.info( "Too many matching components in addressbook-query report. Limited to {limit} items", limit=e.maxLimit()) responses.append( davxml.StatusResponse( davxml.HRef.fromString(request.uri), davxml.Status.fromResponseCode( responsecode.INSUFFICIENT_STORAGE_SPACE), davxml.Error(davxml.NumberOfMatchesWithinLimits()), davxml.ResponseDescription( "Results limited to {limit} items".format( limit=e.maxLimit())), ))
def report_DAV__sync_collection(self, request, sync_collection): """ Generate a sync-collection REPORT. """ # These resource support the report if not config.EnableSyncReport or element.Report( element.SyncCollection(), ) not in self.supportedReports(): log.error( "sync-collection report is only allowed on calendar/inbox/addressbook/notification collection resources %s" % (self, )) raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, element.SupportedReport(), "Report not supported on this resource", )) responses = [] # Do not support limit if sync_collection.sync_limit is not None: raise HTTPError( ErrorResponse( responsecode.INSUFFICIENT_STORAGE_SPACE, element.NumberOfMatchesWithinLimits(), "Report limit not supported", )) # Process Depth and sync-level for backwards compatibility # Use sync-level if present and ignore Depth, else use Depth if sync_collection.sync_level: depth = sync_collection.sync_level if depth == "infinite": depth = "infinity" descriptor = "DAV:sync-level" else: depth = request.headers.getHeader("depth", None) descriptor = "Depth header without DAV:sync-level" if depth not in ("1", "infinity"): log.error("sync-collection report with invalid depth header: %s" % (depth, )) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, "Invalid %s value" % (descriptor, ))) propertyreq = sync_collection.property.children if sync_collection.property else None @inlineCallbacks def _namedPropertiesForResource(request, props, resource, forbidden=False): """ Return the specified properties on the specified resource. @param request: the L{IRequest} for the current request. @param props: a list of property elements or qname tuples for the properties of interest. @param resource: the L{DAVResource} for the targeted resource. @return: a map of OK and NOT FOUND property values. """ properties_by_status = { responsecode.OK: [], responsecode.FORBIDDEN: [], responsecode.NOT_FOUND: [], } for property in props: if isinstance(property, element.WebDAVElement): qname = property.qname() else: qname = property if forbidden: properties_by_status[responsecode.FORBIDDEN].append( propertyName(qname)) else: props = (yield resource.listProperties(request)) if qname in props: try: prop = (yield resource.readProperty(qname, request)) properties_by_status[responsecode.OK].append(prop) except: f = Failure() log.error( "Error reading property %r for resource %s: %s" % (qname, request.uri, f.value)) status = statusForFailure( f, "getting property: %s" % (qname, )) if status not in properties_by_status: properties_by_status[status] = [] properties_by_status[status].append( propertyName(qname)) else: properties_by_status[responsecode.NOT_FOUND].append( propertyName(qname)) returnValue(properties_by_status) # Do some optimization of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = (yield self.inheritedACEsforChildren(request)) changed, removed, notallowed, newtoken = yield self.whatchanged( sync_collection.sync_token, depth) # Now determine which valid resources are readable and which are not ok_resources = [] forbidden_resources = [] if changed: yield self.findChildrenFaster(depth, request, lambda x, y: ok_resources.append((x, y)), lambda x, y: forbidden_resources.append( (x, y)), None, None, changed, (element.Read(), ), inherited_aces=filteredaces) for child, child_uri in ok_resources: href = element.HRef.fromString(child_uri) try: yield responseForHref( request, responses, href, child, functools.partial(_namedPropertiesForResource, forbidden=False) if propertyreq else None, propertyreq) except ConcurrentModification: # This can happen because of a race-condition between the # time we determine which resources exist and the deletion # of one of these resources in another request. In this # case, we ignore the now missing resource rather # than raise an error for the entire report. log.error("Missing resource during sync: %s" % (href, )) for child, child_uri in forbidden_resources: href = element.HRef.fromString(child_uri) try: yield responseForHref( request, responses, href, child, functools.partial(_namedPropertiesForResource, forbidden=True) if propertyreq else None, propertyreq) except ConcurrentModification: # This can happen because of a race-condition between the # time we determine which resources exist and the deletion # of one of these resources in another request. In this # case, we ignore the now missing resource rather # than raise an error for the entire report. log.error("Missing resource during sync: %s" % (href, )) for name in removed: href = element.HRef.fromString(joinURL(request.uri, name)) responses.append( element.StatusResponse( element.HRef.fromString(href), element.Status.fromResponseCode(responsecode.NOT_FOUND))) for name in notallowed: href = element.HRef.fromString(joinURL(request.uri, name)) responses.append( element.StatusResponse( element.HRef.fromString(href), element.Status.fromResponseCode(responsecode.NOT_ALLOWED))) if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems["responses"] = len(responses) responses.append(element.SyncToken.fromString(newtoken)) returnValue(MultiStatusResponse(responses))
def multiget_common(self, request, multiget, collection_type): """ Generate a multiget REPORT. """ # Make sure target resource is of the right type if not self.isCollection(): parent = (yield self.locateParent(request, request.uri)) if collection_type == COLLECTION_TYPE_CALENDAR: if not parent.isPseudoCalendarCollection(): log.error( "calendar-multiget report is not allowed on a resource outside of a calendar collection {res}", res=self) raise HTTPError( StatusResponse(responsecode.FORBIDDEN, "Must be calendar resource")) elif collection_type == COLLECTION_TYPE_ADDRESSBOOK: if not parent.isAddressBookCollection(): log.error( "addressbook-multiget report is not allowed on a resource outside of an address book collection {res}", res=self) raise HTTPError( StatusResponse(responsecode.FORBIDDEN, "Must be address book resource")) responses = [] propertyreq = multiget.property resources = multiget.resources if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems["rcount"] = len(resources) hasData = False if propertyreq.qname() == ("DAV:", "allprop"): propertiesForResource = report_common.allPropertiesForResource elif propertyreq.qname() == ("DAV:", "propname"): propertiesForResource = report_common.propertyNamesForResource elif propertyreq.qname() == ("DAV:", "prop"): propertiesForResource = report_common.propertyListForResource if collection_type == COLLECTION_TYPE_CALENDAR: # Verify that any calendar-data element matches what we can handle result, message, hasData = report_common.validPropertyListCalendarDataTypeVersion( propertyreq) precondition = (caldav_namespace, "supported-calendar-data") elif collection_type == COLLECTION_TYPE_ADDRESSBOOK: # Verify that any address-data element matches what we can handle result, message, hasData = report_common.validPropertyListAddressDataTypeVersion( propertyreq) precondition = (carddav_namespace, "supported-address-data") else: result = True if not result: log.error(message) raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, precondition, "Invalid object data element", )) else: raise AssertionError("We shouldn't be here") # Check size of results is within limit when data property requested if hasData and len(resources) > config.MaxMultigetWithDataHrefs: log.error("Too many resources in multiget report: {count}", count=len(resources)) raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits(), "Too many resources", )) """ Three possibilities exist: 1. The request-uri is a calendar collection, in which case all the hrefs MUST be one-level below that collection and must be calendar object resources. 2. The request-uri is a regular collection, in which case all the hrefs MUST be children of that (at any depth) but MUST also be calendar object resources (i.e. immediate child of a calendar collection). 3. The request-uri is a resource, in which case there MUST be a single href equal to the request-uri, and MUST be a calendar object resource. """ disabled = False if self.isPseudoCalendarCollection(): requestURIis = "calendar" # Do some optimisation of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = (yield self.inheritedACEsforChildren(request)) # Check for disabled access if filteredaces is None: disabled = True # Check private events access status isowner = (yield self.isOwner(request)) elif self.isAddressBookCollection(): requestURIis = "addressbook" # Do some optimisation of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = (yield self.inheritedACEsforChildren(request)) # Check for disabled access if filteredaces is None: disabled = True isowner = None elif self.isCollection(): requestURIis = "collection" filteredaces = None lastParent = None isowner = None else: requestURIis = "resource" filteredaces = None isowner = None if not disabled: @inlineCallbacks def doResponse(): # Special for addressbooks if collection_type == COLLECTION_TYPE_ADDRESSBOOK: if self.isDirectoryBackedAddressBookCollection(): result = (yield doDirectoryAddressBookResponse()) returnValue(result) # Verify that requested resources are immediate children of the request-URI valid_names = [] for href in resources: resource_uri = str(href) name = unquote(resource_uri[resource_uri.rfind("/") + 1:]) if not self._isChildURI(request, resource_uri): responses.append( davxml.StatusResponse( href, davxml.Status.fromResponseCode( responsecode.BAD_REQUEST))) else: valid_names.append(name) if not valid_names: returnValue(None) # Now determine which valid resources are readable and which are not ok_resources = [] bad_resources = [] missing_resources = [] unavailable_resources = [] yield self.findChildrenFaster( "1", request, lambda x, y: ok_resources.append((x, y)), lambda x, y: bad_resources.append((x, y)), lambda x: missing_resources.append(x), lambda x: unavailable_resources.append(x), valid_names, (davxml.Read(), ), inherited_aces=filteredaces) # Get properties for all valid readable resources for resource, href in ok_resources: try: yield report_common.responseForHref( request, responses, davxml.HRef.fromString(href), resource, propertiesForResource, propertyreq, isowner=isowner) except ValueError: log.error( "Invalid calendar resource during multiget: {href}", href=href) responses.append( davxml.StatusResponse( davxml.HRef.fromString(href), davxml.Status.fromResponseCode( responsecode.FORBIDDEN))) except ConcurrentModification: # This can happen because of a race-condition between the # time we determine which resources exist and the deletion # of one of these resources in another request. In this # case, return a 404 for the now missing resource rather # than raise an error for the entire report. log.error("Missing resource during multiget: {href}", href=href) responses.append( davxml.StatusResponse( davxml.HRef.fromString(href), davxml.Status.fromResponseCode( responsecode.NOT_FOUND))) # Indicate error for all valid non-readable resources for ignore_resource, href in bad_resources: responses.append( davxml.StatusResponse( davxml.HRef.fromString(href), davxml.Status.fromResponseCode( responsecode.FORBIDDEN))) # Indicate error for all missing/unavailable resources for href in missing_resources: responses.append( davxml.StatusResponse( davxml.HRef.fromString(href), davxml.Status.fromResponseCode( responsecode.NOT_FOUND))) for href in unavailable_resources: responses.append( davxml.StatusResponse( davxml.HRef.fromString(href), davxml.Status.fromResponseCode( responsecode.SERVICE_UNAVAILABLE))) @inlineCallbacks def doDirectoryAddressBookResponse(): directoryAddressBookLock = None try: # Verify that requested resources are immediate children of the request-URI # and get vCardFilters ;similar to "normal" case below but do not call getChild() vCardFilters = [] valid_hrefs = [] for href in resources: resource_uri = str(href) resource_name = unquote( resource_uri[resource_uri.rfind("/") + 1:]) if self._isChildURI( request, resource_uri) and resource_name.endswith( ".vcf") and len(resource_name) > 4: valid_hrefs.append(href) textMatchElement = carddavxml.TextMatch.fromString( resource_name[:-4]) textMatchElement.attributes[ "match-type"] = "equals" # do equals compare. Default is "contains" vCardFilters.append( carddavxml.PropertyFilter( textMatchElement, name="UID", # attributes )) else: responses.append( davxml.StatusResponse( href, davxml.Status.fromResponseCode( responsecode.NOT_FOUND))) # exit if not valid if not vCardFilters or not valid_hrefs: returnValue(None) addressBookFilter = carddavxml.Filter(*vCardFilters) addressBookFilter = Filter(addressBookFilter) # get vCards and filter limit = config.DirectoryAddressBook.MaxQueryResults results, limited = (yield self.doAddressBookDirectoryQuery( addressBookFilter, propertyreq, limit, defaultKind=None)) if limited: log.error("Too many results in multiget report: {count}", count=len(resources)) raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (dav_namespace, "number-of-matches-within-limits"), "Too many results", )) for href in valid_hrefs: matchingResource = None for vCardResource in results: if href == vCardResource.hRef( ): # might need to compare urls instead - also case sens ok? matchingResource = vCardResource break if matchingResource: yield report_common.responseForHref( request, responses, href, matchingResource, propertiesForResource, propertyreq, vcard=matchingResource.vCard()) else: responses.append( davxml.StatusResponse( href, davxml.Status.fromResponseCode( responsecode.NOT_FOUND))) finally: if directoryAddressBookLock: yield directoryAddressBookLock.release() if requestURIis == "calendar" or requestURIis == "addressbook": yield doResponse() else: for href in resources: resource_uri = str(href) # Do href checks if requestURIis == "calendar": pass elif requestURIis == "addressbook": pass # TODO: we can optimize this one in a similar manner to the calendar case elif requestURIis == "collection": name = unquote(resource_uri[resource_uri.rfind("/") + 1:]) if not self._isChildURI(request, resource_uri, False): responses.append( davxml.StatusResponse( href, davxml.Status.fromResponseCode( responsecode.NOT_FOUND))) continue child = (yield request.locateResource(resource_uri)) if not child or not child.exists(): responses.append( davxml.StatusResponse( href, davxml.Status.fromResponseCode( responsecode.NOT_FOUND))) continue parent = (yield child.locateParent(request, resource_uri)) if collection_type == COLLECTION_TYPE_CALENDAR: if not parent.isCalendarCollection() or not ( yield parent.resourceExists(name)): responses.append( davxml.StatusResponse( href, davxml.Status.fromResponseCode( responsecode.FORBIDDEN))) continue elif collection_type == COLLECTION_TYPE_ADDRESSBOOK: if not parent.isAddressBookCollection() or not ( yield parent.resourceExists(name)): responses.append( davxml.StatusResponse( href, davxml.Status.fromResponseCode( responsecode.FORBIDDEN))) continue # Check privileges on parent - must have at least DAV:read try: yield parent.checkPrivileges(request, (davxml.Read(), )) except AccessDeniedError: responses.append( davxml.StatusResponse( href, davxml.Status.fromResponseCode( responsecode.FORBIDDEN))) continue # Cache the last parent's inherited aces for checkPrivileges optimization if lastParent != parent: lastParent = parent # Do some optimisation of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = ( yield parent.inheritedACEsforChildren(request)) # Check private events access status isowner = (yield parent.isOwner(request)) else: name = unquote(resource_uri[resource_uri.rfind("/") + 1:]) if (resource_uri != request.uri) or not self.exists(): responses.append( davxml.StatusResponse( href, davxml.Status.fromResponseCode( responsecode.NOT_FOUND))) continue parent = (yield self.locateParent(request, resource_uri)) if collection_type == COLLECTION_TYPE_CALENDAR: if not parent.isPseudoCalendarCollection() or not ( yield parent.resourceExists(name)): responses.append( davxml.StatusResponse( href, davxml.Status.fromResponseCode( responsecode.FORBIDDEN))) continue elif collection_type == COLLECTION_TYPE_ADDRESSBOOK: if not parent.isAddressBookCollection() or not ( yield parent.resourceExists(name)): responses.append( davxml.StatusResponse( href, davxml.Status.fromResponseCode( responsecode.FORBIDDEN))) continue child = self # Do some optimisation of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = (yield parent.inheritedACEsforChildren(request)) # Check private events access status isowner = (yield parent.isOwner(request)) # Check privileges - must have at least DAV:read try: yield child.checkPrivileges(request, (davxml.Read(), ), inherited_aces=filteredaces) except AccessDeniedError: responses.append( davxml.StatusResponse( href, davxml.Status.fromResponseCode( responsecode.FORBIDDEN))) continue yield report_common.responseForHref(request, responses, href, child, propertiesForResource, propertyreq, isowner=isowner) returnValue(MultiStatusResponse(responses))
def report_DAV__principal_match(self, request, principal_match): """ Generate a principal-match REPORT. (RFC 3744, section 9.3) """ # Verify root element if not isinstance(principal_match, element.PrincipalMatch): raise ValueError( "%s expected as root element, not %s." % (element.PrincipalMatch.sname(), principal_match.sname())) # Only handle Depth: 0 depth = request.headers.getHeader("depth", "0") if depth != "0": log.error("Non-zero depth is not allowed: %s" % (depth, )) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth, ))) # Get a single DAV:prop element from the REPORT request body propertiesForResource = None propElement = None principalPropElement = None lookForPrincipals = True for child in principal_match.children: if child.qname() == (dav_namespace, "prop"): propertiesForResource = prop_common.propertyListForResource propElement = child elif child.qname() == (dav_namespace, "self"): lookForPrincipals = True elif child.qname() == (dav_namespace, "principal-property"): # Must have one and only one property in this element if len(child.children) != 1: log.error( "Wrong number of properties in DAV:principal-property: %s" % (len(child.children), )) raise HTTPError( StatusResponse( responsecode.BAD_REQUEST, "DAV:principal-property must contain exactly one property" )) lookForPrincipals = False principalPropElement = child.children[0] # Run report for each referenced principal try: responses = [] matchcount = 0 myPrincipalURL = self.currentPrincipal(request).children[0] if lookForPrincipals: # Find the set of principals that represent "self". # First add "self" principal = waitForDeferred( request.locateResource(str(myPrincipalURL))) yield principal principal = principal.getResult() selfItems = [ principal, ] # Get group memberships for "self" and add each of those d = waitForDeferred(principal.groupMemberships()) yield d memberships = d.getResult() selfItems.extend(memberships) # Now add each principal found to the response provided the principal resource is a child of # the current resource. for principal in selfItems: # Get all the URIs that point to the principal resource # FIXME: making the assumption that the principalURL() is the URL of the resource we found principal_uris = [principal.principalURL()] principal_uris.extend(principal.alternateURIs()) # Compare each one to the request URI and return at most one that matches for uri in principal_uris: if uri.startswith(request.uri): # Check size of results is within limit matchcount += 1 if matchcount > max_number_of_matches: raise NumberOfMatchesWithinLimits( max_number_of_matches) d = waitForDeferred( prop_common.responseForHref( request, responses, element.HRef.fromString(uri), principal, propertiesForResource, propElement)) yield d d.getResult() break else: # Do some optimisation of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = waitForDeferred( self.inheritedACEsforChildren(request)) yield filteredaces filteredaces = filteredaces.getResult() children = [] d = waitForDeferred( self.findChildren("infinity", request, lambda x, y: children.append((x, y)), privileges=(element.Read(), ), inherited_aces=filteredaces)) yield d d.getResult() for child, uri in children: # Try to read the requested property from this resource try: prop = waitForDeferred( child.readProperty(principalPropElement.qname(), request)) yield prop prop = prop.getResult() if prop: prop.removeWhitespaceNodes() if prop and len(prop.children) == 1 and isinstance( prop.children[0], element.HRef): # Find principal associated with this property and test it principal = waitForDeferred( request.locateResource(str(prop.children[0]))) yield principal principal = principal.getResult() if principal and isPrincipalResource(principal): d = waitForDeferred( principal.principalMatch(myPrincipalURL)) yield d matched = d.getResult() if matched: # Check size of results is within limit matchcount += 1 if matchcount > max_number_of_matches: raise NumberOfMatchesWithinLimits( max_number_of_matches) d = waitForDeferred( prop_common.responseForHref( request, responses, element.HRef.fromString(uri), child, propertiesForResource, propElement)) yield d d.getResult() except HTTPError: # Just ignore a failure to access the property. We treat this like a property that does not exist # or does not match the principal. pass except NumberOfMatchesWithinLimits: log.error("Too many matching components in principal-match report") raise HTTPError( ErrorResponse(responsecode.FORBIDDEN, element.NumberOfMatchesWithinLimits())) yield MultiStatusResponse(responses)