[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