|
perry.tew at cibavision.novartis.com
perry.tew at cibavision.novartis.com
Mon Oct 20 08:52:15 EST 2003
David,
I just wrote some code for ticket based authentication. It's not
completely where I want it, but it may provide you a framework to get
started. It works for me, but I'm using client certificates for
authentication. You'd need to change the TicketMaster.py script to check
for basic authentication. Here's the code, along with my httpd.conf
settings. If you do use it, I would appreciate criticism and suggestions
for improvement as I'm new to python and may be doing some Java-ish things
that have better solutions in Python. Also, it's still a work in progress
concerning documentation. FYI, I basically ported the TicketMaster
example in the "Writing Apache modules in Perl and C" book.
Inside of my TicketAccessHandler.py is also some authorization stuff going
against an RDBMS. It's pretty sweet. It's set for an intranet to allow
managers of content to handle adding and deleting users from roles rather
than bogging down a site admin with such mundane tasks. It can run
completely in memory and updates are event driven and take place in a
cleanup handler. If you're interested in that, I can sent you the code
for it as well (sql, front end, etc), but I suspect your first priority is
the md5 ticket cookie, so at least the TicketTool.py should be an pretty
exact match of what you may need.
Hope this helps,
-Perry Tew
# ===========================================================
# ===========================================================
# ===========================================================
# TicketAuthenHandler.py ======================================
from mod_python import apache
import TicketTool
from ApachePool import ApachePool
dbpool = ApachePool()
def accesshandler(req):
#apache.log_error("[TicketAuthenHandler] in handler() method")
# ===========================================================
# Authenication secition. This handler contains both authen
# and authz. This is because it doesn't use the default
# Basic authentication. Without using Basic, I can't get the
# authz handler to be called. So, I have them together here
# ===========================================================
# the NameError should only happen once in the life of the apache
child process
global ticketTool
try:
result, msg, user = ticketTool.verify_ticket(req)
except NameError:
# if the ticketTool hasn't been created yet, then do so.
ticketTool = TicketTool.TicketTool(req)
result, msg, user = ticketTool.verify_ticket(req)
#apache.log_error("[TicketAuthenHandler] verify_ticket result was
" + str(result))
#apache.log_error("[TicketAuthenHandler] verify_ticket msg was " +
msg)
# ditch the call if something wasn't correct
if result == 0:
#apache.log_error(msg, apache.APLOG_WARNING)
cookie = ticketTool.make_return_address(req)
cout = cookie.output(header="")
#apache.log_error( "cookie going out:" + cout)
req.err_headers_out['Set-Cookie'] = cout
req.err_headers_out['Pragma'] = 'no-cache'
req.err_headers_out['Cache-Control'] = 'no-cache'
req.err_headers_out['Expires'] = '-1'
return apache.HTTP_FORBIDDEN
# ===========================================================
# Authorization section
# Make sure user's roles are sufficient to access uri
# ===========================================================
#apache.log_error("[TicketAuthzHandler] in handler() method")
uri_roles = get_uri_roles(req)
#apache.log_error("URI ROLES:" + str(uri_roles))
user_roles = get_user_roles(req, user)
#apache.log_error("USER ROLES:" + ",".join(user_roles.keys()))
for a_uri, some_roles in uri_roles.items():
#apache.log_error( "AUTHZ: checking " + a_uri )
unauthorized = 1
if some_roles is not None:
for a_role in some_roles:
#apache.log_error("AUTHZ: examining role:"
+ a_role)
if user_roles.has_key(a_role):
#apache.log_error("AUTHZ: MATCH
>>" + a_role)
unauthorized = 0
else:
#apache.log_error( "AUTHZ: " + a_uri + " is not
protected" )
unauthorized = 0
if unauthorized:
#apache.log_error("AUTHZ: FAILED")
# can't return FORBIDDEN, since that may be used
# by the TicketAuthenHandler to redirect to the
ticket
# master, and I sure don't want to do that for a
missing
# role.
return apache.HTTP_PAYMENT_REQUIRED
return apache.OK
# ===========================================================
# End of main handler
# ===========================================================
def get_user_roles(req, user):
global userCache
global dbpool
try:
if userCache.has_key(user):
#apache.log_error("found user roles in cache")
return userCache[user]
except NameError:
#apache.log_error("userCache doesn't exist, creating it")
userCache = {}
#apache.log_error("userCache: " + str(userCache.keys()))
#apache.log_error("DB: retrieving user roles from database")
db = dbpool.get_connection()
c = db.cursor()
c.execute( "SELECT ROLE_NAME FROM AUTH_USER_ROLES WHERE DN ='%s'"
% (user,))
rset = c.fetchall()
roles = {}
for row in rset:
roles[row[0]] = None
userCache[user] = roles
return roles
c.close()
db.commit()
db.close()
def get_uri_roles(req):
"""
break up the uri, make sure each part or substring of the
uri is cached, then
retrieve the roles from the uriCache
"""
global uriCache
global dbpool
paths = get_paths(req)
uri_roles = [] # list of dicts
try:
uncached_paths = [a_path for a_path in paths if
uriCache.has_key(a_path) == 0]
except NameError:
#apache.log_error("uriCache doesn't exist, creating it")
init_uri_cache(req)
uncached_paths = [a_path for a_path in paths if
uriCache.has_key(a_path) == 0]
if len(uncached_paths) > 0:
#apache.log_error("DB: retrieving uncached uri roles from
database: " + str(uncached_paths))
path_str = ",".join(map(add_quotes, uncached_paths))
db = dbpool.get_connection()
c = db.cursor()
sql = "SELECT URI, ROLE_NAME FROM AUTH_URI WHERE URI IN
(%s)" % path_str
c.execute( sql )
rset = c.fetchall()
for row in rset:
if not uriCache.has_key(row[0]):
uriCache[row[0]] = []
uriCache[row[0]].append(row[1])
c.close()
db.commit()
db.close()
# once all of the request_uri have been updated in the
database, there
# may be more that need updating. For this, assign an
empty hash for those
# uris
uncached_paths = [a_path for a_path in paths if
uriCache.has_key(a_path) == 0]
for i in uncached_paths:
uriCache[i] = None
uri_roles = {}
for i in paths:
uri_roles[i] = uriCache[i]
return uri_roles
def init_user_cache(req):
global userCache
global dbpool
opts = req.get_options()
fully_load = 'no'
try:
fully_load = opts['fullyLoadCache'].lower()
except KeyError:
pass
if fully_load == 'yes':
#apache.log_error("USER CACHE: fully loading from
database")
db = dbpool.get_connection()
c = db.cursor()
c.execute( "SELECT DN, ROLE_NAME FROM AUTH_USER_ROLES")
rset = c.fetchall()
tmp = {}
roles = {}
for row in rset:
if tmp.has_key(row[0]) == 0:
tmp[row[0]] = {}
tmp[row[0]][row[1]] = None
c.close()
db.commit()
db.close()
userCache = tmp
else:
userCache = {}
def init_uri_cache(req):
global uriCache
global dbpool
opts = req.get_options()
fully_load = 'no'
try:
fully_load = opts['fullyLoadCache'].lower()
except KeyError:
pass
if fully_load == 'yes':
#apache.log_error("USER CACHE: fully loading from
database")
db = dbpool.get_connection()
c = db.cursor()
sql = "SELECT URI, ROLE_NAME FROM AUTH_URI"
c.execute( sql )
rset = c.fetchall()
tmp = {}
for row in rset:
if not tmp.has_key(row[0]):
tmp[row[0]] = []
tmp[row[0]].append(row[1])
uriCache = tmp
c.close()
db.commit()
db.close()
else:
uriCache = {}
def add_quotes(val):
return "'" + val + "'"
def get_paths(req):
uri_path = req.parsed_uri[apache.URI_PATH]
#apache.log_error("URI PATH:" + uri_path)
dirs = uri_path.split("/")
current_path = ''
paths = []
i = 0
while i < len(dirs) - 1:
if dirs[i] == '':
paths.append('/')
else:
#current_path = current_path + dirs[i] + '/'
current_path = current_path + '/' + dirs[i]
paths.append(current_path)
##apache.log_error("URI PATH current path:" +
current_path)
i = i + 1
paths.append(uri_path)
#apache.log_error("URI PATH current path:" + str(paths))
return paths
def update_cache(req, userParm=None, uriParm=None):
"""
If user = None, do nothing for the userCache.
If user = 'ALL', recreate the entire cache
If user = other, then delete just that user from the cache
The same applies to the uri.
All of this crap should be moved to the Authz handler, eh?
"""
global userCache
global uriCache
if userParm is not None:
if userParm == 'ALL':
apache.log_error("[TicketAuthenHandler] CLEARING
ENTIRE USER CACHE")
init_user_cache(req)
else:
if userCache.has_key(userParm):
apache.log_error("[TicketAuthenHandler]
Clearing %s from userCache" % userParm)
del(userCache[userParm])
else:
pass
#apache.log_error("[TicketAuthenHandler]
Invalid request to clearCache. %s was not found in userCache" % userParm)
if uriParm is not None:
if uriParm == 'ALL':
apache.log_error("[TicketAuthenHandler] CLEARING
ENTIRE URI CACHE")
uriCache = {}
else:
if uriCache.has_key(uriParm):
apache.log_error("[TicketAuthenHandler]
Clearing %s from uriCache" % uriParm)
del(uriCache[uriParm])
else:
pass
#apache.log_error("[TicketAuthenHandler]
Invalid request to clearCache. %s was not found in uriCache" % uriParm)
# ===========================================================
# ===========================================================
# ===========================================================
# ===========================================================
# ===========================================================
# ===========================================================
# TicketMaster.py =============================================
#TODO - this method should verify that the user indeed exists in the user
#table of the auth system
import TicketTool
import Cookie
from mod_python import apache
from mod_python.util import FieldStorage
ticketTool = None
def handler(req):
apache.log_error( "[TicketMaster] calling handler() method" )
# this will only need doing once during the life of the apache
child process
global ticketTool
if ticketTool == None:
ticketTool = TicketTool.TicketTool(req)
req.add_common_vars()
request_uri = None
# 1. check for a paramater named request_uri
# 2. check for a cookie named request_uri
# 3. check for a req.prev uri
fields = FieldStorage(req)
if fields.has_key('request_uri'):
request_uri = fields['request_uri']
else:
apache.log_error( "[TicketMaster] no request_uri param" )
if req.prev:
request_uri = req.prev.unparsed_uri
apache.log_error( "[TicketMaster] have a prev
request_uri:" + request_uri )
else:
cookies = Cookie.SimpleCookie()
try:
apache.log_error( "[TicketMaster] cookie
headers_in:" + req.headers_in['Cookie'] )
cookies.load(req.headers_in['Cookie'])
request_uri = cookies['request_uri'].value
#request_uri = cookies['request_uri']
apache.log_error( "[TicketMaster] have a
cookie request_uri:" + str(request_uri) )
except KeyError:
apache.log_error( "[TicketMaster] no
cookies were found, what now?" )
# if nothing by here, display and error and move on with life.
# it's too short
if request_uri == None:
apache.log_error( "[TicketMaster] no request_uri could be
found" )
no_cookie_error(req)
return apache.OK
user = ''
try:
user = req.subprocess_env['SSL_CLIENT_S_DN']
apache.log_error("[TicketMaster] user dn:" + user)
except KeyError:
apache.log_error("[TicketMaster] no SSL DN env variable!"
)
display_missing_cert_screen(req, request_uri)
return apache.OK
result = 0
msg = ''
if user:
# I don't authenticate here, since the SSL layer does that
# for me
try:
ticket = ticketTool.make_ticket(req, user)
go_to_uri(req, request_uri, ticket)
return apache.OK
except:
apache.log_error( 'could not create ticket,
missing secret key?', apache.APLOG_ERR)
raise
#return apache.HTTP_INTERNAL_SERVER_ERROR
apache.log_error( "[TicketMaster] no req.user, so cannot make
ticket" )
display_missing_cert_screen(req, request_uri)
return apache.OK
def go_to_uri(req, request_uri, ticket):
apache.log_error( "[TicketMaster] sending refresh to browser to go
here:" + request_uri)
apache.log_error( "[TicketMaster] setting the following cookie:" +
ticket.output(header=""))
req.content_type = 'text/html'
req.headers_out['Set-Cookie'] = ticket.output(header="")
# the following line causes MSIE to wig out, so don't uncomment
it.
#req.headers_out['Refresh'] = '1;' + request_uri
req.headers_out['Pragma'] = 'no-cache'
req.headers_out['Cache-Control'] = 'no-cache'
req.headers_out['Expires'] = '-1'
#req.send_http_header()
req.write("""
<html>
<head>
<title>Successfully Authenticated</title>
</head>
<body>
<h4>Congratulations, you have successfully
authenticated</h4>
<p>Click <a href="%s">here</a> to
continue</p>
<p>A nice explanation about the cookie I
just set would be swell</p>
</body>
</html>
""" % request_uri)
return apache.OK
def display_missing_cert_screen(req,request_uri):
req.content_type = 'text/html'
req.write("""
<html>
<head>
<title>Missing Entrust PKI
Certificate</title>
</head>
<body>
<p>The page you attempted to view (%s) was
protected.</p>
<p>Protection for this web site is based
on Digital Certificate technology.
You need a PKI certificate to access this
portion of the website.
Contact Human Resources.</p>
</body>
</html>""" % request_uri)
def no_cookie_error(req):
req.content_type = 'text/html'
req.write("""
<html>
<head>
<title>Unable to Log In</title>
</head>
<body>
<h4>Unable to Log In</h4>
<p>This site uses cookies for security.
Your browser must be capable
of processing cookies <em>and</em> cookies must be
activated.
Please set your browser to accept cookies,
then press the
<strong>reload</strong> button.</p>
<hr>
</body>
</html>""")
# ===========================================================
# ===========================================================
# ===========================================================
# ===========================================================
# ===========================================================
# ===========================================================
# TicketTool.py==============================================
import md5
import Cookie
import time
from mod_python import apache
defaults = {
'TicketExpires':60000,
'TicketSecret':'/home/webadmin/.secretkey',
'TicketDomain':'.cv.usat'
}
class TicketTool(dict):
serverName = ''
def __init__(self, req):
"""
args: self
req: mod_python request object
"""
# set up the config items
opts = req.get_options()
for key,value in defaults.items():
try:
avalue = opts[key]
except KeyError:
avalue = value
self[key] = avalue
def authenticate(self,user,passwd):
# since this is going to run behind SSL client
# cert authtication, just return true
return 1
def fetch_secret(self):
secret = ""
try:
secret = self['SECRET_KEY']
except KeyError:
secret = open(self['TicketSecret']).read()
self['SECRET_KEY'] = secret
return secret
def invalidate_secret(self):
del(self['SECRET_KEY'])
def make_ticket(self, req, username):
"""
usage: cookie = ticketTool.make_ticket(req, username)
Creates a cookie containing the secure user information
"""
ip_address = req.connection.remote_ip
expires = str(self['TicketExpires'])
now = str(time.time())
secret = self.fetch_secret()
m = md5.new()
m.update(secret+ip_address+now+expires+username)
hash = m.hexdigest()
cookie = Cookie.SimpleCookie()
cookie["Ticket"] =
ip_address+","+expires+","+username+","+now+","+hash
cookie["Ticket"]['path'] = '/'
cookie["Ticket"]['domain'] = self['TicketDomain']
cookie["Ticket"]['max-age'] = self['TicketExpires'] * 3600
# TODO - is the expires in seconds? If so, jack this up!
return cookie
def verify_ticket(self,req):
"""
usage: result, msg, user = ticketTool.verify_ticket(req)
"""
ticket = None
cookie = Cookie.SimpleCookie()
# could get KeyError in two places.
# 1. if there are no cookies
# 2. if there isn't a cookie named 'Ticket'
try:
cookie.load(req.headers_in['Cookie'])
except KeyError:
return 0, "user has no cookies", 'noone'
try:
ticket = cookie['Ticket'].value
apache.log_error("Ticket Cookie value:" +
str(ticket))
except KeyError:
return 0, "user has no ticket cookie", 'noone'
ip, expires_s, user, timestamp_s, hash = ticket.split(",")
apache.log_error("[Ticket Cookie] hash:" + hash)
apache.log_error("[Ticket Cookie] user:" + user)
apache.log_error("[Ticket Cookie] time:" + timestamp_s)
apache.log_error("[Ticket Cookie] expires:" + expires_s)
apache.log_error("[Ticket Cookie] ip:" + ip)
timestamp = float(timestamp_s)
expires = int(expires_s)
if ip != req.connection.remote_ip:
return 0, "IP address mismatch in ticket", 'noone'
if time.time() - timestamp / 60 < expires:
return 0, "ticket has expired", 'noone'
secret = self.fetch_secret()
m = md5.new()
m.update(secret+ip+timestamp_s+expires_s+user)
new_hash = m.hexdigest()
if hash != new_hash:
self.invalidate_secret()
return 0, 'ticket mismatch', 'noone'
req.user = user
return 1, 'ok', user
def make_return_address(self, req):
"""
usage: cookie = ticketTool.make_return_address(req)
"""
protocol = 'http://'
if req.get_options().has_key('is_ssl'):
protocol = 'https://'
request_uri = protocol + req.server.server_hostname + ':'
+ str(req.server.port) + req.unparsed_uri
cookie = Cookie.SimpleCookie()
cookie['request_uri'] = request_uri
cookie['request_uri']['domain'] = self['TicketDomain']
cookie['request_uri']['path'] = '/'
return cookie
# ===========================================================
# ===========================================================
# ===========================================================
# HTTPD.CONF: ==============================================
<Macro CertSecurity>
PythonAccessHandler TicketAuthenHandler
PythonCleanupHandler TicketCleanupHandler
ErrorDocument 403 https://usatux29.cv.usat:22221/ticketMaster
#ErrorDocument 402 /unauthorized.html
ErrorDocument 402 /manager?_action=error_doc_unauth
PythonOption TicketSecret /home/tewpe1/.secretkey
#==================================================================
# PythonOption fullyLoadCache ::= no|yes (default is no)
# This option directs the Authen handler whether to fully load the
# cache upon child startup or cache refresh
PythonOption fullyLoadCache yes
#==================================================================
#==================================================================
# PythonOption is_ssl ::= any value you wish
# only set is_ssl to something if this url is under ssl. It
doesn't
# matter what it's set to, only that it exists. It is used to
help
# construct the redirect_url correctly
#PythonOption is_ssl yes
#==================================================================
</Macro>
<Directory "/web/devel/tewpe1/py/apache2/htdocs/secure">
Use CertSecurity
</Directory>
<Location /ticketMaster>
SSLRequireSSL
SSLOptions +StdEnvVars +ExportCertData
SetHandler python-program
PythonHandler TicketMaster
PythonDebug On
Order allow,deny
Allow from all
PythonOption TicketSecret /home/tewpe1/.secretkey
</Location>
"Hancock, David (DHANCOCK)" <DHANCOCK at arinc.com>
Sent by: mod_python-bounces at modpython.org
10/19/2003 12:27 AM
To: "'mod_python at modpython.org'" <mod_python at modpython.org>
cc:
Subject: [mod_python] req.connection.user generates AttributeError
I'm new to mod_python, and I'm stuck already. I'm working through the
examples in the documentation, and even after careful typing (and cutting
and pasting from the manual), I can't get the authentication example to
work. The line:
user = req.connection.user
Gives an attribute error ('user'). As shown in the manual, I'm calling
req.get_basic_auth_pw() first, but still no joy.
If I try/except to trap the attribute error, I avoid the 500 Server Error
message, but the authentication still doesn't work.
Any ideas? I'm running Windows 2000, Apache 2.0.47, Python 2.2, and
mod_python 3.0.3. (My other computer is a Linux box, but this is what
I've got right going right now). The mod_python is a precompiled binary.
I'll be grateful for any assistance I can get. I'm trying to recreate a
mod_perl module (AuthCookie) which implements a ticket-based
authentication mechanism. It works well in Perl, but my group
standardized on Python and we'd like to keep using Python for Apache
modules, too.
Cheers!
--
David Hancock | dhancock at arinc.com | 410-266-4384 _______________________________________________
Mod_python mailing list
Mod_python at modpython.org
http://mailman.modpython.org/mailman/listinfo/mod_python
-------------- next part --------------
A non-text attachment was scrubbed...
Name: TicketTool.py
Type: application/octet-stream
Size: 3650 bytes
Desc: not available
Url : http://mailman.modpython.org/pipermail/mod_python/attachments/20031020/f88d2166/TicketTool-0003.obj
-------------- next part --------------
A non-text attachment was scrubbed...
Name: TicketAuthenHandler.py
Type: application/octet-stream
Size: 8256 bytes
Desc: not available
Url : http://mailman.modpython.org/pipermail/mod_python/attachments/20031020/f88d2166/TicketAuthenHandler-0003.obj
-------------- next part --------------
A non-text attachment was scrubbed...
Name: TicketMaster.py
Type: application/octet-stream
Size: 4425 bytes
Desc: not available
Url : http://mailman.modpython.org/pipermail/mod_python/attachments/20031020/f88d2166/TicketMaster-0003.obj
|