[Repoze-checkins] r787 - in repoze.pam/trunk: . repoze/pam repoze/pam/plugins

Chris McDonough chrism at agendaless.com
Sun Mar 9 13:58:47 UTC 2008


Author: Chris McDonough <chrism at agendaless.com>
Date: Sun Mar  9 08:58:46 2008
New Revision: 787

Log:
 - API change: IIdentifiers are no longer required to put both 'login'
   and 'password' in a returned identity dictionary.  Instead, an
   IIdentifier can place arbitrary key/value pairs in the identity
   dictionary (or return an empty dictionary).

 - API return value change: the "failure" identity which IIdentifiers
   return is now None rather than an empty dictionary.

 - The IAuthenticator interface now specifies that IAuthenticators
   must not raise an exception when evaluating an identity that does
   not have "expected" key/value pairs (e.g. when an IAuthenticator
   that expects login and password inspects an identity returned by an
   IP-based auth system which only puts the IP address in the
   identity); instead they fail gracefully by returning None.



Modified:
   repoze.pam/trunk/CHANGES.txt
   repoze.pam/trunk/README.txt
   repoze.pam/trunk/repoze/pam/interfaces.py
   repoze.pam/trunk/repoze/pam/middleware.py
   repoze.pam/trunk/repoze/pam/plugins/auth_tkt.py
   repoze.pam/trunk/repoze/pam/plugins/basicauth.py
   repoze.pam/trunk/repoze/pam/plugins/cookie.py
   repoze.pam/trunk/repoze/pam/plugins/form.py
   repoze.pam/trunk/repoze/pam/plugins/sql.py
   repoze.pam/trunk/repoze/pam/tests.py

Modified: repoze.pam/trunk/CHANGES.txt
==============================================================================
--- repoze.pam/trunk/CHANGES.txt	(original)
+++ repoze.pam/trunk/CHANGES.txt	Sun Mar  9 08:58:46 2008
@@ -5,19 +5,34 @@
 
  - Allow form plugin to override the default form.
 
+ - API change: IIdentifiers are no longer required to put both 'login'
+   and 'password' in a returned identity dictionary.  Instead, an
+   IIdentifier can place arbitrary key/value pairs in the identity
+   dictionary (or return an empty dictionary).
+
+ - API return value change: the "failure" identity which IIdentifiers
+   return is now None rather than an empty dictionary.
+
+ - The IAuthenticator interface now specifies that IAuthenticators
+   must not raise an exception when evaluating an identity that does
+   not have "expected" key/value pairs (e.g. when an IAuthenticator
+   that expects login and password inspects an identity returned by an
+   IP-based auth system which only puts the IP address in the
+   identity); instead they fail gracefully by returning None.
+
  - Add (cookie) "auth_tkt" identification plugin.
 
  - Stamp identity dictionaries with a userid by placing a key named
    'repoze.pam.userid' into the identity for each authenticated
    identity.
 
- - If an identity plugin inserts a 'repoze.pam.userid' key into the
+ - If an IIdentifier plugin inserts a 'repoze.pam.userid' key into the
    identity dictionary, consider this identity "preauthenticated".  No
    authenticator plugins will be asked to authenticate this identity.
-   This is designed for things like domain authentication (and the
-   recently added auth_tkt plugin).  Preauthenticated identities will
-   be selected first when deciding which identity to use for any given
-   request.
+   This is designed for things like the recently added auth_tkt
+   plugin, which embeds the user id into the ticket.  Preauthenticated
+   identities will be selected first when deciding which identity to
+   use for any given request.
 
  - Insert a 'repoze.pam.identity' key into the WSGI environment on
    ingress if an identity is found.  Its value will be the identity

Modified: repoze.pam/trunk/README.txt
==============================================================================
--- repoze.pam/trunk/README.txt	(original)
+++ repoze.pam/trunk/README.txt	Sun Mar  9 08:58:46 2008
@@ -18,8 +18,8 @@
   perform the operation implied by the request).  This is also the
   domain of the WSGI application.
  
-  It attemtps to reuse implementations from AuthKit and paste.auth for
-  some of its functionality.  XXX this is, so far, untrue
+  It attemtps to reuse implementations from paste.auth for some of its
+  functionality.
 
 Middleware Responsibilities
 
@@ -83,24 +83,24 @@
 
       Identifiers which nominate themselves as willing to extract data
       for a particular class of request (as provided by the request
-      classifier) will be consulted to retrieve login and password
-      data from the environment.  For example, a basic auth identifier
-      might use the HTTP_AUTHORIZATION header to find login and
-      password information.  Identifiers are also responsible for
-      providing header information to set and remove authentication
-      information in the response.
+      classifier) will be consulted to retrieve credentials data from
+      the environment.  For example, a basic auth identifier might use
+      the HTTP_AUTHORIZATION header to find login and password
+      information.  Identifiers are also responsible for providing
+      header information to set and remove authentication information
+      in the response.
 
   3.  Authentication
 
       Authenticators which nominate themselves as willing to
       authenticate for a particular class of request will be consulted
-      to compare login and password information provided by the
-      identification plugins that returned credentials.  For example,
-      an htpasswd authenticator might look in a file for a user record
-      matching any of the identities.  If it finds one, and if the
-      password listed in the record matches the password provided by
-      an identity, the userid of the user would be returned (which
-      would be the same as the login name).
+      to compare information provided by the identification plugins
+      that returned credentials.  For example, an htpasswd
+      authenticator might look in a file for a user record matching
+      any of the identities.  If it finds one, and if the password
+      listed in the record matches the password provided by an
+      identity, the userid of the user would be returned (which would
+      be the same as the login name).
 
 Response (Egress) Stages
 
@@ -302,19 +302,19 @@
             cookie = cookies.get(self.cookie_name)
 
             if cookie is None:
-                return {}
+                return None
 
             import binascii
             try:
                 auth = cookie.value.decode('base64')
             except binascii.Error: # can't decode
-                return {}
+                return None
 
             try:
                 login, password = auth.split(':', 1)
                 return {'login':login, 'password':password}
             except ValueError: # not enough values to unpack
-                return {}
+                return None
 
         def remember(self, environ, identity):
             cookie_value = '%(login)s:%(password)s' % identity
@@ -353,8 +353,8 @@
     finds one that matches, it attempts to decode it and turn it into
     a login and a password, which it returns as values in a
     dictionary.  This dictionary is thereafter known as an "identity".
-    If it finds no credentials in cookies, it returns an empty
-    dictionary (which is not considered an identity).
+    If it finds no credentials in cookies, it returns None (which is
+    not considered an identity).
 
     More generally, the 'identify' method of an IIdentifier plugin is
     called once on WSGI request "ingress", and it is expected to grub
@@ -365,12 +365,21 @@
     WSGI environment variable set by some upstream middleware or
     whatever else someone might use to stash authentication
     information.  If the plugin finds credentials in the request, it's
-    expected to return an "identity": this is a dictionary of (at
-    least) the form {'login':login_name, 'password':password}.  It may
-    place other information in the dictionary for use by special
-    IAuthenticator plugins, but these are the two required fields in
-    the dictionary.  If it finds no credentials, it is expected to
-    return an empty dictionary.
+    expected to return an "identity": this must be a dictionary.  The
+    dictionary is not required to have any particular keys or value
+    composition, although it's wise if the identification plugin looks
+    for both a login name and a password information to return at
+    least {'login':login_name, 'password':password}, as some
+    authenticator plugins may depend on presence of the names "login"
+    and "password" (e.g. the htpasswd and sql IAuthenticator plugins).
+    If an IIdentifier plugin finds no credentials, it is expected to
+    return None.  An IIdentifier plugin is also permitted to
+    "preauthenticate" an identity.  If the identifier plugin knows
+    that the identity is "good" (e.g. in the case of IP-address-based
+    authentication, or ticket-based authentication), it can insert a
+    special key into the identity dictionary: 'repoze.pam.userid'.  If
+    this key is present in the identity dictionary, no authenticators
+    will be asked to authenticate the identity.
 
   remember(environ, identity) --
 

Modified: repoze.pam/trunk/repoze/pam/interfaces.py
==============================================================================
--- repoze.pam/trunk/repoze/pam/interfaces.py	(original)
+++ repoze.pam/trunk/repoze/pam/interfaces.py	Sun Mar  9 08:58:46 2008
@@ -48,9 +48,7 @@
     def identify(environ):
         """ On ingress:
 
-        environ -> { 'login' : login 
-                       , 'password' : password 
-                       , k1 : v1
+        environ -> {   k1 : v1
                        ,   ...
                        , kN : vN
                        } | None
@@ -58,8 +56,16 @@
         o 'environ' is the WSGI environment.
 
         o If credentials are found, the returned identity mapping will
-          contain at least 'login' and 'password' keys (and others as
-          necessary for special-case needs).
+          contain an arbitrary set of key/value pairs.  If the
+          identity is based on a login and password, the environment
+          is recommended to contain at least 'login' and 'password'
+          keys as this provides compatibility between the plugin and
+          existing authenticator plugins.  If the identity can be
+          'preauthenticated' (e.g. if the userid is embedded in the
+          identity, such as when we're using ticket-based
+          authentication), the plugin should set the userid in the
+          special 'repoze.pam.userid' key; no authenticators will be
+          asked to authenticate the identity thereafer.
 
         o Return None to indicate that the plugin found no appropriate
           credentials.
@@ -103,16 +109,23 @@
 
         o 'environ' is the WSGI environment.
 
-        o 'identity' will be a mapping containing at least 'login' and
-          'password' key/value pairs.
-
+        o 'identity' will be a dictionary (with arbitrary keys and
+          values).
+ 
         o The IAuthenticator should return a single user id (should be
-        a string) or None if the identify cannot be authenticated.
+          a string) or None if the identify cannot be authenticated.
 
         Each instance of a registered IAuthenticator plugin that
         matches the request classifier will be called N times during a
         single request, where N is the number of identities found by
         any IIdentifierPlugin instances.
+
+        An authenticator must not raise an exception if it is provided
+        an identity dictionary that it does not understand (e.g. if it
+        presumes that 'login' and 'password' are keys in the
+        dictionary, it should check for the existence of these keys
+        before attempting to do anything; if they don't exist, it
+        should return None).
         """
 
 class IChallenger(Interface):

Modified: repoze.pam/trunk/repoze/pam/middleware.py
==============================================================================
--- repoze.pam/trunk/repoze/pam/middleware.py	(original)
+++ repoze.pam/trunk/repoze/pam/middleware.py	Sun Mar  9 08:58:46 2008
@@ -45,6 +45,7 @@
             return self.app(environ, start_response)
 
         environ['repoze.pam.plugins'] = self.name_registry
+        environ['repoze.pam.logger'] = self.logger
 
         logger = self.logger
         logger and logger.info(_STARTED)
@@ -59,7 +60,7 @@
         if ids:
             auth_ids = self.authenticate(environ, classification, ids)
 
-            # auth_ids will be a list of four-tuples in the form
+            # auth_ids will be a list of five-tuples in the form
             #  ( (auth_rank, id_rank), authenticator, identifier, identity,
             #    userid )
             #
@@ -69,7 +70,7 @@
             if auth_ids:
                 auth_ids.sort()
                 best = auth_ids[0]
-                ignore, authenticator, identifier, identity, userid = best
+                rank, authenticator, identifier, identity, userid = best
                 identity = Identity(identity) # dont show contents at print
                 # add the identity to the environment; a downstream
                 # application can mutate it to do an 'identity reset'
@@ -130,7 +131,7 @@
         results = []
         for plugin in plugins:
             identity = plugin.identify(environ)
-            if identity:
+            if identity is not None:
                 logger and logger.debug(
                     'identity returned from %s: %s' % (plugin, identity))
                 results.append((plugin, identity))
@@ -164,7 +165,7 @@
                 userid = plugin.authenticate(environ, identity)
                 if userid:
                     logger and logger.debug(
-                        'userid returned from %s: %s' % (plugin, userid))
+                        'userid returned from %s: "%s"' % (plugin, userid))
 
                     # stamp the identity with the userid
                     identity['repoze.pam.userid'] = userid
@@ -183,6 +184,7 @@
         return results
 
     def _filter_preauthenticated(self, identities):
+        logger = self.logger
         results = []
         new_identities = identities[:]
 
@@ -193,6 +195,10 @@
             if userid is not None:
                 # the identifier plugin has already authenticated this
                 # user (domain auth, auth ticket, etc)
+                logger and logger.info(
+                  'userid preauthenticated by %s: "%s" '
+                  '(repoze.pam.userid set)' % (identifier, userid)
+                  )
                 rank = (0, identifier_rank)
                 results.append(
                     (rank, None, identifier, identity, userid)
@@ -320,6 +326,7 @@
     # be able to test without a config file
     from repoze.pam.plugins.basicauth import BasicAuthPlugin
     from repoze.pam.plugins.htpasswd import HTPasswdPlugin
+    from repoze.pam.plugins.auth_tkt import AuthTktCookiePlugin
     from repoze.pam.plugins.cookie import InsecureCookiePlugin
     from repoze.pam.plugins.form import FormPlugin
     basicauth = BasicAuthPlugin('repoze.pam')
@@ -332,11 +339,12 @@
         io.write('%s:%s\n' % (name, crypt.crypt(password, salt)))
     io.seek(0)
     htpasswd = HTPasswdPlugin(io, crypt_check)
+    auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt')
     cookie = InsecureCookiePlugin('oatmeal')
-    form = FormPlugin('__do_login', rememberer_name='cookie')
+    form = FormPlugin('__do_login', rememberer_name='auth_tkt')
     form.classifications = { IIdentifier:['browser'],
                              IChallenger:['browser'] } # only for browser
-    identifiers = [('form', form),('cookie',cookie),('basicauth',basicauth) ]
+    identifiers = [('form', form),('auth_tkt',auth_tkt),('basicauth',basicauth)]
     authenticators = [('htpasswd', htpasswd)]
     challengers = [('form',form), ('basicauth',basicauth)]
     from repoze.pam.classifiers import default_request_classifier

Modified: repoze.pam/trunk/repoze/pam/plugins/auth_tkt.py
==============================================================================
--- repoze.pam/trunk/repoze/pam/plugins/auth_tkt.py	(original)
+++ repoze.pam/trunk/repoze/pam/plugins/auth_tkt.py	Sun Mar  9 08:58:46 2008
@@ -23,7 +23,7 @@
         cookie = cookies.get(self.cookie_name)
 
         if cookie is None or not cookie.value:
-            return {}
+            return None
 
         if self.include_ip:
             remote_addr = environ['REMOTE_ADDR']
@@ -34,7 +34,7 @@
             timestamp, userid, tokens, user_data = auth_tkt.parse_ticket(
                 self.secret, cookie.value, remote_addr)
         except auth_tkt.BadTicket:
-            return {}
+            return None
             
         if environ.get('REMOTE_USER_TOKENS'):
             # We want to add tokens/roles to what's there:
@@ -91,7 +91,7 @@
         if not isinstance(tokens, basestring):
             tokens = ','.join(tokens)
         if not isinstance(pam_tokens, basestring):
-            tokens = ','.join(pam_tokens)
+            pam_tokens = ','.join(pam_tokens)
         old_data = (userid, tokens, userdata)
         new_data = (pam_userid, pam_tokens, pam_userdata)
 

Modified: repoze.pam/trunk/repoze/pam/plugins/basicauth.py
==============================================================================
--- repoze.pam/trunk/repoze/pam/plugins/basicauth.py	(original)
+++ repoze.pam/trunk/repoze/pam/plugins/basicauth.py	Sun Mar  9 08:58:46 2008
@@ -22,20 +22,20 @@
         try:
             authmeth, auth = authorization.split(' ', 1)
         except ValueError: # not enough values to unpack
-            return {}
+            return None
         if authmeth.lower() == 'basic':
             try:
                 auth = auth.strip().decode('base64')
             except binascii.Error: # can't decode
-                return {}
+                return None
             try:
                 login, password = auth.split(':', 1)
             except ValueError: # not enough values to unpack
-                return {}
+                return None
             auth = {'login':login, 'password':password}
             return auth
 
-        return {}
+        return None
 
     # IIdentifier
     def remember(self, environ, identity):

Modified: repoze.pam/trunk/repoze/pam/plugins/cookie.py
==============================================================================
--- repoze.pam/trunk/repoze/pam/plugins/cookie.py	(original)
+++ repoze.pam/trunk/repoze/pam/plugins/cookie.py	Sun Mar  9 08:58:46 2008
@@ -19,18 +19,18 @@
         cookie = cookies.get(self.cookie_name)
 
         if cookie is None:
-            return {}
+            return None
 
         try:
             auth = cookie.value.decode('base64')
         except binascii.Error: # can't decode
-            return {}
+            return None
 
         try:
             login, password = auth.split(':', 1)
             return {'login':login, 'password':password}
         except ValueError: # not enough values to unpack
-            return {}
+            return None
 
     # IIdentifier
     def forget(self, environ, identity):

Modified: repoze.pam/trunk/repoze/pam/plugins/form.py
==============================================================================
--- repoze.pam/trunk/repoze/pam/plugins/form.py	(original)
+++ repoze.pam/trunk/repoze/pam/plugins/form.py	Sun Mar  9 08:58:46 2008
@@ -69,10 +69,10 @@
                 login = form['login']
                 password = form['password']
             except KeyError:
-                return {}
+                return None
             return {'login':login, 'password':password}
 
-        return {}
+        return None
 
     def _get_rememberer(self, environ):
         rememberer = environ['repoze.pam.plugins'][self.rememberer_name]

Modified: repoze.pam/trunk/repoze/pam/plugins/sql.py
==============================================================================
--- repoze.pam/trunk/repoze/pam/plugins/sql.py	(original)
+++ repoze.pam/trunk/repoze/pam/plugins/sql.py	Sun Mar  9 08:58:46 2008
@@ -41,6 +41,8 @@
 
     # IAuthenticator
     def authenticate(self, environ, identity):
+        if not 'login' in identity:
+            return None
         if not self.conn:
             self.conn = self._connect()
         curs = self.conn.cursor()

Modified: repoze.pam/trunk/repoze/pam/tests.py
==============================================================================
--- repoze.pam/trunk/repoze/pam/tests.py	(original)
+++ repoze.pam/trunk/repoze/pam/tests.py	Sun Mar  9 08:58:46 2008
@@ -61,6 +61,17 @@
         self.assertEqual(identity['login'], 'chris')
         self.assertEqual(identity['password'], 'password')
 
+    def test_identify_success_empty_identity(self):
+        environ = self._makeEnviron()
+        identifier = DummyIdentifier({})
+        identifiers = [ ('i', identifier) ]
+        mw = self._makeOne(identifiers=identifiers)
+        results = mw.identify(environ, None)
+        self.assertEqual(len(results), 1)
+        new_identifier, identity = results[0]
+        self.assertEqual(new_identifier, identifier)
+        self.assertEqual(identity, {})
+
     def test_identify_fail(self):
         environ = self._makeEnviron()
         plugin = DummyNoResultsIdentifier()
@@ -643,26 +654,26 @@
         plugin = self._makeOne('realm')
         environ = self._makeEnviron()
         creds = plugin.identify(environ)
-        self.assertEqual(creds, {})
+        self.assertEqual(creds, None)
 
     def test_identify_nonbasic(self):
         plugin = self._makeOne('realm')
         environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Digest abc'})
         creds = plugin.identify(environ)
-        self.assertEqual(creds, {})
+        self.assertEqual(creds, None)
 
     def test_identify_basic_badencoding(self):
         plugin = self._makeOne('realm')
         environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic abc'})
         creds = plugin.identify(environ)
-        self.assertEqual(creds, {})
+        self.assertEqual(creds, None)
 
     def test_identify_basic_badrepr(self):
         plugin = self._makeOne('realm')
         value = 'foo'.encode('base64')
         environ = self._makeEnviron({'HTTP_AUTHORIZATION':'Basic %s' % value})
         creds = plugin.identify(environ)
-        self.assertEqual(creds, {})
+        self.assertEqual(creds, None)
 
     def test_identify_basic_ok(self):
         plugin = self._makeOne('realm')
@@ -818,20 +829,14 @@
         plugin = self._makeOne('oatmeal')
         environ = self._makeEnviron()
         result = plugin.identify(environ)
-        self.assertEqual(result, {})
+        self.assertEqual(result, None)
         
     def test_identify_badcookies(self):
         plugin = self._makeOne('oatmeal')
         environ = self._makeEnviron({'HTTP_COOKIE':'oatmeal=a'})
         result = plugin.identify(environ)
-        self.assertEqual(result, {})
+        self.assertEqual(result, None)
 
-    def test_identify_badcookies(self):
-        plugin = self._makeOne('oatmeal')
-        environ = self._makeEnviron({'HTTP_COOKIE':'oatmeal=a'})
-        result = plugin.identify(environ)
-        self.assertEqual(result, {})
-    
     def test_identify_success(self):
         plugin = self._makeOne('oatmeal')
         auth = 'foo:password'.encode('base64').rstrip()
@@ -916,25 +921,25 @@
         plugin = self._makeOne()
         environ = self._makeFormEnviron()
         result = plugin.identify(environ)
-        self.assertEqual(result, {})
+        self.assertEqual(result, None)
         
     def test_identify_qs_no_values(self):
         plugin = self._makeOne()
         environ = self._makeFormEnviron(do_login=True)
         result = plugin.identify(environ)
-        self.assertEqual(result, {})
+        self.assertEqual(result, None)
 
     def test_identify_nologin(self):
         plugin = self._makeOne()
         environ = self._makeFormEnviron(do_login=True, login='chris')
         result = plugin.identify(environ)
-        self.assertEqual(result, {})
+        self.assertEqual(result, None)
     
     def test_identify_nopassword(self):
         plugin = self._makeOne()
         environ = self._makeFormEnviron(do_login=True, password='password')
         result = plugin.identify(environ)
-        self.assertEqual(result, {})
+        self.assertEqual(result, None)
 
     def test_identify_success(self):
         plugin = self._makeOne()
@@ -1050,7 +1055,7 @@
         plugin = self._makeOne('secret')
         environ = self._makeEnviron()
         result = plugin.identify(environ)
-        self.assertEqual(result, {})
+        self.assertEqual(result, None)
         
     def test_identify_good_cookie_include_ip(self):
         plugin = self._makeOne('secret', include_ip=True)
@@ -1084,7 +1089,7 @@
         plugin = self._makeOne('secret', include_ip=True)
         environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=bogus'})
         result = plugin.identify(environ)
-        self.assertEqual(result, {})
+        self.assertEqual(result, None)
     
     def test_remember_creds_same(self):
         plugin = self._makeOne('secret')
@@ -1263,6 +1268,15 @@
         self.assertEqual(plugin.conn.curs.bindargs, identity)
         self.assertEqual(plugin.conn.curs.closed, True)
 
+    def test_authenticate_nologin(self):
+        conn_factory = self._makeConnectionFactory(('userid', 'password'))
+        plugin = self._makeOne('dsn', 'statement', compare_success,
+                               conn_factory)
+        environ = self._makeEnviron()
+        identity = {}
+        result = plugin.authenticate(environ, identity)
+        self.assertEqual(result, None)
+
 class TestDefaultPasswordCompare(unittest.TestCase):
     def _getFUT(self):
         from repoze.pam.plugins.sql import default_password_compare
@@ -1439,7 +1453,7 @@
 
 class DummyNoResultsIdentifier:
     def identify(self, environ):
-        return {}
+        return None
 
     def remember(self, *arg, **kw):
         pass


More information about the Repoze-checkins mailing list