diff options
-rwxr-xr-x | source/auth.py | 197 | ||||
-rwxr-xr-x | source/pyAggr3g470r.py | 38 |
2 files changed, 232 insertions, 3 deletions
diff --git a/source/auth.py b/source/auth.py new file mode 100755 index 00000000..e2bdcf2b --- /dev/null +++ b/source/auth.py @@ -0,0 +1,197 @@ +#! /usr/bin/env python +#-*- coding: utf-8 -*- +# +# Form based authentication for CherryPy. Requires the +# Session tool to be loaded. +# + +import cherrypy +import hashlib + +import log + +SESSION_KEY = '_cp_username' + +import csv +class excel_french(csv.Dialect): + delimiter = ';' + quotechar = '"' + doublequote = True + skipinitialspace = False + lineterminator = '\n' + quoting = csv.QUOTE_MINIMAL + +csv.register_dialect('excel_french', excel_french) + + + + +def check_credentials(username, password): + """Verifies credentials for username and password. + Returns None on success or a string describing the error on failure""" + # Adapt to your needs + USERS = {} + cr = csv.reader(open("./var/password", "r"), 'excel_french') + for row in cr: + USERS[row[0]] = row[1] + + m = hashlib.sha1() + m.update(password) + if username in USERS.keys() and USERS[username] == m.hexdigest(): + return None + else: + return u"Incorrect username or password." + + # An example implementation which uses an ORM could be: + # u = User.get(username) + # if u is None: + # return u"Username %s is unknown to me." % username + # if u.password != md5.new(password).hexdigest(): + # return u"Incorrect password" + +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) + if conditions is not None: + username = cherrypy.session.get(SESSION_KEY) + if username: + cherrypy.request.login = username + for condition in conditions: + # A condition is just a callable that returns true or false + if not condition(): + raise cherrypy.HTTPRedirect("/auth/login") + else: + raise cherrypy.HTTPRedirect("/auth/login") + +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' + 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): + """ + This class provides login and logout actions. + """ + def __init__(self): + self.logger = log.Log() + self.username = "" + + def on_login(self, username): + """ + Called on successful login. + """ + self.username = username + self.logger.info(username + ' logged in.') + + def on_logout(self, username): + """ + Called on logout. + """ + self.logger.info(username + ' logged out.') + self.username = "" + + def get_loginform(self, username, msg="Enter login information", from_page="/"): + """ + Login page. + """ + msg = "" + return """<html> + <head> + <link rel="stylesheet" type="text/css" href="/css/style.css" /> + </head> + <body> + <img src="/img/tuxrss.png" alt="pyAggr3g470r" /> + <div id="center"> + <div id="main"> + <form method="post" action="/auth/login"> + <input type="hidden" name="from_page" value="%(from_page)s" /> + %(msg)s<br /> + <input type="text" name="username" value="%(username)s" placeholder="Username" /><br /> + <input type="password" name="password" placeholder="Password" /><br /> + <input type="submit" value="Log in" /> + </div><!-- end #main --> + </div><!-- end #center --> + </body> +</html>""" % locals() + + @cherrypy.expose + def login(self, username=None, password=None, from_page="/"): + """ + Open a session for an authenticated user. + """ + if username is None or password is None: + return self.get_loginform("", from_page=from_page) + + error_msg = check_credentials(username, password) + if error_msg: + self.logger.info(error_msg) + return self.get_loginform(username, error_msg, from_page) + else: + cherrypy.session[SESSION_KEY] = cherrypy.request.login = username + self.on_login(username) + raise cherrypy.HTTPRedirect(from_page or "/") + + @cherrypy.expose + def logout(self, from_page="/"): + """ + Cloase a session. + """ + sess = cherrypy.session + username = sess.get(SESSION_KEY, None) + sess[SESSION_KEY] = None + if username: + cherrypy.request.login = None + self.on_logout(username) + raise cherrypy.HTTPRedirect(from_page or "/") diff --git a/source/pyAggr3g470r.py b/source/pyAggr3g470r.py index 081e6223..58737890 100755 --- a/source/pyAggr3g470r.py +++ b/source/pyAggr3g470r.py @@ -51,6 +51,7 @@ import utils import export import mongodb import feedgetter +from auth import AuthController, require, member_of, name_is from qrcode.pyqrnative.PyQRNative import QRCode, QRErrorCorrectLevel, CodeOverflowException from qrcode import qr @@ -100,18 +101,37 @@ htmlnav = '<body>\n<h1><div class="right innerlogo"><a href="/"><img src="/img/t ' href="http://bitbucket.org/cedricbonhomme/pyaggr3g470r/" rel="noreferrer" target="_blank">' + \ 'pyAggr3g470r (source code)</a>' +class RestrictedArea: + + # all methods in this controller (and subcontrollers) is + # open only to members of the admin group + + _cp_config = { + 'auth.require': [member_of('admin')] + } + + @cherrypy.expose + def index(self): + return """This is the admin only area.""" class Root: """ Root class. All pages of pyAggr3g470r are described in this class. """ + _cp_config = {'request.error_response': handle_error, \ + 'tools.sessions.on': True, \ + 'tools.auth.on': True} + def __init__(self): """ """ + self.auth = AuthController() + restricted = RestrictedArea() + self.mongo = mongodb.Articles(conf.MONGODB_ADDRESS, conf.MONGODB_PORT, \ conf.MONGODB_DBNAME, conf.MONGODB_USER, conf.MONGODB_PASSWORD) - + @require() def index(self): """ Main page containing the list of feeds and articles. @@ -220,6 +240,7 @@ class Root: return html + @require() def create_list_of_feeds(self): """ Create the list of feeds. @@ -237,6 +258,7 @@ class Root: self.mongo.nb_unread_articles(feed["feed_id"]), not_read_end, self.mongo.nb_articles(feed["feed_id"])) return html + "</div>" + @require() def management(self): """ Management page. @@ -248,7 +270,7 @@ class Root: nb_favorites = self.mongo.nb_favorites() nb_articles = self.mongo.nb_articles() nb_unread_articles = self.mongo.nb_unread_articles() - + html = htmlheader() html += htmlnav html += """<div class="left inner">\n""" @@ -1000,6 +1022,7 @@ class Root: mail_notification.exposed = True + @require() def like(self, param): """ Mark or unmark an article as favorites. @@ -1014,6 +1037,7 @@ class Root: like.exposed = True + @require() def favorites(self): """ List of favorites articles @@ -1050,6 +1074,7 @@ class Root: favorites.exposed = True + @require() def add_feed(self, url): """ Add a new feed with the URL of a page. @@ -1076,6 +1101,7 @@ class Root: add_feed.exposed = True + @require() def remove_feed(self, feed_id): """ Remove a feed from the file feed.lst and from the MongoDB database. @@ -1097,6 +1123,7 @@ class Root: remove_feed.exposed = True + @require() def change_feed_url(self, feed_id, old_feed_url, new_feed_url): """ Enables to change the URL of a feed already present in the database. @@ -1113,6 +1140,7 @@ class Root: change_feed_url.exposed = True + @require() def change_feed_name(self, feed_id, new_feed_name): """ Enables to change the name of a feed. @@ -1128,6 +1156,7 @@ class Root: change_feed_name.exposed = True + @require() def change_feed_logo(self, feed_id, new_feed_logo): """ Enables to change the name of a feed. @@ -1143,6 +1172,7 @@ class Root: change_feed_logo.exposed = True + @require() def delete_article(self, param): """ Delete an article. @@ -1157,6 +1187,7 @@ class Root: delete_article.exposed = True + @require() def drop_base(self): """ Delete all articles. @@ -1166,6 +1197,7 @@ class Root: drop_base.exposed = True + @require() def export(self, export_method): """ Export articles currently loaded from the MongoDB database with @@ -1181,6 +1213,7 @@ class Root: export.exposed = True + @require() def epub(self, param): """ Export an article to EPUB. @@ -1222,6 +1255,5 @@ if __name__ == '__main__': root.favicon_ico = cherrypy.tools.staticfile.handler(filename=os.path.join(conf.path + "/img/favicon.png")) cherrypy.config.update({ 'server.socket_port': 12556, 'server.socket_host': "0.0.0.0"}) cherrypy.config.update({'error_page.404': error_page_404}) - _cp_config = {'request.error_response': handle_error} cherrypy.quickstart(root, "/" ,config=conf.path + "/cfg/cherrypy.cfg") |