closes #65: introduce user authentication for GUI and API
This commit is contained in:
parent
ae86035b16
commit
6c89b3bdf6
2 changed files with 47 additions and 16 deletions
|
@ -23,7 +23,7 @@ FONT_TEXT_ENCODING = 'latin-1'
|
||||||
FONT_BARCODE = 'free3of9.ttf'
|
FONT_BARCODE = 'free3of9.ttf'
|
||||||
|
|
||||||
PRINTER_NAME = None
|
PRINTER_NAME = None
|
||||||
#PRINTER_NAME = 'DYMO_LabelWriter_450'
|
PRINTER_NAME = 'DYMO_LabelWriter_450'
|
||||||
|
|
||||||
|
|
||||||
def _get_resource(filename):
|
def _get_resource(filename):
|
||||||
|
|
|
@ -44,6 +44,22 @@ re_env_key = re.compile('[^A-Z_]+')
|
||||||
re_slashes = re.compile(r'//+')
|
re_slashes = re.compile(r'//+')
|
||||||
|
|
||||||
|
|
||||||
|
def authenticated(method):
|
||||||
|
"""Decorator to handle authentication."""
|
||||||
|
original_wrapper = tornado.web.authenticated(method)
|
||||||
|
@tornado.web.functools.wraps(method)
|
||||||
|
def my_wrapper(self, *args, **kwargs):
|
||||||
|
# If no authentication was required from the command line or config file.
|
||||||
|
if not self.authentication:
|
||||||
|
return method(self, *args, **kwargs)
|
||||||
|
# un authenticated API calls gets redirected to /v1.0/[...]
|
||||||
|
if self.is_api() and not self.current_user:
|
||||||
|
self.redirect('/v%s%s' % (API_VERSION, self.get_login_url()))
|
||||||
|
return
|
||||||
|
return original_wrapper(self, *args, **kwargs)
|
||||||
|
return my_wrapper
|
||||||
|
|
||||||
|
|
||||||
class BaseHandler(tornado.web.RequestHandler):
|
class BaseHandler(tornado.web.RequestHandler):
|
||||||
"""Base class for request handlers."""
|
"""Base class for request handlers."""
|
||||||
# A property to access the first value of each argument.
|
# A property to access the first value of each argument.
|
||||||
|
@ -66,16 +82,19 @@ class BaseHandler(tornado.web.RequestHandler):
|
||||||
}
|
}
|
||||||
|
|
||||||
def is_api(self):
|
def is_api(self):
|
||||||
|
"""Return True if the path is from an API call."""
|
||||||
return self.request.path.startswith('/v%s' % API_VERSION)
|
return self.request.path.startswith('/v%s' % API_VERSION)
|
||||||
|
|
||||||
def tobool(self, obj):
|
def tobool(self, obj):
|
||||||
|
"""Convert some textual values to boolean."""
|
||||||
if isinstance(obj, (list, tuple)):
|
if isinstance(obj, (list, tuple)):
|
||||||
obj = obj[0]
|
obj = obj[0]
|
||||||
if isinstance(obj, (str, unicode)):
|
if isinstance(obj, (str, unicode)):
|
||||||
obj = obj.lower()
|
obj = obj.lower()
|
||||||
return self._bool_convert.get(obj, obj)
|
return self._bool_convert.get(obj, obj)
|
||||||
|
|
||||||
def _arguments_tobool(self):
|
def arguments_tobool(self):
|
||||||
|
"""Return a dictionary of arguments, converted to booleans where possible."""
|
||||||
return dict([(k, self.tobool(v)) for k, v in self.arguments.iteritems()])
|
return dict([(k, self.tobool(v)) for k, v in self.arguments.iteritems()])
|
||||||
|
|
||||||
def initialize(self, **kwargs):
|
def initialize(self, **kwargs):
|
||||||
|
@ -97,7 +116,7 @@ class RootHandler(BaseHandler):
|
||||||
angular_app_path = os.path.join(os.path.dirname(__file__), "angular_app")
|
angular_app_path = os.path.join(os.path.dirname(__file__), "angular_app")
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
@tornado.web.authenticated
|
@authenticated
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
# serve the ./angular_app/index.html file
|
# serve the ./angular_app/index.html file
|
||||||
with open(self.angular_app_path + "/index.html", 'r') as fd:
|
with open(self.angular_app_path + "/index.html", 'r') as fd:
|
||||||
|
@ -173,7 +192,7 @@ class CollectionHandler(BaseHandler):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
@tornado.web.authenticated
|
@authenticated
|
||||||
def get(self, id_=None, resource=None, resource_id=None, **kwargs):
|
def get(self, id_=None, resource=None, resource_id=None, **kwargs):
|
||||||
if resource:
|
if resource:
|
||||||
# Handle access to sub-resources.
|
# Handle access to sub-resources.
|
||||||
|
@ -192,7 +211,7 @@ class CollectionHandler(BaseHandler):
|
||||||
self.write({self.collection: self.db.query(self.collection)})
|
self.write({self.collection: self.db.query(self.collection)})
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
@tornado.web.authenticated
|
@authenticated
|
||||||
def post(self, id_=None, resource=None, resource_id=None, **kwargs):
|
def post(self, id_=None, resource=None, resource_id=None, **kwargs):
|
||||||
data = escape.json_decode(self.request.body or '{}')
|
data = escape.json_decode(self.request.body or '{}')
|
||||||
if resource:
|
if resource:
|
||||||
|
@ -211,7 +230,7 @@ class CollectionHandler(BaseHandler):
|
||||||
put = post
|
put = post
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
@tornado.web.authenticated
|
@authenticated
|
||||||
def delete(self, id_=None, resource=None, resource_id=None, **kwargs):
|
def delete(self, id_=None, resource=None, resource_id=None, **kwargs):
|
||||||
if resource:
|
if resource:
|
||||||
# Handle access to sub-resources.
|
# Handle access to sub-resources.
|
||||||
|
@ -465,8 +484,9 @@ class EbCSVImportPersonsHandler(BaseHandler):
|
||||||
'company', 'job_title')
|
'company', 'job_title')
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
@tornado.web.authenticated
|
@authenticated
|
||||||
def post(self, **kwargs):
|
def post(self, **kwargs):
|
||||||
|
# import a CSV list of persons
|
||||||
event_handler = EventsHandler(self.application, self.request)
|
event_handler = EventsHandler(self.application, self.request)
|
||||||
event_handler.db = self.db
|
event_handler.db = self.db
|
||||||
targetEvent = None
|
targetEvent = None
|
||||||
|
@ -507,14 +527,15 @@ class EbCSVImportPersonsHandler(BaseHandler):
|
||||||
class SettingsHandler(BaseHandler):
|
class SettingsHandler(BaseHandler):
|
||||||
"""Handle requests for Settings."""
|
"""Handle requests for Settings."""
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
@tornado.web.authenticated
|
@authenticated
|
||||||
def get(self, **kwds):
|
def get(self, **kwds):
|
||||||
query = self._arguments_tobool()
|
query = self.arguments_tobool()
|
||||||
settings = self.db.query('settings', query)
|
settings = self.db.query('settings', query)
|
||||||
self.write({'settings': settings})
|
self.write({'settings': settings})
|
||||||
|
|
||||||
|
|
||||||
class WebSocketEventUpdatesHandler(tornado.websocket.WebSocketHandler):
|
class WebSocketEventUpdatesHandler(tornado.websocket.WebSocketHandler):
|
||||||
|
"""Manage websockets."""
|
||||||
def _clean_url(self, url):
|
def _clean_url(self, url):
|
||||||
return re_slashes.sub('/', url)
|
return re_slashes.sub('/', url)
|
||||||
|
|
||||||
|
@ -549,14 +570,17 @@ class LoginHandler(RootHandler):
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def get(self, **kwds):
|
def get(self, **kwds):
|
||||||
|
# show the login page
|
||||||
if self.is_api():
|
if self.is_api():
|
||||||
self.set_status(401)
|
self.set_status(401)
|
||||||
self.write({'error': 'authentication required', 'message': 'please provide username and password'})
|
self.write({'error': 'authentication required',
|
||||||
|
'message': 'please provide username and password'})
|
||||||
else:
|
else:
|
||||||
with open(self.angular_app_path + "/login.html", 'r') as fd:
|
with open(self.angular_app_path + "/login.html", 'r') as fd:
|
||||||
self.write(fd.read())
|
self.write(fd.read())
|
||||||
|
|
||||||
def _authorize(self, username, password):
|
def _authorize(self, username, password):
|
||||||
|
"""Return True is this username/password is valid."""
|
||||||
res = self.db.query('users', {'username': username})
|
res = self.db.query('users', {'username': username})
|
||||||
if not res:
|
if not res:
|
||||||
return False
|
return False
|
||||||
|
@ -574,6 +598,7 @@ class LoginHandler(RootHandler):
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def post(self):
|
def post(self):
|
||||||
|
# authenticate a user
|
||||||
username = self.get_body_argument('username')
|
username = self.get_body_argument('username')
|
||||||
password = self.get_body_argument('password')
|
password = self.get_body_argument('password')
|
||||||
if self._authorize(username, password):
|
if self._authorize(username, password):
|
||||||
|
@ -596,8 +621,12 @@ class LogoutHandler(RootHandler):
|
||||||
"""Handle user logout requests."""
|
"""Handle user logout requests."""
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def get(self, **kwds):
|
def get(self, **kwds):
|
||||||
|
# log the user out
|
||||||
logging.info('logout')
|
logging.info('logout')
|
||||||
self.logout()
|
self.logout()
|
||||||
|
if self.is_api():
|
||||||
|
self.redirect('/v%s/login' % API_VERSION)
|
||||||
|
else:
|
||||||
self.redirect('/login')
|
self.redirect('/login')
|
||||||
|
|
||||||
|
|
||||||
|
@ -606,16 +635,17 @@ def run():
|
||||||
# command line arguments; can also be written in a configuration file,
|
# command line arguments; can also be written in a configuration file,
|
||||||
# specified with the --config argument.
|
# specified with the --config argument.
|
||||||
define("port", default=5242, help="run on the given port", type=int)
|
define("port", default=5242, help="run on the given port", type=int)
|
||||||
define("data", default=os.path.join(os.path.dirname(__file__), "data"),
|
define("data_dir", default=os.path.join(os.path.dirname(__file__), "data"),
|
||||||
help="specify the directory used to store the data")
|
help="specify the directory used to store the data")
|
||||||
define("ssl_cert", default=os.path.join(os.path.dirname(__file__), 'ssl', 'eventman_cert.pem'),
|
define("ssl_cert", default=os.path.join(os.path.dirname(__file__), 'ssl', 'eventman_cert.pem'),
|
||||||
help="specify the SSL certificate to use for secure connections")
|
help="specify the SSL certificate to use for secure connections")
|
||||||
define("ssl_key", default=os.path.join(os.path.dirname(__file__), 'ssl', 'eventman_key.pem'),
|
define("ssl_key", default=os.path.join(os.path.dirname(__file__), 'ssl', 'eventman_key.pem'),
|
||||||
help="specify the SSL private key to use for secure connections")
|
help="specify the SSL private key to use for secure connections")
|
||||||
define("mongodbURL", default=None,
|
define("mongo_url", default=None,
|
||||||
help="URL to MongoDB server", type=str)
|
help="URL to MongoDB server", type=str)
|
||||||
define("dbName", default='eventman',
|
define("db_name", default='eventman',
|
||||||
help="Name of the MongoDB database to use", type=str)
|
help="Name of the MongoDB database to use", type=str)
|
||||||
|
define("authentication", default=True, help="if set to false, no authentication is required")
|
||||||
define("debug", default=False, help="run in debug mode")
|
define("debug", default=False, help="run in debug mode")
|
||||||
define("config", help="read configuration file",
|
define("config", help="read configuration file",
|
||||||
callback=lambda path: tornado.options.parse_config_file(path, final=False))
|
callback=lambda path: tornado.options.parse_config_file(path, final=False))
|
||||||
|
@ -626,8 +656,9 @@ def run():
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# database backend connector
|
# database backend connector
|
||||||
db_connector = backend.EventManDB(url=options.mongodbURL, dbName=options.dbName)
|
db_connector = backend.EventManDB(url=options.mongo_url, dbName=options.db_name)
|
||||||
init_params = dict(db=db_connector, data_dir=options.data, listen_port=options.port)
|
init_params = dict(db=db_connector, data_dir=options.data_dir, listen_port=options.port,
|
||||||
|
authentication=options.authentication)
|
||||||
|
|
||||||
# If not present, we store a user 'admin' with password 'eventman' into the database.
|
# If not present, we store a user 'admin' with password 'eventman' into the database.
|
||||||
if not db_connector.query('users', {'username': 'admin'}):
|
if not db_connector.query('users', {'username': 'admin'}):
|
||||||
|
|
Loading…
Reference in a new issue