[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