[CalendarServer-changes] [958] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Mon Jan 8 08:48:39 PST 2007


Revision: 958
          http://trac.macosforge.org/projects/calendarserver/changeset/958
Author:   cdaboo at apple.com
Date:     2007-01-08 08:48:38 -0800 (Mon, 08 Jan 2007)

Log Message:
-----------
Merge of cuproxy-857 branch. This completes cuproxy work.

Modified Paths:
--------------
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/conf/caldavd.plist
    CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch
    CalendarServer/trunk/twistedcaldav/config.py
    CalendarServer/trunk/twistedcaldav/customxml.py
    CalendarServer/trunk/twistedcaldav/directory/calendar.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
    CalendarServer/trunk/twistedcaldav/extensions.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/root.py
    CalendarServer/trunk/twistedcaldav/schedule.py
    CalendarServer/trunk/twistedcaldav/static.py
    CalendarServer/trunk/twistedcaldav/test/data/makelargecalendars.py
    CalendarServer/trunk/twistedcaldav/test/test_root.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/trunk/twistedcaldav/test/data/makelargefbset.py

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2007-01-08 16:48:38 UTC (rev 958)
@@ -150,6 +150,9 @@
   <key>NotificationsEnabled</key>
   <true/>
 
+  <key>CalendarUserProxyEnabled</key>
+  <true/>
+
   <key>twistdLocation</key>
   <string>../Twisted/bin/twistd</string>
 
@@ -181,7 +184,7 @@
 
   <key>AdminPrincipals</key>
   <array>
-    <string>/principals/user/admin</string>
+    <string>/principals/user/admin/</string>
   </array>
 </dict>
 </plist>

Modified: CalendarServer/trunk/conf/caldavd.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd.plist	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/conf/caldavd.plist	2007-01-08 16:48:38 UTC (rev 958)
@@ -99,6 +99,9 @@
   <key>NotificationsEnabled</key>
   <true/>
 
+  <key>CalendarUserProxyEnabled</key>
+  <true/>
+
   <key>SACLEnable</key>
   <true/>
 
@@ -127,7 +130,7 @@
 
   <key>AdminPrincipals</key>
   <array>
-    <string>/principals/user/admin</string>
+    <string>/principals/user/admin/</string>
   </array>
 </dict>
 </plist>

Modified: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch	2007-01-08 16:48:38 UTC (rev 958)
@@ -823,7 +823,7 @@
      # HTTP
      ##
  
-@@ -1558,7 +1913,7 @@
+@@ -1558,10 +1913,10 @@
      """
      DAV resource with no children.
      """
@@ -831,8 +831,31 @@
 +    def findChildren(self, depth, request, callback, privileges=None, inherited_aces=None):
          return succeed(None)
  
- class DAVPrincipalResource (DAVLeafResource):
-@@ -1673,6 +2028,24 @@
+-class DAVPrincipalResource (DAVLeafResource):
++class DAVPrincipalResource (DAVResource):
+     """
+     Resource representing a WebDAV principal.  (RFC 3744, section 2)
+     """
+@@ -1571,7 +1926,7 @@
+     # WebDAV
+     ##
+ 
+-    liveProperties = DAVLeafResource.liveProperties + (
++    liveProperties = DAVResource.liveProperties + (
+         (dav_namespace, "alternate-URI-set"),
+         (dav_namespace, "principal-URL"    ),
+         (dav_namespace, "group-member-set" ),
+@@ -1584,9 +1939,6 @@
+     def isCollection(self):
+         return False
+ 
+-    def findChildren(self, depth, request, callback, privileges=None, inherited_aces=None):
+-        return succeed(None)
+-
+     def readProperty(self, property, request):
+         def defer():
+             if type(property) is tuple:
+@@ -1673,6 +2025,24 @@
          else:
              return uri in self.groupMembers()
  
@@ -857,7 +880,7 @@
  class AccessDeniedError(Exception):
      def __init__(self, errors):
          """ 
-@@ -1712,6 +2085,37 @@
+@@ -1712,6 +2082,37 @@
  davxml.registerElement(TwistedACLInheritable)
  davxml.ACE.allowed_children[(twisted_dav_namespace, "inheritable")] = (0, 1)
  

Modified: CalendarServer/trunk/twistedcaldav/config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/config.py	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/twistedcaldav/config.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -23,6 +23,7 @@
 defaultConfigFile = '/etc/caldavd/caldavd.plist'
 
 defaultConfig = {
+    'CalendarUserProxyEnabled': True,
     'DirectoryService': {
         'params': {'node': '/Search'},
         'type': 'twistedcaldav.directory.appleopendirectory.OpenDirectoryService'
@@ -60,7 +61,7 @@
             'ServicePrincipal': '',
             },
         },
-    'AdminPrincipals': ['/principals/user/admin']
+    'AdminPrincipals': ['/principals/user/admin/']
 }
 
 

Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/twistedcaldav/customxml.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -52,6 +52,22 @@
     def getValue(self):
         return str(self)
 
+class CalendarProxyRead (davxml.WebDAVEmptyElement):
+    """
+    A read-only calendar user proxy principal resource.
+    (Apple Extension to CalDAV)
+    """
+    namespace = calendarserver_namespace
+    name = "calendar-proxy-read"
+
+class CalendarProxyWrite (davxml.WebDAVEmptyElement):
+    """
+    A read-write calendar user proxy principal resource.
+    (Apple Extension to CalDAV)
+    """
+    namespace = calendarserver_namespace
+    name = "calendar-proxy-write"
+
 class TwistedCalendarPrincipalURI(davxml.WebDAVTextElement):
     """
     Contains the calendarPrincipalURI value for a directory record corresponding to a principal.
@@ -199,3 +215,5 @@
 davxml.ResourceType.dropboxhome = davxml.ResourceType(davxml.Collection(), DropBoxHome())
 davxml.ResourceType.dropbox = davxml.ResourceType(davxml.Collection(), DropBox())
 davxml.ResourceType.notifications = davxml.ResourceType(davxml.Collection(), Notifications())
+davxml.ResourceType.calendarproxyread = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyRead())
+davxml.ResourceType.calendarproxywrite = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyWrite())

Modified: CalendarServer/trunk/twistedcaldav/directory/calendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendar.py	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/twistedcaldav/directory/calendar.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -81,6 +81,11 @@
         # See DirectoryPrincipalProvisioningResource.__init__()
         return self.directory.principalCollection.principalCollections()
 
+    def principalForRecord(self, record):
+        # FIXME: directory.principalCollection smells like a hack
+        # See DirectoryPrincipalProvisioningResource.__init__()
+        return self.directory.principalCollection.principalForRecord(record)
+
     def homeForDirectoryRecord(self, record):
         typeResource = self.getChild(record.recordType)
         if typeResource is None:
@@ -161,6 +166,10 @@
     def principalCollections(self):
         return self._parent.principalCollections()
 
+    def principalForRecord(self, record):
+        return self._parent.principalForRecord(record)
+
+
 class DirectoryCalendarHomeResource (AutoProvisioningResourceMixIn, CalDAVResource):
     """
     Calendar home collection resource.
@@ -254,9 +263,9 @@
     def defaultAccessControlList(self):
         # FIXME: directory.principalCollection smells like a hack
         # See DirectoryPrincipalProvisioningResource.__init__()
-        myPrincipal = self._parent._parent.directory.principalCollection.principalForRecord(self.record)
+        myPrincipal = self.principalForRecord()
 
-        return davxml.ACL(
+        aces = (
             # DAV:read access for authenticated users.
             davxml.ACE(
                 davxml.Principal(davxml.Authenticated()),
@@ -270,10 +279,33 @@
                 TwistedACLInheritable(),
             ),
         )
+        
+        if config.CalendarUserProxyEnabled:
+            aces += (
+                # DAV:read access for this principal's calendar-proxy-read users.
+                davxml.ACE(
+                    davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-read/"))),
+                    davxml.Grant(davxml.Privilege(davxml.Read())),
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
+                ),
+                # DAV:read/DAV:write access for this principal's calendar-proxy-write users.
+                davxml.ACE(
+                    davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-write/"))),
+                    davxml.Grant(davxml.Privilege(davxml.Read()), davxml.Privilege(davxml.Write())),
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
+                ),
+            )
 
+        return davxml.ACL(*aces)
+
     def principalCollections(self):
         return self._parent.principalCollections()
 
+    def principalForRecord(self):
+        return self._parent.principalForRecord(self.record)
+
     ##
     # Quota
     ##

Copied: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py (from rev 954, CalendarServer/branches/users/cdaboo/cuproxy-857/twistedcaldav/directory/calendaruserproxy.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -0,0 +1,350 @@
+##
+# Copyright (c) 2006 Apple Computer, 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.
+#
+# DRI: Wilfredo Sanchez, wsanchez at apple.com
+##
+
+"""
+Implements a calendar user proxy principal.
+"""
+
+__all__ = [
+    "CalendarUserProxyPrincipalResource",
+]
+
+from urllib import unquote
+
+from twisted.internet.defer import succeed
+from twisted.python import log
+from twisted.python.failure import Failure
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.element.base import dav_namespace
+from twisted.web2.dav.util import joinURL
+from twisted.web2.http import Response
+from twisted.web2.http_headers import MimeType
+
+from twistedcaldav.extensions import DAVFile
+from twistedcaldav.extensions import ReadOnlyWritePropertiesResourceMixIn
+from twistedcaldav.resource import CalendarPrincipalResource
+from twistedcaldav.sql import AbstractSQLDatabase
+from twistedcaldav.static import AutoProvisioningFileMixIn
+
+import os
+
+class PermissionsMixIn (ReadOnlyWritePropertiesResourceMixIn):
+    def defaultAccessControlList(self):
+        aces = (
+            # DAV:read access for authenticated users.
+            davxml.ACE(
+                davxml.Principal(davxml.Authenticated()),
+                davxml.Grant(davxml.Privilege(davxml.Read())),
+            ),
+            # Inheritable DAV:all access for the resource's associated principal.
+            davxml.ACE(
+                davxml.Principal(davxml.HRef(self.parent.principalURL())),
+                davxml.Grant(davxml.Privilege(davxml.WriteProperties())),
+                davxml.Protected(),
+            ),
+        )
+        
+        return davxml.ACL(*aces)
+
+    def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
+        # Permissions here are fixed, and are not subject to inherritance rules, etc.
+        return succeed(self.defaultAccessControlList())
+
+class CalendarUserProxyPrincipalResource (AutoProvisioningFileMixIn, PermissionsMixIn, CalendarPrincipalResource, DAVFile):
+    """
+    Calendar user proxy principal resource.
+    """
+    def __init__(self, path, parent, proxyType):
+        """
+        @param path: the path to the file which will back this resource.
+        @param parent: the parent of this resource.
+        @param proxyType: a C{str} containing the name of the resource.
+        """
+        super(CalendarUserProxyPrincipalResource, self).__init__(path, joinURL(parent.principalURL(), proxyType))
+
+        self.parent = parent
+        self.proxyType = proxyType
+        self._url = joinURL(parent.principalURL(), proxyType)
+        if self.isCollection():
+            self._url += "/"
+
+        # Provision in __init__() because principals are used prior to request
+        # lookups.
+        self.provision()
+
+    def _index(self):
+        """
+        Return the SQL database for this group principal.
+        
+        @return: the L{CalendarUserProxyDatabase} for the principal collection.
+        """
+        
+        # Get the principal collection we are contained in
+        pcollection = self.parent.parent.parent
+        
+        # The db is located in the principal collection root
+        if not hasattr(pcollection, "calendar_user_proxy_db"):
+            setattr(pcollection, "calendar_user_proxy_db", CalendarUserProxyDatabase(pcollection.fp.path))
+        return pcollection.calendar_user_proxy_db
+
+    def resourceType(self):
+        if self.proxyType == "calendar-proxy-read":
+            return davxml.ResourceType.calendarproxyread
+        elif self.proxyType == "calendar-proxy-write":
+            return davxml.ResourceType.calendarproxywrite
+        else:
+            return super(CalendarUserProxyPrincipalResource, self).resourceType()
+
+    def writeProperty(self, property, request):
+        assert isinstance(property, davxml.WebDAVElement)
+
+        if property.qname() == (dav_namespace, "group-member-set"):
+            return self.setGroupMemberSet(property, request)
+
+        return super(CalendarUserProxyPrincipalResource, self).writeProperty(property, request)
+
+    def setGroupMemberSet(self, new_members, request):
+        
+        # Break out the list into a set of URIs.
+        members = [str(h) for h in new_members.children]
+        self._index().setGroupMembers(self._url, members)
+        return succeed(True)
+
+    ##
+    # HTTP
+    ##
+
+    def render(self, request):
+        def format_list(method, *args):
+            def genlist():
+                try:
+                    item = None
+                    for item in method(*args):
+                        yield " -> %s\n" % (item,)
+                    if item is None:
+                        yield " '()\n"
+                except Exception, e:
+                    log.err("Exception while rendering: %s" % (e,))
+                    Failure().printTraceback()
+                    yield "  ** %s **: %s\n" % (e.__class__.__name__, e)
+            return "".join(genlist())
+
+        output = [
+            """<html>"""
+            """<head>"""
+            """<title>%(title)s</title>"""
+            """<style>%(style)s</style>"""
+            """</head>"""
+            """<body>"""
+            """<div class="directory-listing">"""
+            """<h1>Proxy Principal Details</h1>"""
+            """<pre><blockquote>"""
+            % {
+                "title": unquote(request.uri),
+                "style": self.directoryStyleSheet(),
+            }
+        ]
+
+        output.append("".join((
+            "Directory Information\n"
+            "---------------------\n"
+            "Parent Directory GUID: %s\n"  % (self.parent.record.service.guid,),
+            "Realm: %s\n"                  % (self.parent.record.service.realmName,),
+            "\n"
+            "Parent Principal Information\n"
+            "---------------------\n"
+            "GUID: %s\n"                   % (self.parent.record.guid,),
+            "Record type: %s\n"            % (self.parent.record.recordType,),
+            "Short name: %s\n"             % (self.parent.record.shortName,),
+            "Full name: %s\n"              % (self.parent.record.fullName,),
+            "\n"
+            "Proxy Principal Information\n"
+            "---------------------\n"
+            "Principal URL: %s\n"          % (self.principalURL(),),
+            "\nAlternate URIs:\n"          , format_list(self.alternateURIs),
+            "\nGroup members:\n"           , format_list(self.groupMembers),
+        )))
+
+        output.append(
+            """</pre></blockquote></div>"""
+        )
+
+        output.append(self.getDirectoryTable("Collection Listing"))
+
+        output.append("</body></html>")
+
+        output = "".join(output)
+        if type(output) == unicode:
+            output = output.encode("utf-8")
+            mime_params = {"charset": "utf-8"}
+        else:
+            mime_params = {}
+
+        response = Response(code=responsecode.OK, stream=output)
+        response.headers.setHeader("content-type", MimeType("text", "html", mime_params))
+
+        return response
+
+    ##
+    # DAV
+    ##
+
+    def displayName(self):
+        return self.proxyType
+
+    ##
+    # ACL
+    ##
+
+    def alternateURIs(self):
+        # FIXME: Add API to IDirectoryRecord for getting a record URI?
+        return ()
+
+    def principalURL(self):
+        return self._url
+
+    def principalCollections(self):
+        return self.parent.principalCollections()
+
+    def groupMembers(self):
+        return self._index().getMembers(self._url)
+
+    def groupMemberships(self):
+        return self._index().getMemberships(self._url)
+
+
+class CalendarUserProxyDatabase(AbstractSQLDatabase):
+    """
+    A database to maintain calendar user proxy group memberships.
+
+    SCHEMA:
+    
+    Group Database:
+    
+    ROW: GROUPNAME, MEMBER
+    
+    """
+    
+    dbType = "CALENDARUSERPROXY"
+    dbFilename = ".db.calendaruserproxy"
+    dbFormatVersion = "1"
+
+    def __init__(self, path):
+        path = os.path.join(path, CalendarUserProxyDatabase.dbFilename)
+        super(CalendarUserProxyDatabase, self).__init__(path, CalendarUserProxyDatabase.dbFormatVersion)
+
+    def setGroupMembers(self, principalURI, members):
+        """
+        Add a group membership record.
+    
+        @param principalURI: the principalURI of the group principal to add.
+        @param members: the list of principalURIs that are members of this group.
+        """
+        
+        # Remove what is there, then add it back.
+        self._delete_from_db(principalURI)
+        self._add_to_db(principalURI, members)
+        self._db_commit()
+
+    def removeGroup(self, principalURI):
+        """
+        Remove a group membership record.
+    
+        @param principalURI: the principalURI of the group principal to add.
+        """
+        self._delete_from_db(principalURI)
+        self._db_commit()
+    
+    def getMembers(self, principalURI):
+        """
+        Return the list of group members for the specified principal.
+        """
+        members = set()
+        for row in self._db_execute("select MEMBER from GROUPS where GROUPNAME = :1", principalURI):
+            members.add(row[0])
+        return members
+    
+    def getMemberships(self, principalURI):
+        """
+        Return the list of groups the specified principal is a member of.
+        """
+        members = set()
+        for row in self._db_execute("select GROUPNAME from GROUPS where MEMBER = :1", principalURI):
+            members.add(row[0])
+        return members
+
+    def _add_to_db(self, principalURI, members):
+        """
+        Insert the specified entry into the database.
+
+        @param principalURI: the principalURI of the group principal to remove.
+        @param members: the list of principalURIs that are members of this group.
+        """
+        for member in members:
+            self._db_execute(
+                """
+                insert into GROUPS (GROUPNAME, MEMBER)
+                values (:1, :2)
+                """, principalURI, member
+            )
+       
+    def _delete_from_db(self, principalURI):
+        """
+        Deletes the specified entry from the database.
+
+        @param principalURI: the principalURI of the group principal to remove.
+        """
+        self._db_execute("delete from GROUPS where GROUPNAME = :1", principalURI)
+    
+    def _db_type(self):
+        """
+        @return: the collection type assigned to this index.
+        """
+        return CalendarUserProxyDatabase.dbType
+        
+    def _db_init_data_tables(self, q):
+        """
+        Initialise the underlying database tables.
+        @param q:           a database cursor to use.
+        """
+
+        #
+        # GROUPS table
+        #
+        q.execute(
+            """
+            create table GROUPS (
+                GROUPNAME   text,
+                MEMBER      text
+            )
+            """
+        )
+
+##
+# Utilities
+##
+
+authReadACL = davxml.ACL(
+    # Read access for authenticated users.
+    davxml.ACE(
+        davxml.Principal(davxml.Authenticated()),
+        davxml.Grant(davxml.Privilege(davxml.Read())),
+        davxml.Protected(),
+    ),
+)

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -37,6 +37,9 @@
 from twisted.web2.dav import davxml
 from twisted.web2.dav.util import joinURL
 
+from twistedcaldav.config import config
+from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
+from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyPrincipalResource
 from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVFile
 from twistedcaldav.resource import CalendarPrincipalCollectionResource, CalendarPrincipalResource
 from twistedcaldav.static import AutoProvisioningFileMixIn
@@ -105,7 +108,7 @@
             path = None
         
         if path:
-            segments = [unquote(s) for s in path.split("/")]
+            segments = [unquote(s) for s in path.rstrip("/").split("/")]
             if segments[0] == "" and len(segments) == 3:
                 typeResource = self.getChild(segments[1])
                 if typeResource is not None:
@@ -228,6 +231,8 @@
         self.record = record
         self.parent = parent
         self._url = joinURL(parent.principalCollectionURL(), record.shortName)
+        if self.isCollection():
+            self._url += "/"
 
         # Provision in __init__() because principals are used prior to request
         # lookups.
@@ -252,7 +257,23 @@
                     yield "  ** %s **: %s\n" % (e.__class__.__name__, e)
             return "".join(genlist())
 
-        output = ("".join((
+        output = [
+            """<html>"""
+            """<head>"""
+            """<title>%(title)s</title>"""
+            """<style>%(style)s</style>"""
+            """</head>"""
+            """<body>"""
+            """<div class="directory-listing">"""
+            """<h1>Principal Details</h1>"""
+            """<pre><blockquote>"""
+            % {
+                "title": unquote(request.uri),
+                "style": self.directoryStyleSheet(),
+            }
+        ]
+
+        output.append("".join((
             "Directory Information\n"
             "---------------------\n"
             "Directory GUID: %s\n"         % (self.record.service.guid,),
@@ -273,6 +294,15 @@
             "\nCalendar user addresses:\n" , format_list(self.calendarUserAddresses),
         )))
 
+        output.append(
+            """</pre></blockquote></div>"""
+        )
+
+        output.append(self.getDirectoryTable("Collection Listing"))
+
+        output.append("</body></html>")
+
+        output = "".join(output)
         if type(output) == unicode:
             output = output.encode("utf-8")
             mime_params = {"charset": "utf-8"}
@@ -280,7 +310,7 @@
             mime_params = {}
 
         response = Response(code=responsecode.OK, stream=output)
-        response.headers.setHeader("content-type", MimeType("text", "plain", mime_params))
+        response.headers.setHeader("content-type", MimeType("text", "html", mime_params))
 
         return response
 
@@ -294,9 +324,6 @@
         else:
             return self.record.shortName
 
-    def isCollection(self):
-        return False
-
     ##
     # ACL
     ##
@@ -329,11 +356,29 @@
 
         return relatives
 
+    def _calendar_user_proxy_index(self):
+        """
+        Return the SQL database for calendar user proxies.
+        
+        @return: the L{CalendarUserProxyDatabase} for the principal collection.
+        """
+        
+        # Get the principal collection we are contained in
+        pcollection = self.parent.parent
+        
+        # The db is located in the principal collection root
+        if not hasattr(pcollection, "calendar_user_proxy_db"):
+            setattr(pcollection, "calendar_user_proxy_db", CalendarUserProxyDatabase(pcollection.fp.path))
+        return pcollection.calendar_user_proxy_db
+
     def groupMembers(self):
         return self._getRelatives("members")
 
     def groupMemberships(self):
-        return self._getRelatives("groups")
+        groups = self._getRelatives("groups")
+        if config.CalendarUserProxyEnabled:
+            groups.update(self._calendar_user_proxy_index().getMemberships(self._url))
+        return groups
 
     def principalCollections(self):
         return self.parent.principalCollections()
@@ -396,6 +441,29 @@
         else:
             return None
 
+    ##
+    # Static
+    ##
+
+    def createSimilarFile(self, path):
+        log.err("Attempt to create clone %r of resource %r" % (path, self))
+        raise HTTPError(responsecode.NOT_FOUND)
+
+    def getChild(self, name, record=None):
+        if name == "":
+            return self
+
+        if config.CalendarUserProxyEnabled and name in ("calendar-proxy-read", "calendar-proxy-write"):
+            return CalendarUserProxyPrincipalResource(self.fp.child(name).path, self, name)
+        else:
+            return None
+
+    def listChildren(self):
+        if config.CalendarUserProxyEnabled:
+            return ("calendar-proxy-read", "calendar-proxy-write")
+        else:
+            return ()
+
 ##
 # Utilities
 ##

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -118,7 +118,7 @@
                     recordResource = typeResource.getChild(shortName)
                     self.failUnless(isinstance(recordResource, DirectoryPrincipalResource))
 
-                    recordURL = typeURL + shortName
+                    recordURL = typeURL + shortName + "/"
                     self.assertEquals(recordURL, recordResource.principalURL())
 
                     principalCollections = recordResource.principalCollections()

Modified: CalendarServer/trunk/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/extensions.py	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/twistedcaldav/extensions.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -40,12 +40,37 @@
 from twisted.web2.dav.http import StatusResponse
 from twisted.web2.dav.static import DAVFile as SuperDAVFile
 from twisted.web2.dav.resource import DAVResource as SuperDAVResource
+from twisted.web2.dav.resource import DAVPrincipalResource as SuperDAVPrincipalResource
 
 class DAVResource (SuperDAVResource):
     """
     Extended L{twisted.web2.dav.resource.DAVResource} implementation.
     """
 
+class DAVPrincipalResource (SuperDAVPrincipalResource):
+    """
+    Extended L{twisted.web2.dav.static.DAVFile} implementation.
+    """
+    def readProperty(self, property, request):
+        if type(property) is tuple:
+            qname = property
+        else:
+            qname = property.qname()
+
+        if qname == (dav_namespace, "resourcetype"):
+            return succeed(self.resourceType())
+
+        return super(DAVPrincipalResource, self).readProperty(property, request)
+
+    def resourceType(self):
+        # Allow live property to be overriden by dead property
+        if self.deadProperties().contains((dav_namespace, "resourcetype")):
+            return self.deadProperties().get((dav_namespace, "resourcetype"))
+        if self.isCollection():
+            return davxml.ResourceType(davxml.Collection(), davxml.Principal())
+        else:
+            return davxml.ResourceType(davxml.Principal())
+
 class DAVFile (SuperDAVFile):
     """
     Extended L{twisted.web2.dav.static.DAVFile} implementation.
@@ -70,7 +95,6 @@
         return davxml.ResourceType.empty
 
     def render(self, req):
-        """You know what you doing."""
         if not self.fp.exists():
             return responsecode.NOT_FOUND
 
@@ -131,7 +155,7 @@
         """
         Render a directory listing.
         """
-        title = "Directory listing for %s" % urllib.unquote(request.path)
+        pagetitle = "Directory listing for %s" % urllib.unquote(request.path)
     
         output = [
             """<html>"""
@@ -140,51 +164,41 @@
             """<style>%(style)s</style>"""
             """</head>"""
             """<body>"""
+            % {
+                "title": pagetitle,
+                "style": self.directoryStyleSheet(),
+            }
+        ]
+
+        output.append(self.getDirectoryTable(urllib.unquote(request.uri)))
+
+        output.append("</body></html>")
+
+        response = Response(200, {}, "".join(output))
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response
+
+    def getDirectoryTable(self, title):
+        """
+        Generate a directory listing table in HTML.
+        """
+    
+        output = [
             """<div class="directory-listing">"""
             """<h1>%(title)s</h1>"""
             """<table>"""
             """<tr><th>Name</th> <th>Size</th> <th>Last Modified</th> <th>MIME Type</th></tr>"""
             % {
-                "title": urllib.unquote(request.uri),
-                "style": self.directoryStyleSheet(),
+                "title": title,
             }
         ]
 
-        def orNone(value, default="?", f=None):
-            if value is None:
-                return default
-            elif f is not None:
-                return f(value)
-            else:
-                return value
-
         even = False
         for name in sorted(self.listChildren()):
             child = self.getChild(name)
 
-            url = urllib.quote(name, '/')
-            if isinstance(child, SuperDAVFile) and child.isCollection():
-                url += "/"
-                name += "/"
+            url, name, size, lastModified, contentType = self.getChildDirectoryEntry(child, name)
 
-            if isinstance(child, MetaDataMixin):
-                size = child.contentLength()
-                lastModified = child.lastModified()
-                contentType = child.contentType()
-            else:
-                size = None
-                lastModified = None
-                contentType = None
-
-            if self.fp.isdir():
-                contentType = "(collection)"
-            else:
-                contentType = orNone(
-                    contentType,
-                    default="-",
-                    f=lambda m: "%s/%s %s" % (m.mediaType, m.mediaSubtype, m.params)
-                )
-
             # FIXME: gray out resources that are not readable
             output.append(
                 """<tr class="%(even)s">"""
@@ -197,25 +211,64 @@
                     "even": even and "even" or "odd",
                     "url": url,
                     "name": name,
-                    "size": orNone(size),
-                    "lastModified": orNone(
-                        lastModified,
-                        default="",
-                        f=lambda t: time.strftime("%Y-%b-%d %H:%M", time.localtime(t))
-                    ),
+                    "size": size,
+                    "lastModified": lastModified,
                     "type": contentType,
                 }
             )
             even = not even
-        output.append("</table></div></body></html>")
+        output.append("</table></div>")
 
-        response = Response(200, {}, "".join(output))
-        response.headers.setHeader("content-type", MimeType("text", "html"))
-        return response
+        return "".join(output)
 
-class ReadOnlyResourceMixIn (object):
+    def getChildDirectoryEntry(self, child, name):
+
+        def orNone(value, default="?", f=None):
+            if value is None:
+                return default
+            elif f is not None:
+                return f(value)
+            else:
+                return value
+            
+        url = urllib.quote(name, '/')
+        if isinstance(child, SuperDAVFile) and child.isCollection():
+            url += "/"
+            name += "/"
+
+        if isinstance(child, MetaDataMixin):
+            size = child.contentLength()
+            lastModified = child.lastModified()
+            contentType = child.contentType()
+        else:
+            size = None
+            lastModified = None
+            contentType = None
+
+        if self.fp.isdir():
+            contentType = "(collection)"
+        else:
+            contentType = self._orNone(
+                contentType,
+                default="-",
+                f=lambda m: "%s/%s %s" % (m.mediaType, m.mediaSubtype, m.params)
+            )
+
+        return (
+            url,
+            name,
+            orNone(size),
+            orNone(
+                lastModified,
+                default="",
+                f=lambda t: time.strftime("%Y-%b-%d %H:%M", time.localtime(t))
+             ),
+             contentType,
+         )
+
+class ReadOnlyWritePropertiesResourceMixIn (object):
     """
-    Read only resource.
+    Read only that will allow writing of properties resource.
     """
     readOnlyResponse = StatusResponse(
         responsecode.FORBIDDEN,
@@ -225,10 +278,15 @@
     def _forbidden(self, request):
         return self.readOnlyResponse
 
-    http_DELETE    = _forbidden
-    http_MOVE      = _forbidden
-    http_PROPPATCH = _forbidden
-    http_PUT       = _forbidden
+    http_DELETE = _forbidden
+    http_MOVE   = _forbidden
+    http_PUT    = _forbidden
 
+class ReadOnlyResourceMixIn (ReadOnlyWritePropertiesResourceMixIn):
+    """
+    Read only resource.
+    """
+    http_PROPPATCH = ReadOnlyWritePropertiesResourceMixIn._forbidden
+
     def writeProperty(self, property, request):
         raise HTTPError(self.readOnlyResponse)

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -24,7 +24,6 @@
     "CalDAVResource",
     "CalendarPrincipalCollectionResource",
     "CalendarPrincipalResource",
-    "CalendarSchedulingCollectionResource",
     "isCalendarCollectionResource",
     "isPseudoCalendarCollectionResource",
 ]
@@ -38,7 +37,7 @@
 from twisted.web2 import responsecode
 from twisted.web2.dav import davxml
 from twisted.web2.dav.idav import IDAVPrincipalCollectionResource
-from twisted.web2.dav.resource import AccessDeniedError, DAVPrincipalResource, DAVPrincipalCollectionResource
+from twisted.web2.dav.resource import AccessDeniedError, DAVPrincipalCollectionResource
 from twisted.web2.dav.davxml import dav_namespace
 from twisted.web2.dav.http import ErrorResponse
 from twisted.web2.dav.resource import TwistedACLInheritable
@@ -51,7 +50,7 @@
 
 import twistedcaldav
 from twistedcaldav import caldavxml, customxml
-from twistedcaldav.extensions import DAVResource
+from twistedcaldav.extensions import DAVResource, DAVPrincipalResource
 from twistedcaldav.icaldav import ICalDAVResource, ICalendarPrincipalResource
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.customxml import calendarserver_namespace
@@ -551,14 +550,15 @@
         (calendarserver_namespace, "notifications-URL"),
     )
 
+    def isCollection(self):
+        return True
+
     def readProperty(self, property, request):
         def defer():
             if type(property) is tuple:
                 qname = property
-                sname = "{%s}%s" % property
             else:
                 qname = property.qname()
-                sname = property.sname()
 
             namespace, name = qname
 
@@ -607,6 +607,12 @@
 
         return maybeDeferred(defer)
 
+    def groupMembers(self):
+        return ()
+
+    def groupMemberships(self):
+        return ()
+
     def calendarHomeURLs(self):
         if self.hasDeadProperty((caldav_namespace, "calendar-home-set")):
             home_set = self.readDeadProperty((caldav_namespace, "calendar-home-set"))

Modified: CalendarServer/trunk/twistedcaldav/root.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/root.py	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/twistedcaldav/root.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -69,7 +69,7 @@
         def _checkSACLCb((authnUser, authzUser)):
             # Figure out the "username" from the davxml.Principal object
             username = authzUser.children[0].children[0].data
-            username = username.split('/')[-1]
+            username = username.rstrip('/').split('/')[-1]
             
             if RootResource.CheckSACL(username, self.saclService) != 0:
                 return Failure(HTTPError(403))

Modified: CalendarServer/trunk/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/schedule.py	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/twistedcaldav/schedule.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -41,6 +41,7 @@
 from twistedcaldav import itip
 from twistedcaldav.resource import CalDAVResource
 from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.config import config
 from twistedcaldav.ical import Component
 from twistedcaldav.method import report_common
 from twistedcaldav.method.put_common import storeCalendarObjectResource
@@ -56,6 +57,16 @@
     Extends L{DAVResource} to provide CalDAV scheduling collection
     functionality.
     """
+    def __init__(self, parent):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
+
+        CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+        self.parent = parent
+
     def isCollection(self):
         return True
 
@@ -98,6 +109,23 @@
 
     Extends L{DAVResource} to provide CalDAV functionality.
     """
+
+    def defaultAccessControlList(self):
+        if config.CalendarUserProxyEnabled:
+            myPrincipal = self.parent.principalForRecord()
+    
+            return davxml.ACL(
+                # CalDAV:schedule for associated write proxies
+                davxml.ACE(
+                    davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-write"))),
+                    davxml.Grant(
+                        davxml.Privilege(caldavxml.Schedule()),
+                    ),
+                ),
+            )
+        else:
+            return super(ScheduleOutboxResource, self).defaultAccessControlList()
+
     def resourceType(self):
         return davxml.ResourceType.scheduleOutbox
 

Modified: CalendarServer/trunk/twistedcaldav/static.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/static.py	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/twistedcaldav/static.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -496,7 +496,6 @@
 class ScheduleFile (AutoProvisioningFileMixIn, CalDAVFile):
     def __init__(self, path, parent):
         super(ScheduleFile, self).__init__(path, principalCollections=parent.principalCollections())
-        self.parent = parent
 
     def isCollection(self):
         return True
@@ -533,6 +532,10 @@
     """
     Calendar scheduling inbox collection resource.
     """
+    def __init__(self, path, parent):
+        ScheduleFile.__init__(self, path, parent)
+        ScheduleInboxResource.__init__(self, parent)
+
     def provision(self):
         if self.provisionFile():
             # FIXME: This should probably be a directory record option that
@@ -551,6 +554,10 @@
     """
     Calendar scheduling outbox collection resource.
     """
+    def __init__(self, path, parent):
+        ScheduleFile.__init__(self, path, parent)
+        ScheduleOutboxResource.__init__(self, parent)
+
     def __repr__(self):
         return "<%s (calendar outbox collection): %s>" % (self.__class__.__name__, self.fp.path)
 

Modified: CalendarServer/trunk/twistedcaldav/test/data/makelargecalendars.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/data/makelargecalendars.py	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/twistedcaldav/test/data/makelargecalendars.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -20,7 +20,7 @@
 
 import os
 
-user_max = 20
+user_max = 99
 calendars = ("calendar.10", "calendar.100", "calendar.1000",)
 
 for ctr in xrange(1, user_max + 1): 

Copied: CalendarServer/trunk/twistedcaldav/test/data/makelargefbset.py (from rev 954, CalendarServer/branches/users/cdaboo/cuproxy-857/twistedcaldav/test/data/makelargefbset.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/data/makelargefbset.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/data/makelargefbset.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+
+##
+# Copyright (c) 2005-2006 Apple Computer, 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.
+#
+# DRI: Cyrus Daboo, cdaboo at apple.com
+##
+
+import os
+import xattr
+
+user_max = 99
+calendars = ("calendar.1000",)
+
+for ctr in xrange(1, user_max + 1): 
+    path = "calendars/user/user%02d" % (ctr,)
+
+    try: os.makedirs(path)
+    except OSError: pass
+
+    try: os.makedirs(os.path.join(path, "calendar"))
+    except OSError: pass
+
+    for calendar in calendars:
+        inboxname = os.path.join(path, "inbox")
+        attrs = xattr.xattr(inboxname)
+        attrs["WebDAV:{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set"] = """<?xml version='1.0' encoding='UTF-8'?>
+<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>
+  <href xmlns='DAV:'>/calendars/user/user%02d/calendar/</href>
+  <href xmlns='DAV:'>/calendars/user/user%02d/calendar.1000/</href>
+</calendar-free-busy-set>""" % (ctr, ctr,)


Property changes on: CalendarServer/trunk/twistedcaldav/test/data/makelargefbset.py
___________________________________________________________________
Name: svn:executable
   + *

Modified: CalendarServer/trunk/twistedcaldav/test/test_root.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_root.py	2007-01-08 16:43:05 UTC (rev 957)
+++ CalendarServer/trunk/twistedcaldav/test/test_root.py	2007-01-08 16:48:38 UTC (rev 958)
@@ -142,7 +142,7 @@
 
             self.assertEquals(request.authzUser, 
                               davxml.Principal(
-                    davxml.HRef('/principals/user/dreid')))
+                    davxml.HRef('/principals/user/dreid/')))
             
         d = defer.maybeDeferred(resrc.locateChild, request, ['principals'])
         d.addCallback(_Cb)

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20070108/1c02b388/attachment.html


More information about the calendarserver-changes mailing list