[Zope3-checkins] SVN: Zope3/trunk/src/zope/app/pas/ Initial
reinterpretation of the Zope 2 PAS for Zope 3
Jim Fulton
jim at zope.com
Mon Oct 4 15:33:52 EDT 2004
Log message for revision 27741:
Initial reinterpretation of the Zope 2 PAS for Zope 3
TODO:
- Searching
- Delegation to higher-level (less local) services
Changed:
A Zope3/trunk/src/zope/app/pas/
A Zope3/trunk/src/zope/app/pas/README.txt
A Zope3/trunk/src/zope/app/pas/__init__.py
A Zope3/trunk/src/zope/app/pas/interfaces.py
A Zope3/trunk/src/zope/app/pas/pas.py
A Zope3/trunk/src/zope/app/pas/tests.py
A Zope3/trunk/src/zope/app/pas/vocabularies.py
-=-
Added: Zope3/trunk/src/zope/app/pas/README.txt
===================================================================
--- Zope3/trunk/src/zope/app/pas/README.txt 2004-10-04 18:44:50 UTC (rev 27740)
+++ Zope3/trunk/src/zope/app/pas/README.txt 2004-10-04 19:33:52 UTC (rev 27741)
@@ -0,0 +1,496 @@
+================================
+Pluggable Authentication Service
+================================
+
+The Pluggable Authentication Service (PAS) provides a framework for
+authenticating principals and associating information with them. It
+uses a variety of different utilities, called plugins, and subscribers
+to get it's work done.
+
+Authentication
+==============
+
+The primary job of an authentication service is to authenticate
+principals. Given a request object, the authentication service
+returns a principal object, if it can. The PAS does this in two
+steps:
+
+1. It determines a principal ID based on authentication credentials
+ found in a request, and then
+
+2. It constructs a principal from the given ID, combining information
+ from a number of sources.
+
+It uses plug-ins in both phases of it's work. Plugins are named
+utilities that the service is configured to use in some order.
+
+In the first phase, the PAS iterates through a sequence of extractor
+plugins. From each plugin, it attempts to get a set of credentials.
+If it gets credentials, it iterates through a sequence of authentication
+plugins, trying to get a principal id for the given credentials. It
+continues this until it gets a principal id.
+
+Once it has a principal id, it begins the second phase. In the second
+phase, it iterates through a collection of principal-factory plugins until a
+plugin returns a principal object for given principal ID.
+
+When a factory creates a principal, it publishes a principal-created
+event. Subscribers to this event are responsible for adding data,
+especially groups, to the principal. Typically, if a subscriber adds
+data, it should also add corresponding interface declarations.
+
+Let's look at an example. We create a simple plugin that provides
+credential extraction:
+
+ >>> import zope.interface
+ >>> from zope.app.pas import interfaces
+
+ >>> class MyExtractor:
+ ...
+ ... zope.interface.implements(interfaces.IExtractionPlugin)
+ ...
+ ... def extractCredentials(self, request):
+ ... return request.get('credentials')
+
+We need to register this as a utility. Normally, we'd do this in
+ZCML. For the example here, we'll use the provideUtility function from
+`zope.app.tests.ztapi`:
+
+ >>> from zope.app.tests.ztapi import provideUtility
+ >>> provideUtility(interfaces.IExtractionPlugin, MyExtractor(), name='emy')
+
+Now we also create an authenticator plugin that knows about object 42:
+
+ >>> class Auth42:
+ ...
+ ... zope.interface.implements(interfaces.IAuthenticationPlugin)
+ ...
+ ... def authenticateCredentials(self, credentials):
+ ... if credentials == 42:
+ ... return '42', {'domain': 42}
+
+ >>> provideUtility(interfaces.IAuthenticationPlugin, Auth42(), name='a42')
+
+We provide a principal factory plugin:
+
+ >>> class Principal:
+ ...
+ ... description = title = ''
+ ...
+ ... def __init__(self, id):
+ ... self.id = id
+ ...
+ ... def __repr__(self):
+ ... return 'Principal(%r, %r)' % (self.id, self.title)
+
+ >>> from zope.event import notify
+ >>> class PrincipalFactory:
+ ...
+ ... zope.interface.implements(interfaces.IPrincipalFactoryPlugin)
+ ...
+ ... def createAuthenticatedPrincipal(self, id, info, request):
+ ... principal = Principal(id)
+ ... notify(interfaces.AuthenticatedPrincipalCreated(
+ ... principal, info, request))
+ ... return principal
+ ...
+ ... def createFoundPrincipal(self, id, info):
+ ... principal = Principal(id)
+ ... notify(interfaces.FoundPrincipalCreated(principal, info))
+ ... return principal
+
+ >>> provideUtility(interfaces.IPrincipalFactoryPlugin, PrincipalFactory(),
+ ... name='pf')
+
+Finally, we create a PAS instance:
+
+ >>> from zope.app import pas
+ >>> service = pas.PAS()
+
+Now, we'll create a request and try to authenticate:
+
+ >>> from zope.publisher.browser import TestRequest
+ >>> request = TestRequest(credentials=42)
+ >>> service.authenticate(request)
+
+We don't get anything. Why? Because we haven't configured the service
+to use our plugins. Let's fix that:
+
+ >>> service.extractors = ('emy', )
+ >>> service.authenticators = ('a42', )
+ >>> service.factories = ('pf', )
+ >>> principal = service.authenticate(request)
+ >>> principal
+ Principal('42', '')
+
+In addition to getting a principal, an IPASPrincipalCreated event will
+have been generated. We'll use an the testing event logging API to
+see that this is the case:
+
+ >>> from zope.app.event.tests.placelesssetup import getEvents, clearEvents
+
+ >>> [event] = getEvents(interfaces.IAuthenticatedPrincipalCreated)
+
+The event's principal is set to the principal:
+
+ >>> event.principal is principal
+ True
+
+its info is set to the information returned by the authenticator:
+
+ >>> event.info
+ {'domain': 42}
+
+and it's request set to the request we created:
+
+ >>> event.request is request
+ True
+
+Normally, we provide subscribers to these events that add additional
+information to the principal. For examples, we'll add one that sets
+the title to a repr of the event info:
+
+ >>> def add_info(event):
+ ... event.principal.title = `event.info`
+
+ >>> from zope.app.tests.ztapi import subscribe
+ >>> subscribe([interfaces.IPASPrincipalCreated], None, add_info)
+
+Now, if we authenticate a principal, its title will be set:
+
+ >>> service.authenticate(request)
+ Principal('42', "{'domain': 42}")
+
+We can supply multiple plugins. For example, let's override our
+authentication plugin:
+
+ >>> class AuthInt:
+ ...
+ ... zope.interface.implements(interfaces.IAuthenticationPlugin)
+ ...
+ ... def authenticateCredentials(self, credentials):
+ ... if isinstance(credentials, int):
+ ... return str(credentials), {'int': credentials}
+
+ >>> provideUtility(interfaces.IAuthenticationPlugin, AuthInt(), name='aint')
+
+If we put it before the original authenticator:
+
+ >>> service.authenticators = 'aint', 'a42'
+
+Then it will override the original:
+
+ >>> service.authenticate(request)
+ Principal('42', "{'int': 42}")
+
+But if we put it after, the original will be used:
+
+ >>> service.authenticators = 'a42', 'aint'
+ >>> service.authenticate(request)
+ Principal('42', "{'domain': 42}")
+
+But we'll fall back to the new one:
+
+ >>> request = TestRequest(credentials=1)
+ >>> service.authenticate(request)
+ Principal('1', "{'int': 1}")
+
+As with with authenticators, we can specify multiple extractors:
+
+ >>> class OddExtractor:
+ ...
+ ... zope.interface.implements(interfaces.IExtractionPlugin)
+ ...
+ ... def extractCredentials(self, request):
+ ... credentials = request.get('credentials')
+ ... if isinstance(credentials, int) and (credentials%2):
+ ... return 1
+
+ >>> provideUtility(interfaces.IExtractionPlugin, OddExtractor(), name='eodd')
+ >>> service.extractors = 'eodd', 'emy'
+
+ >>> request = TestRequest(credentials=41)
+ >>> service.authenticate(request)
+ Principal('1', "{'int': 1}")
+
+ >>> request = TestRequest(credentials=42)
+ >>> service.authenticate(request)
+ Principal('42', "{'domain': 42}")
+
+And we can specify multiple factories:
+
+ >>> class OddPrincipal(Principal):
+ ...
+ ... def __repr__(self):
+ ... return 'OddPrincipal(%r, %r)' % (self.id, self.title)
+
+ >>> class OddFactory:
+ ...
+ ... zope.interface.implements(interfaces.IPrincipalFactoryPlugin)
+ ...
+ ... def createAuthenticatedPrincipal(self, id, info, request):
+ ... i = info.get('int')
+ ... if not (i and (i%2)):
+ ... return None
+ ... principal = OddPrincipal(id)
+ ... notify(interfaces.AuthenticatedPrincipalCreated(
+ ... principal, info, request))
+ ... return principal
+ ...
+ ... def createFoundPrincipal(self, id, info):
+ ... i = info.get('int')
+ ... if not (i and (i%2)):
+ ... return None
+ ... principal = OddPrincipal(id)
+ ... notify(interfaces.FoundPrincipalCreated(
+ ... principal, info))
+ ... return principal
+
+ >>> provideUtility(interfaces.IPrincipalFactoryPlugin, OddFactory(),
+ ... name='oddf')
+
+ >>> service.factories = 'oddf', 'pf'
+
+ >>> request = TestRequest(credentials=41)
+ >>> service.authenticate(request)
+ OddPrincipal('1', "{'int': 1}")
+
+ >>> request = TestRequest(credentials=42)
+ >>> service.authenticate(request)
+ Principal('42', "{'domain': 42}")
+
+In this example, we used the supplemental information to get the
+integer credentials. It's common for factories to decide whether they
+should be used depending on supplemental information. Factories
+should not try to inspect the principal ids. Why? Because, as we'll
+see later, the PAS may modify ids before giving them to factories.
+Similarly, subscribers should use the supplemental information for any
+data they need.
+
+Get a principal given an id
+===========================
+
+We can ask the PAS for a principal, given an id.
+
+To do this, the PAS uses principal search plugins:
+
+ >>> class Search42:
+ ...
+ ... zope.interface.implements(interfaces.IPrincipalSearchPlugin)
+ ...
+ ... def get(self, principal_id):
+ ... if principal_id == '42':
+ ... return {'domain': 42}
+
+ >>> provideUtility(interfaces.IPrincipalSearchPlugin, Search42(),
+ ... name='s42')
+
+ >>> class IntSearch:
+ ...
+ ... zope.interface.implements(interfaces.IPrincipalSearchPlugin)
+ ...
+ ... def get(self, principal_id):
+ ... try:
+ ... i = int(principal_id)
+ ... except ValueError:
+ ... return None
+ ... if (i >= 0 and i < 100):
+ ... return {'int': i}
+
+ >>> provideUtility(interfaces.IPrincipalSearchPlugin, IntSearch(),
+ ... name='sint')
+
+ >>> service.searchers = 's42', 'sint'
+
+ >>> service.getPrincipal('41')
+ OddPrincipal('41', "{'int': 41}")
+
+In addition to returning a principal, this will generate an event:
+
+ >>> clearEvents()
+ >>> service.getPrincipal('42')
+ Principal('42', "{'domain': 42}")
+
+ >>> [event] = getEvents(interfaces.IPASPrincipalCreated)
+ >>> event.principal
+ Principal('42', "{'domain': 42}")
+
+ >>> event.info
+ {'domain': 42}
+
+
+Issuing a challenge
+===================
+
+If the unauthorized method is called on the PAS, the PAS iterates
+through a sequence of challenge plugins calling their challenge
+methods until one returns True, indicating that a challenge was
+issued. (This is a simplification. See "Protocols" below.)
+
+Nothing will happen if there are no plugins registered.
+
+ >>> service.unauthorized(42, request)
+
+What happens if a plugin is registered depends on the plugin. Lets
+create a plugin that sets a response header:
+
+ >>> class Challenge:
+ ...
+ ... zope.interface.implements(interfaces.IChallengePlugin)
+ ...
+ ... def challenge(self, requests, response):
+ ... response.setHeader('X-Unauthorized', 'True')
+ ... return True
+
+ >>> provideUtility(interfaces.IChallengePlugin, Challenge(), name='c')
+ >>> service.challengers = ('c', )
+
+Now if we call unauthorized:
+
+ >>> service.unauthorized(42, request)
+
+the response `X-Unauthorized` is set:
+
+ >>> request.response.getHeader('X-Unauthorized')
+ 'True'
+
+How challenges work in Zope 3
+-----------------------------
+
+To understand how the challenge plugins work, it's helpful to
+understand how the unauthorized method of authenticaton services
+get called.
+
+If an 'Unauthorized' exception is raised and not caught by application
+code, then the following things happen:
+
+1. The current transaction is aborted.
+
+2. A view is looked up for the exception.
+
+3. The view gets the authentication service and calls it's
+ 'unauthorized' method.
+
+4. The PAS will call it's challenge plugins. If none return a value,
+ then the PAS delegates to the next authentication service above it
+ in the containment hierarchy, or to the global authentication
+ service.
+
+5. The view sets the body of the response.
+
+Protocols
+---------
+
+Sometimes, we want multiple challengers to work together. For
+example, the HTTP specification allows multiple challenges to be isued
+in a response. A challenge plugin can provide a `protocol`
+attribute. If multiple challenge plugins have the same protocol,
+then, if any of them are caled and return True, then they will all be
+called. Let's look at an example. We'll define two challengers that
+add chalenges to a X-Challenges headers:
+
+ >>> class ColorChallenge:
+ ... zope.interface.implements(interfaces.IChallengePlugin)
+ ...
+ ... protocol = 'bridge'
+ ...
+ ... def challenge(self, requests, response):
+ ... challenge = response.getHeader('X-Challenge', '')
+ ... response.setHeader('X-Challenge',
+ ... challenge + 'favorite color? ')
+ ... return True
+
+ >>> provideUtility(interfaces.IChallengePlugin, ColorChallenge(), name='cc')
+ >>> service.challengers = 'cc, ', 'c'
+
+ >>> class BirdChallenge:
+ ... zope.interface.implements(interfaces.IChallengePlugin)
+ ...
+ ... protocol = 'bridge'
+ ...
+ ... def challenge(self, requests, response):
+ ... challenge = response.getHeader('X-Challenge', '')
+ ... response.setHeader('X-Challenge',
+ ... challenge + 'swallow air speed? ')
+ ... return True
+
+ >>> provideUtility(interfaces.IChallengePlugin, BirdChallenge(), name='bc')
+ >>> service.challengers = 'cc', 'c', 'bc'
+
+Now if we call unauthorized:
+
+ >>> request = TestRequest(credentials=42)
+ >>> service.unauthorized(42, request)
+
+the response `X-Unauthorized` is not set:
+
+ >>> request.response.getHeader('X-Unauthorized')
+
+But the X-Challenge header has been set by both of the new challengers
+with the bridge protocol:
+
+ >>> request.response.getHeader('X-Challenge')
+ 'favorite color? swallow air speed? '
+
+Of course, if we put the original challenge first:
+
+ >>> service.challengers = 'c', 'cc', 'bc'
+ >>> request = TestRequest(credentials=42)
+ >>> service.unauthorized(42, request)
+
+We get 'X-Unauthorized' but not 'X-Challenge':
+
+ >>> request.response.getHeader('X-Unauthorized')
+ 'True'
+ >>> request.response.getHeader('X-Challenge')
+
+Issuing challenges during authentication
+----------------------------------------
+
+During authentication, extraction and authentication plugins can raise
+an 'Unauthorized' exception to indicate that a challenge should be
+issued immediately. They might do this if the recognize partial
+credentials that pertain to them.
+
+PAS prefixes
+============
+
+Principal ids are required to be unique system wide. Plugins will
+often provide options for providing id prefixes, so that different
+sets of plugins provide unique ids within a PAS. If there are
+multiple PASs in a system, it's a good idea to give each PAS a
+unique prefix, so that principal ids from different PASs don't
+conflict. We can provide a prefix when a PAS is created:
+
+ >>> service = pas.PAS('mypas_')
+ >>> service.extractors = 'eodd', 'emy'
+ >>> service.authenticators = 'a42', 'aint'
+ >>> service.factories = 'oddf', 'pf'
+ >>> service.searchers = 's42', 'sint'
+
+Now, we'll create a request and try to authenticate:
+
+ >>> request = TestRequest(credentials=42)
+ >>> principal = service.authenticate(request)
+ >>> principal
+ Principal('mypas_42', "{'domain': 42}")
+
+Note that now, our principal's id has the PAS prefix.
+
+We can still lookup a principal, as long as we supply the prefix:
+
+ >>> service.getPrincipal('mypas_42')
+ Principal('mypas_42', "{'domain': 42}")
+
+ >>> service.getPrincipal('mypas_41')
+ OddPrincipal('mypas_41', "{'int': 41}")
+
+Searching
+=========
+
+ XXX Still workin this out
+
+Delegation
+==========
+
+ XXX Still need to write this
Property changes on: Zope3/trunk/src/zope/app/pas/README.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/pas/__init__.py
===================================================================
--- Zope3/trunk/src/zope/app/pas/__init__.py 2004-10-04 18:44:50 UTC (rev 27740)
+++ Zope3/trunk/src/zope/app/pas/__init__.py 2004-10-04 19:33:52 UTC (rev 27741)
@@ -0,0 +1,20 @@
+##############################################################################
+#
+# 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.
+#
+##############################################################################
+"""Pluggable Autentication Service
+
+$Id$
+"""
+
+import interfaces
+from zope.app.pas.pas import PAS
Property changes on: Zope3/trunk/src/zope/app/pas/__init__.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/pas/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/app/pas/interfaces.py 2004-10-04 18:44:50 UTC (rev 27740)
+++ Zope3/trunk/src/zope/app/pas/interfaces.py 2004-10-04 19:33:52 UTC (rev 27741)
@@ -0,0 +1,192 @@
+import zope.interface
+import zope.schema
+
+class IPASPrincipalCreated(zope.interface.Interface):
+ """A PAS principal object has been created
+
+ This event is generated when a transient PAS principal has been created.
+ """
+
+ principal = zope.interface.Attribute("The principal that was created")
+
+ info = zope.schema.Dict(
+ title=u"Supplemental Information",
+ description=(
+ u"Supplemental information returned from authenticator and search\n"
+ u"plugins\n"
+ ),
+ )
+
+class IAuthenticatedPrincipalCreated(IPASPrincipalCreated):
+ """Event indicating that a principal was created by authenticating a reqest
+ """
+
+ request = zope.interface.Attribute(
+ "The request the user was authenticated against")
+
+
+class AuthenticatedPrincipalCreated:
+
+ zope.interface.implements(IAuthenticatedPrincipalCreated)
+
+ def __init__(self, principal, info, request):
+ self.principal = principal
+ self.info = info
+ self.request = request
+
+class IFoundPrincipalCreated(IPASPrincipalCreated):
+ """Event indicating that a principal was created based on a search
+ """
+
+class FoundPrincipalCreated:
+
+ zope.interface.implements(IFoundPrincipalCreated)
+
+ def __init__(self, principal, info):
+ self.principal = principal
+ self.info = info
+
+class IPlugin(zope.interface.Interface):
+ """Provide functionality to be pluged into a PAS
+ """
+
+class IPrincipalIdAwarePlugin(IPlugin):
+ """Principal-Id aware plugin
+
+ A requirements of plugins that deal with principal ids is that
+ principal ids must be unique within a PAS. A PAS manager may want
+ to use plugins to support multiple principal sources. If the ids
+ from the various principal sources overlap, there needs to be some
+ way to disambiguate them. For this reason, it's a good idea for
+ id-aware plugins to provide a way for a PAS manager to configure
+ an id prefix or some other mechanism to make sure that
+ principal-ids from different domains don't overlap.
+ """
+
+class IExtractionPlugin(IPlugin):
+ """Extracts authentication credentials from a request.
+ """
+
+ def extractCredentials(request):
+ """Try to extract credentials from a request
+
+ A return value of None indicates that no credentials could be
+ found. Any other return value is treated as valid credentials.
+ """
+
+class IAuthenticationPlugin(IPrincipalIdAwarePlugin):
+ """Authenticate credentials
+ """
+
+ def authenticateCredentials(credentials):
+ """Authenticate credentials
+
+ If the credentials can be authenticated, return a 2-tuple with
+ a principal id and a dictionary containing supplemental
+ information, if any. Otherwise, return None.
+ """
+
+class IChallengePlugin(IPlugin):
+ """Initiate a challenge to the user to provide credentials.
+ """
+
+ protocol = zope.interface.Attribute("""Optional Challenger protocol
+
+ If a challenger works with other challenger pluggins, then it and
+ the other cooperating plugins should specify a common (non-None)
+ protocol. If a challenger returns True, then other challengers
+ will be called only if they have the same protocol.
+ """)
+
+ def challenge(request, response):
+ """Possibly issue a challenge
+
+ This is typically done in a protocol-specific way.
+
+ If a challenge was issued, return True. (Return False otherwise).
+ """
+
+class IPrincipalFactoryPlugin(IPlugin):
+ """Create a principal object
+ """
+
+ def createAuthenticatedPrincipal(principal_id, info, request):
+ """Create a principal authenticated against a request
+
+ The info argument is a dictionary containing supplemental
+ information that can be used by the factory and by event
+ subscribers. The contents of the info dictionary are defined
+ by the authentication plugin used to authenticate the
+ principal id.
+
+ If a principal is created, an IAuthenticatedPrincipalCreated
+ event must be published and the principal is returned. If no
+ principal is created, return None.
+ """
+
+ def createFoundPrincipal(user_id, info):
+ """Return a principal, if possible.
+
+ The info argument is a dictionary containing supplemental
+ information that can be used by the factory and by event
+ subscribers. The contents of the info dictionary are defined
+ by the search plugin used to find the principal id.
+
+ If a principal is created, an IFoundPrincipalCreated
+ event must be published and the principal is returned. If no
+ principal is created, return None.
+ """
+
+class IPrincipalSearchPlugin(IPrincipalIdAwarePlugin):
+ """Find principals
+
+ Principal search plugins provide two functions:
+
+ - Get principal information, given a principal id
+
+ - Search for principal ids
+
+ The second function is a bit tricky, because there are many ways
+ that one might search for principals.
+
+ XXX Need to say more here. We need to work out what to say. :)
+ XXX In the mean time, see IQuerySchemaSearch. Initially, search
+ XXX plugins should provide IQuerySchemaSearch.
+
+ """
+
+ def get(principal_id):
+ """Try to get principal information for the principal id.
+
+ If the principal id is valid, then return a dictionary
+ containing supplemental information, if any. Otherwise,
+ return None.
+
+ """
+
+class IQuerySchemaSearch(IPrincipalSearchPlugin):
+ """
+ """
+
+ schema = zope.interface.Attribute("""Search Schema
+
+ A schema specifying search parameters.
+ """)
+
+ def search(query, start=None, batch_size=None):
+ """Search for principals
+
+ The query argument is a mapping object with items defined by
+ the plugin's. An iterable of principal ids should be returned.
+
+ If the start argument is privided, then it should be an
+ integer and the given number of initial items should be
+ skipped.
+
+ If the batch_size argument is provided, then it should be an
+ integer and no more than the given number of items should be
+ returned.
+
+ """
+
+
Property changes on: Zope3/trunk/src/zope/app/pas/interfaces.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/pas/pas.py
===================================================================
--- Zope3/trunk/src/zope/app/pas/pas.py 2004-10-04 18:44:50 UTC (rev 27740)
+++ Zope3/trunk/src/zope/app/pas/pas.py 2004-10-04 19:33:52 UTC (rev 27741)
@@ -0,0 +1,153 @@
+##############################################################################
+#
+# 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.
+#
+##############################################################################
+"""Pluggable authentication service implementation
+
+$Id$
+"""
+
+from zope.event import notify
+import zope.interface
+import zope.schema
+
+from zope.app import zapi
+
+from zope.app.pas import vocabularies, interfaces
+from zope.app.pas.interfaces import IExtractionPlugin
+from zope.app.pas.interfaces import IAuthenticationPlugin
+from zope.app.pas.interfaces import IChallengePlugin
+from zope.app.pas.interfaces import IPrincipalFactoryPlugin
+from zope.app.pas.interfaces import IPrincipalSearchPlugin
+
+
+class IPAS(zope.interface.Interface):
+ """Pluggable Authentication Service
+ """
+
+ extractors = zope.schema.Tuple(
+ title=u"Credential Extractors",
+ value_type = zope.schema.Choice(
+ vocabulary = vocabularies.UtilityNames(IExtractionPlugin)),
+ default=(),
+ )
+
+ authenticators = zope.schema.Tuple(
+ title=u"Authenticators",
+ value_type = zope.schema.Choice(
+ vocabulary = vocabularies.UtilityNames(IAuthenticationPlugin)),
+ default=(),
+ )
+
+ challengers = zope.schema.Tuple(
+ title=u"Challengers",
+ value_type = zope.schema.Choice(
+ vocabulary = vocabularies.UtilityNames(IChallengePlugin)),
+ default=(),
+ )
+
+ factories = zope.schema.Tuple(
+ title=u"Principal Factories",
+ value_type = zope.schema.Choice(
+ vocabulary = vocabularies.UtilityNames(IPrincipalFactoryPlugin)),
+ default=(),
+ )
+
+ searchers = zope.schema.Tuple(
+ title=u"Search Plugins",
+ value_type = zope.schema.Choice(
+ vocabulary = vocabularies.UtilityNames(IPrincipalSearchPlugin)),
+ default=(),
+ )
+
+class PAS:
+
+ zope.interface.implements(IPAS)
+
+ authenticators = extractors = challengers = factories = search = ()
+
+ def __init__(self, prefix=''):
+ self.prefix = prefix
+
+ def authenticate(self, request):
+ authenticators = [zapi.queryUtility(IAuthenticationPlugin, name)
+ for name in self.authenticators]
+ for extractor in self.extractors:
+ extractor = zapi.queryUtility(IExtractionPlugin, extractor)
+ if extractor is None:
+ continue
+ credentials = extractor.extractCredentials(request)
+ for authenticator in authenticators:
+ if authenticator is None:
+ continue
+ authenticated = authenticator.authenticateCredentials(
+ credentials)
+ if authenticated is None:
+ continue
+
+ id, info = authenticated
+ return self._create('createAuthenticatedPrincipal',
+ self.prefix+id, info, request)
+
+
+ def _create(self, meth, *args):
+ # We got some data, lets create a user
+ for factory in self.factories:
+ factory = zapi.queryUtility(IPrincipalFactoryPlugin,
+ factory)
+ if factory is None:
+ continue
+
+ principal = getattr(factory, meth)(*args)
+ if principal is None:
+ continue
+
+ return principal
+
+ def getPrincipal(self, id):
+ if not id.startswith(self.prefix):
+ return
+ id = id[len(self.prefix):]
+
+ for searcher in self.searchers:
+ searcher = zapi.queryUtility(IPrincipalSearchPlugin, searcher)
+ if searcher is None:
+ continue
+
+ info = searcher.get(id)
+ if info is None:
+ continue
+
+ return self._create('createFoundPrincipal', self.prefix+id, info)
+
+ def unauthenticatedPrincipal(self):
+ pass
+
+ def unauthorized(self, id, request):
+ protocol = None
+
+ for challenger in self.challengers:
+ challenger = zapi.queryUtility(IChallengePlugin, challenger)
+ if challenger is None:
+ continue # skip non-existant challengers
+
+ challenger_protocol = getattr(challenger, 'protocol', None)
+ if protocol is None or challenger_protocol == protocol:
+ if challenger.challenge(request, request.response):
+ if challenger_protocol is None:
+ break
+ elif protocol is None:
+ protocol = challenger_protocol
+
+ # XXX Fallback code. This will call unauthorized on higher-level
+ # authentication services.
+
Property changes on: Zope3/trunk/src/zope/app/pas/pas.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/pas/tests.py
===================================================================
--- Zope3/trunk/src/zope/app/pas/tests.py 2004-10-04 18:44:50 UTC (rev 27740)
+++ Zope3/trunk/src/zope/app/pas/tests.py 2004-10-04 19:33:52 UTC (rev 27741)
@@ -0,0 +1,22 @@
+"""$Id$
+"""
+import unittest
+from zope.testing import doctest
+from zope.app.tests import placelesssetup, ztapi
+from zope.app.event.tests.placelesssetup import getEvents
+
+
+def test_suite():
+ return unittest.TestSuite((
+ doctest.DocFileSuite('README.txt',
+ setUp=placelesssetup.setUp,
+ tearDown=placelesssetup.tearDown,
+ globs={'provideUtility': ztapi.provideUtility,
+ 'getEvents': getEvents,
+ },
+ ),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
+
Property changes on: Zope3/trunk/src/zope/app/pas/tests.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/pas/vocabularies.py
===================================================================
--- Zope3/trunk/src/zope/app/pas/vocabularies.py 2004-10-04 18:44:50 UTC (rev 27740)
+++ Zope3/trunk/src/zope/app/pas/vocabularies.py 2004-10-04 19:33:52 UTC (rev 27741)
@@ -0,0 +1,64 @@
+##############################################################################
+#
+# 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.
+#
+##############################################################################
+"""Simple utility name vocabulary to support the PAS
+
+XXX Need doc/test for this still.
+XXX For now this is effectively a placeholder.
+
+$Id$
+"""
+
+import zope.interface
+import zope.schema.interfaces
+from zope.app import zapi
+
+class NameTerm:
+
+ def __init__(self, value):
+ self.value = unicode(value)
+
+ def token(self):
+ # Return our value as a token. This is required to be 7-bit
+ # printable ascii. We'll use base64
+ return self.value.encode('base64')[:-1]
+ token = property(token)
+
+ def title(self):
+ return self.value
+ title = property(title)
+
+class UtilityNames:
+
+ zope.interface.implements(zope.schema.interfaces.IVocabularyTokenized)
+
+ def __init__(self, interface):
+ self.interface = interface
+
+ def __contains__(value):
+ return zapi.queryUtility(self.interface, value) is not None
+
+ def getQuery():
+ pass
+
+ def getTerm(value):
+ return NameTerm(value)
+
+ def __iter__():
+ for name, ut in zapi.getUtilitiesFor(self.interface):
+ return NameTerm(name)
+
+ def __len__():
+ """Return the number of valid terms, or sys.maxint."""
+ return len(list(zapi.getUtilitiesFor(self.interface)))
+
Property changes on: Zope3/trunk/src/zope/app/pas/vocabularies.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
More information about the Zope3-Checkins
mailing list