Merge remote-tracking branch 'origin/master'

This commit is contained in:
2016-04-25 11:58:41 +02:00
11 changed files with 365 additions and 135 deletions

View File

@@ -354,9 +354,6 @@ app.on_inserted_nodes += after_inserting_nodes
app.on_fetched_item_projects += before_returning_item_permissions
app.on_fetched_item_projects += project_node_type_has_method
app.on_fetched_resource_projects += before_returning_resource_permissions
# Projects hooks
app.on_insert_projects += before_inserting_projects
app.on_inserted_projects += after_inserting_projects
def post_GET_user(request, payload):
@@ -389,6 +386,8 @@ file_storage.setup_app(app, url_prefix='/storage')
# The encoding module (receive notification and report progress)
from modules.encoding import encoding
from modules.blender_id import blender_id
from modules import projects
app.register_blueprint(encoding, url_prefix='/encoding')
app.register_blueprint(blender_id, url_prefix='/blender_id')
projects.setup_app(app, url_prefix='/p')

View File

@@ -1,17 +1,23 @@
import logging
from flask import g
from flask import abort
from eve.methods.put import put_internal
import json
from eve.methods.post import post_internal
from application import app
from application.utils import remove_private_keys
from eve.methods.put import put_internal
from eve.methods.patch import patch_internal
from flask import g, Blueprint, request, abort, current_app
from application.utils import remove_private_keys, authorization, PillarJSONEncoder
from application.utils.gcs import GoogleCloudStorageBucket
from application.utils.authorization import user_has_role
from manage_extra.node_types.asset import node_type_asset
from manage_extra.node_types.blog import node_type_blog
from manage_extra.node_types.comment import node_type_comment
from manage_extra.node_types.group import node_type_group
from manage_extra.node_types.page import node_type_page
from manage_extra.node_types.comment import node_type_comment
from manage_extra.node_types.post import node_type_post
log = logging.getLogger(__name__)
blueprint = Blueprint('projects', __name__)
def before_inserting_projects(items):
@@ -20,6 +26,11 @@ def before_inserting_projects(items):
:param items: List of project docs that have been inserted (normally one)
"""
# Allow admin users to do whatever they want.
if user_has_role(u'admin'):
return
for item in items:
item.pop('url', None)
@@ -34,52 +45,143 @@ def after_inserting_projects(items):
:param items: List of project docs that have been inserted (normally one)
"""
current_user = g.current_user
users_collection = app.data.driver.db['users']
user = users_collection.find_one({'_id': current_user['user_id']})
users_collection = current_app.data.driver.db['users']
user = users_collection.find_one(current_user['user_id'])
for item in items:
# Create a project specific group (with name matching the project id)
project_group = dict(name=str(item['_id']))
group = post_internal('groups', project_group)
# If Group creation failed, stop
# TODO: undo project creation
if group[3] != 201:
abort(group[3])
else:
group = group[0]
# Assign the current user to the group
if 'groups' in user:
user['groups'].append(group['_id'])
else:
user['groups'] = [group['_id']]
put_internal('users', remove_private_keys(user), _id=user['_id'])
# Assign the group to the project with admin rights
permissions = dict(
world=['GET'],
users=[],
groups=[
dict(group=group['_id'],
methods=['GET', 'PUT', 'POST'])
]
)
# Assign permissions to the project itself, as well as to the node_types
item['permissions'] = permissions
node_type_asset['permissions'] = permissions
node_type_group['permissions'] = permissions
node_type_page['permissions'] = permissions
node_type_comment['permissions'] = permissions
# Assign the basic 'group', 'asset' and 'page' node_types
item['node_types'] = [
node_type_group,
node_type_asset,
node_type_page,
node_type_comment]
# TODO: Depending on user role or status, assign the url attribute
# Initialize storage page (defaults to GCS)
gcs_storage = GoogleCloudStorageBucket(str(item['_id']))
after_inserting_project(item, user)
def after_inserting_project(project, db_user):
project_id = project['_id']
user_id = db_user['_id']
# Create a project-specific admin group (with name matching the project id)
result, _, _, status = post_internal('groups', {'name': str(project_id)})
if status != 201:
log.error('Unable to create admin group for new project %s: %s',
project_id, result)
return abort_with_error(status)
admin_group_id = result['_id']
log.info('Created admin group %s for project %s', admin_group_id, project_id)
# Assign the current user to the group
db_user.setdefault('groups', []).append(admin_group_id)
result, _, _, status = patch_internal('users', {'groups': db_user['groups']}, _id=user_id)
if status != 200:
log.error('Unable to add user %s as member of admin group %s for new project %s: %s',
user_id, admin_group_id, project_id, result)
return abort_with_error(status)
log.debug('Made user %s member of group %s', user_id, admin_group_id)
# Assign the group to the project with admin rights
permissions = {
'world': ['GET'],
'users': [],
'groups': [
{'group': admin_group_id,
'methods': ['GET', 'PUT', 'POST']},
]
}
# Assign permissions to the project itself, as well as to the node_types
project['permissions'] = permissions
node_type_asset['permissions'] = permissions
node_type_group['permissions'] = permissions
node_type_page['permissions'] = permissions
node_type_comment['permissions'] = permissions
# Assign the basic 'group', 'asset' and 'page' node_types
project['node_types'] = [
node_type_group,
node_type_asset,
node_type_page,
node_type_comment]
# Allow admin users to use whatever url they want.
if not user_has_role(u'admin') or not project.get('url'):
project['url'] = "p-{!s}".format(project_id)
# Initialize storage page (defaults to GCS)
if current_app.config.get('TESTING'):
log.warning('Not creating Google Cloud Storage bucket while running unit tests!')
else:
gcs_storage = GoogleCloudStorageBucket(str(project_id))
if gcs_storage.bucket.exists():
log.debug("Created CGS bucket {0}".format(item['_id']))
# Assign a url based on the project id
item['url'] = "p-{}".format(item['_id'])
# Commit the changes
put_internal('projects', remove_private_keys(item), _id=item['_id'])
log.info('Created CGS instance for project %s', project_id)
else:
log.warning('Unable to create CGS instance for project %s', project_id)
# Commit the changes
result, _, _, status = put_internal('projects', remove_private_keys(project), _id=project_id)
if status != 200:
log.warning('Unable to update project %s: %s', project_id, result)
abort_with_error(status)
def _create_new_project(project_name, user_id, overrides):
"""Creates a new project owned by the given user."""
log.info('Creating new project "%s" for user %s', project_name, user_id)
# Create the project itself, the rest will be done by the after-insert hook.
project = {'description': '',
'name': project_name,
'node_types': [node_type_blog,
node_type_post,
node_type_comment],
'status': 'published',
'user': user_id,
'is_private': True,
'permissions': {},
'url': '',
'summary': '',
'category': 'assets', # TODO: allow the user to choose this.
}
if overrides is not None:
project.update(overrides)
result, _, _, status = post_internal('projects', project)
if status != 201:
log.error('Unable to create project "%s": %s', project_name, result)
return abort_with_error(status)
project.update(result)
log.info('Created project %s for user %s', project['_id'], user_id)
return project
@blueprint.route('/create', methods=['POST'])
@authorization.require_login(require_roles={'admin', 'subscriber'})
def create_project(overrides=None):
"""Creates a new project."""
project_name = request.form['project_name']
user_id = g.current_user['user_id']
project = _create_new_project(project_name, user_id, overrides)
# Return the project in the response.
resp = current_app.response_class(json.dumps(project, cls=PillarJSONEncoder),
mimetype='application/json',
status=201,
headers={'Location': '/projects/%s' % project['_id']})
return resp
def abort_with_error(status):
"""Aborts with the given status, or 500 if the status doesn't indicate an error.
If the status is < 400, status 500 is used instead.
"""
abort(status if status // 100 >= 4 else 500)
def setup_app(app, url_prefix):
app.on_insert_projects += before_inserting_projects
app.on_inserted_projects += after_inserting_projects
app.register_blueprint(blueprint, url_prefix=url_prefix)

View File

@@ -25,8 +25,6 @@ class PillarJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.datetime):
if obj.tzinfo is None:
raise ValueError('All datetime.datetime objects should be timezone-aware.')
return obj.strftime(RFC1123_DATE_FORMAT)
if isinstance(obj, bson.ObjectId):

View File

@@ -63,7 +63,8 @@ def validate_token():
return False
g.current_user = {'user_id': db_user['_id'],
'groups': db_user['groups']}
'groups': db_user['groups'],
'roles': set(db_user.get('roles', []))}
return True

View File

@@ -1,4 +1,5 @@
import logging
import functools
from flask import g
from flask import abort
from application import app
@@ -80,3 +81,38 @@ def check_permissions(resource, method, append_allowed_methods=False):
return
abort(403)
def require_login(require_roles=set()):
"""Decorator that enforces users to authenticate.
Optionally only allows access to users with a certain role./
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
current_user = g.get('current_user')
if current_user is None:
log.warning('Unauthenticated acces to %s attempted.', func)
abort(403)
if require_roles and not require_roles.intersection(set(current_user['roles'])):
log.warning('User %s is authenticated, but does not have any required role %s to '
'access %s', current_user['user_id'], require_roles, func)
abort(403)
return func(*args, **kwargs)
return wrapper
return decorator
def user_has_role(role):
"""Returns True iff the user is logged in and has the given role."""
current_user = g.get('current_user')
if current_user is None:
return False
return role in current_user['roles']

View File

@@ -67,13 +67,14 @@ def put_item(collection, item):
@manager.command
def setup_db():
def setup_db(admin_email):
"""Setup the database
- Create admin, subscriber and demo Group collection
- Create admin user (must use valid blender-id credentials)
- Create one project
"""
# groups_collection = app.data.driver.db['groups']
# Create default groups
groups_list = []
for group in ['admin', 'subscriber', 'demo']:
g = {'name': group}
@@ -81,58 +82,31 @@ def setup_db():
groups_list.append(g[0]['_id'])
print("Creating group {0}".format(group))
while True:
admin_username = raw_input('Admin email:')
if len(admin_username) < 1:
print ("Username is too short")
else:
break
# Create admin user
user = {'username': admin_email,
'groups': groups_list,
'roles': ['admin', 'subscriber', 'demo'],
'settings': {'email_communications': 1},
'auth': [],
'full_name': admin_email,
'email': admin_email}
result, _, _, status = post_internal('users', user)
if status != 201:
raise SystemExit('Error creating user {}: {}'.format(admin_email, result))
user.update(result)
print("Created user {0}".format(user['_id']))
user = dict(
username=admin_username,
groups=groups_list,
roles=['admin', 'subscriber', 'demo'],
settings=dict(email_communications=1),
auth=[],
full_name=admin_username,
email=admin_username,
)
user = post_internal('users', user)
print("Created user {0}".format(user[0]['_id']))
# Create a default project by faking a POST request.
with app.test_request_context(data={'project_name': u'Default Project'}):
from flask import g
from application.modules import projects
# TODO: Create a default project
default_permissions = _default_permissions()
g.current_user = {'user_id': user['_id'],
'groups': user['groups'],
'roles': set(user['roles'])}
node_type_blog['permissions'] = default_permissions
node_type_post['permissions'] = default_permissions
node_type_comment['permissions'] = default_permissions
project = dict(
owners=dict(users=[], groups=[]),
description='Default Project',
name='Default Project',
node_types=[
node_type_blog,
node_type_post,
node_type_comment
],
status='published',
user=user[0]['_id'],
is_private=False,
permissions=default_permissions,
url='default-project',
summary='Default Project summary',
category='training'
)
# Manually insert into db, since using post_internal would trigger hook
# TODO: fix this by bassing the context (and the user to g object)
projects_collection = app.data.driver.db['projects']
project = projects_collection.insert_one(project)
print("Created default project {0}".format(project.inserted_id))
gcs_storage = GoogleCloudStorageBucket(str(project.inserted_id))
if gcs_storage.bucket.exists():
print("Created CGS instance")
projects.create_project(overrides={'url': 'default-project',
'is_private': False})
def _default_permissions():

View File

@@ -560,28 +560,6 @@ projects_schema = {
'embeddable': True
},
},
'owners': {
'type': 'dict',
'schema': {
'users': {
'type': 'list',
'schema': {
'type': 'objectid',
}
},
'groups': {
'type': 'list',
'schema': {
'type': 'objectid',
'data_relation': {
'resource': 'groups',
'field': '_id',
'embeddable': True
}
}
}
}
},
'status': {
'type': 'string',
'allowed': [