# HG changeset patch # User Thierry Florac # Date 1425133214 -3600 # Node ID 94ee60dd51e1732275529818147d7743c7aa1c96 First commit diff -r 000000000000 -r 94ee60dd51e1 .hgignore --- /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$ diff -r 000000000000 -r 94ee60dd51e1 LICENSE --- /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. diff -r 000000000000 -r 94ee60dd51e1 MANIFEST.in --- /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 *.*~ diff -r 000000000000 -r 94ee60dd51e1 bootstrap.py --- /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) diff -r 000000000000 -r 94ee60dd51e1 buildout.cfg --- /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 diff -r 000000000000 -r 94ee60dd51e1 docs/HISTORY.txt diff -r 000000000000 -r 94ee60dd51e1 docs/README.txt diff -r 000000000000 -r 94ee60dd51e1 setup.py --- /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 +# 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: -*- + """, + ) diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap.egg-info/PKG-INFO --- /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 diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap.egg-info/SOURCES.txt --- /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 diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap.egg-info/dependency_links.txt --- /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 @@ + diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap.egg-info/entry_points.txt --- /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 diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap.egg-info/namespace_packages.txt --- /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 @@ + diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap.egg-info/not-zip-safe --- /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 @@ + diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap.egg-info/requires.txt --- /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] diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap.egg-info/top_level.txt --- /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 diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/__init__.py --- /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 +# 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') diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/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 @@ + + + + + + diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/doctests/README.txt --- /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 +================== diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/interfaces/__init__.py --- /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 +# 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""" diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/locales/fr/LC_MESSAGES/pyams_ldap.po --- /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 , 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 \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" diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/locales/pyams_ldap.pot --- /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 , 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 \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 "" diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/plugin.py --- /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 +# 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 diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/query.py --- /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 +# 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 diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/tests/__init__.py --- /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 @@ + diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/tests/test_utilsdocs.py --- /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 +# 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') + diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/tests/test_utilsdocstrings.py --- /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 +# 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') diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/zmi/__init__.py --- /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 +# 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 diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/zmi/plugin.py --- /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 +# 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 = '
' + + @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'] = [''. + format(base64.encodebytes(attributes['jpegPhoto'][0]).decode()), ] + result = sorted(attributes.items(), key=lambda x: x[0]) + return result diff -r 000000000000 -r 94ee60dd51e1 src/pyams_ldap/zmi/templates/ldap-attributes.pt --- /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 @@ + + + + + + + + + +
Attribute nameValue