[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