diff options
| author | ianb <devnull@localhost> | 2005-12-13 07:00:20 +0000 |
|---|---|---|
| committer | ianb <devnull@localhost> | 2005-12-13 07:00:20 +0000 |
| commit | 4e73bff9da87e35c7154ab1cc923bb4f9d40711d (patch) | |
| tree | d2e4c92965398700457280d5829dfaa5cdf5b4fb /paste/auth | |
| parent | 55b404e53bc834daf3852069af6de9b1fca4c742 (diff) | |
| download | paste-4e73bff9da87e35c7154ab1cc923bb4f9d40711d.tar.gz | |
Merged changes from cce branch (r3727:HEAD/4008); the branch is now in sync with trunk
Diffstat (limited to 'paste/auth')
| -rw-r--r-- | paste/auth/__init__.py | 1 | ||||
| -rw-r--r-- | paste/auth/basic.py | 67 | ||||
| -rw-r--r-- | paste/auth/cas.py | 94 | ||||
| -rw-r--r-- | paste/auth/cookie.py | 229 | ||||
| -rw-r--r-- | paste/auth/digest.py | 193 | ||||
| -rw-r--r-- | paste/auth/form.py | 73 | ||||
| -rw-r--r-- | paste/auth/multi.py | 81 |
7 files changed, 738 insertions, 0 deletions
diff --git a/paste/auth/__init__.py b/paste/auth/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/paste/auth/__init__.py @@ -0,0 +1 @@ +# diff --git a/paste/auth/basic.py b/paste/auth/basic.py new file mode 100644 index 0000000..8a7e787 --- /dev/null +++ b/paste/auth/basic.py @@ -0,0 +1,67 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +Basic Authentication + +""" +from paste.httpexceptions import HTTPUnauthorized + +class BasicAuthenticator: + """ Implementation of only 'Basic' authentication in 2617 """ + def __init__(self, realm, userfunc): + """ + realm is a globally unique URI like tag:clarkevans.com,2005:basic + that represents the authenticating authority + userfunc(username, password) -> boolean + """ + self.realm = realm + self.userfunc = userfunc + + def build_authentication(self): + head = [('WWW-Authenticate','Basic realm="%s"' % self.realm)] + return HTTPUnauthorized(headers=head) + + def authenticate(self, authorization): + if not authorization: + return self.build_authentication() + (authmeth, auth) = authorization.split(" ",1) + if 'basic' != authmeth.lower(): + return self.build_authentication() + auth = auth.strip().decode('base64') + username, password = auth.split(':') + if self.userfunc(username, password): + return username + return self.build_authentication() + + __call__ = authenticate + +def AuthBasicHandler(application, realm, userfunc): + authenticator = BasicAuthenticator(realm, userfunc) + def basic_application(environ, start_response): + username = environ.get('REMOTE_USER','') + if not username: + authorization = environ.get('HTTP_AUTHORIZATION','') + result = authenticator(authorization) + if isinstance(result,str): + environ['AUTH_TYPE'] = 'basic' + environ['REMOTE_USER'] = result + else: + return result.wsgi_application(environ, start_response) + return application(environ, start_response) + return basic_application + +middleware = AuthBasicHandler + +__all__ = ['AuthBasicHandler'] + +if '__main__' == __name__: + realm = 'tag:clarkevans.com,2005:basic' + def userfunc(username, password): + return username == password + from paste.wsgilib import dump_environ + from paste.util.baseserver import serve + from paste.httpexceptions import * + serve(HTTPExceptionHandler( + AuthBasicHandler(dump_environ, realm, userfunc))) diff --git a/paste/auth/cas.py b/paste/auth/cas.py new file mode 100644 index 0000000..193b79a --- /dev/null +++ b/paste/auth/cas.py @@ -0,0 +1,94 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +CAS 1.0 Authentication + +The Central Authentication System is a straight-forward single sign-on +mechanism developed by Yale University's ITS department. It has since +enjoyed widespread success and is deployed at many major universities +and some corporations. + + https://clearinghouse.ja-sig.org/wiki/display/CAS/Home + http://www.yale.edu/tp/auth/usingcasatyale.html + +This implementation has the goal of maintaining current path arguments +passed to the system so that it can be used as middleware at any stage +of processing. It has the secondary goal of allowing for other +authentication methods to be used concurrently. +""" +import urllib +from paste.wsgilib import construct_url +from paste.httpexceptions import HTTPSeeOther, HTTPForbidden + +class CASLoginFailure(HTTPForbidden): + """ The exception raised if the authority returns 'no' """ + +class CASAuthenticate(HTTPSeeOther): + """ The exception raised to authenticate the user """ + +def AuthCASHandler(application, authority): + """ + This middleware implements CAS 1.0 Authentication There are several + possible outcomes: + + 0. If the REMOTE_USER environment variable is already populated; + then this middleware is a no-op, and the request is passed along + to the application. + + 1. If a query argument 'ticket' is found, then an attempt to + validate said ticket /w the authentication service done. If the + ticket is not validated; an 403 'Forbidden' exception is raised. + Otherwise, the REMOTE_USER variable is set with the NetID that + was validated and AUTH_TYPE is set to "cas". + + 2. Otherwise, a 303 'See Other' is returned to the client directing + them to login using the CAS service. After logon, the service + will send them back to this same URL, only with a 'ticket' query + argument. + + authority: + This is a fully-qualified URL to a CAS 1.0 service. The URL + should end with a '/' and have the 'login' and 'validate' + sub-paths as described in the CAS 1.0 documentation. + """ + assert authority.endswith("/") and authority.startswith("http") + def cas_application(environ, start_response): + username = environ.get('REMOTE_USER','') + if username: + return application(environ, start_response) + qs = environ.get('QUERY_STRING','').split("&") + if qs and qs[-1].startswith("ticket="): + # assume a response from the authority + ticket = qs.pop().split("=",1)[1] + environ['QUERY_STRING'] = "&".join(qs) + service = construct_url(environ) + args = urllib.urlencode( + {'service': service,'ticket': ticket}) + requrl = authority + "validate?" + args + result = urllib.urlopen(requrl).read().split("\n") + if 'yes' == result[0]: + environ['REMOTE_USER'] = result[1] + environ['AUTH_TYPE'] = 'cas' + return application(environ, start_response) + exce = CASLoginFailure() + else: + service = construct_url(environ) + args = urllib.urlencode({'service': service}) + location = authority + "login?" + args + exce = CASAuthenticate(location) + return exce.wsgi_application(environ, start_response) + return cas_application + +middleware = AuthCASHandler + +__all__ = ['CASLoginFailure', 'CASAuthenticate', 'AuthCASHandler' ] + +if '__main__' == __name__: + authority = "https://secure.its.yale.edu/cas/servlet/" + from paste.wsgilib import dump_environ + from paste.util.baseserver import serve + from paste.httpexceptions import * + serve(HTTPExceptionHandler( + AuthCASHandler(dump_environ, authority))) diff --git a/paste/auth/cookie.py b/paste/auth/cookie.py new file mode 100644 index 0000000..96071d7 --- /dev/null +++ b/paste/auth/cookie.py @@ -0,0 +1,229 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +Cookie "Saved" Authentication + +This Authentication middleware saves the current REMOTE_USER, and any +other environment variables specified, in a cookie so that it can be +retrieved during the next request without requiring re-authentication. +This uses a session cookie on the client side (so it goes away when the +user closes their window) and does server-side expiration. + + NOTE: If you use HTTPFound or other redirections; it is likely that + this module will not work unless it is _before_ the middleware + that converts the exception into a response. Therefore, in your + component stack, put this component darn near the top (before + the exception handler). + +According to the cookie specifications, RFC2068 and RFC2109, browsers +should allow each domain at least 20 cookies; each one with a content +size of at least 4k (4096 bytes). This is rather small; so one should +be parsimonious in your cookie name/sizes. +""" +import sha, base64, random, time, string, warnings +from paste.wsgilib import get_cookies + +def make_time(value): + """ return a human readable timestmp """ + return time.strftime("%Y%m%d%H%M",time.gmtime(value)) +_signature_size = len(sha.sha("").digest()) +_header_size = _signature_size + len(make_time(time.time())) + +# build encode/decode functions to safely pack away values +_encode = [('\\','\\x5c'),('"','\\x22'),('=','\\x3d'),(';','\\x3b')] +_decode = [(v,k) for (k,v) in _encode] +_decode.reverse() +def encode(s, sublist = _encode): + return reduce((lambda a,(b,c): string.replace(a,b,c)), sublist, str(s)) +decode = lambda s: encode(s,_decode) + +class CookieTooLarge(RuntimeError): + def __init__(self, content, cookie): + RuntimeError.__init__("Signed cookie exceeds maximum size of 4096") + self.content = content + self.cookie = cookie + +class CookieSigner: + """ + This class converts content into a timed and digitally signed + cookie, as well as having the facility to reverse this procedure. + If the cookie, after the content is encoded and signed exceeds the + maximum length (4096), then CookieTooLarge exception is raised. + + The timeout of the cookie is handled on the server side for a few + reasons. First, if a 'Expires' directive is added to a cookie, then + the cookie becomes persistent (lasting even after the browser window + has closed). Second, the user's clock may be wrong (perhaps + intentionally). The timeout is specified in minutes; and expiration + date returned is rounded to one second. + """ + def __init__(self, secret = None, timeout = None, maxlen = None): + self.timeout = timeout or 30 + self.maxlen = maxlen or 4096 + self.secret = secret or sha.sha(str(random.random()) + + str(time.time())).digest() + + def sign(self, content): + """ + Sign the content returning a valid cookie (that does not + need to be escaped and quoted). The expiration of this + cookie is handled server-side in the auth() function. + """ + cookie = base64.b64encode( + sha.sha(content+self.secret).digest() + + make_time(time.time()+60*self.timeout) + + content).replace("/","_").replace("=","~") + if len(cookie) > self.maxlen: + raise CookieTooLarge(content,cookie) + return cookie + + def auth(self,cookie): + """ + Authenticate the cooke using the signature, verify that it + has not expired; and return the cookie's content + """ + decode = base64.b64decode( + cookie.replace("_","/").replace("~","=")) + signature = decode[:_signature_size] + expires = decode[_signature_size:_header_size] + content = decode[_header_size:] + if signature == sha.sha(content+self.secret).digest(): + if int(expires) > int(make_time(time.time())): + return content + else: + # This is the normal case of an expired cookie; just + # don't bother doing anything here. + pass + else: + # This case can happen if the server is restarted with a + # different secret; or if the user's IP address changed + # due to a proxy. However, it could also be a break-in + # attempt -- so should it be reported? + pass + +class AuthCookieEnviron(list): + """ + This object is a list of `environ` keys that were restored from or + will be added to the digially signed cookie. This object can be + accessed from an `environ` variable by using this module's name. + + environ['paste.auth.cookie'].append('your.environ.variable') + + This environment-specific object can also be used to access/configure + the base handler for all requests by using: + + environ['paste.auth.cookie'].handler + + """ + def __init__(self, handler, scanlist): + list.__init__(self, scanlist) + self.handler = handler + def append(self, value): + if value in self: + return + list.append(self,str(value)) + +class AuthCookieHandler: + """ + This middleware uses cookies to stash-away a previously authenticated + user (and perhaps other variables) so that re-authentication is not + needed. This does not implement sessions; and therefore N servers + can be syncronized to accept the same saved authentication if they + all use the same cookie_name and secret. + + By default, this handler scans the `environ` for the REMOTE_USER + key; if found, it is stored. It can be configured to scan other + `environ` keys as well -- but be careful not to exceed 2-3k (so that + the encoded and signed cookie does not exceed 4k). You can ask it + to handle other environment variables by doing: + + environ['paste.auth.cookie'].append('your.environ.variable') + + """ + environ_name = 'paste.auth.cookie' + signer_class = CookieSigner + environ_class = AuthCookieEnviron + + def __init__(self, application, cookie_name=None, secret=None, + timeout=None, maxlen=None, signer=None, scanlist = None): + if not signer: + signer = self.signer_class(secret,timeout,maxlen) + self.signer = signer + self.scanlist = scanlist or ('REMOTE_USER',) + self.application = application + self.cookie_name = cookie_name or 'PASTE_AUTH_COOKIE' + + def __call__(self, environ, start_response): + if self.environ_name in environ: + raise AssertionError("AuthCookie already installed!") + scanlist = self.environ_class(self,self.scanlist) + jar = get_cookies(environ) + if jar.has_key(self.cookie_name): + content = self.signer.auth(jar[self.cookie_name].value) + if content: + for pair in content.split(";"): + (k,v) = pair.split("=") + k = decode(k) + if k not in scanlist: + scanlist.append(k) + if k in environ: + continue + environ[k] = decode(v) + if 'REMOTE_USER' == k: + environ['AUTH_TYPE'] = 'cookie' + environ[self.environ_name] = scanlist + if "paste.httpexceptions" in environ: + warnings.warn("Since paste.httpexceptions is hooked in your " + "processing chain before paste.auth.cookie, if an " + "HTTPRedirection is raised, the cookies this module sets " + "will not be included in your response.\n") + + def response_hook(status, response_headers, exc_info=None): + """ + Scan the environment for keys specified in the scanlist, + pack up their values, signs the content and issues a cookie. + """ + scanlist = environ.get(self.environ_name) + assert scanlist and isinstance(scanlist,self.environ_class) + content = [] + for k in scanlist: + v = environ.get(k,None) + if v is not None: + content.append("%s=%s" % (encode(k),encode(v))) + if content: + content = ";".join(content) + content = self.signer.sign(content) + cookie = '%s=%s; Path=/;' % (self.cookie_name, content) + if 'https' == environ['wsgi.url_scheme']: + cookie += ' secure;' + response_headers.append(('Set-Cookie',cookie)) + return start_response(status, response_headers, exc_info) + return self.application(environ, response_hook) + +middleware = AuthCookieHandler + +__all__ = ['AuthCookieHandler'] + +if '__main__' == __name__: + from paste.wsgilib import parse_querystring + def AuthStupidHandler(application): + def authstupid_application(environ, start_response): + args = dict(parse_querystring(environ)) + user = args.get('user','') + if user: + environ['REMOTE_USER'] = user + environ['AUTH_TYPE'] = 'stupid' + test = args.get('test','') + if test: + environ['paste.auth.cookie.test'] = test + environ['paste.auth.cookie'].append('paste.auth.cookie.test') + return application(environ, start_response) + return authstupid_application + from paste.wsgilib import dump_environ + from paste.util.baseserver import serve + from paste.httpexceptions import * + serve(AuthCookieHandler( + HTTPExceptionHandler( + AuthStupidHandler(dump_environ)))) diff --git a/paste/auth/digest.py b/paste/auth/digest.py new file mode 100644 index 0000000..598a103 --- /dev/null +++ b/paste/auth/digest.py @@ -0,0 +1,193 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +HTTP Digest Authentication (RFC 2617) + +NOTE: This has not been audited by a security expert, please use + with caution (or better yet, report security holes). + + At this time, this implementation does not provide for further + challenges, nor does it support Authentication-Info header. It + also uses md5, and an option to use sha would be a good thing. +""" +from paste.httpexceptions import HTTPUnauthorized +import md5, time, random, urllib2 + +def digest_password(username, realm, password): + """ Constructs the appropriate hashcode needed for HTTP Digest """ + return md5.md5("%s:%s:%s" % (username,realm,password)).hexdigest() + +def response(challenge, realm, path, username, password): + """ + Build an authorization response for a given challenge. This + implementation uses urllib2 to do the dirty work. + """ + auth = urllib2.AbstractDigestAuthHandler() + auth.add_password(realm,path,username,password) + (token,challenge) = challenge.split(' ',1) + chal = urllib2.parse_keqv_list(urllib2.parse_http_list(challenge)) + class FakeRequest: + def get_full_url(self): + return path + def has_data(self): + return False + def get_method(self): + return "GET" + get_selector = get_full_url + return "Digest %s" % auth.get_authorization(FakeRequest(), chal) + +class DigestAuthenticator: + """ Simple implementation of RFC 2617 - HTTP Digest Authentication """ + def __init__(self, realm, userfunc): + """ + realm is a globally unique URI, like tag:clarkevans.com,2005:bing + userfunc(realm, username) -> MD5('%s:%s:%s') % (user,realm,pass) + """ + self.nonce = {} # list to prevent replay attacks + self.userfunc = userfunc + self.realm = realm + + def build_authentication(self, stale = ''): + """ raises an authentication exception """ + nonce = md5.md5("%s:%s" % (time.time(),random.random())).hexdigest() + opaque = md5.md5("%s:%s" % (time.time(),random.random())).hexdigest() + self.nonce[nonce] = None + parts = { 'realm': self.realm, 'qop': 'auth', + 'nonce': nonce, 'opaque': opaque } + if stale: + parts['stale'] = 'true' + head = ", ".join(['%s="%s"' % (k,v) for (k,v) in parts.items()]) + head = [("WWW-Authenticate", 'Digest %s' % head)] + return HTTPUnauthorized(headers=head) + + def compute(self, ha1, username, response, method, + path, nonce, nc, cnonce, qop): + """ computes the authentication, raises error if unsuccessful """ + if not ha1: + return self.build_authentication() + ha2 = md5.md5('%s:%s' % (method,path)).hexdigest() + if qop: + chk = "%s:%s:%s:%s:%s:%s" % (ha1,nonce,nc,cnonce,qop,ha2) + else: + chk = "%s:%s:%s" % (ha1,nonce,ha2) + if response != md5.md5(chk).hexdigest(): + if nonce in self.nonce: + del self.nonce[nonce] + return self.build_authentication() + pnc = self.nonce.get(nonce,'00000000') + if nc <= pnc: + if nonce in self.nonce: + del self.nonce[nonce] + return self.build_authentication(stale = True) + self.nonce[nonce] = nc + return username + + def authenticate(self, authorization, path, method): + """ This function takes the value of the 'Authorization' header, + the method used (e.g. GET), and the path of the request + relative to the server. The function either returns an + authenticated user, or it raises an exception. + """ + if not authorization: + return self.build_authentication() + (authmeth, auth) = authorization.split(" ",1) + if 'digest' != authmeth.lower(): + return self.build_authentication() + amap = {} + for itm in auth.split(", "): + (k,v) = [s.strip() for s in itm.split("=",1)] + amap[k] = v.replace('"','') + try: + username = amap['username'] + authpath = amap['uri'] + nonce = amap['nonce'] + realm = amap['realm'] + response = amap['response'] + assert authpath.split("?",1)[0] in path + assert realm == self.realm + qop = amap.get('qop','') + cnonce = amap.get('cnonce','') + nc = amap.get('nc','00000000') + if qop: + assert 'auth' == qop + assert nonce and nc + except: + return self.build_authentication() + ha1 = self.userfunc(realm,username) + return self.compute(ha1, username, response, method, authpath, + nonce, nc, cnonce, qop) + + __call__ = authenticate + +def AuthDigestHandler(application, realm, userfunc): + """ + This middleware implements HTTP Digest authentication (RFC 2617) on + the incoming request. There are several possible outcomes: + + 0. If the REMOTE_USER environment variable is already populated; + then this middleware is a no-op, and the request is passed along + to the application. + + 1. If the HTTP_AUTHORIZATION header was not provided, then a + HTTPUnauthorized exception is raised containing the challenge. + + 2. If the HTTP_AUTHORIZATION header specifies anything other + than digest; the REMOTE_USER is left unset and application + processing continues. + + 3. If the response is malformed or or if the user's credientials + do not pass muster, another HTTPUnauthorized is raised. + + 4. IF all goes well, and the user's credintials pass; then + REMOTE_USER environment variable is filled in and the + AUTH_TYPE is listed as 'digest'. + + Besides the application to delegate requests, this middleware + requires two additional arguments: + + realm: + This is a globally unique identifier used to indicate the + authority that is performing the authentication. The taguri + such as tag:yourdomain.com,2006 is sufficient. + + userfunc: + This is a callback function which performs the actual + authentication; the signature of this callback is: + + userfunc(realm, username) -> hashcode + + This module provides a 'digest_password' helper function which + can help construct the hashcode; it is recommended that the + hashcode is stored in a database, not the user's actual password. + """ + authenticator = DigestAuthenticator(realm, userfunc) + def digest_application(environ, start_response): + username = environ.get('REMOTE_USER','') + if not username: + method = environ['REQUEST_METHOD'] + fullpath = environ['SCRIPT_NAME'] + environ["PATH_INFO"] + authorization = environ.get('HTTP_AUTHORIZATION','') + result = authenticator(authorization, fullpath, method) + if isinstance(result, str): + environ['AUTH_TYPE'] = 'digest' + environ['REMOTE_USER'] = result + else: + return result.wsgi_application(environ, start_response) + return application(environ, start_response) + return digest_application + +middleware = AuthDigestHandler + +__all__ = ['digest_password', 'AuthDigestHandler' ] + +if '__main__' == __name__: + realm = 'tag:clarkevans.com,2005:digest' + def userfunc(realm, username): + return digest_password(username, realm, username) + from paste.wsgilib import dump_environ + from paste.util.baseserver import serve + from paste.httpexceptions import * + serve(HTTPExceptionHandler( + AuthDigestHandler(dump_environ, realm, userfunc))) diff --git a/paste/auth/form.py b/paste/auth/form.py new file mode 100644 index 0000000..b53952f --- /dev/null +++ b/paste/auth/form.py @@ -0,0 +1,73 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +HTTP Form Authentication + +""" +from paste.wsgilib import parse_formvars, construct_url + +template = """\ +<html> + <head><title>Please Login</title></head> + <body> + <h1>Please Login</h1> + <form action="%s" method="post"> + <dl> + <dt>Username:</dt> + <dd><input type="text" name="username"></dd> + <dt>Password:</dt> + <dd><input type="password" name="password"></dd> + </dl> + <input type="submit" name="authform" /> + <hr /> + </form> + </body> +</html> +""" + +def AuthFormHandler(application, userfunc, login_page = None): + """ This causes a HTML form to be returned if REMOTE_USER has not + been provided. This is a really simple implementation, it + requires that the query arguments returned from the form have two + variables "username" and "password". These are then passed to + the userfunc; which should return True if authentication is granted. + """ + login_page = login_page or template + def form_application(environ, start_response): + username = environ.get('REMOTE_USER','') + if username: + return application(environ, start_response) + if 'POST' == environ['REQUEST_METHOD']: + formvars = parse_formvars(environ) + username = formvars.get('username') + password = formvars.get('password') + if username and password: + if userfunc(username,password): + environ['AUTH_TYPE'] = 'form' + environ['REMOTE_USER'] = username + environ['REQUEST_METHOD'] = 'GET' + del environ['paste.parsed_formvars'] + return application(environ, start_response) + start_response("200 OK",(('Content-Type', 'text/html'), + ('Content-Length', len(login_page)))) + if "%s" in login_page: + return [login_page % construct_url(environ) ] + return [login_page] + return form_application + +middleware = AuthFormHandler + +__all__ = ['AuthFormHandler'] + +if '__main__' == __name__: + def userfunc(username, password): + return username == password + from paste.wsgilib import dump_environ + from paste.util.baseserver import serve + from paste.httpexceptions import * + from cookie import AuthCookieHandler + serve(HTTPExceptionHandler( + AuthCookieHandler( + AuthFormHandler(dump_environ, userfunc)))) diff --git a/paste/auth/multi.py b/paste/auth/multi.py new file mode 100644 index 0000000..1b593ae --- /dev/null +++ b/paste/auth/multi.py @@ -0,0 +1,81 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +Multi Authentication + +In some environments, the choice of authentication method to be used +depends upon the environment and is not "fixed". This middleware +allows N authentication methods to be registered along with a goodness +function which determines which method should be used. + +Strictly speaking this is not limited to authentication, but it is a +common requirement in that domain; this is why it isn't named +AuthMultiHandler (for now). +""" + +class MultiHandler: + """ This middleware provides two othogonal facilities: + (a) a way to register any number of middlewares + (b) a way to register predicates which cause one of + the registered middlewares to be used + If none of the predicates returns True, then the + application is invoked directly without middleware + """ + def __init__(self, application): + self.application = application + self.default = application + self.binding = {} + self.predicate = [] + def add_method(self, name, factory, *args, **kwargs): + self.binding[name] = factory(self.application, *args, **kwargs) + def add_predicate(self, name, checker): + self.predicate.append((checker,self.binding[name])) + def set_default(self, name): + """ + This method sets the default middleware to be executed, + if none of the rules apply. + """ + self.default = self.binding[name] + def set_query_argument(self, name, key = '*authmeth', value = None): + """ + This method indicates that the named middleware component should + be executed if the given key/value pair occurs in the query args. + """ + lookfor = "%s=%s" % (key, value or name) + self.add_predicate(name, + lambda environ: lookfor in environ.get('QUERY_STRING','')) + def __call__(self, environ, start_response): + for (checker,binding) in self.predicate: + if checker(environ): + return binding(environ, start_response) + return self.default(environ, start_response) + +middleware = MultiHandler + +__all__ = ['MultiHandler'] + +if '__main__' == __name__: + import basic, digest, cas, cookie, form + from paste.httpexceptions import * + from paste.wsgilib import dump_environ + from paste.util.baseserver import serve + multi = MultiHandler(dump_environ) + multi.add_method('basic',basic.middleware, + 'tag:clarkevans.com,2005:basic', + lambda n,p: n == p ) + multi.set_query_argument('basic') + multi.add_method('digest',digest.middleware, + 'tag:clarkevans.com,2005:digest', + lambda r,u: digest.digest_password(u,r,u)) + multi.set_query_argument('digest') + multi.add_method('form',lambda ap: cookie.middleware( + form.middleware(ap, + lambda n,p: n == p))) + multi.set_query_argument('form') + #authority = "https://secure.its.yale.edu/cas/servlet/" + #multi.add_method('cas',lambda ap: cookie.middleware( + # cas.middleware(ap,authority))) + #multi.set_default('cas') + serve(HTTPExceptionHandler(multi)) |
