Page MenuHome

home_project.py
No OneTemporary

home_project.py

import copy
import logging
import datetime
from bson import ObjectId, tz_util
from eve.methods.get import get
from flask import Blueprint, g, current_app, request
from pillar.api import utils
from pillar.api.utils import authentication, authorization
from werkzeug import exceptions as wz_exceptions
from pillar.api.projects import utils as proj_utils
blueprint = Blueprint('blender_cloud.home_project', __name__)
log = logging.getLogger(__name__)
# Users with any of these roles will get a home project.
HOME_PROJECT_USERS = set()
# Users with any of these roles will get full write access to their home project.
HOME_PROJECT_WRITABLE_USERS = {'subscriber', 'demo'}
HOME_PROJECT_DESCRIPTION = ('# Your home project\n\n'
'This is your home project. It allows synchronisation '
'of your Blender settings using the [Blender Cloud addon]'
'(https://cloud.blender.org/services#blender-addon).')
HOME_PROJECT_SUMMARY = 'This is your home project. Here you can sync your Blender settings!'
# HOME_PROJECT_DESCRIPTION = ('# Your home project\n\n'
# 'This is your home project. It has functionality to act '
# 'as a pastebin for text, images and other assets, and '
# 'allows synchronisation of your Blender settings.')
# HOME_PROJECT_SUMMARY = 'This is your home project. Pastebin and Blender settings sync in one!'
SYNC_GROUP_NODE_NAME = 'Blender Sync'
SYNC_GROUP_NODE_DESC = ('The [Blender Cloud Addon](https://cloud.blender.org/services'
'#blender-addon) will synchronize your Blender settings here.')
def create_blender_sync_node(project_id, admin_group_id, user_id):
"""Creates a node for Blender Sync, with explicit write access for the admin group.
Writes the node to the database.
:param project_id: ID of the home project
:type project_id: ObjectId
:param admin_group_id: ID of the admin group of the project. This group will
receive write access to the node.
:type admin_group_id: ObjectId
:param user_id: ID of the owner of the node.
:type user_id: ObjectId
:returns: The created node.
:rtype: dict
"""
log.debug('Creating sync node for project %s, user %s', project_id, user_id)
node = {
'project': ObjectId(project_id),
'node_type': 'group',
'name': SYNC_GROUP_NODE_NAME,
'user': ObjectId(user_id),
'description': SYNC_GROUP_NODE_DESC,
'properties': {'status': 'published'},
'permissions': {
'users': [],
'groups': [
{'group': ObjectId(admin_group_id),
'methods': ['GET', 'PUT', 'POST', 'DELETE']}
],
'world': [],
}
}
r, _, _, status = current_app.post_internal('nodes', node)
if status != 201:
log.warning('Unable to create Blender Sync node for home project %s: %s',
project_id, r)
raise wz_exceptions.InternalServerError('Unable to create Blender Sync node')
node.update(r)
return node
def create_home_project(user_id, write_access):
"""Creates a home project for the given user.
: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)
overrides = {
'category': 'home',
'url': 'home',
'summary': HOME_PROJECT_SUMMARY,
'description': HOME_PROJECT_DESCRIPTION
}
# Maybe the user has a deleted home project.
proj_coll = current_app.data.driver.db['projects']
deleted_proj = proj_coll.find_one({'user': user_id, 'category': 'home', '_deleted': True})
if deleted_proj:
log.info('User %s has a deleted project %s, restoring', user_id, deleted_proj['_id'])
project = deleted_proj
else:
log.debug('User %s does not have a deleted project', user_id)
project = proj_utils.create_new_project(project_name='Home',
user_id=ObjectId(user_id),
overrides=overrides)
# Re-validate the authentication token, so that the put_internal call sees the
# new group created for the project.
authentication.validate_token()
# There are a few things in the on_insert_projects hook we need to adjust.
# Ensure that the project is private, even for admins.
project['permissions']['world'] = []
# Set up the correct node types. No need to set permissions for them,
# as the inherited project permissions are fine.
from pillar.api.node_types.group import node_type_group
from pillar.api.node_types.asset import node_type_asset
# from pillar.api.node_types.text import node_type_text
from pillar.api.node_types.comment import node_type_comment
# 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)
# Everybody should be able to comment on anything in this project.
# This allows people to comment on shared images and see comments.
node_type_comment = assign_permissions(
node_type_comment,
subscriber_methods=['GET', 'POST'],
world_methods=['GET'])
project['node_types'] = [
node_type_group,
node_type_asset,
# node_type_text,
node_type_comment,
]
result, _, _, status = current_app.put_internal('projects', utils.remove_private_keys(project),
_id=project['_id'])
if status != 200:
log.error('Unable to update home project %s for user %s: %s',
project['_id'], user_id, result)
raise wz_exceptions.InternalServerError('Unable to update home project')
project.update(result)
# Create the Blender Sync node, with explicit write permissions on the node itself.
create_blender_sync_node(project['_id'],
project['permissions']['groups'][0]['group'],
user_id)
return project
def assign_permissions(node_type, subscriber_methods, world_methods):
"""Assigns permissions to the node type object.
:param node_type: a node type from pillar.api.node_types.
:type node_type: dict
:param subscriber_methods: allowed HTTP methods for users of role 'subscriber',
'demo' and 'admin'.
:type subscriber_methods: list
:param subscriber_methods: allowed HTTP methods for world
:type subscriber_methods: list
:returns: a copy of the node type, with embedded permissions
:rtype: dict
"""
from pillar.api import service
nt_with_perms = copy.deepcopy(node_type)
perms = nt_with_perms.setdefault('permissions', {})
perms['groups'] = [
{'group': service.role_to_group_id['subscriber'],
'methods': subscriber_methods[:]},
{'group': service.role_to_group_id['demo'],
'methods': subscriber_methods[:]},
{'group': service.role_to_group_id['admin'],
'methods': subscriber_methods[:]},
]
perms['world'] = world_methods[:]
return nt_with_perms
@blueprint.route('/home-project')
@authorization.require_login()
def home_project():
"""Fetches the home project, creating it if necessary.
Eve projections are supported, but at least the following fields must be present:
'permissions', 'category', 'user'
"""
user_id = g.current_user['user_id']
roles = g.current_user.get('roles', ())
log.debug('Possibly creating home project for user %s with roles %s', user_id, roles)
if HOME_PROJECT_USERS and not HOME_PROJECT_USERS.intersection(roles):
log.debug('User %s is not a subscriber, not creating home project.', user_id)
return 'No home project', 404
# 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 = write_access_with_roles(roles)
create_home_project(user_id, write_access)
resp, _, _, status, _ = get('projects', category='home', user=user_id)
if status != 200:
return utils.jsonify(resp), status
if resp['_items']:
project = resp['_items'][0]
else:
log.warning('Home project for user %s not found, while we just created it! Could be '
'due to projections and other arguments on the query string: %s',
user_id, request.query_string)
return 'No home project', 404
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 ['GET', 'PUT', 'POST', 'DELETE']
return ['GET']
def has_home_project(user_id):
"""Returns True iff the user has a home project."""
proj_coll = current_app.data.driver.db['projects']
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."""
proj_coll = current_app.data.driver.db['projects']
return proj_coll.count({'_id': project_id,
'user': user_id,
'category': 'home',
'_deleted': False}) > 0
def mark_node_updated(node_id):
"""Uses pymongo to set the node's _updated to "now"."""
now = datetime.datetime.now(tz=tz_util.utc)
nodes_coll = current_app.data.driver.db['nodes']
return nodes_coll.update_one({'_id': node_id},
{'$set': {'_updated': now}})
def get_home_project_parent_node(node, projection, name_for_log):
"""Returns a partial parent node document, but only if the node is a home project node."""
user_id = authentication.current_user_id()
if not user_id:
log.debug('%s: user not logged in.', name_for_log)
return None
parent_id = node.get('parent')
if not parent_id:
log.debug('%s: ignoring top-level node.', name_for_log)
return None
project_id = node.get('project')
if not project_id:
log.debug('%s: ignoring node without project ID', name_for_log)
return None
project_id = ObjectId(project_id)
if not is_home_project(project_id, user_id):
log.debug('%s: node not part of home project.', name_for_log)
return None
# Get the parent node for permission checking.
parent_id = ObjectId(parent_id)
nodes_coll = current_app.data.driver.db['nodes']
projection['project'] = 1
parent_node = nodes_coll.find_one(parent_id, projection=projection)
if parent_node['project'] != project_id:
log.warning('%s: User %s is trying to reference '
'parent node %s from different project %s, expected project %s.',
name_for_log, user_id, parent_id, parent_node['project'], project_id)
raise wz_exceptions.BadRequest('Trying to create cross-project links.')
return parent_node
def check_home_project_nodes_permissions(nodes):
for node in nodes:
check_home_project_node_permissions(node)
def check_home_project_node_permissions(node):
"""Grants POST access to the node when the user has POST access on its parent."""
parent_node = get_home_project_parent_node(node,
{'permissions': 1,
'project': 1,
'node_type': 1},
'check_home_project_node_permissions')
if parent_node is None or 'permissions' not in parent_node:
return
parent_id = parent_node['_id']
has_access = authorization.has_permissions('nodes', parent_node, 'POST')
if not has_access:
log.debug('check_home_project_node_permissions: No POST access to parent node %s, '
'ignoring.', parent_id)
return
# Grant access!
log.debug('check_home_project_node_permissions: POST access at parent node %s, '
'so granting POST access to new child node.', parent_id)
# Make sure the permissions of the parent node are copied to this node.
node['permissions'] = copy.deepcopy(parent_node['permissions'])
def mark_parents_as_updated(nodes):
for node in nodes:
mark_parent_as_updated(node)
def mark_parent_as_updated(node, original=None):
parent_node = get_home_project_parent_node(node,
{'permissions': 1,
'node_type': 1},
'mark_parent_as_updated')
if parent_node is None:
return
# Mark the parent node as 'updated' if this is an asset and the parent is a group.
if node.get('node_type') == 'asset' and parent_node['node_type'] == 'group':
log.debug('Node %s updated, marking parent=%s as updated too',
node['_id'], 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, not changing access permissions', 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_api_blueprint(blueprint, url_prefix=url_prefix)
app.on_insert_nodes += check_home_project_nodes_permissions
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 pillar.api import service
service.signal_user_changed_role.connect(user_changed_role)

File Metadata

Mime Type
text/x-python
Expires
Thu, Aug 6, 11:04 AM (1 d, 23 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
2e/a8/ea475d754435ef2d213367bfafd2

Event Timeline