Browse Source

fixes #13: permissions to modify unregistered attendees and notes

Davide Alberani 4 years ago
parent
commit
95ace7f8a4
3 changed files with 63 additions and 22 deletions
  1. 57 16
      ibt2.py
  2. 4 4
      monco.py
  3. 2 2
      tests/ibt2_tests.py

+ 57 - 16
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'}):

+ 4 - 4
monco.py

@@ -2,8 +2,8 @@
 
 Classes and functions used to issue queries to a MongoDB database.
 
-Copyright 2016 Davide Alberani <da@erlug.linux.it>
-               RaspiBO <info@raspibo.org>
+Copyright 2016-2017 Davide Alberani <da@erlug.linux.it>
+                    RaspiBO <info@raspibo.org>
 
 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

+ 2 - 2
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()