[CalendarServer-changes] [382] CalendarServer/trunk/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Mon Nov 6 19:32:18 PST 2006


Revision: 382
          http://trac.macosforge.org/projects/calendarserver/changeset/382
Author:   wsanchez at apple.com
Date:     2006-11-06 19:32:18 -0800 (Mon, 06 Nov 2006)

Log Message:
-----------
Revert 377, 378, and 379; meant to go on a branch.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/directory/cred.py
    CalendarServer/trunk/twistedcaldav/directory/resource.py
    CalendarServer/trunk/twistedcaldav/repository.py

Modified: CalendarServer/trunk/twistedcaldav/directory/cred.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/cred.py	2006-11-07 03:31:14 UTC (rev 381)
+++ CalendarServer/trunk/twistedcaldav/directory/cred.py	2006-11-07 03:32:18 UTC (rev 382)
@@ -25,33 +25,55 @@
 ]
 
 from twisted.internet.defer import succeed
+from twisted.cred.credentials import UsernamePassword
 from twisted.cred.error import UnauthorizedLogin
 from twisted.web2.dav.auth import IPrincipalCredentials
 from twisted.web2.dav.auth import TwistedPropertyChecker
 
+import opendirectory
+
 from twistedcaldav import customxml
 
 class DirectoryCredentialsChecker (TwistedPropertyChecker):
-    def __init__(self, service):
-        """
-        @param service: an L{IDirectoryService} provider.
-        """
-        self.service = service
 
     def requestAvatarId(self, credentials):
+
         # If there is no calendar principal URI then the calendar user is disabled.
-        credentials = IPrincipalCredentials(credentials)
-        if not credentials.authnPrincipal.hasDeadProperty(customxml.TwistedCalendarPrincipalURI):
+        pcreds = IPrincipalCredentials(credentials)
+        if not pcreds.authnPrincipal.hasDeadProperty(customxml.TwistedCalendarPrincipalURI):
             # Try regular password check
             return TwistedPropertyChecker.requestAvatarId(self, credentials)
 
-        user = self.service.userWithShortName(credentials.credentials.username)
-        raise UnauthorizedLogin("Unknown credentials type for principal: %s" % (credentials.authnURI,))
+        creds = pcreds.credentials
+        if isinstance(creds, UsernamePassword):
+            user = creds.username
+            pswd = creds.password
+            if opendirectory.authenticateUser(pcreds.authnPrincipal.directory(), user, pswd):
+                return succeed((pcreds.authnURI, pcreds.authzURI,))
+        
+        raise UnauthorizedLogin("Bad credentials for: %s" % (pcreds.authnURI,))
 
-        if not user:
-            raise UnauthorizedLogin("No such user: %s" % (user,))
-
-        if user.authenticate(credentials.credentials):
-            return succeed((credentials.authnURI, credentials.authzURI))
-        else:
-            raise UnauthorizedLogin("Incorrect credentials for user: %s" % (user,)) 
+#class DirectoryCredentialsChecker (TwistedPropertyChecker):
+#    def __init__(self, service):
+#        """
+#        @param service: an L{IDirectoryService} provider.
+#        """
+#        self.service = service
+#
+#    def requestAvatarId(self, credentials):
+#        # If there is no calendar principal URI then the calendar user is disabled.
+#        credentials = IPrincipalCredentials(credentials)
+#        if not credentials.authnPrincipal.hasDeadProperty(customxml.TwistedCalendarPrincipalURI):
+#            # Try regular password check
+#            return TwistedPropertyChecker.requestAvatarId(self, credentials)
+#
+#        user = self.service.userWithShortName(credentials.credentials.username)
+#        raise UnauthorizedLogin("Unknown credentials type for principal: %s" % (credentials.authnURI,))
+#
+#        if not user:
+#            raise UnauthorizedLogin("No such user: %s" % (user,))
+#
+#        if user.authenticate(credentials.credentials):
+#            return succeed((credentials.authnURI, credentials.authzURI))
+#        else:
+#            raise UnauthorizedLogin("Incorrect credentials for user: %s" % (user,)) 

Modified: CalendarServer/trunk/twistedcaldav/directory/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/resource.py	2006-11-07 03:31:14 UTC (rev 381)
+++ CalendarServer/trunk/twistedcaldav/directory/resource.py	2006-11-07 03:32:18 UTC (rev 382)
@@ -21,133 +21,962 @@
 """
 
 __all__ = [
+    "DirectoryPrincipalFile",
+    "DirectoryUserPrincipalProvisioningResource",
+    "DirectoryGroupPrincipalProvisioningResource",
+    "DirectoryResourcePrincipalProvisioningResource",
     "DirectoryPrincipalProvisioningResource",
-    "DirectoryPrincipalTypeResource",
-    "DirectoryPrincipalResource",
 ]
 
 from twisted.python import log
+from twisted.internet import reactor
+from twisted.internet import task
+from twisted.internet.defer import succeed
+from twisted.cred import credentials
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
 from twisted.web2.dav.static import DAVFile
 from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError
+from twisted.web2.http import StatusResponse
 
-from twistedcaldav.extensions import ReadOnlyResourceMixIn
+from twistedcaldav import caldavxml
+from twistedcaldav import customxml
+from twistedcaldav.principalindex import GroupIndex
+from twistedcaldav.principalindex import ResourceIndex
+from twistedcaldav.principalindex import UserIndex
 from twistedcaldav.resource import CalendarPrincipalCollectionResource
 from twistedcaldav.static import CalendarPrincipalFile
-from twistedcaldav.directory.idirectory import IDirectoryService
 
-# FIXME: These should be tied to DAVFile
+import dsattributes
+import opendirectory
+import os
+import unicodedata
 
-class DirectoryPrincipalProvisioningResource (ReadOnlyResourceMixIn, CalendarPrincipalCollectionResource, DAVFile):
+class DirectoryPrincipalFile (CalendarPrincipalFile):
     """
-    Collection resource which provisions directory principals as its children.
+    Directory principal resource.
     """
-    def __init__(self, path, url, directory):
+    def __init__(self, parent, path, url):
         """
         @param path: the path to the file which will back the resource.
-        @param url: the canonical URL for the resource.
+        @param url: the primary URL for the resource.  This is the url which
+            will be returned by L{principalURL}.
         """
+        super(DirectoryPrincipalFile, self).__init__(path, url)
+
+        self._parent = parent
+
+    def checkCredentials(self, creds):
+        """
+        Check whether the provided credentials can be used to authenticate this prinicpal.
+        
+        @param creds: the L{ICredentials} for testing.
+        @return:      True if the credentials match, False otherwise
+        """
+
+        # If there is no calendar principal URI then the calendar user is disabled.
+        if not self.hasDeadProperty(customxml.TwistedCalendarPrincipalURI):
+            return False
+
+        if isinstance(creds, credentials.UsernamePassword):
+            return opendirectory.authenticateUser(self._parent.directory, self.fp.basename(), creds.password)
+        else:
+            return False
+
+    def directory(self):
+        """
+        Get the directory object used for directory operations.
+        
+        @return:      C{object} for the directory instance
+        """
+
+        return self._parent.directory
+
+    def groupMembers(self):
+        """
+        See L{IDAVPrincipalResource.groupMembers}.
+        """
+        
+        # Check for the list of group member GUIDs
+        if self.hasDeadProperty(customxml.TwistedGroupMemberGUIDs):
+            # Get the list of GUIDs from the WebDAV private property
+            memberguids = self.readDeadProperty(customxml.TwistedGroupMemberGUIDs())
+            guids = [str(e) for e in memberguids.children]
+            
+            # Try to find each GUID in collections
+            result = []
+            for guid in guids:
+                uri = DirectoryTypePrincipalProvisioningResource.findAnyGUID(guid)
+                if uri is not None:
+                    result.append(uri)
+            
+            return result            
+
+        return ()
+
+    def groupMemberships(self):
+        """
+        See L{IDAVPrincipalResource.groupMemberships}.
+        """
+        
+        # Find any groups that match this user's GUID
+        guid = self.getGUID()
+        return DirectoryTypePrincipalProvisioningResource.findAnyGroupGUID(guid)
+
+    def getPropertyValue(self, cls):
+        """
+        Get the requested proeprty value or return empty string.
+        """
+        
+        if self.hasDeadProperty(cls()):
+            prop = self.readDeadProperty(cls())
+            return str(prop)
+        else:
+            return ""
+    
+    def setPropertyValue(self, str, cls):
+        """
+        Set the requested property value or remove it if the value is empty.
+        """
+
+        if str:
+            self.writeDeadProperty(cls.fromString(str))
+        else:
+            self.removeDeadProperty(cls())
+
+    def getGUID(self):
+        return self.getPropertyValue(customxml.TwistedGUIDProperty)
+    
+    def readProperty(self, property, request):
+        if type(property) is tuple:
+            qname = property
+        else:
+            qname = property.qname()
+
+        namespace, name = qname
+
+        if namespace == caldavxml.caldav_namespace:
+            if name == "calendar-user-address-set":
+                return succeed(caldavxml.CalendarUserAddressSet(
+                    *[davxml.HRef().fromString(uri) for uri in self.calendarUserAddresses()]
+                ))
+
+        return super(DirectoryPrincipalFile, self).readProperty(qname, request)
+
+    def writeProperty(self, property, request):
+        # This resource is read-only.
+        raise HTTPError(StatusResponse(
+            responsecode.FORBIDDEN,
+            "Protected property %s may not be set." % (property.sname(),)
+        ))
+
+    def calendarUserAddresses(self):
+        # Must have a valid calendar principal uri
+        if self.hasDeadProperty(customxml.TwistedCalendarPrincipalURI):
+            return (self.getPropertyValue(customxml.TwistedCalendarPrincipalURI),)
+        else:
+            # If there is no calendar principal URI then the calendar user is disabled so do not provide
+            # a valid calendar address.
+            return ()
+
+    def matchesCalendarUserAddress(self, request, address):
+        # By default we will always allow either a relative or absolute URI to the principal to
+        # be supplied as a valid calendar user address.
+
+        # Try calendar principal URI
+        return self.hasDeadProperty(customxml.TwistedCalendarPrincipalURI) and (self.getPropertyValue(customxml.TwistedCalendarPrincipalURI) == address)
+
+    def enable(self, calhome, enable):
+        """
+        Enable or disable this principal and access to any calendars owned by it.
+        
+        @param calhome: L{DAVFile} for the container of the calendar home of this user.
+        @param enable: C{True} to enable, C{False} to disable.
+        """
+        # Get home collection resource
+        calrsrc = calhome.getChild(self.principalUID())
+
+        # Handle access for the calendar home
+        if enable:
+            calrsrc.disable(False)
+        else:
+            calrsrc.disable(True)
+
+    def remove(self, calhome):
+        """
+        Remove this principal by hiding (archiving) any calendars owned by it. This is done
+        by turning on the disabling and renaming the calendar home to ensure a future user
+        with the same id won't see the old calendars.
+        
+        @param calhome: L{DAVFile} for the container of the calendar home of this user.
+        """
+        
+        # Get home collection resource
+        calrsrc = calhome.getChild(self.principalUID())
+
+        # Disable access for the calendar home
+        calrsrc.disable(True)
+        
+        # Rename the calendar home to the original name with the GUID appended
+        newname = self.principalUID() + "-" + self.getPropertyValue(customxml.TwistedGUIDProperty)
+        
+        try:
+            # Make sure the new name is not already in use
+            if os.path.exists(newname):
+                count = 1
+                tempname = newname + "-%d"
+                while(os.path.exists(tempname % count)):
+                    count += 1
+                newname = tempname % count 
+            os.rename(calrsrc.fp.path, calrsrc.fp.sibling(newname).path)
+        except OSError:
+            log.msg("Directory: Failed to rename %s to %s when deleting a principal" %
+                    (calrsrc.fp.path, calrsrc.fp.sibling(newname).path))
+            
+            # Remove the disabled property to prevent lock out in the future
+            calrsrc.disable(False)
+
+class DirectoryTypePrincipalProvisioningResource (CalendarPrincipalCollectionResource, DAVFile):
+    """
+    L{DAVFile} resource which provisions user L{CalendarPrincipalFile} resources
+    as needed.
+    
+    This includes a periodic task for refreshing the cached data.
+    """
+    periodicSyncIntervalSeconds = 60.0
+    
+    typeUnknown  = 0
+    typeUser     = 1
+    typeGroup    = 2
+    typeResource = 3
+
+    def __init__(self, path, url):
+        """
+        @param path: the path to the file which will back the resource.
+        @param url: the primary URL for the resource.  Provisioned child
+            resources will use a URL based on C{url} as their primary URLs.
+        @param directory: the reference to the directory to use
+        """
         CalendarPrincipalCollectionResource.__init__(self, url)
         DAVFile.__init__(self, path)
+        self.directory = None
+        self.calendarhomeroot = None
+        self.index = None
+        self.type = DirectoryTypePrincipalProvisioningResource.typeUnknown
 
-        self.directory = IDirectoryService(directory)
+    def setup(self, directory):
+        self.directory = directory
 
-    def createSimilarFile(self, path):
-        raise AssertionError("Not allowed.")
-
-    # FIXME: Remove
     def initialize(self, homeuri, home):
-        log.msg("*** Get rid of initialize() ***")
+        """
+        May be called during repository account initialization.
+         
+        @param homeuri: C{str} uri of the calendar home root.
+        @param home: L{DAVFile} of the calendar home root.
+        """
+        
+        # Make sure index is valid and sync with directory
+        self.index.check()
+        self.calendarhomeroot = (homeuri, home)
 
-    def getChild(self, name):
-        if name == "":
-            return self
+        #
+        # There is a problem with the interaction of Directory Services and the
+        # fork/fork process the server goes through to daemonize itself. For some
+        # resaon, if DS is used before the fork, then calls to it afterwards all
+        # return eServerSendError (-14740) errors.
+        # 
+        # To get around this we must not use opendirectory module calls here, as this
+        # method gets run before the fork/fork. So instead, we schedule a sync
+        # operation to occur one second after the reactor starts up - which is after
+        # the fork/fork. The problem with this is that the http server is already up
+        # and running at that point BEFORE any initially provisioning is done.
+        #
 
-        if name not in self.listChildren():
-            return None
+        # Create a periodic sync operation to keep the cached user list
+        # in sync with the directory server.
+        def periodicSync(self):
+            self.syncNames()
+        
+        # Add initial sync operation
+        reactor.callLater(1.0, periodicSync, self) #@UndefinedVariable
 
+        # Add periodic sync operations
+        runner = task.LoopingCall(periodicSync, self)
+        runner.start(DirectoryTypePrincipalProvisioningResource.periodicSyncIntervalSeconds, now=False)
+    
+    def listNames(self):
+        """
+        List all the names currently in the directory.
+
+        @return: C{list} containing C{str}'s for each name found, or C{None} if failed.
+        """
+        raise NotImplementedError
+
+    def listIndexAttributes(self):
+        """
+        List all the names currently in the directory with specific attributes needed for indexing.
+
+        @return: C{list} containing C{tuple}'s of C{str}'s for each entry found, or C{None} if failed.
+            The C{tuple} elements are: uid, guid, last-modified.
+        """
+        raise NotImplementedError
+
+    def listCommonAttributes(self, names):
+        """
+        List specified names currently in the directory returning useful attributes.
+
+        @param names: C{list} of record entries to list attributes for.
+        @return: C{dict} with keys for each entry found, and a C{dict} value containg the attributes,
+            or C{None} on failure.
+        """
+        raise NotImplementedError
+
+    def validName(self, name):
+        """
+        Verify that the supplied name exists as an entry in the directory and that the
+        name corresponds to one that can use the calendar.
+
+        @param name: C{str} of the name to check.
+        @return: C{True} if the name exists, C{False} otherwise.
+        """
+        raise NotImplementedError
+
+    def directoryAttributes(self, name):
+        """
+        Return the attributes relevant to the directory entry.
+        
+        @param name: C{str} containing the name for the directory entry.
+        @return: C{dict} containing the attribute key/value map.
+        """
+        raise NotImplementedError
+
+    def syncNames(self):
+        """
+        Synchronize the data in the directory with the local cache of resources in the file system.
+        """
+        #log.msg("Directory: Synchronizing cache for %s" % (self.getTitle(),))
+
+        # Get index entries from directory and from cache
+        remoteindex = self.listIndexAttributes()
+        localindex = self.index.listIndex()
+
+        # Create dicts indexed by GUID for each
+        remotedict = {}
+        for entry in remoteindex:
+            remotedict[entry[dsattributes.indexGUID]] = entry
+        localdict = {}
+        for entry in localindex:
+            localdict[entry[dsattributes.indexGUID]] = entry
+
+        # Extract list of GUIDs in each
+        remoteguids = [entry[dsattributes.indexGUID] for entry in remoteindex]
+        localguids = [entry[dsattributes.indexGUID] for entry in localindex]
+
+        remoteguidset = set(remoteguids)
+        remoteguids = None
+        localguidset = set(localguids)
+        localguids = None
+        
+        new_remote = list(remoteguidset.difference(localguidset))
+        removed_remote = list(localguidset.difference(remoteguidset))
+        
+        # Remove old principals
+        old_names = [localdict[guid][dsattributes.indexUID] for guid in removed_remote]
+        old_names.sort()
+        for name in old_names:
+            self.removePrincipal(name, True)
+
+        # Get new ones (but only those with a CalendarPrincipalURI attribute)
+        new_names = [remotedict[guid][dsattributes.indexUID] for guid in new_remote if remotedict[guid][dsattributes.indexCalendarPrincipalURI]]
+
+        # Get all the directory entries for the new ones in one go for better performance
+        if new_names:
+            new_entries = self.listCommonAttributes(new_names)
+            new_names = [n for n in new_entries.iterkeys()]
+            new_names.sort()
+            for name in new_names:
+                self.addPrincipal(name, attrs=new_entries[name], fast=True)
+            
+        # Look for changes in entries
+        common_entries = list(remoteguidset.intersection(localguidset))
+        for guid in common_entries:
+            old = localdict[guid]
+            new = remotedict[guid]
+            if old != new:
+                # Special case issue with unicode normalization of names
+                if ((old[dsattributes.indexUID] != new [dsattributes.indexUID]) and
+                    (unicodedata.normalize("NFKC", old[dsattributes.indexUID].decode("UTF-8")) ==
+                     unicodedata.normalize("NFKC", new[dsattributes.indexUID].decode("UTF-8"))) and
+                    (old[1:] == new[1:])):
+                    continue
+                
+                self.changedPrincipal(old, new)
+        
+        # Commit index after all changes are done
+        self.index.commit()
+
+    def addPrincipal(self, name, attrs=None, fast=False):
+        """
+        Add a new principal resource to the server.
+        
+        @param name: C{str} containing the name of the resource to add.
+        @param attrs: C{dict} directory attributes for this name, or C{None} if attributes need to be read in.
+        @param fast: if C{True} then final commit is not done, if C{False} commit is done.
+        """
+        # This will create it
         child_fp = self.fp.child(name)
+        assert not child_fp.exists()
+
+        assert self.exists()
+        assert self.isCollection()
+
+        child_fp.open("w").close()
+        
+        # Now update the principal's cached data
+        self.updatePrincipal(name, attrs=attrs, fast=fast, new=True, nolog=True)
+        
+        log.msg("Directory: Add %s to %s" % (name, self.getTitle()))
+    
+    def changedPrincipal(self, old, new, fast=False):
+        """
+        Change a new principal resource to sync with directory.
+        
+        @param old: C{str} containing the name of the original resource.
+        @param new: C{str} containing the name of the new resource.
+        @param fast: if C{True} then final commit is not done, if C{False} commit is done.
+        """
+        # Look for change to calendar enabled state
+        
+        # See if the name changed because that is a real pain!
+        if ((old[dsattributes.indexUID] != new[dsattributes.indexUID]) and
+            (unicodedata.normalize("NFKC", old[dsattributes.indexUID].decode("UTF-8")) ==
+             unicodedata.normalize("NFKC", new[dsattributes.indexUID].decode("UTF-8")))):
+            self.renamePrincipal(old[dsattributes.indexUID], new[dsattributes.indexUID])
+        
+        # See if change in enable state
+        enable_state = old[dsattributes.indexCalendarPrincipalURI] != new[dsattributes.indexCalendarPrincipalURI]
+        
+        # Do update (no log message if enable state is being changed as that will generate a log message itself)
+        self.updatePrincipal(new[dsattributes.indexUID], nolog=enable_state)
+
+        # Look for change in calendar enable state
+        if enable_state:
+            self.enablePrincipal(new[dsattributes.indexUID], len(new[dsattributes.indexCalendarPrincipalURI]) != 0)
+
+    def renamePrincipal(self, old, new):
+        """
+        Change a principal resource name to sync with directory.
+        
+        @param old: C{str} containing the name of the original resource.
+        @param new: C{str} containing the name of the new resource.
+        """
+        log.msg("Directory: Renamed Principal %s to %s in %s" % (old, new, self.getTitle()))
+        raise NotImplementedError
+    
+    def updatePrincipal(self, name, attrs = None, fast=False, new=False, nolog=False):
+        """
+        Update details about the named principal in the principal's own property store
+        and the principal collection index.
+        
+        @param name: C{str} containing the principal name to update.
+        @param attrs: C{dict} directory attributes for this name, or C{None} if attributes need to be read in.
+        @param fast: if C{True} then final commit is not done, if C{False} commit is done.
+        @param new: C{True} when this update is the result of adding a new principal,
+            C{False} otherwise.
+        """
+        # Get attributes from directory
+        if attrs is None:
+            attrs = self.directoryAttributes(name)
+        realname = attrs.get(dsattributes.attrRealName, None)
+        guid = attrs.get(dsattributes.attrGUID, None)
+        lastModified = attrs.get(dsattributes.attrLastModified, None)
+        principalurl = attrs.get(dsattributes.attrCalendarPrincipalURI, None)
+
+        # Do provisioning (if the principal is a resource we will turn on the auto-provisioning option)
+        principal = self.getChild(name)
+        if principal is None:
+            log.msg("Directory: Failed to update missing principal: %s in %s" % (name, self.getTitle()))
+            return
+
+        if (new):
+            principal.provisionCalendarAccount(name, None, True, None, self.calendarhomeroot,
+                                               None, ["calendar"], self.type == DirectoryTypePrincipalProvisioningResource.typeResource)
+        
+        # Add directory specific attributes to principal
+        principal.setPropertyValue(realname, davxml.DisplayName)
+        principal.setPropertyValue(guid, customxml.TwistedGUIDProperty)
+        principal.setPropertyValue(lastModified, customxml.TwistedLastModifiedProperty)
+        principal.setPropertyValue(principalurl, customxml.TwistedCalendarPrincipalURI)
+        
+        # Special for group
+        if self.type == DirectoryTypePrincipalProvisioningResource.typeGroup:
+            # Get comma separated list of members and split into a list
+            groupmembers = attrs.get(dsattributes.attrGroupMembers, None)
+            if isinstance(groupmembers, list):
+                members = groupmembers
+            elif isinstance(groupmembers, str):
+                members = [groupmembers]
+            else:
+                members = []
+            
+            # Create and write the group property
+            children = [customxml.TwistedGUIDProperty.fromString(s) for s in members]
+            principal.writeDeadProperty(customxml.TwistedGroupMemberGUIDs(*children))
+        
+        # Do index
+        self.index.addPrincipal(name, principal, fast)
+
+        if not nolog:
+            log.msg("Directory: Updated %s in %s" % (name, self.getTitle()))
+    
+    def enablePrincipal(self, name, enable):
+        """
+        Enable or disable calendar access for this principal.
+        
+        @param enable: C{True} to enable, C{False} to disable
+        """
+        principal = self.getChild(name)
+        if principal is None:
+            log.msg("Directory: Failed to enable/disable missing principal: %s in %s" % (name, self.getTitle()))
+            return
+
+        if enable:
+            principal.enable(self.calendarhomeroot[1], True)
+            log.msg("Directory: Enabled %s in %s" % (name, self.getTitle()))
+        else:
+            principal.enable(self.calendarhomeroot[1], False)
+            log.msg("Directory: Disabled %s in %s" % (name, self.getTitle()))
+
+    def removePrincipal(self, name, fast=False):
+        """
+        Remove a principal from the cached resources.
+        
+        @param name: C{str} containing the name of the principal to remove.
+        @param fast: if C{True} then final commit is not done, if C{False} commit is done.
+        """
+        
+        # Get the principal to 'hide' its calendars
+        principal = self.getChild(name)
+        if principal is None:
+            log.msg("Directory: Failed to remove missing principal: %s in %s" % (name, self.getTitle()))
+        else:
+            principal.remove(self.calendarhomeroot[1])
+
+        # Now remove the principal resource itself
+        child_fp = self.fp.child(name)
         if child_fp.exists():
-            assert child_fp.isdir()
+            os.remove(child_fp.path)
+
+        # Do index
+        self.index.deleteName(name, fast)
+
+        log.msg("Directory: Delete %s from %s" % (name, self.getTitle()))
+    
+    @classmethod
+    def findAnyGUID(clazz, guid):
+        """
+        Find the principal associated with the specified GUID.
+
+        @param guid: the C{str} containing the GUID to match.
+        @return: C{str} with matching principal URI, or C{None}
+        """
+        for url in clazz._principleCollectionSet.keys():
+            try:
+                pcollection = clazz._principleCollectionSet[url]
+                if isinstance(pcollection, DirectoryTypePrincipalProvisioningResource):
+                    principal = pcollection.findGUID(guid)
+                    if principal is not None:
+                        return principal
+            except ReferenceError:
+                pass
+
+        return None
+
+    def findGUID(self, guid):
+        """
+        See if a principal with the specified GUID exists and if so return its principal URI.
+        
+        @param guid: the C{str} containing the GUID to match.
+        @return: C{str} with matching principal URI, or C{None}
+        """
+        name = self.index.nameFromGUID(guid)
+        if name is not None:
+            principal = self.getChild(name)
+            return principal.principalURL()
         else:
-            assert self.exists()
-            assert self.isCollection()
+            return None
 
-            child_fp.makedirs()
+    @classmethod
+    def findAnyGroupGUID(clazz, guid):
+        """
+        Find the principals containing the specified GUID as a group member.
 
-        return DirectoryPrincipalTypeResource(child_fp.path, self, name)
+        @param guid: the C{str} containing the GUID to match.
+        @return: C{list} with matching principal URIs
+        """
+        
+        result = []
+        for url in clazz._principleCollectionSet.keys():
+            try:
+                pcollection = clazz._principleCollectionSet[url]
+                if isinstance(pcollection, DirectoryTypePrincipalProvisioningResource):
+                    result.extend(pcollection.findGroupGUID(guid))
+            except ReferenceError:
+                pass
 
-    def listChildren(self):
-        return self.directory.recordTypes()
+        return result
 
-class DirectoryPrincipalTypeResource (ReadOnlyResourceMixIn, CalendarPrincipalCollectionResource, DAVFile):
-    """
-    Collection resource which provisions directory principals of a specific type as its children.
-    """
-    def __init__(self, path, parent, name):
-        CalendarPrincipalCollectionResource.__init__(self, joinURL(parent.principalCollectionURL(), name))
-        DAVFile.__init__(self, path)
+    def findGroupGUID(self, guid):
+        """
+        Find principals with the specified GUID as a group member.
+        
+        @param guid: the C{str} containing the GUID to match.
+        @return: C{list} with matching principal URIs
+        """
+        # Only both for group collections
+        if self.type != DirectoryTypePrincipalProvisioningResource.typeGroup:
+            return []
+        
+        result = []
+        for name in self.listChildren():
+            principal = self.getChild(name)
+            if principal.hasDeadProperty(customxml.TwistedGroupMemberGUIDs):
+                guids = principal.readDeadProperty(customxml.TwistedGroupMemberGUIDs)
+                for g in guids.children:
+                    if str(g) == guid:
+                        result.append(principal.principalURL())
+                        break
 
-        self.directory = parent.directory
-        self.recordType = name
+        return result
 
-    def createSimilarFile(self, path):
-        raise AssertionError("Not allowed.")
+    def isCollection(self):
+        """
+        See L{IDAVResource.isCollection}.
+        """
+        return True
 
     def getChild(self, name):
+        """
+        Look up a child resource.
+        @return: the child of this resource with the given name.
+        """
         if name == "":
             return self
 
-        record = self.directory.recordWithShortName(self.recordType, name)
-        if record is None:
-            return None
+        child = self.putChildren.get(name, None)
+        if child: return child
 
         child_fp = self.fp.child(name)
         if child_fp.exists():
-            assert child_fp.isfile()
+            return DirectoryPrincipalFile(self, child_fp.path, joinURL(self._url, name))
         else:
-            assert self.exists()
-            assert self.isCollection()
+            return None
 
-            child_fp.open("w").close()
+    def principalSearchPropertySet(self):
+        """
+        See L{IDAVResource.principalSearchPropertySet}.        
+        """
+        return davxml.PrincipalSearchPropertySet(
+            davxml.PrincipalSearchProperty(
+                davxml.PropertyContainer(
+                    davxml.DisplayName()
+                ),
+                davxml.Description(
+                    davxml.PCDATAElement("Display Name"),
+                    **{"xml:lang":"en"}
+                ),
+            ),
+            davxml.PrincipalSearchProperty(
+                davxml.PropertyContainer(
+                    caldavxml.CalendarUserAddressSet()
+                ),
+                davxml.Description(
+                    davxml.PCDATAElement("Calendar User Addresses"),
+                    **{"xml:lang":"en"}
+                ),
+            ),
+        )
 
-        return DirectoryPrincipalResource(child_fp.path, self, name)
+    def createSimilarFile(self, path):
+        if path == self.fp.path:
+            return self
+        else:
+            # TODO: Fix this - not sure how to get URI for second argument of __init__
+            return CalendarPrincipalFile(path, "")
 
-    def listChildren(self):
-        return self.directory.listRecords(self.recordType)
+    def http_PUT        (self, request): return responsecode.FORBIDDEN
+    def http_MKCOL      (self, request): return responsecode.FORBIDDEN
+    def http_MKCALENDAR (self, request): return responsecode.FORBIDDEN
 
-class DirectoryPrincipalResource (ReadOnlyResourceMixIn, CalendarPrincipalFile):
+class DirectoryUserPrincipalProvisioningResource (DirectoryTypePrincipalProvisioningResource):
     """
-    Directory principal resource.
+    L{DAVFile} resource which provisions user L{CalendarPrincipalFile} resources
+    as needed.
     """
-    def __init__(self, path, parent, name):
-        super(DirectoryPrincipalResource, self).__init__(path, parent.principalCollectionURL())
+    def __init__(self, path, url):
+        """
+        @param path: the path to the file which will back the resource.
+        @param url: the primary URL for the resource.  Provisioned child
+            resources will use a URL based on C{url} as their primary URLs.
+        @param directory: the reference to the directory to use
+        """
+        DirectoryTypePrincipalProvisioningResource.__init__(self, path, url)
+        self.index = UserIndex(self)
+        self.type = DirectoryTypePrincipalProvisioningResource.typeUser
 
-        self.directory = parent.directory
-        self.recordType = parent.recordType
-        self.shortName = name
+    def listNames(self):
+        """
+        List all the names currently in the directory.
 
-    ##
-    # ACL
-    ##
+        @return: C{list} containg C{str}'s for each name found, or C{None} if failed.
+        """
 
-    def alternateURIs(self):
-        return ()
+        # Lookup all users
+        return [i[0] for i in opendirectory.listUsers(self.directory)]
+ 
+    def listIndexAttributes(self):
+        """
+        List all the names currently in the directory with specific attributes needed for indexing.
 
-    def groupMembers(self):
-        raise NotImplementedError()
+        @return: C{list} containing C{tuple}'s of C{str}'s for each entry found, or C{None} if failed.
+            The C{tuple} elements are: uid, guid, last-modified.
+        """
+        return opendirectory.listUsers(self.directory)
 
-    def groupMemberships(self):
-        raise NotImplementedError()
+    def listCommonAttributes(self, names):
+        """
+        List specified names currently in the directory returning useful attributes.
 
-    ##
-    # CalDAV
-    ##
+        @param names: C{list} of record entries to list attributes for.
+        @return: C{dict} with keys for each entry found, and a C{dict} value containg the attributes,
+            or C{None} on failure.
+        """
+        return opendirectory.listUsersWithAttributes(self.directory, names)
 
-    def principalUID(self):
-        return self.shortName
+    def validName(self, name):
+        """
+        Verify that the supplied name exists as an entry in the directory.
 
-    def calendarHomeURLs(self):
-        raise NotImplementedError()
+        @param name: C{str} of the namer to check.
+        @return: C{True} if the name exists, C{False} otherwise.
+        """
+        return opendirectory.checkUser(self.directory, name)
 
-    def calendarUserAddresses(self):
-        raise NotImplementedError()
+    def directoryAttributes(self, name):
+        """
+        Return the attributes relevant to the directory entry.
+        
+        @param name: C{str} containing the name for the directory entry.
+        @return: C{dict} containing the attribute key/value map.
+        """
+        result = opendirectory.listUsersWithAttributes(self.directory, [name])
+        if result:
+            return result[name]
+        else:
+            return None
+
+    def getTitle(self):
+        return "User Principals"
+
+class DirectoryGroupPrincipalProvisioningResource (DirectoryTypePrincipalProvisioningResource):
+    """
+    L{DAVFile} resource which provisions user L{CalendarPrincipalFile} resources
+    as needed.
+    """
+    def __init__(self, path, url):
+        """
+        @param path: the path to the file which will back the resource.
+        @param url: the primary URL for the resource.  Provisioned child
+            resources will use a URL based on C{url} as their primary URLs.
+        @param directory: the reference to the directory to use
+        """
+        DirectoryTypePrincipalProvisioningResource.__init__(self, path, url)
+        self.index = GroupIndex(self)
+        self.type = DirectoryTypePrincipalProvisioningResource.typeGroup
+
+    def listNames(self):
+        """
+        List all the names currently in the directory.
+
+        @return: C{list} containg C{str}'s for each name found, or C{None} if failed.
+        """
+
+        # Lookup all users
+        return [i[0] for i in opendirectory.listGroups(self.directory)]
+ 
+    def listIndexAttributes(self):
+        """
+        List all the names currently in the directory with specific attributes needed for indexing.
+
+        @return: C{list} containing C{tuple}'s of C{str}'s for each entry found, or C{None} if failed.
+            The C{tuple} elements are: uid, guid, last-modified.
+        """
+        return opendirectory.listGroups(self.directory)
+
+    def listCommonAttributes(self, names):
+        """
+        List specified names currently in the directory returning useful attributes.
+
+        @param names: C{list} of record entries to list attributes for.
+        @return: C{dict} with keys for each entry found, and a C{dict} value containg the attributes,
+            or C{None} on failure.
+        """
+        return opendirectory.listGroupsWithAttributes(self.directory, names)
+
+    def validName(self, name):
+        """
+        Verify that the supplied name exists as an entry in the directory.
+
+        @param name: C{str} of the namer to check
+        @return: C{True} if the name exists, C{False} otherwise
+        """
+        return opendirectory.checkGroup(self.directory, name)
+
+    def directoryAttributes(self, name):
+        """
+        Return the attributes relevant to the directory entry.
+        
+        @param name: C{str} containing the name for the directory entry.
+        @return: C{dict} containing the attribute key/value map.
+        """
+        result = opendirectory.listGroupsWithAttributes(self.directory, [name])
+        if result:
+            return result[name]
+        else:
+            return None
+
+    def getTitle(self):
+        return "Group Principals"
+
+class DirectoryResourcePrincipalProvisioningResource (DirectoryTypePrincipalProvisioningResource):
+    """
+    L{DAVFile} resource which provisions user L{CalendarPrincipalFile} resources
+    as needed.
+    """
+    def __init__(self, path, url):
+        """
+        @param path: the path to the file which will back the resource.
+        @param url: the primary URL for the resource.  Provisioned child
+            resources will use a URL based on C{url} as their primary URLs.
+        @param directory: the reference to the directory to use
+        """
+        DirectoryTypePrincipalProvisioningResource.__init__(self, path, url)
+        self.index = ResourceIndex(self)
+        self.type = DirectoryTypePrincipalProvisioningResource.typeResource
+
+    def listNames(self):
+        """
+        List all the names currently in the directory.
+
+        @return: C{list} containg C{str}'s for each name found, or C{None} if failed.
+        """
+
+        # Lookup all users
+        return [i[0] for i in opendirectory.listResources(self.directory)]
+ 
+    def listIndexAttributes(self):
+        """
+        List all the names currently in the directory with specific attributes needed for indexing.
+
+        @return: C{list} containing C{tuple}'s of C{str}'s for each entry found, or C{None} if failed.
+            The C{tuple} elements are: uid, guid, last-modified.
+        """
+        return opendirectory.listResources(self.directory)
+
+    def listCommonAttributes(self, names):
+        """
+        List specified names currently in the directory returning useful attributes.
+
+        @param names: C{list} of record entries to list attributes for.
+        @return: C{dict} with keys for each entry found, and a C{dict} value containg the attributes,
+            or C{None} on failure.
+        """
+        return opendirectory.listResourcesWithAttributes(self.directory, names)
+
+    def validName(self, name):
+        """
+        Verify that the supplied name exists as an entry in the directory.
+
+        @param name: C{str} of the namer to check
+        @return: C{True} if the name exists, C{False} otherwise
+        """
+        return opendirectory.checkResource(self.directory, name)
+
+    def directoryAttributes(self, name):
+        """
+        Return the attributes relevant to the directory entry.
+        
+        @param name: C{str} containing the name for the directory entry.
+        @return: C{dict} containing the attribute key/value map.
+        """
+        result = opendirectory.listResourcesWithAttributes(self.directory, [name])
+        if result:
+            return result[name]
+        else:
+            return None
+
+    def getTitle(self):
+        return "Resource Principals"
+
+class DirectoryPrincipalProvisioningResource (DAVFile):
+    """
+    L{DAVFile} resource which provisions calendar principal resources as needed.
+    """
+    def __init__(self, path, url, params={}):
+        """
+        @param path: the path to the file which will back the resource.
+        @param url: the primary URL for the resource.  Provisioned child
+            resources will use a URL based on C{url} as their primary URLs.
+        """
+        super(DirectoryPrincipalProvisioningResource, self).__init__(path)
+
+        assert self.exists(), "%s should exist" % (self,)
+        assert self.isCollection(), "%s should be a collection" % (self,)
+
+        # Extract parameters
+        if (params.has_key("DirectoryNode")):
+            self.directory = opendirectory.odInit(params["DirectoryNode"])
+            if self.directory is None:
+                raise ValueError("Failed to open Open Directory Node: %s" % (params["DirectoryNode"],))
+        else:
+            raise ValueError("DirectoryPrincipalProvisioningResource must be configured with an Open Directory Node")
+
+        # Create children
+        for name, clazz in (
+            ("users" , DirectoryUserPrincipalProvisioningResource),
+            ("groups" , DirectoryGroupPrincipalProvisioningResource),
+            ("resources" , DirectoryResourcePrincipalProvisioningResource),
+        ):
+            child_fp = self.fp.child(name)
+            if not child_fp.exists(): child_fp.makedirs()
+            principalCollection = clazz(child_fp.path, joinURL(url, name) + "/")
+            principalCollection.setup(self.directory)
+            self.putChild(name, principalCollection)
+
+    def isCollection(self):
+        """
+        See L{IDAVResource.isCollection}.
+        """
+        return True
+
+    def initialize(self, homeuri, home):
+        """
+        May be called during repository account initialization.
+        This implementation does nothing.
+        
+        @param homeuri: C{str} uri of the calendar home root.
+        @param home: L{DAVFile} of the calendar home root.
+        """
+        for name in ("users", "groups", "resources",):
+            self.getChild(name).initialize(joinURL(homeuri, name), home.getChild(name))
+    
+    def createSimilarFile(self, path):
+        return DAVFile(path)
+
+    def render(self, request):
+        return StatusResponse(
+            responsecode.OK,
+            "This collection contains principal resources",
+            title=self.displayName()
+        )

Modified: CalendarServer/trunk/twistedcaldav/repository.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/repository.py	2006-11-07 03:31:14 UTC (rev 381)
+++ CalendarServer/trunk/twistedcaldav/repository.py	2006-11-07 03:32:18 UTC (rev 382)
@@ -24,14 +24,6 @@
 
 __all__ = ["RepositoryBuilder"]
 
-import os
-
-from xml.dom.minidom import Element
-from xml.dom.minidom import Text
-import xml.dom.minidom
-
-from zope.interface import implements
-
 from twisted.application.internet import SSLServer, TCPServer
 from twisted.application.service import Application, IServiceCollection, MultiService
 from twisted.cred.portal import Portal
@@ -55,9 +47,13 @@
 from twistedcaldav.resource import CalDAVResource
 from twistedcaldav.static import CalDAVFile, CalendarHomeFile, CalendarPrincipalFile
 from twistedcaldav.directory.cred import DirectoryCredentialsChecker
-from twistedcaldav.directory.idirectory import IDirectoryService
-from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
 
+import os
+
+from xml.dom.minidom import Element
+from xml.dom.minidom import Text
+import xml.dom.minidom
+
 ELEMENT_REPOSITORY = "repository"
 
 ELEMENT_DOCROOT = "docroot"
@@ -110,7 +106,6 @@
 ATTRIBUTE_ENABLE = "enable"
 ATTRIBUTE_ONLYSSL = "onlyssl"
 ATTRIBUTE_CREDENTIALS = "credentials"
-ATTRIBUTE_DIRECTORY_NODE = "node"
 
 ATTRIBUTE_VALUE_PROPERTY = "property"
 ATTRIBUTE_VALUE_DIRECTORY = "directory"
@@ -174,37 +169,13 @@
             MultiService.stopService(self)
             self.logObserver.stop()
     
-    class DirectoryServiceProxy(object):
-        # FIXME: This is a hack to make this config work for now
-        implements(IDirectoryService)
-
-        service = None
-
-        def __getattr__(self, name):
-            attr = getattr(self.service, name)
-
-            if type(attr) is type(self.__getattr__):
-                def m(*args, **kwargs):
-                    return attr(*args, **kwargs)
-                return m
-            else:
-                return attr
-
-        def __setattr__(self, name, value):
-            if name == "service":
-                object.__setattr__(self, name, value)
-            else:
-                raise AttributeError("Attributes are read-only")
-
-    directory = DirectoryServiceProxy()
-
     # Build the server
     builder = RepositoryBuilder(docroot,
                                 doAccounts=doacct,
                                 resetACLs=doacl,
                                 maxsize=maxsize,
                                 quota=quota)
-    builder.buildFromFile(repo, directory)
+    builder.buildFromFile(repo)
     rootresource = builder.docRoot.collection.resource
     
     application = Application("CalDAVServer")
@@ -221,9 +192,7 @@
         portal.registerChecker(auth.TwistedPropertyChecker())
         print "Using property-based password checker."
     elif authenticator.credentials == ATTRIBUTE_VALUE_DIRECTORY:
-        opendirectory = OpenDirectoryService(authenticator.directoryNode)
-        directory.service = opendirectory
-        portal.registerChecker(DirectoryCredentialsChecker(opendirectory))
+        portal.registerChecker(DirectoryCredentialsChecker())
         print "Using directory-based password checker."
     elif authenticator.credentials == ATTRIBUTE_VALUE_KERBEROS:
         if authenticator.type == "basic":
@@ -308,24 +277,24 @@
         if self.quota <= 0:
             self.quota = None
         
-    def buildFromFile(self, filename, directory):
+    def buildFromFile(self, file):
         """
         Parse the required information from an XML file.
         @param file: the path of the XML file to parse.
         """
         # Read in XML
-        fd = open(filename, "r")
+        fd = open(file, "r")
         doc = xml.dom.minidom.parse( fd )
         fd.close()
 
         # Verify that top-level element is correct
         repository_node = doc._get_documentElement()
         if repository_node._get_localName() != ELEMENT_REPOSITORY:
-            self.log("Ignoring file %r because it is not a repository builder file" % (filename,))
+            self.log("Ignoring file \"%s\" because it is not a repository builder file" % (file,))
             return
         self.parseXML(repository_node)
         
-        self.docRoot.build(directory)
+        self.docRoot.build()
         if self.doAccounts:
             self.accounts.provision(
                 self.docRoot.principalCollections,
@@ -381,11 +350,11 @@
                 self.collection.parseXML(child, self)
                 break
 
-    def build(self, directory):
+    def build(self):
         """
         Build the entire repository starting at the root resource.
         """
-        self.collection.build(self.path, "/", directory)
+        self.collection.build(self.path, "/")
         
         # Setup the principal-collection-set property if required
         if self.autoPrincipalCollectionSet:
@@ -407,7 +376,7 @@
     """
     def __init__(self):
         self.name = None
-        self.pytype = None
+        self.pytype = "twistedcaldav.static.CalDAVFile" # FIXME: Why not None?
         self.params = {}
         self.properties = []
         self.acl = None
@@ -489,7 +458,7 @@
                 self.properties.append(Prop())
                 self.properties[-1].parseXML(child)
 
-    def build(self, docroot, urlroot, directory):
+    def build(self, docroot, urlroot):
         """
         Create this collection, initialising any properties and then create any child
         collections.
@@ -513,19 +482,14 @@
         kwargs = {}
         argnames = resource_class.__init__.func_code.co_varnames
         for name, value in (
-            ("path"     , mypath   ),
-            ("url"      , myurl    ),
-            ("directory", directory),
+            ("path", mypath),
+            ("url" , myurl ),
         ):
             if name in argnames:
                 kwargs[name] = value
         if self.params:
             kwargs["params"] = self.params
-        try:
-            self.resource = resource_class(**kwargs)
-        except Exception, e:
-            log.err("Unable to instantiate Python class %r with arguments %r" % (resource_class, kwargs))
-            raise
+        self.resource = resource_class(**kwargs)
 
         self.uri = myurl
         
@@ -538,7 +502,7 @@
             self.resource.setAccessControlList(self.acl.acl)
 
         for member in self.members:
-            child = member.build(mypath, myurl, directory)
+            child = member.build(mypath, myurl)
             # Only putChild if one does not already exists
             if self.resource.putChildren.get(member.name, None) is None:
                 self.resource.putChild(member.name, child)
@@ -732,10 +696,10 @@
                     self.calendarHome.resource,
                 )
 
-#        # Check for proper account home
-#        if not self.accountCollection:
-#            log.err("Accounts cannot be created: no principal collection was marked with an account attribute.")
-#            raise ValueError, "Accounts cannot be created."
+        # Check for proper account home
+        if not self.accountCollection:
+            log.err("Accounts cannot be created: no principal collection was marked with an account attribute.")
+            raise ValueError, "Accounts cannot be created."
 
         # Provision each user
         for repeat, principal in self.items:
@@ -1000,8 +964,6 @@
                 self.onlyssl = node.getAttribute(ATTRIBUTE_ONLYSSL) == ATTRIBUTE_VALUE_YES
             if node.hasAttribute(ATTRIBUTE_CREDENTIALS):
                 self.credentials = node.getAttribute(ATTRIBUTE_CREDENTIALS)
-            if node.hasAttribute(ATTRIBUTE_DIRECTORY_NODE):
-                self.directoryNode = node.getAttribute(ATTRIBUTE_DIRECTORY_NODE)
             for child in node._get_childNodes():
                 if child._get_localName() == ELEMENT_REALM:
                     if child.firstChild is not None:

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


More information about the calendarserver-changes mailing list