[mod_python] req.connection.user generates AttributeError

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


More information about the Mod_python mailing list