First commit
authorThierry Florac <thierry.florac@onf.fr>
Thu, 19 Feb 2015 10:53:29 +0100
changeset 0 f04e1d0a0723
child 1 5595823c66f1
First commit
.hgignore
.installed.cfg
LICENSE
MANIFEST.in
bootstrap.py
buildout.cfg
docs/HISTORY.txt
docs/README.txt
setup.py
src/pyams_security.egg-info/PKG-INFO
src/pyams_security.egg-info/SOURCES.txt
src/pyams_security.egg-info/dependency_links.txt
src/pyams_security.egg-info/entry_points.txt
src/pyams_security.egg-info/namespace_packages.txt
src/pyams_security.egg-info/not-zip-safe
src/pyams_security.egg-info/requires.txt
src/pyams_security.egg-info/top_level.txt
src/pyams_security/__init__.py
src/pyams_security/configure.zcml
src/pyams_security/credential.py
src/pyams_security/doctests/README.txt
src/pyams_security/include.py
src/pyams_security/interfaces/__init__.py
src/pyams_security/locales/fr/LC_MESSAGES/pyams_security.mo
src/pyams_security/locales/fr/LC_MESSAGES/pyams_security.po
src/pyams_security/locales/fr/LC_MESSAGES/pyams_security.po~
src/pyams_security/locales/pyams_security.pot
src/pyams_security/permission.py
src/pyams_security/plugin/__init__.py
src/pyams_security/plugin/admin.py
src/pyams_security/plugin/http.py
src/pyams_security/plugin/userfolder.py
src/pyams_security/principal.py
src/pyams_security/resources/js/authomatic.js
src/pyams_security/resources/js/authomatic.min.js
src/pyams_security/resources/js/security.js
src/pyams_security/resources/js/security.min.js
src/pyams_security/role.py
src/pyams_security/site.py
src/pyams_security/tests/__init__.py
src/pyams_security/tests/test_utilsdocs.py
src/pyams_security/tests/test_utilsdocstrings.py
src/pyams_security/utility.py
src/pyams_security/views/__init__.py
src/pyams_security/views/login.py
src/pyams_security/views/oauth.py
src/pyams_security/views/templates/social-login.pt
src/pyams_security/views/templates/user-registration-end.pt
src/pyams_security/views/templates/user-registration.pt
src/pyams_security/views/userfolder.py
src/pyams_security/vocabulary.py
src/pyams_security/zmi/__init__.py
src/pyams_security/zmi/configure.zcml
src/pyams_security/zmi/interfaces.py
src/pyams_security/zmi/plugin/__init__.py
src/pyams_security/zmi/plugin/admin.py
src/pyams_security/zmi/plugin/directory.py
src/pyams_security/zmi/plugin/userfolder.py
src/pyams_security/zmi/utility.py
src/pyams_security/zmi/widget/__init__.py
src/pyams_security/zmi/widget/templates/ordered-list-display.pt
src/pyams_security/zmi/widget/templates/ordered-list-input.pt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Thu Feb 19 10:53:29 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/.installed.cfg	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,71 @@
+[buildout]
+installed_develop_eggs = /home/tflorac/Dropbox/src/PyAMS/pyams_security/develop-eggs/pyams-security.egg-link
+parts = package i18n pyflakes test
+
+[package]
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/pshell
+	/home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/pviews
+	/home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/pcreate
+	/home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/proutes
+	/home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/prequest
+	/home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/pdistreport
+	/home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/ptweens
+	/home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/pserve
+__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.1-py3.4.egg zc.buildout-2.3.1-py3.4.egg
+_b = /home/tflorac/Dropbox/src/PyAMS/pyams_security/bin
+_d = /home/tflorac/Dropbox/src/PyAMS/pyams_security/develop-eggs
+_e = /var/local/env/pyams/eggs
+bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_security/bin
+develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_security/develop-eggs
+eggs = pyams_security
+	pyramid
+	zope.component
+	zope.interface
+eggs-directory = /var/local/env/pyams/eggs
+recipe = zc.recipe.egg
+
+[i18n]
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/pybabel
+	/home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/pot-create
+	/home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/polint
+__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.1-py3.4.egg zc.buildout-2.3.1-py3.4.egg
+_b = /home/tflorac/Dropbox/src/PyAMS/pyams_security/bin
+_d = /home/tflorac/Dropbox/src/PyAMS/pyams_security/develop-eggs
+_e = /var/local/env/pyams/eggs
+bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_security/bin
+develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_security/develop-eggs
+eggs = babel
+	lingua
+eggs-directory = /var/local/env/pyams/eggs
+recipe = zc.recipe.egg
+
+[pyflakes]
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/pyflakes
+	/home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/pyflakes
+__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.1-py3.4.egg zc.buildout-2.3.1-py3.4.egg
+_b = /home/tflorac/Dropbox/src/PyAMS/pyams_security/bin
+_d = /home/tflorac/Dropbox/src/PyAMS/pyams_security/develop-eggs
+_e = /var/local/env/pyams/eggs
+bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_security/bin
+develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_security/develop-eggs
+eggs = pyflakes
+eggs-directory = /var/local/env/pyams/eggs
+entry-points = pyflakes=pyflakes.scripts.pyflakes:main
+initialization = if not sys.argv[1:]: sys.argv[1:] = ["src"]
+recipe = zc.recipe.egg
+scripts = pyflakes
+
+[test]
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_security/parts/test
+	/home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/test
+__buildout_signature__ = zc.recipe.testrunner-2.0.0-py3.4.egg zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.1-py3.4.egg zope.testrunner-4.4.6-py3.4.egg zc.buildout-2.3.1-py3.4.egg zope.interface-4.1.2-py3.4-linux-x86_64.egg zope.exceptions-4.0.7-py3.4.egg six-1482e89f68d85eea27f4ed7749df2819
+_b = /home/tflorac/Dropbox/src/PyAMS/pyams_security/bin
+_d = /home/tflorac/Dropbox/src/PyAMS/pyams_security/develop-eggs
+_e = /var/local/env/pyams/eggs
+bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_security/bin
+develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_security/develop-eggs
+eggs = pyams_security [test]
+eggs-directory = /var/local/env/pyams/eggs
+location = /home/tflorac/Dropbox/src/PyAMS/pyams_security/parts/test
+recipe = zc.recipe.testrunner
+script = /home/tflorac/Dropbox/src/PyAMS/pyams_security/bin/test
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE	Thu Feb 19 10:53:29 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	Thu Feb 19 10:53:29 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	Thu Feb 19 10:53:29 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	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,61 @@
+[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 = .
+
+parts =
+    package
+    i18n
+    pyflakes
+    test
+
+[package]
+recipe = zc.recipe.egg
+eggs =
+    authomatic
+    pyams_security
+    pyramid
+    zope.component
+    zope.interface
+    zope.password
+
+[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_security [test]
+
+[versions]
+pyams_security = 0.1.0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/README.txt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,85 @@
+======================
+PyAMS security package
+======================
+
+Roles et permissions
+--------------------
+ - déclaration des permissions
+ - déclaration des rôles
+ - association rôles => permissions
+ - gestion des rôles locaux (Cf pyramid_localroles)
+ - fonctions "config.register_permission()" et "config.register_role()"
+
+
+Rôles locaux
+------------
+ - interface ILocalRoles(Interface)
+   __roles__: {'role.id1': {'principal.id1', 'principal.id1'...},
+               'role.id2': {'principal.id1', 'principal.id3'...}}
+ - catalogage des rôles attribués pour recherche rapide
+ * les rôles attribués à un utilisateur font partie de ses "groupes"
+
+
+Modules d'extraction de credentials
+-----------------------------------
+ * extract_credentials
+     -> request
+     <- Credentials
+
+ - HTTP basic
+ - Form -> mémoriser login/password en session
+
+
+Modules d'authentification
+--------------------------
+ * authenticate
+     -> Credentials, request
+     <- principal_id
+
+ - Users locaux avec mot de passe
+ - LDAP
+ - SQLAlchemy
+ - (OAuth (authomatic))
+ * chaque plug-in peut-être présent en plusieurs exemplaires, chacun avec un préfixe propre
+ * exemple pour LDAP avec plusieurs branches autorisées
+
+
+Modules de stockage des utilisateurs
+------------------------------------
+ * find_principal
+     -> principal_id
+     <- PrincipalInfo
+
+ - LDAP  => préfixe = ldap:  => ldap:uid (ou ldap:dn ?)
+ - SQLAlchemy => préfixe = sa.session_name:  => sa.AGENTS:12345, sa.CLIENTS:45678
+ - Users OAuth  => préfixe = oauth:  => oauth:twitter.123456
+ - Users locaux  => préfixe = local:  => local:uid (ou ldap:email ?)
+ - Groupes  => stockage d'une liste d'ID d'utilisateurs
+               préfixe = group:  => group:managers
+
+ - recherche des utilisateurs
+
+
+Module d'encodage des mots de passe
+-----------------------------------
+ - encodage et vérification des mots de passe
+
+
+Utilitaire d'authentification
+-----------------------------
+ - prise en charge des modules
+ - application des fonctions d'authentification
+
+
+Annotations des utilisateurs
+----------------------------
+ - interface générique pour associer des métadonnées à un ID utilisateur
+
+
+Événements
+----------
+ - AuthenticatedPrincipal
+     -> notifié lorsqu'un utilisateur s'authentifie avec succès (HTTP, Form, OAuth)
+     -> enregistrement des credentials en session (si besoin du mot de passe)
+     -> permet d'enregistrer les propriétés de l'utilisateur dans un plug-in de stockage
+        si l'inscription "libre" est autorisée
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,70 @@
+#
+# 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_security 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_security',
+      version=version,
+      description="PyAMS security interfaces and classes",
+      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 security authentication',
+      author='Thierry Florac',
+      author_email='tflorac@ulthar.net',
+      url='http://hg.ztfy.org/pyams/pyams_security',
+      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_security.tests.test_utilsdocs.test_suite",
+      tests_require=tests_require,
+      extras_require=dict(test=tests_require),
+      install_requires=[
+          'setuptools',
+          # -*- Extra requirements: -*-
+          'authomatic',
+          'fanstatic',
+          'pyramid',
+          'zope.component',
+          'zope.interface',
+          'zope.password'
+      ],
+      entry_points={
+          'fanstatic.libraries': [
+              'pyams_security = pyams_security:library'
+          ]
+      })
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security.egg-info/PKG-INFO	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,103 @@
+Metadata-Version: 1.1
+Name: pyams-security
+Version: 0.1.0
+Summary: PyAMS security interfaces and classes
+Home-page: http://hg.ztfy.org/pyams/pyams_security
+Author: Thierry Florac
+Author-email: tflorac@ulthar.net
+License: ZPL
+Description: ======================
+        PyAMS security package
+        ======================
+        
+        Roles et permissions
+        --------------------
+         - déclaration des permissions
+         - déclaration des rôles
+         - association rôles => permissions
+         - gestion des rôles locaux (Cf pyramid_localroles)
+         - fonctions "config.register_permission()" et "config.register_role()"
+        
+        
+        Rôles locaux
+        ------------
+         - interface ILocalRoles(Interface)
+           __roles__: {'role.id1': {'principal.id1', 'principal.id1'...},
+                       'role.id2': {'principal.id1', 'principal.id3'...}}
+         - catalogage des rôles attribués pour recherche rapide
+         * les rôles attribués à un utilisateur font partie de ses "groupes"
+        
+        
+        Modules d'extraction de credentials
+        -----------------------------------
+         * extract_credentials
+             -> request
+             <- Credentials
+        
+         - HTTP basic
+         - Form -> mémoriser login/password en session
+        
+        
+        Modules d'authentification
+        --------------------------
+         * authenticate
+             -> Credentials, request
+             <- principal_id
+        
+         - Users locaux avec mot de passe
+         - LDAP
+         - SQLAlchemy
+         - (OAuth (authomatic))
+         * chaque plug-in peut-être présent en plusieurs exemplaires, chacun avec un préfixe propre
+         * exemple pour LDAP avec plusieurs branches autorisées
+        
+        
+        Modules de stockage des utilisateurs
+        ------------------------------------
+         * find_principal
+             -> principal_id
+             <- PrincipalInfo
+        
+         - LDAP  => préfixe = ldap:  => ldap:uid (ou ldap:dn ?)
+         - SQLAlchemy => préfixe = sa.session_name:  => sa.AGENTS:12345, sa.CLIENTS:45678
+         - Users OAuth  => préfixe = oauth:  => oauth:twitter.123456
+         - Users locaux  => préfixe = local:  => local:uid (ou ldap:email ?)
+         - Groupes  => stockage d'une liste d'ID d'utilisateurs
+                       préfixe = group:  => group:managers
+        
+         - recherche des utilisateurs
+        
+        
+        Module d'encodage des mots de passe
+        -----------------------------------
+         - encodage et vérification des mots de passe
+        
+        
+        Utilitaire d'authentification
+        -----------------------------
+         - prise en charge des modules
+         - application des fonctions d'authentification
+        
+        
+        Annotations des utilisateurs
+        ----------------------------
+         - interface générique pour associer des métadonnées à un ID utilisateur
+        
+        
+        Événements
+        ----------
+         - AuthenticatedPrincipal
+             -> notifié lorsqu'un utilisateur s'authentifie avec succès (HTTP, Form, OAuth)
+             -> enregistrement des credentials en session (si besoin du mot de passe)
+             -> permet d'enregistrer les propriétés de l'utilisateur dans un plug-in de stockage
+                si l'inscription "libre" est autorisée
+        
+        
+        
+Keywords: Pyramid PyAMS security authentication
+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_security.egg-info/SOURCES.txt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,43 @@
+MANIFEST.in
+setup.py
+docs/HISTORY.txt
+docs/README.txt
+src/pyams_security/__init__.py
+src/pyams_security/configure.zcml
+src/pyams_security/credential.py
+src/pyams_security/include.py
+src/pyams_security/permission.py
+src/pyams_security/principal.py
+src/pyams_security/role.py
+src/pyams_security/site.py
+src/pyams_security/utility.py
+src/pyams_security.egg-info/PKG-INFO
+src/pyams_security.egg-info/SOURCES.txt
+src/pyams_security.egg-info/dependency_links.txt
+src/pyams_security.egg-info/entry_points.txt
+src/pyams_security.egg-info/namespace_packages.txt
+src/pyams_security.egg-info/not-zip-safe
+src/pyams_security.egg-info/requires.txt
+src/pyams_security.egg-info/top_level.txt
+src/pyams_security/doctests/README.txt
+src/pyams_security/interfaces/__init__.py
+src/pyams_security/plugin/__init__.py
+src/pyams_security/plugin/admin.py
+src/pyams_security/plugin/http.py
+src/pyams_security/resources/js/authomatic.js
+src/pyams_security/resources/js/authomatic.min.js
+src/pyams_security/resources/js/security.js
+src/pyams_security/resources/js/security.min.js
+src/pyams_security/tests/__init__.py
+src/pyams_security/tests/test_utilsdocs.py
+src/pyams_security/tests/test_utilsdocstrings.py
+src/pyams_security/views/__init__.py
+src/pyams_security/views/login.py
+src/pyams_security/views/oauth.py
+src/pyams_security/views/templates/social-login.pt
+src/pyams_security/zmi/__init__.py
+src/pyams_security/zmi/configure.zcml
+src/pyams_security/zmi/interfaces.py
+src/pyams_security/zmi/utility.py
+src/pyams_security/zmi/plugin/__init__.py
+src/pyams_security/zmi/plugin/admin.py
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security.egg-info/dependency_links.txt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security.egg-info/entry_points.txt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,3 @@
+[fanstatic.libraries]
+pyams_security = pyams_security:library
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security.egg-info/namespace_packages.txt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security.egg-info/not-zip-safe	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security.egg-info/requires.txt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,8 @@
+setuptools
+fanstatic
+pyramid
+zope.component
+zope.interface
+zope.password
+
+[test]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security.egg-info/top_level.txt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,1 @@
+pyams_security
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/__init__.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,37 @@
+#
+# 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 fanstatic import Library
+
+# import interfaces
+
+# import packages
+
+from pyramid.i18n import TranslationStringFactory
+_ = TranslationStringFactory('pyams_security')
+
+
+library = Library('pyams_security', 'resources')
+
+
+def includeme(config):
+    """Pyramid include
+
+    Split in another package to remove cyclic dependencies with TranslationStringFactory
+    """
+    from .include import include_package
+    include_package(config)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/configure.zcml	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,44 @@
+<configure
+	xmlns="http://pylonshq.com/pyramid"
+	xmlns:i18n="http://namespaces.zope.org/i18n"
+	xmlns:zcml="http://namespaces.zope.org/zcml">
+
+	<include package="pyramid_zcml" />
+	<include package="zope.component" file="meta.zcml" />
+	<include package="zope.i18n" file="meta.zcml" />
+
+	<i18n:registerTranslations directory="locales" />
+
+
+	<!-- Zope password registrations -->
+	<configure zcml:condition="installed zope.password">
+
+		<utility
+			name="Plain Text"
+			provides="zope.password.interfaces.IPasswordManager"
+			factory="zope.password.password.PlainTextPasswordManager" />
+
+		<utility
+			name="MD5"
+			provides="zope.password.interfaces.IPasswordManager"
+			factory="zope.password.password.MD5PasswordManager" />
+
+		<utility
+			name="SHA1"
+			provides="zope.password.interfaces.IPasswordManager"
+			factory="zope.password.password.SHA1PasswordManager" />
+
+		<utility
+			name="SSHA"
+			provides="zope.password.interfaces.IPasswordManager"
+			factory="zope.password.password.SSHAPasswordManager" />
+
+	</configure>
+
+
+	<!-- ZMI package -->
+	<configure zcml:condition="installed pyams_zmi">
+		<include package=".zmi" />
+	</configure>
+
+</configure>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/credential.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,37 @@
+#
+# 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
+from pyams_security.interfaces import ICredentials
+
+# import packages
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(ICredentials)
+class Credentials(object):
+    """Credentials class"""
+
+    prefix = FieldProperty(ICredentials['prefix'])
+    id = FieldProperty(ICredentials['id'])
+    attributes = FieldProperty(ICredentials['attributes'])
+
+    def __init__(self, prefix, id, **attributes):
+        self.prefix = prefix
+        self.id = id
+        self.attributes.update(**attributes)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/doctests/README.txt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,3 @@
+======================
+pyams_security package
+======================
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/include.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,58 @@
+#
+# 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
+from pyams_security.permission import register_permission
+from pyams_security.plugin import PluginSelector
+from pyams_security.role import register_role
+from pyams_security.utility import get_principal
+
+
+def include_package(config):
+    """Pyramid include"""
+    # add translations
+    config.add_translation_dirs('pyams_security:locales')
+
+    # add configuration directives
+    config.add_directive('register_permission', register_permission)
+    config.add_directive('register_role', register_role)
+    config.add_request_method(get_principal, 'principal', reify=True)
+
+    # add subscribers predicate
+    config.add_subscriber_predicate('plugin_selector', PluginSelector)
+
+    # add custom routes
+    config.add_route('login', '/login/{provider_name}')
+
+    # register custom permissions
+    config.register_permission({'id': 'system.manage',
+                                'title': "Manage system"})
+    config.register_permission({'id': 'system.view',
+                                'title': "View management screens"})
+
+    # load registry components
+    try:
+        import pyams_zmi
+    except ImportError:
+        config.scan(ignore='pyams_security.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_security/interfaces/__init__.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,500 @@
+#
+# 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 re
+
+# import interfaces
+from pyams_skin.interfaces import IContentSearch
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.container.interfaces import IContainer
+from zope.location.interfaces import IContained
+
+# import packages
+from pyams_utils.schema import EncodedPassword
+from zope.configuration.fields import GlobalObject
+from zope.container.constraints import contains, containers
+from zope.interface import implementer, Interface, Attribute, invariant, Invalid
+from zope.schema import TextLine, Text, Int, Bool, List, Tuple, Set, Dict, Choice, Datetime
+
+from pyams_security import _
+
+
+class IPermission(Interface):
+    """Permission utility class"""
+
+    id = TextLine(title="Unique ID",
+                  required=True)
+
+    title = TextLine(title="Title",
+                     required=True)
+
+    description = Text(title="Description",
+                       required=False)
+
+
+class IRole(Interface):
+    """Role utility class"""
+
+    id = TextLine(title="Unique ID",
+                  required=True)
+
+    title = TextLine(title="Title",
+                     required=True)
+
+    description = Text(title="Description",
+                       required=False)
+
+    permissions = Set(title="Permissions",
+                      description="ID of role's permissions",
+                      value_type=TextLine(),
+                      required=False)
+
+
+class IPrincipalInfo(Interface):
+    """Principal info class
+
+    This is the generic interface of objects defined in request 'principal' attribute
+    """
+
+    id = TextLine(title="Globally unique ID",
+                  required=True)
+
+    title = TextLine(title="Principal name",
+                     required=True)
+
+    groups = Set(title="Principal groups",
+                 description="IDs of principals to which this principal directly belongs",
+                 value_type=TextLine())
+
+
+class ICredentials(Interface):
+    """Credentials interface"""
+
+    prefix = TextLine(title="Credentials plug-in prefix",
+                      description="Prefix of plug-in which extracted credentials")
+
+    id = TextLine(title="Credentials ID")
+
+    attributes = Dict(title="Credentials attributes",
+                      description="Attributes dictionary defined by each credentials plug-in",
+                      required=False,
+                      default={})
+
+
+#
+# Credentials, authentication and directory plug-ins
+#
+
+class IPluginEvent(Interface):
+    """Plug-in event interface"""
+
+    plugin = Attribute("Event plug-in name")
+
+
+class IAuthenticatedPrincipalEvent(IPluginEvent):
+    """Authenticated principal event interface"""
+
+    principal_id = Attribute("Authenticated principal ID")
+
+    infos = Attribute("Event custom infos")
+
+
+@implementer(IAuthenticatedPrincipalEvent)
+class AuthenticatedPrincipalEvent(object):
+    """Authenticated principal event"""
+
+    def __init__(self, plugin, principal_id, **infos):
+        self.plugin = plugin
+        self.principal_id = principal_id
+        self.infos = infos
+
+
+class IPlugin(IContained, IAttributeAnnotatable):
+    """Basic authentication plug-in interface"""
+
+    containers('pyams_security.interfaces.IAuthentication')
+
+    prefix = TextLine(title=_("Plug-in prefix"),
+                      description=_("This prefix is mainly used by authentication plug-ins to mark principals"))
+
+    title = TextLine(title=_("Plug-in title"),
+                     required=False)
+
+    enabled = Bool(title=_("Enabled plug-in?"),
+                   description=_("You can choose to disable any plug-in..."),
+                   required=True,
+                   default=True)
+
+
+class ICredentialsInfo(Interface):
+    """Credentials extraction plug-in base interface"""
+
+    def extract_credentials(self, request):
+        """Extract user credentials from given request
+
+        Result of 'extract_credentials' call should be an ICredentials object for which
+        id is the 'raw' principal ID (without prefix); only authentication plug-ins should
+        add a prefix to principal IDs to distinguish principals
+        """
+
+
+class ICredentialsPlugin(ICredentialsInfo, IPlugin):
+    """Credentials extraction plug-in interface"""
+
+
+class IAuthenticationInfo(Interface):
+    """Principal authentication plug-in base interface"""
+
+    def authenticate(self, credentials, request):
+        """Authenticate given credentials and returns a principal ID or None"""
+
+
+class IAuthenticationPlugin(IAuthenticationInfo, IPlugin):
+    """Principal authentication plug-in interface"""
+
+
+class IAdminAuthenticationPlugin(IAuthenticationPlugin):
+    """Admin authentication plug-in base interface"""
+
+    login = TextLine(title=_("Admin. login"))
+
+    password = EncodedPassword(title=_("Admin. password"))
+
+
+class IDirectoryInfo(Interface):
+    """Principal directory plug-in interface"""
+
+    def get_principal(self, principal_id):
+        """Returns real principal matching given ID, or None"""
+
+    def get_all_principals(self, principal_id):
+        """Returns all principals matching given principal ID"""
+
+    def find_principals(self, query):
+        """Find principals matching given query"""
+
+
+class IDirectoryPlugin(IDirectoryInfo, IPlugin):
+    """Principal directory plug-in info"""
+
+
+class IDirectorySearchPlugin(IDirectoryPlugin, IContentSearch):
+    """Principal directory plug-in supporting search"""
+
+
+class IUsersFolderPlugin(IAuthenticationPlugin, IDirectorySearchPlugin):
+    """Local users folder interface"""
+
+    def check_login(self, login):
+        """Check for existence of given login"""
+
+
+#
+# User registration
+#
+
+def check_password(password):
+    """Check validity of a given password"""
+    nbmaj = 0
+    nbmin = 0
+    nbn = 0
+    nbo = 0
+    for car in password:
+        if ord(car) in range(ord('A'), ord('Z') + 1):
+            nbmaj += 1
+        elif ord(car) in range(ord('a'), ord('z') + 1):
+            nbmin += 1
+        elif ord(car) in range(ord('0'), ord('9') + 1):
+            nbn += 1
+        else:
+            nbo += 1
+    if [nbmin, nbmaj, nbn, nbo].count(0) > 1:
+        raise Invalid(_("Your password must contain at least three of these kinds of characters: "
+                        "lowercase letters, uppercase letters, numbers and special characters"))
+
+
+EMAIL_REGEX = re.compile("[^@]+@[^@]+\.[^@]+")
+
+
+class IUserRegistrationInfo(Interface):
+    """User registration info"""
+
+    login = TextLine(title=_("User login"),
+                     description=_("If you don't provide a custom login, your login will be your email address..."),
+                     required=False)
+
+    @invariant
+    def check_login(self):
+        if not self.login:
+            self.login = self.email
+
+    email = TextLine(title=_("E-mail address"),
+                     description=_("An email will be sent to this address to validate account activation; "
+                                   "it will be used as your future user login"),
+                     required=True)
+
+    @invariant
+    def check_email(self):
+        if not EMAIL_REGEX.match(self.email):
+            raise Invalid(_("Your email address is not valid!"))
+
+    firstname = TextLine(title=_("First name"),
+                         required=True)
+
+    lastname = TextLine(title=_("Last name"),
+                        required=True)
+
+    company_name = TextLine(title=_("Company name"),
+                            required=False)
+
+    password = EncodedPassword(title=_("Password"),
+                               description=_("Password must be at least 8 characters long, and contain at least "
+                                             "three kins of characters between lowercase letters, uppercase "
+                                             "letters, numbers and special characters"),
+                               min_length=8,
+                               required=True)
+
+    confirmed_password = EncodedPassword(title=_("Confirmed password"),
+                                         required=True)
+
+    @invariant
+    def check_password(self):
+        if self.password != self.confirmed_password:
+            raise Invalid(_("You didn't confirmed your password correctly!"))
+        check_password(self.password)
+
+
+class IUserRegistrationConfirmationInfo(Interface):
+    """User registration confirmation info"""
+
+    activation_hash = TextLine(title=_("Activation hash"),
+                               required=True)
+
+    login = TextLine(title=_("User login"),
+                     required=True)
+
+    password = EncodedPassword(title=_("Password"),
+                               min_length=8,
+                               required=True)
+
+    confirmed_password = EncodedPassword(title=_("Confirmed password"),
+                                         required=True)
+
+    @invariant
+    def check_password(self):
+        if self.password != self.confirmed_password:
+            raise Invalid(_("You didn't confirmed your password correctly!"))
+        check_password(self.password)
+
+
+class ILocalUser(IAttributeAnnotatable):
+    """Local user interface"""
+
+    login = TextLine(title=_("User login"),
+                     required=True,
+                     readonly=True)
+
+    @invariant
+    def check_login(self):
+        if not self.login:
+            self.login = self.email
+
+    email = TextLine(title=_("User email address"),
+                     required=True)
+
+    @invariant
+    def check_email(self):
+        if not EMAIL_REGEX.match(self.email):
+            raise Invalid(_("Given email address is not valid!"))
+
+    firstname = TextLine(title=_("First name"),
+                         required=True)
+
+    lastname = TextLine(title=_("Last name"),
+                        required=True)
+
+    title = Attribute("User full name")
+
+    company_name = TextLine(title=_("Company name"),
+                            required=False)
+
+    password_manager = Choice(title=_("Password manager name"),
+                              required=True,
+                              vocabulary='PyAMS password managers',
+                              default='SSHA')
+
+    password = EncodedPassword(title=_("Password"),
+                               min_length=8,
+                               required=False)
+
+    wait_confirmation = Bool(title=_("Wait confirmation?"),
+                             description=_("If 'no', user will be activated immediately without waiting email "
+                                           "confirmation"),
+                             required=True,
+                             default=True)
+
+    self_registered = Bool(title=_("Self-registered profile?"),
+                           required=True,
+                           default=True,
+                           readonly=True)
+
+    activation_secret = TextLine(title=_("Activation secret key"),
+                                 description=_("This private secret is used to create and check activation hash"),
+                                 readonly=True)
+
+    activation_hash = TextLine(title=_("Activation hash"),
+                               description=_("This hash is provided into activation message URL. Activation hash "
+                                             "is missing for local users which were registered without waiting "
+                                             "their confirmation."),
+                               readonly=True)
+
+    activation_date = Datetime(title=_("Activation date"),
+                               required=False)
+
+    activated = Bool(title=_("Activation date"),
+                     required=True,
+                     default=False)
+
+    def check_password(self, password):
+        """Check user password against provided one"""
+
+    def generate_secret(self, login, password):
+        """Generate secret key of this profile"""
+
+    def check_activation(self, hash, login, password):
+        """Check activation for given settings"""
+
+
+#
+# Security manager
+#
+
+class ISecurityManager(IContainer, IDirectoryInfo):
+    """Authentication and principals management utility"""
+
+    contains(IPlugin)
+
+    enable_social_login = Bool(title=_("Enable social login?"),
+                               description=_("Enable login via social OAuth plug-ins"),
+                               required=True,
+                               default=False)
+
+    authomatic_secret = TextLine(title=_("Authomatic secret"),
+                                 description=_("This secret phrase is used to encrypt Authomatic cookie"),
+                                 default='this is not a secret',
+                                 required=True)
+
+    social_login_use_popup = Bool(title=_("Use social popup?"),
+                                  required=True,
+                                  default=False)
+
+    open_registration = Bool(title=_("Open registration?"),
+                             description=_("If 'Yes', any use will be able to create a new user account"),
+                             required=True,
+                             default=False)
+
+    users_folder = Choice(title=_("Users folder"),
+                          description=_("Name of users folder used to store registered principals"),
+                          required=False,
+                          vocabulary='PyAMS users folders')
+
+    @invariant
+    def check_users_folder(self):
+        if self.open_registration and not self.users_folder:
+            raise Invalid(_("You can't activate open registration without selecting a users folder"))
+
+    credentials_plugins_names = Tuple(title=_("Credentials plug-ins"),
+                                      description=_("These plug-ins can be used to extract request credentials"),
+                                      value_type=TextLine(),
+                                      readonly=True,
+                                      default=())
+
+    authentication_plugins_names = Tuple(title=_("Authentication plug-ins"),
+                                         description=_("The plug-ins can be used to check extracted credentials "
+                                                       "against a local or remote users database"),
+                                         value_type=TextLine(),
+                                         default=())
+
+    directory_plugins_names = Tuple(title=_("Directory plug-ins"),
+                                    description=_("The plug-in can be used to extract principals information"),
+                                    value_type=TextLine(),
+                                    default=())
+
+    def get_plugin(self, name):
+        """Get plug-in matching given name"""
+
+    def get_credentials_plugins(self):
+        """Extract list of credentials plug-ins"""
+
+    def order_credentials_plugins(self, names):
+        """Define credentials plug-ins order"""
+
+    def get_authentication_plugins(self):
+        """Extract list of authentication plug-ins"""
+
+    def order_authentication_plugins(self, names):
+        """Define authentication plug-ins order"""
+
+    def get_directory_plugins(self):
+        """Extract list of directory plug-ins"""
+
+    def order_directory_plugins(self, names):
+        """Define directory plug-ins order"""
+
+
+LOGIN_REFERER_KEY = 'pyams_security.login.referer'
+
+
+class ILoginView(Interface):
+    """Login view marker interface"""
+
+
+#
+# Social login configuration
+#
+
+class ISocialLoginConfiguration(Interface):
+    """Social login configuration interface"""
+
+    configuration = Dict(title=_("Social login configuration"),
+                         key_type=TextLine(title=_("Provider name")),
+                         value_type=Dict(title=_("Provider configuration")))
+
+
+class ISocialLoginProviderInfo(Interface):
+    """Social login provider info"""
+
+    provider_name = TextLine(title=_("Provider name"),
+                             required=True)
+
+    provider_id = Int(title=_("Provider ID"),
+                      description=_("This value should be unique between all providers"),
+                      required=True,
+                      min=0)
+
+    klass = GlobalObject(title=_("Provider class"),
+                         required=True)
+
+    consumer_key = TextLine(title=_("Provider consumer key"),
+                            required=True)
+
+    consumer_secret = TextLine(title=_("Provider secret"),
+                               required=True)
+
+    scope = List(title=_("Provider scope"),
+                 required=True,
+                 value_type=TextLine(),
+                 default=['email'])
Binary file src/pyams_security/locales/fr/LC_MESSAGES/pyams_security.mo has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/locales/fr/LC_MESSAGES/pyams_security.po	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,511 @@
+#
+# 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-19 00:29+0100\n"
+"PO-Revision-Date: 2015-02-18 22:19+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.8\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: src/pyams_security/principal.py:49
+msgid "Not logged in"
+msgstr "Non connecté"
+
+#: src/pyams_security/zmi/utility.py:73
+msgid "Security"
+msgstr "Sécurité"
+
+#: src/pyams_security/zmi/utility.py:82
+msgid "Authentication and users directory plug-ins"
+msgstr "Modules d'authentification et dossiers utilisateurs"
+
+#: src/pyams_security/zmi/utility.py:127
+msgid "Security manager"
+msgstr "Gestionnaire de sécurité"
+
+#: src/pyams_security/zmi/utility.py:128
+msgid "Security manager plug-ins"
+msgstr "Modules du gestionnaire de sécurité"
+
+#: src/pyams_security/zmi/utility.py:139
+msgid "Add..."
+msgstr "Ajouter..."
+
+#: src/pyams_security/zmi/utility.py:147
+msgid "Properties..."
+msgstr "Propriétés..."
+
+#: src/pyams_security/zmi/utility.py:160
+#: src/pyams_security/zmi/plugin/admin.py:61
+#: src/pyams_security/zmi/plugin/userfolder.py:74
+msgid "System security manager"
+msgstr "Gestionnaire de sécurité"
+
+#: src/pyams_security/zmi/utility.py:161
+msgid "Security manager properties"
+msgstr "Propriétés du gestionnaire de sécurité"
+
+#: src/pyams_security/zmi/plugin/admin.py:50
+msgid "Add admin authentication..."
+msgstr "Ajouter un compte système..."
+
+#: src/pyams_security/zmi/plugin/admin.py:62
+msgid "Add administration authentication plug-in"
+msgstr "Ajout d'un compte d'administration système"
+
+#: src/pyams_security/zmi/plugin/admin.py:95
+msgid "Edit administration authentication plug-in"
+msgstr "Modification d'un compte d'administration système"
+
+#: src/pyams_security/zmi/plugin/admin.py:117
+msgid "WARNING"
+msgstr "ATTENTION"
+
+#: src/pyams_security/zmi/plugin/admin.py:119
+msgid ""
+"Before disabling plug-in, please verify that you have other administration "
+"access!"
+msgstr ""
+"Avant de désactiver ce compte, veuillez vous assurer que vous disposer d'un "
+"autre compte d'administration correctement configuré !"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:63
+msgid "Add local users folder..."
+msgstr "Ajouter un dossier d'utilisateurs locaux..."
+
+#: src/pyams_security/zmi/plugin/userfolder.py:75
+msgid "Add local users folder plug-in"
+msgstr "Ajout d'un dossier d'utilisateurs locaux"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:108
+msgid "Edit local users folder plug-in properties"
+msgstr "Modification d'un dossier d'utilisateurs locaux"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:158
+msgid "Search users"
+msgstr "Rechercher des utilisateurs"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:167
+msgid "Search results"
+msgstr "Résultats de la recherche"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:180
+#: src/pyams_security/views/login.py:56
+msgid "Login"
+msgstr "Code utilisateur"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:194
+msgid "Name"
+msgstr "Nom"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:208
+#: src/pyams_security/interfaces/__init__.py:244
+msgid "E-mail address"
+msgstr "Adresse de messagerie"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:222
+msgid "Registration date"
+msgstr "Date d'enregistrement"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:242
+#: src/pyams_security/interfaces/__init__.py:364
+#: src/pyams_security/interfaces/__init__.py:367
+msgid "Activation date"
+msgstr "Date d'activation"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:262
+msgid "Add user"
+msgstr "Ajouter un utilisateur"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:275
+msgid "Add new local user"
+msgstr "Ajout d'un utilisateur local"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:346
+msgid "Edit user properties"
+msgstr "Modification des propriétés d'un utilisateur"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:324
+#: src/pyams_security/views/userfolder.py:121
+msgid "Specified login can't be used!"
+msgstr "Le code utilisateur indiqué ne peut pas être utilisé !"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:335
+msgid "User was created successfully"
+msgstr "L'utilisateur a été créé avec succès."
+
+#: src/pyams_security/interfaces/__init__.py:130
+msgid "Plug-in prefix"
+msgstr "Préfixe du module"
+
+#: src/pyams_security/interfaces/__init__.py:131
+msgid ""
+"This prefix is mainly used by authentication plug-ins to mark principals"
+msgstr ""
+"Ce préfixe est utilisé par les modules d'authentification pour identifier "
+"les utilisateurs"
+
+#: src/pyams_security/interfaces/__init__.py:133
+msgid "Plug-in title"
+msgstr "Libellé du module"
+
+#: src/pyams_security/interfaces/__init__.py:136
+msgid "Enabled plug-in?"
+msgstr "Module actif ?"
+
+#: src/pyams_security/interfaces/__init__.py:137
+msgid "You can choose to disable any plug-in..."
+msgstr ""
+"Un module inactif ne peut plus être utilisé pour authentifier ou rechercher "
+"les utilisateurs..."
+
+#: src/pyams_security/interfaces/__init__.py:172
+msgid "Admin. login"
+msgstr "Code utilisateur"
+
+#: src/pyams_security/interfaces/__init__.py:174
+msgid "Admin. password"
+msgstr "Mot de passe"
+
+#: src/pyams_security/interfaces/__init__.py:225
+msgid ""
+"Your password must contain at least three of these kinds of characters: "
+"lowercase letters, uppercase letters, numbers and special characters"
+msgstr ""
+"Votre mot de passe doit contenir au moins trois de ces types de caractères : "
+"minuscules, majuscules, chiffres et autres caractères"
+
+#: src/pyams_security/interfaces/__init__.py:235
+#: src/pyams_security/interfaces/__init__.py:286
+#: src/pyams_security/interfaces/__init__.py:306
+msgid "User login"
+msgstr "Code utilisateur"
+
+#: src/pyams_security/interfaces/__init__.py:236
+msgid ""
+"If you don't provide a custom login, your login will be your email address..."
+msgstr ""
+"Si vous n'indiquez pas de code utilisateur, vous pourrez utiliser votre "
+"adresse de messagerie pour vous connecter..."
+
+#: src/pyams_security/interfaces/__init__.py:245
+msgid ""
+"An email will be sent to this address to validate account activation; it "
+"will be used as your future user login"
+msgstr ""
+"Un message sera envoyé à cette adresse pour la valider et pour vous "
+"permettre d'activer votre compte ; il pourra être utilisé compte identifiant "
+"de connexion si vous n'avez pas indiqué de code utilisateur"
+
+#: src/pyams_security/interfaces/__init__.py:254
+#: src/pyams_security/interfaces/__init__.py:323
+msgid "First name"
+msgstr "Prénom"
+
+#: src/pyams_security/interfaces/__init__.py:257
+#: src/pyams_security/interfaces/__init__.py:326
+msgid "Last name"
+msgstr "Nom"
+
+#: src/pyams_security/interfaces/__init__.py:260
+#: src/pyams_security/interfaces/__init__.py:331
+msgid "Company name"
+msgstr "Société"
+
+#: src/pyams_security/interfaces/__init__.py:263
+#: src/pyams_security/interfaces/__init__.py:289
+#: src/pyams_security/interfaces/__init__.py:339
+#: src/pyams_security/views/login.py:57
+msgid "Password"
+msgstr "Mot de passe"
+
+#: src/pyams_security/interfaces/__init__.py:264
+msgid ""
+"Password must be at least 8 characters long, and contain at least three kins "
+"of characters between lowercase letters, uppercase letters, numbers and "
+"special characters"
+msgstr ""
+"Le mot de passe doit être composé d'au moins huit caractères, et contenir au "
+"moins trois types de caractères parmi les lettres minuscules, les "
+"majuscules, les chiffres et les caractères spéciaux"
+
+#: src/pyams_security/interfaces/__init__.py:270
+#: src/pyams_security/interfaces/__init__.py:293
+msgid "Confirmed password"
+msgstr "Confirmation du mot de passe"
+
+#: src/pyams_security/interfaces/__init__.py:283
+#: src/pyams_security/interfaces/__init__.py:358
+msgid "Activation hash"
+msgstr "Clé d'activation"
+
+#: src/pyams_security/interfaces/__init__.py:315
+msgid "User email address"
+msgstr "Adresse de messagerie"
+
+#: src/pyams_security/interfaces/__init__.py:334
+msgid "Password manager name"
+msgstr "Gestionnaire de mot de passe"
+
+#: src/pyams_security/interfaces/__init__.py:343
+msgid "Wait confirmation?"
+msgstr "Attendre la confirmation ?"
+
+#: src/pyams_security/interfaces/__init__.py:344
+msgid ""
+"If 'no', user will be activated immediately without waiting email "
+"confirmation"
+msgstr ""
+"Si 'non', ce compte utilisateur sera activé immédiatement sans attendre le "
+"message de confirmation"
+
+#: src/pyams_security/interfaces/__init__.py:349
+msgid "Self-registered profile?"
+msgstr "Profil auto-enregistré ?"
+
+#: src/pyams_security/interfaces/__init__.py:354
+msgid "Activation secret key"
+msgstr "Clé secrète"
+
+#: src/pyams_security/interfaces/__init__.py:355
+msgid "This private secret is used to create and check activation hash"
+msgstr ""
+"Cette clé secrète est utilisé pour créer et vérifier la clé d'activation"
+
+#: src/pyams_security/interfaces/__init__.py:359
+msgid ""
+"This hash is provided into activation message URL. Activation hash is "
+"missing for local users which were registered without waiting their "
+"confirmation."
+msgstr ""
+"Cette clé est fournie dans le message de confirmation de l'inscription. "
+"Cette clé d'activation n'est pas définie pour les utilisateurs pour lesquels "
+"l'attente de confirmation n'a pas été demandée."
+
+#: src/pyams_security/interfaces/__init__.py:390
+msgid "Enable social login?"
+msgstr "Activer les réseaux sociaux ?"
+
+#: src/pyams_security/interfaces/__init__.py:391
+msgid "Enable login via social OAuth plug-ins"
+msgstr ""
+"Autoriser la connexion à partir des réseaux sociaux via les modules OAuth"
+
+#: src/pyams_security/interfaces/__init__.py:395
+msgid "Authomatic secret"
+msgstr "Clé OAuth"
+
+#: src/pyams_security/interfaces/__init__.py:396
+msgid "This secret phrase is used to encrypt Authomatic cookie"
+msgstr ""
+"Cette phrase secrète est utilisée pour crypter le cookie d'authentification"
+
+#: src/pyams_security/interfaces/__init__.py:400
+msgid "Use social popup?"
+msgstr "Connection via une popup ?"
+
+#: src/pyams_security/interfaces/__init__.py:404
+msgid "Open registration?"
+msgstr "Inscription ouverte ?"
+
+#: src/pyams_security/interfaces/__init__.py:405
+msgid "If 'Yes', any use will be able to create a new user account"
+msgstr "Si 'oui', toute personne sera à même de se créer un compte utilisateur"
+
+#: src/pyams_security/interfaces/__init__.py:409
+msgid "Users folder"
+msgstr "Dossier d'utilisateurs"
+
+#: src/pyams_security/interfaces/__init__.py:410
+msgid "Name of users folder used to store registered principals"
+msgstr "Nom du dossier dans lequel seront créés les utilisateurs enregistrés"
+
+#: src/pyams_security/interfaces/__init__.py:419
+msgid "Credentials plug-ins"
+msgstr "Modules d'identification"
+
+#: src/pyams_security/interfaces/__init__.py:420
+msgid "These plug-ins can be used to extract request credentials"
+msgstr ""
+"Ces modules peuvent être utilisés pour extraire l'identitité des utilisateurs"
+
+#: src/pyams_security/interfaces/__init__.py:425
+msgid "Authentication plug-ins"
+msgstr "Modules d'authentification"
+
+#: src/pyams_security/interfaces/__init__.py:426
+msgid ""
+"The plug-ins can be used to check extracted credentials against a local or "
+"remote users database"
+msgstr ""
+"Ces modules sont utilisés pour vérifier l'identité des utilisateurs vis à "
+"vis d'une base d'utilisateurs locale ou distante"
+
+#: src/pyams_security/interfaces/__init__.py:431
+msgid "Directory plug-ins"
+msgstr "Modules d'annuaires"
+
+#: src/pyams_security/interfaces/__init__.py:432
+msgid "The plug-in can be used to extract principals information"
+msgstr ""
+"Ces modules peuvent être utilisés pour extraire les propriétés des "
+"utilisateurs"
+
+#: src/pyams_security/interfaces/__init__.py:472
+msgid "Social login configuration"
+msgstr "Configuration d'un module social"
+
+#: src/pyams_security/interfaces/__init__.py:480
+#: src/pyams_security/interfaces/__init__.py:473
+msgid "Provider name"
+msgstr "Nom du fournisseur"
+
+#: src/pyams_security/interfaces/__init__.py:483
+msgid "Provider ID"
+msgstr "ID du fournisseur"
+
+#: src/pyams_security/interfaces/__init__.py:484
+msgid "This value should be unique between all providers"
+msgstr "Cette valeur numérique doit être unique pour tous les fournisseurs"
+
+#: src/pyams_security/interfaces/__init__.py:488
+msgid "Provider class"
+msgstr "Classe du fournisseur"
+
+#: src/pyams_security/interfaces/__init__.py:491
+msgid "Provider consumer key"
+msgstr "Clé cliente"
+
+#: src/pyams_security/interfaces/__init__.py:494
+msgid "Provider secret"
+msgstr "Clé secrète"
+
+#: src/pyams_security/interfaces/__init__.py:497
+msgid "Provider scope"
+msgstr "Portée du fournisseur"
+
+#: src/pyams_security/interfaces/__init__.py:252
+msgid "Your email address is not valid!"
+msgstr "Votre adresse de messagerie est incorrecte !"
+
+#: src/pyams_security/interfaces/__init__.py:276
+#: src/pyams_security/interfaces/__init__.py:299
+msgid "You didn't confirmed your password correctly!"
+msgstr "Vous n'avez pas confirmé votre mot de passe correctement !"
+
+#: src/pyams_security/interfaces/__init__.py:321
+msgid "Given email address is not valid!"
+msgstr "L'adrese de messagerie indiquée est invalide !"
+
+#: src/pyams_security/interfaces/__init__.py:417
+msgid "You can't activate open registration without selecting a users folder"
+msgstr ""
+"Vous ne pouvez pas activer les fonctions d'inscription sans sélectionner de "
+"dossier de stockage des utilisateurs"
+
+#: src/pyams_security/interfaces/__init__.py:474
+msgid "Provider configuration"
+msgstr "Configuration du fournisseur"
+
+#: src/pyams_security/views/login.py:90 src/pyams_security/views/login.py:149
+msgid "Please enter valid credentials to log-in"
+msgstr "Veuillez indiquer vos paramètres de connexion"
+
+#: src/pyams_security/views/login.py:63
+msgid "Reset"
+msgstr "Annuler"
+
+#: src/pyams_security/views/login.py:64 src/pyams_security/views/login.py:141
+msgid "Connect"
+msgstr "Connexion"
+
+#: src/pyams_security/views/login.py:140
+#: src/pyams_security/views/userfolder.py:65
+msgid "Cancel"
+msgstr "Annuler"
+
+#: src/pyams_security/views/login.py:82
+msgid "Missing security manager utility. Please contact administrator!"
+msgstr ""
+"Pas de gestionnaire de sécurité. Veuillez contacter l'administrateur du "
+"site !"
+
+#: src/pyams_security/views/login.py:78
+msgid "Invalid credentials!"
+msgstr "Paramètres de connexion incorrects !"
+
+#: src/pyams_security/views/userfolder.py:53
+msgid "Register new account"
+msgstr "Créer un compte"
+
+#: src/pyams_security/views/userfolder.py:73
+msgid "User registration"
+msgstr "Inscription"
+
+#: src/pyams_security/views/userfolder.py:74
+msgid "Please enter registration info"
+msgstr "Veuillez indiquer les paramètres de votre compte"
+
+#: src/pyams_security/views/userfolder.py:155
+msgid "User registration confirmation"
+msgstr "Confirmation d'inscription"
+
+#: src/pyams_security/views/userfolder.py:156
+msgid "Please confirm your registration info"
+msgstr "Veuillez confirmer les paramètres de votre compte"
+
+#: src/pyams_security/views/userfolder.py:66
+msgid "Register"
+msgstr "Inscrire ce compte"
+
+#: src/pyams_security/views/userfolder.py:147
+msgid "Finalize registration"
+msgstr "Terminer mon inscription"
+
+#: src/pyams_security/views/userfolder.py:119
+msgid "Can't create user profile. Please contact system administrator."
+msgstr ""
+"Impossible de créer votre compte utilisateur. Veuillez contacter "
+"l'administrateur."
+
+#: src/pyams_security/views/userfolder.py:193
+msgid "Can't check user profile. Please contact system administrator."
+msgstr ""
+"Impossible de vérifier votre compte utilisateur. Veuillez contacter "
+"l'administrateur."
+
+#: src/pyams_security/views/userfolder.py:132
+msgid "Your registration is recorded!"
+msgstr "Votre inscription est validée !"
+
+#: src/pyams_security/views/userfolder.py:134
+msgid ""
+"Your registration is recorded. You should receive a confirmation email soon "
+"which will allow you to confirm your inscription."
+msgstr ""
+"Votre demande d'inscription est enregistrée. Vous devriez recevoir un "
+"message de confirmation prochaînement qui vous permettra de confirmer cette "
+"demande."
+
+#: src/pyams_security/views/userfolder.py:198
+msgid "Can't retrieve user profile!"
+msgstr "Impossible d'accéder à votre compte utilisateur !"
+
+#: src/pyams_security/plugin/http.py:44
+msgid "HTTP Basic credentials"
+msgstr "Authentification HTTP Basic"
+
+#: src/pyams_security/plugin/userfolder.py:106
+#: src/pyams_security/plugin/userfolder.py:111
+msgid "Can't activate profile with given params!"
+msgstr ""
+"Impossible de confirmer votre inscription avec les paramètres indiqués !"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/locales/fr/LC_MESSAGES/pyams_security.po~	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,511 @@
+#
+# 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-18 23:59+0100\n"
+"PO-Revision-Date: 2015-02-18 22:19+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.8\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: src/pyams_security/principal.py:49
+msgid "Not logged in"
+msgstr "Non connecté"
+
+#: src/pyams_security/zmi/utility.py:73
+msgid "Security"
+msgstr "Sécurité"
+
+#: src/pyams_security/zmi/utility.py:82
+msgid "Authentication and users directory plug-ins"
+msgstr "Modules d'authentification et dossiers utilisateurs"
+
+#: src/pyams_security/zmi/utility.py:127
+msgid "Security manager"
+msgstr "Gestionnaire de sécurité"
+
+#: src/pyams_security/zmi/utility.py:128
+msgid "Security manager plug-ins"
+msgstr "Modules du gestionnaire de sécurité"
+
+#: src/pyams_security/zmi/utility.py:139
+msgid "Add..."
+msgstr "Ajouter..."
+
+#: src/pyams_security/zmi/utility.py:147
+msgid "Properties..."
+msgstr "Propriétés..."
+
+#: src/pyams_security/zmi/utility.py:160
+#: src/pyams_security/zmi/plugin/admin.py:61
+#: src/pyams_security/zmi/plugin/userfolder.py:74
+msgid "System security manager"
+msgstr "Gestionnaire de sécurité"
+
+#: src/pyams_security/zmi/utility.py:161
+msgid "Security manager properties"
+msgstr "Propriétés du gestionnaire de sécurité"
+
+#: src/pyams_security/zmi/plugin/admin.py:50
+msgid "Add admin authentication..."
+msgstr "Ajouter un compte système..."
+
+#: src/pyams_security/zmi/plugin/admin.py:62
+msgid "Add administration authentication plug-in"
+msgstr "Ajout d'un compte d'administration système"
+
+#: src/pyams_security/zmi/plugin/admin.py:95
+msgid "Edit administration authentication plug-in"
+msgstr "Modification d'un compte d'administration système"
+
+#: src/pyams_security/zmi/plugin/admin.py:117
+msgid "WARNING"
+msgstr "ATTENTION"
+
+#: src/pyams_security/zmi/plugin/admin.py:119
+msgid ""
+"Before disabling plug-in, please verify that you have other administration "
+"access!"
+msgstr ""
+"Avant de désactiver ce compte, veuillez vous assurer que vous disposer d'un "
+"autre compte d'administration correctement configuré !"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:63
+msgid "Add local users folder..."
+msgstr "Ajouter un dossier d'utilisateurs locaux..."
+
+#: src/pyams_security/zmi/plugin/userfolder.py:75
+msgid "Add local users folder plug-in"
+msgstr "Ajout d'un dossier d'utilisateurs locaux"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:108
+msgid "Edit local users folder plug-in properties"
+msgstr "Modification d'un dossier d'utilisateurs locaux"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:158
+msgid "Search users"
+msgstr "Rechercher des utilisateurs"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:167
+msgid "Search results"
+msgstr "Résultats de la recherche"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:180
+#: src/pyams_security/views/login.py:56 src/pyams_security/views/login.py:141
+msgid "Login"
+msgstr "Code utilisateur"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:190
+msgid "Name"
+msgstr "Nom"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:200
+#: src/pyams_security/interfaces/__init__.py:244
+msgid "E-mail address"
+msgstr "Adresse de messagerie"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:210
+msgid "Registration date"
+msgstr "Date d'enregistrement"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:226
+#: src/pyams_security/interfaces/__init__.py:364
+#: src/pyams_security/interfaces/__init__.py:367
+msgid "Activation date"
+msgstr "Date d'activation"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:242
+msgid "Add user"
+msgstr "Ajouter un utilisateur"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:255
+msgid "Add new local user"
+msgstr "Ajout d'un utilisateur local"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:326
+msgid "Edit user properties"
+msgstr "Modification des propriétés d'un utilisateur"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:304
+#: src/pyams_security/views/userfolder.py:121
+msgid "Specified login can't be used!"
+msgstr "Le code utilisateur indiqué ne peut pas être utilisé !"
+
+#: src/pyams_security/zmi/plugin/userfolder.py:315
+msgid "User was created successfully"
+msgstr "L'utilisateur a été créé avec succès."
+
+#: src/pyams_security/interfaces/__init__.py:130
+msgid "Plug-in prefix"
+msgstr "Préfixe du module"
+
+#: src/pyams_security/interfaces/__init__.py:131
+msgid ""
+"This prefix is mainly used by authentication plug-ins to mark principals"
+msgstr ""
+"Ce préfixe est utilisé par les modules d'authentification pour identifier "
+"les utilisateurs"
+
+#: src/pyams_security/interfaces/__init__.py:133
+msgid "Plug-in title"
+msgstr "Libellé du module"
+
+#: src/pyams_security/interfaces/__init__.py:136
+msgid "Enabled plug-in?"
+msgstr "Module actif ?"
+
+#: src/pyams_security/interfaces/__init__.py:137
+msgid "You can choose to disable any plug-in..."
+msgstr ""
+"Un module inactif ne peut plus être utilisé pour authentifier ou rechercher "
+"les utilisateurs..."
+
+#: src/pyams_security/interfaces/__init__.py:172
+msgid "Admin. login"
+msgstr "Code utilisateur"
+
+#: src/pyams_security/interfaces/__init__.py:174
+msgid "Admin. password"
+msgstr "Mot de passe"
+
+#: src/pyams_security/interfaces/__init__.py:225
+msgid ""
+"Your password must contain at least three of these kinds of characters: "
+"lowercase letters, uppercase letters, numbers and special characters"
+msgstr ""
+"Votre mot de passe doit contenir au moins trois de ces types de caractères : "
+"minuscules, majuscules, chiffres et autres caractères"
+
+#: src/pyams_security/interfaces/__init__.py:235
+#: src/pyams_security/interfaces/__init__.py:286
+#: src/pyams_security/interfaces/__init__.py:306
+msgid "User login"
+msgstr "Code utilisateur"
+
+#: src/pyams_security/interfaces/__init__.py:236
+msgid ""
+"If you don't provide a custom login, your login will be your email address..."
+msgstr ""
+"Si vous n'indiquez pas de code utilisateur, vous pourrez utiliser votre "
+"adresse de messagerie pour vous connecter..."
+
+#: src/pyams_security/interfaces/__init__.py:245
+msgid ""
+"An email will be sent to this address to validate account activation; it "
+"will be used as your future user login"
+msgstr ""
+"Un message sera envoyé à cette adresse pour la valider et pour vous "
+"permettre d'activer votre compte ; il pourra être utilisé compte identifiant "
+"de connexion si vous n'avez pas indiqué de code utilisateur"
+
+#: src/pyams_security/interfaces/__init__.py:254
+#: src/pyams_security/interfaces/__init__.py:323
+msgid "First name"
+msgstr "Prénom"
+
+#: src/pyams_security/interfaces/__init__.py:257
+#: src/pyams_security/interfaces/__init__.py:326
+msgid "Last name"
+msgstr "Nom"
+
+#: src/pyams_security/interfaces/__init__.py:260
+#: src/pyams_security/interfaces/__init__.py:331
+msgid "Company name"
+msgstr "Société"
+
+#: src/pyams_security/interfaces/__init__.py:263
+#: src/pyams_security/interfaces/__init__.py:289
+#: src/pyams_security/interfaces/__init__.py:339
+#: src/pyams_security/views/login.py:57
+msgid "Password"
+msgstr "Mot de passe"
+
+#: src/pyams_security/interfaces/__init__.py:264
+msgid ""
+"Password must be at least 8 characters long, and contain at least three kins "
+"of characters between lowercase letters, uppercase letters, numbers and "
+"special characters"
+msgstr ""
+"Le mot de passe doit être composé d'au moins huit caractères, et contenir au "
+"moins trois types de caractères parmi les lettres minuscules, les "
+"majuscules, les chiffres et les caractères spéciaux"
+
+#: src/pyams_security/interfaces/__init__.py:270
+#: src/pyams_security/interfaces/__init__.py:293
+msgid "Confirmed password"
+msgstr "Confirmation du mot de passe"
+
+#: src/pyams_security/interfaces/__init__.py:283
+#: src/pyams_security/interfaces/__init__.py:358
+msgid "Activation hash"
+msgstr "Clé d'activation"
+
+#: src/pyams_security/interfaces/__init__.py:315
+msgid "User email address"
+msgstr "Adresse de messagerie"
+
+#: src/pyams_security/interfaces/__init__.py:334
+msgid "Password manager name"
+msgstr "Gestionnaire de mot de passe"
+
+#: src/pyams_security/interfaces/__init__.py:343
+msgid "Wait confirmation?"
+msgstr "Attendre la confirmation ?"
+
+#: src/pyams_security/interfaces/__init__.py:344
+msgid ""
+"If 'no', user will be activated immediately without waiting email "
+"confirmation"
+msgstr ""
+"Si 'non', ce compte utilisateur sera activé immédiatement sans attendre le "
+"message de confirmation"
+
+#: src/pyams_security/interfaces/__init__.py:349
+msgid "Self-registered profile?"
+msgstr "Profil auto-enregistré ?"
+
+#: src/pyams_security/interfaces/__init__.py:354
+msgid "Activation secret key"
+msgstr "Clé secrète"
+
+#: src/pyams_security/interfaces/__init__.py:355
+msgid "This private secret is used to create and check activation hash"
+msgstr ""
+"Cette clé secrète est utilisé pour créer et vérifier la clé d'activation"
+
+#: src/pyams_security/interfaces/__init__.py:359
+msgid ""
+"This hash is provided into activation message URL. Activation hash is "
+"missing for local users which were registered without waiting their "
+"confirmation."
+msgstr ""
+"Cette clé est fournie dans le message de confirmation de l'inscription. "
+"Cette clé d'activation n'est pas définie pour les utilisateurs pour lesquels "
+"l'attente de confirmation n'a pas été demandée."
+
+#: src/pyams_security/interfaces/__init__.py:390
+msgid "Enable social login?"
+msgstr "Activer les réseaux sociaux ?"
+
+#: src/pyams_security/interfaces/__init__.py:391
+msgid "Enable login via social OAuth plug-ins"
+msgstr ""
+"Autoriser la connexion à partir des réseaux sociaux via les modules OAuth"
+
+#: src/pyams_security/interfaces/__init__.py:395
+msgid "Authomatic secret"
+msgstr "Clé OAuth"
+
+#: src/pyams_security/interfaces/__init__.py:396
+msgid "This secret phrase is used to encrypt Authomatic cookie"
+msgstr ""
+"Cette phrase secrète est utilisée pour crypter le cookie d'authentification"
+
+#: src/pyams_security/interfaces/__init__.py:400
+msgid "Use social popup?"
+msgstr "Connection via une popup ?"
+
+#: src/pyams_security/interfaces/__init__.py:404
+msgid "Open registration?"
+msgstr "Inscription ouverte ?"
+
+#: src/pyams_security/interfaces/__init__.py:405
+msgid "If 'Yes', any use will be able to create a new user account"
+msgstr "Si 'oui', toute personne sera à même de se créer un compte utilisateur"
+
+#: src/pyams_security/interfaces/__init__.py:409
+msgid "Users folder"
+msgstr "Dossier d'utilisateurs"
+
+#: src/pyams_security/interfaces/__init__.py:410
+msgid "Name of users folder used to store registered principals"
+msgstr "Nom du dossier dans lequel seront créés les utilisateurs enregistrés"
+
+#: src/pyams_security/interfaces/__init__.py:419
+msgid "Credentials plug-ins"
+msgstr "Modules d'identification"
+
+#: src/pyams_security/interfaces/__init__.py:420
+msgid "These plug-ins can be used to extract request credentials"
+msgstr ""
+"Ces modules peuvent être utilisés pour extraire l'identitité des utilisateurs"
+
+#: src/pyams_security/interfaces/__init__.py:425
+msgid "Authentication plug-ins"
+msgstr "Modules d'authentification"
+
+#: src/pyams_security/interfaces/__init__.py:426
+msgid ""
+"The plug-ins can be used to check extracted credentials against a local or "
+"remote users database"
+msgstr ""
+"Ces modules sont utilisés pour vérifier l'identité des utilisateurs vis à "
+"vis d'une base d'utilisateurs locale ou distante"
+
+#: src/pyams_security/interfaces/__init__.py:431
+msgid "Directory plug-ins"
+msgstr "Modules d'annuaires"
+
+#: src/pyams_security/interfaces/__init__.py:432
+msgid "The plug-in can be used to extract principals information"
+msgstr ""
+"Ces modules peuvent être utilisés pour extraire les propriétés des "
+"utilisateurs"
+
+#: src/pyams_security/interfaces/__init__.py:472
+msgid "Social login configuration"
+msgstr "Configuration d'un module social"
+
+#: src/pyams_security/interfaces/__init__.py:480
+#: src/pyams_security/interfaces/__init__.py:473
+msgid "Provider name"
+msgstr "Nom du fournisseur"
+
+#: src/pyams_security/interfaces/__init__.py:483
+msgid "Provider ID"
+msgstr "ID du fournisseur"
+
+#: src/pyams_security/interfaces/__init__.py:484
+msgid "This value should be unique between all providers"
+msgstr "Cette valeur numérique doit être unique pour tous les fournisseurs"
+
+#: src/pyams_security/interfaces/__init__.py:488
+msgid "Provider class"
+msgstr "Classe du fournisseur"
+
+#: src/pyams_security/interfaces/__init__.py:491
+msgid "Provider consumer key"
+msgstr "Clé cliente"
+
+#: src/pyams_security/interfaces/__init__.py:494
+msgid "Provider secret"
+msgstr "Clé secrète"
+
+#: src/pyams_security/interfaces/__init__.py:497
+msgid "Provider scope"
+msgstr "Portée du fournisseur"
+
+#: src/pyams_security/interfaces/__init__.py:252
+msgid "Your email address is not valid!"
+msgstr "Votre adresse de messagerie est incorrecte !"
+
+#: src/pyams_security/interfaces/__init__.py:276
+#: src/pyams_security/interfaces/__init__.py:299
+msgid "You didn't confirmed your password correctly!"
+msgstr "Vous n'avez pas confirmé votre mot de passe correctement !"
+
+#: src/pyams_security/interfaces/__init__.py:321
+msgid "Given email address is not valid!"
+msgstr "L'adrese de messagerie indiquée est invalide !"
+
+#: src/pyams_security/interfaces/__init__.py:417
+msgid "You can't activate open registration without selecting a users folder"
+msgstr ""
+"Vous ne pouvez pas activer les fonctions d'inscription sans sélectionner de "
+"dossier de stockage des utilisateurs"
+
+#: src/pyams_security/interfaces/__init__.py:474
+msgid "Provider configuration"
+msgstr "Configuration du fournisseur"
+
+#: src/pyams_security/views/login.py:90 src/pyams_security/views/login.py:149
+msgid "Please enter valid credentials to log-in"
+msgstr "Veuillez indiquer vos paramètres de connexion"
+
+#: src/pyams_security/views/login.py:63
+msgid "Reset"
+msgstr "Annuler"
+
+#: src/pyams_security/views/login.py:64
+msgid "Connect"
+msgstr "Connexion"
+
+#: src/pyams_security/views/login.py:140
+#: src/pyams_security/views/userfolder.py:65
+msgid "Cancel"
+msgstr "Annuler"
+
+#: src/pyams_security/views/login.py:82
+msgid "Missing security manager utility. Please contact administrator!"
+msgstr ""
+"Pas de gestionnaire de sécurité. Veuillez contacter l'administrateur du "
+"site !"
+
+#: src/pyams_security/views/login.py:78
+msgid "Invalid credentials!"
+msgstr "Paramètres de connexion incorrects !"
+
+#: src/pyams_security/views/userfolder.py:53
+msgid "Register new account"
+msgstr "Créer un compte"
+
+#: src/pyams_security/views/userfolder.py:73
+msgid "User registration"
+msgstr "Inscription"
+
+#: src/pyams_security/views/userfolder.py:74
+msgid "Please enter registration info"
+msgstr "Veuillez indiquer les paramètres de votre compte"
+
+#: src/pyams_security/views/userfolder.py:155
+msgid "User registration confirmation"
+msgstr "Confirmation d'inscription"
+
+#: src/pyams_security/views/userfolder.py:156
+msgid "Please confirm your registration info"
+msgstr "Veuillez confirmer les paramètres de votre compte"
+
+#: src/pyams_security/views/userfolder.py:66
+msgid "Register"
+msgstr "Inscrire ce compte"
+
+#: src/pyams_security/views/userfolder.py:147
+msgid "Finalize registration"
+msgstr "Terminer mon inscription"
+
+#: src/pyams_security/views/userfolder.py:119
+msgid "Can't create user profile. Please contact system administrator."
+msgstr ""
+"Impossible de créer votre compte utilisateur. Veuillez contacter "
+"l'administrateur."
+
+#: src/pyams_security/views/userfolder.py:193
+msgid "Can't check user profile. Please contact system administrator."
+msgstr ""
+"Impossible de vérifier votre compte utilisateur. Veuillez contacter "
+"l'administrateur."
+
+#: src/pyams_security/views/userfolder.py:132
+msgid "Your registration is recorded!"
+msgstr "Votre inscription est validée !"
+
+#: src/pyams_security/views/userfolder.py:134
+msgid ""
+"Your registration is recorded. You should receive a confirmation email soon "
+"which will allow you to confirm your inscription."
+msgstr ""
+"Votre demande d'inscription est enregistrée. Vous devriez recevoir un "
+"message de confirmation prochaînement qui vous permettra de confirmer cette "
+"demande."
+
+#: src/pyams_security/views/userfolder.py:198
+msgid "Can't retrieve user profile!"
+msgstr "Impossible d'accéder à votre compte utilisateur !"
+
+#: src/pyams_security/plugin/http.py:44
+msgid "HTTP Basic credentials"
+msgstr "Authentification HTTP Basic"
+
+#: src/pyams_security/plugin/userfolder.py:106
+#: src/pyams_security/plugin/userfolder.py:111
+msgid "Can't activate profile with given params!"
+msgstr ""
+"Impossible de confirmer votre inscription avec les paramètres indiqués !"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/locales/pyams_security.pot	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,471 @@
+# 
+# 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-19 00:29+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.8\n"
+
+#: ./src/pyams_security/principal.py:49
+msgid "Not logged in"
+msgstr ""
+
+#: ./src/pyams_security/zmi/utility.py:73
+msgid "Security"
+msgstr ""
+
+#: ./src/pyams_security/zmi/utility.py:82
+msgid "Authentication and users directory plug-ins"
+msgstr ""
+
+#: ./src/pyams_security/zmi/utility.py:127
+msgid "Security manager"
+msgstr ""
+
+#: ./src/pyams_security/zmi/utility.py:128
+msgid "Security manager plug-ins"
+msgstr ""
+
+#: ./src/pyams_security/zmi/utility.py:139
+msgid "Add..."
+msgstr ""
+
+#: ./src/pyams_security/zmi/utility.py:147
+msgid "Properties..."
+msgstr ""
+
+#: ./src/pyams_security/zmi/utility.py:160
+#: ./src/pyams_security/zmi/plugin/admin.py:61
+#: ./src/pyams_security/zmi/plugin/userfolder.py:74
+msgid "System security manager"
+msgstr ""
+
+#: ./src/pyams_security/zmi/utility.py:161
+msgid "Security manager properties"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/admin.py:50
+msgid "Add admin authentication..."
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/admin.py:62
+msgid "Add administration authentication plug-in"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/admin.py:95
+msgid "Edit administration authentication plug-in"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/admin.py:117
+msgid "WARNING"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/admin.py:119
+msgid ""
+"Before disabling plug-in, please verify that you have other administration "
+"access!"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:63
+msgid "Add local users folder..."
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:75
+msgid "Add local users folder plug-in"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:108
+msgid "Edit local users folder plug-in properties"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:158
+msgid "Search users"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:167
+msgid "Search results"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:180
+#: ./src/pyams_security/views/login.py:56
+msgid "Login"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:194
+msgid "Name"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:208
+#: ./src/pyams_security/interfaces/__init__.py:244
+msgid "E-mail address"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:222
+msgid "Registration date"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:242
+#: ./src/pyams_security/interfaces/__init__.py:364
+#: ./src/pyams_security/interfaces/__init__.py:367
+msgid "Activation date"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:262
+msgid "Add user"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:275
+msgid "Add new local user"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:346
+msgid "Edit user properties"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:324
+#: ./src/pyams_security/views/userfolder.py:121
+msgid "Specified login can't be used!"
+msgstr ""
+
+#: ./src/pyams_security/zmi/plugin/userfolder.py:335
+msgid "User was created successfully"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:130
+msgid "Plug-in prefix"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:131
+msgid ""
+"This prefix is mainly used by authentication plug-ins to mark principals"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:133
+msgid "Plug-in title"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:136
+msgid "Enabled plug-in?"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:137
+msgid "You can choose to disable any plug-in..."
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:172
+msgid "Admin. login"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:174
+msgid "Admin. password"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:225
+msgid ""
+"Your password must contain at least three of these kinds of characters: "
+"lowercase letters, uppercase letters, numbers and special characters"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:235
+#: ./src/pyams_security/interfaces/__init__.py:286
+#: ./src/pyams_security/interfaces/__init__.py:306
+msgid "User login"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:236
+msgid ""
+"If you don't provide a custom login, your login will be your email address..."
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:245
+msgid ""
+"An email will be sent to this address to validate account activation; it will"
+" be used as your future user login"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:254
+#: ./src/pyams_security/interfaces/__init__.py:323
+msgid "First name"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:257
+#: ./src/pyams_security/interfaces/__init__.py:326
+msgid "Last name"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:260
+#: ./src/pyams_security/interfaces/__init__.py:331
+msgid "Company name"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:263
+#: ./src/pyams_security/interfaces/__init__.py:289
+#: ./src/pyams_security/interfaces/__init__.py:339
+#: ./src/pyams_security/views/login.py:57
+msgid "Password"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:264
+msgid ""
+"Password must be at least 8 characters long, and contain at least three kins "
+"of characters between lowercase letters, uppercase letters, numbers and "
+"special characters"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:270
+#: ./src/pyams_security/interfaces/__init__.py:293
+msgid "Confirmed password"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:283
+#: ./src/pyams_security/interfaces/__init__.py:358
+msgid "Activation hash"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:315
+msgid "User email address"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:334
+msgid "Password manager name"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:343
+msgid "Wait confirmation?"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:344
+msgid ""
+"If 'no', user will be activated immediately without waiting email "
+"confirmation"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:349
+msgid "Self-registered profile?"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:354
+msgid "Activation secret key"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:355
+msgid "This private secret is used to create and check activation hash"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:359
+msgid ""
+"This hash is provided into activation message URL. Activation hash is missing"
+" for local users which were registered without waiting their confirmation."
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:390
+msgid "Enable social login?"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:391
+msgid "Enable login via social OAuth plug-ins"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:395
+msgid "Authomatic secret"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:396
+msgid "This secret phrase is used to encrypt Authomatic cookie"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:400
+msgid "Use social popup?"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:404
+msgid "Open registration?"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:405
+msgid "If 'Yes', any use will be able to create a new user account"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:409
+msgid "Users folder"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:410
+msgid "Name of users folder used to store registered principals"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:419
+msgid "Credentials plug-ins"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:420
+msgid "These plug-ins can be used to extract request credentials"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:425
+msgid "Authentication plug-ins"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:426
+msgid ""
+"The plug-ins can be used to check extracted credentials against a local or "
+"remote users database"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:431
+msgid "Directory plug-ins"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:432
+msgid "The plug-in can be used to extract principals information"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:472
+msgid "Social login configuration"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:480
+#: ./src/pyams_security/interfaces/__init__.py:473
+msgid "Provider name"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:483
+msgid "Provider ID"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:484
+msgid "This value should be unique between all providers"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:488
+msgid "Provider class"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:491
+msgid "Provider consumer key"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:494
+msgid "Provider secret"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:497
+msgid "Provider scope"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:252
+msgid "Your email address is not valid!"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:276
+#: ./src/pyams_security/interfaces/__init__.py:299
+msgid "You didn't confirmed your password correctly!"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:321
+msgid "Given email address is not valid!"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:417
+msgid "You can't activate open registration without selecting a users folder"
+msgstr ""
+
+#: ./src/pyams_security/interfaces/__init__.py:474
+msgid "Provider configuration"
+msgstr ""
+
+#: ./src/pyams_security/views/login.py:90
+#: ./src/pyams_security/views/login.py:149
+msgid "Please enter valid credentials to log-in"
+msgstr ""
+
+#: ./src/pyams_security/views/login.py:63
+msgid "Reset"
+msgstr ""
+
+#: ./src/pyams_security/views/login.py:64
+#: ./src/pyams_security/views/login.py:141
+msgid "Connect"
+msgstr ""
+
+#: ./src/pyams_security/views/login.py:140
+#: ./src/pyams_security/views/userfolder.py:65
+msgid "Cancel"
+msgstr ""
+
+#: ./src/pyams_security/views/login.py:82
+msgid "Missing security manager utility. Please contact administrator!"
+msgstr ""
+
+#: ./src/pyams_security/views/login.py:78
+msgid "Invalid credentials!"
+msgstr ""
+
+#: ./src/pyams_security/views/userfolder.py:53
+msgid "Register new account"
+msgstr ""
+
+#: ./src/pyams_security/views/userfolder.py:73
+msgid "User registration"
+msgstr ""
+
+#: ./src/pyams_security/views/userfolder.py:74
+msgid "Please enter registration info"
+msgstr ""
+
+#: ./src/pyams_security/views/userfolder.py:155
+msgid "User registration confirmation"
+msgstr ""
+
+#: ./src/pyams_security/views/userfolder.py:156
+msgid "Please confirm your registration info"
+msgstr ""
+
+#: ./src/pyams_security/views/userfolder.py:66
+msgid "Register"
+msgstr ""
+
+#: ./src/pyams_security/views/userfolder.py:147
+msgid "Finalize registration"
+msgstr ""
+
+#: ./src/pyams_security/views/userfolder.py:119
+msgid "Can't create user profile. Please contact system administrator."
+msgstr ""
+
+#: ./src/pyams_security/views/userfolder.py:193
+msgid "Can't check user profile. Please contact system administrator."
+msgstr ""
+
+#: ./src/pyams_security/views/userfolder.py:132
+msgid "Your registration is recorded!"
+msgstr ""
+
+#: ./src/pyams_security/views/userfolder.py:134
+msgid ""
+"Your registration is recorded. You should receive a confirmation email soon "
+"which will allow you to confirm your inscription."
+msgstr ""
+
+#: ./src/pyams_security/views/userfolder.py:198
+msgid "Can't retrieve user profile!"
+msgstr ""
+
+#: ./src/pyams_security/plugin/http.py:44
+msgid "HTTP Basic credentials"
+msgstr ""
+
+#: ./src/pyams_security/plugin/userfolder.py:106
+#: ./src/pyams_security/plugin/userfolder.py:111
+msgid "Can't activate profile with given params!"
+msgstr ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/permission.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,50 @@
+#
+# 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
+from pyams_security.interfaces import IPermission
+
+# import packages
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IPermission)
+class Permission(object):
+    """Permission utility class"""
+
+    id = FieldProperty(IPermission['id'])
+    title = FieldProperty(IPermission['title'])
+    description = FieldProperty(IPermission['description'])
+
+    def __init__(self, values=None, **args):
+        if not isinstance(values, dict):
+            values = args
+        self.id = values.get('id')
+        self.title = values.get('title')
+        self.description = values.get('description')
+
+
+def register_permission(config, permission):
+    """Register a new permission
+
+    Permissions registry is not required.
+    But only registered permissions can be applied via default
+    ZMI features
+    """
+    if not IPermission.providedBy(permission):
+        permission = Permission(permission)
+    config.registry.registerUtility(permission, IPermission, name=permission.id)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/plugin/__init__.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,41 @@
+#
+# 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
+
+
+class PluginSelector(object):
+    """Plug-in based event selector
+
+    This selector can be used by subscriber to filter authentication
+    events based on the name of the plug-in which fired the event.
+    """
+
+    def __init__(self, name, config):
+        self.plugin_name = name
+
+    def text(self):
+        return 'plugin_selector = %s' % self.plugin_name
+
+    phash = text
+
+    def __call__(self, event):
+        if event.plugin == self.plugin_name:
+            return True
+        return False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/plugin/admin.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,85 @@
+#
+# 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
+from pyams_security.interfaces import IAdminAuthenticationPlugin, IDirectoryPlugin
+from zope.password.interfaces import IPasswordManager
+
+# import packages
+from persistent import Persistent
+from pyams_security.principal import PrincipalInfo
+from pyams_utils.registry import get_utility
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IAdminAuthenticationPlugin, IDirectoryPlugin)
+class AdminAuthenticationPlugin(Persistent, Contained):
+    """Hard-coded administrator authenticator plug-in
+
+    This plug-in should only be enabled in development mode!!!
+    """
+
+    prefix = FieldProperty(IAdminAuthenticationPlugin['prefix'])
+    title = FieldProperty(IAdminAuthenticationPlugin['title'])
+    enabled = FieldProperty(IAdminAuthenticationPlugin['enabled'])
+
+    login = FieldProperty(IAdminAuthenticationPlugin['login'])
+    _password = FieldProperty(IAdminAuthenticationPlugin['password'])
+
+    @property
+    def password(self):
+        return self._password
+
+    @password.setter
+    def password(self, value):
+        manager = get_utility(IPasswordManager, name='SSHA')
+        self._password = manager.encodePassword(value)
+
+    def authenticate(self, credentials, request):
+        if not self.enabled:
+            return None
+        attrs = credentials.attributes
+        login = attrs.get('login')
+        password = attrs.get('password')
+        manager = get_utility(IPasswordManager, name='SSHA')
+        if login == self.login and manager.checkPassword(self._password, password):
+            return "{0}:{1}".format(self.prefix, login)
+
+    def get_principal(self, principal_id):
+        if not self.enabled:
+            return None
+        prefix, login = principal_id.split(':', 1)
+        if (prefix == self.prefix) and (login == self.login):
+            return PrincipalInfo(id=principal_id,
+                                 title=self.title)
+
+    def get_all_principals(self, principal_id):
+        if not self.enabled:
+            return set()
+        if self.get_principal(principal_id) is not None:
+            return {principal_id}
+        return set()
+
+    def find_principals(self, query):
+        if not query:
+            return None
+        query = query.lower()
+        if (query == self.login or
+                query in self.title.lower()):
+            yield PrincipalInfo(id='{0}:{1}'.format(self.prefix, self.login),
+                                title=self.title)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/plugin/http.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,65 @@
+#
+# 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 base64
+
+# import interfaces
+from pyams_security.interfaces import ICredentialsPlugin
+
+# import packages
+from pyams_security.credential import Credentials
+from pyams_utils.registry import utility_config
+from pyams_utils.wsgi import wsgi_environ_cache
+
+from pyams_security import _
+
+
+ENVKEY_PARSED_CREDENTIALS = "pyams_security.http.basic.credentials"
+
+
+@utility_config(name='http', provides=ICredentialsPlugin)
+class HttpBasicCredentialsPlugin(object):
+    """HTTP basic credentials plug-in
+
+    This credential plug-in is mainly used by automation processes using
+    XML-RPC or JSON-RPC requests launched from batch scripts.
+
+    Copied from pyramid_httpauth package.
+    """
+
+    prefix = 'http'
+    title = _("HTTP Basic credentials")
+    enabled = True
+
+    @wsgi_environ_cache(ENVKEY_PARSED_CREDENTIALS)
+    def extract_credentials(self, request, **kwargs):
+        """Extract login/password credentials from given request"""
+        auth = request.headers.get('Authorization')
+        if not auth:
+            return None
+        try:
+            scheme, params = auth.split(' ', 1)
+            if scheme.lower() != 'basic':
+                return None
+            token_bytes = base64.b64decode(params.strip())
+            try:
+                token = token_bytes.decode('utf-8')
+            except UnicodeDecodeError:
+                token = token_bytes.decode('latin-1')
+            login, password = token.split(':', 1)
+            return Credentials(self.prefix, login, login=login, password=password)
+        except (ValueError, TypeError):
+            return None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/plugin/userfolder.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,216 @@
+#
+# 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 base64
+import hashlib
+import hmac
+import random
+import sys
+from datetime import datetime
+from os import urandom
+
+# import interfaces
+from pyams_security.interfaces import ISecurityManager, IUsersFolderPlugin, IAdminAuthenticationPlugin, ILocalUser, \
+    IPrincipalInfo
+from zope.lifecycleevent.interfaces import IObjectAddedEvent
+from zope.password.interfaces import IPasswordManager
+from zope.schema.interfaces import IVocabularyRegistry
+
+# import packages
+from persistent import Persistent
+from pyams_security.principal import PrincipalInfo
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import query_utility, get_utility
+from pyramid.events import subscriber
+from zope.container.contained import Contained
+from zope.container.folder import Folder
+from zope.interface import implementer, provider, Invalid
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
+
+from pyams_security import _
+
+
+@implementer(ILocalUser)
+class User(Persistent, Contained):
+    """Local user persistent class"""
+
+    login = FieldProperty(ILocalUser['login'])
+    email = FieldProperty(ILocalUser['email'])
+    firstname = FieldProperty(ILocalUser['firstname'])
+    lastname = FieldProperty(ILocalUser['lastname'])
+    company_name = FieldProperty(ILocalUser['company_name'])
+    _password = FieldProperty(ILocalUser['password'])
+    _password_salt = None
+    password_manager = FieldProperty(ILocalUser['password_manager'])
+    self_registered = FieldProperty(ILocalUser['self_registered'])
+    activation_secret = FieldProperty(ILocalUser['activation_secret'])
+    activation_hash = FieldProperty(ILocalUser['activation_hash'])
+    activation_date = FieldProperty(ILocalUser['activation_date'])
+    activated = FieldProperty(ILocalUser['activated'])
+
+    @property
+    def title(self):
+        return '{0} {1}'.format(self.firstname, self.lastname)
+
+    @property
+    def password(self):
+        return self._password
+
+    @password.setter
+    def password(self, value):
+        self._password_salt = urandom(4)
+        manager = get_utility(IPasswordManager, name=self.password_manager)
+        if self.password_manager == 'Plain Text':
+            self._password = manager.encodePassword(value)
+        else:
+            self._password = manager.encodePassword(value, salt=self._password_salt)
+
+    def check_password(self, password):
+        if not self.activated:
+            return False
+        manager = query_utility(IPasswordManager, name=self.password_manager)
+        if manager is None:
+            return False
+        return manager.checkPassword(self.password, password)
+
+    def generate_secret(self):
+        seed = self.activation_secret = '-'.join((hex(random.randint(0, sys.maxsize))[2:] for i in range(5)))
+        secret = hmac.new(self.password, self.login.encode(), digestmod=hashlib.sha256)
+        secret.update(seed.encode())
+        self.activation_hash = base64.b32encode(secret.digest()).decode()
+
+    def check_activation(self, hash, login, password):
+        if self.self_registered:
+            # If principal was registered by it's own, we check activation hash
+            # with given login and password
+            manager = get_utility(IPasswordManager, name=self.password_manager)
+            password = manager.encodePassword(password, salt=self._password_salt)
+            secret = hmac.new(password, login.encode(), digestmod=hashlib.sha256)
+            secret.update(self.activation_secret.encode())
+            activation_hash = base64.b32encode(secret.digest()).decode()
+            if hash != activation_hash:
+                raise Invalid(_("Can't activate profile with given params!"))
+        else:
+            # If principal was registered by a site manager, just check that
+            # hash is matching stored one and update user password...
+            if hash != self.activation_hash:
+                raise Invalid(_("Can't activate profile with given params!"))
+            self.password = password
+        self.activation_date = datetime.utcnow()
+        self.activated = True
+
+
+@adapter_config(context=ILocalUser, provides=IPrincipalInfo)
+def UserPrincipalInfoAdapter(user):
+    """User principal info adapter"""
+    return PrincipalInfo(id="{0}:{1}".format(user.__parent__.prefix, user.login),
+                         title=user.title)
+
+
+@implementer(IUsersFolderPlugin)
+class UsersFolder(Folder):
+    """Local users folder"""
+
+    prefix = FieldProperty(IAdminAuthenticationPlugin['prefix'])
+    title = FieldProperty(IAdminAuthenticationPlugin['title'])
+    enabled = FieldProperty(IAdminAuthenticationPlugin['enabled'])
+
+    def authenticate(self, credentials, request):
+        if not self.enabled:
+            return None
+        attrs = credentials.attributes
+        login = attrs.get('login')
+        principal = self.get(login)
+        if principal is not None:
+            password = attrs.get('password')
+            if principal.check_password(password):
+                return "{0}:{1}".format(self.prefix, principal.login)
+
+    def check_login(self, login):
+        if not login:
+            return False
+        return login not in self
+
+    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)
+        if prefix != self.prefix:
+            return None
+        user = self.get(login)
+        if user is not None:
+            return PrincipalInfo(id='{0}:{1}'.format(self.prefix, user.login),
+                                 title=user.title)
+
+    def get_all_principals(self, principal_id):
+        if not self.enabled:
+            return set()
+        if self.get_principal(principal_id) is not None:
+            return {principal_id}
+        return set()
+
+    def find_principals(self, query):
+        # TODO: use users catalog for more efficient search?
+        if not query:
+            return None
+        query = query.lower()
+        for user in self.values():
+            if (query == user.login or
+                    query in user.firstname.lower() or
+                    query in user.lastname.lower()):
+                yield PrincipalInfo(id='{0}:{1}'.format(self.prefix, user.login),
+                                    title=user.title)
+
+    def get_search_results(self, data):
+        # TODO: use users catalog for more efficient search?
+        query = data.get('query')
+        if not query:
+            return ()
+        query = query.lower()
+        for user in self.values():
+            if (query == user.login or
+                    query in user.firstname.lower() or
+                    query in user.lastname.lower()):
+                yield user
+
+
+@provider(IVocabularyRegistry)
+class UsersFolderVocabulary(SimpleVocabulary):
+    """'PyAMS users folders' vocabulary"""
+
+    def __init__(self, *args, **kwargs):
+        terms = []
+        manager = query_utility(ISecurityManager)
+        if manager is not None:
+            for name, plugin in manager.items():
+                if IUsersFolderPlugin.providedBy(plugin):
+                    terms.append(SimpleTerm(name, title=plugin.title))
+        super(UsersFolderVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS users folders', UsersFolderVocabulary)
+
+
+@subscriber(IObjectAddedEvent, context_selector=ILocalUser)
+def handle_new_local_user(event):
+    """Send a confirmation message when a new user is recorded"""
+    user = event.object
+    if user.self_registered:
+        pass
+    else:
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/principal.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,72 @@
+#
+# 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
+from pyams_security.interfaces import IPrincipalInfo
+
+# import packages
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+from pyams_security import _
+
+
+@implementer(IPrincipalInfo)
+class PrincipalInfo(object):
+    """Generic principal info"""
+
+    id = FieldProperty(IPrincipalInfo['id'])
+    title = FieldProperty(IPrincipalInfo['title'])
+    groups = FieldProperty(IPrincipalInfo['groups'])
+
+    def __init__(self, **kwargs):
+        self.id = kwargs.get('id')
+        self.title = kwargs.get('title')
+
+    def __eq__(self, other):
+        return isinstance(other, PrincipalInfo) and (self.id == other.id)
+
+
+@implementer(IPrincipalInfo)
+class UnknownPrincipal(object):
+    """Unknown principal info"""
+
+    id = None
+    title = _("Not logged in")
+
+UnknownPrincipal = UnknownPrincipal()
+
+
+@implementer(IPrincipalInfo)
+class MissingPrincipal(object):
+    """Missing principal info
+
+    This class can be used when a stored principal ID
+    references a principal which can't be found anymore
+    """
+
+    id = FieldProperty(IPrincipalInfo['id'])
+
+    def __init__(self, **kwargs):
+        self.id = kwargs.get('id')
+
+    @property
+    def title(self):
+        return '<MissingPrincipal: {0}>'.format(self.id)
+
+    def __eq__(self, other):
+        return isinstance(other, PrincipalInfo) and (self.id == other.id)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/resources/js/authomatic.js	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,524 @@
+// Generated by CoffeeScript 1.6.2
+/*
+# CoffeeDoc example documentation #
+
+This is a module-level docstring, and will be displayed at the top of the module documentation.
+Documentation generated by [CoffeeDoc](http://github.com/omarkhan/coffeedoc)
+
+npm install -g coffeedoc
+*/
+
+
+(function() {
+  var $, Authomatic, BaseProvider, Flickr, Foursquare, Google, LinkedIn, Oauth1Provider, Oauth2Provider, WindowsLive, deserializeCredentials, format, getProviderClass, globalOptions, jsonPCallbackCounter, log, openWindow, parseQueryString, parseUrl, _ref, _ref1, _ref2, _ref3, _ref4, _ref5,
+    __slice = [].slice,
+    __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+    __hasProp = {}.hasOwnProperty,
+    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
+
+  $ = jQuery;
+
+  jsonPCallbackCounter = 0;
+
+  globalOptions = {
+    logging: true,
+    popupWidth: 800,
+    popupHeight: 600,
+    popupLinkSelector: 'a.authomatic',
+    popupFormSelector: 'form.authomatic',
+    popupFormValidator: function($form) {
+      return true;
+    },
+    backend: null,
+    forceBackend: false,
+    substitute: {},
+    params: {},
+    headers: {},
+    body: '',
+    jsonpCallbackPrefix: 'authomaticJsonpCallback',
+    onPopupInvalid: null,
+    onPopupOpen: null,
+    onLoginComplete: null,
+    onBackendStart: null,
+    onBackendComplete: null,
+    onAccessSuccess: null,
+    onAccessComplete: null
+  };
+
+  log = function() {
+    var args, _ref;
+
+    args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+    if (globalOptions.logging && ((typeof console !== "undefined" && console !== null ? (_ref = console.log) != null ? _ref.apply : void 0 : void 0) != null)) {
+      return typeof console !== "undefined" && console !== null ? console.log.apply(console, ['Authomatic:'].concat(__slice.call(args))) : void 0;
+    }
+  };
+
+  openWindow = function(url) {
+    var height, left, settings, top, width;
+
+    width = globalOptions.popupWidth;
+    height = globalOptions.popupHeight;
+    top = (screen.height / 2) - (height / 2);
+    left = (screen.width / 2) - (width / 2);
+    settings = "width=" + width + ",height=" + height + ",top=" + top + ",left=" + left;
+    log('Opening popup:', url);
+    if (typeof globalOptions.onPopupOpen === "function") {
+      globalOptions.onPopupOpen(url);
+    }
+    return window.open(url, '', settings);
+  };
+
+  parseQueryString = function(queryString) {
+    var item, k, result, v, _i, _len, _ref, _ref1;
+
+    result = {};
+    _ref = queryString.split('&');
+    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+      item = _ref[_i];
+      _ref1 = item.split('='), k = _ref1[0], v = _ref1[1];
+      v = decodeURIComponent(v);
+      if (result.hasOwnProperty(k)) {
+        if (Array.isArray(result[k])) {
+          result[k].push(v);
+        } else {
+          result[k] = [result[k], v];
+        }
+      } else {
+        result[k] = v;
+      }
+    }
+    return result;
+  };
+
+  parseUrl = function(url) {
+    var qs, questionmarkIndex, u;
+
+    log('parseUrl', url);
+    questionmarkIndex = url.indexOf('?');
+    if (questionmarkIndex >= 0) {
+      u = url.substring(0, questionmarkIndex);
+      qs = url.substring(questionmarkIndex + 1);
+    } else {
+      u = url;
+    }
+    return {
+      url: u,
+      query: qs,
+      params: qs ? parseQueryString(qs) : void 0
+    };
+  };
+
+  deserializeCredentials = function(credentials) {
+    var sc, subtype, type, typeId, _ref;
+
+    sc = decodeURIComponent(credentials).split('\n');
+    typeId = sc[1];
+    _ref = typeId.split('-'), type = _ref[0], subtype = _ref[1];
+    return {
+      id: parseInt(sc[0]),
+      typeId: typeId,
+      type: parseInt(type),
+      subtype: parseInt(subtype),
+      rest: sc.slice(2)
+    };
+  };
+
+  getProviderClass = function(credentials) {
+    var subtype, type, _ref;
+
+    _ref = deserializeCredentials(credentials), type = _ref.type, subtype = _ref.subtype;
+    if (type === 1) {
+      if (subtype === 2) {
+        return Flickr;
+      } else {
+        return Oauth1Provider;
+      }
+    } else if (type === 2) {
+      if (subtype === 6) {
+        return Foursquare;
+      } else if (subtype === 9) {
+        return LinkedIn;
+      } else if (subtype === 14) {
+        return WindowsLive;
+      } else if (subtype === 12 || subtype === 15) {
+        return BaseProvider;
+      } else {
+        return Oauth2Provider;
+      }
+    } else {
+      return BaseProvider;
+    }
+  };
+
+  format = function(template, substitute) {
+    return template.replace(/{([^}]*)}/g, function(match, tag) {
+      var level, target, _i, _len, _ref;
+
+      target = substitute;
+      _ref = tag.split('.');
+      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+        level = _ref[_i];
+        target = target[level];
+      }
+      return target;
+    });
+  };
+
+  window.authomatic = new (Authomatic = (function() {
+    function Authomatic() {}
+
+    Authomatic.prototype.setup = function(options) {
+      $.extend(globalOptions, options);
+      return log('Setting up authomatic to:', globalOptions);
+    };
+
+    Authomatic.prototype.popupInit = function() {
+      $(globalOptions.popupLinkSelector).click(function(e) {
+        e.preventDefault();
+        return openWindow($(this).attr('href'));
+      });
+      return $(globalOptions.popupFormSelector).submit(function(e) {
+        var $form, url;
+
+        e.preventDefault();
+        $form = $(this);
+        url = $form.attr('action') + '?' + $form.serialize();
+        if (globalOptions.popupFormValidator($form)) {
+          return openWindow(url);
+        } else {
+          return typeof globalOptions.onPopupInvalid === "function" ? globalOptions.onPopupInvalid($form) : void 0;
+        }
+      });
+    };
+
+    Authomatic.prototype.loginComplete = function(result, closer) {
+      var result_copy;
+
+      result_copy = $.extend(true, {}, result);
+      log('Login procedure complete', result_copy);
+      closer();
+      return globalOptions.onLoginComplete(result_copy);
+    };
+
+    Authomatic.prototype.access = function(credentials, url, options) {
+      var Provider, localEvents, provider, updatedOptions;
+
+      if (options == null) {
+        options = {};
+      }
+      localEvents = {
+        onBackendStart: null,
+        onBackendComplete: null,
+        onAccessSuccess: null,
+        onAccessComplete: null
+      };
+      updatedOptions = {};
+      $.extend(updatedOptions, globalOptions, localEvents, options);
+      url = format(url, updatedOptions.substitute);
+      log('access options', updatedOptions, globalOptions);
+      if (updatedOptions.forceBackend) {
+        Provider = BaseProvider;
+      } else {
+        Provider = getProviderClass(credentials);
+      }
+      provider = new Provider(options.backend, credentials, url, updatedOptions);
+      log('Instantiating provider:', provider);
+      return provider.access();
+    };
+
+    return Authomatic;
+
+  })());
+
+  BaseProvider = (function() {
+    BaseProvider.prototype._x_jsonpCallbackParamName = 'callback';
+
+    function BaseProvider(backend, credentials, url, options) {
+      var parsedUrl;
+
+      this.backend = backend;
+      this.credentials = credentials;
+      this.options = options;
+      this.access = __bind(this.access, this);
+      this.contactProvider = __bind(this.contactProvider, this);
+      this.contactBackend = __bind(this.contactBackend, this);
+      this.backendRequestType = 'auto';
+      this.jsonpRequest = false;
+      this.jsonpCallbackName = "" + globalOptions.jsonpCallbackPrefix + jsonPCallbackCounter;
+      this.deserializedCredentials = deserializeCredentials(this.credentials);
+      this.providerID = this.deserializedCredentials.id;
+      this.providerType = this.deserializedCredentials.type;
+      this.credentialsRest = this.deserializedCredentials.rest;
+      parsedUrl = parseUrl(url);
+      this.url = parsedUrl.url;
+      this.params = {};
+      $.extend(this.params, parsedUrl.params, this.options.params);
+    }
+
+    BaseProvider.prototype.contactBackend = function(callback) {
+      var data, _base;
+
+      if (this.jsonpRequest && this.options.method === !'GET') {
+        this.backendRequestType = 'fetch';
+      }
+      data = {
+        type: this.backendRequestType,
+        credentials: this.credentials,
+        url: this.url,
+        method: this.options.method,
+        body: this.options.body,
+        params: JSON.stringify(this.params),
+        headers: JSON.stringify(this.options.headers)
+      };
+      if (typeof globalOptions.onBackendStart === "function") {
+        globalOptions.onBackendStart(data);
+      }
+      if (typeof (_base = this.options).onBackendStart === "function") {
+        _base.onBackendStart(data);
+      }
+      log("Contacting backend at " + this.options.backend + ".", data);
+      return $.get(this.options.backend, data, callback);
+    };
+
+    BaseProvider.prototype.contactProvider = function(requestElements) {
+      var body, headers, jsonpOptions, method, options, params, url,
+        _this = this;
+
+      url = requestElements.url, method = requestElements.method, params = requestElements.params, headers = requestElements.headers, body = requestElements.body;
+      options = {
+        type: method,
+        data: params,
+        headers: headers,
+        complete: [
+          (function(jqXHR, textStatus) {
+            return log('Request complete.', textStatus, jqXHR);
+          }), globalOptions.onAccessComplete, this.options.onAccessComplete
+        ],
+        success: [
+          (function(data) {
+            return log('Request successful.', data);
+          }), globalOptions.onAccessSuccess, this.options.onAccessSuccess
+        ],
+        error: function(jqXHR, textStatus, errorThrown) {
+          if (jqXHR.state() === 'rejected') {
+            if (_this.options.method === 'GET') {
+              log('Cross domain request failed! trying JSONP request.');
+              _this.jsonpRequest = true;
+            } else {
+              _this.backendRequestType = 'fetch';
+            }
+            return _this.access();
+          }
+        }
+      };
+      if (this.jsonpRequest) {
+        jsonpOptions = {
+          jsonpCallback: this.jsonpCallbackName,
+          jsonp: this._x_jsonpCallbackParamName,
+          cache: true,
+          dataType: 'jsonp',
+          error: function(jqXHR, textStatus, errorThrown) {
+            return log('JSONP failed! State:', jqXHR.state());
+          }
+        };
+        $.extend(options, jsonpOptions);
+        log("Contacting provider with JSONP request.", url, options);
+      } else {
+        log("Contacting provider with cross domain request", url, options);
+      }
+      return $.ajax(url, options);
+    };
+
+    BaseProvider.prototype.access = function() {
+      var callback,
+        _this = this;
+
+      callback = function(data, textStatus, jqXHR) {
+        var responseTo, _base, _base1, _base2;
+
+        if (typeof globalOptions.onBackendComplete === "function") {
+          globalOptions.onBackendComplete(data, textStatus, jqXHR);
+        }
+        if (typeof (_base = _this.options).onBackendComplete === "function") {
+          _base.onBackendComplete(data, textStatus, jqXHR);
+        }
+        responseTo = jqXHR != null ? jqXHR.getResponseHeader('Authomatic-Response-To') : void 0;
+        if (responseTo === 'fetch') {
+          log("Fetch data returned from backend.", jqXHR.getResponseHeader('content-type'), data);
+          if (typeof globalOptions.onAccessSuccess === "function") {
+            globalOptions.onAccessSuccess(data);
+          }
+          if (typeof (_base1 = _this.options).onAccessSuccess === "function") {
+            _base1.onAccessSuccess(data);
+          }
+          if (typeof globalOptions.onAccessComplete === "function") {
+            globalOptions.onAccessComplete(jqXHR, textStatus);
+          }
+          return typeof (_base2 = _this.options).onAccessComplete === "function" ? _base2.onAccessComplete(jqXHR, textStatus) : void 0;
+        } else if (responseTo === 'elements') {
+          log("Request elements data returned from backend.", data);
+          return _this.contactProvider(data);
+        }
+      };
+      if (this.jsonpRequest) {
+        jsonPCallbackCounter += 1;
+      }
+      return this.contactBackend(callback);
+    };
+
+    return BaseProvider;
+
+  })();
+
+  Oauth1Provider = (function(_super) {
+    __extends(Oauth1Provider, _super);
+
+    function Oauth1Provider() {
+      this.contactProvider = __bind(this.contactProvider, this);
+      this.access = __bind(this.access, this);      _ref = Oauth1Provider.__super__.constructor.apply(this, arguments);
+      return _ref;
+    }
+
+    Oauth1Provider.prototype.access = function() {
+      this.jsonpRequest = true;
+      this.params[this._x_jsonpCallbackParamName] = this.jsonpCallbackName;
+      return Oauth1Provider.__super__.access.call(this);
+    };
+
+    Oauth1Provider.prototype.contactProvider = function(requestElements) {
+      delete requestElements.params.callback;
+      return Oauth1Provider.__super__.contactProvider.call(this, requestElements);
+    };
+
+    return Oauth1Provider;
+
+  })(BaseProvider);
+
+  Flickr = (function(_super) {
+    __extends(Flickr, _super);
+
+    function Flickr() {
+      _ref1 = Flickr.__super__.constructor.apply(this, arguments);
+      return _ref1;
+    }
+
+    Flickr.prototype._x_jsonpCallbackParamName = 'jsoncallback';
+
+    return Flickr;
+
+  })(Oauth1Provider);
+
+  Oauth2Provider = (function(_super) {
+    __extends(Oauth2Provider, _super);
+
+    Oauth2Provider.prototype._x_accessToken = 'access_token';
+
+    Oauth2Provider.prototype._x_bearer = 'Bearer';
+
+    function Oauth2Provider() {
+      var args, _ref2;
+
+      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+      this.access = __bind(this.access, this);
+      this.handleTokenType = __bind(this.handleTokenType, this);
+      Oauth2Provider.__super__.constructor.apply(this, args);
+      _ref2 = this.credentialsRest, this.accessToken = _ref2[0], this.refreshToken = _ref2[1], this.expirationTime = _ref2[2], this.tokenType = _ref2[3];
+      this.handleTokenType();
+    }
+
+    Oauth2Provider.prototype.handleTokenType = function() {
+      if (this.tokenType === '1') {
+        return this.options.headers['Authorization'] = "" + this._x_bearer + " " + this.accessToken;
+      } else {
+        return this.params[this._x_accessToken] = this.accessToken;
+      }
+    };
+
+    Oauth2Provider.prototype.access = function() {
+      var requestElements;
+
+      if (this.backendRequestType === 'fetch') {
+        return Oauth2Provider.__super__.access.call(this);
+      } else {
+        requestElements = {
+          url: this.url,
+          method: this.options.method,
+          params: this.params,
+          headers: this.options.headers,
+          body: this.options.body
+        };
+        return this.contactProvider(requestElements);
+      }
+    };
+
+    return Oauth2Provider;
+
+  })(BaseProvider);
+
+  Foursquare = (function(_super) {
+    __extends(Foursquare, _super);
+
+    function Foursquare() {
+      _ref2 = Foursquare.__super__.constructor.apply(this, arguments);
+      return _ref2;
+    }
+
+    Foursquare.prototype._x_accessToken = 'oauth_token';
+
+    return Foursquare;
+
+  })(Oauth2Provider);
+
+  Google = (function(_super) {
+    __extends(Google, _super);
+
+    function Google() {
+      _ref3 = Google.__super__.constructor.apply(this, arguments);
+      return _ref3;
+    }
+
+    Google.prototype._x_bearer = 'OAuth';
+
+    return Google;
+
+  })(Oauth2Provider);
+
+  LinkedIn = (function(_super) {
+    __extends(LinkedIn, _super);
+
+    function LinkedIn() {
+      _ref4 = LinkedIn.__super__.constructor.apply(this, arguments);
+      return _ref4;
+    }
+
+    LinkedIn.prototype._x_accessToken = 'oauth2_access_token';
+
+    return LinkedIn;
+
+  })(Oauth2Provider);
+
+  WindowsLive = (function(_super) {
+    __extends(WindowsLive, _super);
+
+    function WindowsLive() {
+      this.handleTokenType = __bind(this.handleTokenType, this);      _ref5 = WindowsLive.__super__.constructor.apply(this, arguments);
+      return _ref5;
+    }
+
+    WindowsLive.prototype.handleTokenType = function() {
+      if (this.tokenType === '1') {
+        this.options.headers['Authorization'] = "" + this._x_bearer + " " + this.accessToken;
+      }
+      return this.params[this._x_accessToken] = this.accessToken;
+    };
+
+    return WindowsLive;
+
+  })(Oauth2Provider);
+
+}).call(this);
+
+/*
+//@ sourceMappingURL=authomatic.map
+*/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/resources/js/authomatic.min.js	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,1 @@
+(function(){var l,k,z,C,w,u,b,t,q,d,s,y,g,c,o,m,A,x,v,n,i,h,f,e,a,r=[].slice,j=function(D,E){return function(){return D.apply(E,arguments)}},p={}.hasOwnProperty,B=function(G,E){for(var D in E){if(p.call(E,D)){G[D]=E[D]}}function F(){this.constructor=G}F.prototype=E.prototype;G.prototype=new F();G.__super__=E.prototype;return G};l=jQuery;o=0;c={logging:true,popupWidth:800,popupHeight:600,popupLinkSelector:"a.authomatic",popupFormSelector:"form.authomatic",popupFormValidator:function(D){return true},backend:null,forceBackend:false,substitute:{},params:{},headers:{},body:"",jsonpCallbackPrefix:"authomaticJsonpCallback",onPopupInvalid:null,onPopupOpen:null,onLoginComplete:null,onBackendStart:null,onBackendComplete:null,onAccessSuccess:null,onAccessComplete:null};m=function(){var D,E;D=1<=arguments.length?r.call(arguments,0):[];if(c.logging&&((typeof console!=="undefined"&&console!==null?(E=console.log)!=null?E.apply:void 0:void 0)!=null)){return typeof console!=="undefined"&&console!==null?console.log.apply(console,["Authomatic:"].concat(r.call(D))):void 0}};A=function(E){var D,I,G,H,F;F=c.popupWidth;D=c.popupHeight;H=(screen.height/2)-(D/2);I=(screen.width/2)-(F/2);G="width="+F+",height="+D+",top="+H+",left="+I;m("Opening popup:",E);if(typeof c.onPopupOpen==="function"){c.onPopupOpen(E)}return window.open(E,"",G)};x=function(D){var K,F,L,J,G,I,H,E;L={};H=D.split("&");for(G=0,I=H.length;G<I;G++){K=H[G];E=K.split("="),F=E[0],J=E[1];J=decodeURIComponent(J);if(L.hasOwnProperty(F)){if(Array.isArray(L[F])){L[F].push(J)}else{L[F]=[L[F],J]}}else{L[F]=J}}return L};v=function(G){var E,D,F;m("parseUrl",G);D=G.indexOf("?");if(D>=0){F=G.substring(0,D);E=G.substring(D+1)}else{F=G}return{url:F,query:E,params:E?x(E):void 0}};s=function(E){var I,D,G,F,H;I=decodeURIComponent(E).split("\n");F=I[1];H=F.split("-"),G=H[0],D=H[1];return{id:parseInt(I[0]),typeId:F,type:parseInt(G),subtype:parseInt(D),rest:I.slice(2)}};g=function(E){var D,F,G;G=s(E),F=G.type,D=G.subtype;if(F===1){if(D===2){return C}else{return t}}else{if(F===2){if(D===6){return w}else{if(D===9){return b}else{if(D===14){return d}else{if(D===12||D===15){return z}else{return q}}}}}else{return z}}};y=function(E,D){return E.replace(/{([^}]*)}/g,function(H,F){var L,K,J,G,I;K=D;I=F.split(".");for(J=0,G=I.length;J<G;J++){L=I[J];K=K[L]}return K})};window.authomatic=new (k=(function(){function D(){}D.prototype.setup=function(E){l.extend(c,E);return m("Setting up authomatic to:",c)};D.prototype.popupInit=function(){l(c.popupLinkSelector).click(function(E){E.preventDefault();return A(l(this).attr("href"))});return l(c.popupFormSelector).submit(function(G){var E,F;G.preventDefault();E=l(this);F=E.attr("action")+"?"+E.serialize();if(c.popupFormValidator(E)){return A(F)}else{return typeof c.onPopupInvalid==="function"?c.onPopupInvalid(E):void 0}})};D.prototype.loginComplete=function(E,G){var F;F=l.extend(true,{},E);m("Login procedure complete",F);G();return c.onLoginComplete(F)};D.prototype.access=function(I,G,F){var K,H,J,E;if(F==null){F={}}H={onBackendStart:null,onBackendComplete:null,onAccessSuccess:null,onAccessComplete:null};E={};l.extend(E,c,H,F);G=y(G,E.substitute);m("access options",E,c);if(E.forceBackend){K=z}else{K=g(I)}J=new K(F.backend,I,G,E);m("Instantiating provider:",J);return J.access()};return D})());z=(function(){D.prototype._x_jsonpCallbackParamName="callback";function D(H,G,F,E){var I;this.backend=H;this.credentials=G;this.options=E;this.access=j(this.access,this);this.contactProvider=j(this.contactProvider,this);this.contactBackend=j(this.contactBackend,this);this.backendRequestType="auto";this.jsonpRequest=false;this.jsonpCallbackName=""+c.jsonpCallbackPrefix+o;this.deserializedCredentials=s(this.credentials);this.providerID=this.deserializedCredentials.id;this.providerType=this.deserializedCredentials.type;this.credentialsRest=this.deserializedCredentials.rest;I=v(F);this.url=I.url;this.params={};l.extend(this.params,I.params,this.options.params)}D.prototype.contactBackend=function(G){var F,E;if(this.jsonpRequest&&this.options.method===!"GET"){this.backendRequestType="fetch"}F={type:this.backendRequestType,credentials:this.credentials,url:this.url,method:this.options.method,body:this.options.body,params:JSON.stringify(this.params),headers:JSON.stringify(this.options.headers)};if(typeof c.onBackendStart==="function"){c.onBackendStart(F)}if(typeof(E=this.options).onBackendStart==="function"){E.onBackendStart(F)}m("Contacting backend at "+this.options.backend+".",F);return l.get(this.options.backend,F,G)};D.prototype.contactProvider=function(G){var K,H,J,E,M,I,F,L=this;F=G.url,E=G.method,I=G.params,H=G.headers,K=G.body;M={type:E,data:I,headers:H,complete:[(function(N,O){return m("Request complete.",O,N)}),c.onAccessComplete,this.options.onAccessComplete],success:[(function(N){return m("Request successful.",N)}),c.onAccessSuccess,this.options.onAccessSuccess],error:function(N,P,O){if(N.state()==="rejected"){if(L.options.method==="GET"){m("Cross domain request failed! trying JSONP request.");L.jsonpRequest=true}else{L.backendRequestType="fetch"}return L.access()}}};if(this.jsonpRequest){J={jsonpCallback:this.jsonpCallbackName,jsonp:this._x_jsonpCallbackParamName,cache:true,dataType:"jsonp",error:function(N,P,O){return m("JSONP failed! State:",N.state())}};l.extend(M,J);m("Contacting provider with JSONP request.",F,M)}else{m("Contacting provider with cross domain request",F,M)}return l.ajax(F,M)};D.prototype.access=function(){var F,E=this;F=function(L,M,K){var G,I,J,H;if(typeof c.onBackendComplete==="function"){c.onBackendComplete(L,M,K)}if(typeof(I=E.options).onBackendComplete==="function"){I.onBackendComplete(L,M,K)}G=K!=null?K.getResponseHeader("Authomatic-Response-To"):void 0;if(G==="fetch"){m("Fetch data returned from backend.",K.getResponseHeader("content-type"),L);if(typeof c.onAccessSuccess==="function"){c.onAccessSuccess(L)}if(typeof(J=E.options).onAccessSuccess==="function"){J.onAccessSuccess(L)}if(typeof c.onAccessComplete==="function"){c.onAccessComplete(K,M)}return typeof(H=E.options).onAccessComplete==="function"?H.onAccessComplete(K,M):void 0}else{if(G==="elements"){m("Request elements data returned from backend.",L);return E.contactProvider(L)}}};if(this.jsonpRequest){o+=1}return this.contactBackend(F)};return D})();t=(function(E){B(D,E);function D(){this.contactProvider=j(this.contactProvider,this);this.access=j(this.access,this);n=D.__super__.constructor.apply(this,arguments);return n}D.prototype.access=function(){this.jsonpRequest=true;this.params[this._x_jsonpCallbackParamName]=this.jsonpCallbackName;return D.__super__.access.call(this)};D.prototype.contactProvider=function(F){delete F.params.callback;return D.__super__.contactProvider.call(this,F)};return D})(z);C=(function(D){B(E,D);function E(){i=E.__super__.constructor.apply(this,arguments);return i}E.prototype._x_jsonpCallbackParamName="jsoncallback";return E})(t);q=(function(E){B(D,E);D.prototype._x_accessToken="access_token";D.prototype._x_bearer="Bearer";function D(){var F,G;F=1<=arguments.length?r.call(arguments,0):[];this.access=j(this.access,this);this.handleTokenType=j(this.handleTokenType,this);D.__super__.constructor.apply(this,F);G=this.credentialsRest,this.accessToken=G[0],this.refreshToken=G[1],this.expirationTime=G[2],this.tokenType=G[3];this.handleTokenType()}D.prototype.handleTokenType=function(){if(this.tokenType==="1"){return this.options.headers.Authorization=""+this._x_bearer+" "+this.accessToken}else{return this.params[this._x_accessToken]=this.accessToken}};D.prototype.access=function(){var F;if(this.backendRequestType==="fetch"){return D.__super__.access.call(this)}else{F={url:this.url,method:this.options.method,params:this.params,headers:this.options.headers,body:this.options.body};return this.contactProvider(F)}};return D})(z);w=(function(E){B(D,E);function D(){h=D.__super__.constructor.apply(this,arguments);return h}D.prototype._x_accessToken="oauth_token";return D})(q);u=(function(D){B(E,D);function E(){f=E.__super__.constructor.apply(this,arguments);return f}E.prototype._x_bearer="OAuth";return E})(q);b=(function(E){B(D,E);function D(){e=D.__super__.constructor.apply(this,arguments);return e}D.prototype._x_accessToken="oauth2_access_token";return D})(q);d=(function(D){B(E,D);function E(){this.handleTokenType=j(this.handleTokenType,this);a=E.__super__.constructor.apply(this,arguments);return a}E.prototype.handleTokenType=function(){if(this.tokenType==="1"){this.options.headers.Authorization=""+this._x_bearer+" "+this.accessToken}return this.params[this._x_accessToken]=this.accessToken};return E})(q)}).call(this);
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/resources/js/security.js	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,44 @@
+(function($) {
+
+	window.PyAMS_security = {
+
+		/**
+		 * Init social plug-in
+		 */
+		init_social: function (element) {
+
+			MyAMS.ajax.check(window.authomatic,
+							'/--static--/pyams_security/js/authomatic' + (MyAMS.devmode ? '.min.js' : '.js'),
+							function (first_load) {
+								authomatic.setup({
+									popupWidth: 800,
+									popupHeight: 400,
+									onLoginComplete: function(result) {
+										console.log(result);
+										if (result.error) {
+
+										} else if (result.user) {
+											alert(result.user);
+										}
+									}
+								});
+								authomatic.popupInit();
+							});
+		},
+
+		/**
+		 * Ordered plug-ins callback
+		 */
+		plugins: {
+
+			/**
+			 * Change plug-ins names order
+			 */
+			changeOrder: function(table, names) {
+				var input = $('input[name="' + $(this).data('ams-input-name') + '"]', $(this));
+				input.val(names.join(';'));
+			}
+		}
+	};
+
+})(jQuery);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/resources/js/security.min.js	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,1 @@
+(function(a){window.PyAMS_security={init_social:function(b){MyAMS.ajax.check(window.authomatic,"/--static--/pyams_security/js/authomatic"+(MyAMS.devmode?".min.js":".js"),function(c){authomatic.setup({popupWidth:800,popupHeight:400,onLoginComplete:function(d){console.log(d);if(d.error){}else{if(d.user){alert(d.user)}}}});authomatic.popupInit()})},plugins:{changeOrder:function(c,d){var b=a('input[name="'+a(this).data("ams-input-name")+'"]',a(this));b.val(d.join(";"))}}}})(jQuery);
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/role.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,52 @@
+#
+# 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
+from pyams_security.interfaces import IRole
+
+# import packages
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IRole)
+class Role(object):
+    """Role utility class"""
+
+    id = FieldProperty(IRole['id'])
+    title = FieldProperty(IRole['title'])
+    description = FieldProperty(IRole['description'])
+    permissions = FieldProperty(IRole['permissions'])
+
+    def __init__(self, values=None, **args):
+        if not isinstance(values, dict):
+            values = args
+        self.id = values.get('id')
+        self.title = values.get('title')
+        self.description = values.get('description')
+        self.permissions = values.get('permissions')
+
+
+def register_role(config, role):
+    """Register a new role
+
+    Roles registry is not required.
+    But only registered roles can be applied via default
+    ZMI features
+    """
+    if not IRole.providedBy(role):
+        role = Role(role)
+    config.registry.registerUtility(role, IRole, name=role.id)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/site.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,66 @@
+#
+# 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
+from pyams_security.interfaces import ISecurityManager
+from pyams_utils.interfaces.site import ISiteGenerations
+from zope.lifecycleevent.interfaces import IObjectCreatedEvent
+from zope.site.interfaces import INewLocalSite
+
+# import packages
+from pyams_security.plugin.admin import AdminAuthenticationPlugin
+from pyams_security.utility import SecurityManager
+from pyams_utils.registry import utility_config
+from pyams_utils.request import check_request
+from pyams_utils.site import check_required_utilities
+from pyramid.events import subscriber
+from zope.lifecycleevent import ObjectCreatedEvent
+
+
+REQUIRED_UTILITIES = ((ISecurityManager, '', SecurityManager, 'Security manager'),)
+
+
+@subscriber(INewLocalSite)
+def handle_new_local_site(event):
+    """Create a new negotiator when a site is created"""
+    site = event.manager.__parent__
+    check_required_utilities(site, REQUIRED_UTILITIES)
+
+
+@utility_config(name='PyAMS security', provides=ISiteGenerations)
+class SecurityGenerationsChecker(object):
+    """I18n generations checker"""
+
+    generation = 1
+
+    def evolve(self, site, current=None):
+        """Check for required utilities"""
+        check_required_utilities(site, REQUIRED_UTILITIES)
+
+
+@subscriber(IObjectCreatedEvent, context_selector=ISecurityManager)
+def handle_new_security_manager(event):
+    """Automatically create a new administration login"""
+    admin_auth = AdminAuthenticationPlugin()
+    admin_auth.prefix = 'system'
+    admin_auth.title = 'System manager authentication'
+    admin_auth.login = 'admin'
+    admin_auth.password = 'admin'
+    request = check_request()
+    request.registry.notify(ObjectCreatedEvent(admin_auth))
+    utility = event.object
+    utility['__system__'] = admin_auth
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/tests/__init__.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/tests/test_utilsdocs.py	Thu Feb 19 10:53:29 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_security 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_security/tests/test_utilsdocstrings.py	Thu Feb 19 10:53:29 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_security 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_security.%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_security/utility.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,237 @@
+#
+# 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
+from pyams_security.interfaces import ISecurityManager, ICredentialsPlugin, IAuthenticationPlugin, \
+    IDirectoryPlugin, AuthenticatedPrincipalEvent
+from pyramid.interfaces import IAuthenticationPolicy
+
+# import packages
+from pyams_security.principal import UnknownPrincipal, MissingPrincipal
+from pyams_utils.registry import query_utility
+from pyams_utils.request import check_request
+from pyramid.authentication import AuthTktCookieHelper
+from pyramid.decorator import reify
+from pyramid.security import Everyone, Authenticated
+from zope.container.folder import Folder
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(ISecurityManager)
+class SecurityManager(Folder):
+    """Security manager utility"""
+
+    enable_social_login = FieldProperty(ISecurityManager['enable_social_login'])
+    authomatic_secret = FieldProperty(ISecurityManager['authomatic_secret'])
+    social_login_use_popup = FieldProperty(ISecurityManager['social_login_use_popup'])
+    open_registration = FieldProperty(ISecurityManager['open_registration'])
+    users_folder = FieldProperty(ISecurityManager['users_folder'])
+
+    authentication_plugins_names = FieldProperty(ISecurityManager['authentication_plugins_names'])
+    directory_plugins_names = FieldProperty(ISecurityManager['directory_plugins_names'])
+
+    @property
+    def credentials_plugins_names(self):
+        request = check_request()
+        policy = request.registry.queryUtility(IAuthenticationPolicy)
+        return policy.credentials_names
+
+    def __setitem__(self, key, value):
+        super(SecurityManager, self).__setitem__(key, value)
+        if IAuthenticationPlugin.providedBy(value):
+            self.authentication_plugins_names += (key,)
+        if IDirectoryPlugin.providedBy(value):
+            self.directory_plugins_names += (key,)
+
+    def __delitem__(self, key):
+        super(SecurityManager, self).__delitem__(key)
+        if key in self.authentication_plugins_names:
+            del self.authentication_plugins_names[self.authentication_plugins_names.index(key)]
+        if key in self.directory_plugins_names:
+            del self.directory_plugins_names[self.directory_plugins_names.index(key)]
+
+    def get_plugin(self, name):
+        if name in self.credentials_plugins_names:
+            return query_utility(ICredentialsPlugin, name=name)
+        elif name:
+            return self.get(name)
+
+    def get_credentials_plugins(self, request=None):
+        if request is None:
+            request = check_request()
+        policy = request.registry.queryUtility(IAuthenticationPolicy)
+        for plugin in policy.credentials_plugins:
+            if plugin is not None:
+                yield plugin
+
+    def get_authentication_plugins(self):
+        for name in self.authentication_plugins_names or ():
+            plugin = self.get(name)
+            if IAuthenticationPlugin.providedBy(plugin):
+                yield plugin
+
+    def get_directory_plugins(self):
+        for name in self.directory_plugins_names or ():
+            plugin = self.get(name)
+            if IDirectoryPlugin.providedBy(plugin):
+                yield plugin
+
+    # IAuthenticationInfo interface methods
+    def extract_credentials(self, request, **kwargs):
+        for plugin in self.get_credentials_plugins():
+            credentials = plugin.extract_credentials(request, **kwargs)
+            if credentials:
+                return credentials
+
+    def authenticate(self, credentials, request):
+        for plugin in self.get_authentication_plugins():
+            principal_id = plugin.authenticate(credentials, request)
+            if principal_id is not None:
+                request.registry.notify(AuthenticatedPrincipalEvent(plugin.prefix, principal_id))
+                return principal_id
+
+    def authenticated_userid(self, request):
+        credentials = self.extract_credentials(request)
+        if credentials is None:
+            return None
+        principal = self.authenticate(credentials, request)
+        if principal is not None:
+            principal = self.get_principal(principal.id)
+            if principal is not None:
+                return principal.id
+
+    def effective_principals(self, principal_id):
+        principals = set()
+        for plugin in self.get_directory_plugins():
+            principals |= set(plugin.get_all_principals(principal_id))
+        return principals
+
+    # IDirectoryPlugin interface methods
+    def get_principal(self, principal_id):
+        if not principal_id:
+            return UnknownPrincipal
+        for plugin in self.get_directory_plugins():
+            principal = plugin.get_principal(principal_id)
+            if principal is not None:
+                return principal
+        return MissingPrincipal(id=principal_id)
+
+    def find_principals(self, query):
+        principals = set()
+        for plugin in self.get_directory_plugins():
+            principals |= set(plugin.find_principals(query) or set())
+        return sorted(principals, key=lambda x: x.title)
+
+
+@implementer(IAuthenticationPolicy)
+class PyAMSAuthenticationPolicy(object):
+    """PyAMS authentication policy
+
+    This authentication policy relies on a registered ISecurityManager utility.
+    Use same authentication ticket as AuthTktAuthenticationPolicy.
+
+    ``credentials`` is the list of credentials extraction utilities which can be
+    used to get credentials.
+
+    See `pyramid.authentication.AuthTktAuthenticationPolicy` to get description
+    of other constructor arguments.
+    """
+
+    def __init__(self, secret,
+                 credentials=('http', 'session'),
+                 cookie_name='auth_ticket',
+                 secure=False,
+                 include_ip=False,
+                 timeout=None,
+                 reissue_time=None,
+                 max_age=None,
+                 path="/",
+                 http_only=False,
+                 wild_domain=True,
+                 hashalg='sha256',
+                 parent_domain=False,
+                 domain=None):
+        self.credentials_names = credentials
+        self.cookie = AuthTktCookieHelper(secret,
+                                          cookie_name=cookie_name,
+                                          secure=secure,
+                                          include_ip=include_ip,
+                                          timeout=timeout,
+                                          reissue_time=reissue_time,
+                                          max_age=max_age,
+                                          http_only=http_only,
+                                          path=path,
+                                          wild_domain=wild_domain,
+                                          hashalg=hashalg,
+                                          parent_domain=parent_domain,
+                                          domain=domain)
+
+    @reify
+    def credentials_plugins(self):
+        return [query_utility(ICredentialsPlugin, name=name)
+                for name in self.credentials_names]
+
+    def _get_security_manager(self, request):
+        return query_utility(ISecurityManager)
+
+    def unauthenticated_userid(self, request):
+        result = self.cookie.identify(request)
+        if result:
+            return result['userid']
+        for plugin in self.credentials_plugins:
+            if plugin is not None:
+                credentials = plugin.extract_credentials(request)
+                if credentials is not None:
+                    return credentials.id
+
+    def authenticated_userid(self, request):
+        principal_id = self.unauthenticated_userid(request)
+        if principal_id:
+            return principal_id
+        manager = self._get_security_manager(request)
+        if manager is not None:
+            return manager.authenticated_userid(request)
+
+    def effective_principals(self, request):
+        principals = {Everyone}
+        principal_id = self.unauthenticated_userid(request)
+        if principal_id:
+            principals.add(Authenticated)
+            principals.add(principal_id)
+            manager = self._get_security_manager(request)
+            if manager is not None:
+                principals |= set(manager.effective_principals(principal_id))
+        print(principals)
+        return principals
+
+    def remember(self, request, principal, **kw):
+        return self.cookie.remember(request, principal, **kw)
+
+    def forget(self, request):
+        return self.cookie.forget(request)
+
+
+def get_principal(request):
+    """Get principal associated with given request"""
+    manager = query_utility(ISecurityManager)
+    if manager is not None:
+        principal_id = request.authenticated_userid
+        if principal_id:
+            return manager.get_principal(principal_id)
+        else:
+            return UnknownPrincipal
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/views/__init__.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,19 @@
+#
+# 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_security/views/login.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,201 @@
+#
+# 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
+from pyams_security.interfaces import ILoginView, ISecurityManager, LOGIN_REFERER_KEY
+from pyams_skin.interfaces import IModalFullPage
+from pyams_skin.layer import IPyAMSLayer
+from z3c.form.interfaces import IDataExtractedEvent
+
+# import packages
+from pyams_form.form import AddForm, AJAXAddForm, DialogAddForm
+from pyams_form.schema import ResetButton, CloseButton
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_security.credential import Credentials
+from pyams_utils.registry import query_utility
+from pyramid.events import subscriber
+from pyramid.httpexceptions import HTTPFound
+from pyramid.response import Response
+from pyramid.security import remember, forget
+from pyramid.view import view_config, forbidden_view_config
+from z3c.form import field, button
+from zope.interface import implementer, Interface, Invalid
+from zope.schema import TextLine, Password
+
+from pyams_security import _
+
+
+#
+# Login views
+#
+
+@forbidden_view_config(request_type=IPyAMSLayer)
+def ForbiddenView(request):
+    """Default forbidden view"""
+    request.session[LOGIN_REFERER_KEY] = request.view_name
+    return HTTPFound('login.html')
+
+
+class ILoginFormFields(Interface):
+    """Login form fields"""
+
+    login = TextLine(title=_("Login"))
+    password = Password(title=_("Password"))
+
+
+class ILoginFormButtons(Interface):
+    """Login form buttons"""
+
+    reset = ResetButton(name='reset', title=_("Reset"))
+    login = button.Button(name='login', title=_("Connect"))
+
+
+@subscriber(IDataExtractedEvent, form_selector=ILoginView)
+def handle_login_form_data(event):
+    """Check credentials after data extraction"""
+    data = event.data
+    if 'principal_id' in data:
+        del data['principal_id']
+    manager = query_utility(ISecurityManager)
+    if manager is not None:
+        credentials = Credentials('form', id=data['login'], **data)
+        principal_id = manager.authenticate(credentials, event.form.request)
+        if principal_id is None:
+            event.form.widgets.errors += (Invalid(_("Invalid credentials!")),)
+        else:
+            data['principal_id'] = principal_id
+    else:
+        event.form.widgets.errors += (Invalid(_("Missing security manager utility. Please contact administrator!")),)
+
+
+@pagelet_config(name='login.html', layer=IPyAMSLayer)
+@implementer(IModalFullPage, ILoginView)
+class LoginForm(AddForm):
+    """Login form"""
+
+    legend = _("Please enter valid credentials to log-in")
+    fields = field.Fields(ILoginFormFields)
+    buttons = button.Buttons(ILoginFormButtons)
+    ajax_handler = 'login.json'
+    edit_permission = None
+
+    def updateActions(self):
+        super(LoginForm, self).updateActions()
+        if 'login' in self.actions:
+            self.actions['login'].addClass('btn-primary')
+
+    def createAndAdd(self, data):
+        principal_id = data.get('principal_id')
+        if principal_id is not None:
+            headers = remember(self.request, principal_id)
+            response = self.request.response
+            response.headerlist.extend(headers)
+            if not self.request.is_xhr:
+                response.status_code = 302
+                session = self.request.session
+                if LOGIN_REFERER_KEY in session:
+                    response.location = session[LOGIN_REFERER_KEY]
+                    del session[LOGIN_REFERER_KEY]
+                else:
+                    response.location = '/'
+
+
+@view_config(name='login.json', request_type=IPyAMSLayer, renderer='json', xhr=True)
+class LoginAJAXForm(AJAXAddForm, LoginForm):
+    """Login form, AJAX view"""
+
+    def get_ajax_output(self, changes):
+        status = {'status': 'redirect'}
+        session = self.request.session
+        if LOGIN_REFERER_KEY in session:
+            status['location'] = session[LOGIN_REFERER_KEY]
+            del session[LOGIN_REFERER_KEY]
+        return status
+
+
+@forbidden_view_config(request_type=IPyAMSLayer, renderer='json', xhr=True)
+def ForbiddenAJAXView(request):
+    """AJAX call forbidden view"""
+    return {'status': 'modal',
+            'location': 'login-dialog.html'}
+
+
+class ILoginDialogFormButtons(Interface):
+    """Login dialog form buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    login = button.Button(name='login', title=_("Connect"))
+
+
+@pagelet_config(name='login-dialog.html', layer=IPyAMSLayer)
+@implementer(ILoginView)
+class LoginDialogForm(DialogAddForm):
+    """Login dialog form"""
+
+    title = _("Please enter valid credentials to log-in")
+    legend = None
+    fields = field.Fields(ILoginFormFields)
+    buttons = button.Buttons(ILoginDialogFormButtons)
+    ajax_handler = 'login-dialog.json'
+    edit_permission = None
+
+    def update(self):
+        super(LoginDialogForm, self).update()
+        self.request.session[LOGIN_REFERER_KEY] = self.request.referer
+
+    def updateActions(self):
+        super(LoginDialogForm, self).updateActions()
+        if 'login' in self.actions:
+            self.actions['login'].addClass('btn-primary')
+
+    def createAndAdd(self, data):
+        credentials = Credentials('form', id=data['login'], **data)
+        manager = query_utility(ISecurityManager)
+        if manager is not None:
+            principal_id = manager.authenticate(credentials, self.request)
+            if principal_id is not None:
+                headers = remember(self.request, principal_id)
+                response = self.request.response
+                response.headerlist.extend(headers)
+
+
+@view_config(name='login-dialog.json', request_type=IPyAMSLayer, renderer='json', xhr=True)
+class LoginDialogAJAXForm(AJAXAddForm, LoginDialogForm):
+    """Login dialog form, AJAX view"""
+
+    def get_ajax_output(self, changes):
+        status = {'status': 'redirect'}
+        session = self.request.session
+        if LOGIN_REFERER_KEY in session:
+            status['location'] = session[LOGIN_REFERER_KEY]
+            del session[LOGIN_REFERER_KEY]
+        return status
+
+
+#
+# Logout view
+#
+
+@view_config(name='logout.html', request_type=IPyAMSLayer)
+def logout(request):
+    """Logout view"""
+    headers = forget(request)
+    response = Response()
+    response.headerlist.extend(headers)
+    response.status_code = 302
+    response.location = request.referer or '/'
+    return response
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/views/oauth.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,133 @@
+#
+# 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 logging import WARNING
+
+# import interfaces
+from pyams_form.interfaces.form import IWidgetsSuffixViewletsManager
+from pyams_skin.layer import IPyAMSLayer
+from pyams_security.interfaces import AuthenticatedPrincipalEvent, LOGIN_REFERER_KEY, ISecurityManager, ILoginView
+
+# import packages
+from authomatic.adapters import WebObAdapter
+from authomatic.core import Authomatic
+from authomatic.providers import oauth1, oauth2
+from pyams_template.template import template_config
+from pyams_utils.registry import query_utility
+from pyams_viewlet.viewlet import Viewlet, viewlet_config
+from pyramid.httpexceptions import HTTPNotFound
+from pyramid.response import Response
+from pyramid.security import remember
+from pyramid.view import view_config
+
+
+#
+# Authomatic social login
+#
+
+@viewlet_config(name='social-login-suffix', layer=IPyAMSLayer,
+                manager=IWidgetsSuffixViewletsManager, view=ILoginView, weight=50)
+@template_config(template='templates/social-login.pt')
+class SocialLoginViewletsSuffix(Viewlet):
+    """Social login viewlets suffix"""
+
+    def __new__(cls, *args, **kwargs):
+        manager = query_utility(ISecurityManager)
+        if not manager.enable_social_login:
+            return None
+        return Viewlet.__new__(cls)
+
+    @property
+    def use_popup(self):
+        manager = query_utility(ISecurityManager)
+        return manager.social_login_use_popup
+
+
+CONFIG = {
+
+    'twitter': {
+        'id': 10,
+        # Provider class
+        'class_': oauth1.Twitter,
+
+        # Twitter is an AuthorizationProvider so we need to set several other properties too:
+        'consumer_key': 'F41qIog95eVs07RupYccokWbk',
+        'consumer_secret': 'yrEYMT3VnkT4yvCsXvaKDWok5gehU6fjz38e2FTphE8BXaapUA',
+    },
+
+    'facebook': {
+        'id': 20,
+        # Provider class
+        'class_': oauth2.Facebook,
+
+        # Facebook is an AuthorizationProvider too.
+        'consumer_key': '417712258385794',
+        'consumer_secret': '0a2bfc72bdc05caff800b8e46fe8ca64',
+
+        # But it is also an OAuth 2.0 provider and it needs scope.
+        'scope': ['email'],
+    },
+
+    'google': {
+        'id': 30,
+        # Provider class
+        'class_': oauth2.Google,
+        'consumer_key': '203791088227-a2nuqdo2t74ebv1n0s8houb4jfmjtp1t.apps.googleusercontent.com',
+        'consumer_secret': 'gsATqmPQASMtC60OACMcOH0i',
+        'scope': ['email']
+    }
+}
+
+
+@view_config(route_name='login')
+def login(request):
+    """Login view for Authautomatic authentication"""
+    # check security manager utility
+    manager = query_utility(ISecurityManager)
+    if (manager is None) or not manager.enable_social_login:
+        raise HTTPNotFound()
+    # store referer
+    session = request.session
+    if LOGIN_REFERER_KEY not in session:
+        referer = request.referer
+        session[LOGIN_REFERER_KEY] = referer
+    # init authomatic
+    provider_name = request.matchdict.get('provider_name')
+    authomatic = Authomatic(config=CONFIG, secret=manager.authomatic_secret, logging_level=WARNING)
+    # perform login
+    response = Response()
+    result = authomatic.login(WebObAdapter(request, response), provider_name)
+    if result:
+        if result.error:
+            pass
+        elif result.user:
+            if not (result.user.id and result.user.name):
+                result.user.update()
+            principal_id = 'oauth:{0}.{1}'.format(provider_name, result.user.id)
+            request.registry.notify(AuthenticatedPrincipalEvent('oauth',
+                                                                principal_id=principal_id,
+                                                                user=result.user))
+            headers = remember(request, principal_id)
+            response.headerlist.extend(headers)
+        if manager.social_login_use_popup:
+            response.text = result.popup_html()
+        response.status_code = 302
+        if LOGIN_REFERER_KEY in session:
+            response.location = session[LOGIN_REFERER_KEY]
+            del session[LOGIN_REFERER_KEY]
+        else:
+            response.location = '/'
+    return response
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/views/templates/social-login.pt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,10 @@
+<div class="social">
+	<span tal:condition="view.use_popup"
+		  data-ams-plugins="pyams_security"
+		  data-ams-plugin-pyams_security-src="/--static--/pyams_security/js/security.js"
+		  data-ams-plugin-pyams_security-callback="PyAMS_security.init_social"></span>
+	<span>Login with: </span>
+	<a class="authomatic" href="/login/facebook">Facebook</a>
+	<a class="authomatic" href="/login/twitter">Twitter</a>
+	<a class="authomatic" href="/login/google">Google</a>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/views/templates/user-registration-end.pt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,22 @@
+<div class="modal-dialog modal-medium">
+	<div class="modal-content">
+		<div class="modal-header">
+			<h3 class="modal-title">
+				<span class="title">Registration complete!</span>
+			</h3>
+		</div>
+		<div class="modal-body no-padding">
+			<div class="ams-form">
+				<div class="alert alert-success">
+					Your registration is complete.<br />
+					You can now return to home page."
+				</div>
+				<footer>
+					<a class="btn btn-success" href="/">
+						Return to home page
+					</a>
+				</footer>
+			</div>
+		</div>
+	</div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/views/templates/user-registration.pt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,8 @@
+<div>
+	<div class="pull-right">
+		<a class="btn btn-sm btn-success"
+		   href="user-registration.html" data-toggle="modal"
+		   tal:content="view.button_label">Register new account</a>
+		<div class="clearfix"></div>
+	</div>
+</div>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/views/userfolder.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,222 @@
+#
+# 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
+from pyams_form.interfaces.form import IWidgetsSuffixViewletsManager
+from pyams_security.interfaces import ILoginView, ISecurityManager, IUserRegistrationInfo, \
+    IUserRegistrationConfirmationInfo
+from pyams_skin.interfaces import IModalFullPage
+from pyams_skin.layer import IPyAMSLayer
+from z3c.form.interfaces import HIDDEN_MODE, IDataExtractedEvent
+
+# import packages
+from pyams_form.form import DialogAddForm, AJAXAddForm, AddForm
+from pyams_form.schema import CloseButton
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_security.plugin.userfolder import User
+from pyams_template.template import template_config
+from pyams_utils.registry import query_utility, get_utility
+from pyams_viewlet.viewlet import viewlet_config, Viewlet
+from pyramid.events import subscriber
+from pyramid.httpexceptions import HTTPNotFound
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.interface import implementer, Interface, Invalid
+
+from pyams_security import _
+
+
+#
+# User registration form
+#
+
+@viewlet_config(name='user-registration-suffix', layer=IPyAMSLayer,
+                manager=IWidgetsSuffixViewletsManager, view=ILoginView, weight=25)
+@template_config(template='templates/user-registration.pt')
+class UserRegistrationViewletsSuffix(Viewlet):
+    """User registration viewlet suffix"""
+
+    button_label = _("Register new account")
+
+    def __new__(cls, *args, **kwargs):
+        manager = query_utility(ISecurityManager)
+        if not manager.open_registration:
+            return None
+        return Viewlet.__new__(cls)
+
+
+class IUserRegistrationFormButtons(Interface):
+    """User registration form buttons"""
+
+    cancel = CloseButton(name='close', title=_("Cancel"))
+    register = button.Button(name='register', title=_("Register"))
+
+
+@pagelet_config(name='user-registration.html', layer=IPyAMSLayer)
+class UserRegistrationForm(DialogAddForm):
+    """User registration form"""
+
+    title = _("User registration")
+    legend = _("Please enter registration info")
+    icon_css_class = 'fa fa-fw fa-user'
+
+    fields = field.Fields(IUserRegistrationInfo)
+    buttons = button.Buttons(IUserRegistrationFormButtons)
+    ajax_handler = 'user-registration.json'
+    edit_permission = None
+
+    def updateActions(self):
+        super(UserRegistrationForm, self).updateActions()
+        if 'register' in self.actions:
+            self.actions['register'].addClass('btn-primary')
+
+    def updateWidgets(self, prefix=None):
+        super(UserRegistrationForm, self).updateWidgets()
+        self.widgets['password'].input_css_class = 'col-md-4'
+        self.widgets['confirmed_password'].input_css_class = 'col-md-4'
+
+    def create(self, data):
+        return User()
+
+    def update_content(self, user, data):
+        user.login = data.get('login')
+        user.email = data.get('email')
+        user.firstname = data.get('firstname')
+        user.lastname = data.get('lastname')
+        user.company_name = data.get('company_name')
+        user.password = data.get('password')
+        user.generate_secret()
+
+    def add(self, user):
+        manager = get_utility(ISecurityManager)
+        folder = manager.get(manager.users_folder)
+        folder[user.login] = user
+
+
+@subscriber(IDataExtractedEvent, form_selector=UserRegistrationForm)
+def handle_registration_data_extraction(event):
+    """Handle registration data extraction"""
+    data = event.data
+    if not data.get('login'):
+        data['login'] = data['email']
+    manager = get_utility(ISecurityManager)
+    folder = manager.get(manager.users_folder)
+    if folder is None:
+        event.form.widgets.errors += (Invalid(_("Can't create user profile. Please contact system administrator.")),)
+    elif not folder.check_login(data.get('login')):
+        event.form.widgets.errors += (Invalid(_("Specified login can't be used!")),)
+
+
+@view_config(name='user-registration.json', request_type=IPyAMSLayer, renderer='json', xhr=True)
+class UserRegistrationAJAXForm(AJAXAddForm, UserRegistrationForm):
+    """User registration form, AJAX view"""
+
+    def get_ajax_output(self, changes):
+        translate = self.request.localizer.translate
+        return {'status': 'success',
+                'messagebox': {'status': 'success',
+                               'title': translate(_("Your registration is recorded!")),
+                               'icon': 'fa fa-fw fa-2x fa-user',
+                               'content': translate(_("Your registration is recorded. You should receive a "
+                                                      "confirmation email soon which will allow you to "
+                                                      "confirm your inscription.")),
+                               'timeout': None}}
+
+
+#
+# Registration confirmation form
+#
+
+class IUserConfirmationFormButtons(Interface):
+    """User confirmation form buttons"""
+
+    confirm = button.Button(name='confirm', title=_("Finalize registration"))
+
+
+@pagelet_config(name='user-confirmation.html', layer=IPyAMSLayer)
+@implementer(IModalFullPage)
+class UserConfirmationForm(AddForm):
+    """User registration confirmation form"""
+
+    title = _("User registration confirmation")
+    legend = _("Please confirm your registration info")
+    icon_css_class = 'fa fa-fw fa-user'
+
+    fields = field.Fields(IUserRegistrationConfirmationInfo)
+    buttons = button.Buttons(IUserConfirmationFormButtons)
+    ajax_handler = 'user-confirmation.json'
+    edit_permission = None
+
+    def updateWidgets(self, prefix=None):
+        super(UserConfirmationForm, self).updateWidgets()
+        self.widgets['activation_hash'].mode = HIDDEN_MODE
+        self.widgets['activation_hash'].value = self.request.form.get('hash') or self.widgets['activation_hash'].value
+
+    def updateActions(self):
+        super(UserConfirmationForm, self).updateActions()
+        if 'confirm' in self.actions:
+            self.actions['confirm'].addClass('btn-primary')
+
+    def createAndAdd(self, data):
+        user = data.get('user')
+        if user is None:
+            raise HTTPNotFound()
+        self._finishedAdd = True
+
+    def nextURL(self):
+        return '/'
+
+
+@subscriber(IDataExtractedEvent, form_selector=UserConfirmationForm)
+def handle_confirmation_data_extraction(event):
+    """Handle confirmation data extraction"""
+    data = event.data
+    if 'user' in data:
+        del data['user']
+    manager = get_utility(ISecurityManager)
+    folder = manager.get(manager.users_folder)
+    if folder is None:
+        event.form.widgets.errors += (Invalid(_("Can't check user profile. Please contact system administrator.")),)
+    else:
+        login = data.get('login')
+        user = folder.get(login)
+        if user is None:
+            event.form.widgets.errors += (Invalid(_("Can't retrieve user profile!")),)
+        else:
+            try:
+                user.check_activation(data.get('activation_hash'), login, data.get('password'))
+            except Invalid as error:
+                event.form.widgets.errors += (error,)
+            else:
+                data['user'] = user
+
+
+@view_config(name='user-confirmation.json', request_type=IPyAMSLayer, renderer='json', xhr=True)
+class UserConfirmationAJAXForm(AJAXAddForm, UserConfirmationForm):
+    """User registration confirmation form, AJAX view"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'redirect',
+                'location': 'user-registration-end.html'}
+
+
+@pagelet_config(name='user-registration-end.html', layer=IPyAMSLayer)
+@template_config(template='templates/user-registration-end.pt', layer=IPyAMSLayer)
+@implementer(IModalFullPage)
+class UserRegistrationEnd(object):
+    """User registration end"""
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/vocabulary.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,32 @@
+#
+# 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
+from zope.password.interfaces import IPasswordManager
+
+# import packages
+from zope.componentvocabulary.vocabulary import UtilityVocabulary
+from zope.schema.vocabulary import getVocabularyRegistry
+
+
+class PasswordManagerVocabulary(UtilityVocabulary):
+    """Password managers vocabulary"""
+
+    interface = IPasswordManager
+    nameOnly = True
+
+getVocabularyRegistry().register('PyAMS password managers', PasswordManagerVocabulary)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/zmi/__init__.py	Thu Feb 19 10:53:29 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_security/zmi/configure.zcml	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,7 @@
+<configure
+	xmlns="http://pylonshq.com/pyramid">
+
+	<include package="pyramid_zcml" />
+	<include package="z3c.form" file="meta.zcml" />
+
+</configure>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/zmi/interfaces.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,28 @@
+#
+# 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
+from pyams_skin.interfaces.viewlet import IMenuItem, IToolbarAction
+
+# import packages
+
+
+class ISecurityManagerMenu(IMenuItem):
+    """Security manager menu interface"""
+
+
+class ISecurityManagerToolbarAddingMenu(IToolbarAction):
+    """Security manager toolbar addings action viewlet manager"""
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/zmi/plugin/__init__.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,19 @@
+#
+# 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_security/zmi/plugin/admin.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,120 @@
+#
+# 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
+from pyams_security.interfaces import IAdminAuthenticationPlugin, ISecurityManager
+from pyams_security.zmi.interfaces import ISecurityManagerToolbarAddingMenu
+from pyams_skin.interfaces import IContentHelp
+from pyams_skin.layer import IPyAMSLayer
+from z3c.form.interfaces import DISPLAY_MODE
+from zope.component.interfaces import ISite
+
+# import packages
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_security.plugin.admin import AdminAuthenticationPlugin
+from pyams_security.zmi.utility import SecurityManagerPluginsTable
+from pyams_skin.help import ContentHelp
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import query_utility
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogEditForm, AdminDialogAddForm
+from pyams_zmi.layer import IAdminLayer
+from pyramid.url import resource_url
+from pyramid.view import view_config
+from z3c.form import field
+
+from pyams_security import _
+
+
+@viewlet_config(name='add-admin-authentication.menu', context=ISite, layer=IAdminLayer,
+                view=SecurityManagerPluginsTable, manager=ISecurityManagerToolbarAddingMenu,
+                permission='system.manage', weight=1)
+class AdminAuthenticationAddMenu(ToolbarMenuItem):
+    """Admin authentication add menu"""
+
+    label = _("Add admin authentication...")
+    label_css_class = 'fa fa-fw fa-support'
+    url = 'add_admin_authentication.html'
+    modal_target = True
+
+
+@pagelet_config(name='add_admin_authentication.html', context=ISite, layer=IPyAMSLayer,
+                permission='system.manage')
+class AdminAuthenticationAddForm(AdminDialogAddForm):
+    """Admin authentication plug-in add form"""
+
+    title = _("System security manager")
+    legend = _("Add administration authentication plug-in")
+    icon_css_class = 'fa fa-fw fa-support'
+
+    fields = field.Fields(IAdminAuthenticationPlugin).omit('__name__', '__parent__')
+    ajax_handler = 'add_admin_authentication.json'
+    edit_permission = 'system.manage'
+
+    def create(self, data):
+        return AdminAuthenticationPlugin()
+
+    def add(self, plugin):
+        context = query_utility(ISecurityManager)
+        context[plugin.prefix] = plugin
+
+    def nextURL(self):
+        return resource_url(self.context, self.request, 'security-manager.html')
+
+
+@view_config(name='add_admin_authentication.json', context=ISite, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class AdminAuthenticationAJAXAddForm(AJAXAddForm, AdminAuthenticationAddForm):
+    """Admin authentication plug-in add form, AJAX handler"""
+
+
+@pagelet_config(name='properties.html', context=IAdminAuthenticationPlugin, layer=IPyAMSLayer,
+                permission='system.view')
+class AdminAuthenticationEditForm(AdminDialogEditForm):
+    """Admin authentication plug-in edit form"""
+
+    @property
+    def title(self):
+        return self.context.title
+
+    legend = _("Edit administration authentication plug-in")
+    icon_css_class = 'fa fa-fw fa-support'
+
+    fields = field.Fields(IAdminAuthenticationPlugin).omit('__name__', '__parent__')
+    ajax_handler = 'properties.json'
+    edit_permission = 'system.manage'
+
+    def updateWidgets(self, prefix=None):
+        super(AdminAuthenticationEditForm, self).updateWidgets()
+        self.widgets['prefix'].mode = DISPLAY_MODE
+
+
+@view_config(name='properties.json', context=IAdminAuthenticationPlugin, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class AdminAuthenticationAJAXEditForm(AJAXEditForm, AdminAuthenticationEditForm):
+    """Admin authentication plug-in edit form, AJAX handler"""
+
+
+@adapter_config(context=(IAdminAuthenticationPlugin, IAdminLayer, AdminAuthenticationEditForm), provides=IContentHelp)
+class AdminAuthenticationHelpAdapter(ContentHelp):
+    """Admin authentication edit form help adapter"""
+
+    header = _("WARNING")
+    status = 'danger'
+    message = _("""Before disabling plug-in, please verify that you have other administration access!""")
+    message_format = 'rest'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/zmi/plugin/directory.py	Thu Feb 19 10:53:29 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_security/zmi/plugin/userfolder.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,367 @@
+#
+# 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 datetime import datetime
+
+# import interfaces
+from pyams_security.interfaces import IUsersFolderPlugin, ISecurityManager, ILocalUser, IUserRegistrationInfo
+from pyams_security.zmi.interfaces import ISecurityManagerToolbarAddingMenu, ISecurityManagerMenu
+from pyams_skin.interfaces.viewlet import IMenuItem, IToolbarViewletManager
+from pyams_skin.interfaces import IPageHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces.data import IObjectData
+from pyams_zmi.layer import IAdminLayer
+from z3c.form.interfaces import DISPLAY_MODE, IDataExtractedEvent
+from z3c.table.interfaces import IColumn
+from zope.component.interfaces import ISite
+from zope.dublincore.interfaces import IZopeDublinCore
+
+# import packages
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_form.search import SearchView, SearchResultsView
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_security.plugin.userfolder import UsersFolder, User
+from pyams_security.zmi.utility import SecurityManagerPluginsTable
+from pyams_skin.skin import apply_skin
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem, ToolbarAction
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.date import format_datetime
+from pyams_utils.registry import query_utility
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyams_zmi.view import AdminView
+from pyramid.events import subscriber
+from pyramid.view import view_config
+from z3c.form import field
+from z3c.table.column import GetAttrColumn
+from zope.interface import implementer, Interface, Invalid
+
+from pyams_security import _
+
+
+@viewlet_config(name='add-users-folder.menu', context=ISite, layer=IAdminLayer,
+                view=SecurityManagerPluginsTable, manager=ISecurityManagerToolbarAddingMenu,
+                permission='system.manage', weight=10)
+class UsersFolderAddMenu(ToolbarMenuItem):
+    """Local users folder add menu"""
+
+    label = _("Add local users folder...")
+    label_css_class = 'fa fa-fw fa-user'
+    url = 'add-users-folder.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-users-folder.html', context=ISite, layer=IPyAMSLayer,
+                permission='system.manage')
+class UsersFolderAddForm(AdminDialogAddForm):
+    """Users folder plug-in add form"""
+
+    title = _("System security manager")
+    legend = _("Add local users folder plug-in")
+    icon_css_class = 'fa fa-fw fa-user'
+
+    fields = field.Fields(IUsersFolderPlugin).omit('__name__', '__parent__')
+    ajax_handler = 'add-users-folder.json'
+    edit_permission = None
+
+    def create(self, data):
+        return UsersFolder()
+
+    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-users-folder.json', context=ISite, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class UsersFolderAJAXAddForm(AJAXAddForm, UsersFolderAddForm):
+    """users folder plug-in add form, AJAX handler"""
+
+
+@pagelet_config(name='properties.html', context=IUsersFolderPlugin, layer=IPyAMSLayer,
+                permission='system.view')
+class UsersFolderEditForm(AdminDialogEditForm):
+    """Users folder plug-in edit form"""
+
+    @property
+    def title(self):
+        return self.context.title
+
+    legend = _("Edit local users folder plug-in properties")
+    icon_css_class = 'fa fa-fw fa-user'
+
+    fields = field.Fields(IUsersFolderPlugin).omit('__name__', '__parent__')
+    ajax_handler = 'properties.json'
+    edit_permission = 'system.manage'
+
+    def updateWidgets(self, prefix=None):
+        super(UsersFolderEditForm, self).updateWidgets()
+        self.widgets['prefix'].mode = DISPLAY_MODE
+
+
+@view_config(name='properties.json', context=IUsersFolderPlugin, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class UsersFolderAJAXEditForm(AJAXEditForm, UsersFolderEditForm):
+    """Users folder plug-in edit form, AJAX handler"""
+
+
+@adapter_config(name='security.menu', context=(IUsersFolderPlugin, IAdminLayer, Interface, ISecurityManagerMenu),
+                provides=IMenuItem)
+@implementer(IObjectData)
+class UsersFolderContentsMenu(MenuItem):
+    """Users folder contents menu"""
+    
+    url = 'contents.html'
+    object_data = {'ams-target': '#content'}
+
+    @property
+    def label(self):
+        return self.context.title
+    
+
+@pagelet_config(name='search.html', context=IUsersFolderPlugin, layer=IPyAMSLayer, permission='system.view')
+class UsersFolderSearchView(AdminView, SearchView):
+    """Users folder search view"""
+    
+    def __init__(self, context, request):
+        super(UsersFolderSearchView, self).__init__(context, request)
+
+
+@adapter_config(context=(IUsersFolderPlugin, IAdminLayer, UsersFolderSearchView), provides=IPageHeader)
+class UsersFolderSearchViewHeaderAdapter(ContextRequestViewAdapter):
+    """Users folder search view header adapter"""
+
+    icon_class = 'fa fa-fw fa-user'
+
+    @property
+    def title(self):
+        return self.context.title
+
+    subtitle = _("Search users")
+
+
+@view_config(name='search-results.html', context=IUsersFolderPlugin, request_type=IPyAMSLayer,
+             permission='system.view')
+class UsersFolderSearchResultsView(AdminView, SearchResultsView):
+    """Users folder search results view table"""
+
+    id = 'users_folder_search_table'
+    title = _("Search results")
+    cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight datatable'}
+
+    def __init__(self, context, request):
+        super(UsersFolderSearchResultsView, self).__init__(context, request)
+        apply_skin(self.request, 'PyAMS admin skin')
+
+
+@adapter_config(name='login', context=(IUsersFolderPlugin, IAdminLayer, UsersFolderSearchResultsView),
+                provides=IColumn)
+class LoginColumn(GetAttrColumn):
+    """Users login column"""
+
+    _header = _("Login")
+    attrName = 'login'
+    weight = 5
+
+    @property
+    def header(self):
+        return self.request.localizer.translate(self._header)
+
+
+@adapter_config(name='name', context=(IUsersFolderPlugin, IAdminLayer, UsersFolderSearchResultsView),
+                provides=IColumn)
+class NameColumn(GetAttrColumn):
+    """Users name column"""
+
+    _header = _("Name")
+    attrName = 'title'
+    weight = 10
+
+    @property
+    def header(self):
+        return self.request.localizer.translate(self._header)
+
+
+@adapter_config(name='email', context=(IUsersFolderPlugin, IAdminLayer, UsersFolderSearchResultsView),
+                provides=IColumn)
+class EmailColumn(GetAttrColumn):
+    """Users email column"""
+
+    _header = _("E-mail address")
+    attrName = 'email'
+    weight = 20
+
+    @property
+    def header(self):
+        return self.request.localizer.translate(self._header)
+
+
+@adapter_config(name='registration_date', context=(IUsersFolderPlugin, IAdminLayer, UsersFolderSearchResultsView),
+                provides=IColumn)
+class RegistrationDateColumn(GetAttrColumn):
+    """Users registration date column"""
+
+    _header = _("Registration date")
+    weight = 30
+
+    def getValue(self, obj):
+        dc = IZopeDublinCore(obj, None)
+        if dc is not None:
+            return format_datetime(dc.created)
+        else:
+            return '--'
+
+    @property
+    def header(self):
+        return self.request.localizer.translate(self._header)
+
+
+@adapter_config(name='activation_date', context=(IUsersFolderPlugin, IAdminLayer, UsersFolderSearchResultsView),
+                provides=IColumn)
+class ConfirmationDateColumn(GetAttrColumn):
+    """Users activation date column"""
+
+    _header = _("Activation date")
+    weight = 40
+
+    def getValue(self, obj):
+        if obj.activation_date:
+            return format_datetime(obj.activation_date)
+        else:
+            return '--'
+
+    @property
+    def header(self):
+        return self.request.localizer.translate(self._header)
+
+
+@viewlet_config(name='users-folder.toolbar.adding', context=IUsersFolderPlugin,
+                view=UsersFolderSearchView.search_form_factory, layer=IAdminLayer,
+                manager=IToolbarViewletManager, permission='system.manage')
+class UsersFolderAddAction(ToolbarAction):
+    """Users folder adding action"""
+
+    label = _("Add user")
+    url = 'add-user.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-user.html', context=IUsersFolderPlugin, layer=IPyAMSLayer, permission='system.manage')
+class LocalUserAddForm(AdminDialogAddForm):
+    """Local user add form"""
+
+    @property
+    def title(self):
+        return self.context.title
+
+    legend = _("Add new local user")
+    icon_css_class = 'fa fa-fw fa-user'
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+    fields = field.Fields(IUserRegistrationInfo).select('login', 'email', 'firstname', 'lastname', 'company_name') + \
+        field.Fields(ILocalUser).select('password_manager') + \
+        field.Fields(IUserRegistrationInfo).select('password', 'confirmed_password') + \
+        field.Fields(ILocalUser).select('wait_confirmation')
+
+    ajax_handler = 'add-user.json'
+    edit_permission = 'system.manage'
+
+    def updateWidgets(self, prefix=None):
+        super(LocalUserAddForm, self).updateWidgets()
+        self.widgets['password'].input_css_class = 'col-md-4'
+        self.widgets['confirmed_password'].input_css_class = 'col-md-4'
+
+    def create(self, data):
+        return User()
+
+    def update_content(self, user, data):
+        user.login = data.get('login')
+        user.email = data.get('email')
+        user.firstname = data.get('firstname')
+        user.lastname = data.get('lastname')
+        user.company_name = data.get('company_name')
+        user.password_manager = data.get('password_manager')
+        user.password = data.get('password')
+        user.self_registered = False
+        if data.get('wait_confirmation'):
+            user.generate_secret()
+        else:
+            user.activation_date = datetime.utcnow()
+            user.activated = True
+        user.wait_confirmation = data.get('wait_confirmation')
+
+    def add(self, user):
+        self.context[user.login] = user
+
+
+@subscriber(IDataExtractedEvent, form_selector=LocalUserAddForm)
+def handle_new_user_data_extraction(event):
+    """Handle new user form data extraction"""
+    data = event.data
+    if not data.get('login'):
+        data['login'] = data['email']
+    folder = event.form.context
+    if not folder.check_login(data.get('login')):
+        event.form.widgets.errors += (Invalid(_("Specified login can't be used!")),)
+
+
+@view_config(name='add-user.json', context=IUsersFolderPlugin, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class LocalUserAJAXAddForm(AJAXAddForm, LocalUserAddForm):
+    """Local user add form, AJAX view"""
+
+    def get_ajax_output(self, changes):
+        translate = self.request.localizer.translate
+        return {'status': 'success',
+                'message': translate(_("User was created successfully"))}
+
+
+@pagelet_config(name='properties.html', context=ILocalUser, layer=IPyAMSLayer, permission='system.view')
+class LocalUserEditForm(AdminDialogEditForm):
+    """Local user edit form"""
+
+    @property
+    def title(self):
+        return self.context.title
+
+    legend = _("Edit user properties")
+    icon_css_class = 'fa fa-fw fa-user'
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+    fields = field.Fields(ILocalUser).select('login', 'email', 'firstname', 'lastname', 'company_name',
+                                             'self_registered', 'activation_hash', 'activation_date')
+
+    ajax_handler = 'properties.json'
+    edit_permission = 'system.manage'
+
+    def updateWidgets(self, prefix=None):
+        super(LocalUserEditForm, self).updateWidgets()
+        self.widgets['self_registered'].mode = DISPLAY_MODE
+        self.widgets['activation_hash'].mode = DISPLAY_MODE
+        self.widgets['activation_date'].mode = DISPLAY_MODE
+
+
+@view_config(name='properties.json', context=ILocalUser, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class LocalUserAJAXEditForm(AJAXEditForm, LocalUserEditForm):
+    """Local user edit form, AJAX view"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/zmi/utility.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,180 @@
+#
+# 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
+from pyams_security.interfaces import ISecurityManager, IDirectorySearchPlugin
+from pyams_security.zmi.interfaces import ISecurityManagerMenu, ISecurityManagerToolbarAddingMenu
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.interfaces.container import ITableElementEditor
+from pyams_skin.interfaces.viewlet import IToolbarViewletManager
+from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.interfaces.menu import IControlPanelMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IValues, IColumn
+from zope.component.interfaces import ISite
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_security.zmi.widget import OrderedPluginsFieldWidget
+from pyams_skin.container import ContainerView
+from pyams_skin.table import BaseTable, DefaultElementEditorAdapter, ActionColumn
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_skin.viewlet.toolbar import ToolbarMenu
+from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config
+from pyams_utils.registry import query_utility
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogEditForm
+from pyams_zmi.view import AdminView
+from pyramid.url import resource_url
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import implementer, Interface
+
+from pyams_security import _
+
+
+@adapter_config(context=(ISecurityManager, IAdminLayer, Interface), provides=ITableElementEditor)
+class SecurityManagerTableElementEditor(DefaultElementEditorAdapter):
+    """Security manager table element editor"""
+
+    view_name = 'security-manager.html'
+    modal_target = False
+
+    @property
+    def url(self):
+        site = get_parent(self.context, ISite)
+        return resource_url(site, self.request, 'admin.html#{0}'.format(self.view_name))
+
+
+@viewlet_config(name='security-manager.menu', context=ISite, layer=IAdminLayer, manager=IControlPanelMenu,
+                permission='system.view', weight=5)
+@viewletmanager_config(name='security-manager.menu', context=ISite, layer=IAdminLayer)
+@implementer(ISecurityManagerMenu)
+class SecurityManagerMenuItem(MenuItem):
+    """Security manager menu"""
+
+    label = _("Security")
+    icon_class = 'fa fa-fw fa-lock'
+    url = '#security-manager.html'
+
+
+class SecurityManagerPluginsTable(BaseTable):
+    """Security manager plug-ins table"""
+
+    id = 'security_manager_table'
+    title = _("Authentication and users directory plug-ins")
+    cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight datatable'}
+
+
+@adapter_config(name='search', context=(Interface, IAdminLayer, SecurityManagerPluginsTable), provides=IColumn)
+class SecurityManagerPluginsSearchColumn(ActionColumn):
+    """Security manager plugins search column"""
+
+    url = "search.html"
+    weight = 100
+
+    def renderCell(self, item):
+        if not IDirectorySearchPlugin.providedBy(item):
+            return ''
+        return super(SecurityManagerPluginsSearchColumn, self).renderCell(item)
+
+
+@adapter_config(context=(ISite, IAdminLayer, SecurityManagerPluginsTable), provides=IValues)
+class SecurityManagerValuesAdapter(ContextRequestViewAdapter):
+    """Security manager values adapter"""
+
+    @property
+    def values(self):
+        manager = query_utility(ISecurityManager)
+        if manager is not None:
+            return list(manager.values())
+        return ()
+
+
+@pagelet_config(name='security-manager.html', context=ISite, layer=IPyAMSLayer, permission='system.view')
+@implementer(IInnerPage)
+class SecurityManagerView(AdminView, ContainerView):
+    """Security manager view"""
+
+    table_class = SecurityManagerPluginsTable
+
+    def __init__(self, context, request):
+        super(SecurityManagerView, self).__init__(context, request)
+
+
+@adapter_config(context=(ISite, IAdminLayer, SecurityManagerView), provides=IPageHeader)
+class SecurityManagerHeaderAdapter(ContextRequestViewAdapter):
+    """Security manager header adapter"""
+
+    icon_class = 'fa fa-fw fa-lock'
+    title = _("Security manager")
+    subtitle = _("Security manager plug-ins")
+
+
+@viewlet_config(name='security-manager.toolbar.adding', context=ISite, view=SecurityManagerPluginsTable,
+                layer=IAdminLayer, manager=IToolbarViewletManager, permission='system.manage')
+@viewletmanager_config(name='security-manager.toolbar.adding', context=ISite, view=SecurityManagerPluginsTable,
+                       layer=IAdminLayer)
+@implementer(ISecurityManagerToolbarAddingMenu)
+class SecurityManagerToolbarAddingsAction(ToolbarMenu):
+    """Security manager toolbar adding action"""
+
+    label = _("Add...")
+
+
+@viewlet_config(name='security-manager.properties.menu', context=ISite, layer=IAdminLayer,
+                manager=ISecurityManagerMenu, permission='system.view')
+class SecurityManagerPropertiesMenuItem(MenuItem):
+    """Security manager properties menu"""
+
+    label = _("Properties...")
+    url = 'properties.html'
+    modal_target = True
+
+    def get_url(self):
+        manager = query_utility(ISecurityManager)
+        return resource_url(manager, self.request, self.url)
+
+
+@pagelet_config(name='properties.html', context=ISecurityManager, layer=IPyAMSLayer, permission='system.view')
+class SecurityManagerEditForm(AdminDialogEditForm):
+    """Security manager edit form"""
+
+    title = _("System security manager")
+    legend = _("Security manager properties")
+    icon_css_class = 'fa fa-fw fa-lock'
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+    fields = field.Fields(ISecurityManager)
+    fields['credentials_plugins_names'].widgetFactory = OrderedPluginsFieldWidget
+    fields['authentication_plugins_names'].widgetFactory = OrderedPluginsFieldWidget
+    fields['directory_plugins_names'].widgetFactory = OrderedPluginsFieldWidget
+
+    ajax_handler = 'properties.json'
+
+    def getContent(self):
+        return query_utility(ISecurityManager)
+
+
+@view_config(name='properties.json', context=ISecurityManager, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class SecurityManagerAJAXEditForm(AJAXEditForm, SecurityManagerEditForm):
+    """Security manager edit form, AJAX view"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/zmi/widget/__init__.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,56 @@
+#
+# 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 pyams_form.interfaces.form import IFormLayer
+from pyams_form.widget import widgettemplate_config
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from z3c.form.interfaces import IDataConverter
+from zope.schema.interfaces import ITuple
+
+# import packages
+from pyams_utils.adapter import adapter_config
+from z3c.form.browser.widget import HTMLFormElement
+from z3c.form.converter import BaseDataConverter
+from z3c.form.widget import FieldWidget, Widget
+
+
+@widgettemplate_config(mode='input', template='templates/ordered-list-input.pt', layer=IFormLayer)
+@widgettemplate_config(mode='display', template='templates/ordered-list-display.pt', layer=IFormLayer)
+class OrderedPluginsWidget(HTMLFormElement, Widget):
+    """Ordered plug-ins list widget"""
+
+    def str_value(self):
+        return ';'.join(self.value)
+
+    def items(self):
+        for name in self.value:
+            yield self.context.get_plugin(name)
+
+
+def OrderedPluginsFieldWidget(field, request):
+    return FieldWidget(field, OrderedPluginsWidget(request))
+
+
+@adapter_config(context=(ITuple, OrderedPluginsWidget), provides=IDataConverter)
+class OrderdedPluginsDataConverter(BaseDataConverter):
+    """Ordered plugins data converter"""
+
+    def toWidgetValue(self, value):
+        return value
+
+    def toFieldValue(self, value):
+        return tuple(value.split(';'))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/zmi/widget/templates/ordered-list-display.pt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,6 @@
+<table class="table table-bordered table-striped table-hover table-tight"
+	   tal:attributes="id string:${view/field/getName}_list">
+	<tr tal:repeat="item view/items">
+		<td tal:content="item/title | item"></td>
+	</tr>
+</table>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/zmi/widget/templates/ordered-list-input.pt	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,16 @@
+<table class="table table-bordered table-striped table-hover table-tight table-dnd"
+	   data-ams-tablednd-drop-target="PyAMS_security.plugins.changeOrder"
+	   data-ams-plugins="security"
+	   data-ams-plugin-security-src="/--static--/pyams_security/js/security.js"
+	   tal:attributes="id string:${view/field/getName}_list;
+					   data-ams-input-name view/name;">
+	<input type="hidden"
+		   tal:attributes="id view/id;
+						   name view/name;
+						   value view/str_value;" />
+	<tr tal:repeat="item view/items"
+		tal:attributes="id string:${view/field/getName}::${item/__name__ | default};
+						data-ams-element-name item/__name__ | default;">
+		<td tal:content="item/title | item"></td>
+	</tr>
+</table>