diff --git a/api/auth/__init__.py b/api/auth/__init__.py index 44568af84ae4e70442a0cd95052ff9b6697d2810..ec2c9451fbec767056345fd60d6165f26e75cd05 100644 --- a/api/auth/__init__.py +++ b/api/auth/__init__.py @@ -22,6 +22,9 @@ def _get_access(uid, site, container): return INTEGER_ROLES[perm['access']] return -1 +def has_access(uid, container, perm, site='local'): + return _get_access(uid, site, container) >= INTEGER_ROLES[perm] + def always_ok(exec_op): """ This decorator leaves the original method unchanged. diff --git a/api/base.py b/api/base.py index 147ae7202c0dacb2449c6afa757630abb5e43c1b..6e2fd2adcf17efd41fac4edfa8a9450e4bbb20c5 100644 --- a/api/base.py +++ b/api/base.py @@ -13,7 +13,7 @@ from . import files from . import config from .types import Origin from . import validators -from .dao import APIConsistencyException, APIConflictException, APINotFoundException +from .dao import APIConsistencyException, APIConflictException, APINotFoundException, APIPermissionException class RequestHandler(webapp2.RequestHandler): @@ -272,6 +272,8 @@ class RequestHandler(webapp2.RequestHandler): self.request.logger.warning(str(exception)) elif isinstance(exception, APIConsistencyException): code = 400 + elif isinstance(exception, APIPermissionException): + code = 403 elif isinstance(exception, APINotFoundException): code = 404 elif isinstance(exception, APIConflictException): diff --git a/api/dao/__init__.py b/api/dao/__init__.py index c788106ab9d2057ac2ddb30485c8ac110c5e08b5..1cbbcd8ec055a247397f96491ff565000f9acc8e 100644 --- a/api/dao/__init__.py +++ b/api/dao/__init__.py @@ -10,5 +10,8 @@ class APIConflictException(Exception): class APINotFoundException(Exception): pass +class APIPermissionException(Exception): + pass + def noop(*args, **kwargs): # pylint: disable=unused-argument pass diff --git a/api/dao/hierarchy.py b/api/dao/hierarchy.py index 63da5d75cd17cf178bceea06c93f78061f29ac75..a13eb93d8bde776951faba9d052edd337a11e0ee 100644 --- a/api/dao/hierarchy.py +++ b/api/dao/hierarchy.py @@ -9,7 +9,8 @@ import re from .. import files from .. import util from .. import config -from . import APIStorageException, APINotFoundException, containerutil +from ..auth import has_access +from . import APIStorageException, APINotFoundException, APIPermissionException, containerutil log = config.log @@ -161,24 +162,33 @@ def _group_id_fuzzy_match(group_id, project_label): group_id = 'unknown' return group_id, project_label -def _find_or_create_destination_project(group_id, project_label, timestamp): +def _find_or_create_destination_project(group_id, project_label, timestamp, user): group_id, project_label = _group_id_fuzzy_match(group_id, project_label) group = config.db.groups.find_one({'_id': group_id}) - project = config.db.projects.find_one_and_update( - {'group': group['_id'], - 'label': {'$regex': re.escape(project_label), '$options': 'i'} - }, - { - '$setOnInsert': { + + project = config.db.projects.find_one({'group': group['_id'],'label': {'$regex': re.escape(project_label), '$options': 'i'}}) + + if project: + # If the project already exists, check the user's access + if user and not has_access(user, project, 'rw'): + raise APIPermissionException('User {} does not have read-write access to project {}'.format(user, project['label'])) + return project + + else: + # if the project doesn't exit, check the user's access at the group level + if user and not has_access(user, group, 'rw'): + raise APIPermissionException('User {} does not have read-write access to group {}'.format(user, group_id)) + + project = { + 'group': group['_id'], 'label': project_label, - 'permissions': group['roles'], 'public': False, - 'created': timestamp, 'modified': timestamp - } - }, - PROJECTION_FIELDS, - upsert=True, - return_document=pymongo.collection.ReturnDocument.AFTER, - ) + 'permissions': group['roles'], + 'public': False, + 'created': timestamp, + 'modified': timestamp + } + result = config.db.projects.insert_one(project) + project['_id'] = result.inserted_id return project def _create_query(cont, cont_type, parent_type, parent_id, upload_type): @@ -194,7 +204,7 @@ def _create_query(cont, cont_type, parent_type, parent_id, upload_type): 'uid': cont['uid'] } else: - raise NotImplementedError('upload type is not handled by _create_query') + raise NotImplementedError('upload type {} is not handled by _create_query'.format(upload_type)) def _upsert_container(cont, cont_type, parent, parent_type, upload_type, timestamp): cont['modified'] = timestamp @@ -261,7 +271,7 @@ def _get_targets(project_obj, session, acquisition, type_, timestamp): return target_containers -def find_existing_hierarchy(metadata): +def find_existing_hierarchy(metadata, user=None): project = metadata.get('project', {}) session = metadata.get('session', {}) acquisition = metadata.get('acquisition', {}) @@ -276,8 +286,12 @@ def find_existing_hierarchy(metadata): # Confirm session and acquisition exist session_obj = config.db.sessions.find_one({'uid': session_uid}, ['project']) + if session_obj is None: raise APINotFoundException('Session with uid {} does not exist'.format(session_uid)) + if user and not has_access(user, session_obj, 'rw'): + raise APIPermissionException('User {} does not have read-write access to session {}'.format(user, session_uid)) + a = config.db.acquisitions.find_one({'uid': acquisition_uid}, ['_id']) if a is None: raise APINotFoundException('Acquisition with uid {} does not exist'.format(acquisition_uid)) @@ -292,7 +306,7 @@ def find_existing_hierarchy(metadata): return target_containers -def upsert_bottom_up_hierarchy(metadata): +def upsert_bottom_up_hierarchy(metadata, user=None): group = metadata.get('group', {}) project = metadata.get('project', {}) session = metadata.get('session', {}) @@ -310,6 +324,10 @@ def upsert_bottom_up_hierarchy(metadata): session_obj = config.db.sessions.find_one({'uid': session_uid}, ['project']) if session_obj: # skip project creation, if session exists + + if user and not has_access(user, session_obj, 'rw'): + raise APIPermissionException('User {} does not have read-write access to session {}'.format(user, session_uid)) + now = datetime.datetime.utcnow() project_files = dict_fileinfos(project.pop('files', [])) project_obj = config.db.projects.find_one({'_id': session_obj['project']}, projection=PROJECTION_FIELDS + ['name']) @@ -319,10 +337,11 @@ def upsert_bottom_up_hierarchy(metadata): ) return target_containers else: - return upsert_top_down_hierarchy(metadata, 'uid') + return upsert_top_down_hierarchy(metadata, 'uid', user=user) -def upsert_top_down_hierarchy(metadata, type_='label'): +def upsert_top_down_hierarchy(metadata, type_='label', user=None): + log.debug('I know my type is {}'.format(type_)) group = metadata['group'] project = metadata['project'] session = metadata.get('session') @@ -330,7 +349,7 @@ def upsert_top_down_hierarchy(metadata, type_='label'): now = datetime.datetime.utcnow() project_files = dict_fileinfos(project.pop('files', [])) - project_obj = _find_or_create_destination_project(group['_id'], project['label'], now) + project_obj = _find_or_create_destination_project(group['_id'], project['label'], now, user) target_containers = _get_targets(project_obj, session, acquisition, type_, now) target_containers.append( (TargetContainer(project_obj, 'project'), project_files) diff --git a/api/placer.py b/api/placer.py index 8796f8fa9b17a8379c1d96f58832e94367cc0e61..2298558539fe495d1bd3e04fba0dfd00fa801482 100644 --- a/api/placer.py +++ b/api/placer.py @@ -138,7 +138,8 @@ class UIDPlacer(Placer): metadata_validator = validators.from_schema_path(payload_schema_uri) metadata_validator(self.metadata, 'POST') - targets = self.create_hierarchy(self.metadata) + # If not a superuser request, pass uid of user making the upload request + targets = self.create_hierarchy(self.metadata, user=self.context.get('uid')) self.metadata_for_file = {} diff --git a/api/upload.py b/api/upload.py index c3fa30238ac5694b1030cb2f3a45e8e8e08c9b04..65c748fe58245f40d1ee6cbda82bc5934ae12fb0 100644 --- a/api/upload.py +++ b/api/upload.py @@ -143,7 +143,11 @@ class Upload(base.RequestHandler): """Receive a sortable reaper upload.""" if not self.superuser_request: - self.abort(402, 'uploads must be from an authorized drone') + user = self.uid + if not user: + self.abort(403, 'Uploading requires login') + + context = {'uid': self.uid if not self.superuser_request else None} # TODO: what enum if strategy == 'label': @@ -154,7 +158,7 @@ class Upload(base.RequestHandler): strategy = Strategy.uidmatch else: self.abort(500, 'stragegy {} not implemented'.format(strategy)) - return process_upload(self.request, strategy, origin=self.origin) + return process_upload(self.request, strategy, origin=self.origin, context=context) def engine(self): """Handles file uploads from the engine"""