aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xsource/auth.py197
-rwxr-xr-xsource/pyAggr3g470r.py38
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")
bgstack15