--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,19 @@
+
+syntax: regexp
+^develop-eggs$
+syntax: regexp
+^parts$
+syntax: regexp
+^bin$
+syntax: regexp
+^\.installed\.cfg$
+syntax: regexp
+^\.settings$
+syntax: regexp
+^build$
+syntax: regexp
+^dist$
+syntax: regexp
+^\.idea$
+syntax: regexp
+.*\.pyc$
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,42 @@
+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.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/MANIFEST.in Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,5 @@
+include *.txt
+recursive-include docs *
+recursive-include src *
+global-exclude *.pyc
+global-exclude *.*~
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bootstrap.py Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,178 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation 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.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os
+import shutil
+import sys
+import tempfile
+
+from optparse import OptionParser
+
+tmpeggs = tempfile.mkdtemp()
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --find-links to point to local resources, you can keep
+this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", help="use a specific zc.buildout version")
+
+parser.add_option("-t", "--accept-buildout-test-releases",
+ dest='accept_buildout_test_releases',
+ action="store_true", default=False,
+ help=("Normally, if you do not specify a --version, the "
+ "bootstrap script and buildout gets the newest "
+ "*final* versions of zc.buildout and its recipes and "
+ "extensions for you. If you use this flag, "
+ "bootstrap and buildout will get the newest releases "
+ "even if they are alphas or betas."))
+parser.add_option("-c", "--config-file",
+ help=("Specify the path to the buildout configuration "
+ "file to be used."))
+parser.add_option("-f", "--find-links",
+ help=("Specify a URL to search for buildout releases"))
+parser.add_option("--allow-site-packages",
+ action="store_true", default=False,
+ help=("Let bootstrap.py use existing site packages"))
+
+
+options, args = parser.parse_args()
+
+######################################################################
+# load/install setuptools
+
+try:
+ if options.allow_site_packages:
+ import setuptools
+ import pkg_resources
+ from urllib.request import urlopen
+except ImportError:
+ from urllib2 import urlopen
+
+ez = {}
+exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
+
+if not options.allow_site_packages:
+ # ez_setup imports site, which adds site packages
+ # this will remove them from the path to ensure that incompatible versions
+ # of setuptools are not in the path
+ import site
+ # inside a virtualenv, there is no 'getsitepackages'.
+ # We can't remove these reliably
+ if hasattr(site, 'getsitepackages'):
+ for sitepackage_path in site.getsitepackages():
+ sys.path[:] = [x for x in sys.path if sitepackage_path not in x]
+
+setup_args = dict(to_dir=tmpeggs, download_delay=0)
+ez['use_setuptools'](**setup_args)
+import setuptools
+import pkg_resources
+
+# This does not (always?) update the default working set. We will
+# do it.
+for path in sys.path:
+ if path not in pkg_resources.working_set.entries:
+ pkg_resources.working_set.add_entry(path)
+
+######################################################################
+# Install buildout
+
+ws = pkg_resources.working_set
+
+cmd = [sys.executable, '-c',
+ 'from setuptools.command.easy_install import main; main()',
+ '-mZqNxd', tmpeggs]
+
+find_links = os.environ.get(
+ 'bootstrap-testing-find-links',
+ options.find_links or
+ ('http://downloads.buildout.org/'
+ if options.accept_buildout_test_releases else None)
+ )
+if find_links:
+ cmd.extend(['-f', find_links])
+
+setuptools_path = ws.find(
+ pkg_resources.Requirement.parse('setuptools')).location
+
+requirement = 'zc.buildout'
+version = options.version
+if version is None and not options.accept_buildout_test_releases:
+ # Figure out the most recent final version of zc.buildout.
+ import setuptools.package_index
+ _final_parts = '*final-', '*final'
+
+ def _final_version(parsed_version):
+ for part in parsed_version:
+ if (part[:1] == '*') and (part not in _final_parts):
+ return False
+ return True
+ index = setuptools.package_index.PackageIndex(
+ search_path=[setuptools_path])
+ if find_links:
+ index.add_find_links((find_links,))
+ req = pkg_resources.Requirement.parse(requirement)
+ if index.obtain(req) is not None:
+ best = []
+ bestv = None
+ for dist in index[req.project_name]:
+ distv = dist.parsed_version
+ if _final_version(distv):
+ if bestv is None or distv > bestv:
+ best = [dist]
+ bestv = distv
+ elif distv == bestv:
+ best.append(dist)
+ if best:
+ best.sort()
+ version = best[-1].version
+if version:
+ requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+import subprocess
+if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0:
+ raise Exception(
+ "Failed to execute command:\n%s" % repr(cmd)[1:-1])
+
+######################################################################
+# Import and run buildout
+
+ws.add_entry(tmpeggs)
+ws.require(requirement)
+import zc.buildout.buildout
+
+if not [a for a in args if '=' not in a]:
+ args.append('bootstrap')
+
+# if -c was provided, we push it back into args for buildout' main function
+if options.config_file is not None:
+ args[0:0] = ['-c', options.config_file]
+
+zc.buildout.buildout.main(args)
+shutil.rmtree(tmpeggs)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/buildout.cfg Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,67 @@
+[buildout]
+eggs-directory = /var/local/env/pyams/eggs
+
+socket-timeout = 3
+show-picked-versions = true
+newest = false
+
+allow-hosts =
+ bitbucket.org
+ *.python.org
+ *.sourceforge.net
+ github.com
+
+#extends = http://download.ztfy.org/webapp/ztfy.webapp.dev.cfg
+versions = versions
+newest = false
+#allow-picked-versions = false
+
+src = src
+develop = .
+ ../pyams_form
+ ../pyams_pagelet
+ ../pyams_security
+ ../pyams_skin
+ ../pyams_template
+ ../pyams_utils
+ ../pyams_viewlet
+ ../ext/lingua
+
+parts =
+ package
+ i18n
+ pyflakes
+ test
+
+[package]
+recipe = zc.recipe.egg
+eggs =
+ pyams_ldap
+ pyramid
+ zope.component
+ zope.interface
+
+[i18n]
+recipe = zc.recipe.egg
+eggs =
+ babel
+ lingua
+
+[pyflakes]
+recipe = zc.recipe.egg
+eggs = pyflakes
+scripts = pyflakes
+entry-points = pyflakes=pyflakes.scripts.pyflakes:main
+initialization = if not sys.argv[1:]: sys.argv[1:] = ["${buildout:src}"]
+
+[pyflakesrun]
+recipe = collective.recipe.cmd
+on_install = true
+cmds = ${buildout:develop}/bin/${pyflakes:scripts}
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = pyams_ldap [test]
+
+[versions]
+pyams_base = 0.1.0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,71 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# 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.
+#
+
+"""
+This module contains pyams_ package
+"""
+import os
+from setuptools import setup, find_packages
+
+DOCS = os.path.join(os.path.dirname(__file__),
+ 'docs')
+
+README = os.path.join(DOCS, 'README.txt')
+HISTORY = os.path.join(DOCS, 'HISTORY.txt')
+
+version = '0.1.0'
+long_description = open(README).read() + '\n\n' + open(HISTORY).read()
+
+tests_require = []
+
+setup(name='pyams_ldap',
+ version=version,
+ description="PyAMS interfaces and classes for LDAP authentication",
+ long_description=long_description,
+ classifiers=[
+ "License :: OSI Approved :: Zope Public License",
+ "Development Status :: 4 - Beta",
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ],
+ keywords='Pyramid PyAMS LDAP',
+ author='Thierry Florac',
+ author_email='tflorac@ulthar.net',
+ url='http://hg.ztfy.org/pyams/pyams_ldap',
+ license='ZPL',
+ packages=find_packages('src'),
+ package_dir={'': 'src'},
+ namespace_packages=[],
+ include_package_data=True,
+ package_data={'': ['*.zcml', '*.txt', '*.pt', '*.pot', '*.po', '*.mo', '*.png', '*.gif', '*.jpeg', '*.jpg', '*.css', '*.js']},
+ zip_safe=False,
+ # uncomment this to be able to run tests with setup.py
+ test_suite="pyams_.tests.test_utilsdocs.test_suite",
+ tests_require=tests_require,
+ extras_require=dict(test=tests_require),
+ install_requires=[
+ 'setuptools',
+ # -*- Extra requirements: -*-
+ 'pyams_pagelet',
+ 'pyams_security',
+ 'pyams_skin',
+ 'pyams_utils',
+ 'pyams_viewlet',
+ 'pyramid',
+ 'zope.component',
+ 'zope.interface',
+ ],
+ entry_points="""
+ # -*- Entry points: -*-
+ """,
+ )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap.egg-info/PKG-INFO Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,18 @@
+Metadata-Version: 1.1
+Name: pyams-ldap
+Version: 0.1.0
+Summary: PyAMS interfaces and classes for LDAP authentication
+Home-page: http://hg.ztfy.org/pyams/pyams_ldap
+Author: Thierry Florac
+Author-email: tflorac@ulthar.net
+License: ZPL
+Description:
+
+
+Keywords: Pyramid PyAMS LDAP
+Platform: UNKNOWN
+Classifier: License :: OSI Approved :: Zope Public License
+Classifier: Development Status :: 4 - Beta
+Classifier: Programming Language :: Python
+Classifier: Framework :: Pyramid
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap.egg-info/SOURCES.txt Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,24 @@
+MANIFEST.in
+setup.py
+docs/HISTORY.txt
+docs/README.txt
+src/pyams_ldap/__init__.py
+src/pyams_ldap/configure.zcml
+src/pyams_ldap/plugin.py
+src/pyams_ldap/query.py
+src/pyams_ldap.egg-info/PKG-INFO
+src/pyams_ldap.egg-info/SOURCES.txt
+src/pyams_ldap.egg-info/dependency_links.txt
+src/pyams_ldap.egg-info/entry_points.txt
+src/pyams_ldap.egg-info/namespace_packages.txt
+src/pyams_ldap.egg-info/not-zip-safe
+src/pyams_ldap.egg-info/requires.txt
+src/pyams_ldap.egg-info/top_level.txt
+src/pyams_ldap/doctests/README.txt
+src/pyams_ldap/interfaces/__init__.py
+src/pyams_ldap/tests/__init__.py
+src/pyams_ldap/tests/test_utilsdocs.py
+src/pyams_ldap/tests/test_utilsdocstrings.py
+src/pyams_ldap/zmi/__init__.py
+src/pyams_ldap/zmi/plugin.py
+src/pyams_ldap/zmi/templates/ldap-attributes.pt
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap.egg-info/dependency_links.txt Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap.egg-info/entry_points.txt Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,3 @@
+
+ # -*- Entry points: -*-
+
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap.egg-info/namespace_packages.txt Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap.egg-info/not-zip-safe Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap.egg-info/requires.txt Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,11 @@
+setuptools
+pyams_pagelet
+pyams_security
+pyams_skin
+pyams_utils
+pyams_viewlet
+pyramid
+zope.component
+zope.interface
+
+[test]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap.egg-info/top_level.txt Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,1 @@
+pyams_ldap
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/__init__.py Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,35 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+from pyramid.i18n import TranslationStringFactory
+_ = TranslationStringFactory('pyams_ldap')
+
+
+def includeme(config):
+ """Pyramid include"""
+
+ # add translations
+ config.add_translation_dirs('pyams_ldap:locales')
+
+ # load registry components
+ try:
+ import pyams_zmi
+ except ImportError:
+ config.scan(ignore='pyams_ldap.zmi')
+ else:
+ config.scan()
+
+ if hasattr(config, 'load_zcml'):
+ config.load_zcml('configure.zcml')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/configure.zcml Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,7 @@
+<configure
+ xmlns="http://pylonshq.com/pyramid">
+
+ <include package="pyramid_zcml" />
+
+
+</configure>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/doctests/README.txt Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,3 @@
+==================
+pyams_ldap package
+==================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/interfaces/__init__.py Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,157 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+from ldap3 import SEARCH_SCOPE_BASE_OBJECT, SEARCH_SCOPE_SINGLE_LEVEL, SEARCH_SCOPE_WHOLE_SUBTREE
+
+# import interfaces
+from pyams_security.interfaces import IAuthenticationPlugin, IDirectorySearchPlugin
+
+# import packages
+from zope.schema import TextLine, Bool, Int, Choice
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+from pyams_ldap import _
+
+
+SEARCH_SCOPES = {SEARCH_SCOPE_BASE_OBJECT: _("Base object"),
+ SEARCH_SCOPE_SINGLE_LEVEL: _("Single level"),
+ SEARCH_SCOPE_WHOLE_SUBTREE: _("Whole subtree")}
+
+SEARCH_SCOPES_VOCABULARY = SimpleVocabulary([SimpleTerm(v, title=t) for v, t in SEARCH_SCOPES.items()])
+
+
+class ILDAPPlugin(IAuthenticationPlugin, IDirectorySearchPlugin):
+ """LDAP authentication plug-in interface"""
+
+ server_uri = TextLine(title=_("LDAP server URI"),
+ description=_("Full URI (including protocol) of LDAP server"),
+ default="ldap://localhost:389",
+ required=True)
+
+ bind_dn = TextLine(title=_("Bind DN"),
+ description=_("DN used for LDAP bind; keep empty for anonymous"),
+ required=False)
+
+ bind_password = TextLine(title=_("Bind password"),
+ description=_("Password used for LDAP bind"),
+ required=False)
+
+ use_tls = Bool(title=_("Use TLS?"),
+ required=True,
+ default=False)
+
+ use_pool = Bool(title=_("Use connections pool?"),
+ required=True,
+ default=True)
+
+ pool_size = Int(title=_("Pool size"),
+ required=False,
+ default=10)
+
+ pool_lifetime = Int(title=_("Pool lifetime"),
+ description=_("Duration, in seconds, of pool lifetime"),
+ required=False)
+
+ base_dn = TextLine(title=_("Base DN"),
+ description=_("LDAP base DN"),
+ required=True)
+
+ search_scope = Choice(title=_("Search scope"),
+ vocabulary=SEARCH_SCOPES_VOCABULARY,
+ default=SEARCH_SCOPE_WHOLE_SUBTREE,
+ required=True)
+
+ login_attribute = TextLine(title=_("Login attribute"),
+ description=_("LDAP attribute used as user login"),
+ required=True,
+ default='uid')
+
+ login_query = TextLine(title=_("Login query"),
+ description=_("Query template used to authenticate user "
+ "(based on login attribute called 'login')"),
+ required=True,
+ default='(uid={login})')
+
+ uid_attribute = TextLine(title=_("UID attribute"),
+ description=_("LDAP attribute used as principal identifier"),
+ required=True,
+ default='dn')
+
+ uid_query = TextLine(title=_("UID query"),
+ description=_("Query template used to get principal information "
+ "(based on UID attribute called 'login')"),
+ required=True,
+ default="(objectClass=*)")
+
+ title_format = TextLine(title=_("Title format"),
+ description=_("Principal's title format string"),
+ required=True,
+ default='{givenName[0]} {sn[0]}')
+
+ groups_base_dn = TextLine(title=_("Groups base DN"),
+ description=_("Base DN used to search LDAP groups; keep empty to "
+ "disable groups usage"),
+ required=False)
+
+ groups_search_scope = Choice(title=_("Groups search scope"),
+ vocabulary=SEARCH_SCOPES_VOCABULARY,
+ default=SEARCH_SCOPE_WHOLE_SUBTREE,
+ required=False)
+
+ groups_query = TextLine(title=_("Groups query"),
+ description=_("Query template used to get principal groups "
+ "(based on DN and UID attributes called 'dn' "
+ "and 'login')"),
+ required=False,
+ default="(&(objectClass=groupOfUniqueNames)(uniqueMember={dn}))")
+
+ group_prefix = TextLine(title=_("Group prefix"),
+ description=_("Prefix used to identify groups"),
+ required=False,
+ default='group')
+
+ group_uid_attribute = TextLine(title=_("Group UID attribute"),
+ description=_("LDAP attribute used as group identifier"),
+ required=False,
+ default='dn')
+
+ group_title_format = TextLine(title=_("Group title format"),
+ description=_("Principal's title format string"),
+ required=True,
+ default='{cn[0]}')
+
+ users_select_query = TextLine(title=_("Users select query"),
+ description=_("Query template used to select users"),
+ required=True,
+ default='(|(givenName={query}*)(sn={query}*))')
+
+ users_search_query = TextLine(title=_("Users search query"),
+ description=_("Query template used to search users"),
+ required=True,
+ default='(|(givenName={query}*)(sn={query}*))')
+
+ groups_select_query = TextLine(title=_("Groups select query"),
+ description=_("Query template used to select groups"),
+ required=True,
+ default='(cn=*{query}*)')
+
+ groups_search_query = TextLine(title=_("Groups search query"),
+ description=_("Query template used to search groups"),
+ required=True,
+ default='(cn=*{query}*)')
+
+ def get_connection(self):
+ """Get LDAP connection"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/locales/fr/LC_MESSAGES/pyams_ldap.po Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,264 @@
+#
+# French translations for PACKAGE package
+# This file is distributed under the same license as the PACKAGE package.
+# Thierry Florac <tflorac@ulthar.net>, 2015.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2015-02-28 09:16+0100\n"
+"PO-Revision-Date: 2015-02-28 09:17+0100\n"
+"Last-Translator: Thierry Florac <tflorac@ulthar.net>\n"
+"Language-Team: French\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Lingua 3.10.dev0\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: src/pyams_ldap/zmi/plugin.py:68
+msgid "Add LDAP users folder..."
+msgstr "Ajouter un dossier LDAP..."
+
+#: src/pyams_ldap/zmi/plugin.py:84
+msgid "System security manager"
+msgstr "Gestionnaire de sécurité"
+
+#: src/pyams_ldap/zmi/plugin.py:85
+msgid "Add LDAP users folder plug-in"
+msgstr "Ajout d'un dossier d'accès LDAP"
+
+#: src/pyams_ldap/zmi/plugin.py:118 src/pyams_ldap/zmi/plugin.py:206
+msgid "Connection"
+msgstr "Connexion"
+
+#: src/pyams_ldap/zmi/plugin.py:130 src/pyams_ldap/zmi/plugin.py:218
+msgid "Users schema"
+msgstr "Schéma des utilisateurs"
+
+#: src/pyams_ldap/zmi/plugin.py:142 src/pyams_ldap/zmi/plugin.py:230
+msgid "Groups schema"
+msgstr "Schéma des groupes"
+
+#: src/pyams_ldap/zmi/plugin.py:154 src/pyams_ldap/zmi/plugin.py:242
+msgid "Search settings"
+msgstr "Recherches"
+
+#: src/pyams_ldap/zmi/plugin.py:171
+msgid "Edit LDAP users folder plug-in properties"
+msgstr "Modification des propriétés d'un dossier d'accès LDAP"
+
+#: src/pyams_ldap/zmi/plugin.py:272
+msgid "Search users and groups"
+msgstr "Utilisateurs et groupes"
+
+#: src/pyams_ldap/zmi/plugin.py:281
+msgid "Search results"
+msgstr "Résultats de la recherche"
+
+#: src/pyams_ldap/zmi/plugin.py:309
+msgid "Common name"
+msgstr "Nom courant"
+
+#: src/pyams_ldap/zmi/plugin.py:319
+msgid "E-mail"
+msgstr "Adresse email"
+
+#: src/pyams_ldap/zmi/plugin.py:338
+#, python-format
+msgid "Display LDAP entry: {dn}"
+msgstr "Entrée LDAP : {dn}"
+
+#: src/pyams_ldap/interfaces/__init__.py:29
+msgid "Base object"
+msgstr "Objet de base"
+
+#: src/pyams_ldap/interfaces/__init__.py:30
+msgid "Single level"
+msgstr "Un niveau"
+
+#: src/pyams_ldap/interfaces/__init__.py:31
+msgid "Whole subtree"
+msgstr "Sous-arbre complet"
+
+#: src/pyams_ldap/interfaces/__init__.py:39
+msgid "LDAP server URI"
+msgstr "URI du serveur"
+
+#: src/pyams_ldap/interfaces/__init__.py:40
+msgid "Full URI (including protocol) of LDAP server"
+msgstr "URI complète (y compris le protocole) d'accès au serveur LDAP"
+
+#: src/pyams_ldap/interfaces/__init__.py:44
+msgid "Bind DN"
+msgstr "DN de connexion"
+
+#: src/pyams_ldap/interfaces/__init__.py:45
+msgid "DN used for LDAP bind; keep empty for anonymous"
+msgstr "DN utilisé pour la connexion LDAP ; laissez vide pour une connexion anonyme"
+
+#: src/pyams_ldap/interfaces/__init__.py:48
+msgid "Bind password"
+msgstr "Mot de passe"
+
+#: src/pyams_ldap/interfaces/__init__.py:49
+msgid "Password used for LDAP bind"
+msgstr "Mot de passe utilisé pour la connexion LDAP"
+
+#: src/pyams_ldap/interfaces/__init__.py:52
+msgid "Use TLS?"
+msgstr "Utiliser TLS ?"
+
+#: src/pyams_ldap/interfaces/__init__.py:56
+msgid "Use connections pool?"
+msgstr "Pool de connexions ?"
+
+#: src/pyams_ldap/interfaces/__init__.py:60
+msgid "Pool size"
+msgstr "Taille du pool"
+
+#: src/pyams_ldap/interfaces/__init__.py:64
+msgid "Pool lifetime"
+msgstr "Durée de vie du pool"
+
+#: src/pyams_ldap/interfaces/__init__.py:65
+msgid "Duration, in seconds, of pool lifetime"
+msgstr "En secondes"
+
+#: src/pyams_ldap/interfaces/__init__.py:68
+msgid "Base DN"
+msgstr "DN de base"
+
+#: src/pyams_ldap/interfaces/__init__.py:69
+msgid "LDAP base DN"
+msgstr "DN de base pour la recherche des utilisateurs"
+
+#: src/pyams_ldap/interfaces/__init__.py:72
+msgid "Search scope"
+msgstr "Portée de la recherche"
+
+#: src/pyams_ldap/interfaces/__init__.py:77
+msgid "Login attribute"
+msgstr "Attribut de connexion"
+
+#: src/pyams_ldap/interfaces/__init__.py:78
+msgid "LDAP attribute used as user login"
+msgstr "Nom de l'attribut LDAP utilisé lors de la connexion"
+
+#: src/pyams_ldap/interfaces/__init__.py:82
+msgid "Login query"
+msgstr "Requête de connexion"
+
+#: src/pyams_ldap/interfaces/__init__.py:83
+msgid ""
+"Query template used to authenticate user (based on login attribute called "
+"'login')"
+msgstr ""
+"Modèle de la requête utilisée lors de la connexion d'un utilisateur ; "
+"la variable 'login' correspond à la saisie de l'utilisateur"
+
+#: src/pyams_ldap/interfaces/__init__.py:88
+msgid "UID attribute"
+msgstr "Attribut UID"
+
+#: src/pyams_ldap/interfaces/__init__.py:89
+msgid "LDAP attribute used as principal identifier"
+msgstr "Attribut LDAP unique utilisé pour l'identification d'un utilisateur"
+
+#: src/pyams_ldap/interfaces/__init__.py:93
+msgid "UID query"
+msgstr "Requête d'UID"
+
+#: src/pyams_ldap/interfaces/__init__.py:94
+msgid ""
+"Query template used to get principal information (based on UID attribute "
+"called 'login')"
+msgstr ""
+"Modèle de la requête utilisée pour rechercher les informations relatives à un "
+"utilisateur à partir de son UID (variable 'login')"
+
+#: src/pyams_ldap/interfaces/__init__.py:99
+msgid "Title format"
+msgstr "Format du nom"
+
+#: src/pyams_ldap/interfaces/__init__.py:100
+#: src/pyams_ldap/interfaces/__init__.py:132
+msgid "Principal's title format string"
+msgstr "Chaîne de formatage du nom"
+
+#: src/pyams_ldap/interfaces/__init__.py:104
+msgid "Groups base DN"
+msgstr "DN de base"
+
+#: src/pyams_ldap/interfaces/__init__.py:105
+msgid "Base DN used to search LDAP groups; keep empty to disable groups usage"
+msgstr "DN de base pour la recherche des groupes ; laissez le vide pour ne "
+"pas activer la gestion des groupes"
+
+#: src/pyams_ldap/interfaces/__init__.py:109
+msgid "Groups search scope"
+msgstr "Portée de la recherche"
+
+#: src/pyams_ldap/interfaces/__init__.py:114
+msgid "Groups query"
+msgstr "Recherche des groupes"
+
+#: src/pyams_ldap/interfaces/__init__.py:115
+msgid ""
+"Query template used to get principal groups (based on DN and UID attributes "
+"called 'dn' and 'login')"
+msgstr ""
+"Modèle de requête utilisée pour extraire la liste des groupes d'un utilisateur "
+"(à partir de ses attributes appelés 'dn' et 'uid')"
+
+#: src/pyams_ldap/interfaces/__init__.py:121
+msgid "Group prefix"
+msgstr "Préfixe des groupes"
+
+#: src/pyams_ldap/interfaces/__init__.py:122
+msgid "Prefix used to identify groups"
+msgstr "Préfixe utilisé pour identifier les groupes"
+
+#: src/pyams_ldap/interfaces/__init__.py:126
+msgid "Group UID attribute"
+msgstr "Attribut UID"
+
+#: src/pyams_ldap/interfaces/__init__.py:127
+msgid "LDAP attribute used as group identifier"
+msgstr "Attribut LDAP utilisé pour identifier les groupes"
+
+#: src/pyams_ldap/interfaces/__init__.py:131
+msgid "Group title format"
+msgstr "Format du nom"
+
+#: src/pyams_ldap/interfaces/__init__.py:136
+msgid "Users select query"
+msgstr "Sélection des utilisateurs"
+
+#: src/pyams_ldap/interfaces/__init__.py:137
+msgid "Query template used to select users"
+msgstr "Modèle de la requête utilisée pour la sélection des utilisateurs"
+
+#: src/pyams_ldap/interfaces/__init__.py:141
+msgid "Users search query"
+msgstr "Recherche des utilisateurs"
+
+#: src/pyams_ldap/interfaces/__init__.py:142
+msgid "Query template used to search users"
+msgstr "Modèle de la requête utilisée pour la recherche des utilisateurs"
+
+#: src/pyams_ldap/interfaces/__init__.py:146
+msgid "Groups select query"
+msgstr "Sélection des groupes"
+
+#: src/pyams_ldap/interfaces/__init__.py:147
+msgid "Query template used to select groups"
+msgstr "Modèle de la requête utilisée pour la sélection des groupes"
+
+#: src/pyams_ldap/interfaces/__init__.py:151
+msgid "Groups search query"
+msgstr "Recherche des groupes"
+
+#: src/pyams_ldap/interfaces/__init__.py:152
+msgid "Query template used to search groups"
+msgstr "Modèle de la requête utilisée pour la recherche des groupes"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/locales/pyams_ldap.pot Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,257 @@
+#
+# SOME DESCRIPTIVE TITLE
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2015-02-28 09:16+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Lingua 3.10.dev0\n"
+
+#: ./src/pyams_ldap/zmi/plugin.py:68
+msgid "Add LDAP users folder..."
+msgstr ""
+
+#: ./src/pyams_ldap/zmi/plugin.py:84
+msgid "System security manager"
+msgstr ""
+
+#: ./src/pyams_ldap/zmi/plugin.py:85
+msgid "Add LDAP users folder plug-in"
+msgstr ""
+
+#: ./src/pyams_ldap/zmi/plugin.py:118 ./src/pyams_ldap/zmi/plugin.py:206
+msgid "Connection"
+msgstr ""
+
+#: ./src/pyams_ldap/zmi/plugin.py:130 ./src/pyams_ldap/zmi/plugin.py:218
+msgid "Users schema"
+msgstr ""
+
+#: ./src/pyams_ldap/zmi/plugin.py:142 ./src/pyams_ldap/zmi/plugin.py:230
+msgid "Groups schema"
+msgstr ""
+
+#: ./src/pyams_ldap/zmi/plugin.py:154 ./src/pyams_ldap/zmi/plugin.py:242
+msgid "Search settings"
+msgstr ""
+
+#: ./src/pyams_ldap/zmi/plugin.py:171
+msgid "Edit LDAP users folder plug-in properties"
+msgstr ""
+
+#: ./src/pyams_ldap/zmi/plugin.py:272
+msgid "Search users and groups"
+msgstr ""
+
+#: ./src/pyams_ldap/zmi/plugin.py:281
+msgid "Search results"
+msgstr ""
+
+#: ./src/pyams_ldap/zmi/plugin.py:309
+msgid "Common name"
+msgstr ""
+
+#: ./src/pyams_ldap/zmi/plugin.py:319
+msgid "E-mail"
+msgstr ""
+
+#: ./src/pyams_ldap/zmi/plugin.py:338
+#, python-format
+msgid "Display LDAP entry: {dn}"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:29
+msgid "Base object"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:30
+msgid "Single level"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:31
+msgid "Whole subtree"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:39
+msgid "LDAP server URI"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:40
+msgid "Full URI (including protocol) of LDAP server"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:44
+msgid "Bind DN"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:45
+msgid "DN used for LDAP bind; keep empty for anonymous"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:48
+msgid "Bind password"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:49
+msgid "Password used for LDAP bind"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:52
+msgid "Use TLS?"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:56
+msgid "Use connections pool?"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:60
+msgid "Pool size"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:64
+msgid "Pool lifetime"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:65
+msgid "Duration, in seconds, of pool lifetime"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:68
+msgid "Base DN"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:69
+msgid "LDAP base DN"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:72
+msgid "Search scope"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:77
+msgid "Login attribute"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:78
+msgid "LDAP attribute used as user login"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:82
+msgid "Login query"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:83
+msgid ""
+"Query template used to authenticate user (based on login attribute called "
+"'login')"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:88
+msgid "UID attribute"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:89
+msgid "LDAP attribute used as principal identifier"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:93
+msgid "UID query"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:94
+msgid ""
+"Query template used to get principal information (based on UID attribute "
+"called 'login')"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:99
+msgid "Title format"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:100
+#: ./src/pyams_ldap/interfaces/__init__.py:132
+msgid "Principal's title format string"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:104
+msgid "Groups base DN"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:105
+msgid "Base DN used to search LDAP groups; keep empty to disable groups usage"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:109
+msgid "Groups search scope"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:114
+msgid "Groups query"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:115
+msgid ""
+"Query template used to get principal groups (based on DN and UID attributes "
+"called 'dn' and 'login')"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:121
+msgid "Group prefix"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:122
+msgid "Prefix used to identify groups"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:126
+msgid "Group UID attribute"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:127
+msgid "LDAP attribute used as group identifier"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:131
+msgid "Group title format"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:136
+msgid "Users select query"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:137
+msgid "Query template used to select users"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:141
+msgid "Users search query"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:142
+msgid "Query template used to search users"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:146
+msgid "Groups select query"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:147
+msgid "Query template used to select groups"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:151
+msgid "Groups search query"
+msgstr ""
+
+#: ./src/pyams_ldap/interfaces/__init__.py:152
+msgid "Query template used to search groups"
+msgstr ""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/plugin.py Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,330 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import ldap3
+import logging
+logger = logging.getLogger('pyams_ldap')
+
+import re
+
+# import interfaces
+from pyams_ldap.interfaces import ILDAPPlugin
+from zope.intid.interfaces import IIntIds
+
+# import packages
+from beaker.cache import cache_region
+from persistent import Persistent
+from pyams_ldap.query import LDAPQuery
+from pyams_security.principal import PrincipalInfo
+from pyams_utils.registry import query_utility
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+managers = {}
+
+
+FORMAT_ATTRIBUTES = re.compile("\{(\w+)\[?\d*\]?\}")
+
+
+class ConnectionManager(object):
+ """LDAP connections manager"""
+
+ def __init__(self, plugin):
+ self.server = ldap3.Server(plugin.host,
+ port=plugin.port,
+ use_ssl=plugin.use_ssl,
+ tls=plugin.use_tls)
+ self.bind_dn = plugin.bind_dn
+ self.password = plugin.bind_password
+ if plugin.use_pool:
+ self.strategy = ldap3.STRATEGY_REUSABLE_THREADED
+ self.pool_name = 'pyams_ldap:{prefix}'.format(prefix=plugin.prefix)
+ self.pool_size = plugin.pool_size
+ self.pool_lifetime = plugin.pool_lifetime
+ else:
+ self.strategy = ldap3.STRATEGY_ASYNC_THREADED
+ self.pool_name = None
+ self.pool_size = None
+ self.pool_lifetime = None
+
+ def get_connection(self, user=None, password=None):
+ if user:
+ conn = ldap3.Connection(self.server,
+ user=user, password=password,
+ client_strategy=ldap3.STRATEGY_SYNC,
+ auto_bind=True, lazy=False, read_only=True)
+ else:
+ conn = ldap3.Connection(self.server,
+ user=self.bind_dn, password=self.password,
+ client_strategy=self.strategy,
+ pool_name=self.pool_name,
+ pool_size=self.pool_size,
+ pool_lifetime=self.pool_lifetime,
+ auto_bind=True, lazy=False, read_only=True)
+ return conn
+
+@implementer(ILDAPPlugin)
+class LDAPPlugin(Persistent, Contained):
+ """LDAP authentication plug-in"""
+
+ prefix = FieldProperty(ILDAPPlugin['prefix'])
+ title = FieldProperty(ILDAPPlugin['title'])
+ enabled = FieldProperty(ILDAPPlugin['enabled'])
+
+ _scheme = None
+ _host = None
+ _port = None
+ _use_ssl = False
+
+ _server_uri = FieldProperty(ILDAPPlugin['server_uri'])
+ bind_dn = FieldProperty(ILDAPPlugin['bind_dn'])
+ bind_password = FieldProperty(ILDAPPlugin['bind_password'])
+ use_tls = FieldProperty(ILDAPPlugin['use_tls'])
+ use_pool = FieldProperty(ILDAPPlugin['use_pool'])
+ pool_size = FieldProperty(ILDAPPlugin['pool_size'])
+ pool_lifetime = FieldProperty(ILDAPPlugin['pool_lifetime'])
+ base_dn = FieldProperty(ILDAPPlugin['base_dn'])
+ search_scope = FieldProperty(ILDAPPlugin['search_scope'])
+ login_attribute = FieldProperty(ILDAPPlugin['login_attribute'])
+ login_query = FieldProperty(ILDAPPlugin['login_query'])
+ uid_attribute = FieldProperty(ILDAPPlugin['uid_attribute'])
+ uid_query = FieldProperty(ILDAPPlugin['uid_query'])
+ title_format = FieldProperty(ILDAPPlugin['title_format'])
+ groups_base_dn = FieldProperty(ILDAPPlugin['groups_base_dn'])
+ groups_search_scope = FieldProperty(ILDAPPlugin['groups_search_scope'])
+ groups_query = FieldProperty(ILDAPPlugin['groups_query'])
+ group_prefix = FieldProperty(ILDAPPlugin['group_prefix'])
+ group_uid_attribute = FieldProperty(ILDAPPlugin['group_uid_attribute'])
+ group_title_format = FieldProperty(ILDAPPlugin['group_title_format'])
+
+ users_select_query = FieldProperty(ILDAPPlugin['users_select_query'])
+ users_search_query = FieldProperty(ILDAPPlugin['users_search_query'])
+ groups_select_query = FieldProperty(ILDAPPlugin['groups_select_query'])
+ groups_search_query = FieldProperty(ILDAPPlugin['groups_search_query'])
+
+ @property
+ def server_uri(self):
+ return self._server_uri
+
+ @server_uri.setter
+ def server_uri(self, value):
+ self._server_uri = value
+ try:
+ scheme, host = value.split('://', 1)
+ except ValueError:
+ scheme = 'ldap'
+ host = value
+ self._use_ssl = scheme == 'ldaps'
+ self._scheme = scheme
+ try:
+ host, port = host.split(':', 1)
+ port = int(port)
+ except ValueError:
+ port = 636 if self._use_ssl else 389
+ self._host = host
+ self._port = port
+
+ @property
+ def scheme(self):
+ return self._scheme
+
+ @property
+ def host(self):
+ return self._host
+
+ @property
+ def port(self):
+ return self._port
+
+ @property
+ def use_ssl(self):
+ return self._use_ssl
+
+ def _get_id(self):
+ intids = query_utility(IIntIds)
+ return intids.register(self)
+
+ def clear(self):
+ self_id = self._get_id()
+ if self_id in managers:
+ del managers[self_id]
+
+ def get_connection(self, user=None, password=None):
+ self_id = self._get_id()
+ if self_id not in managers:
+ managers[self_id] = ConnectionManager(self)
+ return managers[self_id].get_connection(user, password)
+
+ def authenticate(self, credentials, request):
+ if not self.enabled:
+ return None
+ attrs = credentials.attributes
+ login = attrs.get('login')
+ password = attrs.get('password')
+ conn = self.get_connection()
+ search = LDAPQuery(self.base_dn, self.login_query, self.search_scope, (self.login_attribute,
+ self.uid_attribute))
+ result = search.execute(conn, login=login, password=password)
+ if not result or len(result) > 1:
+ return None
+ result = result[0]
+ login_dn = result[0]
+ try:
+ login_conn = self.get_connection(user=login_dn, password=password)
+ login_conn.unbind()
+ except ldap3.LDAPException:
+ logger.debug("LDAP authentication exception with login %r", login, exc_info=True)
+ return None
+ else:
+ if self.uid_attribute == 'dn':
+ return "{prefix}:{dn}".format(prefix=self.prefix,
+ dn=login_dn)
+ else:
+ attrs = result[1]
+ if self.login_attribute in attrs:
+ return "{prefix}:{attr}".format(prefix=self.prefix,
+ attr=attrs[self.uid_attribute][0])
+
+ def _get_group(self, group_id):
+ if not self.enabled:
+ return None
+
+ def get_principal(self, principal_id):
+ if not self.enabled:
+ return None
+ if not principal_id.startswith(self.prefix + ':'):
+ return None
+ prefix, login = principal_id.split(':', 1)
+ conn = self.get_connection()
+ if login.startswith(self.group_prefix + ':'):
+ group_prefix, group_id = login.split(':', 1)
+ attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format)
+ if self.group_uid_attribute == 'dn':
+ search = LDAPQuery(group_id, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, attributes)
+ else:
+ search = LDAPQuery(self.base_dn, self.uid_query, self.search_scope, attributes)
+ result = search.execute(conn, login=group_id)
+ if not result or len(result) > 1:
+ return None
+ group_dn, attrs = result[0]
+ return PrincipalInfo(id='{prefix}:{group_prefix}:{group_id}'.format(prefix=self.prefix,
+ group_prefix=self.group_prefix,
+ group_id=group_id),
+ title=self.group_title_format.format(**attrs),
+ dn=group_dn)
+ else:
+ attributes = FORMAT_ATTRIBUTES.findall(self.title_format)
+ if self.uid_attribute == 'dn':
+ search = LDAPQuery(login, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, attributes)
+ else:
+ search = LDAPQuery(self.base_dn, self.uid_query, self.search_scope, attributes)
+ result = search.execute(conn, login=login)
+ if not result or len(result) > 1:
+ return None
+ user_dn, attrs = result[0]
+ return PrincipalInfo(id='{prefix}:{login}'.format(prefix=self.prefix,
+ login=login),
+ title=self.title_format.format(**attrs),
+ dn=user_dn)
+
+ def _get_groups(self, principal):
+ if not self.groups_base_dn:
+ raise StopIteration
+ principal_dn = principal.attributes.get('dn')
+ if principal_dn is None:
+ raise StopIteration
+ conn = self.get_connection()
+ attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format)
+ search = LDAPQuery(self.groups_base_dn, self.groups_query, self.groups_search_scope, attributes)
+ for group_dn, group_attrs in search.execute(conn, dn=principal_dn):
+ if self.group_uid_attribute == 'dn':
+ yield '{prefix}:{group_prefix}:{dn}'.format(prefix=self.prefix,
+ group_prefix=self.group_prefix,
+ dn=group_dn)
+ else:
+ yield '{prefix}:{group_prefix}:{attr}'.format(prefix=self.prefix,
+ group_prefix=self.group_prefix,
+ attr=group_attrs[self.group_uid_attribute])
+
+ @cache_region('short')
+ def get_all_principals(self, principal_id):
+ if not self.enabled:
+ return set()
+ principal = self.get_principal(principal_id)
+ if principal is not None:
+ result = {principal_id}
+ if self.groups_query:
+ result |= set(self._get_groups(principal))
+ return result
+ return set()
+
+ def find_principals(self, query):
+ if not self.enabled:
+ raise StopIteration
+ if not query:
+ return None
+ conn = self.get_connection()
+ # users search
+ attributes = FORMAT_ATTRIBUTES.findall(self.title_format) + [self.uid_attribute, ]
+ search = LDAPQuery(self.base_dn, self.users_select_query, self.search_scope, attributes)
+ for user_dn, user_attrs in search.execute(conn, query=query):
+ if self.uid_attribute == 'dn':
+ yield PrincipalInfo(id='{prefix}:{dn}'.format(prefix=self.prefix,
+ dn=user_dn),
+ title=self.title_format.format(**user_attrs),
+ dn=user_dn)
+ else:
+ yield PrincipalInfo(id='{prefix}:{attr}'.format(prefix=self.prefix,
+ attr=user_attrs[self.uid_attribute][0]),
+ title=self.title_format.format(**user_attrs),
+ dn=user_dn)
+ # groups search
+ if self.groups_base_dn:
+ attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format) + [self.group_uid_attribute, ]
+ search = LDAPQuery(self.groups_base_dn, self.groups_select_query, self.groups_search_scope, attributes)
+ for group_dn, group_attrs in search.execute(conn, query=query):
+ if self.group_uid_attribute == 'dn':
+ yield PrincipalInfo(id='{prefix}:{group_prefix}:{dn}'.format(prefix=self.prefix,
+ group_prefix=self.group_prefix,
+ dn=group_dn),
+ title=self.group_title_format.format(**group_attrs),
+ dn=group_dn)
+ else:
+ yield PrincipalInfo(id='{prefix}:{group_prefix}:{attr}'.format(prefix=self.prefix,
+ group_prefix=self.group_prefix,
+ attr=group_attrs[
+ self.group_uid_attribute][0]),
+ title=self.group_title_format.format(**group_attrs),
+ dn=group_dn)
+
+ def get_search_results(self, data):
+ # LDAP search results are made of tuples containing DN and all
+ # entries attributes
+ query = data.get('query')
+ if not query:
+ return ()
+ conn = self.get_connection()
+ # users search
+ search = LDAPQuery(self.base_dn, self.users_search_query, self.search_scope, ldap3.ALL_ATTRIBUTES)
+ for user_dn, user_attrs in search.execute(conn, query=query):
+ yield user_dn, user_attrs
+ # groups search
+ if self.groups_base_dn:
+ search = LDAPQuery(self.groups_base_dn, self.groups_search_query, self.groups_search_scope, ldap3.ALL_ATTRIBUTES)
+ for group_dn, group_attrs in search.execute(conn, query=query):
+ yield group_dn, group_attrs
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/query.py Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,54 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import logging
+logger = logging.getLogger('PyAMS (ldap)')
+
+# import interfaces
+
+# import packages
+
+
+class LDAPQuery(object):
+ """Object representing an LDAP query"""
+
+ def __init__(self, base_dn, filter_tmpl, scope, attributes):
+ self.base_dn = base_dn
+ self.filter_tmpl = filter_tmpl
+ self.scope = scope
+ self.attributes = attributes
+
+ def __str__(self):
+ return ('base_dn={base_dn}, filter_tmpl={filter_tmpl}, '
+ 'scope={scope}, attributes={attributes}'.format(**self.__dict__))
+
+ def execute(self, conn, **kwargs):
+ key = (self.base_dn.format(**kwargs),
+ self.filter_tmpl.format(**kwargs))
+ logger.debug(">>> executing LDAP query: {0} (base {1}) <<< {1}".format(self.filter_tmpl,
+ self.base_dn,
+ str(kwargs.items())))
+ ret = conn.search(search_scope=self.scope,
+ attributes=self.attributes,
+ *key)
+ result, ret = conn.get_response(ret)
+ if result is None:
+ result = []
+ else:
+ result = [(r['dn'], r['attributes']) for r in result
+ if 'dn' in r]
+ logger.debug("<<< LDAP result = {0}".format(str(result)))
+ return result
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/tests/__init__.py Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/tests/test_utilsdocs.py Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,59 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# 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.
+#
+
+"""
+Generic Test case for pyams_ldap doctest
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+import doctest
+import sys
+import os
+
+
+current_dir = os.path.dirname(__file__)
+
+def doc_suite(test_dir, setUp=None, tearDown=None, globs=None):
+ """Returns a test suite, based on doctests found in /doctest."""
+ suite = []
+ if globs is None:
+ globs = globals()
+
+ flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
+ doctest.REPORT_ONLY_FIRST_FAILURE)
+
+ package_dir = os.path.split(test_dir)[0]
+ if package_dir not in sys.path:
+ sys.path.append(package_dir)
+
+ doctest_dir = os.path.join(package_dir, 'doctests')
+
+ # filtering files on extension
+ docs = [os.path.join(doctest_dir, doc) for doc in
+ os.listdir(doctest_dir) if doc.endswith('.txt')]
+
+ for test in docs:
+ suite.append(doctest.DocFileSuite(test, optionflags=flags,
+ globs=globs, setUp=setUp,
+ tearDown=tearDown,
+ module_relative=False))
+
+ return unittest.TestSuite(suite)
+
+def test_suite():
+ """returns the test suite"""
+ return doc_suite(current_dir)
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/tests/test_utilsdocstrings.py Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,62 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# 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.
+#
+
+"""
+Generic Test case for pyams_ldap doc strings
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+import doctest
+import sys
+import os
+
+
+current_dir = os.path.abspath(os.path.dirname(__file__))
+
+def doc_suite(test_dir, globs=None):
+ """Returns a test suite, based on doc tests strings found in /*.py"""
+ suite = []
+ if globs is None:
+ globs = globals()
+
+ flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
+ doctest.REPORT_ONLY_FIRST_FAILURE)
+
+ package_dir = os.path.split(test_dir)[0]
+ if package_dir not in sys.path:
+ sys.path.append(package_dir)
+
+ # filtering files on extension
+ docs = [doc for doc in
+ os.listdir(package_dir) if doc.endswith('.py')]
+ docs = [doc for doc in docs if not doc.startswith('__')]
+
+ for test in docs:
+ fd = open(os.path.join(package_dir, test))
+ content = fd.read()
+ fd.close()
+ if '>>> ' not in content:
+ continue
+ test = test.replace('.py', '')
+ location = 'pyams_ldap.%s' % test
+ suite.append(doctest.DocTestSuite(location, optionflags=flags,
+ globs=globs))
+
+ return unittest.TestSuite(suite)
+
+def test_suite():
+ """returns the test suite"""
+ return doc_suite(current_dir)
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/zmi/__init__.py Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/zmi/plugin.py Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,370 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# 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.
+#
+from pyramid.view import view_config
+from z3c.form.interfaces import DISPLAY_MODE
+from z3c.table.column import GetAttrColumn
+from z3c.table.interfaces import IColumn
+from zope.component.interfaces import ISite
+from pyams_form.form import AJAXAddForm, AJAXEditForm, InnerEditForm, InnerAddForm
+from pyams_form.interfaces.form import IInnerTabForm, IWidgetsSuffixViewletsManager
+from pyams_form.search import SearchView, SearchResultsView
+from pyams_ldap.interfaces import ILDAPPlugin
+from pyams_ldap.plugin import LDAPPlugin
+from pyams_ldap.query import LDAPQuery
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_security.interfaces import ISecurityManager, IPlugin
+from pyams_security.zmi.interfaces import ISecurityManagerToolbarAddingMenu
+from pyams_security.zmi.utility import SecurityManagerPluginsTable
+from pyams_skin.interfaces import IPageHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_skin.skin import apply_skin
+from pyams_skin.table import I18nColumn
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.registry import query_utility
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config, Viewlet
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm, AdminDialogDisplayForm
+from pyams_zmi.interfaces import IAdminView
+from pyams_zmi.layer import IAdminLayer
+from pyams_zmi.view import AdminView
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import base64
+import ldap3
+
+# import interfaces
+
+# import packages
+from z3c.form import field
+from zope.interface import implementer, Interface
+
+from pyams_ldap import _
+
+
+#
+# LDAP users folder views
+#
+
+@viewlet_config(name='add-ldap-folder.menu', context=ISite, layer=IAdminLayer,
+ view=SecurityManagerPluginsTable, manager=ISecurityManagerToolbarAddingMenu,
+ permission='system.manage', weight=60)
+class LDAPPluginAddMenu(ToolbarMenuItem):
+ """LDAP users folder add menu"""
+
+ label = _("Add LDAP users folder...")
+ label_css_class = 'fa fa-fw fa-sitemap'
+ url = 'add-ldap-folder.html'
+ modal_target = True
+
+
+class ILDAPForm(Interface):
+ """LDAP form"""
+
+
+@pagelet_config(name='add-ldap-folder.html', context=ISite, layer=IPyAMSLayer,
+ permission='system.manage')
+@implementer(ILDAPForm)
+class LDAPPluginAddForm(AdminDialogAddForm):
+ """LDAP users folder plug-in add form"""
+
+ title = _("System security manager")
+ legend = _("Add LDAP users folder plug-in")
+ icon_css_class = 'fa fa-fw fa-sitemap'
+
+ fields = field.Fields(IPlugin).omit('__name__', '__parent__')
+ ajax_handler = 'add-ldap-folder.json'
+ edit_permission = 'system.manage'
+
+ def create(self, data):
+ return LDAPPlugin()
+
+ def add(self, plugin):
+ context = query_utility(ISecurityManager)
+ context[plugin.prefix] = plugin
+
+ def nextURL(self):
+ return absolute_url(self.context, self.request, 'security-manager.html')
+
+
+@view_config(name='add-ldap-folder.json', context=ISite, request_type=IPyAMSLayer,
+ permission='system.manage', renderer='json', xhr=True)
+class LDAPPluginAJAXAddForm(AJAXAddForm, LDAPPluginAddForm):
+ """LDAP users folder plug-in add form, AJAX handler"""
+
+
+#
+# LDAP add form tabs
+#
+
+@adapter_config(name='connection', context=(ISite, IAdminLayer, ILDAPForm), provides=IInnerTabForm)
+class LDAPPluginConnectionAddForm(InnerAddForm):
+ """LDAP plug-in add form connection"""
+
+ id = 'ldap_connection_form'
+ tabLabel = _("Connection")
+ legend = None
+ fields = field.Fields(ILDAPPlugin).select('server_uri', 'bind_dn', 'bind_password', 'use_tls',
+ 'use_pool', 'pool_size', 'pool_lifetime')
+ weight = 1
+
+
+@adapter_config(name='users', context=(ISite, IAdminLayer, ILDAPForm), provides=IInnerTabForm)
+class LDAPPluginUsersAddForm(InnerAddForm):
+ """LDAP plug-in add form users schema"""
+
+ id = 'ldap_users_form'
+ tabLabel = _("Users schema")
+ legend = None
+ fields = field.Fields(ILDAPPlugin).select('base_dn', 'search_scope', 'login_attribute', 'login_query',
+ 'uid_attribute', 'uid_query', 'title_format')
+ weight = 2
+
+
+@adapter_config(name='groups', context=(ISite, IAdminLayer, ILDAPForm), provides=IInnerTabForm)
+class LDAPPluginGroupsAddForm(InnerAddForm):
+ """LDAP plug-in add form groups schema"""
+
+ id = 'ldap_groups_form'
+ tabLabel = _("Groups schema")
+ legend = None
+ fields = field.Fields(ILDAPPlugin).select('groups_base_dn', 'groups_search_scope', 'groups_query',
+ 'group_prefix', 'group_uid_attribute', 'group_title_format')
+ weight = 3
+
+
+@adapter_config(name='search', context=(ISite, IAdminLayer, ILDAPForm), provides=IInnerTabForm)
+class LDAPPluginSearchAddForm(InnerAddForm):
+ """LDAP plug-in add form search settings"""
+
+ id = 'ldap_search_form'
+ tabLabel = _("Search settings")
+ legend = None
+ fields = field.Fields(ILDAPPlugin).select('users_select_query', 'users_search_query',
+ 'groups_select_query', 'groups_search_query')
+ weight = 4
+
+
+@pagelet_config(name='properties.html', context=ILDAPPlugin, layer=IPyAMSLayer,
+ permission='system.view')
+@implementer(ILDAPForm)
+class LDAPPluginEditForm(AdminDialogEditForm):
+ """LDAP users folder plug-in edit form"""
+
+ @property
+ def title(self):
+ return self.context.title
+
+ legend = _("Edit LDAP users folder plug-in properties")
+ icon_css_class = 'fa fa-fw fa-sitemap'
+
+ fields = field.Fields(IPlugin).omit('__parent__', '__name__')
+
+ ajax_handler = 'properties.json'
+ edit_permission = 'system.manage'
+
+ def updateWidgets(self, prefix=None):
+ super(LDAPPluginEditForm, self).updateWidgets()
+ self.widgets['prefix'].mode = DISPLAY_MODE
+
+ def update_content(self, content, data):
+ changes = super(LDAPPluginEditForm, self).update_content(content, data)
+ if changes:
+ self.context.clear()
+ return changes
+
+
+@view_config(name='properties.json', context=ILDAPPlugin, request_type=IPyAMSLayer,
+ permission='system.manage', renderer='json', xhr=True)
+@implementer(IAdminView)
+class LDAPPluginAJAXEditForm(AJAXEditForm, LDAPPluginEditForm):
+ """LDAP users folder plug-in edit form, AJAX handler"""
+
+
+#
+# LDAP edit form tabs
+#
+
+@adapter_config(name='connection', context=(ILDAPPlugin, IAdminLayer, ILDAPForm), provides=IInnerTabForm)
+class LDAPPluginConnectionEditForm(InnerEditForm):
+ """LDAP plug-in connection edit form"""
+
+ id = 'ldap_connection_form'
+ tabLabel = _("Connection")
+ legend = None
+ fields = field.Fields(ILDAPPlugin).select('server_uri', 'bind_dn', 'bind_password', 'use_tls',
+ 'use_pool', 'pool_size', 'pool_lifetime')
+ weight = 1
+
+
+@adapter_config(name='users', context=(ILDAPPlugin, IAdminLayer, ILDAPForm), provides=IInnerTabForm)
+class LDAPPluginUsersEditForm(InnerEditForm):
+ """LDAP plug-in users schema edit form"""
+
+ id = 'ldap_users_form'
+ tabLabel = _("Users schema")
+ legend = None
+ fields = field.Fields(ILDAPPlugin).select('base_dn', 'search_scope', 'login_attribute', 'login_query',
+ 'uid_attribute', 'uid_query', 'title_format')
+ weight = 2
+
+
+@adapter_config(name='groups', context=(ILDAPPlugin, IAdminLayer, ILDAPForm), provides=IInnerTabForm)
+class LDAPPluginGroupsEditForm(InnerEditForm):
+ """LDAP plug-in groups schema edit form"""
+
+ id = 'ldap_groups_form'
+ tabLabel = _("Groups schema")
+ legend = None
+ fields = field.Fields(ILDAPPlugin).select('groups_base_dn', 'groups_search_scope', 'groups_query',
+ 'group_prefix', 'group_uid_attribute', 'group_title_format')
+ weight = 3
+
+
+@adapter_config(name='search', context=(ILDAPPlugin, IAdminLayer, ILDAPForm), provides=IInnerTabForm)
+class LDAPPluginSearchEditForm(InnerEditForm):
+ """LDAP plug-in search settings"""
+
+ id = 'ldap_search_form'
+ tabLabel = _("Search settings")
+ legend = None
+ fields = field.Fields(ILDAPPlugin).select('users_select_query', 'users_search_query',
+ 'groups_select_query', 'groups_search_query')
+
+ label_css_class = 'control-label col-md-4'
+ input_css_class = 'col-md-8'
+ weight = 4
+
+
+#
+# Users folder search views
+#
+
+@pagelet_config(name='search.html', context=ILDAPPlugin, layer=IPyAMSLayer, permission='system.view')
+class LDAPPluginSearchView(AdminView, SearchView):
+ """LDAP users folder search view"""
+
+ def __init__(self, context, request):
+ super(LDAPPluginSearchView, self).__init__(context, request)
+
+
+@adapter_config(context=(ILDAPPlugin, IAdminLayer, LDAPPluginSearchView), provides=IPageHeader)
+class LDAPPluginSearchViewHeaderAdapter(ContextRequestViewAdapter):
+ """LDAP users folder search view header adapter"""
+
+ back_url = '#security-manager.html'
+ icon_class = 'fa fa-fw fa-sitemap'
+
+ @property
+ def title(self):
+ return self.context.title
+
+ subtitle = _("Search users and groups")
+
+
+@view_config(name='search-results.html', context=ILDAPPlugin, request_type=IPyAMSLayer,
+ permission='system.view')
+class LDAPPluginSearchResultsView(AdminView, SearchResultsView):
+ """LDAP users folder search results view table"""
+
+ id = 'ldap_folder_search_table'
+ title = _("Search results")
+ cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight datatable'}
+
+ @property
+ def data_attributes(self):
+ return {'tr': {'data-ams-element-name': lambda x: x[0],
+ 'data-ams-url': lambda x: '{url}?dn={dn}'.format(url=absolute_url(self.context, self.request,
+ 'user-properties.html'),
+ dn=x[0]),
+ 'data-toggle': 'modal'}}
+
+ def __init__(self, context, request):
+ super(LDAPPluginSearchResultsView, self).__init__(context, request)
+ apply_skin(self.request, 'PyAMS admin skin')
+
+
+class LDAPColumn(I18nColumn, GetAttrColumn):
+ """Base LDAP column"""
+
+ def getValue(self, obj):
+ return ', '.join(obj[1].get(self.attrName, ()))
+
+
+@adapter_config(name='name', context=(ILDAPPlugin, IAdminLayer, LDAPPluginSearchResultsView),
+ provides=IColumn)
+class LDAPCnColumn(LDAPColumn):
+ """CN column"""
+
+ _header = _("Common name")
+ attrName = 'cn'
+ weight = 5
+
+
+@adapter_config(name='mail', context=(ILDAPPlugin, IAdminLayer, LDAPPluginSearchResultsView),
+ provides=IColumn)
+class LDAPMailColumn(LDAPColumn):
+ """Mail column"""
+
+ _header = _("E-mail")
+ attrName = 'mail'
+ weight = 20
+
+
+#
+# LDAP principal display form
+#
+
+@pagelet_config(name='user-properties.html', context=ILDAPPlugin, layer=IPyAMSLayer)
+class LDAPPrincipalDisplayForm(AdminDialogDisplayForm):
+ """LDAP principal display form"""
+
+ @property
+ def title(self):
+ return self.context.title
+
+ @property
+ def legend(self):
+ return self.request.localizer.translate(_("Display LDAP entry: {dn}")).format(dn=self.request.params['dn'])
+
+ icon_class = 'fa fa-fw fa-sitemap'
+
+ fields = field.Fields(Interface)
+
+
+@viewlet_config(name='ldap-attributes', layer=IAdminLayer, manager=IWidgetsSuffixViewletsManager,
+ view=LDAPPrincipalDisplayForm)
+@template_config(template='templates/ldap-attributes.pt')
+class LDAPPrincipalAttributesViewlet(Viewlet):
+ """LDAP principal attributes"""
+
+ br = '<br />'
+
+ @property
+ def attributes(self):
+ plugin = self.context
+ conn = plugin.get_connection()
+ dn = self.request.params.get('dn')
+ query = LDAPQuery(dn, '(objectclass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, ldap3.ALL_ATTRIBUTES)
+ result = query.execute(conn)
+ if not result or len(result) > 1:
+ return ()
+ dn, attributes = result[0]
+ if 'jpegPhoto' in attributes:
+ attributes['jpegPhoto'] = ['<img src="data:image/jpeg;base64,{0}" />'.
+ format(base64.encodebytes(attributes['jpegPhoto'][0]).decode()), ]
+ result = sorted(attributes.items(), key=lambda x: x[0])
+ return result
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_ldap/zmi/templates/ldap-attributes.pt Sat Feb 28 15:20:14 2015 +0100
@@ -0,0 +1,10 @@
+<table class="table table-bordered table-striped table-hover table-tight datatable">
+ <tr>
+ <th>Attribute name</th>
+ <th>Value</th>
+ </tr>
+ <tr tal:repeat="attr view.attributes">
+ <td tal:content="attr[0]"></td>
+ <td tal:content="structure view.br.join(sorted(attr[1]))"></td>
+ </tr>
+</table>