[Zope3-checkins] CVS: Zope3/src/zope/app/pluggableauth - README.txt:1.1 __init__.py:1.1 configure.zcml:1.1 interfaces.py:1.1

Stephan Richter srichter at cosmos.phy.tufts.edu
Wed Mar 10 12:56:37 EST 2004


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

Added Files:
	README.txt __init__.py configure.zcml interfaces.py 
Log Message:


Moved pluggable authentication service to zope.app.pluggableauth. Added module
aliases (tested) so that old services survive the change.


=== Added File Zope3/src/zope/app/pluggableauth/README.txt ===
$Id: README.txt,v 1.1 2004/03/10 17:56:32 srichter Exp $

The current implementation will be replaced. Following is design
I came up with together with Jim Fulton.
   -- itamar

Note that this design is implemented (in some form) by the pluggable
auth service. This document needs to be updated to reflect the final
implementation. 


Design notes for new AuthenticationService
==========================================

The service contains a list of user sources. They implement interfaces,
starting with:


 class IUserPassUserSource:
     """Authenticate using username and password."""
     def authenticate(username, password):
         "Returns boolean saying if such username/password pair exists"


 class IDigestSupportingUserSource(IUserPassUserSource):
     """Allow fetching password, which is required by digest auth methods"""
     def getPassword(username):
         "Return password for username"


etc.. Probably there will be others as well, for dealing with certificate
authentication and what not. Probably we need to expand above interfaces
to deal with principal titles and descriptions, and so on.

A login method - cookie auth, HTTP basic auth, digest auth, FTP auth,
is registered as a view on one of the above interfaces. 


  class ILoginMethodView:
        def authenticate():
             """Return principal for request, or None."""
        def unauthorized():
             """Tell request that a login is required."""


The authentication service is then implemented something like this:


 class AuthenticationService:
     def authenticate(self, request):
         for us in self.userSources:
              loginView = getView(self, us, "login", request)
              principal = loginView.authenticate()
              if principal is not None:
                  return principal
     def unauthorized(self, request):
         loginView = getView(self, self.userSources[0], request)
         loginView.unauthorized()


=== Added File Zope3/src/zope/app/pluggableauth/__init__.py ===
##############################################################################
#
# 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: __init__.py,v 1.1 2004/03/10 17:56:32 srichter Exp $
"""
import random
import sys
import time
import random
import zope.schema

from warnings import warn
from persistent import Persistent
from BTrees.IOBTree import IOBTree
from BTrees.OIBTree import OIBTree

from zope.interface import implements
from zope.component.interfaces import IViewFactory
from zope.exceptions import NotFoundError

from zope.app import zapi
from zope.app.location import locate
from zope.app.traversing import getPath

from zope.app.container.interfaces import IOrderedContainer, IAddNotifiable
from zope.app.container.interfaces import IContainerNamesContainer, INameChooser
from zope.app.container.interfaces import IContained
from zope.app.container.constraints import ItemTypePrecondition
from zope.app.container.constraints import ContainerTypesConstraint
from zope.app.container.contained import Contained, setitem, uncontained
from zope.app.container.ordered import OrderedContainer

from zope.app.services.servicenames import Authentication
from zope.app.security.interfaces import ILoginPassword
from zope.app.interfaces.services.service import ISimpleService
from zope.app.component.nextservice import queryNextService

from interfaces import IUserSchemafied, IPluggableAuthenticationService
from interfaces import \
     IPrincipalSource, ILoginPasswordPrincipalSource, IContainerPrincipalSource

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
        # The earmark is used as a token which can uniquely identify
        # this authentication service instance even if the service moves
        # from place to place within the same context chain or is renamed.
        # It is included in principal ids of principals which are obtained
        # from this auth service, so code which dereferences a principal
        # (like getPrincipal of this auth service) needs to take the earmark
        # into account. The earmark cannot change once it is assigned.  If it
        # does change, the system will not be able to dereference principal
        # references which embed the old earmark.
        OrderedContainer.__init__(self)

    def addNotify(self, event):
        """ See IAddNotifiable. """
        if self.earmark is None:
            # we manufacture what is intended to be a globally unique
            # earmark if one is not provided in __init__
            myname = zapi.name(self)
            rand_id = gen_key()
            t = int(time.time())
            self.earmark = '%s-%s-%s' % (myname, rand_id, t)

    def authenticate(self, request):
        """ See IAuthenticationService. """
        for ps_key, ps in self.items():
            loginView = zapi.queryView(ps, "login", request)
            if loginView is not None:
                principal = loginView.authenticate()
                if principal is not None:
                    return principal

        next = queryNextService(self, Authentication, None)
        if next is not None:
            return next.authenticate(request)

        return None

    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

    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 tab characters.  The
        three tuple consists of (auth_service_earmark,
        principal_source_id, principal_id).

        In the current strategy, the principal sources that are members
        of this authentication service cannot be renamed; if they are,
        principal references that embed the old name will not be
        dereferenceable.

        """

        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:
            # this is not our reference because its earmark doesnt match ours
            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, principal_src_id
        return source.getPrincipal(id)

    def getPrincipals(self, name):
        """ See IAuthenticationService. """

        for ps_key, ps in self.items():
            for p in ps.getPrincipals(name):
                yield p

        next = queryNextService(self, Authentication, None)
        if next is not None:
            for p in next.getPrincipals(name):
                yield p

    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 IPrincipalSource.providedBy(principal_source):
            raise TypeError("Source must implement IPrincipalSource")
        locate(principal_source, self, id)
        self[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 IBTreePrincipalSource(
    ILoginPasswordPrincipalSource,
    IContainerPrincipalSource,
    INameChooser,
    IContainerNamesContainer,
    ):

    def __setitem__(name, principal):
        """Add a principal

        The name must be the same as the principal login
        """

    __setitem__.precondition  = ItemTypePrecondition(IUserSchemafied)

class IBTreePrincipalSourceContained(IContained):

    __parent__ = zope.schema.Field(
        constraint = ContainerTypesConstraint(IBTreePrincipalSource),
        )

class BTreePrincipalSource(Persistent, Contained):
    """An efficient, scalable provider of Authentication Principals."""

    implements(IBTreePrincipalSource)

    def __init__(self):

        self._principals_by_number = IOBTree()
        self._numbers_by_login = OIBTree()

    # IContainer-related methods

    def __delitem__(self, login):
        """ See IContainer.

        >>> sps = BTreePrincipalSource()
        >>> prin = SimplePrincipal('fred', 'fred', '123')
        >>> sps['fred'] = prin
        >>> int(sps.get('fred') == prin)
        1
        >>> del sps['fred']
        >>> int(sps.get('fred') == prin)
        0

        """
        number = self._numbers_by_login[login]

        uncontained(self._principals_by_number[number], self, login)
        del self._principals_by_number[number]
        del self._numbers_by_login[login]

    def __setitem__(self, login, ob):
        """ See IContainerNamesContainer

        >>> sps = BTreePrincipalSource()
        >>> prin = SimplePrincipal('gandalf', 'shadowfax')
        >>> sps['doesntmatter'] = prin
        >>> sps.get('doesntmatter')
        """
        setitem(self, self.__setitem, login, ob)

    def __setitem(self, login, ob):
        store = self._principals_by_number

        key = gen_key()
        while not store.insert(key, ob):
            key = gen_key()

        ob.id = key
        self._numbers_by_login[ob.login] = key

    def keys(self):
        """ See IContainer.

        >>> sps = BTreePrincipalSource()
        >>> sps.keys()
        []
        >>> prin = SimplePrincipal('arthur', 'tea')
        >>> sps['doesntmatter'] = prin
        >>> sps.keys()
        ['arthur']
        >>> prin = SimplePrincipal('ford', 'towel')
        >>> sps['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')
        >>> sps['doesntmatter'] = prin
        >>> prin = SimplePrincipal('zaphod', 'gargleblaster')
        >>> sps['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')
        >>> sps['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')
        >>> sps['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')
        >>> sps['doesntmatter'] = prin
        >>> [user.login for user in sps.values()]
        ['arthur']
        >>> prin = SimplePrincipal('ford', 'towel')
        >>> sps['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')
        >>> sps['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')
        >>> sps['doesntmatter'] = prin
        >>> [(k, v.login) for k, v in sps.items()]
        [('zaphod', 'zaphod')]
        >>> prin = SimplePrincipal('marvin', 'paranoid')
        >>> sps['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')
        >>> sps['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 IPrincipalSource.

        'id' is the id as returned by principal.getId(),
        not a login.

        """

        id = id.split('\t')[2]
        id = int(id)

        try:
            return self._principals_by_number[id]
        except KeyError:
            raise NotFoundError, id

    def getPrincipals(self, name):
        """ See IPrincipalSource.

        >>> sps = BTreePrincipalSource()
        >>> prin1 = SimplePrincipal('gandalf', 'shadowfax')
        >>> sps['doesntmatter'] = prin1
        >>> prin1 = SimplePrincipal('frodo', 'ring')
        >>> sps['doesntmatter'] = prin1
        >>> prin1 = SimplePrincipal('pippin', 'pipe')
        >>> sps['doesntmatter'] = prin1
        >>> prin1 = SimplePrincipal('sam', 'garden')
        >>> sps['doesntmatter'] = prin1
        >>> prin1 = SimplePrincipal('merry', 'food')
        >>> sps['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.password == password:
            return user


    def checkName(self, name, object):
        """Check to make sure the name is valid

        Don't allow suplicate names:

        >>> sps = BTreePrincipalSource()
        >>> prin1 = SimplePrincipal('gandalf', 'shadowfax')
        >>> sps['gandalf'] = prin1
        >>> sps.checkName('gandalf', prin1)
        Traceback (most recent call last):
        ...
        LoginNameTaken: gandalf

        """
        if name in self._numbers_by_login:
            raise LoginNameTaken(name)

    def chooseName(self, name, object):
        """Choose a name for the principal

        Always choose the object's existing name:

        >>> sps = BTreePrincipalSource()
        >>> prin1 = SimplePrincipal('gandalf', 'shadowfax')
        >>> sps.chooseName(None, prin1)
        'gandalf'

        """
        return object.login

class LoginNameTaken(Exception):
    """A login name is in use
    """


class SimplePrincipal(Persistent, Contained):
    """A no-frills IUserSchemafied implementation."""

    implements(IUserSchemafied, IBTreePrincipalSourceContained)

    def __init__(self, login, password, title='', description=''):
        self._id = ''
        self.login = login
        self.password = password
        self.title = title
        self.description = description

    def _getId(self):
        source = self.__parent__
        auth = source.__parent__
        return "%s\t%s\t%s" %(auth.earmark, source.__name__, self._id)

    def _setId(self, id):
        self._id = id

    id = property(_getId, _setId)

    def getTitle(self):
        warn("Use principal.title instead of principal.getTitle().",
             DeprecationWarning, 2)
        return self.title

    def getDescription(self):
        warn("Use principal.description instead of principal.getDescription().",
             DeprecationWarning, 2)
        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.')
        >>> pal.validate('shdaowfax')
        False
        >>> pal.validate('shadowfax')
        True
        """
        return test_password == self.password

class PrincipalAuthenticationView:
    implements(IViewFactory)

    def __init__(self, context, request):
        self.context = context
        self.request = request

    def authenticate(self):
        # XXX we only handle requests which have basic auth credentials
        # in them currently (ILoginPassword-based requests)
        # If you want a different policy, you'll need to write and register
        # a different view, replacing this one.
        a = ILoginPassword(self.request, None)
        if a is None:
            return
        login = a.getLogin()
        password = a.getPassword()

        p = self.context.authenticate(login, password)
        return p


=== Added File Zope3/src/zope/app/pluggableauth/configure.zcml ===
<configure 
    xmlns="http://namespaces.zope.org/zope"
    xmlns:browser="http://namespaces.zope.org/browser">

  <!-- For backward compatibility -->

  <modulealias
      module="zope.app.pluggableauth"
      alias="zope.app.services.pluggableauth"
      />

  <modulealias
      module=".interfaces"
      alias="zope.app.interfaces.services.pluggableauth"
      />

  

  <content class=".PluggableAuthenticationService">
    <factory
        id="zope.app.services.PluggableAuthenticationService"
        />
    <require
        permission="zope.ManageServices"
        interface=".interfaces.IPluggableAuthenticationService"
        />
<!--
    <allow
        interface="zope.app.container.interfaces.IReadContainer"
        />

    <require
        permission="zope.ManageServices"
        interface="zope.app.container.interfaces.IWriteContainer"
        />
-->
    <require
        permission="zope.ManageServices"
        interface="zope.app.container.interfaces.IAddNotifiable"
        />
    <require
        permission="zope.ManageServices"
        interface="zope.app.interfaces.services.service.ISimpleService"
        />
  </content>

  <content class=".BTreePrincipalSource">
    <factory
        id="zope.app.principalsources.BTreePrincipalSource"
        />
    <allow
        interface="zope.app.container.interfaces.IReadContainer"
        />
    <require
        permission="zope.ManageServices"
        interface="zope.app.container.interfaces.IWriteContainer
                   zope.app.container.interfaces.INameChooser"
        />
    <allow
        interface=".interfaces.IPrincipalSource"
        />
  </content>

  <content class=".SimplePrincipal">
    <factory
        id="zope.app.principals.SimplePrincipal"
        />
    <allow
        interface=".interfaces.IUserSchemafied"
        />
    <require
        permission="zope.ManageServices"
        set_schema=".interfaces.IUserSchemafied"
        />
  </content>

  <browser:view
      name="login"
      for=".interfaces.ILoginPasswordPrincipalSource"
      class="zope.app.pluggableauth.PrincipalAuthenticationView"
      permission="zope.Public" />

  <include package=".browser" />

</configure>


=== Added File Zope3/src/zope/app/pluggableauth/interfaces.py ===
##############################################################################
#
# Copyright (c) 2003 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.

$Id: interfaces.py,v 1.1 2004/03/10 17:56:32 srichter Exp $
"""
from zope.app.i18n import ZopeMessageIDFactory as _
from zope.app.container.interfaces import IContainer, IContained
from zope.app.container.constraints import ItemTypePrecondition
from zope.app.container.constraints import ContainerTypesConstraint
from zope.app.security.interfaces import IAuthenticationService, IPrincipal
from zope.interface import Interface
from zope.schema import Text, TextLine, Password, Field

class IUserSchemafied(IPrincipal):
    """A User object with schema-defined attributes."""

    login = TextLine(
        title=_("Login"),
        description=_("The Login/Username of the user. "
                      "This value can change."),
        required=True)

    password = Password(
        title=_(u"Password"),
        description=_("The password for the user."),
        required=True)

    def validate(test_password):
        """Confirm whether 'password' is the password of the user."""


class IPrincipalSource(Interface):
    """A read-only source of IPrincipals.
    """

    def getPrincipal(id):
        """Get principal meta-data.

        Returns an object of type IPrincipal for the given principal
        id. A NotFoundError is raised if the principal cannot be
        found.

        Note that the id has three parts, separated by tabs.  The
        first two part are an authentication service id and a
        principal source id.  The pricipal source will typically need
        to remove the two leading parts from the id when doing it's
        own internal lookup.

        Note that the authentication service nearest to the requested
        resource is called. It is up to authentication service
        implementations to collaborate with services higher in the
        object hierarchy.
        """

    def getPrincipals(name):
        """Get principals with matching names.

        Get a iterable object with the principals with names that are
        similar to (e.g. contain) the given name.
        """


class IPluggableAuthenticationService(IAuthenticationService, IContainer):
    """An AuthenticationService that can contain multiple pricipal sources.
    """

    def __setitem__(id, principal_source):
        """Add to object"""
    __setitem__.precondition = ItemTypePrecondition(IPrincipalSource)
  
    def removePrincipalSource(id):
        """Remove a PrincipalSource.

        If id is not present, raise KeyError.
        """


class ILoginPasswordPrincipalSource(IPrincipalSource):
    """ A principal source which can authenticate a user given a
    login and a password """

    def authenticate(login, password):
        """ Return a principal matching the login/password pair.

        If there is no principal in this principal source which
        matches the login/password pair, return None.

        Note: A login is different than an id.  Principals may have
        logins that differ from their id.  For example, a user may
        have a login which is his email address.  He'd like to be able
        to change his login when his email address changes without
        effecting his security profile on the site.  """


class IContainerPrincipalSource(IPrincipalSource, IContained):
    """This is a marker interface for specifying principal sources that are
    also containers. """

    __parent__= Field(
        constraint = ContainerTypesConstraint(IPluggableAuthenticationService))




More information about the Zope3-Checkins mailing list