- copying current trunk into new location
git-svn-id: http://svn.dataflake.org/svn/Products.LDAPMultiPlugins/trunk@1506 835909ba-7c00-0410-bfa4-884f43845301
This commit is contained in:
parent
83130f9c8f
commit
5403514ec5
|
@ -0,0 +1,485 @@
|
|||
##############################################################################
|
||||
#
|
||||
# ActiveDirectoryMultiPlugin Shim to use the LDAPUserFolder with the
|
||||
# PluggableAuthenticationService w/ AD
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
__doc__ = """ ActiveDirectoryUserFolder shim module """
|
||||
__version__ = '$Revision$'[11:-2]
|
||||
|
||||
# General Python imports
|
||||
import logging
|
||||
import os
|
||||
from urllib import quote_plus
|
||||
|
||||
# Zope imports
|
||||
from Acquisition import aq_base
|
||||
from Globals import InitializeClass
|
||||
from Globals import DTMLFile
|
||||
from Globals import package_home
|
||||
from AccessControl import ClassSecurityInfo
|
||||
from Products.LDAPUserFolder import manage_addLDAPUserFolder
|
||||
from Products.LDAPUserFolder.LDAPDelegate import filter_format
|
||||
from Products.LDAPUserFolder.utils import BINARY_ATTRIBUTES
|
||||
|
||||
from zope.interface import implementedBy
|
||||
|
||||
from Products.PluggableAuthService.interfaces.plugins import \
|
||||
IUserEnumerationPlugin, IGroupsPlugin, IGroupEnumerationPlugin, \
|
||||
IRoleEnumerationPlugin
|
||||
from Products.PluggableAuthService.utils import classImplements
|
||||
|
||||
from LDAPPluginBase import LDAPPluginBase
|
||||
|
||||
|
||||
logger = logging.getLogger('event.LDAPMultiPlugin')
|
||||
_dtmldir = os.path.join(package_home(globals()), 'dtml')
|
||||
addActiveDirectoryMultiPluginForm = DTMLFile('addActiveDirectoryMultiPlugin',
|
||||
_dtmldir)
|
||||
|
||||
def manage_addActiveDirectoryMultiPlugin( self, id, title, LDAP_server
|
||||
, login_attr
|
||||
, uid_attr, users_base, users_scope, roles
|
||||
, groups_base, groups_scope, binduid, bindpwd
|
||||
, binduid_usage=1, rdn_attr='cn', local_groups=0
|
||||
, use_ssl=0 , encryption='SHA', read_only=0
|
||||
, REQUEST=None
|
||||
):
|
||||
""" Factory method to instantiate a ActiveDirectoryMultiPlugin """
|
||||
# Make sure we really are working in our container (the
|
||||
# PluggableAuthService object)
|
||||
self = self.this()
|
||||
|
||||
# Value needs massaging, there's some magic transcending a simple true
|
||||
# or false expeced by the LDAP delegate :(
|
||||
if use_ssl:
|
||||
use_ssl = 1
|
||||
else:
|
||||
use_ssl = 0
|
||||
|
||||
# Instantiate the folderish adapter object
|
||||
lmp = ActiveDirectoryMultiPlugin(id, title=title)
|
||||
self._setObject(id, lmp)
|
||||
lmp = getattr(aq_base(self), id)
|
||||
lmp_base = aq_base(lmp)
|
||||
|
||||
# Put the "real" LDAPUserFolder inside it
|
||||
manage_addLDAPUserFolder(lmp)
|
||||
luf = getattr(lmp_base, 'acl_users')
|
||||
|
||||
host_elems = LDAP_server.split(':')
|
||||
host = host_elems[0]
|
||||
if len(host_elems) > 1:
|
||||
port = host_elems[1]
|
||||
else:
|
||||
if use_ssl:
|
||||
port = '636'
|
||||
else:
|
||||
port = '389'
|
||||
|
||||
luf.manage_addServer(host, port=port, use_ssl=use_ssl)
|
||||
luf.manage_edit( title
|
||||
, login_attr
|
||||
, uid_attr
|
||||
, users_base
|
||||
, users_scope
|
||||
, roles
|
||||
, groups_base
|
||||
, groups_scope
|
||||
, binduid
|
||||
, bindpwd
|
||||
, binduid_usage=binduid_usage
|
||||
, rdn_attr=rdn_attr
|
||||
, local_groups=local_groups
|
||||
, encryption=encryption
|
||||
, read_only=read_only
|
||||
, REQUEST=None
|
||||
)
|
||||
|
||||
# clean out the __allow_groups__ bit because it is not needed here
|
||||
# and potentially harmful
|
||||
if hasattr(lmp_base, '__allow_groups__'):
|
||||
del lmp_base.__allow_groups__
|
||||
|
||||
uf = lmp.acl_users
|
||||
uf._ldapschema = { 'cn' : { 'ldap_name' : 'cn'
|
||||
, 'friendly_name' : 'Canonical Name'
|
||||
, 'multivalued' : ''
|
||||
, 'public_name' : ''
|
||||
}
|
||||
, 'sn' : { 'ldap_name' : 'sn'
|
||||
, 'friendly_name' : 'Last Name'
|
||||
, 'multivalued' : ''
|
||||
, 'public_name' : 'last_name'
|
||||
}
|
||||
}
|
||||
uf.manage_addLDAPSchemaItem('dn', 'Distinguished Name',
|
||||
public_name='dn')
|
||||
uf.manage_addLDAPSchemaItem('sAMAccountName', 'Windows Login Name',
|
||||
public_name='windows_login_name')
|
||||
uf.manage_addLDAPSchemaItem('objectGUID', 'AD Object GUID',
|
||||
public_name='objectGUID')
|
||||
uf.manage_addLDAPSchemaItem('givenName', 'First Name',
|
||||
public_name='first_name')
|
||||
uf.manage_addLDAPSchemaItem('sn', 'Last Name',
|
||||
public_name='last_name')
|
||||
uf.manage_addLDAPSchemaItem('memberOf',
|
||||
'Group DNs',
|
||||
public_name='memberOf',
|
||||
multivalued=True)
|
||||
|
||||
if REQUEST is not None:
|
||||
REQUEST.RESPONSE.redirect('%s/manage_main' % self.absolute_url())
|
||||
|
||||
class ActiveDirectoryMultiPlugin(LDAPPluginBase):
|
||||
""" The adapter that mediates between the PAS and the LDAPUserFolder """
|
||||
security = ClassSecurityInfo()
|
||||
meta_type = 'ActiveDirectory Multi Plugin'
|
||||
|
||||
_properties = LDAPPluginBase._properties + (
|
||||
{'id':'groupid_attr', 'type':'string', 'mode':'w'},
|
||||
{'id':'grouptitle_attr', 'type':'string', 'mode':'w'},
|
||||
{'id':'group_class', 'type':'string', 'mode':'w'},
|
||||
{'id':'group_recurse', 'type':'int', 'mode':'w'},
|
||||
{'id':'group_recurse_depth', 'type':'int', 'mode':'w'},
|
||||
)
|
||||
|
||||
groupid_attr = 'objectGUID'
|
||||
grouptitle_attr = 'cn'
|
||||
group_class = 'group'
|
||||
group_recurse = 1
|
||||
group_recurse_depth = 1
|
||||
|
||||
def __init__(self, id, title='', groupid_attr='objectGUID',
|
||||
grouptitle_attr='cn', group_class='group', group_recurse=1,
|
||||
group_recurse_depth=1):
|
||||
""" Initialize a new instance """
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.groupid_attr = groupid_attr
|
||||
self.grouptitle_attr = grouptitle_attr
|
||||
self.group_class = group_class
|
||||
self.group_recurse = group_recurse
|
||||
self.group_recurse_depth = group_recurse_depth
|
||||
|
||||
security.declarePublic('getGroupsForPrincipal')
|
||||
def getGroupsForPrincipal(self, user, request=None, attr=None):
|
||||
""" Fulfill GroupsPlugin requirements """
|
||||
if attr is None:
|
||||
attr = self.groupid_attr
|
||||
|
||||
acl = self._getLDAPUserFolder()
|
||||
|
||||
if acl is None:
|
||||
return ()
|
||||
|
||||
view_name = self.getId() + '_getGroupsForPrincipal'
|
||||
criteria = {'user_id':user.getId(), 'attr':attr}
|
||||
|
||||
cached_info = self.ZCacheable_get(view_name = view_name,
|
||||
keywords = criteria,
|
||||
default = None)
|
||||
|
||||
if cached_info is not None:
|
||||
logger.debug('returning cached results from getGroupsForPrincipal')
|
||||
return cached_info
|
||||
|
||||
unmangled_userid = self._demangle(user.getId())
|
||||
if unmangled_userid is None:
|
||||
return ()
|
||||
|
||||
ldap_user = acl.getUserById(unmangled_userid)
|
||||
if ldap_user is None:
|
||||
return ()
|
||||
|
||||
cns = [ x.split(',')[0] for x in (ldap_user.memberOf or []) ]
|
||||
if not cns:
|
||||
return ()
|
||||
cns = [x.split('=')[1] for x in cns]
|
||||
cn_flts = [filter_format('(cn=%s)', (cn,)) for cn in cns]
|
||||
filt = '(&(objectClass=%s)(|%s))' % (self.group_class, ''.join(cn_flts))
|
||||
|
||||
delegate = acl._delegate
|
||||
R = delegate.search(acl.groups_base, acl.groups_scope, filter=filt)
|
||||
|
||||
if R['exception']:
|
||||
logger.error("Failed to locate groups for principal in %s "
|
||||
"(scope=%s, filter=%s): %s",
|
||||
acl.groups_base, acl.groups_scope, filt,
|
||||
R['exception'])
|
||||
return ()
|
||||
if self.group_recurse:
|
||||
groups = self._recurseGroups(R['results'])
|
||||
else:
|
||||
groups = R['results']
|
||||
|
||||
results = [ x[attr][0] for x in groups]
|
||||
|
||||
self.ZCacheable_set(results, view_name=view_name, keywords=criteria)
|
||||
|
||||
return results
|
||||
|
||||
def _recurseGroups(self, ldap_results, temp=None, seen=None, depth=0):
|
||||
""" Given a set of LDAP result data for a group search, return
|
||||
the recursive group memberships for each group: arbitrarily
|
||||
expensive """
|
||||
if seen is None:
|
||||
seen = {}
|
||||
if temp is None:
|
||||
temp = []
|
||||
# Build a single filter so we can do it with a single search.
|
||||
filt_bits = []
|
||||
|
||||
for result in ldap_results:
|
||||
dn = result['dn']
|
||||
|
||||
if seen.has_key(dn):
|
||||
continue
|
||||
temp.append(result)
|
||||
seen[dn] = 1
|
||||
|
||||
if result.has_key('memberOf'):
|
||||
for parent_dn in result['memberOf']:
|
||||
filt = filter_format('(distinguishedName=%s)', (parent_dn,))
|
||||
if filt in filt_bits:
|
||||
continue
|
||||
filt_bits.append(filt)
|
||||
|
||||
if filt_bits:
|
||||
bits_s = ''.join(filt_bits)
|
||||
filt = "(&(objectClass=%s)(|%s))" % (self.group_class, bits_s)
|
||||
acl = self.acl_users
|
||||
delegate = acl._delegate
|
||||
R = delegate.search(acl.groups_base, acl.groups_scope, filter=filt)
|
||||
if R['exception']:
|
||||
logger.error("Failed to recursively search for group in %s "
|
||||
"(scope=%s, filter=%s): %s",
|
||||
acl.groups_base, acl.groups_scope, filt,
|
||||
R['exception'])
|
||||
else:
|
||||
if depth < self.group_recurse_depth:
|
||||
self._recurseGroups(R['results'], temp, seen, depth + 1)
|
||||
|
||||
return temp
|
||||
|
||||
security.declarePrivate('enumerateUsers')
|
||||
def enumerateUsers( self
|
||||
, id=None
|
||||
, login=None
|
||||
, exact_match=0
|
||||
, sort_by=None
|
||||
, max_results=None
|
||||
, **kw
|
||||
):
|
||||
""" Fulfill the UserEnumerationPlugin requirements """
|
||||
view_name = self.getId() + '_enumerateUsers'
|
||||
criteria = {'id':id, 'login':login, 'exact_match':exact_match,
|
||||
'sort_by':sort_by, 'max_results':max_results}
|
||||
criteria.update(kw)
|
||||
|
||||
cached_info = self.ZCacheable_get(view_name = view_name,
|
||||
keywords = criteria,
|
||||
default = None)
|
||||
|
||||
if cached_info is not None:
|
||||
logger.debug('returning cached results from enumerateUsers')
|
||||
return cached_info
|
||||
|
||||
result = []
|
||||
acl = self._getLDAPUserFolder()
|
||||
login_attr = acl.getProperty('_login_attr')
|
||||
uid_attr = acl.getProperty('_uid_attr')
|
||||
plugin_id = self.getId()
|
||||
edit_url = '%s/%s/manage_userrecords' % (plugin_id, acl.getId())
|
||||
|
||||
if login_attr in kw:
|
||||
login = kw[login_attr]
|
||||
del kw[login_attr]
|
||||
|
||||
if uid_attr in kw:
|
||||
id = kw[uid_attr]
|
||||
del kw[uid_attr]
|
||||
|
||||
if acl is None:
|
||||
return ()
|
||||
|
||||
if exact_match:
|
||||
if id:
|
||||
ldap_user = acl.getUserById(id)
|
||||
elif login:
|
||||
ldap_user = acl.getUser(login)
|
||||
else:
|
||||
msg = 'Exact Match specified but no ID or Login given'
|
||||
raise ValueError, msg
|
||||
|
||||
if ldap_user is not None:
|
||||
qs = 'user_dn=%s' % quote_plus(ldap_user.getUserDN())
|
||||
result.append( { 'id' : ldap_user.getId()
|
||||
, 'login' : ldap_user.getProperty(login_attr)
|
||||
, 'pluginid' : plugin_id
|
||||
, 'title': ldap_user.getProperty(login_attr)
|
||||
, 'editurl' : '%s?%s' % (edit_url, qs)
|
||||
} )
|
||||
elif id or login or kw:
|
||||
l_results = []
|
||||
seen = []
|
||||
attrs = (uid_attr, login_attr)
|
||||
|
||||
if id:
|
||||
l_results.extend(acl.findUser(uid_attr, id, attrs=attrs))
|
||||
|
||||
if login:
|
||||
l_results.extend(acl.findUser(login_attr, login, attrs=attrs))
|
||||
|
||||
for key, val in kw.items():
|
||||
l_results.extend(acl.findUser(key, val, attrs=attrs))
|
||||
|
||||
for l_res in l_results:
|
||||
if l_res['dn'] not in seen and l_res.has_key(login_attr):
|
||||
l_res['id'] = l_res[uid_attr]
|
||||
l_res['login'] = l_res[login_attr]
|
||||
l_res['pluginid'] = plugin_id
|
||||
quoted_dn = quote_plus(l_res['dn'])
|
||||
l_res['editurl'] = '%s?user_dn=%s' % (edit_url, quoted_dn)
|
||||
result.append(l_res)
|
||||
seen.append(l_res['dn'])
|
||||
|
||||
if sort_by is not None:
|
||||
result.sort(lambda a, b: cmp( a.get(sort_by, '').lower()
|
||||
, b.get(sort_by, '').lower()
|
||||
) )
|
||||
|
||||
if isinstance(max_results, int) and len(result) > max_results:
|
||||
result = result[:max_results-1]
|
||||
|
||||
else:
|
||||
result = []
|
||||
for uid, name in acl.getUserIdsAndNames():
|
||||
tmp = {}
|
||||
tmp['id'] = uid
|
||||
tmp['login'] = name
|
||||
tmp['pluginid'] = plugin_id
|
||||
tmp['editurl'] = None
|
||||
result.append(tmp)
|
||||
|
||||
if sort_by is not None:
|
||||
result.sort(lambda a, b: cmp( a.get(sort_by, '').lower()
|
||||
, b.get(sort_by, '').lower()
|
||||
) )
|
||||
|
||||
if isinstance(max_results, int) and len(result) > max_results:
|
||||
result = result[:max_results-1]
|
||||
|
||||
result = tuple(result)
|
||||
|
||||
self.ZCacheable_set(result, view_name=view_name, keywords=criteria)
|
||||
|
||||
return result
|
||||
|
||||
security.declarePrivate('enumerateGroups')
|
||||
def enumerateGroups( self
|
||||
, id=None
|
||||
, exact_match=0
|
||||
, sort_by=None
|
||||
, max_results=None
|
||||
, **kw
|
||||
):
|
||||
""" Fulfill the RoleEnumerationPlugin requirements """
|
||||
view_name = self.getId() + '_enumerateGroups'
|
||||
criteria = {'id':id, 'exact_match':exact_match,
|
||||
'sort_by':sort_by, 'max_results':max_results}
|
||||
criteria.update(kw)
|
||||
|
||||
cached_info = self.ZCacheable_get(view_name = view_name,
|
||||
keywords = criteria,
|
||||
default = None)
|
||||
|
||||
if cached_info is not None:
|
||||
logger.debug('returning cached results from enumerateGroups')
|
||||
return cached_info
|
||||
|
||||
acl = self._getLDAPUserFolder()
|
||||
|
||||
if acl is None:
|
||||
return ()
|
||||
|
||||
if id is None and exact_match != 0:
|
||||
raise ValueError, 'Exact Match requested but no id provided'
|
||||
elif id is None:
|
||||
id = ''
|
||||
|
||||
plugin_id = self.getId()
|
||||
|
||||
filt = ['(objectClass=%s)' % self.group_class]
|
||||
if not id:
|
||||
filt.append('(%s=*)' % self.groupid_attr)
|
||||
elif exact_match:
|
||||
filt.append(filter_format('(%s=%s)',(self.groupid_attr, id)))
|
||||
elif id:
|
||||
filt.append(filter_format('(%s=*%s*)',(self.groupid_attr, id)))
|
||||
filt = '(&%s)' % ''.join(filt)
|
||||
|
||||
if self.groupid_attr.lower() in BINARY_ATTRIBUTES:
|
||||
convert_filter = False
|
||||
else:
|
||||
convert_filter = True
|
||||
|
||||
delegate = acl._delegate
|
||||
R = delegate.search( acl.groups_base
|
||||
, acl.groups_scope
|
||||
, filter=filt
|
||||
, convert_filter=convert_filter
|
||||
)
|
||||
|
||||
if R['exception']:
|
||||
logger.error("Failed to enumerate groups in %s "
|
||||
"(scope=%s, filter=%s): %s",
|
||||
acl.groups_base, acl.groups_scope, filt,
|
||||
R['exception'])
|
||||
return ()
|
||||
|
||||
groups = R['results']
|
||||
|
||||
results = []
|
||||
for group in groups:
|
||||
tmp = {}
|
||||
tmp['title'] = '(Group) ' + group[self.grouptitle_attr][0]
|
||||
id = tmp['id'] = group[self.groupid_attr][0]
|
||||
tmp['pluginid'] = plugin_id
|
||||
results.append(tmp)
|
||||
|
||||
if sort_by is not None:
|
||||
results.sort(lambda a, b: cmp( a.get(sort_by, '').lower()
|
||||
, b.get(sort_by, '').lower()
|
||||
) )
|
||||
if isinstance(max_results, int) and len(results) > max_results:
|
||||
results = results[:max_results+1]
|
||||
|
||||
results = tuple(results)
|
||||
|
||||
self.ZCacheable_set(results, view_name=view_name, keywords=criteria)
|
||||
|
||||
return results
|
||||
|
||||
security.declarePrivate('enumerateRoles')
|
||||
def enumerateRoles( self
|
||||
, id=None
|
||||
, exact_match=0
|
||||
, sort_by=None
|
||||
, max_results=None
|
||||
, **kw
|
||||
):
|
||||
""" Fulfill the RoleEnumerationPlugin requirements """
|
||||
return []
|
||||
|
||||
classImplements( ActiveDirectoryMultiPlugin
|
||||
, IUserEnumerationPlugin
|
||||
, IGroupsPlugin
|
||||
, IGroupEnumerationPlugin
|
||||
, IRoleEnumerationPlugin
|
||||
, *implementedBy(LDAPPluginBase)
|
||||
)
|
||||
|
||||
InitializeClass(ActiveDirectoryMultiPlugin)
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
Change log for the LDAPMultiPlugins product
|
||||
|
||||
1.6 (unreleased)
|
||||
|
||||
Bugs fixed
|
||||
|
||||
- ActiveDirectoryMultiPlugin.enumerateGroups: In order to support
|
||||
group searches on the binary objectGUID attribute, utilize a new
|
||||
flag exposed by the LDAPUserFolder LDAPDelegate search method
|
||||
that prevents the customary UTF8-encoding of the search filter
|
||||
expression. **NOTE**: With this change the LDAPUserFolder version
|
||||
dependency changes to version 2.9 or higher!
|
||||
(http://www.dataflake.org/tracker/issue_00576 by Wichert Akkerman)
|
||||
|
||||
- ActiveDirectoryMultiPlugin.enumerateGroups: If the requested group
|
||||
id is a binary string, like a objectGUID attribute, it was mangled
|
||||
by a lowercasing operation. Removed the lowercasing.
|
||||
(http://www.dataflake.org/tracker/issue_00575 by Wichert Akkerman)
|
||||
|
||||
Features added
|
||||
|
||||
- Added caching to the getGroupsForPrincipal method. Thanks to Wichert
|
||||
Akkerman for the patch.
|
||||
(http://www.dataflake.org/tracker/issue_00571)
|
||||
|
||||
|
||||
1.5 (2007/06/13)
|
||||
|
||||
Bugs fixed
|
||||
|
||||
- The product will no longer silently fail to install if the
|
||||
LDAPUserFolder package is not installed. Silent failure does
|
||||
not look like a good strategy here.
|
||||
|
||||
- fixes and import cleanups after running Pyflakes
|
||||
(http://divmod.org:81/svn/Divmod/trunk/Pyflakes/)
|
||||
|
||||
Other
|
||||
|
||||
- added some additional configuration hints to the README, thanks go
|
||||
to Brett Lentz (http://www.dataflake.org/tracker/issue_00559)
|
||||
|
||||
|
||||
1.5-beta (2007/03/03)
|
||||
|
||||
Bugs fixed
|
||||
|
||||
- LDAPMultiPlugin.enumerateUsers: If no useful search criteria are
|
||||
given (meaning no user ID or login is specified), fabricate a
|
||||
criteria set that will return all users, this is the expected
|
||||
behavior when calling enumerateUsers.
|
||||
|
||||
- LDAPMultiPlugin.enumerateUsers: When iterating over search results
|
||||
from the user folder we now look for the special "fake result"
|
||||
emitted by the user folder if there is an error. Not elegant, but
|
||||
needed until error handling is changed in the LDAPUserFolder.
|
||||
|
||||
- Instead of throwing exceptions, the ActiveDirectoryMultiPlugin
|
||||
will now log error conditions and continue, with a patch from
|
||||
Mark Hammond.
|
||||
(http://www.dataflake.org/tracker/issue_00554)
|
||||
|
||||
- Adjusted an import that has been removed from the
|
||||
PluggableAuthService utils module.
|
||||
(http://www.dataflake.org/tracker/issue_00542)
|
||||
|
||||
- Remove the ICredentialsUpdatePlugin implementation - it was
|
||||
implemented wrongly and should not have been part of the contract
|
||||
at all due to an interface misunderstanding.
|
||||
(http://www.dataflake.org/tracker/issue_00539)
|
||||
|
||||
Other
|
||||
|
||||
- Moved the PluggableAuthService dependency up to version 1.4
|
||||
|
||||
|
||||
1.4 (2006/10/16)
|
||||
|
||||
Bugs fixed
|
||||
|
||||
- The ActiveDirectoryMultiPlugin did not ensure to correctly
|
||||
escape search filters it constructed internally.
|
||||
(http://www.dataflake.org/tracker/issue_00507)
|
||||
|
||||
- The add form selection whether or not to use SSL for the LDAP
|
||||
server connection was not handed through correctly, identified
|
||||
by Olivier Nicole (http://www.dataflake.org/tracker/issue_00526)
|
||||
|
||||
Other
|
||||
|
||||
- Revamped the way recursive group memberships are found and applied,
|
||||
not sure if the previous implementation was a bug or not. Many thanks
|
||||
to John Hannon for a patch. This change includes the ability to
|
||||
specify a nesting depth to which the recursive search will go.
|
||||
(http://www.dataflake.org/tracker/issue_00513)
|
||||
|
||||
- Added some notes on how to enable caching using the ZCacheable
|
||||
mechanism
|
||||
|
||||
|
||||
1.3 (2006/07/29)
|
||||
|
||||
Bugs fixed
|
||||
|
||||
* Update the enumerateGroups method to use the new LDAPUserFolder
|
||||
method "searchGroups". This changes the LDAPUserFolder dependency
|
||||
to version 2.7. Patch provided by Leonardo Rochael Almeida.
|
||||
|
||||
* The ActiveDirectoryMultiPlugin enumerateUsers method would only
|
||||
search correctly if login or id were explicitly specified
|
||||
(thanks to Sidnei da Silva for the patch).
|
||||
|
||||
* Make sure to apply the same checks for user existence in
|
||||
getRolesForPrincipal that are used by getPropertiesForUser
|
||||
(http://www.dataflake.org/tracker/issue_00503 by Riccardo Lemmi)
|
||||
|
||||
* Fixed the enumerateUsers implementation to be more efficient and
|
||||
use the new searchUsers method on the LDAPUserFolder (thanks to
|
||||
Wichert Akkerman for the problem description and solution)
|
||||
|
||||
Other
|
||||
|
||||
* Added simple caching of groups information, provided by
|
||||
Leonardo Rochael Almeida.
|
||||
|
||||
* Software dependencies are now documented in a separate
|
||||
DEPENDENCIES.txt file. Please note that the packages mentioned
|
||||
in DEPENDENCIES.txt may have their own dependencies that must be
|
||||
satisfied as well.
|
||||
|
||||
* Replaced all zLOG usage with equivalent calls into the Python
|
||||
logging module, and reducing the chattiness coded into the
|
||||
ActiveDirectoryMultiPlugin (INFO -> DEBUG)
|
||||
|
||||
* Started on a test suite
|
||||
|
||||
|
||||
1.2 (2006/03/02)
|
||||
|
||||
Bugs fixed
|
||||
|
||||
* In order to avoid duplicate search results, the enumerateUsers
|
||||
method used a simple dictionary to store DNs for records that
|
||||
were already processed. However, the keys put into this dictionary
|
||||
were munged and really could not be compared to raw search
|
||||
result DNs anymore. Thanks go to Wichert Akkerman for spotting this
|
||||
obvious error (http://www.dataflake.org/tracker/issue_00485).
|
||||
|
||||
* Speed up enumerateGroups by letting the LDAP server do more of
|
||||
the filtering (thanks to Wichert Akkerman,
|
||||
http://www.dataflake.org/tracker/issue_00483)
|
||||
|
||||
* Applied a performance fix to the ActiveDirectoryPlugin's
|
||||
_recurseGroups method (thanks got to Mark Hammond for the patch,
|
||||
http://www.dataflake.org/tracker/issue_00476)
|
||||
|
||||
|
||||
1.1 (2005/10/29)
|
||||
|
||||
Bugs fixed
|
||||
|
||||
* The LDAPMultiPlugins ignored default roles configured on the
|
||||
LDAPUserFolder and would not add it to the set of roles
|
||||
computed (seen by Sidnei da Silva).
|
||||
|
||||
* enumerateUsers now allows you to do exact-match searches on
|
||||
attributes other than just the user ID and login (patch
|
||||
by Sidnei da Silva). **Note**: This code now requires
|
||||
LDAPUserFolder versions 2.6 or higher, which support exact
|
||||
match searches using LDAPUserFolder.findUsers.
|
||||
|
||||
|
||||
1.0 (2005/08/18)
|
||||
|
||||
Other
|
||||
|
||||
* The interface machinery expected by the PluggableAuthService has
|
||||
been changed to use Zope 3-style interfaces. Thanks go to Leonardo
|
||||
Rochael Almeida who provided a patch to fix the resulting breakage.
|
||||
|
||||
* Changed the initialization code for the plugins to conform to the
|
||||
changed initialization code in the LDAPUserFolder product versions
|
||||
2.6beta3 and up.
|
||||
|
||||
|
||||
1.0beta3
|
||||
|
||||
Other
|
||||
|
||||
* Changes to the way the user IDs are mangled/unmangled to be in line
|
||||
with the changes in the latest PluggableAuthService code
|
||||
(Patch provided by Mark Hammond)
|
||||
|
||||
|
||||
1.0beta2
|
||||
|
||||
Bugs fixed
|
||||
|
||||
* When retrieving properties for a user, None values have to be
|
||||
converted to an empty string to prevent the user propertysheet
|
||||
machinery from blowing up trying to guess what kind of
|
||||
property a None value could represent.
|
||||
|
||||
|
||||
1.0beta1
|
||||
|
||||
Bugs fixed
|
||||
|
||||
* Role retrieval was broken, small fix involves changing a call to the
|
||||
LDAPUserFolder
|
||||
|
||||
|
||||
LDAPMultiPlugins 0.9
|
||||
|
||||
First public release
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
Copyright (c) 2004-2006 Chris McDonough and Jens Vagelpohl.
|
||||
All Rights Reserved.
|
||||
|
||||
This software is subject to the provisions of the Zope Public License,
|
||||
Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE.
|
|
@ -0,0 +1,3 @@
|
|||
Zope >= 2.8.5
|
||||
PluggableAuthService >= 1.4 (http://www.zope.org/Products/PluggableAuthService)
|
||||
LDAPUserFolder >= 2.9 (http://www.dataflake.org/software/ldapuserfolder/)
|
|
@ -0,0 +1,26 @@
|
|||
Installing the LDAPMultiPlugins Product
|
||||
|
||||
It is assumed that you have installed the PluggableAuthService and
|
||||
PluginRegistry products already. They are available from cvs.zope.org.
|
||||
|
||||
This product does not require any special handling after unzipping
|
||||
and untarring it in the Zope Products directory. You should do
|
||||
something like::
|
||||
|
||||
$ cp LDAPMultiPlugins-xyz.tgz <zope_root>/lib/python/Products
|
||||
$ cd <zope_root>/lib/python/Products
|
||||
$ tar zxvf LDAPMultiPlugins-xyz.tgz
|
||||
<watch files being decompressed>
|
||||
|
||||
Windows users can use WinZip or similar, it can handle tarred
|
||||
gzip files. Make sure to move the extracted LDAPMultiPlugins
|
||||
folder to your Zope installation's lib/python/Products-folder.
|
||||
|
||||
That's all. Do not forget to restart Zope afterwards. You will then
|
||||
be able to select the "LDAP Multi Plugin" and the "Active Directory
|
||||
Multi Plugin" from the list of plugins to add when you navigate to
|
||||
your PluggableAuthService-based user folder and select the "Contents"
|
||||
tab in the Zope Management Interface (ZMI).
|
||||
|
||||
See README.txt for any other dependencies and requirements.
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
##############################################################################
|
||||
#
|
||||
# LDAPMultiPlugin Shim to use the LDAPUserFolder with the
|
||||
# PluggableAuthenticationService
|
||||
#
|
||||
# This software is governed by a license. See
|
||||
# LICENSE.txt for the terms of this license.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
__doc__ = """ LDAPUserFolder shim module """
|
||||
__version__ = '$Revision$'[11:-2]
|
||||
|
||||
# General Python imports
|
||||
import logging
|
||||
import os
|
||||
from urllib import quote_plus
|
||||
|
||||
# Zope imports
|
||||
from Acquisition import aq_base
|
||||
from Globals import InitializeClass
|
||||
from Globals import DTMLFile
|
||||
from Globals import package_home
|
||||
from AccessControl import ClassSecurityInfo
|
||||
from Products.LDAPUserFolder import manage_addLDAPUserFolder
|
||||
|
||||
from zope.interface import implementedBy
|
||||
|
||||
from Products.PluggableAuthService.interfaces.plugins import \
|
||||
IUserEnumerationPlugin, IGroupsPlugin, IGroupEnumerationPlugin, \
|
||||
IRoleEnumerationPlugin
|
||||
from Products.PluggableAuthService.utils import classImplements
|
||||
|
||||
from LDAPPluginBase import LDAPPluginBase
|
||||
|
||||
logger = logging.getLogger('event.LDAPMultiPlugin')
|
||||
_dtmldir = os.path.join(package_home(globals()), 'dtml')
|
||||
addLDAPMultiPluginForm = DTMLFile('addLDAPMultiPlugin', _dtmldir)
|
||||
|
||||
def manage_addLDAPMultiPlugin( self, id, title, LDAP_server, login_attr
|
||||
, uid_attr, users_base, users_scope, roles
|
||||
, groups_base, groups_scope, binduid, bindpwd
|
||||
, binduid_usage=1, rdn_attr='cn', local_groups=0
|
||||
, use_ssl=0 , encryption='SHA', read_only=0
|
||||
, REQUEST=None
|
||||
):
|
||||
""" Factory method to instantiate a LDAPMultiPlugin """
|
||||
# Make sure we really are working in our container (the
|
||||
# PluggableAuthService object)
|
||||
self = self.this()
|
||||
|
||||
# Value needs massaging, there's some magic transcending a simple true
|
||||
# or false expeced by the LDAP delegate :(
|
||||
if use_ssl:
|
||||
use_ssl = 1
|
||||
else:
|
||||
use_ssl = 0
|
||||
|
||||
# Instantiate the folderish adapter object
|
||||
lmp = LDAPMultiPlugin(id, title=title)
|
||||
self._setObject(id, lmp)
|
||||
lmp = getattr(aq_base(self), id)
|
||||
lmp_base = aq_base(lmp)
|
||||
|
||||
# Put the "real" LDAPUserFolder inside it
|
||||
manage_addLDAPUserFolder(lmp)
|
||||
luf = getattr(lmp_base, 'acl_users')
|
||||
|
||||
host_elems = LDAP_server.split(':')
|
||||
host = host_elems[0]
|
||||
if len(host_elems) > 1:
|
||||
port = host_elems[1]
|
||||
else:
|
||||
if use_ssl:
|
||||
port = '636'
|
||||
else:
|
||||
port = '389'
|
||||
|
||||
luf.manage_addServer(host, port=port, use_ssl=use_ssl)
|
||||
luf.manage_edit( title
|
||||
, login_attr
|
||||
, uid_attr
|
||||
, users_base
|
||||
, users_scope
|
||||
, roles
|
||||
, groups_base
|
||||
, groups_scope
|
||||
, binduid
|
||||
, bindpwd
|
||||
, binduid_usage=binduid_usage
|
||||
, rdn_attr=rdn_attr
|
||||
, local_groups=local_groups
|
||||
, encryption=encryption
|
||||
, read_only=read_only
|
||||
, REQUEST=None
|
||||
)
|
||||
|
||||
# clean out the __allow_groups__ bit because it is not needed here
|
||||
# and potentially harmful
|
||||
lmp_base = aq_base(lmp)
|
||||
if hasattr(lmp_base, '__allow_groups__'):
|
||||
del lmp_base.__allow_groups__
|
||||
|
||||
if REQUEST is not None:
|
||||
REQUEST.RESPONSE.redirect('%s/manage_main' % self.absolute_url())
|
||||
|
||||
|
||||
|
||||
class LDAPMultiPlugin(LDAPPluginBase):
|
||||
""" The adapter that mediates between the PAS and the LDAPUserFolder """
|
||||
security = ClassSecurityInfo()
|
||||
meta_type = 'LDAP Multi Plugin'
|
||||
|
||||
security.declarePrivate('getGroupsForPrincipal')
|
||||
def getGroupsForPrincipal(self, user, request=None, attr=None):
|
||||
""" Fulfill GroupsPlugin requirements """
|
||||
view_name = self.getId() + '_getGroupsForPrincipal'
|
||||
criteria = {'id':user.getId(), 'attr':attr}
|
||||
|
||||
cached_info = self.ZCacheable_get(view_name = view_name,
|
||||
keywords = criteria,
|
||||
default = None)
|
||||
|
||||
if cached_info is not None:
|
||||
logger.debug('returning cached results from enumerateUsers')
|
||||
return cached_info
|
||||
|
||||
acl = self._getLDAPUserFolder()
|
||||
|
||||
if acl is None:
|
||||
return ()
|
||||
|
||||
unmangled_userid = self._demangle(user.getId())
|
||||
if unmangled_userid is None:
|
||||
return ()
|
||||
|
||||
ldap_user = acl.getUserById(unmangled_userid)
|
||||
|
||||
if ldap_user is None:
|
||||
return ()
|
||||
|
||||
groups = acl.getGroups(ldap_user.getUserDN(), attr=attr)
|
||||
|
||||
result = tuple([x[0] for x in groups])
|
||||
self.ZCacheable_set(result, view_name=view_name, keywords=criteria)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
security.declarePrivate('enumerateUsers')
|
||||
def enumerateUsers( self
|
||||
, id=None
|
||||
, login=None
|
||||
, exact_match=0
|
||||
, sort_by=None
|
||||
, max_results=None
|
||||
, **kw
|
||||
):
|
||||
""" Fulfill the UserEnumerationPlugin requirements """
|
||||
view_name = self.getId() + '_enumerateUsers'
|
||||
criteria = {'id':id, 'login':login, 'exact_match':exact_match,
|
||||
'sort_by':sort_by, 'max_results':max_results}
|
||||
criteria.update(kw)
|
||||
|
||||
cached_info = self.ZCacheable_get(view_name = view_name,
|
||||
keywords = criteria,
|
||||
default = None)
|
||||
|
||||
if cached_info is not None:
|
||||
logger.debug('returning cached results from enumerateUsers')
|
||||
return cached_info
|
||||
|
||||
result = []
|
||||
acl = self._getLDAPUserFolder()
|
||||
login_attr = acl.getProperty('_login_attr')
|
||||
uid_attr = acl.getProperty('_uid_attr')
|
||||
rdn_attr = acl.getProperty('_rdnattr')
|
||||
plugin_id = self.getId()
|
||||
edit_url = '%s/%s/manage_userrecords' % (plugin_id, acl.getId())
|
||||
|
||||
if acl is None:
|
||||
return ()
|
||||
|
||||
if exact_match and (id or login):
|
||||
if id:
|
||||
ldap_user = acl.getUserById(id)
|
||||
elif login:
|
||||
ldap_user = acl.getUser(login)
|
||||
|
||||
if ldap_user is not None:
|
||||
qs = 'user_dn=%s' % quote_plus(ldap_user.getUserDN())
|
||||
result.append( { 'id' : ldap_user.getId()
|
||||
, 'login' : ldap_user.getProperty(login_attr)
|
||||
, 'pluginid' : plugin_id
|
||||
, 'editurl' : '%s?%s' % (edit_url, qs)
|
||||
} )
|
||||
else:
|
||||
l_results = []
|
||||
seen = []
|
||||
criteria = {}
|
||||
|
||||
if id:
|
||||
if uid_attr == 'dn':
|
||||
# Workaround: Due to the way findUser reacts when a DN
|
||||
# is searched for I need to hack around it... This
|
||||
# limits the usefulness of searching by ID if the user
|
||||
# folder uses the full DN aas user ID.
|
||||
criteria[rdn_attr] = id
|
||||
else:
|
||||
criteria[uid_attr] = id
|
||||
|
||||
if login:
|
||||
criteria[login_attr] = login
|
||||
|
||||
for key, val in kw.items():
|
||||
if key not in (login_attr, uid_attr):
|
||||
criteria[key] = val
|
||||
|
||||
# If no criteria are given create a criteria set that will
|
||||
# return all users
|
||||
if not login and not id:
|
||||
criteria[login_attr] = ''
|
||||
|
||||
l_results = acl.searchUsers(**criteria)
|
||||
|
||||
for l_res in l_results:
|
||||
|
||||
# If the LDAPUserFolder returns an error, bail
|
||||
if ( l_res.get('sn', '') == 'Error' and
|
||||
l_res.get('cn', '') == 'n/a' ):
|
||||
return ()
|
||||
|
||||
if l_res['dn'] not in seen:
|
||||
l_res['id'] = l_res[uid_attr]
|
||||
l_res['login'] = l_res[login_attr]
|
||||
l_res['pluginid'] = plugin_id
|
||||
quoted_dn = quote_plus(l_res['dn'])
|
||||
l_res['editurl'] = '%s?user_dn=%s' % (edit_url, quoted_dn)
|
||||
result.append(l_res)
|
||||
seen.append(l_res['dn'])
|
||||
|
||||
if sort_by is not None:
|
||||
result.sort(lambda a, b: cmp( a.get(sort_by, '').lower()
|
||||
, b.get(sort_by, '').lower()
|
||||
) )
|
||||
|
||||
if isinstance(max_results, int) and len(result) > max_results:
|
||||
result = result[:max_results-1]
|
||||
|
||||
result = tuple(result)
|
||||
self.ZCacheable_set(result, view_name=view_name, keywords=criteria)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
security.declarePrivate('enumerateGroups')
|
||||
def enumerateGroups( self
|
||||
, id=None
|
||||
, exact_match=False
|
||||
, sort_by=None
|
||||
, max_results=None
|
||||
, **kw
|
||||
):
|
||||
""" Fulfill the GroupEnumerationPlugin requirements """
|
||||
view_name = self.getId() + '_enumerateGroups'
|
||||
criteria = {'id':id, 'exact_match':exact_match,
|
||||
'sort_by':sort_by, 'max_results':max_results}
|
||||
criteria.update(kw)
|
||||
|
||||
cached_info = self.ZCacheable_get(view_name = view_name,
|
||||
keywords = criteria,
|
||||
default = None)
|
||||
|
||||
if cached_info is not None:
|
||||
logger.debug('returning cached results from enumerateGroups')
|
||||
return cached_info
|
||||
|
||||
acl = self._getLDAPUserFolder()
|
||||
|
||||
if acl is None:
|
||||
return ()
|
||||
|
||||
if (id is not None and not exact_match and not kw):
|
||||
# likely from a PAS.getUserById(). In any case 'id' and
|
||||
# 'exact_match' means only a single result should be
|
||||
# available so try to fetch specific group info from
|
||||
# cache.
|
||||
group_info = self._getGroupInfoCache(id)
|
||||
if group_info is not None:
|
||||
return (group_info,)
|
||||
|
||||
if id is None and exact_match:
|
||||
raise ValueError, 'Exact Match requested but no id provided'
|
||||
elif id is not None:
|
||||
kw[self.groupid_attr] = id
|
||||
|
||||
plugin_id = self.getId()
|
||||
|
||||
results = acl.searchGroups(exact_match=exact_match, **kw)
|
||||
|
||||
if len(results) == 1 and results[0]['cn'] == 'n/a':
|
||||
# we didn't give enough known criteria for searches
|
||||
return ()
|
||||
|
||||
if isinstance(max_results, int) and len(results) > max_results:
|
||||
results = results[:max_results+1]
|
||||
|
||||
for rec in results:
|
||||
rec['pluginid'] = plugin_id
|
||||
rec['id'] = rec[self.groupid_attr]
|
||||
self._setGroupInfoCache(rec)
|
||||
|
||||
results = tuple(results)
|
||||
self.ZCacheable_set(results, view_name=view_name, keywords=criteria)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
security.declarePrivate('enumerateRoles')
|
||||
def enumerateRoles( self
|
||||
, id=None
|
||||
, exact_match=0
|
||||
, sort_by=None
|
||||
, max_results=None
|
||||
, **kw
|
||||
):
|
||||
""" Fulfill the RoleEnumerationPlugin requirements """
|
||||
# For LDAP, roles and groups are really one and the same thing.
|
||||
# We can simply call enumerateGroups here.
|
||||
return self.enumerateGroups( id=id
|
||||
, exact_match=exact_match
|
||||
, sort_by=sort_by
|
||||
, max_results=max_results
|
||||
, **kw
|
||||
)
|
||||
|
||||
classImplements( LDAPMultiPlugin
|
||||
, IUserEnumerationPlugin
|
||||
, IGroupsPlugin
|
||||
, IGroupEnumerationPlugin
|
||||
, IRoleEnumerationPlugin
|
||||
, *implementedBy(LDAPPluginBase)
|
||||
)
|
||||
|
||||
InitializeClass(LDAPMultiPlugin)
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
##############################################################################
|
||||
#
|
||||
# LDAPPluginBase Base class for LDAP-based PAS-Plugins
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
__doc__ = """ LDAPPluginBase module """
|
||||
__version__ = '$Revision$'[11:-2]
|
||||
|
||||
# General Python imports
|
||||
import copy
|
||||
import logging
|
||||
|
||||
# Zope imports
|
||||
from Acquisition import aq_base
|
||||
from OFS.Folder import Folder
|
||||
from OFS.Cache import Cacheable
|
||||
from Globals import InitializeClass
|
||||
from AccessControl import ClassSecurityInfo
|
||||
from AccessControl.SecurityManagement import getSecurityManager
|
||||
|
||||
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
|
||||
from Products.PluggableAuthService.interfaces.plugins import \
|
||||
IAuthenticationPlugin, IRolesPlugin, ICredentialsResetPlugin, \
|
||||
IPropertiesPlugin
|
||||
from Products.PluggableAuthService.utils import classImplements
|
||||
|
||||
|
||||
logger = logging.getLogger('event.LDAPMultiPlugin')
|
||||
|
||||
|
||||
class LDAPPluginBase(Folder, BasePlugin, Cacheable):
|
||||
""" Base class for LDAP-based PAS plugins """
|
||||
security = ClassSecurityInfo()
|
||||
|
||||
manage_options = ( BasePlugin.manage_options[:1]
|
||||
+ Folder.manage_options
|
||||
+ Cacheable.manage_options
|
||||
)
|
||||
|
||||
_properties = BasePlugin._properties + Folder._properties
|
||||
|
||||
|
||||
# default 'id' attribute for groups
|
||||
groupid_attr = 'cn'
|
||||
|
||||
def __init__(self, id, title=''):
|
||||
""" Initialize a new instance """
|
||||
self.id = id
|
||||
self.title = title
|
||||
|
||||
|
||||
security.declarePrivate('_getLDAPUserFolder')
|
||||
def _getLDAPUserFolder(self):
|
||||
""" Safely retrieve a LDAPUserFolder to work with """
|
||||
embedded_luf = getattr(aq_base(self), 'acl_users', None)
|
||||
|
||||
return embedded_luf
|
||||
|
||||
|
||||
security.declarePrivate('authenticateCredentials')
|
||||
def authenticateCredentials(self, credentials):
|
||||
""" Fulfill AuthenticationPlugin requirements """
|
||||
acl = self._getLDAPUserFolder()
|
||||
login = credentials.get('login')
|
||||
password = credentials.get('password')
|
||||
|
||||
if not acl or not login or not password:
|
||||
return None, None
|
||||
|
||||
user = acl.getUser(login, pwd=password)
|
||||
|
||||
if user is None:
|
||||
return None, None
|
||||
|
||||
return (user.getId(), user.getUserName())
|
||||
|
||||
|
||||
security.declarePrivate('resetCredentials')
|
||||
def resetCredentials(self, request, response):
|
||||
""" Fulfill CredentialsResetPlugin requirements """
|
||||
user = getSecurityManager().getUser()
|
||||
acl = self._getLDAPUserFolder()
|
||||
|
||||
if user:
|
||||
acl._expireUser(user)
|
||||
|
||||
|
||||
security.declarePrivate('getPropertiesForUser')
|
||||
def getPropertiesForUser(self, user, request=None):
|
||||
""" Fullfill PropertiesPlugin requirements """
|
||||
acl = self._getLDAPUserFolder()
|
||||
|
||||
if acl is None:
|
||||
return {}
|
||||
|
||||
unmangled_userid = self._demangle(user.getId())
|
||||
if unmangled_userid is None:
|
||||
return {}
|
||||
|
||||
ldap_user = acl.getUserById(unmangled_userid)
|
||||
|
||||
if ldap_user is None:
|
||||
return {}
|
||||
|
||||
# XXX Direct attribute access. Waaa!
|
||||
properties = copy.deepcopy(ldap_user._properties)
|
||||
|
||||
# Need to clean up: The propertysheet mechanism will
|
||||
# blow up if "None" is encountered
|
||||
for key, val in properties.items():
|
||||
if val is None:
|
||||
properties[key] = ''
|
||||
|
||||
return properties
|
||||
|
||||
|
||||
security.declarePrivate('getRolesForPrincipal')
|
||||
def getRolesForPrincipal(self, user, request=None):
|
||||
""" Fullfill RolesPlugin requirements """
|
||||
acl = self._getLDAPUserFolder()
|
||||
|
||||
if acl is None:
|
||||
return ()
|
||||
|
||||
unmangled_userid = self._demangle(user.getId())
|
||||
if unmangled_userid is None:
|
||||
return ()
|
||||
|
||||
ldap_user = acl.getUserById(unmangled_userid)
|
||||
if ldap_user is None:
|
||||
return ()
|
||||
|
||||
groups = self.getGroupsForPrincipal(user, request)
|
||||
roles = list(acl._mapRoles(groups))
|
||||
roles.extend(acl._roles)
|
||||
|
||||
return tuple(roles)
|
||||
|
||||
|
||||
security.declarePrivate('_demangle')
|
||||
def _demangle(self, princid):
|
||||
# User must start with our prefix (which is likely to be blank anyway)
|
||||
if not princid.startswith(self.prefix):
|
||||
return None
|
||||
return princid[len(self.prefix):]
|
||||
|
||||
# Helper methods for simple group caching
|
||||
security.declarePrivate('_getGroupInfoCacheKey')
|
||||
def _getGroupInfoCacheKey(self, gid):
|
||||
"""_getGroupInfoCacheKey(id) -> (view_name, keywords)
|
||||
|
||||
given a group id, return view_name and keywords to be used when
|
||||
querying and storing into the group cache
|
||||
"""
|
||||
view_name = self.getId() + '__GroupInfoCache'
|
||||
keywords = { 'id' : gid }
|
||||
return view_name, keywords
|
||||
|
||||
security.declarePrivate('_setGroupInfoCache')
|
||||
def _setGroupInfoCache(self, info):
|
||||
"""Cache a group info"""
|
||||
gid = info['id']
|
||||
view_name, keywords = self._getGroupInfoCacheKey(gid)
|
||||
self.ZCacheable_set(info, view_name=view_name, keywords=keywords)
|
||||
|
||||
security.declarePrivate('_getGroupInfoCache')
|
||||
def _getGroupInfoCache(self, gid, default=None):
|
||||
"""Retrieve a group info from cache, given its group id.
|
||||
|
||||
Returns None or the passed-in default if the cache
|
||||
has no group with such id
|
||||
"""
|
||||
view_name, keywords = self._getGroupInfoCacheKey(gid)
|
||||
result = self.ZCacheable_get( view_name=view_name
|
||||
, keywords=keywords
|
||||
, default=default
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
classImplements( LDAPPluginBase
|
||||
, IAuthenticationPlugin
|
||||
, ICredentialsResetPlugin
|
||||
, IPropertiesPlugin
|
||||
, IRolesPlugin
|
||||
)
|
||||
|
||||
InitializeClass(LDAPPluginBase)
|
|
@ -0,0 +1,54 @@
|
|||
Zope Public License (ZPL) Version 2.1
|
||||
|
||||
A copyright notice accompanies this license document that
|
||||
identifies the copyright holders.
|
||||
|
||||
This license has been certified as open source. It has also
|
||||
been designated as GPL compatible by the Free Software
|
||||
Foundation (FSF).
|
||||
|
||||
Redistribution and use in source and binary forms, with or
|
||||
without modification, are permitted provided that the
|
||||
following conditions are met:
|
||||
|
||||
1. Redistributions in source code must retain the
|
||||
accompanying copyright notice, this list of conditions,
|
||||
and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the accompanying
|
||||
copyright notice, this list of conditions, and the
|
||||
following disclaimer in the documentation and/or other
|
||||
materials provided with the distribution.
|
||||
|
||||
3. Names of the copyright holders must not be used to
|
||||
endorse or promote products derived from this software
|
||||
without prior written permission from the copyright
|
||||
holders.
|
||||
|
||||
4. The right to distribute this software or to use it for
|
||||
any purpose does not give you the right to use
|
||||
Servicemarks (sm) or Trademarks (tm) of the copyright
|
||||
holders. Use of them is covered by separate agreement
|
||||
with the copyright holders.
|
||||
|
||||
5. If any files are modified, you must cause the modified
|
||||
files to carry prominent notices stating that you changed
|
||||
the files and the date of any change.
|
||||
|
||||
Disclaimer
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS''
|
||||
AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
|
||||
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
||||
NO EVENT SHALL THE COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
README for the Zope LDAPMultiPlugins Product
|
||||
|
||||
The LDAPMultiPlugins provides PluggableAuthService plugins that use
|
||||
LDAP as the backend for the services they provide. The
|
||||
PluggableAuthService is a Zope user folder product that can be extended
|
||||
in modular fashion using various plugins. See DEPENDENCIES.txt for
|
||||
software needed by this package.
|
||||
|
||||
Please make sure to read the documentation included in the
|
||||
LDAPUserFolder package as well.
|
||||
|
||||
|
||||
**Caching**
|
||||
|
||||
The results of some calls into the plugins provided by these package can
|
||||
be cached using the Zope ZCacheable mechanism:
|
||||
|
||||
- In the Zope Management Interface (ZMI) of your PluggableAuthService
|
||||
instance, select 'RAM Cache Manager' from the dropdown, give it an ID
|
||||
and configure it according to your needs.
|
||||
|
||||
- Click on your LDAP/ActiveDirectoryMultiPlugin and use the 'Cache'
|
||||
ZMI tab on the far right to associate the newly created RAM Cache
|
||||
Manager object with the plugin.
|
||||
|
||||
Now your plugin will use the RAM Cache Manager object to cache results
|
||||
from some of the possibly expensive API calls.
|
||||
|
||||
|
||||
**Special features - Active Directory Multi Plugin**
|
||||
|
||||
* Properties of the ADMultiPlugin instance:
|
||||
|
||||
* groupid_attr - the LDAP attribute used for group ids.
|
||||
|
||||
* grouptitle_attr - the LDAP attribute used to compose group titles.
|
||||
|
||||
* group_class - the LDAP class of group objects.
|
||||
|
||||
* group_recurse - boolean indicating whether to determine group
|
||||
memberships of a user by unrolling nested group relationships
|
||||
(expensive). This feature is not guaranteed to work at this moment.
|
||||
|
||||
|
||||
**Active Directory configuration hints**
|
||||
|
||||
In order for groups support to work correctly, you may have to set the
|
||||
following properties. Every situation is different, but this has helped
|
||||
some people succeed:
|
||||
|
||||
* On the "Properties" tab for the ActiveDirectoryMultiPlugin, set the
|
||||
groupid_attr property to "name".
|
||||
|
||||
* On the contained LDAPUserFolder's "Configure" tab, choose a
|
||||
property other than "objectGUID", e.g. "sAMAccountName" for the
|
||||
User ID property. To get to the LDAPUserFolder, click on the
|
||||
ActiveDirectoryMultiPlugin "Content" tab.
|
||||
|
||||
Please see README.ActiveDirectory from the LDAPUserFolder package for
|
||||
additional information.
|
||||
|
|
@ -0,0 +1 @@
|
|||
1.5
|
|
@ -0,0 +1,44 @@
|
|||
##############################################################################
|
||||
#
|
||||
# __init__.py Initialization code for the LDAP Multi Plugins
|
||||
#
|
||||
# This software is governed by a license. See
|
||||
# LICENSE.txt for the terms of this license.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
__doc__ = """ LDAPUserFolder shims initialization module """
|
||||
__version__ = '$Revision$'[11:-2]
|
||||
|
||||
from AccessControl.Permissions import add_user_folders
|
||||
from Products.PluggableAuthService.PluggableAuthService import \
|
||||
registerMultiPlugin
|
||||
from LDAPMultiPlugin import LDAPMultiPlugin, \
|
||||
manage_addLDAPMultiPlugin, \
|
||||
addLDAPMultiPluginForm
|
||||
from ActiveDirectoryMultiPlugin import ActiveDirectoryMultiPlugin, \
|
||||
manage_addActiveDirectoryMultiPlugin, \
|
||||
addActiveDirectoryMultiPluginForm
|
||||
|
||||
def initialize(context):
|
||||
""" Initialize the LDAPMultiPlugin """
|
||||
registerMultiPlugin(LDAPMultiPlugin.meta_type)
|
||||
registerMultiPlugin(ActiveDirectoryMultiPlugin.meta_type)
|
||||
|
||||
context.registerClass( LDAPMultiPlugin
|
||||
, permission=add_user_folders
|
||||
, constructors=( addLDAPMultiPluginForm
|
||||
, manage_addLDAPMultiPlugin
|
||||
)
|
||||
, icon='www/ldapmultiplugin.png'
|
||||
, visibility=None
|
||||
)
|
||||
|
||||
context.registerClass( ActiveDirectoryMultiPlugin
|
||||
, permission=add_user_folders
|
||||
, constructors=( addActiveDirectoryMultiPluginForm
|
||||
, manage_addActiveDirectoryMultiPlugin
|
||||
)
|
||||
, icon='www/admultiplugin.png'
|
||||
, visibility=None
|
||||
)
|
|
@ -0,0 +1,199 @@
|
|||
<dtml-var manage_page_header>
|
||||
|
||||
|
||||
<dtml-var "manage_form_title(this(), _,
|
||||
form_title='Add Active Directory Multi Plugin to the PluggabbleAuthService',
|
||||
help_product='ActiveDirectoryMultiPlugin',
|
||||
)">
|
||||
|
||||
<p class="form-help">
|
||||
Add a new Active Directory Multi Plugin to the PluggableAuthService with this form.
|
||||
</p>
|
||||
|
||||
<form action="manage_addActiveDirectoryMultiPlugin" method="POST">
|
||||
|
||||
<table cellspacing="0" cellpadding="3">
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
ID
|
||||
</div></td>
|
||||
<td align="left" valign="top" colspan="3">
|
||||
<input type="text" name="id" size="40" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-optional">
|
||||
Title
|
||||
</div></td>
|
||||
<td align="left" valign="top" colspan="3">
|
||||
<input type="text" name="title" size="40" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
LDAP Server[:port]
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<input type="text" name="LDAP_server" size="40"
|
||||
value="my.ldap.server" />
|
||||
</td>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Use SSL <input type="checkbox" name="use_ssl" />
|
||||
</div></td>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Read-only <input type="checkbox" name="read_only" />
|
||||
</div></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Login Name Attribute
|
||||
</div></td>
|
||||
<td align="left" valign="top" colspan="3">
|
||||
<select name="login_attr">
|
||||
<option value="sAMAccountName">Windows Login Name (sAMAccountName)
|
||||
</option>
|
||||
<option value="cn">Canonical Name (cn)</option>
|
||||
<option value="sn">Last Name (sn)</option>
|
||||
<option value="givenName">First Name (givenName)</option>
|
||||
<option value="dn">Distinguished Name (dn)</option>
|
||||
<option value="objectGUID">Object GUID (objectGUID)</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="LEFT" valign="TOP"><div class="form-label">
|
||||
User ID Attribute
|
||||
</div></td>
|
||||
<td align="LEFT" valign="TOP" colspan="3">
|
||||
<select name="uid_attr">
|
||||
<option value="objectGUID">Object GUID (objectGUID)</option>
|
||||
<option value="cn">Canonical Name (cn)</option>
|
||||
<option value="givenName">First Name (givenName)</option>
|
||||
<option value="sn">Last Name (sn)</option>
|
||||
<option value="dn">Distinguished Name (dn)</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
RDN Attribute
|
||||
</div></td>
|
||||
<td align="left" valign="top" colspan="3">
|
||||
<select name="rdn_attr">
|
||||
<option value="cn">Canonical Name (cn)</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Users Base DN
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<input type="text" name="users_base" size="40"
|
||||
value="ou=people,o=Organization,c=US" />
|
||||
</td>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Scope
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<select name="users_scope:int">
|
||||
<option value="0"> BASE </option>
|
||||
<option value="1"> ONELEVEL </option>
|
||||
<option value="2" selected> SUBTREE </options>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Group storage
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<select name="local_groups:int">
|
||||
<option value="0" selected>
|
||||
Groups stored on LDAP server
|
||||
</option>
|
||||
<option value="1">
|
||||
Groups not stored on LDAP server
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Groups Base DN
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<input type="text" name="groups_base" size="40"
|
||||
value="ou=groups,o=Organization,c=US" />
|
||||
</td>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Scope
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<select name="groups_scope:int">
|
||||
<option value="0"> BASE </option>
|
||||
<option value="1"> ONELEVEL </option>
|
||||
<option value="2" selected> SUBTREE </option>
|
||||
</select></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-optional">
|
||||
Manager DN
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<input type="text" name="binduid:string" size="40" />
|
||||
</td>
|
||||
<td align="left" valign="top"><div class="form-optional">
|
||||
Password
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<input type="password" name="bindpwd:string" size="13" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
User password encryption
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<select name="encryption">
|
||||
<option selected> SHA </option>
|
||||
<option> SSHA </option>
|
||||
<option> crypt </option>
|
||||
<option> clear </option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Default User Roles
|
||||
</div></td>
|
||||
<td align="left" valign="top" colspan="3">
|
||||
<input type="text" name="roles" size="40" value="Anonymous" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td colspan="3">
|
||||
<br>
|
||||
<input type="submit" value=" Add ">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<dtml-var manage_page_footer>
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
<dtml-var manage_page_header>
|
||||
|
||||
<dtml-var "manage_form_title(this(), _,
|
||||
form_title='Add LDAP Multi Plugin to the PluggabbleAuthService',
|
||||
help_product='LDAPMultiPlugin',
|
||||
)">
|
||||
|
||||
<p class="form-help">
|
||||
Add a new LDAP Multi Plugin to the PluggableAuthService with this form.
|
||||
</p>
|
||||
|
||||
<form action="manage_addLDAPMultiPlugin" method="POST">
|
||||
|
||||
<table cellspacing="0" cellpadding="3">
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
ID
|
||||
</div></td>
|
||||
<td align="left" valign="top" colspan="3">
|
||||
<input type="text" name="id" size="40" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-optional">
|
||||
Title
|
||||
</div></td>
|
||||
<td align="left" valign="top" colspan="3">
|
||||
<input type="text" name="title" size="40" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
LDAP Server[:port]
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<input type="text" name="LDAP_server" size="40"
|
||||
value="my.ldap.server" />
|
||||
</td>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Use SSL <input type="checkbox" name="use_ssl" />
|
||||
</div></td>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Read-only <input type="checkbox" name="read_only" />
|
||||
</div></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Login Name Attribute
|
||||
</div></td>
|
||||
<td align="left" valign="top" colspan="3">
|
||||
<select name="login_attr">
|
||||
<option value="cn">Canonical Name (cn)</option>
|
||||
<option value="uid">UID (uid)</option>
|
||||
<option value="sn">Surname (sn)</option>
|
||||
<option value="dn">Distinguished Name (dn)</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="LEFT" valign="TOP"><div class="form-label">
|
||||
User ID Attribute
|
||||
</div></td>
|
||||
<td align="LEFT" valign="TOP" colspan="3">
|
||||
<select name="uid_attr">
|
||||
<option value="cn">Canonical Name (cn)</option>
|
||||
<option value="uid">UID (uid)</option>
|
||||
<option value="sn">Surname (sn)</option>
|
||||
<option value="dn">Distinguished Name (dn)</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
RDN Attribute
|
||||
</div></td>
|
||||
<td align="left" valign="top" colspan="3">
|
||||
<select name="rdn_attr">
|
||||
<option value="cn">Canonical Name (cn)</option>
|
||||
<option value="uid">UID (uid)</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Users Base DN
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<input type="text" name="users_base" size="40"
|
||||
value="ou=people,o=Organization,c=US" />
|
||||
</td>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Scope
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<select name="users_scope:int">
|
||||
<option value="0"> BASE </option>
|
||||
<option value="1"> ONELEVEL </option>
|
||||
<option value="2" selected> SUBTREE </options>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Group storage
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<select name="local_groups:int">
|
||||
<option value="0" selected>
|
||||
Groups stored on LDAP server
|
||||
</option>
|
||||
<option value="1">
|
||||
Groups not stored on LDAP server
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Groups Base DN
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<input type="text" name="groups_base" size="40"
|
||||
value="ou=groups,o=Organization,c=US" />
|
||||
</td>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Scope
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<select name="groups_scope:int">
|
||||
<option value="0"> BASE </option>
|
||||
<option value="1"> ONELEVEL </option>
|
||||
<option value="2" selected> SUBTREE </option>
|
||||
</select></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-optional">
|
||||
Manager DN
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<input type="text" name="binduid:string" size="40" />
|
||||
</td>
|
||||
<td align="left" valign="top"><div class="form-optional">
|
||||
Password
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<input type="password" name="bindpwd:string" size="13" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
User password encryption
|
||||
</div></td>
|
||||
<td align="left" valign="top">
|
||||
<select name="encryption">
|
||||
<option selected> SHA </option>
|
||||
<option> SSHA </option>
|
||||
<option> crypt </option>
|
||||
<option> clear </option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" valign="top"><div class="form-label">
|
||||
Default User Roles
|
||||
</div></td>
|
||||
<td align="left" valign="top" colspan="3">
|
||||
<input type="text" name="roles" size="40" value="Anonymous" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td colspan="3">
|
||||
<br>
|
||||
<input type="submit" value=" Add ">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<dtml-var manage_page_footer>
|
||||
|
|
@ -0,0 +1 @@
|
|||
""" This space intentionally left blank """
|
|
@ -0,0 +1,63 @@
|
|||
#####################################################################
|
||||
#
|
||||
# test_LDAPMultiPlugins.py
|
||||
#
|
||||
# This software is governed by a license. See
|
||||
# LICENSE.txt for the terms of this license.
|
||||
#
|
||||
#####################################################################
|
||||
""" Unit tests for LDAPMultiPlugin and ActiveDirectoryMultiPlugin
|
||||
|
||||
$Id$
|
||||
"""
|
||||
|
||||
from unittest import main
|
||||
from unittest import makeSuite
|
||||
from unittest import TestSuite
|
||||
from unittest import TestCase
|
||||
|
||||
from Products.PluggableAuthService.interfaces.plugins import \
|
||||
IUserEnumerationPlugin, IGroupsPlugin, IGroupEnumerationPlugin, \
|
||||
IRoleEnumerationPlugin
|
||||
|
||||
|
||||
class LMPBaseTests(TestCase):
|
||||
|
||||
def _getTargetClass(self):
|
||||
from Products.LDAPMultiPlugins.LDAPPluginBase import LDAPPluginBase
|
||||
return LDAPPluginBase
|
||||
|
||||
|
||||
def test_interfaces(self):
|
||||
from zope.interface.verify import verifyClass
|
||||
|
||||
verifyClass(IUserEnumerationPlugin, self._getTargetClass())
|
||||
verifyClass(IGroupsPlugin, self._getTargetClass())
|
||||
verifyClass(IGroupEnumerationPlugin, self._getTargetClass())
|
||||
verifyClass(IRoleEnumerationPlugin, self._getTargetClass())
|
||||
|
||||
|
||||
class ADMPTests(LMPBaseTests):
|
||||
|
||||
def _getTargetClass(self):
|
||||
from Products.LDAPMultiPlugins.ActiveDirectoryMultiPlugin import \
|
||||
ActiveDirectoryMultiPlugin
|
||||
return ActiveDirectoryMultiPlugin
|
||||
|
||||
|
||||
class LMPTests(LMPBaseTests):
|
||||
|
||||
def _getTargetClass(self):
|
||||
from Products.LDAPMultiPlugins.LDAPMultiPlugin import LDAPMultiPlugin
|
||||
return LDAPMultiPlugin
|
||||
|
||||
|
||||
|
||||
def test_suite():
|
||||
return TestSuite((
|
||||
makeSuite( ADMPTests ),
|
||||
makeSuite( LMPTests ),
|
||||
))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(defaultTest='test_suite')
|
Binary file not shown.
After Width: | Height: | Size: 203 B |
Binary file not shown.
After Width: | Height: | Size: 203 B |
Reference in New Issue