Revision: 2090 http://trac.macosforge.org/projects/calendarserver/changeset/2090 Author: cdaboo@apple.com Date: 2008-01-04 14:38:23 -0800 (Fri, 04 Jan 2008) Log Message: ----------- Handle X-CALENDARSERVER-ACCESS on PUT. This includes setting up ACLs for PRIVATE to prevent access by anyone except DAV:owner. Still have to do data filtering for CONFIDENTIAL and RESTRICTED. Modified Paths: -------------- CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/__init__.py CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/customxml.py CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/ical.py CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/method/put_common.py CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/resource.py Added Paths: ----------- CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/fileops.py Modified: CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/__init__.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/__init__.py 2008-01-02 22:24:22 UTC (rev 2089) +++ CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/__init__.py 2008-01-04 22:38:23 UTC (rev 2090) @@ -31,6 +31,7 @@ "directory", "dropbox", "extensions", + "fileops", "ical", "icaldav", "index", Modified: CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/customxml.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/customxml.py 2008-01-02 22:24:22 UTC (rev 2089) +++ CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/customxml.py 2008-01-04 22:38:23 UTC (rev 2090) @@ -60,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. Added: CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/fileops.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/fileops.py (rev 0) +++ CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/fileops.py 2008-01-04 22:38:23 UTC (rev 2090) @@ -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-2081/twistedcaldav/ical.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/ical.py 2008-01-02 22:24:22 UTC (rev 2089) +++ CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/ical.py 2008-01-04 22:38:23 UTC (rev 2090) @@ -379,7 +379,7 @@ return None - def accessLevel(self): + def accessLevel(self, default=ACCESS_PUBLIC): """ Return the access level for this component. @return: the access level for the calendar data. @@ -389,7 +389,7 @@ access = self.propertyValue(Component.ACCESS_PROPERTY) if access: access = access.upper() - return Component.accessMap.get(access, Component.ACCESS_PUBLIC) + return Component.accessMap.get(access, default) def duplicate(self): """ Modified: CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/method/put_common.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/method/put_common.py 2008-01-02 22:24:22 UTC (rev 2089) +++ CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/method/put_common.py 2008-01-04 22:38:23 UTC (rev 2090) @@ -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-2081/twistedcaldav/resource.py =================================================================== --- CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/resource.py 2008-01-02 22:24:22 UTC (rev 2089) +++ CalendarServer/branches/users/cdaboo/private_events-2081/twistedcaldav/resource.py 2008-01-04 22:38:23 UTC (rev 2090) @@ -50,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 @@ -238,12 +240,55 @@ 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): """