diff --git a/pillar/application/modules/blender_cloud/home_project.py b/pillar/application/modules/blender_cloud/home_project.py index f0199e54..da447d92 100644 --- a/pillar/application/modules/blender_cloud/home_project.py +++ b/pillar/application/modules/blender_cloud/home_project.py @@ -89,6 +89,8 @@ def create_home_project(user_id, write_access): :param user_id: the user ID of the owner :param write_access: whether the user has full write access to the home project. :type write_access: bool + :returns: the project + :rtype: dict """ log.info('Creating home project for user %s', user_id) @@ -127,10 +129,9 @@ def create_home_project(user_id, write_access): # from manage_extra.node_types.text import node_type_text from manage_extra.node_types.comment import node_type_comment - if not write_access: - # Take away write access from the admin group, and grant it to - # certain node types. - project['permissions']['groups'][0]['methods'] = ['GET'] + # For non-subscribers: take away write access from the admin group, + # and grant it to certain node types. + project['permissions']['groups'][0]['methods'] = home_project_permissions(write_access) project['node_types'] = [ node_type_group, @@ -174,8 +175,7 @@ def home_project(): # Create the home project before we do the Eve query. This costs an extra round-trip # to the database, but makes it easier to do projections correctly. if not has_home_project(user_id): - write_access = bool(not HOME_PROJECT_WRITABLE_USERS or - HOME_PROJECT_WRITABLE_USERS.intersection(roles)) + write_access = write_access_with_roles(roles) create_home_project(user_id, write_access) resp, _, _, status, _ = get('projects', category=u'home', user=user_id) @@ -193,6 +193,28 @@ def home_project(): return utils.jsonify(project), status +def write_access_with_roles(roles): + """Returns whether or not one of these roles grants write access to the home project. + + :rtype: bool + """ + + write_access = bool(not HOME_PROJECT_WRITABLE_USERS or + HOME_PROJECT_WRITABLE_USERS.intersection(roles)) + return write_access + + +def home_project_permissions(write_access): + """Returns the project permissions, given the write access of the user. + + :rtype: list + """ + + if write_access: + return [u'GET', u'PUT', u'POST', u'DELETE'] + return [u'GET'] + + def has_home_project(user_id): """Returns True iff the user has a home project.""" @@ -200,6 +222,14 @@ def has_home_project(user_id): return proj_coll.count({'user': user_id, 'category': 'home', '_deleted': False}) > 0 +def get_home_project(user_id, projection=None): + """Returns the home project""" + + proj_coll = current_app.data.driver.db['projects'] + return proj_coll.find_one({'user': user_id, 'category': 'home', '_deleted': False}, + projection=projection) + + def is_home_project(project_id, user_id): """Returns True iff the given project exists and is the user's home project.""" @@ -311,6 +341,39 @@ def mark_parent_as_updated(node, original=None): mark_node_updated(parent_node['_id']) +def user_changed_role(sender, user): + """Responds to the 'user changed' signal from the Badger service. + + Changes the permissions on the home project based on the 'subscriber' role. + + :returns: whether this function actually made changes. + :rtype: bool + """ + + user_id = user['_id'] + if not has_home_project(user_id): + log.debug('User %s does not have a home project', user_id) + return + + proj_coll = current_app.data.driver.db['projects'] + proj = get_home_project(user_id, projection={'permissions': 1, '_id': 1}) + + write_access = write_access_with_roles(user['roles']) + target_permissions = home_project_permissions(write_access) + + current_perms = proj['permissions']['groups'][0]['methods'] + if set(current_perms) == set(target_permissions): + return False + + project_id = proj['_id'] + log.info('Updating permissions on user %s home project %s from %s to %s', + user_id, project_id, current_perms, target_permissions) + proj_coll.update_one({'_id': project_id}, + {'$set': {'permissions.groups.0.methods': list(target_permissions)}}) + + return True + + def setup_app(app, url_prefix): app.register_blueprint(blueprint, url_prefix=url_prefix) @@ -318,3 +381,6 @@ def setup_app(app, url_prefix): app.on_inserted_nodes += mark_parents_as_updated app.on_updated_nodes += mark_parent_as_updated app.on_replaced_nodes += mark_parent_as_updated + + from application.modules import service + service.signal_user_changed_role.connect(user_changed_role) diff --git a/pillar/application/modules/service.py b/pillar/application/modules/service.py index ef43f70d..849ceb53 100644 --- a/pillar/application/modules/service.py +++ b/pillar/application/modules/service.py @@ -2,6 +2,7 @@ import logging +import blinker from bson import ObjectId from flask import Blueprint, current_app, g, request from werkzeug import exceptions as wz_exceptions @@ -11,6 +12,7 @@ from application.modules import local_auth, users blueprint = Blueprint('service', __name__) log = logging.getLogger(__name__) +signal_user_changed_role = blinker.NamedSignal('badger:user_changed_role') ROLES_WITH_GROUPS = {u'admin', u'demo', u'subscriber'} @@ -99,6 +101,10 @@ def badger(): users_coll.update_one({'_id': db_user['_id']}, {'$set': updates}) + # Let the rest of the world know this user was updated. + db_user.update(updates) + signal_user_changed_role.send(current_app, user=db_user) + return '', 204 diff --git a/pillar/application/utils/authorization.py b/pillar/application/utils/authorization.py index 54056774..9c811d6c 100644 --- a/pillar/application/utils/authorization.py +++ b/pillar/application/utils/authorization.py @@ -100,7 +100,9 @@ def has_permissions(collection_name, resource, method, append_allowed_methods=Fa assign_to = resource assign_to['allowed_methods'] = list(set(allowed_methods)) return True - + else: + log.debug('Permission denied, method %s not in allowed methods %s', + method, allowed_methods) return False diff --git a/tests/test_home_project.py b/tests/test_home_project.py new file mode 100644 index 00000000..e477dea8 --- /dev/null +++ b/tests/test_home_project.py @@ -0,0 +1,86 @@ +# -*- encoding: utf-8 -*- + +from common_test_class import AbstractPillarTest + + +class HomeProjectUserChangedRoleTest(AbstractPillarTest): + def test_without_home_project(self): + from application.modules.blender_cloud import home_project + + self.user_id = self.create_user() + + with self.app.test_request_context(): + changed = home_project.user_changed_role(None, {'_id': self.user_id}) + self.assertFalse(changed) + + # Shouldn't do anything, shouldn't crash either. + + def test_already_subscriber_role(self): + from application.modules.blender_cloud import home_project + from application.utils.authentication import validate_token + + self.user_id = self.create_user(roles=set('subscriber')) + self.create_valid_auth_token(self.user_id, 'token') + + with self.app.test_request_context(headers={'Authorization': self.make_header('token')}): + validate_token() + + home_proj = home_project.create_home_project(self.user_id, write_access=True) + changed = home_project.user_changed_role(None, {'_id': self.user_id, + 'roles': ['subscriber']}) + self.assertFalse(changed) + + # The home project should still be writable, so we should be able to create a node. + self.create_test_node(home_proj['_id']) + + def test_granting_subscriber_role(self): + from application.modules.blender_cloud import home_project + from application.utils.authentication import validate_token + + self.user_id = self.create_user(roles=set()) + self.create_valid_auth_token(self.user_id, 'token') + + with self.app.test_request_context(headers={'Authorization': self.make_header('token')}): + validate_token() + + home_proj = home_project.create_home_project(self.user_id, write_access=False) + changed = home_project.user_changed_role(None, {'_id': self.user_id, + 'roles': ['subscriber']}) + self.assertTrue(changed) + + # The home project should be writable, so we should be able to create a node. + self.create_test_node(home_proj['_id']) + + def test_revoking_subscriber_role(self): + from application.modules.blender_cloud import home_project + from application.utils.authentication import validate_token + + self.user_id = self.create_user(roles=set('subscriber')) + self.create_valid_auth_token(self.user_id, 'token') + + with self.app.test_request_context(headers={'Authorization': self.make_header('token')}): + validate_token() + + home_proj = home_project.create_home_project(self.user_id, write_access=True) + changed = home_project.user_changed_role(None, {'_id': self.user_id, + 'roles': []}) + self.assertTrue(changed) + + # The home project should NOT be writable, so we should NOT be able to create a node. + self.create_test_node(home_proj['_id'], 403) + + def create_test_node(self, project_id, status_code=201): + from application.utils import dumps + + node = { + 'project': project_id, + 'node_type': 'group', + 'name': 'test group node', + 'user': self.user_id, + 'properties': {}, + } + + resp = self.client.post('/nodes', data=dumps(node), + headers={'Authorization': self.make_header('token'), + 'Content-Type': 'application/json'}) + self.assertEqual(status_code, resp.status_code, resp.data)