[Zope-CVS] SVN: ldappas/trunk/ First implementation of a PAS LDAP Authentication plugin

Florent Guillaume fg at nuxeo.com
Wed Oct 13 10:15:16 EDT 2004


Log message for revision 28082:
  First implementation of a PAS LDAP Authentication plugin

Changed:
  A   ldappas/trunk/__init__.py
  A   ldappas/trunk/authentication.py
  A   ldappas/trunk/configure.zcml
  A   ldappas/trunk/interfaces.py
  A   ldappas/trunk/ldappas-configure.zcml.in
  A   ldappas/trunk/tests/
  A   ldappas/trunk/tests/__init__.py
  A   ldappas/trunk/tests/test_authentication.py

-=-
Added: ldappas/trunk/__init__.py
===================================================================
--- ldappas/trunk/__init__.py	2004-10-13 14:13:19 UTC (rev 28081)
+++ ldappas/trunk/__init__.py	2004-10-13 14:15:14 UTC (rev 28082)
@@ -0,0 +1,17 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation 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
+#
+##############################################################################
+"""LDAP PAS Plugins packages.
+
+$Id$
+"""


Property changes on: ldappas/trunk/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: ldappas/trunk/authentication.py
===================================================================
--- ldappas/trunk/authentication.py	2004-10-13 14:13:19 UTC (rev 28081)
+++ ldappas/trunk/authentication.py	2004-10-13 14:15:14 UTC (rev 28082)
@@ -0,0 +1,202 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation 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.
+#
+##############################################################################
+"""LDAP PAS Authentication plugin
+
+$Id$
+"""
+
+from zope.app import zapi
+from persistent import Persistent
+from zope.app.container.contained import Contained
+from zope.interface import implements
+
+from zope.interface import Interface
+from zope.app.pas.interfaces import IAuthenticationPlugin
+from ldapadapter.interfaces import ILDAPAdapter
+from interfaces import ILDAPAuthentication
+
+from ldap.filter import filter_format
+from ldapadapter.exceptions import ServerDown
+from ldapadapter.exceptions import InvalidCredentials
+from ldapadapter.exceptions import NoSuchObject
+
+
+class LDAPAuthentication(Persistent, Contained):
+    """A Persistent LDAP Authentication plugin for PAS.
+
+    An authentication plugin is configured using an LDAP Adapter that
+    will be use to check user credentials that encapsulates server
+    information, and additional authentication-specific informations.
+    """
+
+    implements(IAuthenticationPlugin, ILDAPAuthentication)
+
+    adapterName = ''
+    searchBase = ''
+    searchScope = ''
+    loginAttribute = ''
+    principalIdPrefix = ''
+    idAttribute = ''
+
+    def __init__(self):
+        pass
+
+    def getLDAPAdapter(self):
+        """Get the LDAP adapter according to our configuration.
+
+        Returns None if adapter connection is configured or available.
+        """
+        da = zapi.queryUtility(ILDAPAdapter, self.adapterName)
+        return da
+
+    def authenticateCredentials(self, credentials):
+        r"""See zope.app.pas.interfaces.IAuthenticationPlugin.
+
+        An LDAP Adapter has to be registered, we'll use a fake one
+        (registered by the test framework).
+
+        >>> auth = LDAPAuthentication()
+        >>> auth.adapterName = 'fake_ldap_adapter'
+        >>> auth.searchBase = 'dc=test'
+        >>> auth.searchScope = 'sub'
+        >>> auth.loginAttribute = 'cn'
+        >>> auth.principalIdPrefix = ''
+        >>> auth.idAttribute = 'uid'
+        >>> da = auth.getLDAPAdapter()
+        >>> authCreds = auth.authenticateCredentials
+
+        Incorrect credentials types are rejected.
+
+        >>> authCreds(123) is None
+        True
+        >>> authCreds({'glop': 'bzz'}) is None
+        True
+
+        You cannot authenticate if the search returns several results.
+
+        >>> len(da.connect().search('dc=test', 'sub', '(cn=many)')) > 1
+        True
+        >>> authCreds({'login': 'many', 'password': 'p'}) is None
+        True
+
+        You cannot authenticate if the search returns nothing.
+
+        >>> conn = da.connect()
+        >>> len(conn.search('dc=test', 'sub', '(cn=none)')) == 0
+        True
+        >>> authCreds({'login': 'none', 'password': 'p'}) is None
+        True
+
+        You cannot authenticate with the wrong password.
+
+        >>> authCreds({'login': 'ok', 'password': 'hm'}) is None
+        True
+
+        Authentication succeeds if you provide the correct password.
+
+        >>> authCreds({'login': 'ok', 'password': '42pw'})
+        (u'42', {'login': 'ok'})
+
+        The id returned comes from a configurable attribute, and can be
+        prefixed so that it is unique.
+
+        >>> auth.principalIdPrefix = 'ldap.'
+        >>> auth.idAttribute = 'cn'
+        >>> authCreds({'login': 'ok', 'password': '42pw'})
+        (u'ldap.ok', {'login': 'ok'})
+
+        The id attribute 'dn' can be specified to use the full dn as id.
+
+        >>> auth.idAttribute = 'dn'
+        >>> authCreds({'login': 'ok', 'password': '42pw'})
+        (u'ldap.uid=42,dc=test', {'login': 'ok'})
+
+        If the id attribute returns several values, the first one is
+        used.
+
+        >>> auth.idAttribute = 'mult'
+        >>> conn.search('dc=test', 'sub', '(cn=ok)')[0][1]['mult']
+        [u'm1', u'm2']
+        >>> authCreds({'login': 'ok', 'password': '42pw'})
+        (u'ldap.m1', {'login': 'ok'})
+
+        Authentication fails if the id attribute is not present:
+
+        >>> auth.idAttribute = 'nonesuch'
+        >>> conn.search('dc=test', 'sub', '(cn=ok)')[0][1]['nonesuch']
+        Traceback (most recent call last):
+        ...
+        KeyError: 'nonesuch'
+        >>> authCreds({'login': 'ok', 'password': '42pw'}) is None
+        True
+
+        You cannot authenticate if the server to which the adapter
+        connects is down.
+
+        >>> da._isDown = True
+        >>> authCreds({'login': 'ok', 'password': '42pw'}) is None
+        True
+        >>> da._isDown = False
+
+        You cannot authenticate if the plugin has a bad configuration.
+
+        >>> auth.searchBase = 'dc=bzzt'
+        >>> authCreds({'login': 'ok', 'password': '42pw'}) is None
+        True
+        """
+
+        if not isinstance(credentials, dict):
+            return None
+        if not ('login' in credentials and 'password' in credentials):
+            return None
+
+        da = self.getLDAPAdapter()
+        if da is None:
+            return None
+
+        login = credentials['login']
+        password = credentials['password']
+
+        # Search for a matching entry.
+        try:
+            conn = da.connect()
+        except ServerDown:
+            return None
+        filter = filter_format('(%s=%s)', (self.loginAttribute, login))
+        try:
+            res = conn.search(self.searchBase, self.searchScope, filter=filter)
+        except NoSuchObject:
+            return None
+        if len(res) != 1:
+            # Search returned no result or too many.
+            return None
+        dn, entry = res[0]
+
+        # Find the id we'll return.
+        id_attr = self.idAttribute
+        if id_attr == 'dn':
+            id = dn
+        elif entry.get(id_attr):
+            id = entry[id_attr][0]
+        else:
+            return None
+        id = self.principalIdPrefix + id
+
+        # Check authentication.
+        try:
+            conn = da.connect(dn, password)
+        except InvalidCredentials:
+            return None
+
+        return id, {'login': login}


Property changes on: ldappas/trunk/authentication.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: ldappas/trunk/configure.zcml
===================================================================
--- ldappas/trunk/configure.zcml	2004-10-13 14:13:19 UTC (rev 28081)
+++ ldappas/trunk/configure.zcml	2004-10-13 14:15:14 UTC (rev 28082)
@@ -0,0 +1,50 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser"
+    i18n_domain="ldappas"
+    >
+
+  <localUtility class=".authentication.LDAPAuthentication">
+
+    <implements
+        interface="zope.app.annotation.interfaces.IAttributeAnnotatable"
+        />
+
+    <require
+        permission="zope.ManageServices"
+        interface="zope.app.pas.interfaces.IAuthenticationPlugin"
+        />
+
+    <require
+        permission="zope.ManageServices"
+        interface="ldappas.interfaces.ILDAPAuthentication"
+        set_schema="ldappas.interfaces.ILDAPAuthentication"
+        />
+
+  </localUtility>
+
+  <browser:addMenuItem
+      title="PAS LDAP Authentication Plugin"
+      description="A PAS LDAP Authentication Plugin"
+      class=".authentication.LDAPAuthentication"
+      permission="zope.ManageServices"
+      view="addPASLDAPAuthenticationPlugin"
+      />
+
+  <browser:addform
+      schema="ldappas.interfaces.ILDAPAuthentication"
+      label="Add a PAS LDAP Authentication Plugin"
+      content_factory=".authentication.LDAPAuthentication"
+      name="addPASLDAPAuthenticationPlugin"
+      permission="zope.ManageServices"
+      />
+
+  <browser:editform
+      schema="ldappas.interfaces.ILDAPAuthentication"
+      name="edit.html"
+      menu="zmi_views"
+      title="Edit"
+      permission="zope.ManageServices"
+      />
+
+</configure>


Property changes on: ldappas/trunk/configure.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: ldappas/trunk/interfaces.py
===================================================================
--- ldappas/trunk/interfaces.py	2004-10-13 14:13:19 UTC (rev 28081)
+++ ldappas/trunk/interfaces.py	2004-10-13 14:15:14 UTC (rev 28082)
@@ -0,0 +1,57 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation 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.
+#
+##############################################################################
+"""LDAP PAS Plugin interfaces
+
+$Id$
+"""
+
+import re
+from zope.app.i18n import ZopeMessageIDFactory as _
+from zope.interface import Interface
+from zope.schema import TextLine
+
+class ILDAPAuthentication(Interface):
+    adapterName = TextLine(
+        title=_("LDAP Adapter"),
+        default=u'ldapadapter',
+        required=True,
+        )
+    searchBase = TextLine(
+        title=_("Search base"),
+        default=u'dc=example,dc=org',
+        required=True,
+        )
+    searchScope = TextLine(
+        title=_("Search scope"),
+        default=u'sub',
+        required=True,
+        )
+    loginAttribute = TextLine(
+        title=_("Login attribute"),
+        constraint=re.compile("[a-zA-Z][-a-zA-Z0-9]*$").match,
+        default=u'uid',
+        required=True,
+        )
+    principalIdPrefix = TextLine(
+        title=_("Principal id prefix"),
+        default=u'ldap.',
+        required=False,
+        )
+    idAttribute = TextLine(
+        title=_("Id attribute"),
+        constraint=re.compile("[a-zA-Z][-a-zA-Z0-9]*$").match,
+        default=u'uid',
+        required=True,
+        )
+    #searchObjectClasses


Property changes on: ldappas/trunk/interfaces.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: ldappas/trunk/ldappas-configure.zcml.in
===================================================================
--- ldappas/trunk/ldappas-configure.zcml.in	2004-10-13 14:13:19 UTC (rev 28081)
+++ ldappas/trunk/ldappas-configure.zcml.in	2004-10-13 14:15:14 UTC (rev 28082)
@@ -0,0 +1,5 @@
+<configure xmlns='http://namespaces.zope.org/zope'>
+
+  <include package="ldappas" />
+
+</configure>


Property changes on: ldappas/trunk/ldappas-configure.zcml.in
___________________________________________________________________
Name: svn:eol-style
   + native

Added: ldappas/trunk/tests/__init__.py
===================================================================
--- ldappas/trunk/tests/__init__.py	2004-10-13 14:13:19 UTC (rev 28081)
+++ ldappas/trunk/tests/__init__.py	2004-10-13 14:15:14 UTC (rev 28082)
@@ -0,0 +1,17 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation 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
+#
+##############################################################################
+"""LDAP PAS Plugins test packages.
+
+$Id$
+"""


Property changes on: ldappas/trunk/tests/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: ldappas/trunk/tests/test_authentication.py
===================================================================
--- ldappas/trunk/tests/test_authentication.py	2004-10-13 14:13:19 UTC (rev 28081)
+++ ldappas/trunk/tests/test_authentication.py	2004-10-13 14:15:14 UTC (rev 28082)
@@ -0,0 +1,80 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation 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.
+#
+##############################################################################
+"""LDAP PAS Plugins tests
+
+$Id$
+"""
+
+import sys
+import unittest
+from zope.testing import doctest
+
+from zope.interface import implements
+from zope.app import zapi
+from zope.app.tests import setup
+from zope.app.servicenames import Utilities
+from zope.app.utility import LocalUtilityService
+
+from ldapadapter.interfaces import ILDAPAdapter
+from ldapadapter.exceptions import ServerDown
+from ldapadapter.exceptions import InvalidCredentials
+from ldapadapter.exceptions import NoSuchObject
+
+class FakeLDAPAdapter:
+    implements(ILDAPAdapter)
+    _isDown = False
+    def connect(self, dn=None, password=None):
+        if self._isDown:
+            raise ServerDown
+        if not dn and not password:
+            return FakeLDAPConnection()
+        if dn == 'uid=42,dc=test' and password == '42pw':
+            return FakeLDAPConnection()
+        raise InvalidCredentials
+
+class FakeLDAPConnection:
+    def search(self, base, scope='sub', filter='(objectClass=*)', attrs=[]):
+        if base.endswith('dc=bzzt'):
+            raise NoSuchObject
+        if filter == '(cn=many)':
+            return [(u'uid=1,dc=test', {'cn': [u'many']}),
+                    (u'uid=2,dc=test', {'cn': [u'many']})]
+        if filter == '(cn=none)':
+            return []
+        if filter == '(cn=ok)':
+            return [(u'uid=42,dc=test', {'cn': [u'ok'],
+                                         'uid': [u'42'],
+                                         'mult': [u'm1', u'm2'],
+                                         })]
+        return []
+
+def setUp(test):
+    root = setup.placefulSetUp(site=True)
+    sm = zapi.getServices(root)
+    setup.addService(sm, Utilities, LocalUtilityService())
+    setup.addUtility(sm, 'fake_ldap_adapter', ILDAPAdapter,
+                     FakeLDAPAdapter())
+
+def tearDown(test):
+    setup.placefulTearDown()
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocTestSuite('ldappas.authentication',
+                             setUp=setUp, tearDown=tearDown,
+                             ),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')


Property changes on: ldappas/trunk/tests/test_authentication.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native



More information about the Zope-CVS mailing list