[CalendarServer-changes] [5895] CalendarServer/branches/new-store

source_changes at macosforge.org source_changes at macosforge.org
Wed Jul 14 04:50:42 PDT 2010


Revision: 5895
          http://trac.macosforge.org/projects/calendarserver/changeset/5895
Author:   glyph at apple.com
Date:     2010-07-14 04:50:41 -0700 (Wed, 14 Jul 2010)
Log Message:
-----------
detect X-APPLE-DROPBOX property and enforce permissions based on it; also simplify permission checking logic.

Modified Paths:
--------------
    CalendarServer/branches/new-store/twistedcaldav/storebridge.py
    CalendarServer/branches/new-store/txcaldav/calendarstore/file.py

Modified: CalendarServer/branches/new-store/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/storebridge.py	2010-07-14 11:44:03 UTC (rev 5894)
+++ CalendarServer/branches/new-store/twistedcaldav/storebridge.py	2010-07-14 11:50:41 UTC (rev 5895)
@@ -129,15 +129,28 @@
 
 
 
-def requiresPermissions(*permissions):
+def requiresPermissions(*permissions, **kw):
     """
     A decorator to wrap http_ methods in, to indicate that they should not be
     run until the current user principal has been authorized for the given
     permission set.
     """
+    fromParent = kw.get('fromParent')
+    # FIXME: direct unit tests
     def wrap(thunk):
         def authAndContinue(self, request):
-            d = self.authorize(request, permissions)
+            if permissions:
+                d = self.authorize(request, permissions)
+            else:
+                d = succeed(None)
+            if fromParent:
+                d.addCallback(
+                    lambda whatever:
+                        request.locateResource(parentForURL(request.uri))
+                ).addCallback(
+                    lambda parent:
+                        parent.authorize(request, fromParent)
+                )
             d.addCallback(lambda whatever: thunk(self, request))
             return d
         return authAndContinue
@@ -394,6 +407,7 @@
                 principalCollections=self.principalCollections())
         else:
             result = CalendarAttachment(
+                self._newStoreCalendarObject,
                 attachment, principalCollections=self.principalCollections())
         self.propagateTransaction(result)
         return result
@@ -455,6 +469,9 @@
         """
         d = super(CalendarObjectDropbox, self).accessControlList(*a, **kw)
         def moreACLs(originalACL):
+            othersCanWrite = (
+                self._newStoreCalendarObject.attendeesCanManageAttachments()
+            )
             originalACEs = list(originalACL.children)
             cuas = self._newStoreCalendarObject.component().getAttendees()
             newACEs = []
@@ -463,9 +480,16 @@
                     calendarUserAddress
                 )
                 principalURL = principal.principalURL()
+                if othersCanWrite:
+                    privileges = [davxml.Privilege(davxml.All())]
+                else:
+                    privileges = [
+                        davxml.Privilege(davxml.Read()),
+                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet())
+                    ]
                 newACEs.append(davxml.ACE(
                     davxml.Principal(davxml.HRef(principalURL)),
-                    davxml.Grant(davxml.Privilege(davxml.Read())),
+                    davxml.Grant(*privileges),
                     davxml.Protected(),
                     TwistedACLInheritable(),
                 ))
@@ -475,7 +499,7 @@
 
 
 
-class ProtoCalendarAttachment(_GetChildHelper, CalDAVResource):
+class ProtoCalendarAttachment(_GetChildHelper):
 
     def __init__(self, calendarObject, attachmentName, **kw):
         super(ProtoCalendarAttachment, self).__init__(**kw)
@@ -491,6 +515,12 @@
         return NO_CONTENT
 
 
+    # FIXME: Permissions should dictate a different response, sometimes.
+    def http_GET(self, request):
+        return NOT_FOUND
+
+
+    @requiresPermissions(fromParent=[davxml.Bind()])
     def http_PUT(self, request):
         # FIXME: MIME-Type from header
         # FIXME: direct test
@@ -501,15 +531,16 @@
         )
         def done(ignored):
             t.loseConnection()
-            return NO_CONTENT
+            return CREATED
         return readStream(request.stream, t.write).addCallback(done)
 
 
 
 class CalendarAttachment(_GetChildHelper):
 
-    def __init__(self, attachment, **kw):
+    def __init__(self, calendarObject, attachment, **kw):
         super(CalendarAttachment, self).__init__(**kw)
+        self._newStoreCalendarObject = calendarObject
         self._newStoreAttachment = attachment
 
 
@@ -529,7 +560,7 @@
         return None
 
 
-    # FIXME: @requiresPermissions(davxml.Write())
+    @requiresPermissions(davxml.WriteContent())
     def http_PUT(self, request):
         # FIXME: direct test
         # FIXME: MIME-Type from header
@@ -554,9 +585,14 @@
         return Response(OK, None, stream)
 
 
+    @requiresPermissions(fromParent=[davxml.Unbind()])
     def http_DELETE(self, request):
-        # FIXME: implement
-        raise NotImplementedError()
+        self._newStoreCalendarObject.removeAttachmentWithName(
+            self._newStoreAttachment.name()
+        )
+        del self._newStoreCalendarObject
+        self.__class__ = ProtoCalendarAttachment
+        return NO_CONTENT
 
 
     def isCollection(self):
@@ -589,21 +625,13 @@
         return True
 
 
-    # FIXME: @requiresPermissions(fromParent=[Bind()])
+    @requiresPermissions(fromParent=[davxml.Unbind()])
     @inlineCallbacks
     def http_DELETE(self, request):
         """
         Override http_DELETE to validate 'depth' header. 
         """
 
-        #
-        # Check authentication and access controls
-        #
-        parentURL = parentForURL(request.uri)
-        parent = (yield request.locateResource(parentURL))
-
-        yield parent.authorize(request, (davxml.Unbind(),))
-
         depth = request.headers.getHeader("depth", "infinity")
         if depth != "infinity":
             msg = "illegal depth header for DELETE on collection: %s" % (
@@ -719,6 +747,7 @@
         return FORBIDDEN
 
 
+    # FIXME: access control
     @inlineCallbacks
     def http_MOVE(self, request):
         """
@@ -881,9 +910,11 @@
         # FIXME: Tests
         return True
 
+
     def name(self):
         return self._newStoreObject.name()
 
+
     def etag(self):
         # FIXME: far too slow to be used for real, but I needed something to
         # placate the etag computation in the case where the file doesn't exist
@@ -919,24 +950,14 @@
         return self._newStoreObject.iCalendarText()
 
 
-    @inlineCallbacks
+    @requiresPermissions(fromParent=[davxml.Unbind()])
     def http_DELETE(self, request):
         """
         Override http_DELETE to validate 'depth' header. 
         """
+        return self.storeRemove(request, True, request.uri)
 
-        #
-        # Check authentication and access controls
-        #
-        parentURL = parentForURL(request.uri)
-        parent = (yield request.locateResource(parentURL))
 
-        yield parent.authorize(request, (davxml.Unbind(),))
-
-        response = (yield self.storeRemove(request, True, request.uri))
-        returnValue(response)
-
-
     @inlineCallbacks
     def storeStream(self, stream):
         # FIXME: direct tests
@@ -1221,6 +1242,7 @@
     def isCollection(self):
         return True
 
+
     def isAddressBookCollection(self):
         """
         Yes, it is a calendar collection.
@@ -1228,20 +1250,12 @@
         return True
 
 
+    @requiresPermissions(fromParent=[davxml.Unbind()])
     @inlineCallbacks
     def http_DELETE(self, request):
         """
         Override http_DELETE to validate 'depth' header. 
         """
-
-        #
-        # Check authentication and access controls
-        #
-        parentURL = parentForURL(request.uri)
-        parent = (yield request.locateResource(parentURL))
-
-        yield parent.authorize(request, (davxml.Unbind(),))
-
         depth = request.headers.getHeader("depth", "infinity")
         if depth != "infinity":
             msg = "illegal depth header for DELETE on collection: %s" % (
@@ -1339,6 +1353,7 @@
         return FORBIDDEN
 
 
+    # FIXME: access control
     @inlineCallbacks
     def http_MOVE(self, request):
         """
@@ -1362,6 +1377,7 @@
         returnValue(NO_CONTENT)
 
 
+
 class ProtoAddressBookCollectionFile(CalDAVFile):
     """
     A resource representing an addressbook collection which hasn't yet been created.
@@ -1384,6 +1400,7 @@
     def isCollection(self):
         return True
 
+
     def createSimilarFile(self, path):
         # FIXME: this is necessary for 
         # twistedcaldav.test.test_mkcol.
@@ -1480,6 +1497,7 @@
     def isCollection(self):
         return True
 
+
     def createSimilarFile(self, path):
         # FIXME: this is necessary for 
         # twistedcaldav.test.test_mkcol.
@@ -1621,24 +1639,14 @@
         return self._newStoreObject.vCardText()
 
 
-    @inlineCallbacks
+    @requiresPermissions(fromParent=[davxml.Unbind()])
     def http_DELETE(self, request):
         """
         Override http_DELETE to validate 'depth' header. 
         """
+        return self.storeRemove(request, request.uri)
 
-        #
-        # Check authentication and access controls
-        #
-        parentURL = parentForURL(request.uri)
-        parent = (yield request.locateResource(parentURL))
 
-        yield parent.authorize(request, (davxml.Unbind(),))
-
-        response = (yield self.storeRemove(request, request.uri))
-        returnValue(response)
-
-
     @inlineCallbacks
     def storeStream(self, stream):
         # FIXME: direct tests
@@ -1746,10 +1754,12 @@
         """
         Initialize with a notification collection.
 
-        @param notifications: the wrapped http://daboo.name/groups/daboofamily/wiki/c4c42/6585_Sale.html.
+        @param notifications: the wrapped notification collection backend
+            object.
         @type notifications: L{txdav.common.inotification.INotificationCollection}
 
-        @param home: the home through which the given notification collection was accessed.
+        @param home: the home through which the given notification collection
+            was accessed.
         @type home: L{txdav.icommonstore.ICommonHome}
         """
         self._newStoreNotifications = notifications
@@ -1812,7 +1822,8 @@
 
 
 
-class StoreNotificationCollectionFile(_NotificationChildHelper, NotificationCollectionFile):
+class StoreNotificationCollectionFile(_NotificationChildHelper,
+                                      NotificationCollectionFile):
     """
     Wrapper around a L{txcaldav.icalendar.ICalendar}.
     """
@@ -1829,12 +1840,12 @@
     def isCollection(self):
         return True
 
+
     @inlineCallbacks
     def http_DELETE(self, request):
         """
         Override http_DELETE to reject. 
         """
-
         raise HTTPError(StatusResponse(FORBIDDEN, "Cannot delete notification collections"))
 
 
@@ -1853,6 +1864,8 @@
         """
         raise HTTPError(StatusResponse(FORBIDDEN, "Cannot move notification collections"))
 
+
+
 class StoreNotificationObjectFile(NotificationFile):
     """
     A resource wrapping a calendar object.
@@ -1913,23 +1926,14 @@
         return self._newStoreObject.xmldata()
 
 
-    @inlineCallbacks
+    @requiresPermissions(fromParent=[davxml.Unbind()])
     def http_DELETE(self, request):
         """
         Override http_DELETE to validate 'depth' header. 
         """
+        return self.storeRemove(request, request.uri)
 
-        #
-        # Check authentication and access controls
-        #
-        parentURL = parentForURL(request.uri)
-        parent = (yield request.locateResource(parentURL))
 
-        yield parent.authorize(request, (davxml.Unbind(),))
-
-        response = (yield self.storeRemove(request, request.uri))
-        returnValue(response)
-
     @inlineCallbacks
     def storeRemove(self, request, where):
         """
@@ -1965,6 +1969,7 @@
 
         returnValue(NO_CONTENT)
 
+
     def _initializeWithObject(self, notificationObject):
         self._newStoreObject = notificationObject
         self._dead_properties = _NewStorePropertiesWrapper(
@@ -1985,9 +1990,11 @@
         super(ProtoStoreNotificationObjectFile, self).__init__(*a, **kw)
         self._newStoreParentNotifications = parentNotifications
 
+
     def isCollection(self):
         return False
 
+
     def exists(self):
         # FIXME: tests
         return False
@@ -1997,3 +2004,5 @@
         # FIXME: tests, workingness
         return succeed(0)
 
+
+

Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/file.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/file.py	2010-07-14 11:44:03 UTC (rev 5894)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/file.py	2010-07-14 11:50:41 UTC (rev 5895)
@@ -336,6 +336,8 @@
         """
         # FIXME: rollback, tests for rollback
         self._dropboxPath().child(name).remove()
+        if name in self._attachments:
+            del self._attachments[name]
 
 
     def attachmentWithName(self, name):
@@ -352,7 +354,6 @@
         else:
             # FIXME: test for non-existent attachment.
             return None
-        # But, ahem.
 
 
     def _allSubcomponents(self, component):
@@ -362,17 +363,31 @@
                 yield deeper
 
 
-    def dropboxID(self):
+    def _anyProperty(self, name):
         component = self.component()
         for subcomp in self._allSubcomponents(component):
-            dropboxProperty = subcomp.getProperty("X-APPLE-DROPBOX")
+            dropboxProperty = subcomp.getProperty(name)
             if dropboxProperty is not None:
-                componentDropboxID = dropboxProperty.value().split("/")[-1]
-                return componentDropboxID
+                return dropboxProperty.value()
+        return None
+
+
+    def attendeesCanManageAttachments(self):
+        return bool(self._anyProperty("X-APPLE-DROPBOX"))
+
+
+    def dropboxID(self):
         # FIXME: direct tests
-
-        # FIXME: read ATTACH properties as well as X-APPLE-DROPBOX properties,
-        # since X-APPLE-DROPBOX only appears when others can write attachments.
+        dropboxProperty = self._anyProperty("X-APPLE-DROPBOX")
+        if dropboxProperty is not None:
+            componentDropboxID = dropboxProperty.split("/")[-1]
+            return componentDropboxID
+        attachProperty = self._anyProperty("ATTACH")
+        if attachProperty is not None:
+            # FIXME: more aggressive checking to see if this URI is really the
+            # 'right' URI.  Maybe needs to happen in the front end.
+            attachPath = attachProperty.split("/")[-2]
+            return attachPath
         return self.uid() + ".dropbox"
 
 
@@ -434,6 +449,7 @@
         # TwistedGETContentMD5.fromString(md5)
 
 
+
 contentTypeKey = PropertyName.fromString(GETContentType.sname())
 # md5key = PropertyName.fromString(TwistedGETContentMD5.sname())
 
@@ -508,6 +524,7 @@
     """
     Just enough resource to keep the calendar's sql DB classes going.
     """
+
     def __init__(self, calendar):
         self.calendar = calendar
         self.fp = self.calendar._path
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100714/89e59c0a/attachment-0001.html>


More information about the calendarserver-changes mailing list