[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