[CalendarServer-changes] [3164] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Thu Oct 16 19:19:00 PDT 2008


Revision: 3164
          http://trac.macosforge.org/projects/calendarserver/changeset/3164
Author:   cdaboo at apple.com
Date:     2008-10-16 19:18:59 -0700 (Thu, 16 Oct 2008)
Log Message:
-----------
Merge better-proxy branch to trunk.

Modified Paths:
--------------
    CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch
    CalendarServer/trunk/twistedcaldav/customxml.py
    CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryrecords.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py
    CalendarServer/trunk/twistedcaldav/extensions.py
    CalendarServer/trunk/twistedcaldav/resource.py

Added Paths:
-----------
    CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch

Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch (from rev 3163, CalendarServer/branches/users/cdaboo/better-proxy-3132/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch	                        (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch	2008-10-17 02:18:59 UTC (rev 3164)
@@ -0,0 +1,214 @@
+Index: twisted/web2/dav/method/report_expand.py
+===================================================================
+--- twisted/web2/dav/method/report_expand.py	(revision 19773)
++++ twisted/web2/dav/method/report_expand.py	(working copy)
+@@ -1,6 +1,6 @@
+ # -*- test-case-name: twisted.web2.dav.test.test_report_expand -*-
+ ##
+-# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
++# Copyright (c) 2005-2008 Apple Computer, Inc. All rights reserved.
+ #
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
+ # of this software and associated documentation files (the "Software"), to deal
+@@ -19,8 +19,6 @@
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ # SOFTWARE.
+-#
+-# DRI: Wilfredo Sanchez, wsanchez at apple.com
+ ##
+ 
+ """
+@@ -29,86 +27,143 @@
+ 
+ __all__ = ["report_DAV__expand_property"]
+ 
++from twisted.internet.defer import inlineCallbacks, returnValue
+ from twisted.python import log
+ from twisted.python.failure import Failure
+-from twisted.internet.defer import deferredGenerator, waitForDeferred
+ from twisted.web2 import responsecode
+ from twisted.web2.dav import davxml
+-from twisted.web2.dav.http import statusForFailure
+ from twisted.web2.dav.davxml import dav_namespace
++from twisted.web2.dav.http import statusForFailure, MultiStatusResponse
++from twisted.web2.dav.method import prop_common
++from twisted.web2.dav.method.propfind import propertyName
++from twisted.web2.dav.resource import AccessDeniedError
++from twisted.web2.dav.util import parentForURL
++from twisted.web2.http import HTTPError, StatusResponse
+ 
++ at inlineCallbacks
+ def report_DAV__expand_property(self, request, expand_property):
+     """
+     Generate an expand-property REPORT. (RFC 3253, section 3.8)
++    
++    TODO: for simplicity we will only support one level of expansion.
+     """
+-    # FIXME: Handle depth header
+-
++    # Verify root element
+     if not isinstance(expand_property, davxml.ExpandProperty):
+         raise ValueError("%s expected as root element, not %s."
+                          % (davxml.ExpandProperty.sname(), expand_property.sname()))
+ 
++    # Only handle Depth: 0
++    depth = request.headers.getHeader("depth", "0")
++    if depth != "0":
++        log.err("Non-zero depth is not allowed: %s" % (depth,))
++        raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,)))
++    
+     #
+-    # Expand DAV:allprop
++    # Get top level properties to expand and make sure we only have one level
+     #
+     properties = {}
+ 
+     for property in expand_property.children:
+-        namespace = property.getAttribute("namespace")
+-        name      = property.getAttribute("name")
++        namespace = property.attributes.get("namespace", dav_namespace)
++        name      = property.attributes.get("name", "")
++        
++        # Make sure children have no children
++        props_to_find = []
++        for child in property.children:
++            if child.children:
++                log.err("expand-property REPORT only supports single level expansion")
++                raise HTTPError(StatusResponse(
++                    responsecode.NOT_IMPLEMENTED,
++                    "expand-property REPORT only supports single level expansion"
++                ))
++            child_namespace = child.attributes.get("namespace", dav_namespace)
++            child_name      = child.attributes.get("name", "")
++            props_to_find.append((child_namespace, child_name))
+ 
+-        if not namespace: namespace = dav_namespace
++        properties[(namespace, name)] = props_to_find
+ 
+-        if (namespace, name) == (dav_namespace, "allprop"):
+-            all_properties = waitForDeferred(self.listAllProp(request))
+-            yield all_properties
+-            all_properties = all_properties.getResult()
+-
+-            for all_property in all_properties:
+-                properties[all_property.qname()] = property
+-        else:
+-            properties[(namespace, name)] = property
+-
+     #
+-    # Look up the requested properties
++    # Generate the expanded responses status for each top-level property
+     #
+     properties_by_status = {
+         responsecode.OK        : [],
+         responsecode.NOT_FOUND : [],
+     }
++    
++    filteredaces = None
++    lastParent = None
+ 
+-    for property in properties:
+-        my_properties = waitForDeferred(self.listProperties(request))
+-        yield my_properties
+-        my_properties = my_properties.getResult()
++    for qname in properties.iterkeys():
++        try:
++            prop = (yield self.readProperty(qname, request))
++            
++            # Form the PROPFIND-style DAV:prop element we need later
++            props_to_return = davxml.PropertyContainer(*properties[qname])
+ 
+-        if property in my_properties:
+-            try:
+-                value = waitForDeferred(self.readProperty(property, request))
+-                yield value
+-                value = value.getResult()
++            # Now dereference any HRefs
++            responses = []
++            for href in prop.children:
++                if isinstance(href, davxml.HRef):
++                    
++                    # Locate the Href resource and its parent
++                    resource_uri = str(href)
++                    child = (yield request.locateResource(resource_uri))
++    
++                    if not child or not child.exists():
++                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
++                        continue
++                    parent = (yield request.locateResource(parentForURL(resource_uri)))
++    
++                    # Check privileges on parent - must have at least DAV:read
++                    try:
++                        yield parent.checkPrivileges(request, (davxml.Read(),))
++                    except AccessDeniedError:
++                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
++                        continue
++                    
++                    # Cache the last parent's inherited aces for checkPrivileges optimization
++                    if lastParent != parent:
++                        lastParent = parent
++                
++                        # Do some optimisation of access control calculation by determining any inherited ACLs outside of
++                        # the child resource loop and supply those to the checkPrivileges on each child.
++                        filteredaces = (yield parent.inheritedACEsforChildren(request))
+ 
+-                if isinstance(value, davxml.HRef):
+-                    raise NotImplementedError()
+-                else:
+-                    raise NotImplementedError()
+-            except:
+-                f = Failure()
++                    # Check privileges - must have at least DAV:read
++                    try:
++                        yield child.checkPrivileges(request, (davxml.Read(),), inherited_aces=filteredaces)
++                    except AccessDeniedError:
++                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
++                        continue
++            
++                    # Now retrieve all the requested properties on the HRef resource
++                    yield prop_common.responseForHref(
++                        request,
++                        responses,
++                        href,
++                        child,
++                        prop_common.propertyListForResource,
++                        props_to_return,
++                    )
++            
++            prop.children = responses
++            properties_by_status[responsecode.OK].append(prop)
++        except:
++            f = Failure()
+ 
+-                log.err("Error reading property %r for resource %s: %s"
+-                        % (property, self, f.value))
++            log.err("Error reading property %r for resource %s: %s" % (qname, request.uri, f.value))
+ 
+-                status = statusForFailure(f, "getting property: %s" % (property,))
+-                if status not in properties_by_status:
+-                    properties_by_status[status] = []
++            status = statusForFailure(f, "getting property: %s" % (qname,))
++            if status not in properties_by_status: properties_by_status[status] = []
++            properties_by_status[status].append(propertyName(qname))
+ 
+-                raise NotImplementedError()
++    # Build the overall response
++    propstats = [
++        davxml.PropertyStatus(
++            davxml.PropertyContainer(*properties_by_status[status]),
++            davxml.Status.fromResponseCode(status)
++        )
++        for status in properties_by_status if properties_by_status[status]
++    ]
+ 
+-                #properties_by_status[status].append(
+-                #    ____propertyName(property)
+-                #)
+-        else:
+-            properties_by_status[responsecode.NOT_FOUND].append(property)
+-
+-    raise NotImplementedError()
+-
+-report_DAV__expand_property = deferredGenerator(report_DAV__expand_property)
++    returnValue(MultiStatusResponse((davxml.PropertyStatusResponse(davxml.HRef(request.uri), *propstats),)))

Modified: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch	2008-10-16 23:34:49 UTC (rev 3163)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch	2008-10-17 02:18:59 UTC (rev 3164)
@@ -208,7 +208,22 @@
      ##
      # DAV
      ##
-@@ -570,19 +647,21 @@
+@@ -553,12 +630,13 @@
+     def supportedReports(self):
+         """
+         See L{IDAVResource.supportedReports}.
+-        This implementation lists the three main ACL reports.
++        This implementation lists the three main ACL reports and expand-property.
+         """
+         result = []
+         result.append(davxml.Report(davxml.ACLPrincipalPropSet(),))
+         result.append(davxml.Report(davxml.PrincipalMatch(),))
+         result.append(davxml.Report(davxml.PrincipalPropertySearch(),))
++        result.append(davxml.Report(davxml.ExpandProperty(),))
+         return result
+ 
+     ##
+@@ -570,19 +648,21 @@
          See L{IDAVResource.authorize}.
          """
          def onError(failure):
@@ -235,7 +250,7 @@
                      response = UnauthorizedResponse(request.credentialFactories,
                                                      request.remoteAddr)
                  else:
-@@ -593,7 +672,7 @@
+@@ -593,7 +673,7 @@
                  # class is supposed to be a FORBIDDEN status code and
                  # "Authorization will not help" according to RFC2616
                  #
@@ -244,7 +259,7 @@
  
              d = self.checkPrivileges(request, privileges, recurse)
              d.addErrback(onErrors)
-@@ -606,16 +685,21 @@
+@@ -606,16 +686,21 @@
  
      def authenticate(self, request):
          def loginSuccess(result):
@@ -270,7 +285,7 @@
  
          authHeader = request.headers.getHeader('authorization')
  
-@@ -631,9 +715,10 @@
+@@ -631,9 +716,10 @@
  
                  # Try to match principals in each principal collection on the resource
                  def gotDetails(details):
@@ -284,7 +299,7 @@
  
                  def login(pcreds):
                      d = request.portal.login(pcreds, None, *request.loginInterfaces)
-@@ -641,13 +726,15 @@
+@@ -641,13 +727,15 @@
  
                      return d
  
@@ -304,7 +319,7 @@
  
      ##
      # ACL
-@@ -656,49 +743,23 @@
+@@ -656,49 +744,23 @@
      def currentPrincipal(self, request):
          """
          @param request: the request being processed.
@@ -363,7 +378,7 @@
          """
          @return: the L{davxml.ACL} element containing the default access control
              list for this resource.
-@@ -710,6 +771,17 @@
+@@ -710,6 +772,17 @@
          #
          return readonlyACL
  
@@ -381,7 +396,7 @@
      def setAccessControlList(self, acl):
          """
          See L{IDAVResource.setAccessControlList}.
-@@ -748,13 +820,16 @@
+@@ -748,13 +821,16 @@
          # 10. Verify that new acl is not in conflict with itself
          # 11. Update acl on the resource
  
@@ -399,7 +414,7 @@
  
          # Need to get list of supported privileges
          supported = []
-@@ -773,10 +848,7 @@
+@@ -773,10 +849,7 @@
          yield supportedPrivs
          supportedPrivs = supportedPrivs.getResult()
          for item in supportedPrivs.children:
@@ -411,7 +426,7 @@
              addSupportedPrivilege(item)
  
          # Steps 1 - 6
-@@ -910,8 +982,7 @@
+@@ -910,8 +983,7 @@
          supportedPrivs = supportedPrivs.getResult()
  
          # Other principals types don't make sense as actors.
@@ -421,7 +436,7 @@
              "Principal is not an actor: %r" % (principal,)
          )
  
-@@ -1019,15 +1090,16 @@
+@@ -1019,15 +1091,16 @@
          def getMyURL():
              url = request.urlForResource(self)
  
@@ -441,7 +456,7 @@
                  "Expected %s response from readDeadProperty() exception, not %s"
                  % (responsecode.NOT_FOUND, e.response.code)
              )
-@@ -1038,9 +1110,9 @@
+@@ -1038,9 +1111,9 @@
  
              if myURL == "/":
                  # If we get to the root without any ACLs, then use the default.
@@ -453,7 +468,7 @@
  
          # Dynamically update privileges for those ace's that are inherited.
          if inheritance:
-@@ -1076,7 +1148,7 @@
+@@ -1076,7 +1149,7 @@
                                  # Adjust ACE for inherit on this resource
                                  children = list(ace.children)
                                  children.remove(TwistedACLInheritable())
@@ -462,7 +477,7 @@
                                  aces.append(davxml.ACE(*children))
              else:
                  aces.extend(inherited_aces)
-@@ -1105,8 +1177,7 @@
+@@ -1105,8 +1178,7 @@
          the child resource loop and supply those to the checkPrivileges on each child.
  
          @param request: the L{IRequest} for the request in progress.
@@ -472,7 +487,7 @@
          """
          
          # Get the parent ACLs with inheritance and preserve the <inheritable> element.
-@@ -1128,21 +1199,9 @@
+@@ -1128,21 +1200,9 @@
                  # Adjust ACE for inherit on this resource
                  children = list(ace.children)
                  children.remove(TwistedACLInheritable())
@@ -496,7 +511,7 @@
  
      inheritedACEsforChildren = deferredGenerator(inheritedACEsforChildren)
  
-@@ -1152,49 +1211,69 @@
+@@ -1152,49 +1212,69 @@
  
          This implementation returns an empty set.
          """
@@ -594,7 +609,7 @@
      def samePrincipal(self, principal1, principal2):
          """
          Check whether the two prinicpals are exactly the same in terms of
-@@ -1219,7 +1298,6 @@
+@@ -1219,7 +1299,6 @@
              return False
                  
      def matchPrincipal(self, principal1, principal2, request):
@@ -602,7 +617,7 @@
          """
          Check whether the principal1 is a principal in the set defined by
          principal2.
-@@ -1244,6 +1322,9 @@
+@@ -1244,6 +1323,9 @@
              if isinstance(principal1, davxml.Unauthenticated):
                  yield False
                  return
@@ -612,7 +627,7 @@
              else:
                  yield True
                  return
-@@ -1260,10 +1341,7 @@
+@@ -1260,10 +1342,7 @@
              yield False
              return
  
@@ -624,7 +639,7 @@
  
          principal2 = waitForDeferred(self.resolvePrincipal(principal2, request))
          yield principal2
-@@ -1271,7 +1349,6 @@
+@@ -1271,7 +1350,6 @@
  
          assert principal2 is not None, "principal2 is None"
  
@@ -632,7 +647,7 @@
          # Compare two HRefs and do group membership test as well
          if principal1 == principal2:
              yield True
-@@ -1289,6 +1366,7 @@
+@@ -1289,6 +1367,7 @@
  
      matchPrincipal = deferredGenerator(matchPrincipal)
  
@@ -640,7 +655,7 @@
      def principalIsGroupMember(self, principal1, principal2, request):
          """
          Check whether one principal is a group member of another.
-@@ -1299,18 +1377,21 @@
+@@ -1299,18 +1378,21 @@
          @return: L{Deferred} with result C{True} if principal1 is a member of principal2, C{False} otherwise
          """
          
@@ -673,7 +688,7 @@
          
      def validPrincipal(self, ace_principal, request):
          """
-@@ -1351,11 +1432,16 @@
+@@ -1351,11 +1433,16 @@
          @return C{True} if C{href_principal} is valid, C{False} otherwise.
  
          This implementation tests for a href element that corresponds to
@@ -693,7 +708,7 @@
          return d
  
      def resolvePrincipal(self, principal, request):
-@@ -1404,8 +1490,7 @@
+@@ -1404,8 +1491,7 @@
              try:
                  principal = principal.getResult()
              except HTTPError, e:
@@ -703,7 +718,7 @@
                      "Expected %s response from readProperty() exception, not %s"
                      % (responsecode.NOT_FOUND, e.response.code)
                  )
-@@ -1432,15 +1517,15 @@
+@@ -1432,15 +1518,15 @@
                  log.err("DAV:self ACE is set on non-principal resource %r" % (self,))
                  yield None
                  return
@@ -722,7 +737,7 @@
              "Not a meta-principal: %r" % (principal,)
          )
  
-@@ -1517,6 +1602,270 @@
+@@ -1517,6 +1603,270 @@
          return None
  
      ##
@@ -993,7 +1008,7 @@
      # HTTP
      ##
  
-@@ -1525,15 +1874,11 @@
+@@ -1525,15 +1875,11 @@
          #litmus = request.headers.getRawHeaders("x-litmus")
          #if litmus: log.msg("*** Litmus test: %s ***" % (litmus,))
  
@@ -1011,7 +1026,7 @@
  
          def setHeaders(response):
              response = IResponse(response)
-@@ -1567,7 +1912,7 @@
+@@ -1567,7 +1913,7 @@
      def findChildren(self, depth, request, callback, privileges=None, inherited_aces=None):
          return succeed(None)
  
@@ -1020,7 +1035,7 @@
      """
      Resource representing a WebDAV principal.  (RFC 3744, section 2)
      """
-@@ -1577,7 +1922,7 @@
+@@ -1577,7 +1923,7 @@
      # WebDAV
      ##
  
@@ -1029,7 +1044,7 @@
          (dav_namespace, "alternate-URI-set"),
          (dav_namespace, "principal-URL"    ),
          (dav_namespace, "group-member-set" ),
-@@ -1585,14 +1930,11 @@
+@@ -1585,14 +1931,11 @@
      )
  
      def davComplianceClasses(self):
@@ -1045,7 +1060,7 @@
      def readProperty(self, property, request):
          def defer():
              if type(property) is tuple:
-@@ -1610,10 +1952,20 @@
+@@ -1610,10 +1953,20 @@
                      return davxml.PrincipalURL(davxml.HRef(self.principalURL()))
  
                  if name == "group-member-set":
@@ -1068,7 +1083,7 @@
  
                  if name == "resourcetype":
                      if self.isCollection():
-@@ -1655,7 +2007,7 @@
+@@ -1655,7 +2008,7 @@
          principals.  Subclasses should override this method to provide member
          URLs for this resource if appropriate.
          """
@@ -1077,7 +1092,7 @@
  
      def groupMemberships(self):
          """
-@@ -1666,6 +2018,7 @@
+@@ -1666,6 +2019,7 @@
          """
          unimplemented(self)
  
@@ -1085,7 +1100,7 @@
      def principalMatch(self, href):
          """
          Check whether the supplied principal matches this principal or is a
-@@ -1675,10 +2028,33 @@
+@@ -1675,10 +2029,33 @@
          """
          uri = str(href)
          if self.principalURL() == uri:
@@ -1121,7 +1136,7 @@
  class AccessDeniedError(Exception):
      def __init__(self, errors):
          """ 
-@@ -1718,6 +2094,37 @@
+@@ -1718,6 +2095,37 @@
  davxml.registerElement(TwistedACLInheritable)
  davxml.ACE.allowed_children[(twisted_dav_namespace, "inheritable")] = (0, 1)
  

Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py	2008-10-16 23:34:49 UTC (rev 3163)
+++ CalendarServer/trunk/twistedcaldav/customxml.py	2008-10-17 02:18:59 UTC (rev 3164)
@@ -23,6 +23,7 @@
 change.
 """
 
+from twisted.web2.dav.davxml import dav_namespace
 from twisted.web2.dav.davxml import twisted_dav_namespace
 from twisted.web2.dav import davxml
 
@@ -96,6 +97,30 @@
     namespace = calendarserver_namespace
     name = "calendar-proxy-write"
 
+class CalendarProxyReadFor (davxml.WebDAVElement):
+    """
+    List of principals granting read-only proxy status.
+    (Apple Extension to CalDAV)
+    """
+    namespace = calendarserver_namespace
+    name = "calendar-proxy-read-for"
+    hidden = True
+    protected = True
+
+    allowed_children = { (dav_namespace, "href"): (0, None) }
+
+class CalendarProxyWriteFor (davxml.WebDAVElement):
+    """
+    List of principals granting read-write proxy status.
+    (Apple Extension to CalDAV)
+    """
+    namespace = calendarserver_namespace
+    name = "calendar-proxy-write-for"
+    hidden = True
+    protected = True
+
+    allowed_children = { (dav_namespace, "href"): (0, None) }
+
 class TwistedCalendarPrincipalURI(davxml.WebDAVTextElement):
     """
     Contains the calendarPrincipalURI value for a directory record corresponding to a principal.

Modified: CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2008-10-16 23:34:49 UTC (rev 3163)
+++ CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2008-10-17 02:18:59 UTC (rev 3164)
@@ -13,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from uuid import UUID
 
 """
 Apple Open Directory directory service implementation.
@@ -24,9 +23,9 @@
     "OpenDirectoryInitError",
 ]
 
-import itertools
 import sys
 from random import random
+from uuid import UUID
 
 import opendirectory
 import dsattributes
@@ -300,6 +299,34 @@
         return record
 
 
+    def groupsForGUID(self, guid):
+        
+        # Lookup in index
+        try:
+            return self._storage(DirectoryService.recordType_groups)["groupsForGUID"][guid]
+        except KeyError:
+            return ()
+
+    def proxiesForGUID(self, recordType, guid):
+        
+        # Lookup in index
+        try:
+            return self._storage(recordType)["proxiesForGUID"][guid]
+        except KeyError:
+            return ()
+
+    def readOnlyProxiesForGUID(self, recordType, guid):
+        
+        # Lookup in index
+        try:
+            return self._storage(recordType)["readOnlyProxiesForGUID"][guid]
+        except KeyError:
+            return ()
+
+    def _indexGroup(self, group, guids, index):
+        for guid in guids:
+            index.setdefault(guid, set()).add(group)
+
     _ODFields = {
         'fullName' : dsattributes.kDS1AttrDistinguishedName,
         'firstName' : dsattributes.kDS1AttrFirstName,
@@ -385,6 +412,12 @@
 
             disabledNames = set()
             disabledGUIDs = set()
+            
+            if recordType == DirectoryService.recordType_groups:
+                groupsForGUID = {}
+            elif recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
+                proxiesForGUID = {}
+                readOnlyProxiesForGUID = {}
         else:
             storage = self._records[recordType]
 
@@ -393,6 +426,12 @@
 
             disabledNames = storage["disabled names"]
             disabledGUIDs = storage["disabled guids"]
+            
+            if recordType == DirectoryService.recordType_groups:
+                groupsForGUID = storage["groupsForGUID"]
+            elif recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
+                proxiesForGUID = storage["proxiesForGUID"]
+                readOnlyProxiesForGUID = storage["readOnlyProxiesForGUID"]
 
         enabled_count = 0
         for (recordShortName, value) in results:
@@ -534,6 +573,15 @@
                     records[record.shortName] = guids[record.guid] = record
                     self.log_debug("Added record %s to OD record cache" % (record,))
 
+                    # Do group indexing if needed
+                    if recordType == DirectoryService.recordType_groups:
+                        self._indexGroup(record, record._memberGUIDs, groupsForGUID)
+
+                    # Do proxy indexing if needed
+                    elif recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
+                        self._indexGroup(record, record._proxyGUIDs, proxiesForGUID)
+                        self._indexGroup(record, record._readOnlyProxyGUIDs, readOnlyProxiesForGUID)
+
         if shortName is None and guid is None:
             #
             # Replace the entire cache
@@ -546,6 +594,15 @@
                 "disabled guids": disabledGUIDs,
             }
 
+            # Add group indexing if needed
+            if recordType == DirectoryService.recordType_groups:
+                storage["groupsForGUID"] = groupsForGUID
+
+            # Add proxy indexing if needed
+            elif recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
+                storage["proxiesForGUID"] = proxiesForGUID
+                storage["readOnlyProxiesForGUID"] = readOnlyProxiesForGUID
+
             def rot():
                 storage["status"] = "stale"
                 removals = set()
@@ -735,9 +792,7 @@
                 yield userRecord
 
     def groups(self):
-        for groupRecord in self.service.recordsForType(DirectoryService.recordType_groups).itervalues():
-            if self.guid in groupRecord._memberGUIDs:
-                yield groupRecord
+        return self.service.groupsForGUID(self.guid)
 
     def proxies(self):
         if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
@@ -751,12 +806,10 @@
                 yield proxyRecord
 
     def proxyFor(self):
-        for proxyRecord in itertools.chain(
-            self.service.recordsForType(DirectoryService.recordType_resources).itervalues(),
-            self.service.recordsForType(DirectoryService.recordType_locations).itervalues(),
-        ):
-            if self.guid in proxyRecord._proxyGUIDs:
-                yield proxyRecord
+        result = set()
+        result.update(self.service.proxiesForGUID(DirectoryService.recordType_resources, self.guid))
+        result.update(self.service.proxiesForGUID(DirectoryService.recordType_locations, self.guid))
+        return result
 
     def readOnlyProxies(self):
         if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
@@ -770,12 +823,10 @@
                 yield proxyRecord
 
     def readOnlyProxyFor(self):
-        for proxyRecord in itertools.chain(
-            self.service.recordsForType(DirectoryService.recordType_resources).itervalues(),
-            self.service.recordsForType(DirectoryService.recordType_locations).itervalues(),
-        ):
-            if self.guid in proxyRecord._readOnlyProxyGUIDs:
-                yield proxyRecord
+        result = set()
+        result.update(self.service.readOnlyProxiesForGUID(DirectoryService.recordType_resources, self.guid))
+        result.update(self.service.readOnlyProxiesForGUID(DirectoryService.recordType_locations, self.guid))
+        return result
 
     def verifyCredentials(self, credentials):
         if isinstance(credentials, UsernamePassword):

Modified: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2008-10-16 23:34:49 UTC (rev 3163)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2008-10-17 02:18:59 UTC (rev 3164)
@@ -133,6 +133,15 @@
         else:
             return super(CalendarUserProxyPrincipalResource, self).resourceType()
 
+    def isProxyType(self, read_write):
+        if (
+            read_write and self.proxyType == "calendar-proxy-write" or
+            not read_write and self.proxyType == "calendar-proxy-read"
+        ):
+            return True
+        else:
+            return False
+
     def isCollection(self):
         return True
 

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2008-10-16 23:34:49 UTC (rev 3163)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2008-10-17 02:18:59 UTC (rev 3164)
@@ -476,6 +476,10 @@
         members = (yield self.groupMembers())
         
         memberships = (yield self.groupMemberships())
+        
+        proxyFor = (yield self.proxyFor(True))
+        
+        readOnlyProxyFor = (yield self.proxyFor(False))
 
         returnValue("".join((
             """<div class="directory-listing">"""
@@ -500,6 +504,8 @@
             """\nAlternate URIs:\n"""          , format_list(format_link(u) for u in self.alternateURIs()),
             """\nGroup members:\n"""           , format_principals(members),
             """\nGroup memberships:\n"""       , format_principals(memberships),
+            """\nRead-write Proxy For:\n"""    , format_principals(proxyFor),
+            """\nRead-only Proxy For:\n"""     , format_principals(readOnlyProxyFor),
             """</pre></blockquote></div>""",
             output
         )))
@@ -597,6 +603,36 @@
 
         returnValue(groups)
 
+    @inlineCallbacks
+    def proxyFor(self, read_write, resolve_memberships=True):
+        proxyFors = set()
+
+        if resolve_memberships:
+            memberships = self._getRelatives("groups")
+            for membership in memberships:
+                results = (yield membership.proxyFor(read_write, False))
+                proxyFors.update(results)
+
+        if config.EnableProxyPrincipals:
+            # Get any directory specified proxies
+            if read_write:
+                directoryProxies = self._getRelatives("proxyFor", proxy='read-write')
+            else:
+                directoryProxies = self._getRelatives("readOnlyProxyFor", proxy='read-only')
+            proxyFors.update([subprincipal.parent for subprincipal in directoryProxies])
+
+            # Get proxy group UIDs and map to principal resources
+            proxies = []
+            memberships = (yield self._calendar_user_proxy_index().getMemberships(self.principalUID()))
+            for uid in memberships:
+                subprincipal = self.parent.principalForUID(uid)
+                if subprincipal and subprincipal.isProxyType(read_write):
+                    proxies.append(subprincipal.parent)
+
+            proxyFors.update(proxies)
+
+        returnValue(proxyFors)
+
     def principalCollections(self):
         return self.parent.principalCollections()
 
@@ -640,6 +676,10 @@
         
         memberships = (yield self.groupMemberships())
         
+        proxyFor = (yield self.proxyFor(True))
+        
+        readOnlyProxyFor = (yield self.proxyFor(False))
+        
         returnValue("".join((
             """<div class="directory-listing">"""
             """<h1>Principal Details</h1>"""
@@ -663,6 +703,8 @@
             """\nAlternate URIs:\n"""          , format_list(format_link(u) for u in self.alternateURIs()),
             """\nGroup members:\n"""           , format_principals(members),
             """\nGroup memberships:\n"""       , format_principals(memberships),
+            """\nRead-write Proxy For:\n"""    , format_principals(proxyFor),
+            """\nRead-only Proxy For:\n"""     , format_principals(readOnlyProxyFor),
             """\nCalendar homes:\n"""          , format_list(format_link(u) for u in self.calendarHomeURLs()),
             """\nCalendar user addresses:\n""" , format_list(format_link(a) for a in self.calendarUserAddresses()),
             """</pre></blockquote></div>""",

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryrecords.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryrecords.py	2008-10-16 23:34:49 UTC (rev 3163)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryrecords.py	2008-10-17 02:18:59 UTC (rev 3164)
@@ -337,7 +337,102 @@
                 ("EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E", "62368DDF-0C62-4C97-9A58-DE9FD46131A0", "D10F3EE0-5014-41D3-8488-3819D3EF3B2A"),
             )
 
-def fakeODRecord(fullName, shortName=None, guid=None, email=None):
+        def test_groupmembers(self):
+            self._service.fakerecords = {
+                DirectoryService.recordType_users: [
+                    fakeODRecord("User 01"),
+                    fakeODRecord("User 02"),
+                ],
+                DirectoryService.recordType_groups: [
+                    fakeODRecord("Group 01", members=[
+                        guidForShortName("user01"),
+                        guidForShortName("user02"),
+                    ]),
+                    fakeODRecord("Group 02", members=[
+                        guidForShortName("resource01"),
+                        guidForShortName("user02"),
+                    ]),
+                ],
+                DirectoryService.recordType_resources: [
+                    fakeODRecord("Resource 01"),
+                    fakeODRecord("Resource 02"),
+                ],
+                DirectoryService.recordType_locations: [
+                    fakeODRecord("Location 01"),
+                    fakeODRecord("Location 02"),
+                ],
+            }
+
+            self._service.reloadCache(DirectoryService.recordType_users)
+            self._service.reloadCache(DirectoryService.recordType_groups)
+            self._service.reloadCache(DirectoryService.recordType_resources)
+            self._service.reloadCache(DirectoryService.recordType_locations)
+
+            group1 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group01")
+            self.assertTrue(group1 is not None)
+
+            group2 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group02")
+            self.assertTrue(group2 is not None)
+
+            user1 = self._service.recordWithShortName(DirectoryService.recordType_users, "user01")
+            self.assertTrue(user1 is not None)
+            self.assertEqual(set((group1,)), user1.groups()) 
+            
+            user2 = self._service.recordWithShortName(DirectoryService.recordType_users, "user02")
+            self.assertTrue(user2 is not None)
+            self.assertEqual(set((group1, group2)), user2.groups()) 
+            
+            self._service.fakerecords[DirectoryService.recordType_groups] = [
+                fakeODRecord("Group 01", members=[
+                    guidForShortName("user01"),
+                ]),
+                fakeODRecord("Group 02", members=[
+                    guidForShortName("resource01"),
+                    guidForShortName("user02"),
+                ]),
+            ]
+            self._service.reloadCache(DirectoryService.recordType_groups)
+
+            group1 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group01")
+            self.assertTrue(group1 is not None)
+
+            group2 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group02")
+            self.assertTrue(group2 is not None)
+
+            user1 = self._service.recordWithShortName(DirectoryService.recordType_users, "user01")
+            self.assertTrue(user1 is not None)
+            self.assertEqual(set((group1,)), user1.groups()) 
+            
+            user2 = self._service.recordWithShortName(DirectoryService.recordType_users, "user02")
+            self.assertTrue(user2 is not None)
+            self.assertEqual(set((group2,)), user2.groups()) 
+            
+            self._service.fakerecords[DirectoryService.recordType_groups] = [
+                fakeODRecord("Group 03", members=[
+                    guidForShortName("user01"),
+                    guidForShortName("user02"),
+                ]),
+            ]
+            self._service.reloadCache(DirectoryService.recordType_groups, guid=guidForShortName("group03"))
+
+            group1 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group01")
+            self.assertTrue(group1 is not None)
+
+            group2 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group02")
+            self.assertTrue(group2 is not None)
+
+            group3 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group03")
+            self.assertTrue(group2 is not None)
+
+            user1 = self._service.recordWithShortName(DirectoryService.recordType_users, "user01")
+            self.assertTrue(user1 is not None)
+            self.assertEqual(set((group1, group3)), user1.groups()) 
+            
+            user2 = self._service.recordWithShortName(DirectoryService.recordType_users, "user02")
+            self.assertTrue(user2 is not None)
+            self.assertEqual(set((group2, group3)), user2.groups()) 
+
+def fakeODRecord(fullName, shortName=None, guid=None, email=None, members=None):
     if shortName is None:
         shortName = shortNameForFullName(fullName)
 
@@ -355,6 +450,9 @@
         dsattributes.kDSNAttrEMailAddress: email,
         dsattributes.kDSNAttrMetaNodeLocation: "/LDAPv3/127.0.0.1",
     }
+    
+    if members:
+        attrs[dsattributes.kDSNAttrGroupMembers] = members
 
     return [ shortName, attrs ]
 

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2008-10-16 23:34:49 UTC (rev 3163)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2008-10-17 02:18:59 UTC (rev 3164)
@@ -14,7 +14,7 @@
 # limitations under the License.
 ##
 
-from twisted.internet.defer import DeferredList
+from twisted.internet.defer import DeferredList, inlineCallbacks
 from twisted.web2.dav import davxml
 
 from twistedcaldav.directory.directory import DirectoryService
@@ -74,6 +74,13 @@
         d = principal.groupMemberships()
         d.addCallback(gotMemberships)
         return d
+    
+    @inlineCallbacks
+    def _proxyForTest(self, recordType, recordName, expectedProxies, read_write):
+        principal = self._getPrincipalByShortName(recordType, recordName)
+        proxies = (yield principal.proxyFor(read_write))
+        proxies = set([principal.displayName() for principal in proxies])
+        self.assertEquals(proxies, set(expectedProxies))
 
     def test_groupMembersRegular(self):
         """
@@ -254,3 +261,20 @@
             self.assertEquals(notifier.changedCount, 1)
         finally:
             DirectoryPrincipalResource.cacheNotifierFactory = oldCacheNotifier
+
+    def test_proxyFor(self):
+
+        return self._proxyForTest(
+            DirectoryService.recordType_users, "wsanchez", 
+            ("Mecury Seven", "Gemini Twelve", "Apollo Eleven", "Orion", ),
+            True
+        )
+
+    def test_readOnlyProxyFor(self):
+
+        return self._proxyForTest(
+            DirectoryService.recordType_users, "wsanchez", 
+            ("Non-calendar proxy", ),
+            False
+        )
+

Modified: CalendarServer/trunk/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/extensions.py	2008-10-16 23:34:49 UTC (rev 3163)
+++ CalendarServer/trunk/twistedcaldav/extensions.py	2008-10-17 02:18:59 UTC (rev 3164)
@@ -773,7 +773,7 @@
 
                 return f
 
-            for qname in qnames:
+            for qname in sorted(qnames):
                 d = self.readProperty(qname, request)
                 d.addCallback(gotProperty)
                 d.addErrback(gotError, qname)

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2008-10-16 23:34:49 UTC (rev 3163)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2008-10-17 02:18:59 UTC (rev 3164)
@@ -697,6 +697,8 @@
         (calendarserver_namespace, "first-name"       ),
         (calendarserver_namespace, "last-name"        ),
         (calendarserver_namespace, "email-address-set"),
+        (calendarserver_namespace, "calendar-proxy-read-for"  ),
+        (calendarserver_namespace, "calendar-proxy-write-for" ),
     )
 
     @classmethod
@@ -710,76 +712,85 @@
     def isCollection(self):
         return True
 
+    @inlineCallbacks
     def readProperty(self, property, request):
-        def defer():
-            if type(property) is tuple:
-                qname = property
-            else:
-                qname = property.qname()
+        if type(property) is tuple:
+            qname = property
+        else:
+            qname = property.qname()
 
-            namespace, name = qname
+        namespace, name = qname
 
-            if namespace == caldav_namespace:
-                if name == "calendar-home-set":
-                    return caldavxml.CalendarHomeSet(
-                        *[davxml.HRef(url) for url in self.calendarHomeURLs()]
-                    )
+        if namespace == caldav_namespace:
+            if name == "calendar-home-set":
+                returnValue(caldavxml.CalendarHomeSet(
+                    *[davxml.HRef(url) for url in self.calendarHomeURLs()]
+                ))
 
-                if name == "calendar-user-address-set":
-                    return succeed(caldavxml.CalendarUserAddressSet(
-                        *[davxml.HRef(uri) for uri in self.calendarUserAddresses()]
-                    ))
+            elif name == "calendar-user-address-set":
+                returnValue(caldavxml.CalendarUserAddressSet(
+                    *[davxml.HRef(uri) for uri in self.calendarUserAddresses()]
+                ))
 
-                if name == "schedule-inbox-URL":
-                    url = self.scheduleInboxURL()
-                    if url is None:
-                        return None
-                    else:
-                        return caldavxml.ScheduleInboxURL(davxml.HRef(url))
+            elif name == "schedule-inbox-URL":
+                url = self.scheduleInboxURL()
+                if url is None:
+                    returnValue(None)
+                else:
+                    returnValue(caldavxml.ScheduleInboxURL(davxml.HRef(url)))
 
-                if name == "schedule-outbox-URL":
-                    url = self.scheduleOutboxURL()
-                    if url is None:
-                        return None
-                    else:
-                        return caldavxml.ScheduleOutboxURL(davxml.HRef(url))
+            elif name == "schedule-outbox-URL":
+                url = self.scheduleOutboxURL()
+                if url is None:
+                    returnValue(None)
+                else:
+                    returnValue(caldavxml.ScheduleOutboxURL(davxml.HRef(url)))
 
-                if name == "calendar-user-type":
-                    return caldavxml.CalendarUserType(
-                        self.record.getCUType()
-                    )
+            elif name == "calendar-user-type":
+                returnValue(caldavxml.CalendarUserType(self.record.getCUType()))
 
-            elif namespace == calendarserver_namespace:
-                if name == "dropbox-home-URL" and config.EnableDropBox:
-                    url = self.dropboxURL()
-                    if url is None:
-                        return None
-                    else:
-                        return customxml.DropBoxHomeURL(davxml.HRef(url))
+        elif namespace == calendarserver_namespace:
+            if name == "dropbox-home-URL" and config.EnableDropBox:
+                url = self.dropboxURL()
+                if url is None:
+                    returnValue(None)
+                else:
+                    returnValue(customxml.DropBoxHomeURL(davxml.HRef(url)))
 
-                if name == "first-name":
-                    firstName = self.record.firstName
-                    if firstName:
-                        return customxml.FirstNameProperty(firstName)
-                    else:
-                        return None
+            elif name == "first-name":
+                firstName = self.record.firstName
+                if firstName:
+                    returnValue(customxml.FirstNameProperty(firstName))
+                else:
+                    returnValue(None)
 
-                if name == "last-name":
-                    lastName = self.record.lastName
-                    if lastName:
-                        return customxml.LastNameProperty(lastName)
-                    else:
-                        return None
+            elif name == "last-name":
+                lastName = self.record.lastName
+                if lastName:
+                    returnValue(customxml.LastNameProperty(lastName))
+                else:
+                    returnValue(None)
 
-                if name == "email-address-set":
-                    return succeed(customxml.EmailAddressSet(
-                        *[customxml.EmailAddressProperty(addr) for addr in self.record.emailAddresses]
-                    ))
+            elif name == "email-address-set":
+                returnValue(customxml.EmailAddressSet(
+                    *[customxml.EmailAddressProperty(addr) for addr in self.record.emailAddresses]
+                ))
 
-            return super(CalendarPrincipalResource, self).readProperty(property, request)
+            elif name == "calendar-proxy-read-for":
+                results = (yield self.proxyFor(False))
+                returnValue(customxml.CalendarProxyReadFor(
+                    *[davxml.HRef(principal.principalURL()) for principal in results]
+                ))
 
-        return maybeDeferred(defer)
+            elif name == "calendar-proxy-write-for":
+                results = (yield self.proxyFor(True))
+                returnValue(customxml.CalendarProxyWriteFor(
+                    *[davxml.HRef(principal.principalURL()) for principal in results]
+                ))
 
+        result = (yield super(CalendarPrincipalResource, self).readProperty(property, request))
+        returnValue(result)
+
     def groupMembers(self):
         return succeed(())
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20081016/6347a57c/attachment-0001.html 


More information about the calendarserver-changes mailing list