code cleanup
This commit is contained in:
parent
81c441277d
commit
4f876e8299
1 changed files with 62 additions and 127 deletions
189
ibt2.py
189
ibt2.py
|
@ -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),
|
||||||
|
|
Loading…
Reference in a new issue