From ac8f7ee9a12e272afe0ca1293d0204f132efed68 Mon Sep 17 00:00:00 2001 From: eagw Date: Mon, 1 Aug 2022 12:55:41 -0400 Subject: [PATCH 01/17] Add annotation access controls at the folder level --- .../rest/annotation.py | 92 +++++++++++ .../web_client/views/hierarchyWidget.js | 150 ++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 girder_annotation/girder_large_image_annotation/web_client/views/hierarchyWidget.js diff --git a/girder_annotation/girder_large_image_annotation/rest/annotation.py b/girder_annotation/girder_large_image_annotation/rest/annotation.py index e4ca0e453..74dede488 100644 --- a/girder_annotation/girder_large_image_annotation/rest/annotation.py +++ b/girder_annotation/girder_large_image_annotation/rest/annotation.py @@ -21,6 +21,7 @@ import cherrypy import orjson +from bson.objectid import ObjectId from girder import logger from girder.api import access from girder.api.describe import Description, autoDescribeRoute, describeRoute @@ -58,6 +59,8 @@ def __init__(self): self.route('GET', ('item', ':id'), self.getItemAnnotations) self.route('POST', ('item', ':id'), self.createItemAnnotations) self.route('DELETE', ('item', ':id'), self.deleteItemAnnotations) + self.route('GET', ('folder', ':id'), self.returnFolderAnnotations) + self.route('GET', ('folder', ':id', 'present'), self.existFolderAnnotations) self.route('GET', ('old',), self.getOldAnnotations) self.route('DELETE', ('old',), self.deleteOldAnnotations) self.route('GET', ('counts',), self.getItemListAnnotationCounts) @@ -558,6 +561,95 @@ def deleteItemAnnotations(self, item): count += 1 return count + def getFolderAnnotations(self, id, recurse, user, limit=False, offset=False, sort=False, sortDir=False): + recursivePipeline = [ + {'$graphLookup': { + 'from': 'folder', + 'startWith': '$_id', + 'connectFromField': '_id', + 'connectToField': 'parentId', + 'as': '__children' + }}, + {'$unwind': {'path': '$__children'}}, + {'$replaceRoot': {'newRoot': '$__children'}}] if recurse else [] + accessPipeline = [ + {'$match': { + '$or': [ + {'access.users': + {'$elemMatch': { + 'id': user['_id'], + 'level': {'$gte': 2} + }}}, + {'access.groups': + {'$elemMatch': { + 'id': {'$in': user['groups']}, + 'level': {'$gte': 2} + }}} + ] + }} + ] if not user['admin'] else [] + pipeline = [ + {'$match': {'_id': 'none'}}, + {'$unionWith': { + 'coll': 'folder', + 'pipeline': [{'$match': {'_id': ObjectId(id)}}] + + recursivePipeline + + [{'$unionWith': { + 'coll': 'folder', + 'pipeline': [{'$match': {'_id': ObjectId(id)}}] + }}, {'$lookup': { + 'from': 'item', + 'localField': '_id', + 'foreignField': 'folderId', + 'as': '__items' + }}, {'$lookup': { + 'from': 'annotation', + 'localField': '__items._id', + 'foreignField': 'itemId', + 'as': '__annotations' + }}, {'$unwind': '$__annotations'}, + {'$replaceRoot': {'newRoot': '$__annotations'}}, + {'$match': {'_active': {'$ne': False}}} + ] + accessPipeline + }}, + ] + pipeline = pipeline + [{'$sort': {sort: sortDir}}] if sort else pipeline + pipeline = pipeline + [{'$skip': offset}] if offset else pipeline + pipeline = pipeline + [{'$limit': limit}] if limit else pipeline + + return Annotation().collection.aggregate(pipeline) + + + @autoDescribeRoute( + Description('Check if there are any annotations from the items in a folder') + .param('id', 'The ID of the folder', required=True, paramType='path') + .param('recurse', 'Whether or not to recursively check ' + 'subfolders for annotations', required=False, default=True, dataType='boolean') + .errorResponse() + ) + @access.public + def existFolderAnnotations(self, id, recurse): + annotations = self.getFolderAnnotations(id, recurse, self.getCurrentUser(), 1) + try: + next(annotations) + yield True + except StopIteration: + yield False + + + @autoDescribeRoute( + Description('Get the annotations from the items in a folder') + .param('id', 'The ID of the folder', required=True, paramType='path') + .param('recurse', 'Whether or not to retrieve all ' + 'annotations from subfolders', required=False, default=False, dataType='boolean') + .pagingParams(defaultSort='created', defaultSortDir=-1) + .errorResponse() + ) + @access.public + def returnFolderAnnotations(self, id, recurse, limit, offset, sort): + return self.getFolderAnnotations(id, recurse, self.getCurrentUser(), limit, offset, + sort[0][0], sort[0][1]) + @autoDescribeRoute( Description('Report on old annotations.') .param('age', 'The minimum age in days.', required=False, diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/hierarchyWidget.js b/girder_annotation/girder_large_image_annotation/web_client/views/hierarchyWidget.js new file mode 100644 index 000000000..d5d922227 --- /dev/null +++ b/girder_annotation/girder_large_image_annotation/web_client/views/hierarchyWidget.js @@ -0,0 +1,150 @@ +import $ from 'jquery'; + +import { wrap } from '@girder/core/utilities/PluginUtils'; +import { restRequest } from '@girder/core/rest'; +import AccessWidget from '@girder/core/views/widgets/AccessWidget'; +import HierarchyWidget from '@girder/core/views/widgets/HierarchyWidget'; +import UserCollection from '@girder/core/collections/UserCollection'; + +import AnnotationCollection from '../collections/AnnotationCollection'; + +wrap(HierarchyWidget, 'initialize', function (initialize, settings) { + initialize.call(this, settings); + + if (this.parentModel.get('_modelType') === 'folder') { + fetchCollections(this, this.parentModel.id); + } + this.folderListView.on('g:folderClicked', () => { + fetchCollections(this, this.parentModel.id); + addAccessControl(this); + }); +}); + +wrap(HierarchyWidget, 'render', function (render) { + render.call(this); + + if (this.parentModel.get('_modelType') === 'folder' && this.recurseCollection) { + addAccessControl(this); + } +}); + +function fetchCollections(root, folderId) { + restRequest({ + type: 'GET', + url: 'annotation/folder/' + folderId + '/present', + data: { + id: folderId, + recurse: true + } + }).done((resp) => { + if (resp[0]) { + root.users = new UserCollection(); + + root.recurseCollection = new AnnotationCollection([], {comparator: null}); + root.recurseCollection.altUrl = 'annotation/folder/' + folderId; + root.recurseCollection.fetch({ + id: folderId, + sort: 'created', + sortDir: -1, + recurse: true + }).done(() => { + root.recurseCollection.each((model) => { + root.users.add({'_id': model.get('creatorId')}); + }); + $.when.apply($, root.users.map((model) => { + return model.fetch(); + })).always(() => { + root.render(); + }); + }); + + root.collection = new AnnotationCollection([], {comparator: null}); + root.collection.altUrl = 'annotation/folder/' + folderId; + root.collection.fetch({ + id: folderId, + sort: 'created', + sortDir: -1, + recurse: false + }).done(() => { + root.collection.each((model) => { + root.users.add({'_id': model.get('creatorId')}); + }); + $.when.apply($, root.users.map((model) => { + return model.fetch(); + })).always(() => { + root.render(); + }); + }); + } + }); +} + +function addAccessControl(root) { + if (root.$('.g-edit-annotation-access').length === 0) { + if (root.$('.g-folder-actions-menu > .divider').length > 0) { + root.$('.g-folder-actions-menu > .divider').before( + '
  • ' + + '' + + '' + + 'Annotation access control' + + '' + + '
  • ' + ); + } else { + root.$('.g-folder-actions-menu > .dropdown-header').after( + '
  • ' + + '' + + '' + + 'Annotation access control' + + '' + + '
  • ' + + '' ); } else { - root.$('.g-folder-actions-menu > .dropdown-header').after( + root.$('ul.g-folder-actions-menu').append( '
  • ' + '' + '' + 'Annotation access control' + '' + - '
  • ' + - '' ); } root.events['click .g-edit-annotation-access'] = editAnnotAccess;