diff options
author | ianb <devnull@localhost> | 2006-01-18 21:50:29 +0000 |
---|---|---|
committer | ianb <devnull@localhost> | 2006-01-18 21:50:29 +0000 |
commit | 47c5d974199a85af11bce55a5d2f1b7933a8ee0d (patch) | |
tree | d21af14b7ec7328e5fbee1b1f9beb2d4faeca937 /paste/auth | |
parent | bfcdad55bf7cd2e23889ebab7a2eb6caaa4d8313 (diff) | |
download | paste-47c5d974199a85af11bce55a5d2f1b7933a8ee0d.tar.gz |
Added middleware for reading mod_auth_tkt-style signed cookies (paste.auth.auth_tkt). Added middleware to set user and group based on IP addresses (paste.auth.grantip). Added some modules for handling ranges of IP addresses, taken from some Python Cookbook recipes -- license is unclear on these, but I've requested clarification from the author (shouldn't be released until that is clarified).
Diffstat (limited to 'paste/auth')
-rw-r--r-- | paste/auth/auth_tkt.py | 303 | ||||
-rw-r--r-- | paste/auth/grantip.py | 111 |
2 files changed, 414 insertions, 0 deletions
diff --git a/paste/auth/auth_tkt.py b/paste/auth/auth_tkt.py new file mode 100644 index 0000000..b2dea54 --- /dev/null +++ b/paste/auth/auth_tkt.py @@ -0,0 +1,303 @@ +########################################################################## +# +# Copyright (c) 2005 Imaginary Landscape LLC and Contributors. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +########################################################################## +""" +Implementation of cookie signing as done in `mod_auth_tkt +<http://www.openfusion.com.au/labs/mod_auth_tkt/>`_. + +mod_auth_tkt is an Apache module that looks for these signed cookies +and sets ``REMOTE_USER``, ``REMOTE_USER_TOKENS`` (a comma-separated +list of groups) and ``REMOTE_USER_DATA`` (arbitrary string data). + +This module is an alternative to the ``paste.auth.cookie`` module; +it's primary benefit is compatibility with mod_auth_tkt, which in turn +makes it possible to use the same authentication process with +non-Python code run under Apache. +""" + +import time as time_mod +import md5 +import cgi +import Cookie +from paste import request + +class AuthTicket(object): + + """ + This class represents an authentication token. You must pass in + the shared secret, the userid, and the IP address. Optionally you + can include tokens (a list of strings, representing role names), + 'user_data', which is arbitrary data available for your own use in + later scripts. Lastly, you can override the cookie name and + timestamp. + + Once you provide all the arguments, use .cookie_value() to + generate the appropriate authentication ticket. .cookie() + generates a Cookie object, the str() of which is the complete + cookie header to be sent. + + CGI usage:: + + token = auth_tkt.AuthTick('sharedsecret', 'username', + os.environ['REMOTE_ADDR'], tokens=['admin']) + print 'Status: 200 OK' + print 'Content-type: text/html' + print token.cookie() + print + ... redirect HTML ... + + Webware usage:: + + token = auth_tkt.AuthTick('sharedsecret', 'username', + self.request().environ()['REMOTE_ADDR'], tokens=['admin']) + self.response().setCookie('auth_tkt', token.cookie_value()) + + Be careful not to do an HTTP redirect after login; use meta + refresh or Javascript -- some browsers have bugs where cookies + aren't saved when set on a redirect. + """ + + def __init__(self, secret, userid, ip, tokens=(), user_data='', + time=None, cookie_name='auth_tkt', + secure=False): + self.secret = secret + self.userid = userid + self.ip = ip + self.tokens = ','.join(tokens) + self.user_data = user_data + if time is None: + self.time = time_mod.time() + else: + self.time = time + self.cookie_name = cookie_name + self.secure = secure + + def digest(self): + return calculate_digest( + self.ip, self.time, self.secret, self.userid, self.tokens, + self.user_data) + + def cookie_value(self): + v = '%s%08x%s!' % (self.digest(), int(self.time), self.userid) + if self.tokens: + v += self.tokens + '!' + v += self.user_data + return v + + def cookie(self): + c = Cookie.SimpleCookie() + c[self.cookie_name] = self.cookie_value().encode('base64').strip().replace('\n', '') + c[self.cookie_name]['path'] = '/' + if self.secure: + c[self.cookie_name]['secure'] = 'true' + return c + +class BadTicket(Exception): + """ + Exception raised when a ticket can't be parsed. If we get + far enough to determine what the expected digest should have + been, expected is set. This should not be shown by default, + but can be useful for debugging. + """ + def __init__(self, msg, expected=None): + self.expected = expected + Exception.__init__(self, msg) + +def parse_ticket(secret, ticket, ip): + """ + Parse the ticket, returning (timestamp, userid, tokens, user_data). + + If the ticket cannot be parsed, ``BadTicket`` will be raised with + an explanation. + """ + ticket = ticket.strip('"') + digest = ticket[:32] + try: + timestamp = int(ticket[32:40], 16) + except ValueError, e: + raise BadTicket('Timestamp is not a hex integer: %s' % e) + try: + userid, data = ticket[40:].split('!', 1) + except ValueError: + raise BadTicket('userid is not followed by !') + if '!' in data: + tokens, user_data = data.split('!', 1) + else: + # @@: Is this the right order? + tokens = '' + user_data = data + + expected = calculate_digest(ip, timestamp, secret, + userid, tokens, user_data) + + if expected != digest: + raise BadTicket('Digest signature is not correct', + expected=(expected, digest)) + + tokens = tokens.split(',') + + return (timestamp, userid, tokens, user_data) + +def calculate_digest(ip, timestamp, secret, userid, tokens, user_data): + digest0 = md5.new( + encode_ip_timestamp(ip, timestamp) + secret + userid + '\0' + + tokens + '\0' + user_data).hexdigest() + digest = md5.new(digest0 + secret).hexdigest() + return digest + +def encode_ip_timestamp(ip, timestamp): + ip_chars = ''.join(map(chr, map(int, ip.split('.')))) + t = int(timestamp) + ts = ((t & 0xff000000) >> 24, + (t & 0xff0000) >> 16, + (t & 0xff00) >> 8, + t & 0xff) + ts_chars = ''.join(map(chr, ts)) + return ip_chars + ts_chars + + +class AuthTKTMiddleware(object): + + """ + Middleware that checks for signed cookies that match what + `mod_auth_tkt <http://www.openfusion.com.au/labs/mod_auth_tkt/>`_ + looks for (if you have mod_auth_tkt installed, you don't need this + middleware, since Apache will set the environmental variables for + you). + + This also adds two functions to the request: + + ``environ['set_user'](username, tokens='', user_data='')`` + + This sets a cookie that logs the user in. ``tokens`` is a + string (comma-separated groups) or a list of strings. + ``user_data`` is a string for your own use. + + ``environ['logout_user']``() + + Logs out the user. + """ + + def __init__(self, app, secret, cookie_name='auth_tkt', secure=False, + include_ip=True): + self.app = app + self.secret = secret + self.cookie_name = cookie_name + self.secure = secure + self.include_ip = include_ip + + def __call__(self, environ, start_response): + cookies = request.get_cookies(environ) + if cookies.has_key(self.cookie_name): + cookie_value = cookies[self.cookie_name].value + else: + cookie_value = '' + if cookie_value: + if self.include_ip: + remote_addr = environ['REMOTE_ADDR'] + else: + # mod_auth_tkt uses this dummy value when IP is not + # checked: + remote_addr = '0.0.0.0' + # @@: This should handle bad signatures better: + # Also, timeouts should cause cookie refresh + timestamp, userid, tokens, user_data = parse_ticket( + secret, cookie, remote_addr) + tokens = ','.join(tokens) + environ['REMOTE_USER'] = userid + if environ.get('REMOTE_USER_TOKENS'): + # We want to add tokens/roles to what's there: + tokens = environ['REMOTE_USER_TOKENS'] + ',' + tokens + environ['REMOTE_USER_TOKENS'] = + environ['REMOTE_USER_DATA'] = user_data + environ['AUTH_TYPE'] = 'cookie' + set_cookies = [] + def set_user(userid, tokens='', user_data=''): + set_cookies.extend(self.set_user_cookie( + environ, userid, tokens, user_data)) + def logout_user(): + set_cookies.extend(self.logout_user_cookie(environ)) + environ['paste.auth_tkt.set_user'] = set_user + environ['paste.auth_tkt.logout_user'] = logout_user + def cookie_setting_start_response(status, headers, exc_info=None): + headers.extend(set_cookies) + return start_response(status, headers, exc_info) + return self.app(environ, cookie_setting_start_response) + + def set_user_cookie(self, environ, userid, tokens, user_data): + if not isinstance(tokens, basestring): + tokens = ','.join(tokens) + if self.include_ip: + remote_addr = environ['REMOTE_ADDR'] + else: + remote_addr = '0.0.0.0' + ticket = AuthTicket( + self.secret, + userid, + remote_addr, + tokens=tokens, + user_data=user_data, + cookie_name=self.cookie_name, + secure=self.secure) + # @@: Should we set REMOTE_USER etc in the current + # environment right now as well? + cookies = [ + ('Set-Cookie', '%s=%s; Path=/' % ( + self.cookie_name, ticket.cookie_value()))] + return cookies + + def logout_user_cookie(self, environ): + cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME')) + wild_domain = '.' + cur_domain + cookies = [ + ('Set-Cookie', '%s=""; Path=/' % self.cookie_name), + ('Set-Cookie', '%s=""; Path=/; Domain=%s' % + (self.cookie_name, cur_domain)), + ('Set-Cookie', '%s=""; Path=/; Domain=%s' % + (self.cookie_name, wild_domain)), + ] + return cookies + +def make_auth_tkt_middleware( + app, + global_conf, + secret=None, + cookie_name='auth_tkt', + secure=False, + include_ip=True): + """ + Creates the `AuthTKTMiddleware + <class-paste.auth.auth_tkt.AuthTKTMiddleware.html>`_. + + ``secret`` is requird, but can be set globally or locally. + """ + from paste.deploy.converters import asbool + secure = asbool(secure) + include_ip = asbool(include_ip) + if secret is None: + secret = global_conf.get('secret') + if not secret: + raise ValueError( + "You must provide a 'secret' (in global or local configuration)") + return AuthTKTMiddleware( + app, secret, cookie_name, secure, include_ip) diff --git a/paste/auth/grantip.py b/paste/auth/grantip.py new file mode 100644 index 0000000..cf6a64c --- /dev/null +++ b/paste/auth/grantip.py @@ -0,0 +1,111 @@ +""" +Grant roles and logins based on IP address. +""" +from paste.util import ip4 + +class GrantIPMiddleware(object): + + """ + On each request, ``ip_map`` is checked against ``REMOTE_ADDR`` + and logins and roles are assigned based on that. + + ``ip_map`` is a map of {ip_mask: (username, roles)}. Either + ``username`` or ``roles`` may be None. Roles may also be prefixed + with ``-``, like ``'-system'`` meaning that role should be + revoked. ``'__remove__'`` for a username will remove the username. + + If ``clobber_username`` is true (default) then any user + specification will override the current value of ``REMOTE_USER``. + ``'__remove__'`` will always clobber the username. + + ``ip_mask`` is something that `paste.util.ip4:IP4Range + <class-paste.util.ip4.IP4Range.html>`_ can parse. Simple IP + addresses, IP/mask, ip<->ip ranges, and hostnames are allowed. + """ + + def __init__(self, app, ip_map, clobber_username=True): + self.app = app + self.ip_map = [] + for key, value in ip_map.items(): + self.ip_map.append((ip4.IP4Range(key), + self._convert_user_role(value[0], value[1]))) + self.clobber_username = clobber_username + + def _convert_user_role(self, username, roles): + if roles and isinstance(roles, basestring): + roles = roles.split(',') + return (username, roles) + + def __call__(self, environ, start_response): + addr = ip4.ip2int(environ['REMOTE_ADDR'], False) + remove_user = False + add_roles = [] + for range, (username, roles) in self.ip_map: + if addr in range: + if roles: + add_roles.extend(roles) + if username == '__remove__': + remove_user = True + elif username: + if (not environ.get('REMOTE_USER') + or self.clobber_username): + environ['REMOTE_USER'] = username + if (remove_user and 'REMOTE_USER' in environ): + del environ['REMOTE_USER'] + if roles: + self._set_roles(environ, add_roles) + return self.app(environ, start_response) + + def _set_roles(self, environ, roles): + cur_roles = environ.get('REMOTE_USER_TOKENS', '').split(',') + # Get rid of empty roles: + cur_roles = filter(None, cur_roles) + remove_roles = [] + for role in roles: + if role.startswith('-'): + remove_roles.append(role[1:]) + else: + if role not in cur_roles: + cur_roles.append(role) + for role in remove_roles: + if role in cur_roles: + cur_roles.remove(role) + environ['REMOTE_USER_TOKENS'] = ','.join(cur_roles) + + +def make_grantip(app, global_conf, clobber_username=False, **kw): + """ + Grant roles or usernames based on IP addresses. + + Config looks like this:: + + [filter:grant] + use = egg:Paste#grantip + clobber_username = true + # Give localhost system role (no username): + 127.0.0.1 = -:system + # Give everyone in 192.168.0.* editor role: + 192.168.0.0/24 = -:editor + # Give one IP the username joe: + 192.168.0.7 = joe + # And one IP is should not be logged in: + 192.168.0.10 = __remove__:-editor + + """ + from paste.deploy.converters import asbool + clobber_username = asbool(clobber_username) + ip_map = {} + for key, value in kw.items(): + if ':' in value: + username, role = value.split(':', 1) + else: + username = value + role = '' + if username == '-': + username = '' + if role == '-': + role = '' + ip_map[key] = value + return GrantIPMiddleware(app, ip_map, clobber_username) + + |