From 95ace7f8a43cd0b50e3b556db434122fecdf6803 Mon Sep 17 00:00:00 2001 From: Davide Alberani Date: Sun, 26 Feb 2017 20:00:18 +0100 Subject: [PATCH] fixes #13: permissions to modify unregistered attendees and notes --- ibt2.py | 73 +++++++++++++++++++++++++++++++++++---------- monco.py | 8 ++--- tests/ibt2_tests.py | 4 +-- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/ibt2.py b/ibt2.py index 7fe0a51..873b9f9 100755 --- a/ibt2.py +++ b/ibt2.py @@ -138,6 +138,20 @@ class BaseHandler(tornado.web.RequestHandler): self._users_cache[current_user] = user_info return user_info + def is_registered(self): + """Check if the current user is registered. + + :returns: True if a registered user is logged in + :rtype: bool""" + return self.current_user_info.get('isRegistered') + + def is_admin(self): + """Check if the current user is an admin. + + :returns: True if the logged in user is an admin + :rtype: bool""" + return self.current_user_info.get('isAdmin') + def user_authorized(self, username, password): """Check if a combination of username/password is valid. @@ -185,7 +199,7 @@ class BaseHandler(tornado.web.RequestHandler): :returns: if the logged in user has the permission :rtype: bool""" if (owner_id and str(self.current_user) != str(owner_id) and not - self.current_user_info.get('isAdmin')): + self.is_admin()): self.build_error(status=401, message='insufficient permissions: must be the owner or admin') return False return True @@ -213,6 +227,26 @@ class BaseHandler(tornado.web.RequestHandler): doc['updated_at'] = now return doc + @staticmethod + def update_global_settings(db_connector, settings=None): + """Update global settings from the db. + + :param db_connector: database connector + :type db_connector: Monco instance + :param settings: the dict to update (in place, and returned) + :type settings: dict + + :returns: the updated dictionary, also modified in place + :rtype: dict""" + if settings is None: + settings = {} + settings.clear() + for setting in db_connector.query('settings'): + if not ('_id' in setting and 'value' in setting): + continue + settings[setting['_id']] = setting['value'] + return settings + class RootHandler(BaseHandler): """Handler for the / path.""" @@ -257,6 +291,8 @@ class AttendeesHandler(BaseHandler): return self.build_error(status=404, message='unable to access the resource') if not self.has_permission(doc.get('created_by')): return + if self.global_settings.get('protectUnregistered') and not self.is_admin(): + return self.build_error(status=401, message='insufficient permissions: must be admin') self.add_access_info(data) merged, doc = self.db.update(self.collection, {'_id': id_}, data) self.write(doc) @@ -270,6 +306,8 @@ class AttendeesHandler(BaseHandler): return self.build_error(status=404, message='unable to access the resource') if not self.has_permission(doc.get('created_by')): return + if self.global_settings.get('protectUnregistered') and not self.is_admin(): + return self.build_error(status=401, message='insufficient permissions: must be admin') howMany = self.db.delete(self.collection, id_) self.write({'success': True, 'deleted entries': howMany.get('n')}) @@ -352,6 +390,8 @@ class DaysInfoHandler(BaseHandler): if not day: return self.build_error(status=404, message='unable to access the resource') data = self.clean_body + if 'notes' in data and self.global_settings.get('protectDayNotes') and not self.is_admin(): + return self.build_error(status=401, message='insufficient permissions: must be admin') data['day'] = day self.add_access_info(data) merged, doc = self.db.update('days', {'day': day}, data) @@ -369,6 +409,8 @@ class GroupsHandler(BaseHandler): data = self.clean_body newName = (data.get('newName') or '').strip() if newName: + if self.global_settings.get('protectGroupName') and not self.is_admin(): + return self.build_error(status=401, message='insufficient permissions: must be admin') query = {'day': day, 'group': group} data = {'group': newName} self.db.updateMany('attendees', query, data) @@ -383,7 +425,7 @@ class GroupsHandler(BaseHandler): group = group.strip() if not (day and group): return self.build_error(status=404, message='unable to access the resource') - if not self.current_user_info.get('isAdmin'): + if not self.is_admin(): return self.build_error(status=401, message='insufficient permissions: must be admin') query = {'day': day, 'group': group} howMany = self.db.delete('attendees', query) @@ -400,6 +442,8 @@ class GroupsInfoHandler(BaseHandler): if not (day and group): return self.build_error(status=404, message='unable to access the resource') data = self.clean_body + if 'notes' in data and self.global_settings.get('protectGroupNotes') and not self.is_admin(): + return self.build_error(status=401, message='insufficient permissions: must be admin') data['day'] = day data['group'] = group self.add_access_info(data) @@ -421,7 +465,7 @@ class UsersHandler(BaseHandler): if 'password' in output: del output['password'] else: - if not self.current_user_info.get('isAdmin'): + if not self.is_admin(): return self.build_error(status=401, message='insufficient permissions: must be an admin') output = {self.collection: self.db.query(self.collection, self.clean_arguments)} for user in output['users']: @@ -444,7 +488,7 @@ class UsersHandler(BaseHandler): data['password'] = utils.hash_password(password) data['email'] = email self.add_access_info(data) - if 'isAdmin' in data and not self.current_user_info.get('isAdmin'): + if 'isAdmin' in data and not self.is_admin(): del data['isAdmin'] doc = self.db.add(self.collection, data) if 'password' in doc: @@ -460,7 +504,7 @@ class UsersHandler(BaseHandler): return if 'username' in data: del data['username'] - if 'isAdmin' in data and (str(self.current_user) == id_ or not self.current_user_info.get('isAdmin')): + if 'isAdmin' in data and (str(self.current_user) == id_ or not self.is_admin()): del data['isAdmin'] if 'password' in data: password = (data['password'] or '').strip() @@ -518,13 +562,13 @@ class SettingsHandler(BaseHandler): self.write(res) def post(self, id_=None): - if not self.current_user_info.get('isAdmin'): + if not self.is_admin(): return self.build_error(status=401, message='insufficient permissions: must be an admin') data = self.clean_body if id_ is not None: # if we access a specific resource, we assume the data is in {_id: value} format if id_ not in data: - self.write({'success': False}) + return self.build_error(status=404, message='incomplete data') return data = {id_: data[id_]} for key, value in data.items(): @@ -532,7 +576,8 @@ class SettingsHandler(BaseHandler): self.db.update(self.collection, {'_id': key}, {'value': value}) else: self.db.add(self.collection, {'_id': key, 'value': value}) - self.write({'success': True}) + settings = SettingsHandler.update_global_settings(self.db, self.global_settings) + self.write(settings) put = post @@ -619,16 +664,12 @@ def run(): # database backend connector db_connector = monco.Monco(url=options.mongo_url, dbName=options.db_name) - # all global settings stored in the db - settings = {} - for setting in db_connector.query('settings'): - key = setting.get('setting') - if not key: - continue - settings[key] = setting + # global settings stored in the db + global_settings = {} + BaseHandler.update_global_settings(db_connector, global_settings) init_params = dict(db=db_connector, listen_port=options.port, logger=logger, - ssl_options=ssl_options, global_settings=settings) + ssl_options=ssl_options, global_settings=global_settings) # If not present, we store a user 'admin' with password 'ibt2' into the database. if not db_connector.query('users', {'username': 'admin'}): diff --git a/monco.py b/monco.py index e1b11dc..01e96ae 100644 --- a/monco.py +++ b/monco.py @@ -2,8 +2,8 @@ Classes and functions used to issue queries to a MongoDB database. -Copyright 2016 Davide Alberani - RaspiBO +Copyright 2016-2017 Davide Alberani + RaspiBO Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -75,7 +75,7 @@ class MoncoError(Exception): pass -class MoncoConnection(MoncoError): +class MoncoConnectionError(MoncoError): """Monco exceptions raise when a connection problem occurs.""" pass @@ -124,7 +124,7 @@ class Monco(object): if dbName: self._dbName = dbName if not self._dbName: - raise MoncoConnection('no database name specified') + raise MoncoConnectionError('no database name specified') self.connection = pymongo.MongoClient(self._url) self.db = self.connection[self._dbName] return self.db diff --git a/tests/ibt2_tests.py b/tests/ibt2_tests.py index 172fec4..b6399f3 100755 --- a/tests/ibt2_tests.py +++ b/tests/ibt2_tests.py @@ -251,7 +251,7 @@ class Ibt2Tests(unittest.TestCase): r.raise_for_status() rj = r.json() r.connection.close() - self.assertTrue(rj.get('success'), True) + self.assertTrue('error' not in rj) r = requests.get(BASE_URL + 'settings') r.raise_for_status() rj = r.json() @@ -271,7 +271,7 @@ class Ibt2Tests(unittest.TestCase): r.raise_for_status() rj = r.json() r.connection.close() - self.assertTrue(rj.get('success'), True) + self.assertTrue('error' not in rj) r = requests.get(BASE_URL + 'settings/key2') r.raise_for_status() rj = r.json()