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 = 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__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)
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.err("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 = [] # 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 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.err("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.err("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, 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.err("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.err("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 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.err("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.err("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.err("Too many matching components in principal-match report") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, element.NumberOfMatchesWithinLimits() )) yield MultiStatusResponse(responses)
def report_DAV__expand_property(self, request, expand_property): """ Generate an expand-property REPORT. (RFC 3253, section 3.8) TODO: for simplicity we will only support one level of expansion. """ # Verify root element if not isinstance(expand_property, davxml.ExpandProperty): raise ValueError("%s expected as root element, not %s." % (davxml.ExpandProperty.sname(), expand_property.sname())) # Only handle Depth: 0 depth = request.headers.getHeader("depth", "0") if depth != "0": log.err("Non-zero depth is not allowed: %s" % (depth,)) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,))) # # Get top level properties to expand and make sure we only have one level # properties = {} for property in expand_property.children: namespace = property.attributes.get("namespace", dav_namespace) name = property.attributes.get("name", "") # Make sure children have no children props_to_find = [] for child in property.children: if child.children: log.err("expand-property REPORT only supports single level expansion") raise HTTPError(StatusResponse( responsecode.NOT_IMPLEMENTED, "expand-property REPORT only supports single level expansion" )) child_namespace = child.attributes.get("namespace", dav_namespace) child_name = child.attributes.get("name", "") props_to_find.append((child_namespace, child_name)) properties[(namespace, name)] = props_to_find # # Generate the expanded responses status for each top-level property # properties_by_status = { responsecode.OK : [], responsecode.NOT_FOUND : [], } filteredaces = None lastParent = None for qname in properties.iterkeys(): try: prop = (yield self.readProperty(qname, request)) # Form the PROPFIND-style DAV:prop element we need later props_to_return = davxml.PropertyContainer(*properties[qname]) # Now dereference any HRefs responses = [] for href in prop.children: if isinstance(href, davxml.HRef): # Locate the Href resource and its parent resource_uri = str(href) 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 request.locateResource(parentForURL(resource_uri))) # 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 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 # Now retrieve all the requested properties on the HRef resource yield prop_common.responseForHref( request, responses, href, child, prop_common.propertyListForResource, props_to_return, ) prop.children = responses properties_by_status[responsecode.OK].append(prop) except: f = Failure() log.err("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)) # Build the overall response propstats = [ davxml.PropertyStatus( davxml.PropertyContainer(*properties_by_status[status]), davxml.Status.fromResponseCode(status) ) for status in properties_by_status if properties_by_status[status] ] returnValue(MultiStatusResponse((davxml.PropertyStatusResponse(davxml.HRef(request.uri), *propstats),)))
def report_DAV__sync_collection(self, request, sync_collection): """ Generate a sync-collection REPORT. """ if not self.isPseudoCalendarCollection() and not self.isAddressBookCollection() or not config.EnableSyncReport: log.err("sync-collection report is only allowed on calendar/inbox/addressbook collection resources %s" % (self,)) raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, davxml.SupportedReport())) responses = [] 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{DAVFile} 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, 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.err("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, newtoken = self.whatchanged(sync_collection.sync_token) # Now determine which valid resources are readable and which are not ok_resources = [] forbidden_resources = [] if changed: yield self.findChildrenFaster( "1", request, lambda x, y: ok_resources.append((x, y)), lambda x, y: forbidden_resources.append((x, y)), changed, (davxml.Read(),), inherited_aces=filteredaces ) for child, child_uri in ok_resources: href = davxml.HRef.fromString(child_uri) yield responseForHref( request, responses, href, child, functools.partial(_namedPropertiesForResource, forbidden=False) if propertyreq else None, propertyreq) for child, child_uri in forbidden_resources: href = davxml.HRef.fromString(child_uri) yield responseForHref( request, responses, href, child, functools.partial(_namedPropertiesForResource, forbidden=True) if propertyreq else None, propertyreq) for name in removed: href = davxml.HRef.fromString(joinURL(request.uri, name)) responses.append(davxml.StatusResponse(davxml.HRef.fromString(href), davxml.Status.fromResponseCode(responsecode.NOT_FOUND))) if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems["responses"] = len(responses) responses.append(SyncToken.fromString(newtoken)) returnValue(MultiStatusResponse(responses))
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, davxml.PrincipalPropertySearch): raise ValueError("%s expected as root element, not %s." % (davxml.PrincipalPropertySearch.sname(), principal_property_search.sname())) # Only handle Depth: 0 depth = request.headers.getHeader("depth", "0") if depth != "0": log.err("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(davxml.PropertyContainer) props.removeWhitespaceNodes() match = child.childOfType(davxml.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, davxml.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=(davxml.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, davxml.HRef.fromString(uri), child, propertiesForResource, propElement )) yield d d.getResult() except NumberOfMatchesWithinLimits: log.err("Too many matching components in prinicpal-property-search report") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits() )) yield MultiStatusResponse(responses)