[CalendarServer-changes] [6811] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed Jan 26 20:07:57 PST 2011


Revision: 6811
          http://trac.macosforge.org/projects/calendarserver/changeset/6811
Author:   sagen at apple.com
Date:     2011-01-26 20:07:57 -0800 (Wed, 26 Jan 2011)
Log Message:
-----------
Adding LDAP directory implementation

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/platform/darwin/od/setup_directory.py
    CalendarServer/trunk/calendarserver/tools/purge.py
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_livedirectory.py

Modified: CalendarServer/trunk/calendarserver/platform/darwin/od/setup_directory.py
===================================================================
--- CalendarServer/trunk/calendarserver/platform/darwin/od/setup_directory.py	2011-01-26 18:22:17 UTC (rev 6810)
+++ CalendarServer/trunk/calendarserver/platform/darwin/od/setup_directory.py	2011-01-27 04:07:57 UTC (rev 6811)
@@ -94,11 +94,21 @@
 
 masterGroups = [
     (
+        "odtestsubgroupb",
+        {
+            dsattributes.kDS1AttrGeneratedUID : ["6C6CD282-E6E3-11DF-9492-0800200C9A66"],
+            dsattributes.kDS1AttrDistinguishedName : ["OD Test Subgroup B"],
+            dsattributes.kDSNAttrGroupMembers : ["9DC04A72-E6DD-11DF-9492-0800200C9A66"],
+            dsattributes.kDS1AttrPrimaryGroupID : ["33401"],
+        },
+    ),
+    (
         "odtestgrouptop",
         {
             dsattributes.kDS1AttrGeneratedUID : ["6C6CD280-E6E3-11DF-9492-0800200C9A66"],
             dsattributes.kDS1AttrDistinguishedName : ["OD Test Group Top"],
             dsattributes.kDSNAttrGroupMembers : ["9DC04A70-E6DD-11DF-9492-0800200C9A66", "9DC04A71-E6DD-11DF-9492-0800200C9A66"],
+            dsattributes.kDSNAttrNestedGroups : ["6C6CD282-E6E3-11DF-9492-0800200C9A66"],
             dsattributes.kDS1AttrPrimaryGroupID : ["33400"],
         },
     ),
@@ -162,7 +172,7 @@
             dsattributes.kDS1AttrGeneratedUID : ["6C6CD281-E6E3-11DF-9492-0800200C9A66"],
             dsattributes.kDS1AttrDistinguishedName : ["OD Test Subgroup A"],
             dsattributes.kDSNAttrGroupMembers : ["9DC04A74-E6DD-11DF-9492-0800200C9A66", "9DC04A75-E6DD-11DF-9492-0800200C9A66"],
-            dsattributes.kDS1AttrPrimaryGroupID : ["33400"],
+            dsattributes.kDS1AttrPrimaryGroupID : ["33402"],
         },
     ),
 ]

Modified: CalendarServer/trunk/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/purge.py	2011-01-26 18:22:17 UTC (rev 6810)
+++ CalendarServer/trunk/calendarserver/tools/purge.py	2011-01-27 04:07:57 UTC (rev 6811)
@@ -100,7 +100,7 @@
         try:
             rootResource = getRootResource(config, self._store)
             directory = rootResource.getDirectory()
-            total = (yield purgeOldEvents(self._store, directory, rootResource,
+            (yield purgeOldEvents(self._store, directory, rootResource,
                 self.cutoff, self.batchSize, verbose=self.verbose,
                 dryrun=self.dryrun))
         except Exception, e:

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2011-01-26 18:22:17 UTC (rev 6810)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2011-01-27 04:07:57 UTC (rev 6811)
@@ -199,6 +199,93 @@
     </dict>
     -->
 
+    <!--  OpenLDAP Directory Service -->
+    <!--
+    <key>DirectoryService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.ldapdirectory.LdapDirectoryService</string>
+
+      <key>params</key>
+      <dict>
+        <key>restrictEnabledRecords</key>
+        <false/>
+        <key>restrictToGroup</key>
+        <string>odtestgrouptop</string>
+        <key>cacheTimeout</key>
+        <integer>30</integer>
+        <key>uri</key>
+        <string>ldap://example.com/</string>
+        <key>tls</key>
+        <false/>
+        <key>tlsCACertFile</key>
+        <string></string>
+        <key>tlsCACertDir</key>
+        <string></string>
+        <key>tlsRequireCert</key>
+        <string>never</string>
+        <key>credentials</key>
+        <dict>
+          <key>dn</key>
+          <string></string>
+          <key>password</key>
+          <string></string>
+        </dict>
+        <key>authMethod</key>
+        <string>LDAP</string>
+        <key>rdnSchema</key>
+        <dict>
+          <key>base</key>
+          <string>dc=example,dc=com</string>
+          <key>guidAttr</key>
+          <string>apple-generateduid</string>
+          <key>users</key>
+          <dict>
+            <key>rdn</key>
+            <string>cn=users</string>
+            <key>attr</key>
+            <string>uid</string>
+            <key>emailSuffix</key>
+            <string></string>
+            <key>filter</key>
+            <string></string>
+          </dict>
+          <key>groups</key>
+          <dict>
+            <key>rdn</key>
+            <string>cn=groups</string>
+            <key>attr</key>
+            <string>cn</string>
+            <key>emailSuffix</key>
+            <string></string>
+            <key>filter</key>
+            <string></string>
+          </dict>
+          <key>attributeMapping</key>
+          <dict>
+            <key>fullName</key>
+            <string>cn</string>
+            <key>emailAddresses</key>
+            <string>mail</string>
+            <key>firstName</key>
+            <string>givenName</string>
+            <key>lastName</key>
+            <string>sn</string>
+          </dict>
+        </dict>
+        <key>groupSchema</key>
+        <dict>
+          <key>membersAttr</key>
+          <string>apple-group-memberguid</string>
+          <key>nestedGroupsAttr</key>
+          <string>apple-group-nestedgroup</string>
+          <key>memberIdAttr</key>
+          <string>apple-generateduid</string>
+        </dict>
+      </dict>
+    </dict>
+    -->
+
     <!-- Resource and Location Service -->
     <key>ResourceService</key>
     <dict>

Modified: CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2011-01-26 18:22:17 UTC (rev 6810)
+++ CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2011-01-27 04:07:57 UTC (rev 6811)
@@ -54,7 +54,7 @@
         return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.node)
 
 
-    def __init__(self, params, dosetup=True):
+    def __init__(self, params):
         """
         @param params: a dictionary containing the following keys:
             node: an OpenDirectory node name to bind to.
@@ -64,9 +64,6 @@
             restrictToGroup: C{str} guid or name of group used to
               restrict enabled users.
             cacheTimeout: C{int} number of minutes before cache is invalidated.
-        @param dosetup: if C{True} then the directory records are initialized,
-                        if C{False} they are not.
-                        This should only be set to C{False} when doing unit tests.
         """
         defaults = {
             'node' : '/Search',
@@ -105,8 +102,6 @@
         else:
             self.restrictToGUID = True
         self.restrictedTimestamp = 0
-        self._records = {}
-        self._delayedCalls = set()
 
     @property
     def restrictedGUIDs(self):

Added: CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/ldapdirectory.py	2011-01-27 04:07:57 UTC (rev 6811)
@@ -0,0 +1,818 @@
+##
+# Copyright (c) 2008-2009 Aymeric Augustin. All rights reserved.
+# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+LDAP directory service implementation.  Supports principal-property-search
+and restrictToGroup features.
+
+The following attributes from standard schemas are used:
+* Core (RFC 4519):
+    . cn | commonName
+    . givenName
+    . member (if not using NIS groups)
+    . ou
+    . sn | surname
+    . uid | userid (if using NIS groups)
+* COSINE (RFC 4524):
+    . mail
+* InetOrgPerson (RFC 2798):
+    . displayName (if cn is unavailable)
+* NIS (RFC):
+    . gecos (if cn is unavailable)
+    . memberUid (if using NIS groups)
+"""
+
+__all__ = [
+    "LdapDirectoryService",
+]
+
+import ldap
+try:
+    # Note: PAM support is currently untested
+    import PAM
+    pamAvailable = True
+except ImportError:
+    pamAvailable = False
+
+import time
+from twisted.cred.credentials import UsernamePassword
+from twistedcaldav.directory.cachingdirectory import (CachingDirectoryService,
+    CachingDirectoryRecord)
+from twistedcaldav.directory import augment
+from twistedcaldav.directory.directory import DirectoryConfigurationError
+from twisted.internet.defer import succeed
+
+class LdapDirectoryService(CachingDirectoryService):
+    """
+    LDAP based implementation of L{IDirectoryService}.
+    """
+    baseGUID = "5A871574-0C86-44EE-B11B-B9440C3DC4DD"
+
+    def __repr__(self):
+        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName,
+            self.uri)
+
+    def __init__(self, params):
+        """
+        @param params: a dictionary containing the following keys:
+            cacheTimeout, realmName, uri, tls, tlsCACertFile, tlsCACertDir,
+            tlsRequireCert, crendentials, rdnSchema, groupSchema
+        """
+
+        defaults = {
+            "cacheTimeout": 30,
+            "restrictEnabledRecords": False,
+            "restrictToGroup": "",
+            "recordTypes": ("users", "groups"),
+            "uri": "ldap://localhost/",
+            "tls": False,
+            "tlsCACertFile": None,
+            "tlsCACertDir": None,
+            "tlsRequireCert": None, # never, allow, try, demand, hard
+            "credentials": {
+                "dn": None,
+                "password": None,
+            },
+            "authMethod": "LDAP",
+            "rdnSchema": {
+                "base": "dc=example,dc=com",
+                "guidAttr": None,
+                "users": {
+                    "rdn": "ou=People",
+                    "attr": "uid", # used only to synthesize email address
+                    "emailSuffix": None, # used only to synthesize email address
+                    "filter": None, # additional filter for this type
+                },
+                "groups": {
+                    "rdn": "ou=Group",
+                    "attr": "cn", # used only to synthesize email address
+                    "emailSuffix": None, # used only to synthesize email address
+                    "filter": None, # additional filter for this type
+                },
+                "locations": {
+                    "rdn": "ou=Locations",
+                    "attr": "cn", # used only to synthesize email address
+                    "emailSuffix": None, # used only to synthesize email address
+                    "filter": None, # additional filter for this type
+                },
+                "resources": {
+                    "rdn": "ou=Resources",
+                    "attr": "cn", # used only to synthesize email address
+                    "emailSuffix": None, # used only to synthesize email address
+                    "filter": None, # additional filter for this type
+                },
+            },
+            "groupSchema": {
+                "membersAttr": "member", # how members are specified
+                "nestedGroupsAttr": None, # how nested groups are specified
+                "memberIdAttr": None, # which attribute the above refer to
+            },
+            "attributeMapping": { # maps internal record names to LDAP
+                "fullName" : "cn",
+                "emailAddresses" : "mail",
+                "firstName" : "givenName",
+                "lastName" : "sn",
+            },
+        }
+        ignored = None
+        params = self.getParams(params, defaults, ignored)
+
+        self._recordTypes = params["recordTypes"]
+
+        super(LdapDirectoryService, self).__init__(params["cacheTimeout"])
+
+        self.realmName = params["uri"]
+        self.uri = params["uri"]
+        self.tls = params["tls"]
+        self.tlsCACertFile = params["tlsCACertFile"]
+        self.tlsCACertDir = params["tlsCACertDir"]
+        self.tlsRequireCert = params["tlsRequireCert"]
+        self.credentials = params["credentials"]
+        self.authMethod = params["authMethod"]
+        self.rdnSchema = params["rdnSchema"]
+        self.groupSchema = params["groupSchema"]
+        self.attributeMapping = params["attributeMapping"]
+
+        self.base = ldap.dn.str2dn(self.rdnSchema["base"])
+
+        # Certain attributes (such as entryUUID) may be hidden and not
+        # returned by default when queried for all attributes. Therefore it is
+        # necessary to explicitly pass all the possible attributes list
+        # for ldap searches
+        attrSet = set(["mail", "uid", "userid", "cn", "commonName",
+                       "displayName", "gecos", "givenName", "sn", "surname"])
+        if self.rdnSchema["guidAttr"]:
+            attrSet.add(self.rdnSchema["guidAttr"])
+        for recordType in self.recordTypes():
+            if self.rdnSchema[recordType]["attr"]:
+                attrSet.add(self.rdnSchema[recordType]["attr"])
+        if self.groupSchema["membersAttr"]:
+            attrSet.add(self.groupSchema["membersAttr"])
+        if self.groupSchema["nestedGroupsAttr"]:
+            attrSet.add(self.groupSchema["nestedGroupsAttr"])
+        if self.groupSchema["memberIdAttr"]:
+            attrSet.add(self.groupSchema["memberIdAttr"])
+        self.attrList = list(attrSet)
+
+        self.typeRDNs = {}
+        for recordType in self.recordTypes():
+            self.typeRDNs[recordType] = ldap.dn.str2dn(
+                self.rdnSchema[recordType]["rdn"]
+            )
+
+        # Create LDAP connection
+        self.log_info("Connecting to LDAP %s" % (repr(self.uri),))
+
+        self.ldap = self.createLDAPConnection()
+        if self.credentials.get("dn", ""):
+            try:
+                self.log_info("Binding to LDAP %s" %
+                    (repr(self.credentials.get("dn")),))
+                self.ldap.simple_bind_s(self.credentials.get("dn"),
+                    self.credentials.get("password"))
+            except ldap.INVALID_CREDENTIALS:
+                msg = "Can't bind to LDAP %s: check credentials" % (self.uri,)
+                self.log_error(msg)
+                raise DirectoryConfigurationError(msg)
+
+        # Separate LDAP connection used solely for authenticating clients
+        self.authLDAP = None
+
+        # Restricting access by directory group
+        self.restrictEnabledRecords = params['restrictEnabledRecords']
+        self.restrictToGroup = params['restrictToGroup']
+        self.restrictedTimestamp = 0
+
+
+    def recordTypes(self):
+        return self._recordTypes
+
+
+    def createLDAPConnection(self):
+        """
+        Create and configure LDAP connection
+        """
+        cxn = ldap.ldapobject.ReconnectLDAPObject(self.uri)
+
+        if self.tlsCACertFile:
+            cxn.set_option(ldap.OPT_X_TLS_CACERTFILE, self.tlsCACertFile)
+        if self.tlsCACertDir:
+            cxn.set_option(ldap.OPT_X_TLS_CACERTDIR, self.tlsCACertDir)
+
+        if self.tlsRequireCert == "never":
+            cxn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_NEVER)
+        elif self.tlsRequireCert == "allow":
+            cxn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_ALLOW)
+        elif self.tlsRequireCert == "try":
+            cxn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_TRY)
+        elif self.tlsRequireCert == "demand":
+            cxn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND)
+        elif self.tlsRequireCert == "hard":
+            cxn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_HARD)
+
+        if self.tls:
+            cxn.start_tls_s()
+
+        return cxn
+
+
+    def authenticate(self, dn, password):
+        """
+        Perform simple bind auth, raising ldap.INVALID_CREDENTIALS if
+        bad password
+        """
+        if self.authLDAP is None:
+            self.log_debug("Creating authentication connection to LDAP")
+            self.authLDAP = self.createLDAPConnection()
+        self.log_debug("Authenticating %s" % (dn,))
+        self.authLDAP.bind_s(dn, password)
+
+
+    @property
+    def restrictedGUIDs(self):
+        """
+        Look up (and cache) the set of guids that are members of the
+        restrictToGroup.  If restrictToGroup is not set, return None to
+        indicate there are no group restrictions.
+
+        guidAttr must also be specified in config for restrictToGroups to work.
+        """
+        if self.restrictEnabledRecords and self.rdnSchema["guidAttr"]:
+
+            if time.time() - self.restrictedTimestamp > self.cacheTimeout:
+                # fault in the members of group of name self.restrictToGroup
+
+                recordType = self.recordType_groups
+                base = self.typeRDNs[recordType] + self.base
+                filter = "(cn=%s)" % (self.restrictToGroup,)
+                self.log_info("Retrieving ldap record with base %s and filter %s." %
+                    (ldap.dn.dn2str(base), filter))
+                result = self.ldap.search_s(ldap.dn.dn2str(base),
+                    ldap.SCOPE_SUBTREE, filter, self.attrList)
+
+                if len(result) == 1:
+                    dn, attrs = result[0]
+                    if self.groupSchema["membersAttr"]:
+                        members = self._getMultipleLdapAttributes(attrs,
+                            self.groupSchema["membersAttr"])
+                    if self.groupSchema["nestedGroupsAttr"]:
+                        nestedGroups = self._getMultipleLdapAttributes(attrs,
+                            self.groupSchema["nestedGroupsAttr"])
+
+                else:
+                    members = []
+                    nestedGroups = []
+
+                self._cachedRestrictedGUIDs = set(self._expandGroupMembership(members, nestedGroups, returnGroups=True))
+                self.log_debug("Got %d restricted group members" % (len(self._cachedRestrictedGUIDs),))
+                self.restrictedTimestamp = time.time()
+            return self._cachedRestrictedGUIDs
+        else:
+            # No restrictions
+            return None
+
+
+    def _expandGroupMembership(self, members, nestedGroups,
+        processedGUIDs=None, returnGroups=False):
+
+        if processedGUIDs is None:
+            processedGUIDs = set()
+
+        if isinstance(members, str):
+            members = [members]
+
+        if isinstance(nestedGroups, str):
+            nestedGroups = [nestedGroups]
+
+        for memberGUID in members:
+            if memberGUID not in processedGUIDs:
+                processedGUIDs.add(memberGUID)
+                yield memberGUID
+
+        for groupGUID in nestedGroups:
+            if groupGUID in processedGUIDs:
+                continue
+
+            recordType = self.recordType_groups
+            base = self.typeRDNs[recordType] + self.base
+            filter = "(%s=%s)" % (self.rdnSchema["guidAttr"], groupGUID)
+
+            self.log_info("Retrieving ldap record with base %s and filter %s." %
+                (ldap.dn.dn2str(base), filter))
+            result = self.ldap.search_s(ldap.dn.dn2str(base),
+                ldap.SCOPE_SUBTREE, filter, self.attrList)
+
+            if len(result) == 0:
+                continue
+
+            if len(result) == 1:
+                dn, attrs = result[0]
+                if self.groupSchema["membersAttr"]:
+                    subMembers = self._getMultipleLdapAttributes(attrs,
+                        self.groupSchema["membersAttr"])
+                else:
+                    subMembers = []
+
+                if self.groupSchema["nestedGroupsAttr"]:
+                    subNestedGroups = self._getMultipleLdapAttributes(attrs,
+                        self.groupSchema["nestedGroupsAttr"])
+                else:
+                    subNestedGroups = []
+
+            processedGUIDs.add(groupGUID)
+            if returnGroups:
+                yield groupGUID
+
+            for GUID in self._expandGroupMembership(subMembers,
+                subNestedGroups, processedGUIDs, returnGroups):
+                yield GUID
+
+
+    def _getUniqueLdapAttribute(self, attrs, *keys):
+        """
+        Get the first value for one or several attributes
+        Useful when attributes have aliases (e.g. sn vs. surname)
+        """
+        for key in keys:
+            values = attrs.get(key)
+            if values is not None:
+                return values[0]
+        return None
+
+
+    def _getMultipleLdapAttributes(self, attrs, *keys):
+        """
+        Get all values for one or several attributes
+        """
+        results = []
+        for key in keys:
+            values = attrs.get(key)
+            if values is not None:
+                results += values
+        return set(results)
+
+
+    def _ldapResultToRecord(self, dn, attrs, recordType):
+        """
+        Convert the attrs returned by a LDAP search into a LdapDirectoryRecord
+        object.
+
+        Mappings are hardcoded below but the most standard LDAP schemas were
+        used to define them
+        """
+
+        guid = None
+        shortNames = ()
+        authIDs = set()
+        fullName = None
+        firstName = None
+        lastName = None
+        emailAddresses = set()
+        calendarUserAddresses = set()
+        enabledForCalendaring = None
+        uid = None
+
+        # First check for and add guid
+        guidAttr = self.rdnSchema["guidAttr"]
+        if guidAttr:
+            guid = self._getUniqueLdapAttribute(attrs, guidAttr)
+
+        # Find or build email
+        emailAddresses = self._getMultipleLdapAttributes(attrs, "mail")
+        emailSuffix = self.rdnSchema[recordType]["emailSuffix"]
+
+        if len(emailAddresses) == 0 and emailSuffix is not None:
+            emailPrefix = self._getUniqueLdapAttribute(attrs,
+                self.rdnSchema[recordType]["attr"])
+            emailAddresses.add(emailPrefix + emailSuffix)
+
+        # LDAP attribute -> principal matchings
+        if recordType == self.recordType_users:
+            shortNames = (self._getUniqueLdapAttribute(attrs, "uid", "userid"),)
+            fullName = self._getUniqueLdapAttribute(attrs, "cn", "commonName",
+                "displayName", "gecos")
+            firstName = self._getUniqueLdapAttribute(attrs, "givenName")
+            lastName = self._getUniqueLdapAttribute(attrs, "sn", "surname")
+            calendarUserAddresses = emailAddresses
+            enabledForCalendaring = True
+        elif recordType == self.recordType_groups:
+            shortNames = (self._getUniqueLdapAttribute(attrs, "cn"),)
+            fullName = self._getUniqueLdapAttribute(attrs, "cn")
+            enabledForCalendaring = False
+        elif recordType in (self.recordType_resources,
+            self.recordType_locations):
+            shortNames = (self._getUniqueLdapAttribute(attrs, "cn"),)
+            fullName = self._getUniqueLdapAttribute(attrs, "cn")
+            calendarUserAddresses = emailAddresses
+            enabledForCalendaring = True
+
+        record = LdapDirectoryRecord(
+            service                 = self,
+            recordType              = recordType,
+            guid                    = guid,
+            shortNames              = shortNames,
+            authIDs                 = authIDs,
+            fullName                = fullName,
+            firstName               = firstName,
+            lastName                = lastName,
+            emailAddresses          = emailAddresses,
+            calendarUserAddresses   = calendarUserAddresses,
+            enabledForCalendaring   = enabledForCalendaring,
+            uid                     = uid,
+            dn                      = dn,
+            attrs                   = attrs,
+        )
+
+        # Look up augment information
+        # TODO: this needs to be deferred but for now we hard code the
+        # deferred result because we know it is completing immediately.
+        d = augment.AugmentService.getAugmentRecord(record.guid,
+            recordType)
+        d.addCallback(lambda x:record.addAugmentInformation(x))
+
+        return record
+
+
+    def queryDirectory(self, recordTypes, indexType, indexKey):
+        """
+        Queries the LDAP directory for the record which has an attribute value
+        matching the indexType and indexKey parameters.
+
+        recordTypes is a list of record types to limit the search to.
+        indexType specifies one of the CachingDirectoryService contstants
+            identifying which attribute to search on.
+        indexKey is the value to search for.
+
+        Nothing is returned -- the resulting record (if any) is placed in
+        the cache.
+        """
+        self.log_debug("LDAP query for types %s, indexType %s and indexKey %s"
+            % (recordTypes, indexType, indexKey))
+
+        for recordType in recordTypes:
+            # Build base for this record Type
+            base = self.typeRDNs[recordType] + self.base
+
+            # Build filter
+            filter = "(!(objectClass=organizationalUnit))"
+            typeFilter = self.rdnSchema[recordType]["filter"]
+            if typeFilter:
+                filter = "(&%s%s)" % (filter, typeFilter)
+
+            if indexType == self.INDEX_TYPE_GUID:
+                # Query on guid only works if guid attribute has been defined.
+                # Support for query on guid even if is auto-generated should
+                # be added.
+                guidAttr = self.rdnSchema["guidAttr"]
+                if not guidAttr: return
+                filter = "(&%s(%s=%s))" % (filter, guidAttr, indexKey)
+
+            elif indexType == self.INDEX_TYPE_SHORTNAME:
+                if recordType == self.recordType_users:
+                    filter = "(&%s(|(uid=%s)(userid=%s)))" % (
+                        filter, indexKey, indexKey)
+                elif recordType in (self.recordType_groups,
+                    self.recordType_resources, self.recordType_locations):
+                    filter = "(&%s(cn=%s))" % (filter, indexKey)
+
+            elif indexType == self.INDEX_TYPE_CUA:
+                # indexKey is of the form "mailto:test at example.net"
+                email = indexKey[7:] # strip "mailto:"
+                emailSuffix = self.rdnSchema[recordType]["emailSuffix"]
+                if emailSuffix is not None and email.partition("@")[2] == emailSuffix:
+                    filter = "(&%s(|(&(!(mail=*))(%s=%s))(mail=%s)))" % (
+                        filter,
+                        self.rdnSchema[recordType]["attr"],
+                        email.partition("@")[0],
+                        email
+                    )
+                else:
+                    filter = "(&%s(mail=%s))" % (filter, email)
+
+            elif indexType == self.INDEX_TYPE_AUTHID:
+                return
+
+            # Query the LDAP server
+            self.log_info("Retrieving ldap record with base %s and filter %s." %
+                (ldap.dn.dn2str(base), filter))
+            result = self.ldap.search_s(ldap.dn.dn2str(base),
+                ldap.SCOPE_SUBTREE, filter, self.attrList)
+
+            if result:
+                dn, attrs = result.pop()
+
+                unrestricted = True
+                if self.restrictedGUIDs is not None:
+                    guidAttr = self.rdnSchema["guidAttr"]
+                    if guidAttr:
+                        guid = self._getUniqueLdapAttribute(attrs, guidAttr)
+                        if guid not in self.restrictedGUIDs:
+                            unrestricted = False
+
+                record = self._ldapResultToRecord(dn, attrs, recordType)
+                self.log_debug("Got LDAP record %s" % (record,))
+                self.recordCacheForType(recordType).addRecord(record,
+                    indexType, indexKey
+                )
+
+                if not unrestricted:
+                    self.log_debug("%s is not enabled because it's not a member of group: %s" % (guid, self.restrictToGroup))
+                    record.enabledForCalendaring = False
+                    record.enabledForAddressBooks = False
+
+
+    def recordsMatchingFields(self, fields, operand="or", recordType=None):
+        """
+        Carries out the work of a principal-property-search against LDAP
+        Returns a deferred list of directory records.
+        """
+
+        records = []
+
+        recordTypes = [recordType] if recordType else self.recordTypes()
+        for recordType in recordTypes:
+            filter = buildFilter(self.attributeMapping, fields, operand=operand)
+
+            if filter is not None:
+
+                # Query the LDAP server
+                base = self.typeRDNs[recordType] + self.base
+
+                self.log_debug("LDAP search %s %s" %
+                    (ldap.dn.dn2str(base), filter))
+                results = self.ldap.search_s(ldap.dn.dn2str(base),
+                    ldap.SCOPE_SUBTREE, filter, self.attrList)
+
+                for dn, attrs in results:
+                    # Skip if group restriction is in place and guid is not
+                    # a member
+                    if self.restrictedGUIDs is not None:
+                        guidAttr = self.rdnSchema["guidAttr"]
+                        if guidAttr:
+                            guid = self._getUniqueLdapAttribute(attrs, guidAttr)
+                            if guid not in self.restrictedGUIDs:
+                                continue
+
+                    record = self._ldapResultToRecord(dn, attrs, recordType)
+                    records.append(record)
+
+        return succeed(records)
+
+
+def buildFilter(mapping, fields, operand="or"):
+    """
+    Create an LDAP filter string from a list of tuples representing directory
+    attributes to search
+
+    mapping is a dict mapping internal directory attribute names to ldap names.
+    fields is a list of tuples...
+        (directory field name, value to search, caseless (ignored), matchType)
+    ...where matchType is one of "starts-with", "contains", "exact"
+    """
+
+    converted = []
+    for field, value, caseless, matchType in fields:
+        ldapField = mapping.get(field, None)
+        if ldapField:
+            if matchType == "starts-with":
+                value = "%s*" % (value,)
+            elif matchType == "contains":
+                value = "*%s*" % (value,)
+            # otherwise it's an exact match
+            converted.append("(%s=%s)" % (ldapField, value))
+
+    if len(converted) == 0:
+        filter = None
+    elif len(converted) == 1:
+        filter = converted[0]
+    else:
+        operand = ("|" if operand == "or" else "&")
+        filter = "(%s%s)" % (operand, "".join(converted))
+
+    return filter
+
+
+class LdapDirectoryRecord(CachingDirectoryRecord):
+    """
+    LDAP implementation of L{IDirectoryRecord}.
+    """
+    def __init__(
+        self, service, recordType,
+        guid, shortNames, authIDs, fullName,
+        firstName, lastName, emailAddresses,
+        calendarUserAddresses, enabledForCalendaring, uid,
+        dn, attrs
+    ):
+        super(LdapDirectoryRecord, self).__init__(
+            service               = service,
+            recordType            = recordType,
+            guid                  = guid,
+            shortNames            = shortNames,
+            authIDs               = authIDs,
+            fullName              = fullName,
+            firstName             = firstName,
+            lastName              = lastName,
+            emailAddresses        = emailAddresses,
+            calendarUserAddresses = calendarUserAddresses,
+            enabledForCalendaring = enabledForCalendaring,
+            uid                   = uid,
+        )
+
+        # Save attributes of dn and attrs in case you might need them later
+        self.dn = dn
+        self.attrs = attrs
+
+        # Identifiers of the members of this record if it is a group
+        membersAttrs = []
+        if self.service.groupSchema["membersAttr"]:
+            membersAttrs.append(self.service.groupSchema["membersAttr"])
+        if self.service.groupSchema["nestedGroupsAttr"]:
+            membersAttrs.append(self.service.groupSchema["nestedGroupsAttr"])
+        self._memberIds = self.service._getMultipleLdapAttributes(attrs,
+            *membersAttrs)
+
+        # Identifier of this record as a group member
+        memberIdAttr = self.service.groupSchema["memberIdAttr"]
+        if memberIdAttr:
+            self._memberId = self.service._getUniqueLdapAttribute(attrs,
+                memberIdAttr)
+        else:
+            self._memberId = self.dn
+
+
+    def members(self):
+        """ Return the records representing members of this group """
+
+        try:
+            return self._members_storage
+        except AttributeError:
+            self._members_storage = self._members()
+            return self._members_storage
+
+    def _members(self):
+        """ Fault in records for the members of this group """
+
+        memberIdAttr = self.service.groupSchema["memberIdAttr"]
+        results = []
+
+        for memberId in self._memberIds:
+
+            for recordType in self.service.recordTypes():
+
+                if memberIdAttr:
+                    base = self.service.base
+                    filter = "(%s=%s)" % (memberIdAttr, memberId)
+                    self.log_debug("Retrieving subtree of %s with filter %s" %
+                        (ldap.dn.dn2str(base), filter),
+                        system="LdapDirectoryService")
+                    result = self.service.ldap.search_s(ldap.dn.dn2str(base),
+                        ldap.SCOPE_SUBTREE, filter, self.service.attrList)
+
+                else:
+                    self.log_debug("Retrieving %s." % memberId,
+                        system="LdapDirectoryService")
+                    result = self.service.ldap.search_s(memberId,
+                        ldap.SCOPE_BASE, attrlist=self.service.attrList)
+
+                if result:
+                    # TODO: what about duplicates?
+
+                    dn, attrs = result.pop()
+
+                    if recordType == self.service.recordType_users:
+                        shortName = self.service._getUniqueLdapAttribute(attrs,
+                            "uid", "userid")
+                    elif recordType in (
+                        self.service.recordType_groups,
+                        self.service.recordType_resources,
+                        self.service.recordType_locations
+                    ):
+                        shortName = self.service._getUniqueLdapAttribute(attrs,
+                            "cn")
+
+                    record = self.service.recordWithShortName(recordType,
+                        shortName)
+                    if record:
+                        results.append(record)
+                        break
+
+        return results
+
+    def groups(self):
+        """ Return the records representing groups this record is a member of """
+        try:
+            return self._groups_storage
+        except AttributeError:
+            self._groups_storage = self._groups()
+            return self._groups_storage
+
+    def _groups(self):
+        """ Fault in the groups of which this record is a member """
+
+        recordType = self.service.recordType_groups
+        base = self.service.typeRDNs[recordType] + self.service.base
+
+        membersAttrs = []
+        if self.service.groupSchema["membersAttr"]:
+            membersAttrs.append(self.service.groupSchema["membersAttr"])
+        if self.service.groupSchema["nestedGroupsAttr"]:
+            membersAttrs.append(self.service.groupSchema["nestedGroupsAttr"])
+
+        if len(membersAttrs) == 1:
+            filter = "(%s=%s)" % (membersAttrs[0], self._memberId)
+        else:
+            filter = "(|%s)" % ( "".join(
+                    ["(%s=%s)" % (a, self._memberId) for a in membersAttrs]
+                ),
+            )
+        self.log_debug("Finding groups containing %s" % (self._memberId,))
+        results = self.service.ldap.search_s(ldap.dn.dn2str(base),
+            ldap.SCOPE_SUBTREE, filter, self.service.attrList)
+
+        groups = []
+        for dn, attrs in results:
+            shortName = self.service._getUniqueLdapAttribute(attrs, "cn")
+            self.log_debug("%s is a member of %s" % (self._memberId, shortName))
+            groups.append(self.service.recordWithShortName(recordType,
+                shortName))
+
+        return groups
+
+
+    def verifyCredentials(self, credentials):
+        """ Supports PAM or simple LDAP bind for username+password """
+
+        if isinstance(credentials, UsernamePassword):
+
+            # TODO: investigate:
+            # Check that the username supplied matches one of the shortNames
+            # (The DCS might already enforce this constraint, not sure)
+            if credentials.username not in self.shortNames:
+                return False
+
+            # Check cached password
+            try:
+                if credentials.password == self.password:
+                    return True
+            except AttributeError:
+                pass
+
+            if self.service.authMethod.upper() == "PAM":
+                # Authenticate against PAM (UNTESTED)
+
+                if not pamAvailable:
+                    msg = "PAM module is not installed"
+                    self.log_error(msg)
+                    raise DirectoryConfigurationError(msg)
+
+                def pam_conv(auth, query_list, userData):
+                    return [(credentials.password, 0)]
+
+                auth = PAM.pam()
+                auth.start("caldav")
+                auth.set_item(PAM.PAM_USER, credentials.username)
+                auth.set_item(PAM.PAM_CONV, pam_conv)
+                try:
+                    auth.authenticate()
+                except PAM.error:
+                    return False
+                else:
+                    # Cache the password to avoid further LDAP queries
+                    self.password = credentials.password
+                    return True
+
+            elif self.service.authMethod.upper() == "LDAP":
+
+                # Authenticate against LDAP
+                try:
+                    self.service.authenticate(self.dn, credentials.password)
+                    # Cache the password to avoid further LDAP queries
+                    self.password = credentials.password
+                    return True
+
+                except ldap.INVALID_CREDENTIALS:
+                    self.log_info("Invalid credentials for %s" %
+                        (repr(self.dn),), system="LdapDirectoryService")
+                    return False
+
+            else:
+                msg = "Unknown Authentication Method '%s'" % (
+                    self.service.authMethod.upper(),)
+                self.log_error(msg)
+                raise DirectoryConfigurationError(msg)
+
+        return super(LdapDirectoryRecord, self).verifyCredentials(credentials)

Added: CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_ldapdirectory.py	2011-01-27 04:07:57 UTC (rev 6811)
@@ -0,0 +1,81 @@
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.directory.ldapdirectory import buildFilter
+from twistedcaldav.test.util import TestCase
+
+class BuildFilterTestCase(TestCase):
+
+    def test_buildFilter(self):
+        mapping = {
+            "fullName" : "cn",
+            "emailAddresses" : "mail",
+            "firstName" : "givenName",
+            "lastName" : "sn",
+        }
+
+        entries = [
+            {
+                "fields" : [
+                    ("fullName", "mor", True, u"starts-with"),
+                    ("emailAddresses", "mor", True, u"starts-with"),
+                    ("firstName", "mor", True, u"starts-with"),
+                    ("lastName", "mor", True, u"starts-with")
+                ],
+                "operand" : "or",
+                "recordType" : None,
+                "expected" : "(|(cn=mor*)(mail=mor*)(givenName=mor*)(sn=mor*))"
+            },
+            {
+                "fields" : [
+                    ("fullName", "mor", True, u"starts-with"),
+                ],
+                "operand" : "or",
+                "recordType" : None,
+                "expected" : "(cn=mor*)"
+            },
+            {
+                "fields" : [
+                    ("fullName", "mor", True, u"contains"),
+                    ("emailAddresses", "mor", True, u"equals"),
+                    ("invalid", "mor", True, u"starts-with"),
+                ],
+                "operand" : "and",
+                "recordType" : None,
+                "expected" : "(&(cn=*mor*)(mail=mor))"
+            },
+            {
+                "fields" : [
+                    ("invalid", "mor", True, u"contains"),
+                    ("invalid", "mor", True, u"starts-with"),
+                ],
+                "operand" : "and",
+                "recordType" : None,
+                "expected" : None
+            },
+            {
+                "fields" : [ ],
+                "operand" : "and",
+                "recordType" : None,
+                "expected" : None
+            },
+        ]
+        for entry in entries:
+            self.assertEquals(
+                buildFilter(mapping, entry["fields"],
+                    operand=entry["operand"]),
+                entry["expected"]
+            )

Added: CalendarServer/trunk/twistedcaldav/directory/test/test_livedirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_livedirectory.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_livedirectory.py	2011-01-27 04:07:57 UTC (rev 6811)
@@ -0,0 +1,189 @@
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+runLDAPTests = False
+runODTests = False
+
+try:
+    import ldap
+
+    testServer = "localhost"
+    base = "dc=example,dc=com"
+
+    try:
+        cxn = ldap.open(testServer)
+        results = cxn.search_s(base, ldap.SCOPE_SUBTREE, "(uid=odtestamanda)",
+            ["cn"])
+        if len(results) == 1:
+            runLDAPTests = True
+    except ldap.LDAPError:
+        pass # Don't run live tests
+
+except ImportError:
+    print "Could not import ldap module (skipping LDAP tests)"
+
+try:
+    from calendarserver.platform.darwin.od import opendirectory, dsattributes
+
+    directory = opendirectory.odInit("/Search")
+
+    results = list(opendirectory.queryRecordsWithAttribute_list(
+        directory,
+        dsattributes.kDS1AttrGeneratedUID,
+        "9DC04A70-E6DD-11DF-9492-0800200C9A66",
+        dsattributes.eDSExact,
+        False,
+        dsattributes.kDSStdRecordTypeUsers,
+        None,
+        count=0
+    ))
+    recordNames = [x[0] for x in results]
+    if "odtestamanda" in recordNames:
+        runODTests = True
+    else:
+        print "Test OD records not found (skipping OD tests)"
+
+except ImportError:
+    print "Could not import OpenDirectory framework (skipping OD tests)"
+
+
+if runLDAPTests or runODTests:
+
+    from twistedcaldav.test.util import TestCase
+    from twistedcaldav.directory import augment
+    from twistedcaldav.directory.test.test_xmlfile import augmentsFile
+    from twisted.internet.defer import inlineCallbacks
+
+    augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
+
+
+    class LiveDirectoryTests(object):
+
+        def test_ldapRecordWithShortName(self):
+            record = self.svc.recordWithShortName("users", "odtestamanda")
+            self.assertTrue(record is not None)
+
+        def test_ldapRecordWithGUID(self):
+            record = self.svc.recordWithGUID("9DC04A70-E6DD-11DF-9492-0800200C9A66")
+            self.assertTrue(record is not None)
+
+        @inlineCallbacks
+        def test_ldapRecordsMatchingFields(self):
+            fields = (
+                ("firstName", "Amanda", True, "exact"),
+                ("lastName", "Te", True, "starts-with"),
+            )
+            records = list(
+                (yield self.svc.recordsMatchingFields(fields, operand="and"))
+            )
+            self.assertEquals(1, len(records))
+            record = self.svc.recordWithGUID("9DC04A70-E6DD-11DF-9492-0800200C9A66")
+            self.assertEquals(records, [record])
+
+        @inlineCallbacks
+        def test_restrictToGroup(self):
+            self.svc.restrictEnabledRecords = True
+            self.svc.restrictToGroup = "odtestgrouptop"
+
+            # Faulting in specific records will return records outside of
+            # the restrictToGroup, but they won't be enabledForCalendaring
+            # and AddressBooks:
+
+            # Amanda is a direct member of that group
+            record = self.svc.recordWithShortName("users", "odtestamanda")
+            self.assertTrue(record.enabledForCalendaring)
+            self.assertTrue(record.enabledForAddressBooks)
+
+            # Betty is a direct member of that group
+            record = self.svc.recordWithShortName("users", "odtestbetty")
+            self.assertTrue(record.enabledForCalendaring)
+            self.assertTrue(record.enabledForAddressBooks)
+
+            # Carlene is in a nested group
+            record = self.svc.recordWithShortName("users", "odtestcarlene")
+            self.assertTrue(record.enabledForCalendaring)
+            self.assertTrue(record.enabledForAddressBooks)
+
+            # Denise is not in the group
+            record = self.svc.recordWithShortName("users", "odtestdenise")
+            self.assertFalse(record.enabledForCalendaring)
+            self.assertFalse(record.enabledForAddressBooks)
+
+            # Searching for records using principal-property-search will not
+            # yield records outside of the restrictToGroup:
+
+            fields = (
+                ("lastName", "Test", True, "exact"),
+            )
+            records = list(
+                (yield self.svc.recordsMatchingFields(fields))
+            )
+            self.assertEquals(3, len(records))
+
+            # These two are directly in the restrictToGroup:
+            record = self.svc.recordWithShortName("users", "odtestamanda")
+            self.assertTrue(record in records)
+            record = self.svc.recordWithShortName("users", "odtestbetty")
+            self.assertTrue(record in records)
+            # Carlene is still picked up because she is in a nested group
+            record = self.svc.recordWithShortName("users", "odtestcarlene")
+            self.assertTrue(record in records)
+
+
+
+    if runLDAPTests:
+
+        from twistedcaldav.directory.ldapdirectory import LdapDirectoryService
+        print "Running live LDAP tests against %s" % (testServer,)
+
+        class LiveLDAPDirectoryServiceCase(LiveDirectoryTests, TestCase):
+
+            def setUp(self):
+                params = {
+                    "uri": "ldap://%s" % (testServer,),
+                    "rdnSchema": {
+                        "base": base,
+                        "guidAttr": "apple-generateduid",
+                        "users": {
+                            "rdn": "cn=users",
+                            "attr": "uid", # used only to synthesize email address
+                            "emailSuffix": None, # used only to synthesize email address
+                            "filter": None, # additional filter for this type
+                        },
+                        "groups": {
+                            "rdn": "cn=groups",
+                            "attr": "cn", # used only to synthesize email address
+                            "emailSuffix": None, # used only to synthesize email address
+                            "filter": None, # additional filter for this type
+                        },
+                    },
+                    "groupSchema": {
+                        "membersAttr": "apple-group-memberguid", # how members are specified
+                        "nestedGroupsAttr" : "apple-group-nestedgroup", # how nested groups are specified
+                        "memberIdAttr": "apple-generateduid", # which attribute the above refers to
+                    },
+                }
+                self.svc = LdapDirectoryService(params)
+
+
+    if runODTests:
+
+        from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
+        class LiveODDirectoryServiceCase(LiveDirectoryTests, TestCase):
+
+            def setUp(self):
+                params = {}
+                self.svc = OpenDirectoryService(params)

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py	2011-01-26 18:22:17 UTC (rev 6810)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py	2011-01-27 04:07:57 UTC (rev 6811)
@@ -54,14 +54,10 @@
             super(OpenDirectory, self).setUp()
             augment.AugmentService = augment.AugmentXMLDB(xmlFiles=())
             try:
-                self._service = OpenDirectoryService({"node" : "/Search"}, dosetup=False)
+                self._service = OpenDirectoryService({"node" : "/Search"})
             except ImportError, e:
                 raise SkipTest("OpenDirectory module is not available: %s" % (e,))
 
-        def tearDown(self):
-            for call in self._service._delayedCalls:
-                call.cancel()
-
         def service(self):
             return self._service
 
@@ -455,6 +451,5 @@
                 {
                     "node" : "/Search",
                     "recordTypes" : (DirectoryService.recordType_users, DirectoryService.recordType_groups),
-                },
-                dosetup = False
+                }
             )

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2011-01-26 18:22:17 UTC (rev 6810)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2011-01-27 04:07:57 UTC (rev 6811)
@@ -52,6 +52,61 @@
         "restrictToGroup": "",
         "recordTypes": ("users", "groups"),
     },
+    "twistedcaldav.directory.ldapdirectory.LdapDirectoryService": {
+        "cacheTimeout": 30,
+        "restrictEnabledRecords": False,
+        "restrictToGroup": "",
+        "recordTypes": ("users", "groups"),
+        "uri": "ldap://localhost/",
+        "tls": False,
+        "tlsCACertFile": None,
+        "tlsCACertDir": None,
+        "tlsRequireCert": None, # never, allow, try, demand, hard
+        "credentials": {
+            "dn": None,
+            "password": None,
+        },
+        "authMethod": "LDAP",
+        "rdnSchema": {
+            "base": "dc=example,dc=com",
+            "guidAttr": None,
+            "users": {
+                "rdn": "ou=People",
+                "attr": "uid", # used only to synthesize email address
+                "emailSuffix": None, # used only to synthesize email address
+                "filter": None, # additional filter for this type
+            },
+            "groups": {
+                "rdn": "ou=Group",
+                "attr": "cn", # used only to synthesize email address
+                "emailSuffix": None, # used only to synthesize email address
+                "filter": None, # additional filter for this type
+            },
+            "locations": {
+                "rdn": "ou=Locations",
+                "attr": "cn", # used only to synthesize email address
+                "emailSuffix": None, # used only to synthesize email address
+                "filter": None, # additional filter for this type
+            },
+            "resources": {
+                "rdn": "ou=Resources",
+                "attr": "cn", # used only to synthesize email address
+                "emailSuffix": None, # used only to synthesize email address
+                "filter": None, # additional filter for this type
+            },
+        },
+        "groupSchema": {
+            "membersAttr": "member", # how members are specified
+            "nestedGroupsAttr": None, # how nested groups are specified
+            "memberIdAttr": None, # which attribute the above refer to
+        },
+        "attributeMapping": { # maps internal record names to LDAP
+            "fullName" : "cn",
+            "emailAddresses" : "mail",
+            "firstName" : "givenName",
+            "lastName" : "sn",
+        },
+    },
 }
 
 DEFAULT_RESOURCE_PARAMS = {
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110126/045c7a7a/attachment-0001.html>


More information about the calendarserver-changes mailing list