Managing home project permissions when granting/revoking subscriber/demo role.

This is hooked into the badger service using a Blinker signal. This signal
also needs to be sent from a PUT on the user document.
This commit is contained in:
Sybren A. Stüvel 2016-07-06 11:05:24 +02:00
parent 4b1b02318b
commit 91238aacb7
4 changed files with 167 additions and 7 deletions

View File

@ -89,6 +89,8 @@ def create_home_project(user_id, write_access):
:param user_id: the user ID of the owner :param user_id: the user ID of the owner
:param write_access: whether the user has full write access to the home project. :param write_access: whether the user has full write access to the home project.
:type write_access: bool :type write_access: bool
:returns: the project
:rtype: dict
""" """
log.info('Creating home project for user %s', user_id) 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.text import node_type_text
from manage_extra.node_types.comment import node_type_comment from manage_extra.node_types.comment import node_type_comment
if not write_access: # For non-subscribers: take away write access from the admin group,
# Take away write access from the admin group, and grant it to # and grant it to certain node types.
# certain node types. project['permissions']['groups'][0]['methods'] = home_project_permissions(write_access)
project['permissions']['groups'][0]['methods'] = ['GET']
project['node_types'] = [ project['node_types'] = [
node_type_group, 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 # 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. # to the database, but makes it easier to do projections correctly.
if not has_home_project(user_id): if not has_home_project(user_id):
write_access = bool(not HOME_PROJECT_WRITABLE_USERS or write_access = write_access_with_roles(roles)
HOME_PROJECT_WRITABLE_USERS.intersection(roles))
create_home_project(user_id, write_access) create_home_project(user_id, write_access)
resp, _, _, status, _ = get('projects', category=u'home', user=user_id) resp, _, _, status, _ = get('projects', category=u'home', user=user_id)
@ -193,6 +193,28 @@ def home_project():
return utils.jsonify(project), status 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): def has_home_project(user_id):
"""Returns True iff the user has a home project.""" """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 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): def is_home_project(project_id, user_id):
"""Returns True iff the given project exists and is the user's home project.""" """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']) 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): def setup_app(app, url_prefix):
app.register_blueprint(blueprint, url_prefix=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_inserted_nodes += mark_parents_as_updated
app.on_updated_nodes += mark_parent_as_updated app.on_updated_nodes += mark_parent_as_updated
app.on_replaced_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)

View File

@ -2,6 +2,7 @@
import logging import logging
import blinker
from bson import ObjectId from bson import ObjectId
from flask import Blueprint, current_app, g, request from flask import Blueprint, current_app, g, request
from werkzeug import exceptions as wz_exceptions from werkzeug import exceptions as wz_exceptions
@ -11,6 +12,7 @@ from application.modules import local_auth, users
blueprint = Blueprint('service', __name__) blueprint = Blueprint('service', __name__)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
signal_user_changed_role = blinker.NamedSignal('badger:user_changed_role')
ROLES_WITH_GROUPS = {u'admin', u'demo', u'subscriber'} ROLES_WITH_GROUPS = {u'admin', u'demo', u'subscriber'}
@ -99,6 +101,10 @@ def badger():
users_coll.update_one({'_id': db_user['_id']}, users_coll.update_one({'_id': db_user['_id']},
{'$set': updates}) {'$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 return '', 204

View File

@ -100,7 +100,9 @@ def has_permissions(collection_name, resource, method, append_allowed_methods=Fa
assign_to = resource assign_to = resource
assign_to['allowed_methods'] = list(set(allowed_methods)) assign_to['allowed_methods'] = list(set(allowed_methods))
return True return True
else:
log.debug('Permission denied, method %s not in allowed methods %s',
method, allowed_methods)
return False return False

View File

@ -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)