[Zope3-checkins] CVS: Zope3/src/zope/app/services/pluggableauth - __init__.py:1.2 configure.zcml:1.2

Chris McDonough chrism@zope.com
Mon, 23 Jun 2003 18:46:48 -0400


Update of /cvs-repository/Zope3/src/zope/app/services/pluggableauth
In directory cvs.zope.org:/tmp/cvs-serv30079/src/zope/app/services/pluggableauth

Added Files:
	__init__.py configure.zcml 
Log Message:
Merge pluggable_authentication_service-branch to HEAD.

You can now use a pluggable authentication service in place of a simple authentication service.


=== Zope3/src/zope/app/services/pluggableauth/__init__.py 1.1 => 1.2 ===
--- /dev/null	Mon Jun 23 18:46:48 2003
+++ Zope3/src/zope/app/services/pluggableauth/__init__.py	Mon Jun 23 18:46:17 2003
@@ -0,0 +1,497 @@
+##############################################################################
+#
+# Copyright (c) 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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.
+#
+##############################################################################
+"""Pluggable Authentication service implementation.
+
+$Id$
+"""
+
+from __future__ import generators
+import random
+import sys
+import datetime
+from persistence import Persistent
+from zodb.btrees.IOBTree import IOBTree
+from zodb.btrees.OIBTree import OIBTree
+from zope.interface import implements
+from zope.component import getAdapter, queryAdapter
+from zope.context.wrapper import Wrapper
+from zope.context import getWrapperData
+from zope.app.services.servicenames import Authentication
+from zope.component.interfaces import IViewFactory
+from zope.app.container.btree import BTreeContainer
+from zope.app.container.ordered import OrderedContainer
+from zope.app.interfaces.container import IContainer
+from zope.app.interfaces.container import IContainerNamesContainer
+from zope.app.interfaces.container import IOrderedContainer
+from zope.app.interfaces.container import IAddNotifiable
+from zope.app.interfaces.services.pluggableauth import IUserSchemafied
+from zope.app.interfaces.security import ILoginPassword
+from zope.app.interfaces.services.pluggableauth \
+     import IPluggableAuthenticationService
+from zope.app.interfaces.services.pluggableauth import IPrincipalSource
+from zope.app.interfaces.services.pluggableauth import IReadPrincipalSource,\
+     ILoginPasswordPrincipalSource, IWritePrincipalSource
+from zope.app.interfaces.services.service import ISimpleService
+from zope.app.component.nextservice import queryNextService
+from zope.app.zapi import queryView
+from zope.app.traversing import getPath
+from zope.context import ContextMethod
+from zope.exceptions import NotFoundError
+import random
+
+def gen_key():
+    """Return a random int (1, MAXINT), suitable for use as a BTree key."""
+
+    return random.randint(0,sys.maxint-1)
+
+class PluggableAuthenticationService(OrderedContainer):
+
+    implements(IPluggableAuthenticationService, ISimpleService,
+               IOrderedContainer, IAddNotifiable)
+
+    def __init__(self, earmark=None):
+        self.earmark = earmark
+        OrderedContainer.__init__(self)
+
+    def afterAddHook(self, object, container):
+        """ See IAddNotifiable. """
+        if self.earmark is None:
+            # XXX need to generate a better earmark that's more likely
+            # to be unique and which you can use to actually identify
+            # the auth service in error messages
+            self.earmark = str(random.randint(0, sys.maxint-1))
+
+    afterAddHook = ContextMethod(afterAddHook)
+
+    def authenticate(self, request):
+        """ See IAuthenticationService. """
+        for ps_key, ps in self.items():
+            loginView = queryView(ps, "login", request)
+            if loginView is not None:
+                principal = loginView.authenticate()
+                if principal is not None:
+                    id = '\t'.join((self.earmark, ps_key,
+                                    str(principal.getId())))
+                    return PrincipalWrapper(principal, self, id=id)
+
+        next = queryNextService(self, Authentication, None)
+        if next is not None:
+            return next.authenticate(request)
+
+        return None
+    authenticate = ContextMethod(authenticate)
+
+    def unauthenticatedPrincipal(self):
+        """ See IAuthenticationService. """
+        return None # XXX Do we need to implement or use another?
+
+    def unauthorized(self, id, request):
+        """ See IAuthenticationService. """
+
+        next = queryNextService(self, Authentication, None)
+        if next is not None:
+            return next.unauthorized(id, request)
+
+        return None
+    unauthorized = ContextMethod(unauthorized)
+
+    def getPrincipal(self, id):
+        """ See IAuthenticationService.
+
+        For this implementation, an 'id' is a string which can be
+        split into a 3-tuple by splitting on newline characters.  The
+        three tuple consists of (auth_service_earmark,
+        principal_source_id, principal_id).
+
+        """
+        next = None
+        
+        try:
+            auth_svc_earmark, principal_src_id, principal_id = id.split('\t',2)
+        except (TypeError, ValueError, AttributeError):
+            auth_svc_earmark, principal_src_id, principal_id = None, None, None
+            next = queryNextService(self, Authentication, None)
+
+        if auth_svc_earmark != self.earmark:
+            next = queryNextService(self, Authentication, None)
+        
+        if next is not None:
+            return next.getPrincipal(id)
+
+        source = self.get(principal_src_id)
+        if source is None:
+            raise NotFoundError
+        p = source.getPrincipal(principal_id)
+        return PrincipalWrapper(p, self, id=id)
+
+    getPrincipal = ContextMethod(getPrincipal)
+
+    def getPrincipals(self, name):
+        """ See IAuthenticationService. """
+
+        for ps_key, ps in self.items():
+            for p in ps.getPrincipals(name):
+                id = '\t'.join((self.earmark, ps_key, str(p.getId())))
+                yield PrincipalWrapper(p, self, id=id)
+
+        next = queryNextService(self, Authentication, None)
+        if next is not None:
+            for p in next.getPrincipals(name):
+                yield p
+    getPrincipals = ContextMethod(getPrincipals)
+
+    def addPrincipalSource(self, id, principal_source):
+        """ See IPluggableAuthenticationService.
+
+        >>> pas = PluggableAuthenticationService()
+        >>> sps = BTreePrincipalSource()
+        >>> pas.addPrincipalSource('simple', sps)
+        >>> sps2 = BTreePrincipalSource()
+        >>> pas.addPrincipalSource('not_quite_so_simple', sps2)
+        >>> pas.keys()
+        ['simple', 'not_quite_so_simple']
+        """
+
+        if not IReadPrincipalSource.isImplementedBy(principal_source):
+            raise TypeError("Source must implement IReadPrincipalSource")
+        self.setObject(id, principal_source)
+        
+    def removePrincipalSource(self, id):
+        """ See IPluggableAuthenticationService.
+
+        >>> pas = PluggableAuthenticationService()
+        >>> sps = BTreePrincipalSource()
+        >>> pas.addPrincipalSource('simple', sps)
+        >>> sps2 = BTreePrincipalSource()
+        >>> pas.addPrincipalSource('not_quite_so_simple', sps2)
+        >>> sps3 = BTreePrincipalSource()
+        >>> pas.addPrincipalSource('simpler', sps3)
+        >>> pas.keys()
+        ['simple', 'not_quite_so_simple', 'simpler']
+        >>> pas.removePrincipalSource('not_quite_so_simple')
+        >>> pas.keys()
+        ['simple', 'simpler']
+        """
+            
+        del self[id]
+
+class BTreePrincipalSource(Persistent):
+    """An efficient, scalable provider of Authentication Principals."""
+
+    implements(ILoginPasswordPrincipalSource, IPrincipalSource,
+               IContainerNamesContainer)
+
+    def __init__(self):
+
+        self._principals_by_number = IOBTree()
+        self._numbers_by_login = OIBTree()
+
+    # IContainer-related methods
+
+    def __delitem__(self, key):
+        """ See IContainer.
+
+        >>> sps = BTreePrincipalSource()
+        >>> prin = SimplePrincipal('fred', 'fred', '123')
+        >>> sps.setObject('fred', prin)
+        'fred'
+        >>> int(sps.get('fred') == prin)
+        1
+        >>> del sps['fred']
+        >>> int(sps.get('fred') == prin)
+        0
+        
+        """
+        number = self._numbers_by_login[key]
+
+        del self._principals_by_number[number]
+        del self._numbers_by_login[key]
+
+    def setObject(self, id, object):
+        """ See IContainerNamesContainer
+
+        >>> sps = BTreePrincipalSource()
+        >>> prin = SimplePrincipal('gandalf', 'shadowfax')
+        >>> dummy = sps.setObject('doesntmatter', prin)
+        >>> sps.get('doesntmatter')
+        """
+
+        store = self._principals_by_number
+
+        key = gen_key()
+        while not store.insert(key, object):
+            key = gen_key()
+
+        object.id = key
+        self._numbers_by_login[object.login] = key
+
+        return object.login
+
+    def keys(self):
+        """ See IContainer.
+
+        >>> sps = BTreePrincipalSource()
+        >>> sps.keys()
+        []
+        >>> prin = SimplePrincipal('arthur', 'tea')
+        >>> key = sps.setObject('doesntmatter', prin)
+        >>> sps.keys()
+        ['arthur']
+        >>> prin = SimplePrincipal('ford', 'towel')
+        >>> key = sps.setObject('doesntmatter', prin)
+        >>> sps.keys()
+        ['arthur', 'ford']
+        """
+
+        return list(self._numbers_by_login.keys())
+
+    def __iter__(self):
+        """ See IContainer.
+
+        >>> sps = BTreePrincipalSource()
+        >>> sps.keys()
+        []
+        >>> prin = SimplePrincipal('trillian', 'heartOfGold')
+        >>> key = sps.setObject('doesntmatter', prin)
+        >>> prin = SimplePrincipal('zaphod', 'gargleblaster')
+        >>> key = sps.setObject('doesntmatter', prin)
+        >>> [i for i in sps]
+        ['trillian', 'zaphod']
+        """
+
+        return iter(self.keys())
+
+    def __getitem__(self, key):
+        """ See IContainer
+
+        >>> sps = BTreePrincipalSource()
+        >>> prin = SimplePrincipal('gag', 'justzisguy')
+        >>> key = sps.setObject('doesntmatter', prin)
+        >>> sps['gag'].login
+        'gag'
+        """
+
+        number = self._numbers_by_login[key]
+        return self._principals_by_number[number]
+
+    def get(self, key, default=None):
+        """ See IContainer
+
+        >>> sps = BTreePrincipalSource()
+        >>> prin = SimplePrincipal(1, 'slartibartfast', 'fjord')
+        >>> key = sps.setObject('slartibartfast', prin)
+        >>> principal = sps.get('slartibartfast')
+        >>> sps.get('marvin', 'No chance, dude.')
+        'No chance, dude.'
+        """
+
+        try:
+            number = self._numbers_by_login[key]
+        except KeyError:
+            return default
+
+        return self._principals_by_number[number]
+
+    def values(self):
+        """ See IContainer.
+
+        >>> sps = BTreePrincipalSource()
+        >>> sps.keys()
+        []
+        >>> prin = SimplePrincipal('arthur', 'tea')
+        >>> key = sps.setObject('doesntmatter', prin)
+        >>> [user.login for user in sps.values()]
+        ['arthur']
+        >>> prin = SimplePrincipal('ford', 'towel')
+        >>> key = sps.setObject('doesntmatter', prin)
+        >>> [user.login for user in sps.values()]
+        ['arthur', 'ford']
+        """
+
+        return [self._principals_by_number[n]
+                for n in self._numbers_by_login.values()]
+
+    def __len__(self):
+        """ See IContainer
+
+        >>> sps = BTreePrincipalSource()
+        >>> int(len(sps) == 0)
+        1
+        >>> prin = SimplePrincipal(1, 'trillian', 'heartOfGold')
+        >>> key = sps.setObject('trillian', prin)
+        >>> int(len(sps) == 1)
+        1
+        """
+
+        return len(self._principals_by_number)
+
+    def items(self):
+        """ See IContainer.
+
+        >>> sps = BTreePrincipalSource()
+        >>> sps.keys()
+        []
+        >>> prin = SimplePrincipal('zaphod', 'gargleblaster')
+        >>> key = sps.setObject('doesntmatter', prin)
+        >>> [(k, v.login) for k, v in sps.items()]
+        [('zaphod', 'zaphod')]
+        >>> prin = SimplePrincipal('marvin', 'paranoid')
+        >>> key = sps.setObject('doesntmatter', prin)
+        >>> [(k, v.login) for k, v in sps.items()]
+        [('marvin', 'marvin'), ('zaphod', 'zaphod')]
+        """
+
+        # We're being expensive here (see values() above) for convenience
+        return [(p.login, p) for p in self.values()]
+
+    def __contains__(self, key):
+        """ See IContainer.
+
+        >>> sps = BTreePrincipalSource()
+        >>> prin = SimplePrincipal('slinkp', 'password')
+        >>> key = sps.setObject('doesntmatter', prin)
+        >>> int('slinkp' in sps)
+        1
+        >>> int('desiato' in sps)
+        0
+        """
+        return self._numbers_by_login.has_key(key)
+
+    has_key = __contains__
+
+    # PrincipalSource-related methods
+
+    def getPrincipal(self, id):
+        """ See IReadPrincipalSource.
+
+        'id' is the id as returned by principal.getId(),
+        not a login.
+
+        """
+        try:
+            id = int(id)
+        except TypeError:
+            raise NotFoundError
+        try:
+            return self._principals_by_number[id]
+        except KeyError:
+            raise NotFoundError
+
+    def getPrincipals(self, name):
+        """ See IReadPrincipalSource.
+
+        >>> sps = BTreePrincipalSource()
+        >>> prin1 = SimplePrincipal('gandalf', 'shadowfax')
+        >>> dummy = sps.setObject('doesntmatter', prin1)
+        >>> prin1 = SimplePrincipal('frodo', 'ring')
+        >>> dummy = sps.setObject('doesntmatter', prin1)
+        >>> prin1 = SimplePrincipal('pippin', 'pipe')
+        >>> dummy = sps.setObject('doesntmatter', prin1)
+        >>> prin1 = SimplePrincipal('sam', 'garden')
+        >>> dummy = sps.setObject('doesntmatter', prin1)
+        >>> prin1 = SimplePrincipal('merry', 'food')
+        >>> dummy = sps.setObject('doesntmatter', prin1)
+        >>> [p.login for p in sps.getPrincipals('a')]
+        ['gandalf', 'sam']
+        >>> [p.login for p in sps.getPrincipals('')]
+        ['frodo', 'gandalf', 'merry', 'pippin', 'sam']
+        >>> [p.login for p in sps.getPrincipals('sauron')]
+        []
+        """
+
+        for k in self.keys():
+            if k.find(name) != -1:
+                yield self[k]
+
+    def authenticate(self, login, password):
+        """ See ILoginPasswordPrincipalSource. """
+        number = self._numbers_by_login.get(login)
+        if number is None:
+            return
+        user = self._principals_by_number[number]
+        if user.validate(password):
+            return user
+
+class SimplePrincipal(Persistent):
+    """A no-frills IUserSchemafied implementation."""
+
+    implements(IUserSchemafied)
+
+    def __init__(self, login, password, title='', description=''):
+
+        self.id = ''
+        self.login = login
+        self.password = password
+        self.title = title
+        self.description = description
+
+    def getId(self):
+        """ See IPrincipal. """
+        return self.id
+
+    def getTitle(self):
+        """ See IPrincipal. """
+        return self.title
+
+    def getDescription(self):
+        """ See IPrincipal. """
+        return self.description
+
+    def getLogin(self):
+        """ See IReadUser. """
+        return self.login
+    
+    def validate(self, test_password):
+        """ See IReadUser.
+
+        >>> pal = SimplePrincipal('gandalf', 'shadowfax', 'The Grey Wizard',
+        ...                       'Cool old man with neato fireworks. '
+        ...                       'Has a nice beard.')
+        >>> int(pal.validate('shdaowfax'))
+        0
+        >>> int(pal.validate('shadowfax'))
+        1
+        """
+
+        return test_password == self.password
+
+class PrincipalAuthenticationView:
+    implements(IViewFactory)
+    
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+        
+    def authenticate(self):
+        for interface in (ILoginPassword,):
+            a = queryAdapter(self.request, interface, None)
+            if a is None:
+                return
+            login = a.getLogin()
+            password = a.getPassword()
+
+            p = self.context.authenticate(login, password)
+            return p
+
+    
+class PrincipalWrapper(Wrapper):
+    """ A wrapper for a principal as returned from the authentication
+    service.  The id of the principal as returned by the wrapper is
+    a three-tuple instead of the integer id returned by the simple
+    principal."""
+    def getId(self):
+        """ Return the id as passed in to the wrapper """
+        return getWrapperData(self)['id']
+
+    


=== Zope3/src/zope/app/services/pluggableauth/configure.zcml 1.1 => 1.2 ===
--- /dev/null	Mon Jun 23 18:46:48 2003
+++ Zope3/src/zope/app/services/pluggableauth/configure.zcml	Mon Jun 23 18:46:17 2003
@@ -0,0 +1,75 @@
+<zopeConfigure xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser">
+
+<content class=".PluggableAuthenticationService">
+    <factory
+        id="zope.app.services.PluggableAuthenticationService"
+        permission="zope.ManageServices"
+        />
+    <require
+        permission="zope.ManageServices"
+        interface="
+  zope.app.interfaces.services.pluggableauth.IPluggableAuthenticationService"
+        />
+    <allow
+        interface="zope.app.interfaces.container.IReadContainer"
+        />
+    <require
+        permission="zope.ManageServices"
+        interface="zope.app.interfaces.container.IWriteContainer"
+        />
+    <require
+        permission="zope.ManageServices"
+        interface="zope.app.interfaces.container.IAddNotifiable"
+        />
+    <require
+        permission="zope.ManageServices"
+        interface="zope.app.interfaces.services.service.ISimpleService"
+        />
+</content>
+
+<content class=".BTreePrincipalSource">
+    <factory
+        id="zope.app.principalsources.BTreePrincipalSource"
+        permission="zope.ManageServices"
+        />
+    <allow
+        interface="zope.app.interfaces.container.IReadContainer"
+        />
+    <require
+        permission="zope.ManageServices"
+        interface="zope.app.interfaces.container.IWriteContainer"
+        />
+    <require
+        permission="zope.ManageServices"
+        interface="
+  zope.app.interfaces.services.pluggableauth.IWritePrincipalSource"
+        />
+    <allow
+        interface="
+  zope.app.interfaces.services.pluggableauth.IReadPrincipalSource"
+        />
+</content>
+
+<content class=".SimplePrincipal">
+    <factory
+        id="zope.app.principals.SimplePrincipal"
+        permission="zope.ManageServices"
+        />
+    <allow
+        interface="zope.app.interfaces.services.pluggableauth.IUserSchemafied"
+        />
+    <require
+        permission="zope.ManageServices"
+        set_schema="zope.app.interfaces.services.pluggableauth.IUserSchemafied"
+        />
+</content>
+
+<browser:view
+       name="login"
+       for="zope.app.interfaces.services.pluggableauth.IPrincipalSource"
+       class="zope.app.services.pluggableauth.PrincipalAuthenticationView"
+       permission="zope.Public"
+/> <!-- XXX why do we need a permission? -->
+
+</zopeConfigure>