Added PATCH support for organizations

With a PATCH request you can now:
  - assign users,
  - remove a user,
  - edit the name, description, and website fields.

Only the organization admin user can do this.
This commit is contained in:
2017-08-22 17:47:54 +02:00
parent 93d534fe94
commit efc1890871
5 changed files with 311 additions and 12 deletions

View File

@@ -1,6 +1,7 @@
def setup_app(app):
from . import encoding, blender_id, projects, local_auth, file_storage
from . import users, nodes, latest, blender_cloud, service, activities
from . import organizations
encoding.setup_app(app, url_prefix='/encoding')
blender_id.setup_app(app, url_prefix='/blender_id')
@@ -13,3 +14,4 @@ def setup_app(app):
service.setup_app(app, api_prefix='/service')
nodes.setup_app(app, url_prefix='/nodes')
activities.setup_app(app)
organizations.setup_app(app)

View File

@@ -138,10 +138,6 @@ organizations_schema = {
'maxlength': 128,
'required': True
},
'url': {
'type': 'string',
'maxlength': 128,
},
'description': {
'type': 'string',
'maxlength': 256,

View File

@@ -10,6 +10,7 @@ import typing
import attr
import bson
import werkzeug.exceptions as wz_exceptions
from pillar import attrs_extra, current_app
from pillar.api.utils import remove_private_keys
@@ -157,6 +158,9 @@ class OrgManager:
if user_doc is not None:
user_id = user_doc['_id']
if user_id and not users_coll.count({'_id': user_id}):
raise wz_exceptions.UnprocessableEntity('User does not exist')
self._log.info('Removing user %s / %s from organization %s', user_id, email, org_id)
org_doc = self._get_org(org_id)
@@ -185,13 +189,13 @@ class OrgManager:
return org_doc
def _get_org(self, org_id: bson.ObjectId):
def _get_org(self, org_id: bson.ObjectId, *, projection=None):
"""Returns the organization, or raises a ValueError."""
assert isinstance(org_id, bson.ObjectId)
org_coll = current_app.db('organizations')
org = org_coll.find_one(org_id)
org = org_coll.find_one(org_id, projection=projection)
if org is None:
raise ValueError(f'Organization {org_id} not found')
return org
@@ -236,9 +240,20 @@ class OrgManager:
if revoke_roles:
do_badger('revoke', roles=revoke_roles, user_id=user_id)
# def setup_app(app):
# from . import eve_hooks, api, patch
#
# eve_hooks.setup_app(app)
# api.setup_app(app)
# patch.setup_app(app)
def user_is_admin(self, org_id: bson.ObjectId) -> bool:
"""Returns whether the currently logged in user is the admin of the organization."""
from pillar.api.utils.authentication import current_user_id
uid = current_user_id()
if uid is None:
return False
org = self._get_org(org_id, projection={'admin_uid': 1})
return org['admin_uid'] == uid
def setup_app(app):
from . import patch
patch.setup_app(app)

View File

@@ -0,0 +1,115 @@
"""Organization patching support."""
import logging
import bson
from flask import Blueprint, jsonify
import werkzeug.exceptions as wz_exceptions
from pillar.api.utils.authentication import current_user_id
from pillar.api.utils import authorization, str2id, jsonify
from pillar.api import patch_handler
from pillar import current_app
log = logging.getLogger(__name__)
patch_api_blueprint = Blueprint('pillar.api.organizations.patch', __name__)
class OrganizationPatchHandler(patch_handler.AbstractPatchHandler):
item_name = 'organization'
@authorization.require_login()
def patch_assign_users(self, org_id: bson.ObjectId, patch: dict):
"""Assigns users to an organization.
The calling user must be admin of the organization.
"""
self._assert_is_admin(org_id)
# Do some basic validation.
try:
emails = patch['emails']
except KeyError:
raise wz_exceptions.BadRequest('No key "email" in patch.')
if not all(isinstance(email, str) for email in emails):
raise wz_exceptions.BadRequest('Invalid list of email addresses')
log.info('User %s uses PATCH to add users to organization %s', current_user_id(), org_id)
org_doc = current_app.org_manager.assign_users(org_id, emails)
return jsonify(org_doc)
@authorization.require_login()
def patch_remove_user(self, org_id: bson.ObjectId, patch: dict):
"""Removes a user from an organization.
The calling user must be admin of the organization.
"""
self._assert_is_admin(org_id)
# Do some basic validation.
email = patch.get('email') or None
user_id = patch.get('user_id')
user_oid = str2id(user_id) if user_id else None
log.info('User %s uses PATCH to remove user from organization %s',
current_user_id(), org_id)
org_doc = current_app.org_manager.remove_user(org_id, user_id=user_oid, email=email)
return jsonify(org_doc)
def _assert_is_admin(self, org_id):
om = current_app.org_manager
if not om.user_is_admin(org_id):
log.warning('User %s uses PATCH to edit organization %s, '
'but is not admin of that Organization. Request denied.',
current_user_id(), org_id)
raise wz_exceptions.Forbidden()
@authorization.require_login()
def patch_edit_from_web(self, org_id: bson.ObjectId, patch: dict):
"""Updates Organization fields from the web."""
from pymongo.results import UpdateResult
self._assert_is_admin(org_id)
# Only take known fields from the patch, don't just copy everything.
update = {
'name': patch['name'].strip(),
'description': patch.get('description', '').strip(),
'website': patch.get('website', '').strip(),
}
self.log.info('User %s edits Organization %s: %s', current_user_id(), org_id, update)
validator = current_app.validator_for_resource('organizations')
if not validator.validate_update(update, org_id):
resp = jsonify({
'_errors': validator.errors,
'_message': ', '.join(f'{field}: {error}'
for field, error in validator.errors.items()),
})
resp.status_code = 422
return resp
organizations_coll = current_app.db('organizations')
result: UpdateResult = organizations_coll.update_one(
{'_id': org_id},
{'$set': update}
)
if result.matched_count != 1:
self.log.warning('User %s edits Organization %s but update matched %i items',
current_user_id(), org_id, result.matched_count)
raise wz_exceptions.BadRequest()
return '', 204
def setup_app(app):
OrganizationPatchHandler(patch_api_blueprint)
app.register_api_blueprint(patch_api_blueprint, url_prefix='/organizations')