myauth.py 5.8 KB
# -*- encoding: utf-8 -*-

# TODO:
# - testar logout
# - session fixation
# - member_of...

import cherrypy
import sqlite3
from hashlib import sha256
from mako.lookup import TemplateLookup
import urllib
import html
from os import path

# path where this file is located
server_path = path.dirname(path.realpath(__file__))

SESSION_KEY = 'userid'

templates = TemplateLookup(directories=[server_path+'/templates'], input_encoding='utf-8')


def credentials_ok(uid, password, db):
    '''Given a userid, password and database, checks if the userid exists in
    the database, a checks if the password is correct. The password is
    updated if it's initially empty.
    Returns the name of the student on success, otherwise returns None.
    '''
    success = False
    tryhash = sha256(password.encode('utf-8')).hexdigest()

    # search student in database
    conn = sqlite3.connect(db)
    sql_cmd = 'SELECT * FROM students WHERE number=?'
    found = conn.execute(sql_cmd, [uid]).fetchone()
    if found is not None:
        num, name, pw_hash = found
        if pw_hash == '':
            # update password on first login
            pw_hash = tryhash
            sql_cmd = 'UPDATE students SET password=? WHERE number=?'
            conn.execute(sql_cmd, (pw_hash, num))
            conn.commit()
            cherrypy.log.error('Student %s updated his password.' % uid, 'APPLICATION')

        # check password
        success = (tryhash == pw_hash)
        if success:
            cherrypy.log.error('Student %s logged in.' % uid, 'APPLICATION')
        else:
            cherrypy.log.error('Student %s wrong password.' % uid, 'APPLICATION')
    else:
        cherrypy.log.error('Student %s not found!' % uid, 'APPLICATION')
    conn.close()
    return name if success else None


def check_auth(*args, **kwargs):
    """A tool that looks in config for 'auth.require'. If found and it
    is not None, a login is required and the entry is evaluated as a list of
    conditions that the user must fulfill"""
    conditions = cherrypy.request.config.get('auth.require', None)
    get_parmas = urllib.request.quote(cherrypy.request.request_line.split()[1])
    if conditions is not None:
        user = cherrypy.session.get(SESSION_KEY, None)

        # if already logged in, must satisfy conditions
        # else redirects to login page
        if user is not None:
            cherrypy.request.login = user
            for condition in conditions:
                # A condition is just a callable that returns true or false
                if not condition():
                    raise cherrypy.HTTPRedirect("/auth/login?from_page=%s" % get_parmas)
        else:
            raise cherrypy.HTTPRedirect("/auth/login?from_page=%s" % get_parmas)

cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth)


def require(*conditions):
    """A decorator that appends conditions to the auth.require config
    variable."""
    def decorate(f):
        if not hasattr(f, '_cp_config'):
            f._cp_config = dict()
        if 'auth.require' not in f._cp_config:
            f._cp_config['auth.require'] = []
        f._cp_config['auth.require'].extend(conditions)
        return f
    return decorate


# Conditions are callables that return True
# if the user fulfills the conditions they define, False otherwise
#
# They can access the current username as cherrypy.request.login
#
# Define those at will however suits the application.

def member_of(groupname):
    def check():
        # replace with actual check if <username> is in <groupname>
        return cherrypy.request.login == 'joe' and groupname == 'admin' # FIXME
    return check


def name_is(reqd_username):
    return lambda: reqd_username == cherrypy.request.login


# These might be handy
def any_of(*conditions):
    """Returns True if any of the conditions match"""
    def check():
        for c in conditions:
            if c():
                return True
        return False
    return check


# By default all conditions are required, but this might still be
# needed if you want to use it inside of an any_of(...) condition
def all_of(*conditions):
    """Returns True if all of the conditions match"""
    def check():
        for c in conditions:
            if not c():
                return False
        return True
    return check


# ============================================================================
class AuthController(object):
    def __init__(self, database):
        self.database = database

    @cherrypy.expose
    def login(self, uid=None, pw=None, from_page='/'):
        # XSS vulnerability if from_page is maliciously formed.
        # e.g.   /auth/login?from_page="/><script>...
        # will inject the string "/><string>... into the variable from_page
        # of this function. Rendering a login page will then inject (reflect)
        # the script. Not sure how serious it can be, since it is not stored
        # in the server, only executed on the client browser...
        # html.escape will replace > < & etc used in html by &lt; etc
        from_page = html.escape(from_page)

        db = self.database
        if uid is not None and pw is not None:
            name = credentials_ok(uid, pw, db)
            if name is not None:
                cherrypy.session[SESSION_KEY] = cherrypy.request.login = uid
                cherrypy.session['name'] = name
                raise cherrypy.HTTPRedirect(from_page)
        logintemplate = templates.get_template('/login.html')
        return logintemplate.render(from_page=from_page)

    @cherrypy.expose
    def logout(self):
        # FIXME logout is not working!!!
        cherrypy.lib.sessions.expire()  # session cookie expires on client
        user = cherrypy.session.get(SESSION_KEY, None)
        cherrypy.session[SESSION_KEY] = None
        if user is not None:
            cherrypy.request.login = None
        raise cherrypy.HTTPRedirect('/')