Revision: 2100 http://trac.macosforge.org/projects/calendarserver/changeset/2100 Author: cdaboo@apple.com Date: 2008-01-10 13:16:14 -0800 (Thu, 10 Jan 2008) Log Message: ----------- Full commit of private event support. Modified Paths: -------------- CalendarServer/branches/users/cdaboo/private_events-2098/conf/caldavd-test.plist CalendarServer/branches/users/cdaboo/private_events-2098/conf/caldavd.plist CalendarServer/branches/users/cdaboo/private_events-2098/lib-patches/Twisted/twisted.web2.dav.test.test_prop.patch CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/__init__.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/caldavxml.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/config.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/customxml.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/directory/calendar.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/directory/calendaruserproxy.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/ical.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/__init__.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/put_common.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/report_calquery.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/report_common.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/report_multiget.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/query/calendarquery.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/resource.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/schedule.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/static.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/test/test_config.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/test/test_xml.py Added Paths: ----------- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/fileops.py CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/get.py Modified: CalendarServer/branches/users/cdaboo/private_events-2098/conf/caldavd-test.plist =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/conf/caldavd-test.plist 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/conf/caldavd-test.plist 2008-01-10 21:16:14 UTC (rev 2100) @@ -330,7 +330,11 @@ <key>EnableNotifications</key> <true/> + <!-- Private Events --> + <key>EnablePrivateEvents</key> + <true/> + <!-- Twisted --> Modified: CalendarServer/branches/users/cdaboo/private_events-2098/conf/caldavd.plist =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/conf/caldavd.plist 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/conf/caldavd.plist 2008-01-10 21:16:14 UTC (rev 2100) @@ -264,6 +264,10 @@ <key>EnableNotifications</key> <true/> + <!-- Private Events --> + <key>EnablePrivateEvents</key> + <true/> + </dict> </plist> Modified: CalendarServer/branches/users/cdaboo/private_events-2098/lib-patches/Twisted/twisted.web2.dav.test.test_prop.patch =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/lib-patches/Twisted/twisted.web2.dav.test.test_prop.patch 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/lib-patches/Twisted/twisted.web2.dav.test.test_prop.patch 2008-01-10 21:16:14 UTC (rev 2100) @@ -11,7 +11,7 @@ import random -@@ -37,8 +39,14 @@ +@@ -37,8 +39,13 @@ from twisted.web2.dav.test.util import serialize import twisted.web2.dav.test.util @@ -22,8 +22,48 @@ + (dav_namespace, "quota-used-bytes" ), +) -+live_properties = [lookupElement(qname)() for qname in DAVResource.liveProperties if (qname[0] == dav_namespace) and qname not in dynamicLiveProperties] + # # See whether dead properties are available # +@@ -49,6 +56,10 @@ + """ + PROPFIND, PROPPATCH requests + """ ++ ++ def liveProperties(self): ++ return [lookupElement(qname)() for qname in self.resource_class.liveProperties if (qname[0] == dav_namespace) and qname not in dynamicLiveProperties] ++ + def test_PROPFIND_basic(self): + """ + PROPFIND request +@@ -85,7 +96,7 @@ + self.fail("PROPFIND failed (status %s) to locate live properties: %s" + % (status.code, properties)) + +- properties_to_find = [p.qname() for p in live_properties] ++ properties_to_find = [p.qname() for p in self.liveProperties()] + + for property in properties: + qname = property.qname() +@@ -102,7 +113,7 @@ + else: + self.fail("No response for URI /") + +- query = davxml.PropertyFind(davxml.PropertyContainer(*live_properties)) ++ query = davxml.PropertyFind(davxml.PropertyContainer(*self.liveProperties())) + + request = SimpleRequest(self.site, "PROPFIND", "/") + +@@ -146,9 +157,9 @@ + % (status.code, properties)) + + if which.name == "allprop": +- properties_to_find = [p.qname() for p in live_properties if not p.hidden] ++ properties_to_find = [p.qname() for p in self.liveProperties() if not p.hidden] + else: +- properties_to_find = [p.qname() for p in live_properties] ++ properties_to_find = [p.qname() for p in self.liveProperties()] + + for property in properties: + qname = property.qname() Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/__init__.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/__init__.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/__init__.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -31,6 +31,7 @@ "directory", "dropbox", "extensions", + "fileops", "ical", "icaldav", "index", Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/caldavxml.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/caldavxml.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/caldavxml.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -191,7 +191,7 @@ self.filter_name = self.filter_name.encode("utf-8") self.defined = not self.qualifier or (self.qualifier.qname() != (caldav_namespace, "is-not-defined")) - def match(self, item): + def match(self, item, access=None): """ Returns True if the given calendar item (either a component, property or parameter value) matches this filter, False otherwise. @@ -201,11 +201,11 @@ # be negated by the caller if not self.defined: return True - if self.qualifier and not self.qualifier.match(item): return False + if self.qualifier and not self.qualifier.match(item, access): return False if len(self.filters) > 0: for filter in self.filters: - if filter._match(item): + if filter._match(item, access): return True return False else: @@ -475,12 +475,7 @@ @param resource: the resource whose calendar data is to be returned. @return: an L{CalendarData} with the (filtered) calendar data. """ - # Check for filtering or not - if self.children: - filtered = self.getFromICalendar(resource.iCalendar()) - return CalendarData.fromCalendar(filtered) - else: - return resource.iCalendarXML() + return self.elementFromCalendar(resource.iCalendar()) def elementFromCalendar(self, calendar): """ @@ -494,6 +489,138 @@ filtered = self.getFromICalendar(calendar) return CalendarData.fromCalendar(filtered) + def elementFromResourceWithAccessRestrictions(self, resource, access): + """ + Return a new CalendarData element comprised of the possibly filtered + calendar data from the specified resource. If no filter is being applied + read the data directly from the resource without parsing it. If a filter + is required, parse the iCal data and filter using this CalendarData. + + Also, apply appropriate access restriction filtering to the data. + + @param resource: the resource whose calendar data is to be returned. + @param access: private event access restriction level. + @return: an L{CalendarData} with the (filtered) calendar data. + """ + return self.elementFromCalendarWithAccessRestrictions(resource.iCalendar(), access) + + def elementFromCalendarWithAccessRestrictions(self, calendar, access): + """ + Return a new CalendarData element comprised of the possibly filtered + calendar. + + Also, apply appropriate access restriction filtering to the data. + + @param calendar: the calendar that is to be filtered and returned. + @param access: private event access restriction level. + @return: an L{CalendarData} with the (filtered) calendar data. + """ + + # Do normal filtering first + filtered_calendar = self.getFromICalendar(calendar) + + if access in (iComponent.ACCESS_CONFIDENTIAL, iComponent.ACCESS_RESTRICTED): + # Create a CALDAV:calendar-data element with the appropriate iCalendar Component/Property + # filter in place for the access restriction in use + + extra_access = () + if access == iComponent.ACCESS_RESTRICTED: + extra_access = ( + Property(name="SUMMARY"), + Property(name="LOCATION"), + ) + + filter = CalendarData( + CalendarComponent( + + # VCALENDAR proeprties + Property(name="PRODID"), + Property(name="VERSION"), + Property(name="CALSCALE"), + Property(name=iComponent.ACCESS_PROPERTY), + + # VEVENT + CalendarComponent( + Property(name="UID"), + Property(name="RECURRENCE-ID"), + Property(name="SEQUENCE"), + Property(name="DTSTAMP"), + Property(name="STATUS"), + Property(name="TRANSP"), + Property(name="DTSTART"), + Property(name="DTEND"), + Property(name="DURATION"), + Property(name="RRULE"), + Property(name="RDATE"), + Property(name="EXRULE"), + Property(name="EXDATE"), + *extra_access, + **{"name":"VEVENT"} + ), + + # VTODO + CalendarComponent( + Property(name="UID"), + Property(name="RECURRENCE-ID"), + Property(name="SEQUENCE"), + Property(name="DTSTAMP"), + Property(name="STATUS"), + Property(name="DTSTART"), + Property(name="COMPLETED"), + Property(name="DUE"), + Property(name="DURATION"), + Property(name="RRULE"), + Property(name="RDATE"), + Property(name="EXRULE"), + Property(name="EXDATE"), + *extra_access, + **{"name":"VTODO"} + ), + + # VJOURNAL + CalendarComponent( + Property(name="UID"), + Property(name="RECURRENCE-ID"), + Property(name="SEQUENCE"), + Property(name="DTSTAMP"), + Property(name="STATUS"), + Property(name="TRANSP"), + Property(name="DTSTART"), + Property(name="RRULE"), + Property(name="RDATE"), + Property(name="EXRULE"), + Property(name="EXDATE"), + *extra_access, + **{"name":"VJOURNAL"} + ), + + # VFREEBUSY + CalendarComponent( + Property(name="UID"), + Property(name="DTSTAMP"), + Property(name="DTSTART"), + Property(name="DTEND"), + Property(name="DURATION"), + Property(name="FREEBUSY"), + *extra_access, + **{"name":"VFREEBUSY"} + ), + + # VTIMEZONE + CalendarComponent( + AllProperties(), + AllComponents(), + name="VTIMEZONE", + ), + name="VCALENDAR", + ), + ) + + # Now "filter" the resource calendar data through the CALDAV:calendar-data element + return filter.elementFromCalendar(filtered_calendar) + else: + return CalendarData.fromCalendar(filtered_calendar) + def getFromICalendar(self, calendar): """ Returns a calendar object containing the data in the given calendar @@ -528,6 +655,16 @@ """ Returns a calendar component derived from this element. """ + data = self.calendarData() + if data: + return iComponent.fromString(data) + else: + return None + + def calendarData(self): + """ + Returns the calendar data derived from this element. + """ for data in self.children: if not isinstance(data, davxml.PCDATAElement): return None @@ -535,7 +672,7 @@ # We guaranteed in __init__() that there is only one child... break - return iComponent.fromString(str(data)) + return str(data) def expandRecurrence(self, calendar): """ @@ -756,12 +893,16 @@ allowed_children = { (caldav_namespace, "comp-filter"): (1, 1) } - def match(self, component): + def match(self, component, access=None): """ Returns True if the given calendar component matches this filter, False otherwise. """ + # We only care about certain access restrictions. + if access not in (iComponent.ACCESS_CONFIDENTIAL, iComponent.ACCESS_RESTRICTED): + access = None + # We need to prepare ourselves for a time-range query by pre-calculating # the set of instances up to the latest time-range limit. That way we can # avoid having to do some form of recurrence expansion for each query sub-part. @@ -773,7 +914,7 @@ self.children[0].setInstances(instances) # <filter> contains exactly one <comp-filter> - return self.children[0].match(component) + return self.children[0].match(component, access) def valid(self): """ @@ -825,7 +966,7 @@ } allowed_attributes = { "name": True } - def match(self, item): + def match(self, item, access): """ Returns True if the given calendar item (which is a component) matches this filter, False otherwise. @@ -841,20 +982,26 @@ if len(self.filters) > 0: for filter in self.filters: - if filter._match(item): + if filter._match(item, access): return True return False else: return True - def _match(self, component): + def _match(self, component, access): # At least one subcomponent must match (or is-not-defined is set) for subcomponent in component.subcomponents(): + # If access restrictions are in force, restrict matching to specific components only. + # In particular do not match VALARM. + if access and subcomponent.name() not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE",): + continue + + # Try to match the component name if isinstance(self.filter_name, str): if subcomponent.name() != self.filter_name: continue else: if subcomponent.name() not in self.filter_name: continue - if self.match(subcomponent): break + if self.match(subcomponent, access): break else: return not self.defined return self.defined @@ -980,10 +1127,22 @@ } allowed_attributes = { "name": True } - def _match(self, component): + def _match(self, component, access): + # When access restriction is in force, we need to only allow matches against the properties + # allowed by the access restriction level. + if access: + allowedProperties = iComponent.confidentialPropertiesMap.get(component.name(), None) + if allowedProperties and access == iComponent.ACCESS_RESTRICTED: + allowedProperties += iComponent.extraRestrictedProperties + else: + allowedProperties = None + # At least one property must match (or is-not-defined is set) for property in component.properties(): - if property.name() == self.filter_name and self.match(property): break + # Apply access restrictions, if any. + if allowedProperties is not None and property.name() not in allowedProperties: + continue + if property.name() == self.filter_name and self.match(property, access): break else: return not self.defined return self.defined @@ -1036,7 +1195,7 @@ } allowed_attributes = { "name": True } - def _match(self, property): + def _match(self, property, access): # We have to deal with the problem that the 'Native' form of a property # will be missing the TZID parameter due to the conversion performed. Converting # to non-native for the entire calendar object causes problems elsewhere, so its @@ -1049,7 +1208,7 @@ # At least one property must match (or is-not-defined is set) result = not self.defined for parameterName in property.params().keys(): - if parameterName == self.filter_name and self.match(property.params()[parameterName]): + if parameterName == self.filter_name and self.match(property.params()[parameterName], access): result = self.defined break @@ -1063,7 +1222,7 @@ """ name = "is-defined" - def match(self, component): + def match(self, component, access): return component is not None class IsNotDefined (CalDAVEmptyElement): @@ -1073,7 +1232,7 @@ """ name = "is-not-defined" - def match(self, component): + def match(self, component, access): # Oddly, this needs always to return True so that it appears there is # a match - but we then "negate" the result if is-not-defined is set. # Actually this method should never be called as we special case the @@ -1128,7 +1287,7 @@ else: self.negate = False - def match(self, item): + def match(self, item, access): """ Match the text for the item. If the item is a property, then match the property value, @@ -1219,7 +1378,7 @@ # No other tests return True - def match(self, property): + def match(self, property, access): """ NB This is only called when doing a time-range match on a property. """ Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/config.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/config.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/config.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -145,6 +145,7 @@ # "EnableDropBox" : False, # Calendar Drop Box "EnableNotifications": False, # Drop Box Notifications + "EnablePrivateEvents": False, # Private Events # # Implementation details Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/customxml.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/customxml.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/customxml.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -34,6 +34,10 @@ "calendar-proxy", ) +calendarserver_private_events_compliance = ( + "calendarserver-private-events", +) + class TwistedGUIDProperty (davxml.WebDAVTextElement): """ Contains the GUID value for a directory record corresponding to a principal. @@ -56,6 +60,17 @@ def getValue(self): return str(self) +class TwistedCalendarAccessProperty (davxml.WebDAVTextElement): + """ + Contains the calendar access level (private events) for the resource. + """ + namespace = twisted_dav_namespace + name = "calendar-access" + hidden = True + + def getValue(self): + return str(self) + class CalendarProxyRead (davxml.WebDAVEmptyElement): """ A read-only calendar user proxy principal resource. Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/directory/calendar.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/directory/calendar.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/directory/calendar.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -24,6 +24,7 @@ "DirectoryCalendarHomeResource", ] +from twisted.internet.defer import succeed from twisted.web2.dav import davxml from twisted.web2.dav.util import joinURL from twisted.web2.dav.resource import TwistedACLInheritable, TwistedQuotaRootProperty @@ -255,6 +256,9 @@ # ACL ## + def owner(self, request): + return succeed(davxml.HRef(self.principalForRecord().principalURL())) + def defaultAccessControlList(self): # FIXME: directory.principalCollection smells like a hack # See DirectoryPrincipalProvisioningResource.__init__() Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/directory/calendaruserproxy.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/directory/calendaruserproxy.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/directory/calendaruserproxy.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -33,11 +33,10 @@ from twisted.web2.dav.util import joinURL from twisted.web2.http import HTTPError, StatusResponse -from twistedcaldav import caldavxml -from twistedcaldav import customxml from twistedcaldav.config import config from twistedcaldav.extensions import DAVFile, DAVPrincipalResource from twistedcaldav.extensions import ReadOnlyWritePropertiesResourceMixIn +from twistedcaldav.resource import CalDAVComplianceMixIn from twistedcaldav.sql import AbstractSQLDatabase from twistedcaldav.sql import db_prefix from twistedcaldav.static import AutoProvisioningFileMixIn @@ -74,16 +73,13 @@ # Permissions here are fixed, and are not subject to inherritance rules, etc. return succeed(self.defaultAccessControlList()) -class CalendarUserProxyPrincipalResource (AutoProvisioningFileMixIn, PermissionsMixIn, DAVPrincipalResource, DAVFile): +class CalendarUserProxyPrincipalResource (AutoProvisioningFileMixIn, PermissionsMixIn, DAVPrincipalResource, DAVFile, CalDAVComplianceMixIn): """ Calendar user proxy principal resource. """ def davComplianceClasses(self): - extra_compliance = caldavxml.caldav_compliance - if config.EnableProxyPrincipals: - extra_compliance += customxml.calendarserver_proxy_compliance - return tuple(super(CalendarUserProxyPrincipalResource, self).davComplianceClasses()) + extra_compliance + return tuple(super(CalendarUserProxyPrincipalResource, self).davComplianceClasses()) + self.caldavComplianceClasses() def __init__(self, path, parent, proxyType): """ Copied: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/fileops.py (from rev 2099, CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/fileops.py) =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/fileops.py (rev 0) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/fileops.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -0,0 +1,111 @@ +## +# Copyright (c) 2005-2008 Apple Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## +from twisted.internet.defer import deferredGenerator +from twisted.web2.dav.fileop import put +from twisted.internet.defer import waitForDeferred +from twisted.web2.dav.fileop import copy + +""" +Various file utilities. +""" + +from twisted.web2.dav.xattrprops import xattrPropertyStore + +# This class simulates a DAVFile with enough information for use with xattrPropertyStore. +class FakeXAttrResource(object): + + def __init__(self, fp): + self.fp = fp + +@deferredGenerator +def putWithXAttrs(stream, filepath): + """ + Write a file to a possibly existing path and preserve any xattrs at that path. + + @param stream: the stream to write to the destination. + @type stream: C{file} + @param filepath: the destination file. + @type filepath: L{FilePath} + """ + + # Preserve existings xattrs + props = [] + if filepath.exists(): + xold = xattrPropertyStore(FakeXAttrResource(filepath)) + for item in xold.list(): + props.append((xold.get(item))) + xold = None + + # First do the actual file copy + d = waitForDeferred(put(stream, filepath)) + yield d + response = d.getResult() + + # Restore original xattrs. + if props: + xnew = xattrPropertyStore(FakeXAttrResource(filepath)) + for prop in props: + xnew.set(prop) + xnew = None + + yield response + +@deferredGenerator +def copyWithXAttrs(source_filepath, destination_filepath, destination_uri): + """ + Copy a file from one path to another and also copy xattrs we care about. + + @param source_filepath: the file to copy from + @type source_filepath: L{FilePath} + @param destination_filepath: the file to copy to + @type destination_filepath: L{FilePath} + @param destination_uri: the URI of the destination resource + @type destination_uri: C{str} + """ + + # First do the actual file copy + d = waitForDeferred(copy(source_filepath, destination_filepath, destination_uri, "0")) + yield d + response = d.getResult() + + # Now copy over xattrs. + copyXAttrs(source_filepath, destination_filepath) + + yield response + +def copyToWithXAttrs(from_fp, to_fp): + """ + Copy a file from one path to another and also copy xattrs we care about. + + @param from_fp: file being copied + @type from_fp: L{FilePath} + @param to_fp: file to copy to + @type to_fp: L{FilePath} + """ + + # First do the actual file copy. + from_fp.copyTo(to_fp) + + # Now copy over xattrs. + copyXAttrs(from_fp, to_fp) + +def copyXAttrs(from_fp, to_fp): + # Create xattr stores for each file and copy over all xattrs. + xfrom = xattrPropertyStore(FakeXAttrResource(from_fp)) + xto = xattrPropertyStore(FakeXAttrResource(to_fp)) + + for item in xfrom.list(): + xto.set(xfrom.get(item)) Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/ical.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/ical.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/ical.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -160,6 +160,31 @@ """ X{iCalendar} component. """ + + # Private Event access levels. + ACCESS_PROPERTY = "X-CALENDARSERVER-ACCESS" + ACCESS_PUBLIC = "PUBLIC" + ACCESS_PRIVATE = "PRIVATE" + ACCESS_CONFIDENTIAL = "CONFIDENTIAL" + ACCESS_RESTRICTED = "RESTRICTED" + + accessMap = { + "PUBLIC" : ACCESS_PUBLIC, + "PRIVATE" : ACCESS_PRIVATE, + "CONFIDENTIAL" : ACCESS_CONFIDENTIAL, + "RESTRICTED" : ACCESS_RESTRICTED, + } + + confidentialPropertiesMap = { + "VCALENDAR": ("PRODID", "VERSION", "CALSCALE", ACCESS_PROPERTY), + "VEVENT": ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "STATUS", "TRANSP", "DTSTART", "DTEND", "DURATION", "RRULE", "RDATE", "EXRULE", "EXDATE", ), + "VTODO": ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "STATUS", "DTSTART", "COMPLETED", "DUE", "DURATION", "RRULE", "RDATE", "EXRULE", "EXDATE", ), + "VJOURNAL": ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "STATUS", "DTSTART", "RRULE", "RDATE", "EXRULE", "EXDATE", ), + "VFREEBUSY": ("UID", "DTSTAMP", "DTSTART", "DTEND", "DURATION", "FREEBUSY", ), + "VTIMEZONE": None, + } + extraRestrictedProperties = ("SUMMARY", "LOCATION",) + @classmethod def fromString(clazz, string): """ @@ -364,6 +389,18 @@ return None + def accessLevel(self, default=ACCESS_PUBLIC): + """ + Return the access level for this component. + @return: the access level for the calendar data. + """ + assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,) + + access = self.propertyValue(Component.ACCESS_PROPERTY) + if access: + access = access.upper() + return Component.accessMap.get(access, default) + def duplicate(self): """ Duplicate this object and all its contents. Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/__init__.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/__init__.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/__init__.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -24,6 +24,7 @@ __all__ = [ "copymove", "delete", + "get", "mkcalendar", "mkcol", "put", Copied: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/get.py (from rev 2099, CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/method/get.py) =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/get.py (rev 0) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/get.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -0,0 +1,72 @@ +## +# Copyright (c) 2005-2008 Apple Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +""" +CalDAV GET method. +""" + +__all__ = ["http_GET"] + +from twisted.internet.defer import deferredGenerator, waitForDeferred +from twisted.web2.dav import davxml +from twisted.web2.http import HTTPError +from twisted.web2.http import Response +from twisted.web2.http_headers import MimeType +from twisted.web2.stream import MemoryStream + +from twistedcaldav import caldavxml +from twistedcaldav.customxml import TwistedCalendarAccessProperty +from twistedcaldav.ical import Component + +def http_GET(self, request): + + # Look for calendar access restriction on existing resource. + if self.exists(): + try: + access = self.readDeadProperty(TwistedCalendarAccessProperty) + except HTTPError: + access = None + + if access in (Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED): + + # Check authorization first + d = waitForDeferred(self.authorize(request, (davxml.Read(),))) + yield d + d.getResult() + + # Non DAV:owner's have limited access to the data + d = waitForDeferred(self.isOwner(request)) + yield d + isowner = d.getResult() + + if not isowner: + + # Now "filter" the resource calendar data through the CALDAV:calendar-data element and apply + # access restrictions to the data. + caldata = caldavxml.CalendarData().elementFromResourceWithAccessRestrictions(self, access).calendarData() + + response = Response() + response.stream = MemoryStream(caldata) + response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8")) + yield response + return + + # Do normal GET behavior + d = waitForDeferred(super(CalDAVFile, self).http_GET(request)) + yield d + yield d.getResult() + +http_GET = deferredGenerator(http_GET) Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/put_common.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/put_common.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/put_common.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -31,7 +31,7 @@ from twisted.web2.dav import davxml from twisted.web2.dav.element.base import dav_namespace from twisted.web2.dav.element.base import PCDATAElement -from twisted.web2.dav.fileop import copy, delete, put +from twisted.web2.dav.fileop import delete from twisted.web2.dav.http import ErrorResponse from twisted.web2.dav.resource import TwistedGETContentMD5 from twisted.web2.dav.stream import MD5StreamWrapper @@ -46,10 +46,16 @@ from twistedcaldav.caldavxml import NoUIDConflict from twistedcaldav.caldavxml import NumberOfRecurrencesWithinLimits from twistedcaldav.caldavxml import caldav_namespace -from twistedcaldav.ical import Component +from twistedcaldav.customxml import calendarserver_namespace +from twistedcaldav.customxml import TwistedCalendarAccessProperty +from twistedcaldav.fileops import copyToWithXAttrs +from twistedcaldav.fileops import putWithXAttrs +from twistedcaldav.fileops import copyWithXAttrs +from twistedcaldav.ical import Component, Property from twistedcaldav.index import ReservationError from twistedcaldav.instance import TooManyInstancesError +@deferredGenerator def storeCalendarObjectResource( request, sourcecal, destinationcal, @@ -151,7 +157,7 @@ if self.source_index_deleted: doSourceIndexRecover() self.destination_index_deleted = False - logging.debug("Rollback: soyurce re-indexed %s" % (source.fp.path,), system="Store Resource") + logging.debug("Rollback: source re-indexed %s" % (source.fp.path,), system="Store Resource") except: log.err("Rollback: exception caught and not handled: %s" % failure.Failure()) @@ -258,6 +264,39 @@ return result, message + @deferredGenerator + def validAccess(): + """ + Make sure that the X-CALENDARSERVER-ACCESS property is properly dealt with. + """ + + if calendar.hasProperty(Component.ACCESS_PROPERTY): + + # Must be a value we know about + access = calendar.accessLevel(default=None) + if access is None: + raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "valid-access-restriction"))) + + # Only DAV:owner is able to set the property to other than PUBLIC + d = waitForDeferred(destinationparent.owner(request)) + yield d + parent_owner = d.getResult() + + authz = destinationparent.currentPrincipal(request) + if davxml.Principal(parent_owner) != authz and access != Component.ACCESS_PUBLIC: + raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "valid-access-restriction-change"))) + + yield access, calendardata + else: + # Check whether an access property was present before and write that into the calendar data + newcalendardata = calendardata + if not source and destination.exists() and destination.hasDeadProperty(TwistedCalendarAccessProperty): + old_access = str(destination.readDeadProperty(TwistedCalendarAccessProperty)) + calendar.addProperty(Property(name=Component.ACCESS_PROPERTY, value=old_access)) + newcalendardata = str(calendar) + + yield None, newcalendardata + def noUIDConflict(uid): """ Check that the UID of the new calendar object conforms to the requirements of @@ -305,6 +344,7 @@ Handle validation operations here. """ reserved = False + access = None if destinationcal: # Valid resource name check result, message = validResourceName() @@ -361,6 +401,12 @@ log.err(message) raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "max-resource-size"))) + # Check access + if destinationcal and config.EnablePrivateEvents: + d = waitForDeferred(validAccess()) + yield d + access, calendardata = d.getResult() + # Reserve UID destination_index = destinationparent.index() @@ -437,7 +483,7 @@ if overwrite: rollback.destination_copy = FilePath(destination.fp.path) rollback.destination_copy.path += ".rollback" - destination.fp.copyTo(rollback.destination_copy) + copyToWithXAttrs(destination.fp, rollback.destination_copy) logging.debug("Rollback: backing up destination %s to %s" % (destination.fp.path, rollback.destination_copy.path), system="Store Resource") else: rollback.destination_created = True @@ -446,7 +492,7 @@ if deletesource: rollback.source_copy = FilePath(source.fp.path) rollback.source_copy.path += ".rollback" - source.fp.copyTo(rollback.source_copy) + copyToWithXAttrs(source.fp, rollback.source_copy) logging.debug("Rollback: backing up source %s to %s" % (source.fp.path, rollback.source_copy.path), system="Store Resource") """ @@ -465,10 +511,10 @@ # Do put or copy based on whether source exists if source is not None: - response = maybeDeferred(copy, source.fp, destination.fp, destination_uri, "0") + response = maybeDeferred(copyWithXAttrs, source.fp, destination.fp, destination_uri) else: md5 = MD5StreamWrapper(MemoryStream(calendardata)) - response = maybeDeferred(put, md5, destination.fp) + response = maybeDeferred(putWithXAttrs, md5, destination.fp) response = waitForDeferred(response) yield response response = response.getResult() @@ -485,8 +531,18 @@ md5 = md5.getMD5() destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5)) + # Update calendar-access property value on the resource + if access: + destination.writeDeadProperty(TwistedCalendarAccessProperty(access)) + + # Do not remove the property if access was not specified and we are storing in a calendar. + # This ensure that clients that do not preserve the iCalendar property do not cause access + # restrictions to be lost. + elif not destinationcal: + destination.removeDeadProperty(TwistedCalendarAccessProperty) + response = IResponse(response) - + def doDestinationIndex(caltoindex): """ Do destination resource indexing, replacing any index previous stored. @@ -616,5 +672,3 @@ # if the rollback has already ocurred or changes already committed. rollback.Rollback() raise - -storeCalendarObjectResource = deferredGenerator(storeCalendarObjectResource) Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/report_calquery.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/report_calquery.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/report_calquery.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -31,6 +31,7 @@ from twisted.web2.http import HTTPError, StatusResponse from twistedcaldav.caldavxml import caldav_namespace +from twistedcaldav.customxml import TwistedCalendarAccessProperty from twistedcaldav.method import report_common import urllib @@ -104,7 +105,7 @@ @param uri: the uri for the calendar collecton resource. """ - def queryCalendarObjectResource(resource, uri, name, calendar, query_ok = False): + def queryCalendarObjectResource(resource, uri, name, calendar, query_ok=False, isowner=True): """ Run a query on the specified calendar. @param resource: the L{CalDAVFile} for the calendar. @@ -113,7 +114,16 @@ @param calendar: the L{Component} calendar read from the resource. """ - if query_ok or filter.match(calendar): + # Handle private events access restrictions + if not isowner: + try: + access = resource.readDeadProperty(TwistedCalendarAccessProperty) + except HTTPError: + access = None + else: + access = None + + if query_ok or filter.match(calendar, access): # Check size of results is within limit matchcount[0] += 1 if matchcount[0] > max_number_of_results: @@ -124,7 +134,7 @@ else: href = davxml.HRef.fromString(uri) - return report_common.responseForHref(request, responses, href, resource, calendar, propertiesForResource, query) + return report_common.responseForHref(request, responses, href, resource, calendar, propertiesForResource, query, isowner) else: return succeed(None) @@ -146,7 +156,12 @@ filteredaces = waitForDeferred(calresource.inheritedACEsforChildren(request)) yield filteredaces filteredaces = filteredaces.getResult() - + + # Check private events access status + d = waitForDeferred(calresource.isOwner(request)) + yield d + isowner = d.getResult() + # Check for disabled access if filteredaces is not None: # See whether the filter is valid for an index only query @@ -183,7 +198,7 @@ else: calendar = None - d = waitForDeferred(queryCalendarObjectResource(child, uri, child_uri_name, calendar, query_ok = index_query_ok)) + d = waitForDeferred(queryCalendarObjectResource(child, uri, child_uri_name, calendar, query_ok = index_query_ok, isowner=isowner)) yield d d.getResult() else: @@ -205,6 +220,11 @@ tz = tz.getResult() filter.settimezone(tz) + # Check private events access status + d = waitForDeferred(calresource.isOwner(request)) + yield d + isowner = d.getResult() + calendar = calresource.iCalendar() d = waitForDeferred(queryCalendarObjectResource(calresource, uri, None, calendar)) yield d Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/report_common.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/report_common.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/report_common.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ## +from twistedcaldav.customxml import TwistedCalendarAccessProperty __all__ = [ "applyToCalendarCollections", @@ -93,7 +94,7 @@ applyToCalendarCollections = deferredGenerator(applyToCalendarCollections) -def responseForHref(request, responses, href, resource, calendar, propertiesForResource, propertyreq): +def responseForHref(request, responses, href, resource, calendar, propertiesForResource, propertyreq, isowner=True): """ Create an appropriate property status response for the given resource. @@ -102,10 +103,12 @@ @param href: the L{HRef} element of the resource being targetted. @param resource: the L{CalDAVFile} for the targetted resource. @param calendar: the L{Component} for the calendar for the resource. This may be None - if the calendar has not already been read in, in which case the resource - will be used to get the calendar if needed. + if the calendar has not already been read in, in which case the resource + will be used to get the calendar if needed. @param propertiesForResource: the method to use to get the list of properties to return. @param propertyreq: the L{PropertyContainer} element for the properties of interest. + @param isowner: C{True} if the authorized principal making the request is the DAV:owner, + C{False} otherwise. """ def _defer(properties_by_status): @@ -122,38 +125,42 @@ ) ) - d = propertiesForResource(request, propertyreq, resource, calendar) + d = propertiesForResource(request, propertyreq, resource, calendar, isowner) d.addCallback(_defer) return d -def allPropertiesForResource(request, prop, resource, calendar=None): #@UnusedVariable +def allPropertiesForResource(request, prop, resource, calendar=None, isowner=True): #@UnusedVariable """ Return all (non-hidden) properties for the specified resource. @param request: the L{IRequest} for the current request. @param prop: the L{PropertyContainer} element for the properties of interest. @param resource: the L{CalDAVFile} for the targetted resource. @param calendar: the L{Component} for the calendar for the resource. This may be None - if the calendar has not already been read in, in which case the resource - will be used to get the calendar if needed. + if the calendar has not already been read in, in which case the resource + will be used to get the calendar if needed. + @param isowner: C{True} if the authorized principal making the request is the DAV:owner, + C{False} otherwise. @return: a map of OK and NOT FOUND property values. """ def _defer(props): - return _namedPropertiesForResource(request, props, resource, calendar) + return _namedPropertiesForResource(request, props, resource, calendar, isowner) d = resource.listAllprop(request) d.addCallback(_defer) return d -def propertyNamesForResource(request, prop, resource, calendar=None): #@UnusedVariable +def propertyNamesForResource(request, prop, resource, calendar=None, isowner=True): #@UnusedVariable """ Return property names for all properties on the specified resource. @param request: the L{IRequest} for the current request. @param prop: the L{PropertyContainer} element for the properties of interest. @param resource: the L{CalDAVFile} for the targetted resource. @param calendar: the L{Component} for the calendar for the resource. This may be None - if the calendar has not already been read in, in which case the resource - will be used to get the calendar if needed. + if the calendar has not already been read in, in which case the resource + will be used to get the calendar if needed. + @param isowner: C{True} if the authorized principal making the request is the DAV:owner, + C{False} otherwise. @return: a map of OK and NOT FOUND property values. """ @@ -167,19 +174,21 @@ d.addCallback(_defer) return d -def propertyListForResource(request, prop, resource, calendar=None): +def propertyListForResource(request, prop, resource, calendar=None, isowner=True): """ Return the specified properties on the specified resource. @param request: the L{IRequest} for the current request. @param prop: the L{PropertyContainer} element for the properties of interest. @param resource: the L{CalDAVFile} for the targetted resource. @param calendar: the L{Component} for the calendar for the resource. This may be None - if the calendar has not already been read in, in which case the resource - will be used to get the calendar if needed. + if the calendar has not already been read in, in which case the resource + will be used to get the calendar if needed. + @param isowner: C{True} if the authorized principal making the request is the DAV:owner, + C{False} otherwise. @return: a map of OK and NOT FOUND property values. """ - return _namedPropertiesForResource(request, prop.children, resource, calendar) + return _namedPropertiesForResource(request, prop.children, resource, calendar, isowner) def validPropertyListCalendarDataTypeVersion(prop): """ @@ -204,15 +213,17 @@ return result, message, generate_calendar_data -def _namedPropertiesForResource(request, props, resource, calendar=None): +def _namedPropertiesForResource(request, props, resource, calendar=None, isowner=True): """ 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{CalDAVFile} for the targetted resource. @param calendar: the L{Component} for the calendar for the resource. This may be None - if the calendar has not already been read in, in which case the resource - will be used to get the calendar if needed. + if the calendar has not already been read in, in which case the resource + will be used to get the calendar if needed. + @param isowner: C{True} if the authorized principal making the request is the DAV:owner, + C{False} otherwise. @return: a map of OK and NOT FOUND property values. """ properties_by_status = { @@ -222,10 +233,19 @@ for property in props: if isinstance(property, caldavxml.CalendarData): + # Handle private events access restrictions + if not isowner: + try: + access = resource.readDeadProperty(TwistedCalendarAccessProperty) + except HTTPError: + access = None + else: + access = None + if calendar: - propvalue = property.elementFromCalendar(calendar) + propvalue = property.elementFromCalendarWithAccessRestrictions(calendar, access) else: - propvalue = property.elementFromResource(resource) + propvalue = property.elementFromResourceWithAccessRestrictions(resource, access) if propvalue is None: raise ValueError("Invalid CalDAV:calendar-data for request: %r" % (property,)) properties_by_status[responsecode.OK].append(propvalue) @@ -358,7 +378,7 @@ elif (calendar.getOrganizer() is None) and same_calendar_user: continue - if filter.match(calendar): + if filter.match(calendar, None): # Check size of results is within limit matchtotal += 1 if matchtotal > max_number_of_matches: Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/report_multiget.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/report_multiget.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/method/report_multiget.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -63,17 +63,15 @@ if propertyreq.qname() == ("DAV:", "allprop"): propertiesForResource = report_common.allPropertiesForResource - generate_calendar_data = False elif propertyreq.qname() == ("DAV:", "propname"): propertiesForResource = report_common.propertyNamesForResource - generate_calendar_data = False elif propertyreq.qname() == ("DAV:", "prop"): propertiesForResource = report_common.propertyListForResource # Verify that any calendar-data element matches what we can handle - result, message, generate_calendar_data = report_common.validPropertyListCalendarDataTypeVersion(propertyreq) + result, message, _ignore = report_common.validPropertyListCalendarDataTypeVersion(propertyreq) if not result: log.err(message) raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data"))) @@ -113,14 +111,21 @@ # Check for disabled access if filteredaces is None: disabled = True + + # Check private events access status + d = waitForDeferred(self.isOwner(request)) + yield d + isowner = d.getResult() elif self.isCollection(): requestURIis = "collection" filteredaces = None lastParent = None + isowner = None else: requestURIis = "resource" filteredaces = None + isowner = None if not disabled: @@ -169,7 +174,7 @@ # Get properties for all valid readable resources for resource, href in ok_resources: - d = waitForDeferred(report_common.responseForHref(request, responses, davxml.HRef.fromString(href), resource, None, propertiesForResource, propertyreq)) + d = waitForDeferred(report_common.responseForHref(request, responses, davxml.HRef.fromString(href), resource, None, propertiesForResource, propertyreq, isowner=isowner)) yield d d.getResult() @@ -233,7 +238,11 @@ filteredaces = waitForDeferred(parent.inheritedACEsforChildren(request)) yield filteredaces filteredaces = filteredaces.getResult() - + + # Check private events access status + d = waitForDeferred(parent.isOwner(request)) + yield d + isowner = d.getResult() else: name = unquote(resource_uri[resource_uri.rfind("/") + 1:]) if (resource_uri != request.uri) or not self.exists(): @@ -254,6 +263,11 @@ filteredaces = waitForDeferred(parent.inheritedACEsforChildren(request)) yield filteredaces filteredaces = filteredaces.getResult() + + # Check private events access status + d = waitForDeferred(parent.isOwner(request)) + yield d + isowner = d.getResult() # Check privileges - must have at least DAV:read try: @@ -264,7 +278,7 @@ responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN))) continue - d = waitForDeferred(report_common.responseForHref(request, responses, href, child, None, propertiesForResource, propertyreq)) + d = waitForDeferred(report_common.responseForHref(request, responses, href, child, None, propertiesForResource, propertyreq, isowner=isowner)) yield d d.getResult() Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/query/calendarquery.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/query/calendarquery.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/query/calendarquery.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -57,6 +57,10 @@ assert vcalfilter.filter_name == "VCALENDAR" if len(vcalfilter.children) > 0: + # Only comp-filters are handled + for _ignore in [x for x in vcalfilter.children if not isinstance(x, caldavxml.ComponentFilter)]: + raise ValueError + return compfilterListExpression(vcalfilter.children) else: return expression.allExpression() @@ -111,7 +115,7 @@ # Handle embedded components - we do not right now as our Index does not handle them comps = [] - for c in [x for x in compfilter.filters if isinstance(x, caldavxml.ComponentFilter)]: + for _ignore in [x for x in compfilter.filters if isinstance(x, caldavxml.ComponentFilter)]: raise ValueError if len(comps) > 1: compsExpression = expression.orExpression[comps] @@ -162,7 +166,7 @@ # Handle embedded parameters - we do not right now as our Index does not handle them params = [] - for p in propfilter.filters: + for _ignore in propfilter.filters: raise ValueError if len(params) > 1: paramsExpression = expression.orExpression[params] Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/resource.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/resource.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/resource.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -19,6 +19,7 @@ """ __all__ = [ + "CalDAVComplianceMixIn", "CalDAVResource", "CalendarPrincipalCollectionResource", "CalendarPrincipalResource", @@ -30,6 +31,8 @@ from twisted.internet import reactor from twisted.internet.defer import Deferred, maybeDeferred, succeed +from twisted.internet.defer import waitForDeferred +from twisted.internet.defer import deferredGenerator from twisted.web2 import responsecode from twisted.web2.dav import davxml from twisted.web2.dav.idav import IDAVPrincipalCollectionResource @@ -47,7 +50,9 @@ import twistedcaldav from twistedcaldav import caldavxml, customxml from twistedcaldav.config import config +from twistedcaldav.customxml import TwistedCalendarAccessProperty from twistedcaldav.extensions import DAVResource, DAVPrincipalResource +from twistedcaldav.ical import Component from twistedcaldav.icaldav import ICalDAVResource, ICalendarPrincipalResource from twistedcaldav.caldavxml import caldav_namespace from twistedcaldav.customxml import calendarserver_namespace @@ -59,7 +64,18 @@ else: serverVersion = twisted.web2.server.VERSION + " TwistedCalDAV/?" -class CalDAVResource (DAVResource): +class CalDAVComplianceMixIn(object): + + def caldavComplianceClasses(self): + extra_compliance = caldavxml.caldav_compliance + if config.EnableProxyPrincipals: + extra_compliance += customxml.calendarserver_proxy_compliance + if config.EnablePrivateEvents: + extra_compliance += customxml.calendarserver_private_events_compliance + return extra_compliance + + +class CalDAVResource (DAVResource, CalDAVComplianceMixIn): """ CalDAV resource. @@ -118,12 +134,10 @@ ## def davComplianceClasses(self): - extra_compliance = caldavxml.caldav_compliance - if config.EnableProxyPrincipals: - extra_compliance += customxml.calendarserver_proxy_compliance - return tuple(super(CalDAVResource, self).davComplianceClasses()) + extra_compliance + return tuple(super(CalDAVResource, self).davComplianceClasses()) + self.caldavComplianceClasses() liveProperties = DAVResource.liveProperties + ( + (dav_namespace, "owner"), # Private Events needs this but it is also OK to return empty (caldav_namespace, "supported-calendar-component-set"), (caldav_namespace, "supported-calendar-data" ), ) @@ -140,7 +154,13 @@ namespace, name = qname - if namespace == caldav_namespace: + if namespace == dav_namespace: + if name == "owner": + d = self.owner(request) + d.addCallback(lambda x: davxml.Owner(x)) + return d + + elif namespace == caldav_namespace: if name == "supported-calendar-component-set": # CalDAV-access-09, section 5.2.3 if self.hasDeadProperty(qname): @@ -220,12 +240,83 @@ return self.hasDeadProperty(AccessDisabled) # FIXME: Perhaps this is better done in authorize() instead. - def accessControlList(self, *args, **kwargs): + @deferredGenerator + def accessControlList(self, request, *args, **kwargs): if self.isDisabled(): - return succeed(None) + yield None + return - return super(CalDAVResource, self).accessControlList(*args, **kwargs) + d = waitForDeferred(super(CalDAVResource, self).accessControlList(request, *args, **kwargs)) + yield d + acls = d.getResult() + # Look for private events access classification + if self.hasDeadProperty(TwistedCalendarAccessProperty): + access = self.readDeadProperty(TwistedCalendarAccessProperty) + if access.getValue() in (Component.ACCESS_PRIVATE, Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED,): + # Need to insert ACE to prevent non-owner principals from seeing this resource + d = waitForDeferred(self.owner(request)) + yield d + owner = d.getResult() + if access.getValue() == Component.ACCESS_PRIVATE: + ace = davxml.ACE( + davxml.Invert( + davxml.Principal(owner), + ), + davxml.Deny( + davxml.Privilege( + davxml.Read(), + ), + davxml.Privilege( + davxml.Write(), + ), + ), + davxml.Protected(), + ) + else: + ace = davxml.ACE( + davxml.Invert( + davxml.Principal(owner), + ), + davxml.Deny( + davxml.Privilege( + davxml.Write(), + ), + ), + davxml.Protected(), + ) + + acls = davxml.ACL(ace, *acls.children) + yield acls + + @deferredGenerator + def owner(self, request): + """ + Return the DAV:owner property value (MUST be a DAV:href or None). + """ + d = waitForDeferred(self.locateParent(request, request.urlForResource(self))) + yield d + parent = d.getResult() + if parent and isinstance(parent, CalDAVResource): + d = waitForDeferred(parent.owner(request)) + yield d + yield d.getResult() + else: + yield None + + @deferredGenerator + def isOwner(self, request): + """ + Determine whether the DAV:owner of this resource matches the currently authorized principal + in the request. + """ + + d = waitForDeferred(self.owner(request)) + yield d + owner = d.getResult() + result = (davxml.Principal(owner) == self.currentPrincipal(request)) + yield result + ## # CalDAV ## @@ -471,7 +562,7 @@ ), ) -class CalendarPrincipalResource (DAVPrincipalResource): +class CalendarPrincipalResource (DAVPrincipalResource, CalDAVComplianceMixIn): """ CalDAV principal resource. @@ -480,10 +571,7 @@ implements(ICalendarPrincipalResource) def davComplianceClasses(self): - extra_compliance = caldavxml.caldav_compliance - if config.EnableProxyPrincipals: - extra_compliance += customxml.calendarserver_proxy_compliance - return tuple(super(CalendarPrincipalResource, self).davComplianceClasses()) + extra_compliance + return tuple(super(CalendarPrincipalResource, self).davComplianceClasses()) + self.caldavComplianceClasses() liveProperties = tuple(DAVPrincipalResource.liveProperties) + ( (caldav_namespace, "calendar-home-set" ), Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/schedule.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/schedule.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/schedule.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -35,6 +35,7 @@ from twisted.web2.dav.util import joinURL from twistedcaldav import caldavxml +from twistedcaldav import customxml from twistedcaldav import itip from twistedcaldav import logging from twistedcaldav.resource import CalDAVResource @@ -271,6 +272,11 @@ if not calendar.isValidITIP(): logging.err("POST request must have a calendar component that satisfies iTIP requirements: %s" % (calendar,), system="CalDAV Outbox POST") raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"))) + + # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs + if calendar.hasProperty(Component.ACCESS_PROPERTY): + logging.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component POST request: %s" % (calendar,), system="CalDAV Outbox POST") + raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions"))) # Verify that the ORGANIZER's cu address maps to the request.uri outboxURL = None Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/static.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/static.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/static.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -795,5 +795,6 @@ # FIXME: Little bit of a circular dependency here... twistedcaldav.method.copymove.CalDAVFile = CalDAVFile twistedcaldav.method.delete.CalDAVFile = CalDAVFile +twistedcaldav.method.get.CalDAVFile = CalDAVFile twistedcaldav.method.mkcol.CalDAVFile = CalDAVFile twistedcaldav.method.put.CalDAVFile = CalDAVFile Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/test/test_config.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/test/test_config.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/test/test_config.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -25,16 +25,15 @@ <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> - <key>Verbose</key> - <true/> - <key>HTTPPort</key> - <integer>8008</integer> + <key>Verbose</key> + <true/> + <key>HTTPPort</key> + <integer>8008</integer> </dict> </plist> """ def _testVerbose(testCase): - from twistedcaldav.config import config testCase.assertEquals(config.Verbose, True) @@ -222,3 +221,9 @@ config.EnableProxyPrincipals = False self.assertTrue("calendar-proxy" not in resource.davComplianceClasses()) + + config.EnablePrivateEvents = True + self.assertTrue("calendarserver-private-events" in resource.davComplianceClasses()) + + config.EnablePrivateEvents = False + self.assertTrue("calendarserver-private-events" not in resource.davComplianceClasses()) Modified: CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/test/test_xml.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/test/test_xml.py 2008-01-10 20:51:00 UTC (rev 2099) +++ CalendarServer/branches/users/cdaboo/private_events-2098/twistedcaldav/test/test_xml.py 2008-01-10 21:16:14 UTC (rev 2100) @@ -46,7 +46,7 @@ name=component_name ), name="VCALENDAR" - ).match(self.calendar): + ).match(self.calendar, None): self.fail("Calendar has %s%s?" % (no, component_name)) def test_PropertyFilter(self): @@ -68,7 +68,7 @@ name="VEVENT" ), name="VCALENDAR" - ).match(self.calendar): + ).match(self.calendar, None): self.fail("Calendar has %sVEVENT with %s?" % (no, property_name)) def test_ParameterFilter(self): @@ -99,7 +99,7 @@ name="VEVENT" ), name="VCALENDAR" - ).match(self.calendar): + ).match(self.calendar, None): self.fail("Calendar has %sVEVENT with UID %s? (caseless=%s)" % (no, uid, caseless)) def test_TimeRange(self):