From fbfd3ddce24a9f86838f64753d8654ddf6c31d81 Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Wed, 1 Jun 2011 15:21:38 +0200 Subject: reworked digest.AuthTokenGenerator AuthTokenGenerator gained a base class AbstractTokenGenerator. This class provides an additional method check_password implementing the interface required by BasicAuthMiddleware. In addition AbstractTokenGenerator gained two subclasses HtdigestTokenGenerator and UpdatingHtdigestTokenGenerator. They both read authentication information from a apache htdigest file. The latter also checks the file for updates. --- wsgitools/digest.py | 124 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 7 deletions(-) (limited to 'wsgitools') diff --git a/wsgitools/digest.py b/wsgitools/digest.py index 2a8da05..4b7dd96 100755 --- a/wsgitools/digest.py +++ b/wsgitools/digest.py @@ -22,6 +22,7 @@ except ImportError: import binascii import base64 import time +import os sysrand = random.SystemRandom() @@ -87,9 +88,50 @@ class AuthenticationRequired(Exception): not visible to other code. """ +__all__.append("AbstractTokenGenerator") +class AbstractTokenGenerator: + """Interface class for generating authentication tokens for + L{AuthDigestMiddleware}. + + @ivar realm: is a string according to RFC2617. + @type realm: str + """ + def __init__(self, realm): + """ + @type realm: str + """ + self.realm = realm + + def __call__(self, username, algo="md5"): + """Generates an authentication token from a username. + @type username: str + @type algo: str + @param algo: currently the only value supported by + AuthDigestMiddleware is "md5" + @rtype: str or None + @returns: a valid token or None to signal that authentication should + fail + """ + raise NotImplementedError + + def check_password(self, username, password, environ=None): + """ + This function implements the interface for verifying passwords + used by L{BasicAuthMiddleware}. It works by computing a token + from the user and comparing it to the token returned by the + __call__ method. + + @type username: str + @type password: str + @param environ: ignored + @rtype: bool + """ + token = md5("%s:%s:%s" % (username, self.realm, password)).hexdigest() + return token == self(username) + __all__.append("AuthTokenGenerator") -class AuthTokenGenerator: - """Generates authentification tokens for L{AuthDigestMiddleware}. The +class AuthTokenGenerator(AbstractTokenGenerator): + """Generates authentication tokens for L{AuthDigestMiddleware}. The interface consists of beeing callable with a username and having a realm attribute being a string.""" def __init__(self, realm, getpass): @@ -101,13 +143,10 @@ class AuthTokenGenerator: expected as result. C{None} may be used as an invalid password. An example for getpass would be C{{username: password}.get}. """ - self.realm = realm + AbstractTokenGenerator.__init__(self, realm) self.getpass = getpass + def __call__(self, username, algo="md5"): - """Generates an authentification token from a username. - @type username: str - @rtype: str or None - """ assert algo.lower() in ["md5", "md5-sess"] password = self.getpass(username) if password is None: @@ -115,6 +154,77 @@ class AuthTokenGenerator: a1 = "%s:%s:%s" % (username, self.realm, password) return md5(a1).hexdigest() +__all__.append("HtdigestTokenGenerator") +class HtdigestTokenGenerator(AbstractTokenGenerator): + """Reads authentication tokens for L{AuthDigestMiddleware from an + apache htdigest file. + """ + def __init__(self, realm, htdigestfile, ignoreparseerrors=False): + """ + @type realm: str + @type htdigestfile: str + @type ignoreparseerrors: bool + @param ignoreparseerrors: passed to readhtdigest + @raises IOError: + @raises ValueError: + """ + AbstractTokenGenerator.__init__(self, realm) + self.users = {} + self.readhtdigest(htdigestfile, ignoreparseerrors) + + def readhtdigest(self, htdigestfile, ignoreparseerrors=False): + """ + @type htdigestfile: str + @type ignoreparseerrors: bool + @param ignoreparseerrors: do not raise ValueErrors for bad files + @raises IOError: + @raises ValueError: + """ + self.users = {} + for line in file(htdigestfile): + parts = line.rstrip("\n").split(":") + if len(parts) != 3: + if ignoreparseerrors: + continue + raise ValueError("invalid number of colons in htdigest file") + user, realm, token = parts + if realm != self.realm: + continue + if user in self.users and not ignoreparseerrors: + raise ValueError("duplicate user in htdigest file") + self.users[user] = token + + def __call__(self, user, algo="md5"): + assert algo.lower() in ["md5", "md5-sess"] + return self.users.get(user) + +__all__.append("UpdatingHtdigestTokenGenerator") +class UpdatingHtdigestTokenGenerator(HtdigestTokenGenerator): + """Behaves like HtdigestTokenGenerator, checks the htdigest file + for changes on each invocation. + """ + def __init__(self, realm, htdigestfile, ignoreparseerrors=False): + try: + self.statcache = os.stat(htdigestfile) + except OSError, err: + raise IOError(str(err)) + HtdigestTokenGenerator.__init__(self, realm, htdigestfile, + ignoreparseerrors) + self.htdigestfile = htdigestfile + self.ignoreparseerrors = ignoreparseerrors + + def __call__(self, user, algo="md5"): + try: + statcache = os.stat(self.htdigestfile) + except OSError: + return None + if self.statcache != statcache: + try: + self.readhtdigest(self.htdigestfile, self.ignoreparseerrors) + except (IOError, ValueError): + return None + return HtdigestTokenGenerator.__call__(self, user, algo) + __all__.append("NonceStoreBase") class NonceStoreBase: """Nonce storage interface.""" -- cgit v1.2.3