diff --git a/Products/LDAPMultiPlugins/ActiveDirectoryMultiPlugin.py b/Products/LDAPMultiPlugins/ActiveDirectoryMultiPlugin.py new file mode 100644 index 0000000..3f5b078 --- /dev/null +++ b/Products/LDAPMultiPlugins/ActiveDirectoryMultiPlugin.py @@ -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) + diff --git a/Products/LDAPMultiPlugins/CHANGES.txt b/Products/LDAPMultiPlugins/CHANGES.txt new file mode 100644 index 0000000..e29363b --- /dev/null +++ b/Products/LDAPMultiPlugins/CHANGES.txt @@ -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 + diff --git a/Products/LDAPMultiPlugins/COPYRIGHT.txt b/Products/LDAPMultiPlugins/COPYRIGHT.txt new file mode 100644 index 0000000..03e851d --- /dev/null +++ b/Products/LDAPMultiPlugins/COPYRIGHT.txt @@ -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. diff --git a/Products/LDAPMultiPlugins/DEPENDENCIES.txt b/Products/LDAPMultiPlugins/DEPENDENCIES.txt new file mode 100644 index 0000000..5b3a50e --- /dev/null +++ b/Products/LDAPMultiPlugins/DEPENDENCIES.txt @@ -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/) diff --git a/Products/LDAPMultiPlugins/INSTALL.txt b/Products/LDAPMultiPlugins/INSTALL.txt new file mode 100644 index 0000000..4e0a789 --- /dev/null +++ b/Products/LDAPMultiPlugins/INSTALL.txt @@ -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 /lib/python/Products + $ cd /lib/python/Products + $ tar zxvf LDAPMultiPlugins-xyz.tgz + + + 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. + diff --git a/Products/LDAPMultiPlugins/LDAPMultiPlugin.py b/Products/LDAPMultiPlugins/LDAPMultiPlugin.py new file mode 100644 index 0000000..cc62e00 --- /dev/null +++ b/Products/LDAPMultiPlugins/LDAPMultiPlugin.py @@ -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) + diff --git a/Products/LDAPMultiPlugins/LDAPPluginBase.py b/Products/LDAPMultiPlugins/LDAPPluginBase.py new file mode 100644 index 0000000..7ce120d --- /dev/null +++ b/Products/LDAPMultiPlugins/LDAPPluginBase.py @@ -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) diff --git a/Products/LDAPMultiPlugins/LICENSE.txt b/Products/LDAPMultiPlugins/LICENSE.txt new file mode 100644 index 0000000..89be00b --- /dev/null +++ b/Products/LDAPMultiPlugins/LICENSE.txt @@ -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. + diff --git a/Products/LDAPMultiPlugins/README.txt b/Products/LDAPMultiPlugins/README.txt new file mode 100644 index 0000000..37da6b8 --- /dev/null +++ b/Products/LDAPMultiPlugins/README.txt @@ -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. + diff --git a/Products/LDAPMultiPlugins/VERSION.txt b/Products/LDAPMultiPlugins/VERSION.txt new file mode 100644 index 0000000..c239c60 --- /dev/null +++ b/Products/LDAPMultiPlugins/VERSION.txt @@ -0,0 +1 @@ +1.5 diff --git a/Products/LDAPMultiPlugins/__init__.py b/Products/LDAPMultiPlugins/__init__.py new file mode 100644 index 0000000..b8a91b3 --- /dev/null +++ b/Products/LDAPMultiPlugins/__init__.py @@ -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 + ) diff --git a/Products/LDAPMultiPlugins/dtml/addActiveDirectoryMultiPlugin.dtml b/Products/LDAPMultiPlugins/dtml/addActiveDirectoryMultiPlugin.dtml new file mode 100644 index 0000000..2499021 --- /dev/null +++ b/Products/LDAPMultiPlugins/dtml/addActiveDirectoryMultiPlugin.dtml @@ -0,0 +1,199 @@ + + + + + +

+ Add a new Active Directory Multi Plugin to the PluggableAuthService with this form. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ID +
+ +
+ Title +
+ +
+ LDAP Server[:port] +
+ +
+ Use SSL  +
+ Read-only  +
+ Login Name Attribute +
+ +
+ User ID Attribute +
+ +
+ RDN Attribute +
+ +
+ Users Base DN +
+ +
+ Scope +
+ +
+ Group storage +
+ +
+ Groups Base DN +
+ +
+ Scope +
+
+ Manager DN +
+ +
+ Password +
+ +
+ User password encryption +
+ +
+ Default User Roles +
+ +
  +
+ +
+
+ + + diff --git a/Products/LDAPMultiPlugins/dtml/addLDAPMultiPlugin.dtml b/Products/LDAPMultiPlugins/dtml/addLDAPMultiPlugin.dtml new file mode 100644 index 0000000..9e031a0 --- /dev/null +++ b/Products/LDAPMultiPlugins/dtml/addLDAPMultiPlugin.dtml @@ -0,0 +1,195 @@ + + + + +

+ Add a new LDAP Multi Plugin to the PluggableAuthService with this form. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ID +
+ +
+ Title +
+ +
+ LDAP Server[:port] +
+ +
+ Use SSL  +
+ Read-only  +
+ Login Name Attribute +
+ +
+ User ID Attribute +
+ +
+ RDN Attribute +
+ +
+ Users Base DN +
+ +
+ Scope +
+ +
+ Group storage +
+ +
+ Groups Base DN +
+ +
+ Scope +
+
+ Manager DN +
+ +
+ Password +
+ +
+ User password encryption +
+ +
+ Default User Roles +
+ +
  +
+ +
+
+ + + diff --git a/Products/LDAPMultiPlugins/tests/__init__.py b/Products/LDAPMultiPlugins/tests/__init__.py new file mode 100644 index 0000000..049524c --- /dev/null +++ b/Products/LDAPMultiPlugins/tests/__init__.py @@ -0,0 +1 @@ +""" This space intentionally left blank """ diff --git a/Products/LDAPMultiPlugins/tests/test_LDAPMultiPlugins.py b/Products/LDAPMultiPlugins/tests/test_LDAPMultiPlugins.py new file mode 100644 index 0000000..8979f7c --- /dev/null +++ b/Products/LDAPMultiPlugins/tests/test_LDAPMultiPlugins.py @@ -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') diff --git a/Products/LDAPMultiPlugins/www/admultiplugin.png b/Products/LDAPMultiPlugins/www/admultiplugin.png new file mode 100644 index 0000000..ab583d9 Binary files /dev/null and b/Products/LDAPMultiPlugins/www/admultiplugin.png differ diff --git a/Products/LDAPMultiPlugins/www/ldapmultiplugin.png b/Products/LDAPMultiPlugins/www/ldapmultiplugin.png new file mode 100644 index 0000000..ab583d9 Binary files /dev/null and b/Products/LDAPMultiPlugins/www/ldapmultiplugin.png differ