code cleanup

This commit is contained in:
Davide Alberani 2017-01-16 22:57:25 +01:00
parent 81c441277d
commit 4f876e8299

189
ibt2.py
View file

@ -17,7 +17,6 @@ limitations under the License.
import os import os
import re import re
import string
import logging import logging
import datetime import datetime
from operator import itemgetter from operator import itemgetter
@ -59,27 +58,36 @@ class BaseHandler(tornado.web.RequestHandler):
# Cache currently connected users. # Cache currently connected users.
_users_cache = {} _users_cache = {}
# set of documents we're managing (a collection in MongoDB or a table in a SQL database)
document = None
collection = None
# A property to access the first value of each argument. # A property to access the first value of each argument.
arguments = property(lambda self: dict([(k, v[0]) arguments = property(lambda self: dict([(k, v[0])
for k, v in self.request.arguments.iteritems()])) for k, v in self.request.arguments.iteritems()]))
_bool_convert = {
'0': False,
'n': False,
'f': False,
'no': False,
'off': False,
'false': False,
'1': True,
'y': True,
't': True,
'on': True,
'yes': True,
'true': True
}
_re_split_salt = re.compile(r'\$(?P<salt>.+)\$(?P<hash>.+)') _re_split_salt = re.compile(r'\$(?P<salt>.+)\$(?P<hash>.+)')
@property
def clean_body(self):
"""Return a clean dictionary from a JSON body, suitable for a query on MongoDB.
:returns: a clean copy of the body arguments
:rtype: dict"""
data = escape.json_decode(self.request.body or '{}')
return self._clean_dict(data)
def _clean_dict(self, data):
"""Filter a dictionary (in place) to remove unwanted keywords in db queries.
:param data: dictionary to clean
:type data: dict"""
if isinstance(data, dict):
for key in data.keys():
if isinstance(key, (str, unicode)) and key.startswith('$'):
del data[key]
return data
def write_error(self, status_code, **kwargs): def write_error(self, status_code, **kwargs):
"""Default error handler.""" """Default error handler."""
if isinstance(kwargs.get('exc_info', (None, None))[1], BaseException): if isinstance(kwargs.get('exc_info', (None, None))[1], BaseException):
@ -94,18 +102,6 @@ class BaseHandler(tornado.web.RequestHandler):
"""Return True if the path is from an API call.""" """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):
"""Convert some textual values to boolean."""
if isinstance(obj, (list, tuple)):
obj = obj[0]
if isinstance(obj, (str, unicode)):
obj = obj.lower()
return self._bool_convert.get(obj, obj)
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()])
def initialize(self, **kwargs): def initialize(self, **kwargs):
"""Add every passed (key, value) as attributes of the instance.""" """Add every passed (key, value) as attributes of the instance."""
for key, value in kwargs.iteritems(): for key, value in kwargs.iteritems():
@ -118,7 +114,10 @@ class BaseHandler(tornado.web.RequestHandler):
@property @property
def current_user_info(self): def current_user_info(self):
"""Information about the current user.""" """Information about the current user.
:returns: full information about the current user
:rtype: dict"""
current_user = self.current_user current_user = self.current_user
if current_user in self._users_cache: if current_user in self._users_cache:
return self._users_cache[current_user] return self._users_cache[current_user]
@ -168,6 +167,13 @@ class BaseHandler(tornado.web.RequestHandler):
self.set_status(status) self.set_status(status)
self.write({'error': True, 'message': message}) self.write({'error': True, 'message': message})
def has_permission(self, owner_id):
if (owner_id and str(self.current_user_info.get('_id')) != str(owner_id) and not
self.current_user_info.get('isAdmin')):
self.build_error(status=401, message='insufficient permissions: must be the owner or admin')
return False
return True
def logout(self): def logout(self):
"""Remove the secure cookie used fro authentication.""" """Remove the secure cookie used fro authentication."""
if self.current_user in self._users_cache: if self.current_user in self._users_cache:
@ -186,56 +192,7 @@ class RootHandler(BaseHandler):
self.write(fd.read()) self.write(fd.read())
class CollectionHandler(BaseHandler): class AttendeesHandler(BaseHandler):
"""Base class for handlers that need to interact with the database backend.
Introduce basic CRUD operations."""
# set of documents we're managing (a collection in MongoDB or a table in a SQL database)
document = None
collection = None
# set of documents used to store incremental sequences
counters_collection = 'counters'
_id_chars = string.ascii_lowercase + string.digits
def _filter_results(self, results, params):
"""Filter a list using keys and values from a dictionary.
:param results: the list to be filtered
:type results: list
:param params: a dictionary of items that must all be present in an original list item to be included in the return
:type params: dict
:returns: list of items that have all the keys with the same values as params
:rtype: list"""
if not params:
return results
params = monco.convert(params)
filtered = []
for result in results:
add = True
for key, value in params.iteritems():
if key not in result or result[key] != value:
add = False
break
if add:
filtered.append(result)
return filtered
def _clean_dict(self, data):
"""Filter a dictionary (in place) to remove unwanted keywords in db queries.
:param data: dictionary to clean
:type data: dict"""
if isinstance(data, dict):
for key in data.keys():
if isinstance(key, (str, unicode)) and key.startswith('$'):
del data[key]
return data
class AttendeesHandler(CollectionHandler):
document = 'attendee' document = 'attendee'
collection = 'attendees' collection = 'attendees'
@ -249,10 +206,8 @@ class AttendeesHandler(CollectionHandler):
@gen.coroutine @gen.coroutine
def post(self, **kwargs): def post(self, **kwargs):
data = escape.json_decode(self.request.body or '{}') data = self.clean_body
self._clean_dict(data) user_id = self.current_user_info.get('_id')
user_info = self.current_user_info
user_id = user_info.get('_id')
now = datetime.datetime.now() now = datetime.datetime.now()
data['created_by'] = user_id data['created_by'] = user_id
data['created_at'] = now data['created_at'] = now
@ -263,19 +218,16 @@ class AttendeesHandler(CollectionHandler):
@gen.coroutine @gen.coroutine
def put(self, id_, **kwargs): def put(self, id_, **kwargs):
data = escape.json_decode(self.request.body or '{}') data = self.clean_body
self._clean_dict(data)
if '_id' in data: if '_id' in data:
del data['_id'] del data['_id']
doc = self.db.getOne(self.collection, {'_id': id_}) or {} doc = self.db.getOne(self.collection, {'_id': id_}) or {}
owner_id = doc.get('created_by')
user_info = self.current_user_info
if not doc: if not doc:
return self.build_error(status=404, message='unable to access the resource') return self.build_error(status=404, message='unable to access the resource')
if (owner_id and str(self.current_user_info.get('_id')) != str(owner_id) and not owner_id = doc.get('created_by')
self.current_user_info.get('isAdmin')): if not self.has_permission(owner_id):
return self.build_error(status=401, message='insufficient permissions: must be the owner or admin') return
user_id = user_info.get('_id') user_id = self.current_user_info.get('_id')
now = datetime.datetime.now() now = datetime.datetime.now()
data['updated_by'] = user_id data['updated_by'] = user_id
data['updated_at'] = now data['updated_at'] = now
@ -288,17 +240,16 @@ class AttendeesHandler(CollectionHandler):
self.write({'success': False}) self.write({'success': False})
return return
doc = self.db.getOne(self.collection, {'_id': id_}) or {} doc = self.db.getOne(self.collection, {'_id': id_}) or {}
owner_id = doc.get('created_by')
if not doc: if not doc:
return self.build_error(status=404, message='unable to access the resource') return self.build_error(status=404, message='unable to access the resource')
if (owner_id and str(self.current_user_info.get('_id')) != str(owner_id) and not owner_id = doc.get('created_by')
self.current_user_info.get('isAdmin')): if not self.has_permission(owner_id):
return self.build_error(status=401, message='insufficient permissions: must be the owner or admin') return
howMany = self.db.delete(self.collection, id_) howMany = self.db.delete(self.collection, id_)
self.write({'success': True, 'deleted entries': howMany.get('n')}) self.write({'success': True, 'deleted entries': howMany.get('n')})
class DaysHandler(CollectionHandler): class DaysHandler(BaseHandler):
"""Handle requests for Days.""" """Handle requests for Days."""
def _summarize(self, days): def _summarize(self, days):
@ -362,7 +313,7 @@ class DaysHandler(CollectionHandler):
self.write({}) self.write({})
class UsersHandler(CollectionHandler): class UsersHandler(BaseHandler):
"""Handle requests for Users.""" """Handle requests for Users."""
document = 'user' document = 'user'
collection = 'users' collection = 'users'
@ -370,8 +321,8 @@ class UsersHandler(CollectionHandler):
@gen.coroutine @gen.coroutine
def get(self, id_=None, **kwargs): def get(self, id_=None, **kwargs):
if id_: if id_:
if str(self.current_user_info.get('_id')) != id_ and not self.current_user_info.get('isAdmin'): if not self.has_permission(id_):
return self.build_error(status=401, message='insufficient permissions: must be the owner or admin') return
output = self.db.getOne(self.collection, {'_id': id_}) output = self.db.getOne(self.collection, {'_id': id_})
if 'password' in output: if 'password' in output:
del output['password'] del output['password']
@ -386,8 +337,7 @@ class UsersHandler(CollectionHandler):
@gen.coroutine @gen.coroutine
def post(self, **kwargs): def post(self, **kwargs):
data = escape.json_decode(self.request.body or '{}') data = self.clean_body
self._clean_dict(data)
if '_id' in data: if '_id' in data:
del data['_id'] del data['_id']
username = (data.get('username') or '').strip() username = (data.get('username') or '').strip()
@ -410,22 +360,21 @@ class UsersHandler(CollectionHandler):
@gen.coroutine @gen.coroutine
def put(self, id_=None, **kwargs): def put(self, id_=None, **kwargs):
data = escape.json_decode(self.request.body or '{}') data = self.clean_body
self._clean_dict(data)
if id_ is None: if id_ is None:
return self.build_error(status=404, message='unable to access the resource') return self.build_error(status=404, message='unable to access the resource')
if not self.has_permission(id_):
return
if '_id' in data: if '_id' in data:
# Avoid overriding _id
del data['_id'] del data['_id']
if 'username' in data: if 'username' in data:
del data['username'] del data['username']
if 'password' in data: if 'password' in data:
if data['password']: password = (data['password'] or '').strip()
data['password'] = utils.hash_password(data['password']) if password:
data['password'] = utils.hash_password(password)
else: else:
del data['password'] del data['password']
if str(self.current_user_info.get('_id')) != id_ and not self.current_user_info.get('isAdmin'):
return self.build_error(status=401, message='insufficient permissions: must be the owner or admin')
merged, doc = self.db.update(self.collection, {'_id': id_}, data) merged, doc = self.db.update(self.collection, {'_id': id_}, data)
self.write(doc) self.write(doc)
@ -433,23 +382,14 @@ class UsersHandler(CollectionHandler):
def delete(self, id_=None, **kwargs): def delete(self, id_=None, **kwargs):
if id_ is None: if id_ is None:
return self.build_error(status=404, message='unable to access the resource') return self.build_error(status=404, message='unable to access the resource')
if str(self.current_user_info.get('_id')) != id_ and not self.current_user_info.get('isAdmin'): if not self.has_permission(id_):
return self.build_error(status=401, message='insufficient permissions: must be the owner or admin') return
howMany = self.db.delete(self.collection, id_)
if id_ in self._users_cache: if id_ in self._users_cache:
del self._users_cache[id_] del self._users_cache[id_]
howMany = self.db.delete(self.collection, id_)
self.write({'success': True, 'deleted entries': howMany.get('n')}) self.write({'success': True, 'deleted entries': howMany.get('n')})
class SettingsHandler(BaseHandler):
"""Handle requests for Settings."""
@gen.coroutine
def get(self, **kwargs):
query = self.arguments_tobool()
settings = self.db.query('settings', query)
self.write({'settings': settings})
class CurrentUserHandler(BaseHandler): class CurrentUserHandler(BaseHandler):
"""Handle requests for information about the logged in user.""" """Handle requests for information about the logged in user."""
@gen.coroutine @gen.coroutine
@ -478,7 +418,7 @@ class LoginHandler(RootHandler):
password = self.get_body_argument('password') password = self.get_body_argument('password')
username = self.get_body_argument('username') username = self.get_body_argument('username')
except tornado.web.MissingArgumentError: except tornado.web.MissingArgumentError:
data = escape.json_decode(self.request.body or '{}') data = self.clean_body
username = data.get('username') username = data.get('username')
password = data.get('password') password = data.get('password')
if not (username and password): if not (username and password):
@ -517,8 +457,6 @@ def run():
# specified with the --config argument. # specified with the --config argument.
define("port", default=3000, help="run on the given port", type=int) define("port", default=3000, help="run on the given port", type=int)
define("address", default='', help="bind the server at the given address", type=str) define("address", default='', help="bind the server at the given address", type=str)
define("data_dir", default=os.path.join(os.path.dirname(__file__), "data"),
help="specify the directory used to store the data")
define("ssl_cert", default=os.path.join(os.path.dirname(__file__), 'ssl', 'ibt2_cert.pem'), define("ssl_cert", default=os.path.join(os.path.dirname(__file__), 'ssl', 'ibt2_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', 'ibt2_key.pem'), define("ssl_key", default=os.path.join(os.path.dirname(__file__), 'ssl', 'ibt2_key.pem'),
@ -527,7 +465,6 @@ def run():
help="URL to MongoDB server", type=str) help="URL to MongoDB server", type=str)
define("db_name", default='ibt2', define("db_name", default='ibt2',
help="Name of the MongoDB database to use", type=str) help="Name of the MongoDB database to use", type=str)
define("authentication", default=False, help="if set to true, 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))
@ -544,8 +481,7 @@ def run():
# database backend connector # database backend connector
db_connector = monco.Monco(url=options.mongo_url, dbName=options.db_name) db_connector = monco.Monco(url=options.mongo_url, dbName=options.db_name)
init_params = dict(db=db_connector, data_dir=options.data_dir, listen_port=options.port, init_params = dict(db=db_connector, listen_port=options.port, logger=logger, ssl_options=ssl_options)
authentication=options.authentication, logger=logger, ssl_options=ssl_options)
# If not present, we store a user 'admin' with password 'ibt2' into the database. # If not present, we store a user 'admin' with password 'ibt2' into the database.
if not db_connector.query('users', {'username': 'admin'}): if not db_connector.query('users', {'username': 'admin'}):
@ -578,7 +514,6 @@ def run():
(_users_path, UsersHandler, init_params), (_users_path, UsersHandler, init_params),
(r'/v%s%s' % (API_VERSION, _users_path), UsersHandler, init_params), (r'/v%s%s' % (API_VERSION, _users_path), UsersHandler, init_params),
(r"/(?:index.html)?", RootHandler, init_params), (r"/(?:index.html)?", RootHandler, init_params),
(r"/settings", SettingsHandler, init_params),
(r'/login', LoginHandler, init_params), (r'/login', LoginHandler, init_params),
(r'/v%s/login' % API_VERSION, LoginHandler, init_params), (r'/v%s/login' % API_VERSION, LoginHandler, init_params),
(r'/logout', LogoutHandler), (r'/logout', LogoutHandler),