[CalendarServer-changes] [3416] CalendarServer/branches/users/wsanchez/deployment

source_changes at macosforge.org source_changes at macosforge.org
Fri Nov 28 11:45:46 PST 2008


Revision: 3416
          http://trac.macosforge.org/projects/calendarserver/changeset/3416
Author:   cdaboo at apple.com
Date:     2008-11-28 11:45:46 -0800 (Fri, 28 Nov 2008)
Log Message:
-----------
Merge expand REPORT and expanded proxy principal membership properties to deployment.

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

Added Paths:
-----------
    CalendarServer/branches/users/wsanchez/deployment/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch

Added: CalendarServer/branches/users/wsanchez/deployment/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch	2008-11-28 19:45:46 UTC (rev 3416)
@@ -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/branches/users/wsanchez/deployment/lib-patches/Twisted/twisted.web2.dav.resource.patch
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/lib-patches/Twisted/twisted.web2.dav.resource.patch	2008-11-27 04:19:41 UTC (rev 3415)
+++ CalendarServer/branches/users/wsanchez/deployment/lib-patches/Twisted/twisted.web2.dav.resource.patch	2008-11-28 19:45:46 UTC (rev 3416)
@@ -192,7 +192,22 @@
      ##
      # DAV
      ##
-@@ -570,19 +646,21 @@
+@@ -553,12 +629,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 +647,21 @@
          See L{IDAVResource.authorize}.
          """
          def onError(failure):
@@ -219,7 +234,7 @@
                      response = UnauthorizedResponse(request.credentialFactories,
                                                      request.remoteAddr)
                  else:
-@@ -593,7 +671,7 @@
+@@ -593,7 +672,7 @@
                  # class is supposed to be a FORBIDDEN status code and
                  # "Authorization will not help" according to RFC2616
                  #
@@ -228,7 +243,7 @@
  
              d = self.checkPrivileges(request, privileges, recurse)
              d.addErrback(onErrors)
-@@ -606,16 +684,21 @@
+@@ -606,16 +685,21 @@
  
      def authenticate(self, request):
          def loginSuccess(result):
@@ -254,7 +269,7 @@
  
          authHeader = request.headers.getHeader('authorization')
  
-@@ -631,9 +714,10 @@
+@@ -631,9 +715,10 @@
  
                  # Try to match principals in each principal collection on the resource
                  def gotDetails(details):
@@ -268,7 +283,7 @@
  
                  def login(pcreds):
                      d = request.portal.login(pcreds, None, *request.loginInterfaces)
-@@ -641,13 +725,15 @@
+@@ -641,13 +726,15 @@
  
                      return d
  
@@ -288,7 +303,7 @@
  
      ##
      # ACL
-@@ -656,49 +742,23 @@
+@@ -656,49 +743,23 @@
      def currentPrincipal(self, request):
          """
          @param request: the request being processed.
@@ -347,7 +362,7 @@
          """
          @return: the L{davxml.ACL} element containing the default access control
              list for this resource.
-@@ -710,6 +770,17 @@
+@@ -710,6 +771,17 @@
          #
          return readonlyACL
  
@@ -365,7 +380,7 @@
      def setAccessControlList(self, acl):
          """
          See L{IDAVResource.setAccessControlList}.
-@@ -748,13 +819,16 @@
+@@ -748,13 +820,16 @@
          # 10. Verify that new acl is not in conflict with itself
          # 11. Update acl on the resource
  
@@ -383,7 +398,7 @@
  
          # Need to get list of supported privileges
          supported = []
-@@ -1038,9 +1112,9 @@
+@@ -1038,9 +1113,9 @@
  
              if myURL == "/":
                  # If we get to the root without any ACLs, then use the default.
@@ -395,7 +410,7 @@
  
          # Dynamically update privileges for those ace's that are inherited.
          if inheritance:
-@@ -1076,7 +1150,7 @@
+@@ -1076,7 +1151,7 @@
                                  # Adjust ACE for inherit on this resource
                                  children = list(ace.children)
                                  children.remove(TwistedACLInheritable())
@@ -404,7 +419,7 @@
                                  aces.append(davxml.ACE(*children))
              else:
                  aces.extend(inherited_aces)
-@@ -1105,8 +1179,7 @@
+@@ -1105,8 +1180,7 @@
          the child resource loop and supply those to the checkPrivileges on each child.
  
          @param request: the L{IRequest} for the request in progress.
@@ -414,7 +429,7 @@
          """
          
          # Get the parent ACLs with inheritance and preserve the <inheritable> element.
-@@ -1128,21 +1201,9 @@
+@@ -1128,21 +1202,9 @@
                  # Adjust ACE for inherit on this resource
                  children = list(ace.children)
                  children.remove(TwistedACLInheritable())
@@ -438,7 +453,7 @@
  
      inheritedACEsforChildren = deferredGenerator(inheritedACEsforChildren)
  
-@@ -1152,49 +1213,69 @@
+@@ -1152,49 +1214,69 @@
  
          This implementation returns an empty set.
          """
@@ -536,7 +551,7 @@
      def samePrincipal(self, principal1, principal2):
          """
          Check whether the two prinicpals are exactly the same in terms of
-@@ -1219,7 +1300,6 @@
+@@ -1219,7 +1301,6 @@
              return False
                  
      def matchPrincipal(self, principal1, principal2, request):
@@ -544,7 +559,7 @@
          """
          Check whether the principal1 is a principal in the set defined by
          principal2.
-@@ -1244,6 +1324,9 @@
+@@ -1244,6 +1325,9 @@
              if isinstance(principal1, davxml.Unauthenticated):
                  yield False
                  return
@@ -554,7 +569,7 @@
              else:
                  yield True
                  return
-@@ -1271,7 +1354,6 @@
+@@ -1271,7 +1355,6 @@
  
          assert principal2 is not None, "principal2 is None"
  
@@ -562,7 +577,7 @@
          # Compare two HRefs and do group membership test as well
          if principal1 == principal2:
              yield True
-@@ -1289,6 +1371,7 @@
+@@ -1289,6 +1372,7 @@
  
      matchPrincipal = deferredGenerator(matchPrincipal)
  
@@ -570,7 +585,7 @@
      def principalIsGroupMember(self, principal1, principal2, request):
          """
          Check whether one principal is a group member of another.
-@@ -1299,18 +1382,21 @@
+@@ -1299,18 +1383,21 @@
          @return: L{Deferred} with result C{True} if principal1 is a member of principal2, C{False} otherwise
          """
          
@@ -603,7 +618,7 @@
          
      def validPrincipal(self, ace_principal, request):
          """
-@@ -1351,11 +1437,16 @@
+@@ -1351,11 +1438,16 @@
          @return C{True} if C{href_principal} is valid, C{False} otherwise.
  
          This implementation tests for a href element that corresponds to
@@ -623,7 +638,7 @@
          return d
  
      def resolvePrincipal(self, principal, request):
-@@ -1432,7 +1523,7 @@
+@@ -1432,7 +1524,7 @@
                  log.err("DAV:self ACE is set on non-principal resource %r" % (self,))
                  yield None
                  return
@@ -632,7 +647,7 @@
  
          if isinstance(principal, davxml.HRef):
              yield principal
-@@ -1517,6 +1608,270 @@
+@@ -1517,6 +1609,270 @@
          return None
  
      ##
@@ -903,7 +918,7 @@
      # HTTP
      ##
  
-@@ -1525,15 +1880,11 @@
+@@ -1525,15 +1881,11 @@
          #litmus = request.headers.getRawHeaders("x-litmus")
          #if litmus: log.msg("*** Litmus test: %s ***" % (litmus,))
  
@@ -921,7 +936,7 @@
  
          def setHeaders(response):
              response = IResponse(response)
-@@ -1567,7 +1918,7 @@
+@@ -1567,7 +1919,7 @@
      def findChildren(self, depth, request, callback, privileges=None, inherited_aces=None):
          return succeed(None)
  
@@ -930,7 +945,7 @@
      """
      Resource representing a WebDAV principal.  (RFC 3744, section 2)
      """
-@@ -1577,7 +1928,7 @@
+@@ -1577,7 +1929,7 @@
      # WebDAV
      ##
  
@@ -939,7 +954,7 @@
          (dav_namespace, "alternate-URI-set"),
          (dav_namespace, "principal-URL"    ),
          (dav_namespace, "group-member-set" ),
-@@ -1585,14 +1936,11 @@
+@@ -1585,14 +1937,11 @@
      )
  
      def davComplianceClasses(self):
@@ -955,7 +970,7 @@
      def readProperty(self, property, request):
          def defer():
              if type(property) is tuple:
-@@ -1610,10 +1958,20 @@
+@@ -1610,10 +1959,20 @@
                      return davxml.PrincipalURL(davxml.HRef(self.principalURL()))
  
                  if name == "group-member-set":
@@ -978,7 +993,7 @@
  
                  if name == "resourcetype":
                      if self.isCollection():
-@@ -1655,7 +2013,7 @@
+@@ -1655,7 +2014,7 @@
          principals.  Subclasses should override this method to provide member
          URLs for this resource if appropriate.
          """
@@ -987,7 +1002,7 @@
  
      def groupMemberships(self):
          """
-@@ -1666,6 +2024,7 @@
+@@ -1666,6 +2025,7 @@
          """
          unimplemented(self)
  
@@ -995,7 +1010,7 @@
      def principalMatch(self, href):
          """
          Check whether the supplied principal matches this principal or is a
-@@ -1675,10 +2034,33 @@
+@@ -1675,10 +2035,33 @@
          """
          uri = str(href)
          if self.principalURL() == uri:
@@ -1031,7 +1046,7 @@
  class AccessDeniedError(Exception):
      def __init__(self, errors):
          """ 
-@@ -1718,6 +2100,37 @@
+@@ -1718,6 +2101,37 @@
  davxml.registerElement(TwistedACLInheritable)
  davxml.ACE.allowed_children[(twisted_dav_namespace, "inheritable")] = (0, 1)
  

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/customxml.py	2008-11-27 04:19:41 UTC (rev 3415)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/customxml.py	2008-11-28 19:45:46 UTC (rev 3416)
@@ -23,7 +23,8 @@
 change.
 """
 
-from twisted.web2.dav.resource import twisted_dav_namespace
+from twisted.web2.dav.davxml import dav_namespace
+from twisted.web2.dav.davxml import twisted_dav_namespace
 from twisted.web2.dav import davxml
 
 from twistedcaldav.ical import Component as iComponent
@@ -87,6 +88,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/branches/users/wsanchez/deployment/twistedcaldav/directory/aggregate.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/aggregate.py	2008-11-27 04:19:41 UTC (rev 3415)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/aggregate.py	2008-11-28 19:45:46 UTC (rev 3416)
@@ -88,7 +88,11 @@
         return set(self._recordTypes)
 
     def listRecords(self, recordType):
-        return self._query("listRecords", recordType)
+        records = self._query("listRecords", recordType)
+        if records is None:
+            return ()
+        else:
+            return records
 
     def recordWithShortName(self, recordType, shortName):
         return self._query("recordWithShortName", recordType, shortName)

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/appleopendirectory.py	2008-11-27 04:19:41 UTC (rev 3415)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/appleopendirectory.py	2008-11-28 19:45:46 UTC (rev 3416)
@@ -23,10 +23,10 @@
     "OpenDirectoryInitError",
 ]
 
-import itertools
 import sys
 import os
 from random import random
+from uuid import UUID
 
 import opendirectory
 import dsattributes
@@ -474,6 +474,105 @@
 
         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,
+        'lastName' : dsattributes.kDS1AttrLastName,
+        'emailAddresses' : dsattributes.kDSNAttrEMailAddress,
+    }
+
+    _toODRecordTypes = {
+        DirectoryService.recordType_users :
+            dsattributes.kDSStdRecordTypeUsers,
+        DirectoryService.recordType_locations :
+            dsattributes.kDSStdRecordTypePlaces,
+        DirectoryService.recordType_groups :
+            dsattributes.kDSStdRecordTypeGroups,
+        DirectoryService.recordType_resources :
+            dsattributes.kDSStdRecordTypeResources,
+    }
+
+    _fromODRecordTypes = dict([(b, a) for a, b in _toODRecordTypes.iteritems()])
+
+    def recordsMatchingFields(self, fields, operand="or", recordType=None):
+
+        # Note that OD applies case-sensitivity globally across the entire
+        # query, not per expression, so the current code uses whatever is
+        # specified in the last field in the fields list
+
+        operand = (dsquery.expression.OR if operand == "or"
+            else dsquery.expression.AND)
+
+        expressions = []
+        for field, value, caseless, matchType in fields:
+            if field in self._ODFields:
+                ODField = self._ODFields[field]
+                if matchType == "starts-with":
+                    comparison = dsattributes.eDSStartsWith
+                else:
+                    comparison = dsattributes.eDSContains
+                expressions.append(dsquery.match(ODField, value, comparison))
+
+
+        if recordType is None:
+            recordTypes = self._toODRecordTypes.values()
+        else:
+            recordTypes = (self._toODRecordTypes[recordType],)
+
+        for recordType in recordTypes:
+
+            try:
+                self.log_info("Calling OD: Type %s, Operand %s, Caseless %s, %s" % (recordType, operand, caseless, fields))
+                results = opendirectory.queryRecordsWithAttributes(
+                    self.directory,
+                    dsquery.expression(operand, expressions).generate(),
+                    caseless,
+                    recordType,
+                    [ dsattributes.kDS1AttrGeneratedUID ]
+                )
+                self.log_info("Got back %d records from OD" % (len(results),))
+                for key, val in results.iteritems():
+                    self.log_debug("OD result: %s %s" % (key, val))
+                    try:
+                        guid = val[dsattributes.kDS1AttrGeneratedUID]
+                        rec = self.recordWithGUID(guid)
+                        if rec:
+                            yield rec
+                    except KeyError:
+                        pass
+
+            except Exception, e:
+                self.log_error("OD search failed: %s" % (e,))
+                raise
+
     def reloadCache(self, recordType, shortName=None, guid=None):
         if shortName:
             self.log_info("Faulting record %s into %s record cache" % (shortName, recordType))
@@ -488,6 +587,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]
 
@@ -496,6 +601,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"]
 
         for (recordShortName, value) in results:
             enabledForCalendaring = True
@@ -638,6 +749,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
@@ -650,6 +770,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()
@@ -885,9 +1014,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):
@@ -901,12 +1028,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):
@@ -920,12 +1045,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/branches/users/wsanchez/deployment/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/calendaruserproxy.py	2008-11-27 04:19:41 UTC (rev 3415)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/calendaruserproxy.py	2008-11-28 19:45:46 UTC (rev 3416)
@@ -132,6 +132,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/branches/users/wsanchez/deployment/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/principal.py	2008-11-27 04:19:41 UTC (rev 3415)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/principal.py	2008-11-28 19:45:46 UTC (rev 3416)
@@ -425,6 +425,14 @@
         d = waitForDeferred(self.groupMemberships())
         yield d
         memberships = d.getResult()
+        
+        d = waitForDeferred(self.proxyFor(True))
+        yield d
+        proxyFor = d.getResult()
+        
+        d = waitForDeferred(self.proxyFor(False))
+        yield d
+        readOnlyProxyFor = d.getResult()
 
         yield "".join((
             """<div class="directory-listing">"""
@@ -446,6 +454,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
         ))
@@ -546,6 +556,40 @@
 
         yield groups
 
+    @deferredGenerator
+    def proxyFor(self, read_write, resolve_memberships=True):
+        proxyFors = set()
+
+        if resolve_memberships:
+            memberships = self._getRelatives("groups")
+            for membership in memberships:
+                d = waitForDeferred(membership.proxyFor(read_write, False))
+                yield d
+                results = d.getResult()
+                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 = []
+            d = waitForDeferred(self._calendar_user_proxy_index().getMemberships(self.principalUID()))
+            yield d
+            memberships = d.getResult()
+            for uid in memberships:
+                subprincipal = self.parent.principalForUID(uid)
+                if subprincipal and subprincipal.isProxyType(read_write):
+                    proxies.append(subprincipal.parent)
+
+            proxyFors.update(proxies)
+
+        yield proxyFors
+
     def principalCollections(self):
         return self.parent.principalCollections()
 
@@ -595,6 +639,14 @@
         yield d
         memberships = d.getResult()
         
+        d = waitForDeferred(self.proxyFor(True))
+        yield d
+        proxyFor = d.getResult()
+        
+        d = waitForDeferred(self.proxyFor(False))
+        yield d
+        readOnlyProxyFor = d.getResult()
+        
         yield "".join((
             """<div class="directory-listing">"""
             """<h1>Principal Details</h1>"""
@@ -615,6 +667,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/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectoryrecords.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectoryrecords.py	2008-11-27 04:19:41 UTC (rev 3415)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_opendirectoryrecords.py	2008-11-28 19:45:46 UTC (rev 3416)
@@ -300,7 +300,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, addLocator=True):
+        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, addLocator=True, members=None):
     if shortName is None:
         shortName = shortNameForFullName(fullName)
 
@@ -318,6 +413,9 @@
         dsattributes.kDSNAttrEMailAddress: email,
         dsattributes.kDSNAttrMetaNodeLocation: "/LDAPv3/127.0.0.1",
     }
+    
+    if members:
+        attrs[dsattributes.kDSNAttrGroupMembers] = members
 
     if addLocator:
         attrs[dsattributes.kDSNAttrServicesLocator] = "FE588D50-0514-4DF9-BCB5-8ECA5F3DA274:030572AE-ABEC-4E0F-83C9-FCA304769E5F:calendar"

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2008-11-27 04:19:41 UTC (rev 3415)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2008-11-28 19:45:46 UTC (rev 3416)
@@ -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/branches/users/wsanchez/deployment/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/extensions.py	2008-11-27 04:19:41 UTC (rev 3415)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/extensions.py	2008-11-28 19:45:46 UTC (rev 3416)
@@ -636,7 +636,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/branches/users/wsanchez/deployment/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/resource.py	2008-11-27 04:19:41 UTC (rev 3415)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/resource.py	2008-11-28 19:45:46 UTC (rev 3416)
@@ -603,6 +603,8 @@
         (caldav_namespace, "calendar-user-address-set"),
         (caldav_namespace, "schedule-inbox-URL"       ),
         (caldav_namespace, "schedule-outbox-URL"      ),
+        (calendarserver_namespace, "calendar-proxy-read-for"  ),
+        (calendarserver_namespace, "calendar-proxy-write-for" ),
     )
 
     @classmethod
@@ -616,52 +618,78 @@
     def isCollection(self):
         return True
 
+    @deferredGenerator
     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":
+                yield caldavxml.CalendarHomeSet(
+                    *[davxml.HRef(url) for url in self.calendarHomeURLs()]
+                )
+                return
 
-                if name == "calendar-user-address-set":
-                    return succeed(caldavxml.CalendarUserAddressSet(
-                        *[davxml.HRef(uri) for uri in self.calendarUserAddresses()]
-                    ))
+            elif name == "calendar-user-address-set":
+                yield caldavxml.CalendarUserAddressSet(
+                    *[davxml.HRef(uri) for uri in self.calendarUserAddresses()]
+                )
+                return
 
-                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:
+                    yield None
+                    return
+                else:
+                    yield caldavxml.ScheduleInboxURL(davxml.HRef(url))
+                    return
 
-                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:
+                    yield None
+                    return
+                else:
+                    yield caldavxml.ScheduleOutboxURL(davxml.HRef(url))
+                    return
 
-            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:
+                    yield None
+                    return
+                else:
+                    yield customxml.DropBoxHomeURL(davxml.HRef(url))
+                    return
 
-            return super(CalendarPrincipalResource, self).readProperty(property, request)
+            elif name == "calendar-proxy-read-for":
+                d = waitForDeferred(self.proxyFor(False))
+                yield d
+                results = d.getResult()
+                yield customxml.CalendarProxyReadFor(
+                    *[davxml.HRef(principal.principalURL()) for principal in results]
+                )
+                return
 
-        return maybeDeferred(defer)
+            elif name == "calendar-proxy-write-for":
+                d = waitForDeferred(self.proxyFor(True))
+                yield d
+                results = d.getResult()
+                yield customxml.CalendarProxyWriteFor(
+                    *[davxml.HRef(principal.principalURL()) for principal in results]
+                )
+                return
 
+        d = waitForDeferred(super(CalendarPrincipalResource, self).readProperty(property, request))
+        yield d
+        yield d.getResult()
+
     def groupMembers(self):
         return succeed(())
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20081128/c19b5c6e/attachment-0001.html>


More information about the calendarserver-changes mailing list