summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJens Vagelpohl <jens@dataflake.org>2010-02-01 12:52:24 +0000
committerJens Vagelpohl <jens@dataflake.org>2010-02-01 12:52:24 +0000
commit0421f78dc346242e71b1b63f07ff02ed88b3ec3c (patch)
tree478d5aa4d8467bf862b73fb86e3b8d4f39d0e393
downloadProducts.LDAPConnector-0421f78dc346242e71b1b63f07ff02ed88b3ec3c.zip
Products.LDAPConnector-0421f78dc346242e71b1b63f07ff02ed88b3ec3c.tar.gz
- initial import
git-svn-id: file:///svn-public/Products.LDAPConnector/trunk@1882 835909ba-7c00-0410-bfa4-884f43845301
-rw-r--r--Products/LDAPConnector/CHANGES.txt6
-rw-r--r--Products/LDAPConnector/COPYRIGHT.txt9
-rw-r--r--Products/LDAPConnector/INSTALL.txt6
-rw-r--r--Products/LDAPConnector/LDAPConnector.py90
-rw-r--r--Products/LDAPConnector/LICENSE.txt54
-rw-r--r--Products/LDAPConnector/README.txt26
-rw-r--r--Products/LDAPConnector/VERSION.txt1
-rw-r--r--Products/LDAPConnector/__init__.py35
-rw-r--r--Products/LDAPConnector/browser/__init__.py16
-rw-r--r--Products/LDAPConnector/browser/cache.py104
-rw-r--r--Products/LDAPConnector/browser/configuration.py62
-rw-r--r--Products/LDAPConnector/browser/configure.zcml37
-rw-r--r--Products/LDAPConnector/browser/search.py109
-rw-r--r--Products/LDAPConnector/browser/servers.py102
-rw-r--r--Products/LDAPConnector/browser/templates/cache.pt112
-rw-r--r--Products/LDAPConnector/browser/templates/configuration.pt85
-rw-r--r--Products/LDAPConnector/browser/templates/search.pt127
-rw-r--r--Products/LDAPConnector/browser/templates/servers.pt153
-rw-r--r--Products/LDAPConnector/browser/testing.zcml37
-rw-r--r--Products/LDAPConnector/browser/tests/__init__.py16
-rw-r--r--Products/LDAPConnector/browser/tests/base.py38
-rw-r--r--Products/LDAPConnector/browser/tests/test_cache.py56
-rw-r--r--Products/LDAPConnector/browser/tests/test_configuration.py49
-rw-r--r--Products/LDAPConnector/browser/tests/test_servers.py103
-rw-r--r--Products/LDAPConnector/configure.zcml40
-rw-r--r--Products/LDAPConnector/exportimport.py139
-rw-r--r--Products/LDAPConnector/profiles/__init__.py17
-rw-r--r--Products/LDAPConnector/profiles/configure.zcml6
-rw-r--r--Products/LDAPConnector/tests/__init__.py16
-rw-r--r--Products/LDAPConnector/tests/dummy.py40
-rw-r--r--Products/LDAPConnector/tests/test_LDAPConnector.py86
-rw-r--r--Products/LDAPConnector/tests/test_exportimport.py331
-rw-r--r--Products/LDAPConnector/tests/test_utils.py112
-rw-r--r--Products/LDAPConnector/upgrade/__init__.py16
-rw-r--r--Products/LDAPConnector/upgrade/configure.zcml6
-rw-r--r--Products/LDAPConnector/utils.py79
-rw-r--r--Products/LDAPConnector/www/add.pt33
-rw-r--r--Products/LDAPConnector/www/ldapconnector.gifbin0 -> 132 bytes
-rw-r--r--Products/LDAPConnector/xml/ldapconnector.xml19
-rw-r--r--Products/__init__.py6
-rw-r--r--README.txt1
-rw-r--r--setup.py55
42 files changed, 2435 insertions, 0 deletions
diff --git a/Products/LDAPConnector/CHANGES.txt b/Products/LDAPConnector/CHANGES.txt
new file mode 100644
index 0000000..ca71472
--- /dev/null
+++ b/Products/LDAPConnector/CHANGES.txt
@@ -0,0 +1,6 @@
+Changelog for Products.LDAPConnector
+====================================
+
+1.0 (unreleased)
+----------------
+- initial release
diff --git a/Products/LDAPConnector/COPYRIGHT.txt b/Products/LDAPConnector/COPYRIGHT.txt
new file mode 100644
index 0000000..a1791d8
--- /dev/null
+++ b/Products/LDAPConnector/COPYRIGHT.txt
@@ -0,0 +1,9 @@
+Copyright (c) 2009 Jens Vagelpohl and Contributors.
+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/LDAPConnector/INSTALL.txt b/Products/LDAPConnector/INSTALL.txt
new file mode 100644
index 0000000..3d9f724
--- /dev/null
+++ b/Products/LDAPConnector/INSTALL.txt
@@ -0,0 +1,6 @@
+Installation
+============
+Products.LDAPConnector is installed as an egg using setuptools[1].
+After installation you need to restart Zope to load the package.
+
+[1] http://peak.telecommunity.com/DevCenter/setuptools
diff --git a/Products/LDAPConnector/LDAPConnector.py b/Products/LDAPConnector/LDAPConnector.py
new file mode 100644
index 0000000..b5f838e
--- /dev/null
+++ b/Products/LDAPConnector/LDAPConnector.py
@@ -0,0 +1,90 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" Persistent Zope2 LDAP connection
+
+$Id$
+"""
+
+import uuid
+
+from dataflake.ldapconnection.connection import LDAPConnection
+from dataflake.ldapconnection.interfaces import ILDAPConnection
+
+from AccessControl import ClassSecurityInfo
+from Acquisition import aq_base
+from App.class_init import default__class_init__ as InitializeClass
+from OFS.SimpleItem import SimpleItem
+from zope.interface import implements
+
+from Products.LDAPConnector.utils import cachedSearch
+from Products.LDAPConnector.utils import invalidateCache
+
+
+class LDAPConnector(LDAPConnection, SimpleItem):
+ """ Persistent representation of the LDAPConnection class
+ """
+ security = ClassSecurityInfo()
+ meta_type = 'LDAPConnector'
+ implements(ILDAPConnection)
+
+ manage_options = (
+ ( {'label': 'Configuration', 'action': 'manage_main'}
+ , {'label': 'Servers', 'action': 'manage_servers'}
+ , {'label': 'Cache', 'action': 'manage_cache'}
+ , {'label': 'Test', 'action': 'manage_search'}
+ ) + SimpleItem.manage_options
+ )
+
+ # Security overrides for methods inherited from the LDAPConnection class
+ security.declarePrivate( 'addServer'
+ , 'logger'
+ , 'removeServer'
+ , 'connect'
+ , 'search'
+ , 'delete'
+ , 'insert'
+ , 'modify'
+ )
+
+ def __init__(self, id, title=''):
+ super(LDAPConnector, self).__init__()
+ self.id = id
+ self.title = title
+ self.hash = uuid.uuid4()
+ self.timeout = 600
+
+ search = cachedSearch(LDAPConnection.search)
+ modify = invalidateCache(LDAPConnection.modify)
+ delete = invalidateCache(LDAPConnection.delete)
+
+InitializeClass(LDAPConnector)
+
+
+def manage_addLDAPConnector(self, id, title='', REQUEST=None):
+ """ Called when adding an instance in the Zope2 ZMI
+ """
+ container = self.this()
+ container_url = container.absolute_url()
+
+ if getattr(aq_base(container), id, None) is not None:
+ msg = 'Duplicate ID, please choose another.'
+ goto = container_url
+ else:
+ container._setObject(id, LDAPConnector(id, title))
+ msg = 'Created LDAPConnector instance %s.' % id
+ goto = '%s/%s' % (container_url, id)
+
+ if REQUEST is not None:
+ qs = 'manage_tabs_message=%s' % msg
+ REQUEST.RESPONSE.redirect('%s/manage_main?%s' % (goto, qs))
+
diff --git a/Products/LDAPConnector/LICENSE.txt b/Products/LDAPConnector/LICENSE.txt
new file mode 100644
index 0000000..89be00b
--- /dev/null
+++ b/Products/LDAPConnector/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/LDAPConnector/README.txt b/Products/LDAPConnector/README.txt
new file mode 100644
index 0000000..3f2c8fe
--- /dev/null
+++ b/Products/LDAPConnector/README.txt
@@ -0,0 +1,26 @@
+========================
+ Products.LDAPConnector
+========================
+
+.. contents::
+
+Bug tracker
+===========
+Please post questions, bug reports or feature requests to the bug tracker
+at http://www.dataflake.org/tracker/
+
+SVN version
+===========
+You can retrieve the latest code from Subversion using setuptools or
+zc.buildout via this URL:
+
+http://svn.dataflake.org/svn/Products.LDAPConnector/trunk#egg=Products.LDAPConnector
+
+Debugging problems
+==================
+All log messages are sent to the standard Zope event log 'event.log'. In
+order to see more verbose logging output you need to increase the log level
+in your Zope instance's zope.conf. See the 'eventlog' directive. Setting
+the 'level' key to 'debug' will maximize log output and may help pinpoint
+problems during setup and testing.
+
diff --git a/Products/LDAPConnector/VERSION.txt b/Products/LDAPConnector/VERSION.txt
new file mode 100644
index 0000000..d822536
--- /dev/null
+++ b/Products/LDAPConnector/VERSION.txt
@@ -0,0 +1 @@
+1.0dev
diff --git a/Products/LDAPConnector/__init__.py b/Products/LDAPConnector/__init__.py
new file mode 100644
index 0000000..0001839
--- /dev/null
+++ b/Products/LDAPConnector/__init__.py
@@ -0,0 +1,35 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" Products.LDAPConnector initialization code
+
+$Id$
+"""
+
+import os
+
+def initialize(context):
+ from App.Common import package_home
+ from Products.LDAPConnector.LDAPConnector import LDAPConnector
+ from Products.LDAPConnector.LDAPConnector import manage_addLDAPConnector
+ from Products.PageTemplates.PageTemplateFile import PageTemplateFile
+
+ _wwwdir = os.path.join(package_home(globals()), 'www')
+
+ context.registerClass( LDAPConnector
+ , permission='LDAPConnector: add'
+ , constructors=( PageTemplateFile('add.pt', _wwwdir)
+ , manage_addLDAPConnector
+ )
+ , icon='www/ldapconnector.gif'
+ )
+
diff --git a/Products/LDAPConnector/browser/__init__.py b/Products/LDAPConnector/browser/__init__.py
new file mode 100644
index 0000000..a3fc95c
--- /dev/null
+++ b/Products/LDAPConnector/browser/__init__.py
@@ -0,0 +1,16 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" Browser views for Products.LDAPConnector
+
+$Id$
+"""
diff --git a/Products/LDAPConnector/browser/cache.py b/Products/LDAPConnector/browser/cache.py
new file mode 100644
index 0000000..e9126ed
--- /dev/null
+++ b/Products/LDAPConnector/browser/cache.py
@@ -0,0 +1,104 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" ZMI caching configuration view for Products.LDAPConnector
+
+$Id$
+"""
+
+from Products.Five import BrowserView
+from Products.LDAPConnector.utils import getCache
+
+_marker = ()
+
+
+class CacheView(BrowserView):
+ """ The ZMI Cache view
+ """
+
+ def __call__(self):
+ """ Render the view
+ """
+ timeout = self.request.get('cache_timeout', _marker)
+ if timeout != _marker:
+ self.setCacheTimeout(timeout)
+
+ invalidation = self.request.get('invalidate_selected', _marker)
+ if invalidation != _marker:
+ self.invalidateItems(self.request.get('identifiers', []))
+
+ invalidation = self.request.get('invalidate_all', _marker)
+ if invalidation != _marker:
+ self.invalidateAll()
+
+ super_view = super(CacheView, self)
+ if getattr(super_view, '__call__', None) is not None:
+ return super_view.__call__()
+
+ def setCacheTimeout(self, timeout):
+ """ Change the lifetime for cached records
+ """
+ try:
+ timeout = int(timeout)
+ except ValueError:
+ self.request.set('manage_tabs_message', 'Invalid timeout value.')
+ return
+
+ self.context.timeout = timeout
+ cache = getCache(self.context.hash, timeout)
+ cache.setTimeout(timeout)
+
+ self.request.set('manage_tabs_message', 'Cache timeout changed.')
+
+ def getCacheTimeout(self):
+ """ Get the current cache timeout value
+ """
+ return getCache(self.context.hash, self.context.timeout).getTimeout()
+
+ def getCacheItems(self):
+ """ Get the cache contents
+ """
+ results = []
+ raw_data = getCache(self.context.hash, self.context.timeout).items()
+
+ for key, values in raw_data:
+ base, scope, fltr, bound_as = key.split(':::')
+ results.append( { 'key': key
+ , 'base': base
+ , 'scope': scope
+ , 'fltr': fltr
+ , 'bound_as': bound_as
+ , 'dns': [x['dn'] for x in values['results']]
+ , 'size': values['size']
+ , 'exception': values.get('exception', '')
+ } )
+ return results
+
+ def invalidateItems(self, identifiers):
+ """ Invalidate specific cache values
+ """
+ if not identifiers:
+ self.request.set('manage_tabs_message', 'No records selected.')
+ return
+
+ for key in identifiers:
+ getCache(self.context.hash, self.context.timeout).invalidate(key)
+
+ self.request.set('manage_tabs_message', 'Records removed from cache.')
+
+ def invalidateAll(self):
+ """ Clear out the cache
+ """
+ getCache(self.context.hash, self.context.timeout).invalidate()
+
+ self.request.set('manage_tabs_message', 'Cache cleared.')
+
diff --git a/Products/LDAPConnector/browser/configuration.py b/Products/LDAPConnector/browser/configuration.py
new file mode 100644
index 0000000..6caff2a
--- /dev/null
+++ b/Products/LDAPConnector/browser/configuration.py
@@ -0,0 +1,62 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" ZMI Configuration view for Products.LDAPConnector
+
+$Id$
+"""
+
+import codecs
+
+from Products.Five import BrowserView
+
+_marker = ()
+
+
+class ConfigurationView(BrowserView):
+ """ The ZMI Configuration view
+ """
+
+ def __call__(self):
+ """ Render the view
+ """
+ if self.request.get('submitted', _marker) != _marker:
+ self.edit()
+
+ super_view = super(ConfigurationView, self)
+ if getattr(super_view, '__call__', None) is not None:
+ return super_view.__call__()
+
+ def edit(self):
+ """ Change LDAPConnector settings
+ """
+ for key in ( 'title'
+ , 'bind_dn'
+ , 'bind_pwd'
+ , 'read_only'
+ ):
+ setattr(self.context, key, self.request.get(key))
+
+ for key in ('ldap_encoding', 'api_encoding'):
+ value = self.request.get(key, _marker)
+ if value:
+ try:
+ codecs.lookup(value)
+ setattr(self.context, key, value)
+ except LookupError:
+ msg = 'Unknown encoding "%s"' % value
+ self.request.set('manage_tabs_message', msg)
+ return
+ elif value != _marker:
+ setattr(self.context, key, value)
+
+ self.request.set('manage_tabs_message', 'Settings changed.')
diff --git a/Products/LDAPConnector/browser/configure.zcml b/Products/LDAPConnector/browser/configure.zcml
new file mode 100644
index 0000000..d733bf8
--- /dev/null
+++ b/Products/LDAPConnector/browser/configure.zcml
@@ -0,0 +1,37 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser">
+
+ <browser:page
+ for="dataflake.ldapconnection.interfaces.ILDAPConnection"
+ name="manage_search"
+ class=".search.SearchView"
+ template="templates/search.pt"
+ permission="ldapconnector.read"
+ />
+
+ <browser:page
+ for="dataflake.ldapconnection.interfaces.ILDAPConnection"
+ name="manage_main"
+ class=".configuration.ConfigurationView"
+ template="templates/configuration.pt"
+ permission="ldapconnector.edit"
+ />
+
+ <browser:page
+ for="dataflake.ldapconnection.interfaces.ILDAPConnection"
+ name="manage_servers"
+ class=".servers.ServersView"
+ template="templates/servers.pt"
+ permission="ldapconnector.edit"
+ />
+
+ <browser:page
+ for="dataflake.ldapconnection.interfaces.ILDAPConnection"
+ name="manage_cache"
+ class=".cache.CacheView"
+ template="templates/cache.pt"
+ permission="ldapconnector.edit"
+ />
+
+</configure>
diff --git a/Products/LDAPConnector/browser/search.py b/Products/LDAPConnector/browser/search.py
new file mode 100644
index 0000000..b86145b
--- /dev/null
+++ b/Products/LDAPConnector/browser/search.py
@@ -0,0 +1,109 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" ZMI search view for Products.LDAPConnector
+
+$Id$
+"""
+
+from dataflake.ldapconnection.utils import BINARY_ATTRIBUTES
+import ldap
+
+from Products.Five import BrowserView
+
+
+class SearchView(BrowserView):
+ """ The ZMI search view
+ """
+
+ def __init__(self, context, request):
+ super(SearchView, self).__init__(context, request)
+ self.results = []
+ self.status = ''
+
+ def __call__(self):
+ """ Render the view
+ """
+ self._do_search()
+ return super(SearchView, self).__call__()
+
+ def _do_search(self):
+ """ Run a LDAP search against the active connection
+
+ This method drives the ZMI `Test` tab. To prevent blowups
+ it will catch LDAP exceptions
+ """
+ searchbase = self.request.get('searchbase')
+ searchscope = self.request.get('scope', 2)
+ searchfilter = self.request.get('fltr')
+
+ if searchbase and searchfilter:
+ try:
+ data = self.context.search( searchbase
+ , searchscope
+ , searchfilter
+ )
+ self.results = self._format_results(data['results'])
+
+ if data.get('exception', None):
+ self.status = data['exception']
+ elif data['size'] == 0:
+ self.status = 'No matching results.'
+ except ldap.LDAPError, e:
+ self.status = repr(e)
+ else:
+ self.status = 'Not enough search criteria provided'
+
+ def _format_results(self, data):
+ """ Helper to make the result output easier to display
+ """
+ for i in range(len(data)):
+ record = data[i]
+ for key, value in record.items():
+
+ if key.lower() in BINARY_ATTRIBUTES:
+ if isinstance(value, (list, tuple)):
+ data_len = len(value[0])
+ else:
+ data_len = len(value)
+ record[key] = '(binary data, %i bytes)' % data_len
+ continue
+
+ if isinstance(value, (list, tuple)):
+ clean = []
+ for val in value:
+ try:
+ val.decode(self.context.api_encoding)
+ clean.append(val)
+ except UnicodeDecodeError, e:
+ clean.append('(non-printable text)')
+ record[key] = ', '.join(clean)
+ else:
+ try:
+ value.decode(self.context.api_encoding)
+ except UnicodeDecodeError:
+ record[key] = '(non-printable text)'
+
+ non_dn_keys = [x for x in record.keys() if
+ x not in ('dn', 'attributes')]
+ non_dn_keys.sort()
+ record['attributes'] = non_dn_keys
+ data[i] = record
+
+ def tree_sort(item):
+ dn_elements = ldap.dn.explode_dn(item['dn'])
+ dn_elements.reverse()
+ return dn_elements
+ data.sort(key=tree_sort)
+
+ return data
+
diff --git a/Products/LDAPConnector/browser/servers.py b/Products/LDAPConnector/browser/servers.py
new file mode 100644
index 0000000..9835511
--- /dev/null
+++ b/Products/LDAPConnector/browser/servers.py
@@ -0,0 +1,102 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" ZMI Server configuration view for Products.LDAPConnector
+
+$Id$
+"""
+
+import copy
+import ldap
+import ldapurl
+
+from Products.Five import BrowserView
+
+_marker = ()
+
+
+class ServersView(BrowserView):
+ """ The ZMI Configuration view
+ """
+
+ def __call__(self):
+ """ Render the view
+ """
+ submitted = self.request.method.lower() == 'post'
+ if submitted:
+ if 'host' in self.request.form.keys():
+ self.add()
+ elif 'svr_remove' in self.request.form.keys():
+ self.remove()
+
+ super_view = super(ServersView, self)
+ if getattr(super_view, '__call__', None) is not None:
+ return super_view.__call__()
+
+ def getServers(self):
+ """ Get the server definitions for display
+ """
+ servers = copy.copy(self.context.servers.values())
+
+ for server in servers:
+ try:
+ test_conn = self.context._connect( server['url']
+ , self.context.bind_dn
+ , self.context.bind_pwd
+ )
+ test_conn.simple_bind_s( self.context.bind_dn
+ , self.context.bind_pwd
+ )
+ test_conn.unbind()
+ server['status'] = 'OK'
+ except ldap.LDAPError, e:
+ server['status'] = repr(e)
+
+ return servers
+
+ def add(self):
+ """ Add a server definition
+ """
+ req = self.request
+ self.context.addServer( req.get('host')
+ , req.get('port', '389')
+ , req.get('protocol', 'ldap')
+ , req.get('connection_timeout', 5)
+ , req.get('operations_timeout', -1)
+ )
+ self.context._p_changed = True
+ req.set('manage_tabs_message', 'LDAP server definition added.')
+
+ def remove(self):
+ """ Remove a server definition
+ """
+ identifiers = self.request.get('identifiers', _marker)
+ if not identifiers:
+ self.request.set( 'manage_tabs_message'
+ , 'No server definition selected.'
+ )
+ return
+
+ for identifier in identifiers:
+ ldap_url = ldapurl.LDAPUrl(identifier)
+ if ':' in ldap_url.hostport:
+ host, port = ldap_url.hostport.split(':')
+ else:
+ host = ldap_url.hostport
+ port = '389'
+ self.context.removeServer(host, port, ldap_url.urlscheme)
+ self.context._p_changed = True
+
+ self.request.set( 'manage_tabs_message'
+ , 'LDAP server definitions removed.'
+ )
+
diff --git a/Products/LDAPConnector/browser/templates/cache.pt b/Products/LDAPConnector/browser/templates/cache.pt
new file mode 100644
index 0000000..ba1b6c9
--- /dev/null
+++ b/Products/LDAPConnector/browser/templates/cache.pt
@@ -0,0 +1,112 @@
+<h1 tal:replace="structure context/manage_page_header">HEADER</h1>
+<h1 tal:define="management_view string:Cache"
+ tal:replace="structure context/manage_tabs">TABS</h1>
+
+<p class="form-help">
+ Use this view to determine how long LDAP search results are cached
+ and examine the cache contents.<br/>
+ <i>Please note: Changing the cache timeout does not affect records
+ already in the cache.</i>
+</p>
+
+<form action="manage_cache" method="post">
+ <span class="form-label"> Cache timeout (seconds) </span>
+ <input type="text" size="10" name="cache_timeout" value=""
+ tal:attributes="value view/getCacheTimeout"/>
+ <input type="submit" value=" Change "/>
+</form>
+
+<p><br/></p>
+
+<script type="text/javascript">
+<!--
+ function toggle_visibility(id) {
+ var e = document.getElementById(id);
+ var plus = document.getElementById(id + '_plus');
+ var minus = document.getElementById(id + '_minus');
+ if(e.style.display == '') {
+ e.style.display = 'none';
+ plus.style.display = '';
+ minus.style.display = 'none';
+ }
+ else {
+ e.style.display = '';
+ plus.style.display = 'none';
+ minus.style.display = '';
+ }
+ }
+//-->
+</script>
+
+<form action="manage_cache" method="post">
+
+ <table cellspacing="0" cellpadding="2" width="98%"
+ tal:define="cached view/getCacheItems;
+ timeout view/getCacheTimeout">
+
+ <tr class="list-header">
+ <td colspan="3" class="form-label"> Cache contents </td>
+ </tr>
+
+ <tr tal:repeat="rec cached">
+ <td width="16" valign="top">
+ <input type="checkbox" name="identifiers:list" value=""
+ tal:attributes="value rec/key"/>
+ </td>
+ <td width="16" valign="top">
+ <a href="#" id="" onClick=""
+ tal:condition="rec/size|nothing"
+ tal:attributes="onClick string:toggle_visibility('${rec/key}');; return false;;"
+ ><img src="/p_/pl" alt="+" border="0" id=""
+ tal:attributes="id string:${rec/key}_plus"
+ /><img src="/p_/mi" alt="-" border="0" style="display: none;" id=""
+ tal:attributes="id string:${rec/key}_minus"/></a>
+
+ </td>
+ <td class="form-text">
+ <strong>
+ <a href="#" onClick=""
+ tal:attributes="onClick string:toggle_visibility('${rec/key}');; return false;;"
+ tal:content="string:${rec/fltr} scope ${rec/scope} from ${rec/base} bound as ${rec/bound_as}"
+ >dc=localhost</a>
+ </strong>
+ <i tal:condition="rec/size|nothing"
+ tal:content="string: (${rec/size} values)">(10 values)</i>
+ <i style="color:red;"
+ tal:condition="rec/exception|nothing"
+ tal:content="string:(${rec/exception})">(Error!)</i>
+ <div id="" style="display: none;"
+ tal:attributes="id rec/key">
+ <tal:block repeat="dn rec/dns">
+ <span tal:replace="dn"/><br/>
+ </tal:block>
+ </div>
+ </td>
+ </tr>
+
+ <tr tal:condition="cached|nothing">
+ <td width="16" class="form-label"> </td>
+ <td colspan="2" class="form-label">
+ <input type="submit" name="invalidate_selected" value=" Delete "/>
+ <input type="submit" name="invalidate_all" value=" Delete all "/>
+ </td>
+ </tr>
+
+ <tr tal:condition="not:timeout|nothing">
+ <td colspan="3" class="form-text">
+ Caching is disabled. Enable caching by setting a cache timeout
+ value &gt;0 (see above).
+ </td>
+ </tr>
+
+ <tr tal:condition="not: cached|nothing">
+ <td colspan="3" class="form-text">
+ The cache is empty.
+ </td>
+ </tr>
+
+ </table>
+
+</form>
+
+<h1 tal:replace="structure context/manage_page_footer">FOOTER</h1>
diff --git a/Products/LDAPConnector/browser/templates/configuration.pt b/Products/LDAPConnector/browser/templates/configuration.pt
new file mode 100644
index 0000000..c62367f
--- /dev/null
+++ b/Products/LDAPConnector/browser/templates/configuration.pt
@@ -0,0 +1,85 @@
+<h1 tal:replace="structure context/manage_page_header">HEADER</h1>
+
+<h1 tal:define="management_view string:Configuration"
+ tal:replace="structure context/manage_tabs">TABS</h1>
+
+<p class="form-help">
+ Use this view to manage shared settings for all LDAP connections
+ defined on the <i>Servers</i> tab.
+</p>
+
+<form action="manage_main" method="post">
+
+ <table cellspacing="0" cellpadding="2" width="98%">
+
+ <tr>
+ <td align="left" valign="top" class="form-optional">
+ Title
+ </td>
+ <td align="left" valign="top" colspan="3">
+ <input type="text" name="title" size="40" value=""
+ tal:attributes="value context/title"/>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="left" valign="top" class="form-optional">
+ LDAP login
+ </td>
+ <td align="left" valign="top">
+ <input type="text" name="bind_dn:string" size="40" value=""
+ tal:attributes="value context/bind_dn"/>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="left" valign="top" class="form-optional">
+ LDAP password
+ </td>
+ <td align="left" valign="top">
+ <input type="password" name="bind_pwd:string" size="13" value=""
+ tal:attributes="value context/bind_pwd"/>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="left" valign="top" class="form-label">
+ LDAP server string encoding
+ </td>
+ <td align="left" valign="top">
+ <input type="text" name="ldap_encoding:string" size="40" value=""
+ tal:attributes="value context/ldap_encoding"/>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="left" valign="top" class="form-label">
+ API string encoding
+ </td>
+ <td align="left" valign="top">
+ <input type="text" name="api_encoding:string" size="40" value=""
+ tal:attributes="value context/api_encoding"/>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="left" valign="top" class="form-label">
+ Read-only
+ </td>
+ <td align="left" valign="top">
+ <input type="checkbox" name="read_only"
+ tal:attributes="checked context/read_only"/>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="2">
+ <input type="submit" value=" Change " name="submitted">
+ </td>
+ </tr>
+
+ </table>
+
+</form>
+
+<h1 tal:replace="structure context/manage_page_footer">FOOTER</h1>
diff --git a/Products/LDAPConnector/browser/templates/search.pt b/Products/LDAPConnector/browser/templates/search.pt
new file mode 100644
index 0000000..86d46f0
--- /dev/null
+++ b/Products/LDAPConnector/browser/templates/search.pt
@@ -0,0 +1,127 @@
+<h1 tal:replace="structure context/manage_page_header">HEADER</h1>
+
+<h1 tal:define="management_view string:Test"
+ tal:replace="structure context/manage_tabs">TABS</h1>
+
+<p class="form-help">
+ Test the connection by performing a LDAP search.
+</p>
+
+<form action="manage_search" method="post">
+
+ <table cellspacing="0" cellpadding="2" width="98%">
+
+
+ <tr>
+ <td align="left" valign="top" class="form-label">
+ Search base
+ </td>
+ <td align="left" valign="top" colspan="3">
+ <input type="text" name="searchbase" size="40" value=""
+ tal:attributes="value request/searchbase|string:"/>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="left" valign="top" class="form-label">
+ Search scope
+ </td>
+ <td align="left" valign="top">
+ <select name="scope:int"
+ tal:define="chosen request/scope|string:2">
+ <option value="0"
+ tal:attributes="selected python: int(chosen) == 0">
+ Base (only search the search base record)
+ </option>
+ <option value="1"
+ tal:attributes="selected python: int(chosen) == 1">
+ Onelevel (search only the level under the search base)
+ </option>
+ <option value="2"
+ tal:attributes="selected python: int(chosen) == 2">
+ Subtree (search everything underneath and including the search base)
+ </option>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td align="left" valign="top" class="form-label">
+ LDAP filter expression
+ </td>
+ <td align="left" valign="top">
+ <input type="text" name="fltr" size="40" value=""
+ tal:attributes="value request/fltr|string:(objectClass=*)"/>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="2">
+ <input type="submit" name="start_search" value=" Search ">
+ </td>
+ </tr>
+
+ </table>
+
+</form>
+
+<script type="text/javascript">
+<!--
+ function toggle_visibility(id) {
+ var e = document.getElementById(id);
+ var plus = document.getElementById(id + '_plus');
+ var minus = document.getElementById(id + '_minus');
+ if(e.style.display == '') {
+ e.style.display = 'none';
+ plus.style.display = '';
+ minus.style.display = 'none';
+ }
+ else {
+ e.style.display = '';
+ plus.style.display = 'none';
+ minus.style.display = '';
+ }
+ }
+//-->
+</script>
+
+<table width="98%"
+ tal:condition="request/start_search|nothing">
+
+ <tr class="list-header">
+ <td class="form-label" colspan="2"> Search results </td>
+ </tr>
+
+ <tr tal:condition="view/status">
+ <td class="form-text" colspan="2"
+ tal:content="view/status"> Error message </td>
+ </tr>
+
+ <tr tal:repeat="rec view/results">
+ <td width="16" valign="top" style="white-space: nowrap">
+ <a href="#" id="" onClick=""
+ tal:attributes="onClick string:toggle_visibility('${rec/dn}');; return false;;"
+ ><img src="/p_/pl" alt="+" border="0" id=""
+ tal:attributes="id string:${rec/dn}_plus"
+ /><img src="/p_/mi" alt="-" border="0" style="display: none;" id=""
+ tal:attributes="id string:${rec/dn}_minus"/></a>
+ </td>
+ <td class="form-text">
+ <strong>
+ <a href="#" onClick=""
+ tal:attributes="onClick string:toggle_visibility('${rec/dn}');; return false;;"
+ tal:content="rec/dn"> cn=foo, dc=localhost </a>
+ </strong>
+ <div id="" style="{display: none;}"
+ tal:attributes="id rec/dn">
+ <tal:block repeat="key rec/attributes">
+ <span tal:replace="string:${key}: ${rec/?key}"/><br/>
+ </tal:block>
+ </div>
+ </td>
+ </tr>
+
+</table>
+
+
+<h1 tal:replace="structure context/manage_page_footer">FOOTER</h1>
+
diff --git a/Products/LDAPConnector/browser/templates/servers.pt b/Products/LDAPConnector/browser/templates/servers.pt
new file mode 100644
index 0000000..f5539a8
--- /dev/null
+++ b/Products/LDAPConnector/browser/templates/servers.pt
@@ -0,0 +1,153 @@
+<h1 tal:replace="structure context/manage_page_header">HEADER</h1>
+<h1 tal:define="management_view string:Servers"
+ tal:replace="structure context/manage_tabs">TABS</h1>
+
+<p class="form-help">
+ Use this view to manage LDAP server configurations.
+</p>
+
+<form action="manage_servers" method="post">
+
+ <table cellspacing="0" cellpadding="2" width="98%"
+ tal:define="servers view/getServers">
+
+ <tr class="list-header">
+ <td colspan="5" class="form-label"> LDAP server definitions </td>
+ </tr>
+
+ <tr tal:condition="servers|nothing">
+ <td width="16" class="form-label"> </td>
+ <td class="form-label">URL</td>
+ <td class="form-label">Connect timeout (secs)</td>
+ <td class="form-label">Operations timeout (secs)</td>
+ <td class="form-label">Status</td>
+ </tr>
+
+ <tr tal:repeat="svr servers">
+ <td width="16">
+ <input type="checkbox" name="identifiers:list" value=""
+ tal:attributes="value svr/url"/>
+ </td>
+ <td class="form-text" tal:content="svr/url">
+ ldap://localhost:389/
+ </td>
+ <td class="form-text"
+ tal:define="tm python: int(svr['conn_timeout']);
+ tm_str python: tm > -1 and tm or 'No timeout'"
+ tal:content="tm_str">
+ 5
+ </td>
+ <td class="form-text"
+ tal:define="tm python: int(svr['op_timeout']);
+ tm_str python: tm > -1 and tm or 'No timeout'"
+ tal:content="tm_str">
+ 5
+ </td>
+ <td class="form-text" tal:content="svr/status">OK</td>
+
+ </tr>
+
+ <tr tal:condition="servers|nothing">
+ <td width="16"> </td>
+ <td colspan="4">
+ <input type="submit" value=" Delete " name="svr_remove">
+ </td>
+ </tr>
+
+ <tr tal:condition="not: servers|nothing">
+ <td colspan="5" class="form-text">
+ No servers defined
+ </td>
+ </tr>
+
+ </table>
+
+</form>
+
+<p><br/></p>
+
+<form action="manage_servers" method="post">
+
+ <table cellspacing="0" cellpadding="2" width="98%">
+
+ <tr class="list-header">
+ <td colspan="2" class="form-label"> Add LDAP server definition </td>
+ </tr>
+
+ <tr>
+ <td class="form-label">
+ Host, IP or filesystem socket path
+ </td>
+ <td class="form-element">
+ <input type="text" size="40" name="host" value="my.ldap.server">
+ </td>
+ </tr>
+
+ <tr>
+ <td class="form-label">
+ Port
+ </td>
+ <td class="form-element">
+ <input type="text" size="6" name="port" value="389">
+ </td>
+ </tr>
+
+ <tr>
+ <td class="form-label">
+ Protocol
+ </td>
+ <td class="form-element">
+ <select name="protocol">
+ <option value="ldap" selected> ldap: LDAP </option>
+ <option value="ldaps"> ldaps: LDAP over SSL </option>
+ <option value="ldapi">
+ ldapi: LDAP over IPC (filesystem socket)
+ </option>
+ </select>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="form-label">
+ Connection Timeout
+ </td>
+ <td class="form-element">
+ <select name="connection_timeout:int">
+ <option value="2"> 2 seconds </option>
+ <option value="5" selected> 5 seconds </option>
+ <option value="10"> 10 seconds </option>
+ <option value="30"> 30 seconds </option>
+ <option value="60"> 60 seconds </option>
+ <option value="-1"> No timeout </option>
+ </select>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="form-label">
+ Operation Timeout
+ </td>
+ <td class="form-element">
+ <select name="operations_timeout:int">
+ <option value="5"> 5 seconds </option>
+ <option value="10"> 10 seconds </option>
+ <option value="30"> 30 seconds </option>
+ <option value="60"> 60 seconds </option>
+ <option value="120"> 120 seconds </option>
+ <option value="300"> 300 seconds </option>
+ <option value="-1" selected> No timeout </option>
+ </select>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="2">
+ <input type="submit" name=" Add " value=" Add Server " name="svr_add">
+ </td>
+ </tr>
+
+ </table>
+
+</form>
+
+<h1 tal:replace="structure context/manage_page_footer">FOOTER</h1>
diff --git a/Products/LDAPConnector/browser/testing.zcml b/Products/LDAPConnector/browser/testing.zcml
new file mode 100644
index 0000000..f5f81f9
--- /dev/null
+++ b/Products/LDAPConnector/browser/testing.zcml
@@ -0,0 +1,37 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser">
+
+ <browser:page
+ for="dataflake.ldapconnection.interfaces.ILDAPConnection"
+ name="manage_search"
+ class=".search.SearchView"
+ template="templates/search.pt"
+ permission="zope.Public"
+ />
+
+ <browser:page
+ for="dataflake.ldapconnection.interfaces.ILDAPConnection"
+ name="manage_main"
+ class=".configuration.ConfigurationView"
+ template="templates/configuration.pt"
+ permission="zope.Public"
+ />
+
+ <browser:page
+ for="dataflake.ldapconnection.interfaces.ILDAPConnection"
+ name="manage_servers"
+ class=".servers.ServersView"
+ template="templates/servers.pt"
+ permission="zope.Public"
+ />
+
+ <browser:page
+ for="dataflake.ldapconnection.interfaces.ILDAPConnection"
+ name="manage_caches"
+ class=".caches.CachesView"
+ template="templates/caches.pt"
+ permission="zope.Public"
+ />
+
+</configure>
diff --git a/Products/LDAPConnector/browser/tests/__init__.py b/Products/LDAPConnector/browser/tests/__init__.py
new file mode 100644
index 0000000..ece80b7
--- /dev/null
+++ b/Products/LDAPConnector/browser/tests/__init__.py
@@ -0,0 +1,16 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" Products.LDAPConnector browser view tests
+
+$Id$
+"""
diff --git a/Products/LDAPConnector/browser/tests/base.py b/Products/LDAPConnector/browser/tests/base.py
new file mode 100644
index 0000000..6dc3f06
--- /dev/null
+++ b/Products/LDAPConnector/browser/tests/base.py
@@ -0,0 +1,38 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" LDAPConnector browser view tests base class
+
+$Id$
+"""
+
+import unittest
+
+class ViewTests(unittest.TestCase):
+
+ def _makeOne(self, context, *args, **kw):
+ return self._getTargetClass()(context, *args, **kw)
+
+ def _getTargetClass(self):
+ # Must be overridden
+ raise NotImplemented
+
+ def _makeConnector(self, id='testing', *args, **kw):
+ from Products.LDAPConnector.LDAPConnector import LDAPConnector
+ return LDAPConnector(id, *args, **kw)
+
+ def setUp(self):
+ from Products.LDAPConnector.tests.dummy import DummyRequest
+ super(ViewTests, self).setUp()
+ self.request = DummyRequest()
+ self.conn = self._makeConnector()
+ self.view = self._makeOne(self.conn, self.request)
diff --git a/Products/LDAPConnector/browser/tests/test_cache.py b/Products/LDAPConnector/browser/tests/test_cache.py
new file mode 100644
index 0000000..107ddbd
--- /dev/null
+++ b/Products/LDAPConnector/browser/tests/test_cache.py
@@ -0,0 +1,56 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" LDAPConnector cache browser view tests
+
+$Id$
+"""
+
+import unittest
+
+from Products.LDAPConnector.browser.tests.base import ViewTests
+
+class CacheViewTests(ViewTests):
+
+ def _getTargetClass(self):
+ from Products.LDAPConnector.browser.cache import CacheView
+ return CacheView
+
+ def test_timeout_default(self):
+ self.view()
+
+ self.assertEquals(self.conn.timeout, 600)
+ self.assertEquals(self.view.getCacheTimeout(), 600)
+
+ def test_set_timeout(self):
+ self.request.form['cache_timeout'] = '999'
+
+ self.view()
+
+ self.assertEquals(self.conn.timeout, 999)
+ self.assertEquals(self.view.getCacheTimeout(), 999)
+
+ def test_set_timeout_invalid(self):
+ self.request.form['cache_timeout'] = 'INVALID'
+
+ self.view()
+
+ self.assertEquals(self.conn.timeout, 600)
+ self.assertEquals(self.view.getCacheTimeout(), 600)
+ self.failUnless(self.request.get('manage_tabs_message'))
+
+
+def test_suite():
+ return unittest.TestSuite((
+ unittest.makeSuite(CacheViewTests),
+ ))
+
diff --git a/Products/LDAPConnector/browser/tests/test_configuration.py b/Products/LDAPConnector/browser/tests/test_configuration.py
new file mode 100644
index 0000000..7d9f7f6
--- /dev/null
+++ b/Products/LDAPConnector/browser/tests/test_configuration.py
@@ -0,0 +1,49 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" LDAPConnector configuration browser view tests
+
+$Id$
+"""
+
+import unittest
+
+from Products.LDAPConnector.browser.tests.base import ViewTests
+
+class ConfigurationViewTests(ViewTests):
+
+ def _getTargetClass(self):
+ from Products.LDAPConnector.browser.configuration import \
+ ConfigurationView
+ return ConfigurationView
+
+ def test_editing(self):
+ self.request.form['title'] = 'New Title'
+ self.request.form['bind_dn'] = 'cn=Manager,dc=localhost'
+ self.request.form['bind_pwd'] = 'secret'
+ self.request.form['read_only'] = 'on'
+ self.request.form['submitted'] = 'submitted'
+
+ self.view()
+
+ self.failUnless(self.request.get('manage_tabs_message'))
+ self.assertEquals(self.conn.title, 'New Title')
+ self.assertEquals(self.conn.bind_dn, 'cn=Manager,dc=localhost')
+ self.assertEquals(self.conn.bind_pwd, 'secret')
+ self.failUnless(self.conn.read_only)
+
+
+def test_suite():
+ return unittest.TestSuite((
+ unittest.makeSuite(ConfigurationViewTests),
+ ))
+
diff --git a/Products/LDAPConnector/browser/tests/test_servers.py b/Products/LDAPConnector/browser/tests/test_servers.py
new file mode 100644
index 0000000..09eb8b8
--- /dev/null
+++ b/Products/LDAPConnector/browser/tests/test_servers.py
@@ -0,0 +1,103 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" LDAPConnector servers browser view tests
+
+$Id$
+"""
+
+import unittest
+
+from Products.LDAPConnector.browser.tests.base import ViewTests
+
+class ServersViewTests(ViewTests):
+
+ def _getTargetClass(self):
+ from Products.LDAPConnector.browser.servers import \
+ ServersView
+ return ServersView
+
+ def test_add_remove_server(self):
+ self.failIf(self.conn.servers)
+ self.failIf(self.view.getServers())
+
+ self.request.method = 'POST'
+ self.request.form['host'] = 'server1.dom.com'
+ self.request.form['port'] = '636'
+ self.request.form['protocol'] = 'ldaps'
+ self.request.form['operations_timeout'] = 5
+ self.request.form['connection_timeout'] = 10
+ self.view()
+
+ self.request.form['host'] = 'server2.dom.com'
+ self.request.form['port'] = '1389'
+ self.request.form['protocol'] = 'ldap'
+ self.request.form['operations_timeout'] = 2
+ self.request.form['connection_timeout'] = 1
+ self.view()
+
+ servers = self.conn.servers.values()
+ svr1 = { 'url': 'ldaps://server1.dom.com:636'
+ , 'op_timeout': 5
+ , 'conn_timeout': 10
+ }
+ svr2 = { 'url': 'ldap://server2.dom.com:1389'
+ , 'op_timeout': 2
+ , 'conn_timeout': 1
+ }
+
+ self.assertEquals(len(servers), 2)
+ self.failUnless(svr1 in servers)
+ self.failUnless(svr2 in servers)
+
+ self.request.form.clear()
+ self.request.form['svr_remove'] = True
+ self.request.form['identifiers'] = ['ldaps://server1.dom.com:636']
+ self.view()
+ self.assertEquals(self.conn.servers.values(), [svr2])
+
+ self.request.form['identifiers'] = ['ldapi:///unknown']
+ self.view()
+ self.assertEquals(self.conn.servers.values(), [svr2])
+
+ self.request.form['identifiers'] = ['ldap://server2.dom.com:1389']
+ self.view()
+ self.failIf(self.conn.servers)
+ self.failIf(self.view.getServers())
+
+ def test_remove_server_noselection(self):
+ self.request.method = 'POST'
+ self.request.form['svr_remove'] = True
+ self.request.form['identifiers'] = []
+ self.view()
+ self.failUnless(self.request.get('manage_tabs_message'))
+
+ def test_getServers(self):
+ self.request.method = 'POST'
+ self.request.form['host'] = 'server1.dom.com'
+ self.request.form['port'] = '636'
+ self.request.form['protocol'] = 'ldaps'
+ self.request.form['operations_timeout'] = 1
+ self.request.form['connection_timeout'] = 1
+ self.view()
+
+ server_info = self.view.getServers()
+ self.assertEquals(len(server_info), 1)
+ self.failUnless(server_info[0]['status'])
+ self.assertNotEquals(server_info[0]['status'], 'OK')
+
+
+def test_suite():
+ return unittest.TestSuite((
+ unittest.makeSuite(ServersViewTests),
+ ))
+
diff --git a/Products/LDAPConnector/configure.zcml b/Products/LDAPConnector/configure.zcml
new file mode 100644
index 0000000..f9ae32a
--- /dev/null
+++ b/Products/LDAPConnector/configure.zcml
@@ -0,0 +1,40 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:zcml="http://namespaces.zope.org/zcml"
+ xmlns:pluggableauthservice="http://namespaces.zope.org/pluggableauthservice"
+ i18n_domain="ldapconnector"
+ >
+
+ <permission id="ldapconnector.add" title="LDAPConnector: add"/>
+ <permission id="ldapconnector.edit" title="LDAPConnector: edit"/>
+ <permission id="ldapconnector.read" title="LDAPConnector: read"/>
+ <permission id="ldapconnector.write" title="LDAPConnector: write"/>
+
+ <include package=".browser"/>
+
+ <include package=".upgrade"/>
+
+ <include package=".profiles"/>
+
+ <adapter
+ factory=".exportimport.LDAPConnectorExportImport"
+ provides="Products.GenericSetup.interfaces.IFilesystemImporter"
+ for="dataflake.ldapconnection.interfaces.ILDAPConnection"
+ />
+
+ <adapter
+ factory=".exportimport.LDAPConnectorExportImport"
+ provides="Products.GenericSetup.interfaces.IFilesystemExporter"
+ for="dataflake.ldapconnection.interfaces.ILDAPConnection"
+ />
+
+ <configure zcml:condition="installed Products.PluggableAuthService">
+
+ <pluggableauthservice:registerMultiPlugin
+ meta_type="LDAPConnector"
+ />
+
+ </configure>
+
+
+</configure>
diff --git a/Products/LDAPConnector/exportimport.py b/Products/LDAPConnector/exportimport.py
new file mode 100644
index 0000000..3c4bb8c
--- /dev/null
+++ b/Products/LDAPConnector/exportimport.py
@@ -0,0 +1,139 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" GenericSetup support for LDAPConnector instances
+
+$Id$
+"""
+
+import ldapurl
+from xml.dom.minidom import parseString
+
+from Acquisition import Implicit
+from zope.interface import implements
+
+from Products.GenericSetup.interfaces import IFilesystemExporter
+from Products.GenericSetup.interfaces import IFilesystemImporter
+from Products.PageTemplates.PageTemplateFile import PageTemplateFile
+
+
+class LDAPConnectorExportImport(Implicit):
+ """ Exporter/Importer for LDAPConnector instances
+ """
+ implements(IFilesystemExporter, IFilesystemImporter)
+
+ encoding = None
+
+ def __init__(self, context):
+ self.context = context
+
+ def export(self, export_context, subdir, root=False):
+ """ See IFilesystemExporter.
+ """
+ template = PageTemplateFile( 'xml/ldapconnector.xml'
+ , globals()
+ ).__of__(self.context)
+ info = { 'title': self.context.title
+ , 'bind_dn': self.context.bind_dn
+ , 'bind_pwd': self.context.bind_pwd
+ , 'read_only': int(bool(self.context.read_only))
+ }
+ servers = []
+ for svr in self.context.servers.values():
+ ldap_url = ldapurl.LDAPUrl(svr['url'])
+ if ':' in ldap_url.hostport:
+ host, port = ldap_url.hostport.split(':')
+ else:
+ host = ldap_url.hostport
+ port = '389'
+ servers.append( { 'host': host
+ , 'port': port
+ , 'protocol': ldap_url.urlscheme
+ , 'connection_timeout': svr['conn_timeout']
+ , 'operations_timeout': svr['op_timeout']
+ }
+ )
+ info['servers'] = servers
+
+ export_context.writeDataFile( '%s.xml' % self.context.getId()
+ , template(info=info)
+ , 'text/xml'
+ , subdir
+ )
+
+ def listExportableItems(self):
+ """ See IFilesystemExporter.
+ """
+ return ()
+
+ def import_(self, import_context, subdir, root=False):
+ """ See IFilesystemImporter
+ """
+ self.encoding = import_context.getEncoding()
+
+ if import_context.shouldPurge():
+ self.context.__init__(self.context.id, self.context.title)
+
+ data = import_context.readDataFile( '%s.xml' % self.context.getId()
+ , subdir
+ )
+
+ if data is not None:
+ dom = parseString(data)
+ root = dom.firstChild
+
+ self.context.title = self._getNodeAttr(root, 'title', '')
+ self.context.bind_dn = self._getNodeAttr(root, 'bind_dn', '')
+ self.context.bind_pwd = self._getNodeAttr(root, 'bind_pwd', '')
+ self.context.read_only = self._getNodeAttr(root, 'read_only', False)
+
+ for server in root.getElementsByTagName('server'):
+ host = self._getNodeAttr(server, 'host')
+
+ if not host:
+ continue
+
+ port = self._getNodeAttr(server, 'port', 389)
+ protocol = self._getNodeAttr(server, 'protocol', 'ldap')
+ try:
+ ct = int(self._getNodeAttr(server, 'connection_timeout', 5))
+ except ValueError:
+ ct = 5
+ try:
+ ot = int(self._getNodeAttr(server, 'operations_timeout', -1))
+ except ValueError:
+ ot = -1
+
+ self.context.addServer( host
+ , port
+ , protocol
+ , conn_timeout=ct
+ , op_timeout=ot
+ )
+
+ def _getNodeAttr(self, node, attrname, default=None):
+ attr = node.attributes.get(attrname)
+ if attr is None:
+ return default
+ value = attr.value
+ if isinstance(value, unicode) and self.encoding is not None:
+ value = value.encode(self.encoding)
+
+ # boolean?
+ lower_value = value.lower()
+ if lower_value in ('false', 'no', '0'):
+ value = False
+ elif lower_value in ('true', 'yes', '1'):
+ value = True
+
+ return value
+
diff --git a/Products/LDAPConnector/profiles/__init__.py b/Products/LDAPConnector/profiles/__init__.py
new file mode 100644
index 0000000..283760b
--- /dev/null
+++ b/Products/LDAPConnector/profiles/__init__.py
@@ -0,0 +1,17 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" GenericSetup import/export profiles for Products.LDAPConnector
+
+$Id$
+"""
+
diff --git a/Products/LDAPConnector/profiles/configure.zcml b/Products/LDAPConnector/profiles/configure.zcml
new file mode 100644
index 0000000..28642a7
--- /dev/null
+++ b/Products/LDAPConnector/profiles/configure.zcml
@@ -0,0 +1,6 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser">
+
+
+</configure>
diff --git a/Products/LDAPConnector/tests/__init__.py b/Products/LDAPConnector/tests/__init__.py
new file mode 100644
index 0000000..fa44f1c
--- /dev/null
+++ b/Products/LDAPConnector/tests/__init__.py
@@ -0,0 +1,16 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" Products.LDAPConnector tests
+
+$Id$
+"""
diff --git a/Products/LDAPConnector/tests/dummy.py b/Products/LDAPConnector/tests/dummy.py
new file mode 100644
index 0000000..8423273
--- /dev/null
+++ b/Products/LDAPConnector/tests/dummy.py
@@ -0,0 +1,40 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" LDAPConnector test dummy implementations
+
+$Id$
+"""
+
+class DummyRequest:
+
+ def __init__(self):
+ self.form = {}
+ self.RESPONSE = DummyResponse()
+
+ def get(self, key, default=None):
+ if key in self.form.keys():
+ return self.form[key]
+
+ return getattr(self, key, default)
+
+ def set(self, key, value):
+ setattr(self, key, value)
+
+class DummyResponse:
+
+ def __init__(self, *args, **kw):
+ self.redirected = ''
+
+ def redirect(self, url):
+ self.redirected = url
+
diff --git a/Products/LDAPConnector/tests/test_LDAPConnector.py b/Products/LDAPConnector/tests/test_LDAPConnector.py
new file mode 100644
index 0000000..919ae82
--- /dev/null
+++ b/Products/LDAPConnector/tests/test_LDAPConnector.py
@@ -0,0 +1,86 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" LDAPConnector class tests
+
+$Id$
+"""
+
+import unittest
+import uuid
+
+from Products.LDAPConnector.tests.dummy import DummyRequest
+
+
+class LDAPConnectorTests(unittest.TestCase):
+
+ def _makeOne(self, *args, **kw):
+ from Products.LDAPConnector.LDAPConnector import LDAPConnector
+ return LDAPConnector(*args, **kw)
+
+ def test_conformance(self):
+ from dataflake.ldapconnection.interfaces import ILDAPConnection
+ from Products.LDAPConnector.LDAPConnector import LDAPConnector
+ from zope.interface.verify import verifyClass
+ verifyClass(ILDAPConnection, LDAPConnector)
+
+ def test_constructor_defaults(self):
+ from dataflake.cache.timeout import LockingTimeoutCache
+ conn = self._makeOne('test_id')
+ self.assertEquals(conn.getId(), 'test_id')
+ self.assertEquals(conn.title, '')
+ self.assertEquals(conn.timeout, 600)
+ self.assertEquals(conn.bind_dn, '')
+ self.assertEquals(conn.bind_pwd, '')
+ self.failIf(conn.read_only)
+ self.failIf(conn.servers)
+ self.failUnless(isinstance(conn.hash, uuid.UUID))
+
+ def test_zope_factory(self):
+ from dataflake.cache.timeout import LockingTimeoutCache
+ from OFS.Folder import Folder
+ from Products.LDAPConnector.LDAPConnector import manage_addLDAPConnector
+
+ container = Folder('container')
+ request = DummyRequest()
+ manage_addLDAPConnector( container
+ , 'test_id'
+ , title='Test Title'
+ , REQUEST=request
+ )
+ conn = container.test_id
+
+ self.failUnless(request.RESPONSE.redirected)
+ self.assertEquals(conn.getId(), 'test_id')
+ self.assertEquals(conn.title, 'Test Title')
+ self.assertEquals(conn.timeout, 600)
+ self.assertEquals(conn.bind_dn, '')
+ self.assertEquals(conn.bind_pwd, '')
+ self.failIf(conn.read_only)
+ self.failIf(conn.servers)
+ self.failUnless(isinstance(conn.hash, uuid.UUID))
+
+ def test_zope_factory_duplicate(self):
+ from OFS.Folder import Folder
+ from Products.LDAPConnector.LDAPConnector import manage_addLDAPConnector
+
+ container = Folder('container')
+ setattr(container, 'test_id', 'FAKE')
+ manage_addLDAPConnector(container, 'test_id', title='Test Title')
+
+ self.assertEquals(container.test_id, 'FAKE')
+
+
+def test_suite():
+ return unittest.TestSuite((
+ unittest.makeSuite(LDAPConnectorTests),
+ ))
diff --git a/Products/LDAPConnector/tests/test_exportimport.py b/Products/LDAPConnector/tests/test_exportimport.py
new file mode 100644
index 0000000..c634f7e
--- /dev/null
+++ b/Products/LDAPConnector/tests/test_exportimport.py
@@ -0,0 +1,331 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" LDAPConnector exporter and importer tests
+
+$Id$
+"""
+
+import unittest
+
+from Products.GenericSetup.tests.common import BaseRegistryTests
+from Products.GenericSetup.tests.common import DummyExportContext
+from Products.GenericSetup.tests.common import DummyImportContext
+from Products.GenericSetup.tests.conformance \
+ import ConformsToIFilesystemExporter
+from Products.GenericSetup.tests.conformance \
+ import ConformsToIFilesystemImporter
+
+
+class _TestBase(BaseRegistryTests):
+
+ def _makeOne(self, context, *args, **kw):
+ return self._getTargetClass()(context, *args, **kw)
+
+ def _getTargetClass(self):
+ from Products.LDAPConnector.exportimport import \
+ LDAPConnectorExportImport
+ return LDAPConnectorExportImport
+
+ def _makePlugin(self, id='testing', *args, **kw):
+ from Products.LDAPConnector.LDAPConnector import LDAPConnector
+ return LDAPConnector(id, *args, **kw)
+
+ def _setupDefaultTraversable(self):
+ from zope.interface import Interface
+ from zope.component import provideAdapter
+ from zope.traversing.interfaces import ITraversable
+ from zope.traversing.adapters import DefaultTraversable
+ provideAdapter(DefaultTraversable, (Interface,), ITraversable)
+
+
+class LDAPConnectorImportTests(_TestBase, ConformsToIFilesystemImporter):
+
+ def test_import_empty(self):
+ plugin = self._makePlugin('empty').__of__(self.root)
+ adapter = self._makeOne(plugin)
+ context = DummyImportContext(plugin)
+ context._files['plugins/empty.xml'] = _EMPTY
+
+ adapter.import_(context, 'plugins', False)
+
+ self.assertEquals(plugin.title, 'Test title')
+ self.assertEquals(plugin.bind_dn, 'cn=Manager,dc=localhost')
+ self.assertEquals(plugin.bind_pwd, 'secret')
+ self.failUnless(plugin.read_only)
+ self.failIf(plugin.servers)
+
+ def test_import_with_servers(self):
+ plugin = self._makePlugin('with_servers').__of__(self.root)
+ adapter = self._makeOne(plugin)
+ context = DummyImportContext(plugin)
+ context._files['plugins/with_servers.xml'] = _WITH_SERVERS
+
+ adapter.import_(context, 'plugins', False)
+
+ self.assertEquals(plugin.title, 'Test title')
+ self.assertEquals(plugin.bind_dn, 'cn=Manager,dc=localhost')
+ self.assertEquals(plugin.bind_pwd, 'secret')
+ self.failUnless(plugin.read_only)
+
+ servers = plugin.servers.values()
+ svr1 = { 'url': 'ldaps://server1.dom.com:636'
+ , 'op_timeout': 5
+ , 'conn_timeout': 10
+ }
+ svr2 = { 'url': 'ldap://server2.dom.com:1389'
+ , 'op_timeout': 2
+ , 'conn_timeout': 1
+ }
+
+ self.assertEquals(len(servers), 2)
+ self.failUnless(svr1 in servers)
+ self.failUnless(svr2 in servers)
+
+ def test_import_deformed(self):
+ plugin = self._makePlugin('deformed').__of__(self.root)
+ adapter = self._makeOne(plugin)
+ context = DummyImportContext(plugin)
+ context._files['plugins/deformed.xml'] = _DEFORMED
+
+ adapter.import_(context, 'plugins', False)
+
+ self.assertEquals(plugin.title, 'Test title')
+ self.assertEquals(plugin.bind_dn, 'cn=Manager,dc=localhost')
+ self.assertEquals(plugin.bind_pwd, 'secret')
+ self.failIf(plugin.read_only)
+
+ svr = { 'url': 'ldap://server1.dom.com:389'
+ , 'op_timeout': -1
+ , 'conn_timeout': 5
+ }
+ self.assertEquals(plugin.servers.values(), [svr])
+
+ def test_import_without_purge_leaves_existing_servers(self):
+ plugin = self._makePlugin('with_servers').__of__(self.root)
+ plugin.addServer( 'server1.dom.com'
+ , '636'
+ , 'ldaps'
+ , conn_timeout=10
+ , op_timeout=5
+ )
+ plugin.addServer( 'server2.dom.com'
+ , '1389'
+ , 'ldap'
+ , conn_timeout=1
+ , op_timeout=2
+ )
+ adapter = self._makeOne(plugin)
+ context = DummyImportContext(plugin, purge=False)
+ context._files['plugins/with_servers.xml'] = _EMPTY
+
+ adapter.import_(context, 'plugins', False)
+
+ self.assertEquals(plugin.title, 'Test title')
+ self.assertEquals(plugin.bind_dn, 'cn=Manager,dc=localhost')
+ self.assertEquals(plugin.bind_pwd, 'secret')
+ self.failUnless(plugin.read_only)
+
+ servers = plugin.servers.values()
+ svr1 = { 'url': 'ldaps://server1.dom.com:636'
+ , 'op_timeout': 5
+ , 'conn_timeout': 10
+ }
+ svr2 = { 'url': 'ldap://server2.dom.com:1389'
+ , 'op_timeout': 2
+ , 'conn_timeout': 1
+ }
+
+ self.assertEquals(len(servers), 2)
+ self.failUnless(svr1 in servers)
+ self.failUnless(svr2 in servers)
+
+ def test_import_with_purge_wipes_existing_servers(self):
+ plugin = self._makePlugin('with_servers').__of__(self.root)
+ plugin.addServer( 'server1.dom.com'
+ , '636'
+ , 'ldaps'
+ , conn_timeout=10
+ , op_timeout=5
+ )
+ plugin.addServer( 'server2.dom.com'
+ , '1389'
+ , 'ldap'
+ , conn_timeout=1
+ , op_timeout=2
+ )
+ adapter = self._makeOne(plugin)
+ context = DummyImportContext(plugin, purge=True)
+ context._files['plugins/with_servers.xml'] = _EMPTY
+
+ adapter.import_(context, 'plugins', False)
+
+ self.assertEquals(plugin.title, 'Test title')
+ self.assertEquals(plugin.bind_dn, 'cn=Manager,dc=localhost')
+ self.assertEquals(plugin.bind_pwd, 'secret')
+ self.failUnless(plugin.read_only)
+ self.failIf(plugin.servers)
+
+
+class LDAPConnectorExportTests(_TestBase, ConformsToIFilesystemExporter):
+
+ def test_listExportableItems(self):
+ plugin = self._makePlugin().__of__(self.root)
+ adapter = self._makeOne(plugin)
+ self.failIf(adapter.listExportableItems())
+
+ plugin.addServer( 'server1.dom.com'
+ , '636'
+ , 'ldaps'
+ , conn_timeout=10
+ , op_timeout=5
+ )
+ self.failIf(adapter.listExportableItems())
+
+ def test_export_default(self):
+ self._setupDefaultTraversable()
+
+ plugin = self._makePlugin('default').__of__(self.root)
+ adapter = self._makeOne(plugin)
+ context = DummyExportContext(plugin)
+
+ adapter.export(context, 'plugins', False)
+ self.assertEquals(len(context._wrote), 1)
+
+ filename, text, content_type = context._wrote[0]
+ self.assertEqual(filename, 'plugins/default.xml')
+ self._compareDOM(text, _DEFAULT)
+ self.assertEqual(content_type, 'text/xml')
+
+ def test_export_empty(self):
+ self._setupDefaultTraversable()
+
+ plugin = self._makePlugin('empty').__of__(self.root)
+ plugin.title = 'Test title'
+ plugin.bind_dn = 'cn=Manager,dc=localhost'
+ plugin.bind_pwd = 'secret'
+ plugin.read_only = True
+ adapter = self._makeOne(plugin)
+ context = DummyExportContext(plugin)
+
+ adapter.export(context, 'plugins', False)
+ self.assertEquals(len(context._wrote), 1)
+
+ filename, text, content_type = context._wrote[0]
+ self.assertEqual(filename, 'plugins/empty.xml')
+ self._compareDOM(text, _EMPTY)
+ self.assertEqual(content_type, 'text/xml')
+
+ def test_export_with_servers(self):
+ self._setupDefaultTraversable()
+
+ plugin = self._makePlugin('with_servers').__of__(self.root)
+ plugin.title = 'Test title'
+ plugin.bind_dn = 'cn=Manager,dc=localhost'
+ plugin.bind_pwd = 'secret'
+ plugin.read_only = True
+ plugin.addServer( 'server1.dom.com'
+ , '636'
+ , 'ldaps'
+ , conn_timeout=10
+ , op_timeout=5
+ )
+ plugin.addServer( 'server2.dom.com'
+ , '1389'
+ , 'ldap'
+ , conn_timeout=1
+ , op_timeout=2
+ )
+ adapter = self._makeOne(plugin)
+ context = DummyExportContext(plugin)
+
+ adapter.export(context, 'plugins', False)
+ self.assertEquals(len(context._wrote), 1)
+
+ filename, text, content_type = context._wrote[0]
+ self.assertEqual(filename, 'plugins/with_servers.xml')
+ self._compareDOM(text, _WITH_SERVERS)
+ self.assertEqual(content_type, 'text/xml')
+
+
+def test_suite():
+ return unittest.TestSuite((
+ unittest.makeSuite(LDAPConnectorImportTests),
+ unittest.makeSuite(LDAPConnectorExportTests),
+ ))
+
+
+_DEFAULT = """\
+<?xml version="1.0" ?>
+<ldapconnector
+ bind_dn=""
+ bind_pwd=""
+ read_only="0"
+ title="">
+</ldapconnector>
+"""
+
+_EMPTY = """\
+<?xml version="1.0" ?>
+<ldapconnector
+ bind_dn="cn=Manager,dc=localhost"
+ bind_pwd="secret"
+ read_only="1"
+ title="Test title">
+</ldapconnector>
+"""
+
+_WITH_SERVERS = """\
+<?xml version="1.0" ?>
+<ldapconnector
+ bind_dn="cn=Manager,dc=localhost"
+ bind_pwd="secret"
+ read_only="1"
+ title="Test title">
+ <server
+ connection_timeout="1"
+ host="server2.dom.com"
+ operations_timeout="2"
+ port="1389"
+ protocol="ldap"
+ />
+ <server
+ connection_timeout="10"
+ host="server1.dom.com"
+ operations_timeout="5"
+ port="636"
+ protocol="ldaps"
+ />
+</ldapconnector>
+"""
+
+_DEFORMED = """\
+<?xml version="1.0" ?>
+<ldapconnector
+ bind_dn="cn=Manager,dc=localhost"
+ bind_pwd="secret"
+ read_only="No"
+ title="Test title">
+ <server
+ connection_timeout="1"
+ host=""
+ operations_timeout="2"
+ port="1389"
+ protocol="ldap"
+ />
+ <server
+ connection_timeout="INVALID"
+ host="server1.dom.com"
+ operations_timeout="INVALID"
+ />
+</ldapconnector>
+"""
diff --git a/Products/LDAPConnector/tests/test_utils.py b/Products/LDAPConnector/tests/test_utils.py
new file mode 100644
index 0000000..c5ea2ed
--- /dev/null
+++ b/Products/LDAPConnector/tests/test_utils.py
@@ -0,0 +1,112 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" utility function tests
+
+$Id$
+"""
+
+import ldap
+import unittest
+
+
+class UtilityTests(unittest.TestCase):
+
+ def test_getCache(self):
+ from Products.LDAPConnector.utils import getCache
+ from dataflake.cache.timeout import LockingTimeoutCache
+
+ cache = getCache('testkey', 600)
+ self.failUnless(isinstance(cache, LockingTimeoutCache))
+ self.assertEquals(cache.getTimeout(), 600)
+
+ def test_cachedSearch_decorator(self):
+ dummy = Dummy('hash0')
+ self.failIf(dummy.getCacheContents())
+
+ dummy.must_cache('cachekey1')
+ self.assertEquals( dummy.getCacheContents()
+ , [{'results': [{'dn': 'DN:cachekey1'}]}]
+ )
+
+ dummy.must_cache('cachekey2')
+ cache_contents = dummy.getCacheContents()
+ self.assertEquals(len(cache_contents), 2)
+ self.failUnless({'results': [{'dn': 'DN:cachekey1'}]} in cache_contents)
+ self.failUnless({'results': [{'dn': 'DN:cachekey2'}]} in cache_contents)
+
+ def test_cachedSearch_decorator_error(self):
+ dummy = Dummy('hash1')
+ self.failIf(dummy.getCacheContents())
+
+ self.assertRaises(ldap.LDAPError, dummy.error_raiser, 'cachekey1')
+ self.assertEquals( dummy.getCacheContents()
+ , [ { 'exception': "LDAPError('Ouch!',)"
+ , 'results': []
+ , 'size': 0
+ }
+ ]
+ )
+
+ def test_invalidateCache_decorator(self):
+ dummy = Dummy('hash2')
+ self.failIf(dummy.getCacheContents())
+
+ dummy.must_cache('cachekey1')
+ dummy.must_cache('cachekey2')
+
+ dummy.must_invalidate('DN:cachekey2')
+ self.assertEquals( dummy.getCacheContents()
+ , [{'results': [{'dn': 'DN:cachekey1'}]}]
+ )
+
+ dummy.must_invalidate('UNKNOWN KEY')
+ self.assertEquals( dummy.getCacheContents()
+ , [{'results': [{'dn': 'DN:cachekey1'}]}]
+ )
+
+ dummy.must_invalidate('DN:cachekey1')
+ self.failIf(dummy.getCacheContents())
+
+
+from Products.LDAPConnector.utils import cachedSearch
+from Products.LDAPConnector.utils import getCache
+from Products.LDAPConnector.utils import invalidateCache
+
+class Dummy:
+
+ def __init__(self, hash):
+ self.hash = hash
+ self.timeout = 30
+ self.bind_dn = 'cn=Manager,dc=localhost'
+
+ def getCacheContents(self):
+ return getCache(self.hash, self.timeout).values()
+
+ @cachedSearch
+ def must_cache(self, key):
+ val = {'results': [{'dn': 'DN:%s' % key}]}
+ return val
+
+ @cachedSearch
+ def error_raiser(self, key):
+ raise ldap.LDAPError('Ouch!')
+
+ @invalidateCache
+ def must_invalidate(self, key):
+ pass
+
+
+def test_suite():
+ return unittest.TestSuite((
+ unittest.makeSuite(UtilityTests),
+ ))
diff --git a/Products/LDAPConnector/upgrade/__init__.py b/Products/LDAPConnector/upgrade/__init__.py
new file mode 100644
index 0000000..b999a18
--- /dev/null
+++ b/Products/LDAPConnector/upgrade/__init__.py
@@ -0,0 +1,16 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" GenericSetup upgrade code for Products.LDAPConnector
+
+$Id$
+"""
diff --git a/Products/LDAPConnector/upgrade/configure.zcml b/Products/LDAPConnector/upgrade/configure.zcml
new file mode 100644
index 0000000..0d6c3d7
--- /dev/null
+++ b/Products/LDAPConnector/upgrade/configure.zcml
@@ -0,0 +1,6 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
+ i18n_domain="ldapconnector">
+
+</configure>
diff --git a/Products/LDAPConnector/utils.py b/Products/LDAPConnector/utils.py
new file mode 100644
index 0000000..6c23b2b
--- /dev/null
+++ b/Products/LDAPConnector/utils.py
@@ -0,0 +1,79 @@
+##############################################################################
+#
+# Copyright (c) 2010 Jens Vagelpohl and Contributors. 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.
+#
+##############################################################################
+""" Products.LDAPConnector utility functions and definitions
+
+$Id$
+"""
+
+import ldap
+
+from dataflake.cache.timeout import LockingTimeoutCache
+
+# This mapping holds the search results cache
+_CACHES = {}
+
+def getCache(key, timeout):
+ """ Get or create a cache object for the given key
+ """
+ if key not in _CACHES:
+ _CACHES[key] = LockingTimeoutCache()
+ _CACHES[key].setTimeout(timeout)
+ return _CACHES[key]
+
+def cachedSearch(func):
+ """ Decorator to get results from the cache or cache new results
+ """
+ def _cached(*args, **kw):
+ cache = getCache(args[0].hash, args[0].timeout)
+ num_args = len(args)
+ base = args[1]
+ scope = num_args > 2 and args[2] or kw.get('scope')
+ fltr = num_args > 3 and args[3] or kw.get('fltr')
+ bound_as = ( num_args > 7 and args[8] or
+ kw.get('bind_dn') or
+ args[0].bind_dn )
+ cache_key = '%s:::%s:::%s:::%s' % (base, scope, fltr, bound_as)
+
+ result = cache.get(cache_key, default=None)
+ if result is None:
+ try:
+ result = func(*args, **kw)
+ except ldap.LDAPError, e:
+ cache.set( cache_key
+ , { 'size': 0, 'results': [], 'exception': repr(e)}
+ )
+ raise
+ else:
+ cache.set(cache_key, result)
+
+ return result
+
+ return _cached
+
+def invalidateCache(func):
+ """ Decorator to invalidate cache entries if they are changed
+ """
+ def _invalidate(*args, **kw):
+ func(*args, **kw)
+
+ cache = getCache(args[0].hash, args[0].timeout)
+ dn = args[1]
+
+ for key, value in cache.items():
+ if dn in [x['dn'] for x in value['results']]:
+ cache.invalidate(key)
+
+ func(*args, **kw)
+
+ return _invalidate
+
diff --git a/Products/LDAPConnector/www/add.pt b/Products/LDAPConnector/www/add.pt
new file mode 100644
index 0000000..805bbea
--- /dev/null
+++ b/Products/LDAPConnector/www/add.pt
@@ -0,0 +1,33 @@
+<h1 tal:replace="structure context/manage_page_header">HEADER</h1>
+
+<h3>Add LDAPConnector</h3>
+
+<p class="form-help">
+ Provide an id and an optional title for the new LDAPConnector instance.
+</p>
+
+<form action="manage_addLDAPConnector" method="post">
+ <table cellspacing="0" cellpadding="2" border="0">
+ <tr>
+ <td align="left" valign="top" class="form-label"> Id </td>
+ <td align="left" valign="top">
+ <input type="text" name="id" size="40" value=""/>
+ </td>
+ </tr>
+ <tr>
+ <td align="left" valign="top" class="form-optional"> Title </td>
+ <td align="left" valign="top">
+ <input type="text" name="title" size="40"/>
+ </td>
+ </tr>
+ <tr>
+ <td align="left" valign="top">
+ </td>
+ <td align="left" valign="top" class="form-element">
+ <input class="form-element" type="submit" name="submit" value=" Add "/>
+ </td>
+ </tr>
+ </table>
+</form>
+
+<h1 tal:replace="structure context/manage_page_footer">FOOTER</h1>
diff --git a/Products/LDAPConnector/www/ldapconnector.gif b/Products/LDAPConnector/www/ldapconnector.gif
new file mode 100644
index 0000000..6a5a0b9
--- /dev/null
+++ b/Products/LDAPConnector/www/ldapconnector.gif
Binary files differ
diff --git a/Products/LDAPConnector/xml/ldapconnector.xml b/Products/LDAPConnector/xml/ldapconnector.xml
new file mode 100644
index 0000000..96680a5
--- /dev/null
+++ b/Products/LDAPConnector/xml/ldapconnector.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" ?>
+<ldapconnector xmlns:tal="http://xml.zope.org/namespaces/tal"
+ title="TITLE" bind_dn="DN" bind_pwd="PWD"
+ tal:define="info options/info"
+ tal:attributes="title info/title;
+ bind_dn info/bind_dn;
+ bind_pwd info/bind_pwd;
+ read_only info/read_only;
+ ">
+ <server host="HOST" port="PORT" protocol="PROTOCOL"
+ connection_timeout="CONN_TIMEOUT" operations_timeout="OP_TIMEOUT"
+ tal:repeat="svr info/servers"
+ tal:attributes="host svr/host;
+ port svr/port;
+ protocol svr/protocol;
+ connection_timeout svr/connection_timeout;
+ operations_timeout svr/operations_timeout;
+ "/>
+</ldapconnector>
diff --git a/Products/__init__.py b/Products/__init__.py
new file mode 100644
index 0000000..f48ad10
--- /dev/null
+++ b/Products/__init__.py
@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+ __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+ from pkgutil import extend_path
+ __path__ = extend_path(__path__, __name__)
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..5e87591
--- /dev/null
+++ b/README.txt
@@ -0,0 +1 @@
+(see Products/LDAPConnector/README.txt)
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..3d87e21
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,55 @@
+import os
+from setuptools import setup
+from setuptools import find_packages
+
+NAME = 'LDAPConnector'
+here = os.path.abspath(os.path.dirname(__file__))
+package = os.path.join(here, 'Products', NAME)
+
+def _read(name):
+ f = open(os.path.join(package, name))
+ return f.read()
+
+_boundary = '\n' + ('-' * 60) + '\n\n'
+
+setup(name='Products.%s' % NAME,
+ version=_read('VERSION.txt').strip(),
+ description='Persistent Zope 2 LDAP connection manager',
+ long_description=( _read('README.txt')
+ + _boundary
+ + _read('CHANGES.txt')
+ + _boundary
+ + "Download\n========"
+ ),
+ classifiers=[
+ "Development Status :: 5 - Production/Stable",
+ "Framework :: Zope2",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Zope Public License",
+ "Programming Language :: Python",
+ "Topic :: Internet :: WWW/HTTP :: Site Management",
+ "Topic :: Software Development",
+ "Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP",
+ ],
+ keywords='web application server zope zope2 ldap',
+ author="Jens Vagelpohl and contributors",
+ author_email="jens@dataflake.org",
+ url="http://pypi.python.org/pypi/Products.%s" % NAME,
+ license="ZPL 2.1 (http://www.zope.org/Resources/License/ZPL-2.1)",
+ packages=find_packages(),
+ include_package_data=True,
+ namespace_packages=['Products'],
+ zip_safe=False,
+ install_requires=[
+ 'setuptools',
+ 'Zope2',
+ 'Products.GenericSetup',
+ 'dataflake.ldapconnection',
+ 'dataflake.cache',
+ ],
+ entry_points="""
+ [zope2.initialize]
+ Products.%s = Products.%s:initialize
+ """ % (NAME, NAME),
+ )
+