def __init__(self, base_uri, errors): """ An error response which is due to unsufficient privileges, as determined by L{DAVResource.checkPrivileges}. @param base_uri: the base URI for the resources with errors (the URI of the resource on which C{checkPrivileges} was called). @param errors: a sequence of tuples, as returned by C{checkPrivileges}. """ denials = [] for subpath, privileges in errors: if subpath is None: uri = base_uri else: uri = joinURL(base_uri, subpath) for p in privileges: denials.append( element.Resource(element.HRef(uri), element.Privilege(p))) super(NeedPrivilegesResponse, self).__init__(responsecode.FORBIDDEN, element.NeedPrivileges(*denials))
def writeProperty(self, property, request): assert isinstance(property, davxml.WebDAVElement) # Strictly speaking CS:calendar-availability is a live property in the sense that the # server enforces what can be stored, however it need not actually # exist so we cannot list it in liveProperties on this resource, since its # its presence there means that hasProperty will always return True for it. if property.qname() == customxml.CalendarAvailability.qname(): if not property.valid(): raise HTTPError( ErrorResponse(responsecode.CONFLICT, (caldav_namespace, "valid-calendar-data"), description="Invalid property")) yield self.parent._newStoreHome.setAvailability( property.calendar()) returnValue(None) elif property.qname() == caldavxml.CalendarFreeBusySet.qname(): # Verify that the calendars added in the PROPPATCH are valid. We do not check # whether existing items in the property are still valid - only new ones. property.children = [ davxml.HRef(normalizeURL(str(href))) for href in property.children ] new_calendars = set([str(href) for href in property.children]) old_calendars = set() for cal in (yield self.parent._newStoreHome.calendars()): if cal.isUsedForFreeBusy(): old_calendars.add( HRef(joinURL(self.parent.url(), cal.name()))) added_calendars = new_calendars.difference(old_calendars) for href in added_calendars: cal = (yield request.locateResource(str(href))) if cal is None or not cal.exists( ) or not isCalendarCollectionResource(cal): # Validate that href's point to a valid calendar. raise HTTPError( ErrorResponse( responsecode.CONFLICT, (caldav_namespace, "valid-calendar-url"), "Invalid URI", )) # Remove old ones for href in old_calendars.difference(new_calendars): cal = (yield request.locateResource(str(href))) if cal is not None and cal.exists( ) and isCalendarCollectionResource( cal) and cal._newStoreObject.isUsedForFreeBusy(): yield cal._newStoreObject.setUsedForFreeBusy(False) # Add new ones for href in new_calendars: cal = (yield request.locateResource(str(href))) if cal is not None and cal.exists( ) and isCalendarCollectionResource( cal) and not cal._newStoreObject.isUsedForFreeBusy(): yield cal._newStoreObject.setUsedForFreeBusy(True) returnValue(None) elif property.qname() in (caldavxml.ScheduleDefaultCalendarURL.qname(), customxml.ScheduleDefaultTasksURL.qname()): yield self.writeDefaultCalendarProperty(request, property) returnValue(None) yield super(ScheduleInboxResource, self).writeProperty(property, request)
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, element.ExpandProperty): raise ValueError( "%s expected as root element, not %s." % (element.ExpandProperty.sname(), expand_property.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 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.error( "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 = element.PropertyContainer(*properties[qname]) # Now dereference any HRefs responses = [] for href in prop.children: if isinstance(href, element.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( element.StatusResponse( href, element.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, (element.Read(), )) except AccessDeniedError: responses.append( element.StatusResponse( href, element.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, (element.Read(), ), inherited_aces=filteredaces) except AccessDeniedError: responses.append( element.StatusResponse( href, element.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.error( "Error reading property {qname} for resource {req}: {failure}", qname=qname, req=request.uri, failure=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 = [ element.PropertyStatus( element.PropertyContainer(*properties_by_status[pstatus]), element.Status.fromResponseCode(pstatus)) for pstatus in properties_by_status if properties_by_status[pstatus] ] returnValue( MultiStatusResponse( (element.PropertyStatusResponse(element.HRef(request.uri), *propstats), )))
continue xml_status = davxml.Status.fromResponseCode(status) xml_container = davxml.PropertyContainer(*properties) xml_propstat = davxml.PropertyStatus(xml_container, xml_status) propstats.append(xml_propstat) # Always need to have at least one propstat present (required by Prefer header behavior) if len(propstats) == 0: propstats.append( davxml.PropertyStatus( davxml.PropertyContainer(), davxml.Status.fromResponseCode(responsecode.OK))) xml_resource = davxml.HRef(uri) xml_response = davxml.PropertyStatusResponse(xml_resource, *propstats) xml_responses.append(xml_response) # # Return response # yield MultiStatusResponse(xml_responses) http_PROPFIND = deferredGenerator(http_PROPFIND) ## # Utilities ##
xml_status = davxml.Status.fromResponseCode(status) xml_container = davxml.PropertyContainer(*properties) xml_propstat = davxml.PropertyStatus(xml_container, xml_status) propstats.append(xml_propstat) # Always need to have at least one propstat present (required by Prefer header behavior) if len(propstats) == 0: propstats.append( davxml.PropertyStatus( davxml.PropertyContainer(), davxml.Status.fromResponseCode(responsecode.OK))) xml_response = davxml.PropertyStatusResponse( davxml.HRef(uri), *propstats) # This needed for propfind cache tracking of children changes if depth == "1": if resource != self: if hasattr(resource, "owner_url"): request.childCacheURIs.append(resource.owner_url()) elif hasattr(resource, "url"): request.childCacheURIs.append(resource.url()) else: xml_response = davxml.StatusResponse( davxml.HRef(uri), davxml.Status.fromResponseCode(respcode)) xml_responses.append(xml_response) if not hasattr(request, "extendedLogItems"):
def _handleInvite(self, request, invitedoc): def _handleInviteSet(inviteset): userid = None cn = None access = None summary = None for item in inviteset.children: if isinstance(item, element.HRef): userid = str(item) continue if isinstance(item, customxml.CommonName): cn = str(item) continue if isinstance(item, customxml.InviteSummary): summary = str(item) continue if isinstance(item, customxml.ReadAccess) or isinstance( item, customxml.ReadWriteAccess): access = item continue if userid and access and summary: return (userid, cn, access, summary) else: error_text = [] if userid is None: error_text.append("missing href") if access is None: error_text.append("missing access") if summary is None: error_text.append("missing summary") raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request"), "%s: %s" % ( ", ".join(error_text), inviteset, ), )) def _handleInviteRemove(inviteremove): userid = None access = [] for item in inviteremove.children: if isinstance(item, element.HRef): userid = str(item) continue if isinstance(item, customxml.ReadAccess) or isinstance( item, customxml.ReadWriteAccess): access.append(item) continue if userid is None: raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request"), "Missing href: %s" % (inviteremove, ), )) if len(access) == 0: access = None else: access = set(access) return (userid, access) setDict, removeDict, updateinviteDict = {}, {}, {} okusers = set() badusers = set() for item in invitedoc.children: if isinstance(item, customxml.InviteSet): userid, cn, access, summary = _handleInviteSet(item) setDict[userid] = (cn, access, summary) # Validate each userid on add only uid = (yield self.validUserIDForShare(userid, request)) if uid is None: uid = yield self.principalForCalendarGroupAddress(userid) (badusers if uid is None else okusers).add(userid) elif isinstance(item, customxml.InviteRemove): userid, access = _handleInviteRemove(item) removeDict[userid] = access # Treat removed userids as valid as we will fail invalid ones silently okusers.add(userid) # Only make changes if all OK if len(badusers) == 0: okusers = set() badusers = set() # Special case removing and adding the same user and treat that as an add sameUseridInRemoveAndSet = [ u for u in removeDict.keys() if u in setDict ] for u in sameUseridInRemoveAndSet: removeACL = removeDict[u] cn, newACL, summary = setDict[u] updateinviteDict[u] = (cn, removeACL, newACL, summary) del removeDict[u] del setDict[u] for userid, access in removeDict.iteritems(): result = (yield self.uninviteUIDFromShare(userid, access, request)) # If result is False that means the user being removed was not # actually invited, but let's not return an error in this case. okusers.add(userid) for userid, (cn, access, summary) in setDict.iteritems(): result = (yield self.inviteUIDToShare(userid, cn, access, summary, request)) (okusers if result else badusers).add(userid) for userid, (cn, removeACL, newACL, summary) in updateinviteDict.iteritems(): result = (yield self.inviteUserUpdateToShare(userid, cn, removeACL, newACL, summary, request)) (okusers if result else badusers).add(userid) # In this case bad items do not prevent ok items from being processed ok_code = responsecode.OK else: # In this case a bad item causes all ok items not to be processed so failed dependency is returned ok_code = responsecode.FAILED_DEPENDENCY # Do a final validation of the entire set of invites invites = (yield self.validateInvites(request)) numRecords = len(invites) # Set the sharing state on the collection shared = self.isShared() if shared and numRecords == 0: yield self.downgradeFromShare(request) elif not shared and numRecords != 0: yield self.upgradeToShare() # Create the multistatus response - only needed if some are bad if badusers: xml_responses = [] xml_responses.extend([ element.StatusResponse( element.HRef(userid), element.Status.fromResponseCode(ok_code)) for userid in sorted(okusers) ]) xml_responses.extend([ element.StatusResponse( element.HRef(userid), element.Status.fromResponseCode(responsecode.FORBIDDEN)) for userid in sorted(badusers) ]) # # Return response # returnValue(MultiStatusResponse(xml_responses)) else: returnValue(responsecode.OK)
def shareeAccessControlList(self, request, *args, **kwargs): """ Return WebDAV ACLs appropriate for the current user accessing the shared collection. For an "invite" share we take the privilege granted to the sharee in the invite and map that to WebDAV ACLs. For a "direct" share, if it is a wiki collection we map the wiki privileges into WebDAV ACLs, otherwise we use whatever privileges exist on the underlying shared collection. @param request: the request used to locate the owner resource. @type request: L{txweb2.iweb.IRequest} @param args: The arguments for L{txweb2.dav.idav.IDAVResource.accessControlList} @param kwargs: The keyword arguments for L{txweb2.dav.idav.IDAVResource.accessControlList}, plus keyword-only arguments. @return: the appropriate WebDAV ACL for the sharee @rtype: L{davxml.ACL} """ assert self._isShareeResource, "Only call this for a sharee resource" assert self.isCalendarCollection() or self.isAddressBookCollection( ), "Only call this for a address book or calendar resource" sharee = yield self.principalForUID( self._newStoreObject.viewerHome().uid()) access = yield self._checkAccessControl() if access == "original" and not self._newStoreObject.ownerHome( ).external(): original = (yield request.locateResource(self._share_url)) result = (yield original.accessControlList(request, *args, **kwargs)) returnValue(result) # Direct shares use underlying privileges of shared collection userprivs = [] if access in ( "read-only", "read-write", ): userprivs.append(element.Privilege(element.Read())) userprivs.append(element.Privilege(element.ReadACL())) userprivs.append( element.Privilege(element.ReadCurrentUserPrivilegeSet())) if access in ("read-only", ): userprivs.append(element.Privilege(element.WriteProperties())) if access in ("read-write", ): userprivs.append(element.Privilege(element.Write())) proxyprivs = list(userprivs) try: proxyprivs.remove(element.Privilege(element.ReadACL())) except ValueError: # If wiki says no-access then ReadACL won't be in the list pass aces = ( # Inheritable specific access for the resource's associated principal. element.ACE( element.Principal(element.HRef(sharee.principalURL())), element.Grant(*userprivs), element.Protected(), TwistedACLInheritable(), ), ) if self.isCalendarCollection(): aces += ( # Inheritable CALDAV:read-free-busy access for authenticated users. element.ACE( element.Principal(element.Authenticated()), element.Grant(element.Privilege(caldavxml.ReadFreeBusy())), TwistedACLInheritable(), ), ) # Give read access to config.ReadPrincipals aces += config.ReadACEs # Give all access to config.AdminPrincipals aces += config.AdminACEs if self.isCalendarCollection() and config.EnableProxyPrincipals: aces += ( # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users. element.ACE( element.Principal( element.HRef( joinURL(sharee.principalURL(), "calendar-proxy-read/"))), element.Grant( element.Privilege(element.Read()), element.Privilege( element.ReadCurrentUserPrivilegeSet()), element.Privilege(element.WriteProperties()), ), element.Protected(), TwistedACLInheritable(), ), # DAV:read/DAV:read-current-user-privilege-set/DAV:write access for this principal's calendar-proxy-write users. element.ACE( element.Principal( element.HRef( joinURL(sharee.principalURL(), "calendar-proxy-write/"))), element.Grant(*proxyprivs), element.Protected(), TwistedACLInheritable(), ), ) returnValue(element.ACL(*aces))