[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