From 4cb371b4c4c112c84cc36ddcc3671b06534ab793 Mon Sep 17 00:00:00 2001 From: Harsha Kethineni Date: Tue, 8 Aug 2017 15:28:01 -0500 Subject: [PATCH 1/5] savesearch handler and collection added --- api/api.py | 7 ++ api/config.py | 1 + api/dao/containerstorage.py | 13 ++++ api/handlers/savedsearchhandler.py | 67 +++++++++++++++++++ raml/schemas/definitions/search.json | 46 +++++++++++++ raml/schemas/input/search-input.json | 6 ++ .../python/test_savedsearch.py | 49 ++++++++++++++ 7 files changed, 189 insertions(+) create mode 100644 api/handlers/savedsearchhandler.py create mode 100644 raml/schemas/definitions/search.json create mode 100644 raml/schemas/input/search-input.json create mode 100644 test/integration_tests/python/test_savedsearch.py diff --git a/api/api.py b/api/api.py index 25f1d3611..d2a2e74ed 100644 --- a/api/api.py +++ b/api/api.py @@ -13,6 +13,7 @@ from .handlers.reporthandler import ReportHandler from .handlers.resolvehandler import ResolveHandler from .handlers.roothandler import RootHandler +from .handlers.savedsearchhandler import SavedSearchHandler from .handlers.schemahandler import SchemaHandler from .handlers.userhandler import UserHandler from .jobs.handlers import BatchHandler, JobsHandler, JobHandler, GearsHandler, GearHandler, RulesHandler, RuleHandler @@ -109,6 +110,12 @@ def prefix(path, routes): route('/dataexplorer/search/nodes', DataExplorerHandler, h='get_nodes', m=['POST']), route('/dataexplorer/index/fields', DataExplorerHandler, h='index_field_names', m=['POST']), + # Search Saving + route('/savesearch', SavedSearchHandler, m=['POST']), + route('/savesearch', SavedSearchHandler, h='get_all', m=['GET']), + route('/savesearch/', SavedSearchHandler, m=['GET','DELETE']), + route('/savesearch/', SavedSearchHandler, h='replace_search', m=['POST']), + # Users route( '/users', UserHandler, h='get_all', m=['GET']), diff --git a/api/config.py b/api/config.py index 74657503d..1e00f653e 100644 --- a/api/config.py +++ b/api/config.py @@ -172,6 +172,7 @@ def apply_env_variables(config): 'project-update.json', 'rule-new.json', 'rule-update.json', + 'search-input.json', 'session.json', 'session-update.json', 'subject.json', diff --git a/api/dao/containerstorage.py b/api/dao/containerstorage.py index de2790385..b2c8580bd 100644 --- a/api/dao/containerstorage.py +++ b/api/dao/containerstorage.py @@ -434,3 +434,16 @@ def inflate_job_info(self, analysis): analysis['job'] = job return analysis + +class SearchStorage(ContainerStorage): + + def __init__(self): + super(SearchStorage, self).__init__('searches', use_object_id=True) + + def replace_el(self, search): + self.delete_el(search['_id']) + return self.create_el(search) + + + + diff --git a/api/handlers/savedsearchhandler.py b/api/handlers/savedsearchhandler.py new file mode 100644 index 000000000..362f47b1b --- /dev/null +++ b/api/handlers/savedsearchhandler.py @@ -0,0 +1,67 @@ +import bson +from ..web import base +from .. import config, validators +from ..auth import require_login +from ..dao.containerstorage import SearchStorage + +from ..auth import groupauth +from ..dao import noop + + +log = config.log +storage = SearchStorage() + +class SavedSearchHandler(base.RequestHandler): + + def __init__(self, request=None, response=None): + super(SavedSearchHandler, self).__init__(request, response) + + @require_login + def post(self): + payload = self.request.json_body + validators.validate_data(payload, 'search-input.json', 'input', 'POST') + payload['permissions'] = [{"_id": self.uid, "access": "admin"}] + result = storage.create_el(payload) + if result.acknowledged: + if result.inserted_id: + return {'_id': result.inserted_id} + return {"hi" : "bye"} + + def get_all(self): + log.debug(self.uid) + return storage.get_all_el({}, {'_id': self.uid}, {'label': 1}) + + def get(self, sid): + result = storage.get_el(sid) + if result is None: + self.abort(404, 'Element {} not found'.format(sid)) + return result + + def delete(self, sid): + search = storage.get_container(sid) + permchecker = groupauth.default(self, search) + result = permchecker(storage.exec_op)('DELETE', sid) + if result.deleted_count == 1: + return {'deleted': result.deleted_count} + else: + self.abort(404, 'Group {} not removed'.format(sid)) + return result + + def replace_search(self, sid): + payload = self.request.json_body + if payload.get('_id'): + del(payload['_id']) + if payload.get('permissions'): + perms = payload['permissions'] + del(payload['permissions']) + validators.validate_data(payload, 'search-input.json', 'input', 'POST') + payload['_id'] = bson.ObjectId(sid) + payload['permissions'] = perms + search = storage.get_container(sid) + permchecker = groupauth.default(self, search) + permchecker(noop)('DELETE', sid) + result = storage.replace_el(payload) + if result.acknowledged: + if result.inserted_id: + return {'_id': result.inserted_id} + return {"hi" : "bye"} diff --git a/raml/schemas/definitions/search.json b/raml/schemas/definitions/search.json new file mode 100644 index 000000000..aa34ae937 --- /dev/null +++ b/raml/schemas/definitions/search.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions":{ + "filter" : { + "type" : "object" + + }, + "search" : { + "type": "object", + "properties": { + "filters": { + "type" : "array", + "items" : {"$ref": "#/definitions/filter"} + }, + "search_string": {"type" : "string"}, + "all_data": {"type": "boolean"}, + "return_type": {"enum": ["session", "acquisition", "analysis", "file"]} + }, + "additionalProperties": false, + "required": ["return_type"] + }, + "search-input":{ + "type": "object", + "properties": { + "label": {"$ref": "../definitions/container.json#/definitions/label"}, + "search": {"$ref": "#/definitions/search"} + }, + "additionalProperties": false + }, + "search-output":{ + "type": "object", + "properties": { + "_id": {"$ref":"../definitions/objectid.json#"}, + "label": {"$ref": "../definitions/container.json#/definitions/label"}, + "uid": {"$ref":"../definitions/container.json#/definitions/uid"}, + "created": {"$ref":"../definitions/created-modified.json#/definitions/created"}, + "modified": {"$ref":"../definitions/created-modified.json#/definitions/modified"}, + "permissions": { + "type":"array", + "items":{"$ref":"../definitions/permission.json#/definitions/permission-output-default-required"} + }, + "search": {"$ref":"#/definitions/search"} + } + } + } +} \ No newline at end of file diff --git a/raml/schemas/input/search-input.json b/raml/schemas/input/search-input.json new file mode 100644 index 000000000..053cd9e5f --- /dev/null +++ b/raml/schemas/input/search-input.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Search", + "type": "object", + "allOf":[{"$ref":"../definitions/search.json#/definitions/search-input"}] +} \ No newline at end of file diff --git a/test/integration_tests/python/test_savedsearch.py b/test/integration_tests/python/test_savedsearch.py new file mode 100644 index 000000000..31843eb97 --- /dev/null +++ b/test/integration_tests/python/test_savedsearch.py @@ -0,0 +1,49 @@ + +def test_search_saving(as_admin, data_builder): + + # Try posting a malformed search + r = as_admin.post('/savesearch', json={"not-label":"random-string"}) + assert r.status_code == 400 + + # Try getting a non-existent saved search + r = as_admin.get('/savesearch/000000000000000000000000') + assert r.status_code == 404 + + # Save a search + r = as_admin.post('/savesearch', json={'label': 'search1', 'search': {'return_type': 'session'}}) + assert r.ok + search = r.json()['_id'] + + # Get all searched user has access to + r = as_admin.get('/savesearch') + assert r.ok + + # Get the saved search by id + r = as_admin.get('/savesearch/' + search) + assert r.ok + assert r.json()['label'] == 'search1' + + # Malformed search replace + payload = {'label': 'good-label', 'search' : { 'not-return-type' : 'not-container'}} + r = as_admin.post('/savesearch/' + search, json=payload) + assert r.status_code == 400 + + # Replace search + r = as_admin.get('/savesearch/' + search) + assert r.ok + assert r.json()['label'] == 'search1' + payload = r.json() + payload['label'] = 'newSearch' + r = as_admin.post('/savesearch/' + search, json=payload) + assert r.ok + assert r.json()['_id'] == search + r = as_admin.get('/savesearch/' + search) + assert r.ok + assert r.json()['label'] == 'newSearch' + + # Delete saved search + r = as_admin.delete('/savesearch/' + search) + assert r.ok + r = as_admin.get('/savesearch') + assert r.ok + assert len(r.json()) == 0 From f0024185c8e64280d43bc8bc17913e3cd8a83fa4 Mon Sep 17 00:00:00 2001 From: Harsha Kethineni Date: Wed, 9 Aug 2017 10:13:35 -0500 Subject: [PATCH 2/5] added abao tests --- api/handlers/savedsearchhandler.py | 3 +- raml/api.raml | 1 + raml/resources/savesearch.raml | 29 +++++++++ raml/schemas/output/search-list.json | 11 ++++ raml/schemas/output/search-output.json | 8 +++ .../integration_tests/abao/abao_test_hooks.js | 65 ++++++++++++++++++- 6 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 raml/resources/savesearch.raml create mode 100644 raml/schemas/output/search-list.json create mode 100644 raml/schemas/output/search-output.json diff --git a/api/handlers/savedsearchhandler.py b/api/handlers/savedsearchhandler.py index 362f47b1b..64ed62ea6 100644 --- a/api/handlers/savedsearchhandler.py +++ b/api/handlers/savedsearchhandler.py @@ -52,12 +52,11 @@ def replace_search(self, sid): if payload.get('_id'): del(payload['_id']) if payload.get('permissions'): - perms = payload['permissions'] del(payload['permissions']) validators.validate_data(payload, 'search-input.json', 'input', 'POST') payload['_id'] = bson.ObjectId(sid) - payload['permissions'] = perms search = storage.get_container(sid) + payload['permissions'] = search['permissions'] permchecker = groupauth.default(self, search) permchecker(noop)('DELETE', sid) result = storage.replace_el(payload) diff --git a/raml/api.raml b/raml/api.raml index 27623b518..8ade5bf47 100644 --- a/raml/api.raml +++ b/raml/api.raml @@ -55,3 +55,4 @@ resourceTypes: /acquisitions: !include resources/acquisitions.raml /projects: !include resources/projects.raml /report: !include resources/report.raml +/savesearch: !include resources/savesearch.raml diff --git a/raml/resources/savesearch.raml b/raml/resources/savesearch.raml new file mode 100644 index 000000000..2e6751783 --- /dev/null +++ b/raml/resources/savesearch.raml @@ -0,0 +1,29 @@ +type: container +get: + description: List all saved searches user has access to. + responses: + 200: + body: + application/json: + schema: !include ../schemas/output/search-list.json +post: + body: + application/json: + schema: !include ../schemas/input/search-input.json +/{SearchId}: + type: container + get: + responses: + 200: + body: + application/json: + schema: !include ../schemas/output/search-output.json + delete: + description: Delete a saved search + responses: + 200: + body: + application/json: + schema: !include ../schemas/output/container-delete.json + example: !include ../examples/output/container-delete.json + diff --git a/raml/schemas/output/search-list.json b/raml/schemas/output/search-list.json new file mode 100644 index 000000000..058612f9c --- /dev/null +++ b/raml/schemas/output/search-list.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type":"array", + "items":{ + "type":"object", + "allOf":[{"$ref":"../definitions/search.json#/definitions/search-output"}], + "required":[ + "_id", "label", "permissions", "search" + ] + } +} \ No newline at end of file diff --git a/raml/schemas/output/search-output.json b/raml/schemas/output/search-output.json new file mode 100644 index 000000000..03fb92b8a --- /dev/null +++ b/raml/schemas/output/search-output.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type":"object", + "allOf":[{"$ref":"../definitions/search.json#/definitions/search-output"}], + "required":[ + "_id", "label", "permissions", "search" + ] +} \ No newline at end of file diff --git a/tests/integration_tests/abao/abao_test_hooks.js b/tests/integration_tests/abao/abao_test_hooks.js index 270c43aae..99814b096 100644 --- a/tests/integration_tests/abao/abao_test_hooks.js +++ b/tests/integration_tests/abao/abao_test_hooks.js @@ -21,8 +21,9 @@ var example_acquisition_id = ''; var test_project_1 = null; var test_project_tag = 'test-project-tag'; var delete_project_id = ''; -var device_id = 'bootstrapper_Bootstrapper' -var injected_api_key = 'XZpXI40Uk85eozjQkU1zHJ6yZHpix+j0mo1TMeGZ4dPzIqVPVGPmyfeK' +var device_id = 'bootstrapper_Bootstrapper'; +var injected_api_key = 'XZpXI40Uk85eozjQkU1zHJ6yZHpix+j0mo1TMeGZ4dPzIqVPVGPmyfeK'; +var search_id = ''; // Tests we're skipping, fix these @@ -1461,3 +1462,63 @@ hooks.before("GET /devices/{DeviceId} -> 404", function(test, done) { test.request.params.DeviceId = 'bad_device_id'; done(); }); + +// Save Search Tests +hooks.before("POST /savesearch -> 200", function(test, done) { + test.request.body = { + "label": "Lable", + "search": { + "return_type": "session" + } + }; + done(); +}) +hooks.after("POST /savesearch -> 200", function(test, done) { + search_id = test.response.body['_id']; + done(); +}) + +hooks.before("POST /savesearch -> 400", function(test, done) { + test.request.body = { + "not-label": "Label" + }; + done(); +}) + +hooks.before("GET /savesearch/{SearchId} -> 200", function(test, done) { + test.request.params = { + SearchId: search_id + }; + done(); +}) + +hooks.before("POST /savesearch/{SearchId} -> 200", function(test, done) { + test.request.params = { + SearchId: search_id + }; + test.request.body = { + "label": "New Label", + "search": { + "return_type": "session" + }, + "_id": search_id + }; + done(); +}) + +hooks.before("POST /savesearch/{SearchId} -> 400", function(test, done) { + test.request.params = { + SearchId: search_id + }; + test.request.body = { + "not-label": "Label2" + }; + done(); +}) + +hooks.before("DELETE /savesearch/{SearchId} -> 200", function(test, done) { + test.request.params = { + SearchId: search_id + }; + done(); +}) From 0db22bed80d0aa4c6f1246598e54b89ff1ad68e8 Mon Sep 17 00:00:00 2001 From: Harsha Kethineni Date: Wed, 9 Aug 2017 11:29:36 -0500 Subject: [PATCH 3/5] refactored code to make names consistent --- api/api.py | 12 +++--- api/config.py | 1 + api/dao/containerstorage.py | 2 +- api/dao/containerutil.py | 1 + api/handlers/listhandler.py | 1 + ...dsearchhandler.py => savesearchhandler.py} | 22 +++++++--- raml/api.raml | 2 +- raml/schemas/definitions/search.json | 2 +- .../python/test_savedsearch.py | 43 ++++++++++++++----- .../integration_tests/abao/abao_test_hooks.js | 14 +++--- 10 files changed, 67 insertions(+), 33 deletions(-) rename api/handlers/{savedsearchhandler.py => savesearchhandler.py} (83%) diff --git a/api/api.py b/api/api.py index d2a2e74ed..6de93e09d 100644 --- a/api/api.py +++ b/api/api.py @@ -13,7 +13,7 @@ from .handlers.reporthandler import ReportHandler from .handlers.resolvehandler import ResolveHandler from .handlers.roothandler import RootHandler -from .handlers.savedsearchhandler import SavedSearchHandler +from .handlers.savesearchhandler import SaveSearchHandler from .handlers.schemahandler import SchemaHandler from .handlers.userhandler import UserHandler from .jobs.handlers import BatchHandler, JobsHandler, JobHandler, GearsHandler, GearHandler, RulesHandler, RuleHandler @@ -111,10 +111,10 @@ def prefix(path, routes): route('/dataexplorer/index/fields', DataExplorerHandler, h='index_field_names', m=['POST']), # Search Saving - route('/savesearch', SavedSearchHandler, m=['POST']), - route('/savesearch', SavedSearchHandler, h='get_all', m=['GET']), - route('/savesearch/', SavedSearchHandler, m=['GET','DELETE']), - route('/savesearch/', SavedSearchHandler, h='replace_search', m=['POST']), + route('/savesearches', SaveSearchHandler, m=['POST']), + route('/savesearches', SaveSearchHandler, h='get_all', m=['GET']), + route('/savesearches/', SaveSearchHandler, m=['GET','DELETE']), + route('/savesearches/', SaveSearchHandler, h='replace_search', m=['POST']), # Users @@ -240,7 +240,7 @@ def prefix(path, routes): # Collections / Projects - prefix('/', [ + prefix('/', [ prefix('/', [ route('/', PermissionsListHandler, m=['POST']), route('//<_id:{uid}>', PermissionsListHandler, m=['GET', 'PUT', 'DELETE']), diff --git a/api/config.py b/api/config.py index 1e00f653e..1ad3d7649 100644 --- a/api/config.py +++ b/api/config.py @@ -229,6 +229,7 @@ def initialize_db(): # TODO review all indexes db.users.create_index('api_key.key') db.projects.create_index([('gid', 1), ('name', 1)]) + db.savesearches.create_index('creator') db.sessions.create_index('project') db.sessions.create_index('uid') db.sessions.create_index('created') diff --git a/api/dao/containerstorage.py b/api/dao/containerstorage.py index b2c8580bd..52523d1bd 100644 --- a/api/dao/containerstorage.py +++ b/api/dao/containerstorage.py @@ -438,7 +438,7 @@ def inflate_job_info(self, analysis): class SearchStorage(ContainerStorage): def __init__(self): - super(SearchStorage, self).__init__('searches', use_object_id=True) + super(SearchStorage, self).__init__('savesearches', use_object_id=True) def replace_el(self, search): self.delete_el(search['_id']) diff --git a/api/dao/containerutil.py b/api/dao/containerutil.py index bc748a0c5..836d1cf41 100644 --- a/api/dao/containerutil.py +++ b/api/dao/containerutil.py @@ -17,6 +17,7 @@ 'project': 'projects', 'session': 'sessions', 'user': 'users', + 'savesearch': 'savesearches', } PLURAL_TO_SINGULAR = {p: s for s, p in SINGULAR_TO_PLURAL.iteritems()} diff --git a/api/handlers/listhandler.py b/api/handlers/listhandler.py index fd1ab0405..5e7da6dae 100644 --- a/api/handlers/listhandler.py +++ b/api/handlers/listhandler.py @@ -82,6 +82,7 @@ def initialize_list_configurations(): 'acquisitions': copy.deepcopy(container_default_configurations), 'collections': copy.deepcopy(container_default_configurations), 'analyses': copy.deepcopy(container_default_configurations), + 'savesearches': copy.deepcopy(container_default_configurations), } # preload the Storage instances for all configurations for cont_name, cont_config in list_container_configurations.iteritems(): diff --git a/api/handlers/savedsearchhandler.py b/api/handlers/savesearchhandler.py similarity index 83% rename from api/handlers/savedsearchhandler.py rename to api/handlers/savesearchhandler.py index 64ed62ea6..a64344729 100644 --- a/api/handlers/savedsearchhandler.py +++ b/api/handlers/savesearchhandler.py @@ -11,16 +11,17 @@ log = config.log storage = SearchStorage() -class SavedSearchHandler(base.RequestHandler): +class SaveSearchHandler(base.RequestHandler): def __init__(self, request=None, response=None): - super(SavedSearchHandler, self).__init__(request, response) + super(SaveSearchHandler, self).__init__(request, response) @require_login def post(self): payload = self.request.json_body validators.validate_data(payload, 'search-input.json', 'input', 'POST') payload['permissions'] = [{"_id": self.uid, "access": "admin"}] + payload['creator'] = self.uid result = storage.create_el(payload) if result.acknowledged: if result.inserted_id: @@ -49,10 +50,7 @@ def delete(self, sid): def replace_search(self, sid): payload = self.request.json_body - if payload.get('_id'): - del(payload['_id']) - if payload.get('permissions'): - del(payload['permissions']) + payload = self._scrub_replace(payload) validators.validate_data(payload, 'search-input.json', 'input', 'POST') payload['_id'] = bson.ObjectId(sid) search = storage.get_container(sid) @@ -64,3 +62,15 @@ def replace_search(self, sid): if result.inserted_id: return {'_id': result.inserted_id} return {"hi" : "bye"} + + def _scrub_replace(self, payload): + ''' + Function to turn a search returned from a GET to a legal post/replace + ''' + if payload.get('_id'): + del(payload['_id']) + if payload.get('permissions'): + del(payload['permissions']) + if payload.get('creator'): + del(payload['creator']) + return payload diff --git a/raml/api.raml b/raml/api.raml index 8ade5bf47..94a5e2361 100644 --- a/raml/api.raml +++ b/raml/api.raml @@ -55,4 +55,4 @@ resourceTypes: /acquisitions: !include resources/acquisitions.raml /projects: !include resources/projects.raml /report: !include resources/report.raml -/savesearch: !include resources/savesearch.raml +/savesearches: !include resources/savesearch.raml diff --git a/raml/schemas/definitions/search.json b/raml/schemas/definitions/search.json index aa34ae937..6f98cba15 100644 --- a/raml/schemas/definitions/search.json +++ b/raml/schemas/definitions/search.json @@ -32,7 +32,7 @@ "properties": { "_id": {"$ref":"../definitions/objectid.json#"}, "label": {"$ref": "../definitions/container.json#/definitions/label"}, - "uid": {"$ref":"../definitions/container.json#/definitions/uid"}, + "creator": {"$ref":"../definitions/container.json#/definitions/uid"}, "created": {"$ref":"../definitions/created-modified.json#/definitions/created"}, "modified": {"$ref":"../definitions/created-modified.json#/definitions/modified"}, "permissions": { diff --git a/test/integration_tests/python/test_savedsearch.py b/test/integration_tests/python/test_savedsearch.py index 31843eb97..97b4320f3 100644 --- a/test/integration_tests/python/test_savedsearch.py +++ b/test/integration_tests/python/test_savedsearch.py @@ -2,48 +2,69 @@ def test_search_saving(as_admin, data_builder): # Try posting a malformed search - r = as_admin.post('/savesearch', json={"not-label":"random-string"}) + r = as_admin.post('/savesearches', json={"not-label":"random-string"}) assert r.status_code == 400 # Try getting a non-existent saved search - r = as_admin.get('/savesearch/000000000000000000000000') + r = as_admin.get('/savesearches/000000000000000000000000') assert r.status_code == 404 # Save a search - r = as_admin.post('/savesearch', json={'label': 'search1', 'search': {'return_type': 'session'}}) + r = as_admin.post('/savesearches', json={'label': 'search1', 'search': {'return_type': 'session'}}) assert r.ok search = r.json()['_id'] # Get all searched user has access to - r = as_admin.get('/savesearch') + r = as_admin.get('/savesearches') assert r.ok # Get the saved search by id - r = as_admin.get('/savesearch/' + search) + r = as_admin.get('/savesearches/' + search) assert r.ok assert r.json()['label'] == 'search1' # Malformed search replace payload = {'label': 'good-label', 'search' : { 'not-return-type' : 'not-container'}} - r = as_admin.post('/savesearch/' + search, json=payload) + r = as_admin.post('/savesearches/' + search, json=payload) assert r.status_code == 400 # Replace search - r = as_admin.get('/savesearch/' + search) + r = as_admin.get('/savesearches/' + search) assert r.ok assert r.json()['label'] == 'search1' payload = r.json() payload['label'] = 'newSearch' - r = as_admin.post('/savesearch/' + search, json=payload) + r = as_admin.post('/savesearches/' + search, json=payload) assert r.ok assert r.json()['_id'] == search - r = as_admin.get('/savesearch/' + search) + r = as_admin.get('/savesearches/' + search) assert r.ok assert r.json()['label'] == 'newSearch' + # Add permission to search + r = as_admin.post('/savesearches/' + search + '/permissions', json={'access': 'admin', '_id': 'user@user.com'}) + assert r.ok + r = as_admin.get('/savesearches/' + search) + assert r.ok + assert r.json()['permissions'][1]['_id'] == 'user@user.com' + + # Modify permission + r = as_admin.put('/savesearches/' + search + '/permissions/user@user.com', json={'access': 'ro'}) + assert r.ok + r = as_admin.get('/savesearches/' + search) + assert r.ok + assert r.json()['permissions'][1]['access'] == 'ro' + + # Remove permission + r = as_admin.delete('/savesearches/' + search + '/permissions/user@user.com') + assert r.ok + r = as_admin.get('/savesearches/' + search) + assert r.ok + assert len(r.json()['permissions']) == 1 + # Delete saved search - r = as_admin.delete('/savesearch/' + search) + r = as_admin.delete('/savesearches/' + search) assert r.ok - r = as_admin.get('/savesearch') + r = as_admin.get('/savesearches') assert r.ok assert len(r.json()) == 0 diff --git a/tests/integration_tests/abao/abao_test_hooks.js b/tests/integration_tests/abao/abao_test_hooks.js index 99814b096..b39380ccd 100644 --- a/tests/integration_tests/abao/abao_test_hooks.js +++ b/tests/integration_tests/abao/abao_test_hooks.js @@ -1464,7 +1464,7 @@ hooks.before("GET /devices/{DeviceId} -> 404", function(test, done) { }); // Save Search Tests -hooks.before("POST /savesearch -> 200", function(test, done) { +hooks.before("POST /savesearches -> 200", function(test, done) { test.request.body = { "label": "Lable", "search": { @@ -1473,26 +1473,26 @@ hooks.before("POST /savesearch -> 200", function(test, done) { }; done(); }) -hooks.after("POST /savesearch -> 200", function(test, done) { +hooks.after("POST /savesearches -> 200", function(test, done) { search_id = test.response.body['_id']; done(); }) -hooks.before("POST /savesearch -> 400", function(test, done) { +hooks.before("POST /savesearches -> 400", function(test, done) { test.request.body = { "not-label": "Label" }; done(); }) -hooks.before("GET /savesearch/{SearchId} -> 200", function(test, done) { +hooks.before("GET /savesearches/{SearchId} -> 200", function(test, done) { test.request.params = { SearchId: search_id }; done(); }) -hooks.before("POST /savesearch/{SearchId} -> 200", function(test, done) { +hooks.before("POST /savesearches/{SearchId} -> 200", function(test, done) { test.request.params = { SearchId: search_id }; @@ -1506,7 +1506,7 @@ hooks.before("POST /savesearch/{SearchId} -> 200", function(test, done) { done(); }) -hooks.before("POST /savesearch/{SearchId} -> 400", function(test, done) { +hooks.before("POST /savesearches/{SearchId} -> 400", function(test, done) { test.request.params = { SearchId: search_id }; @@ -1516,7 +1516,7 @@ hooks.before("POST /savesearch/{SearchId} -> 400", function(test, done) { done(); }) -hooks.before("DELETE /savesearch/{SearchId} -> 200", function(test, done) { +hooks.before("DELETE /savesearches/{SearchId} -> 200", function(test, done) { test.request.params = { SearchId: search_id }; From 1f6b8c4b95f49a71404ede48369236c80e7324f9 Mon Sep 17 00:00:00 2001 From: Harsha Kethineni Date: Wed, 9 Aug 2017 16:43:56 -0500 Subject: [PATCH 4/5] permission RAML test and stringifies filters in mongo --- api/dao/containerstorage.py | 17 +++-- api/handlers/savesearchhandler.py | 27 ++++++- raml/examples/output/search-list.json | 4 ++ raml/examples/output/search.json | 10 +++ raml/resources/savesearch.raml | 11 ++- raml/schemas/output/search-list.json | 2 +- .../integration_tests/abao/abao_test_hooks.js | 71 ++++++++++++++++++- tests/integration_tests/abao/load_fixture.py | 13 ++++ 8 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 raml/examples/output/search-list.json create mode 100644 raml/examples/output/search.json diff --git a/api/dao/containerstorage.py b/api/dao/containerstorage.py index 52523d1bd..17ed90464 100644 --- a/api/dao/containerstorage.py +++ b/api/dao/containerstorage.py @@ -1,5 +1,5 @@ import datetime - +import pymongo import bson import copy @@ -10,7 +10,7 @@ from ..jobs.jobs import Job from ..jobs.queue import Queue from ..jobs.rules import copy_site_rules_for_project -from ..web.errors import APIStorageException, APINotFoundException +from ..web.errors import APIStorageException, APINotFoundException, APIConflictException from .basecontainerstorage import ContainerStorage log = config.log @@ -440,10 +440,15 @@ class SearchStorage(ContainerStorage): def __init__(self): super(SearchStorage, self).__init__('savesearches', use_object_id=True) + def create_el(self, payload): + log.debug(payload) + payload = self._to_mongo(payload) + try: + result = self.dbc.insert_one(payload, bypass_document_validation=True) + except pymongo.errors.DuplicateKeyError: + raise APIConflictException('Object with id {} already exists.'.format(payload['_id'])) + return result + def replace_el(self, search): self.delete_el(search['_id']) return self.create_el(search) - - - - diff --git a/api/handlers/savesearchhandler.py b/api/handlers/savesearchhandler.py index a64344729..b12d761ab 100644 --- a/api/handlers/savesearchhandler.py +++ b/api/handlers/savesearchhandler.py @@ -1,4 +1,5 @@ import bson +from ast import literal_eval from ..web import base from .. import config, validators from ..auth import require_login @@ -11,6 +12,23 @@ log = config.log storage = SearchStorage() + +def string_filters(payload): + if payload.get('search') and payload['search'].get('filters'): + filters = [] + for filter_ in payload['search'].get('filters',[]): + filters.append(str(filter_)) + payload['search']['filters'] = filters + return payload + +def unstring_filters(payload): + if payload['search'].get('filters'): + filters= [] + for filter_ in payload['search'].get('filters',[]): + filters.append(literal_eval(filter_)) + payload['search']['filters']= filters + return payload + class SaveSearchHandler(base.RequestHandler): def __init__(self, request=None, response=None): @@ -20,13 +38,15 @@ def __init__(self, request=None, response=None): def post(self): payload = self.request.json_body validators.validate_data(payload, 'search-input.json', 'input', 'POST') + payload = string_filters(payload) payload['permissions'] = [{"_id": self.uid, "access": "admin"}] payload['creator'] = self.uid - result = storage.create_el(payload) + result = storage.create_el(payload) if result.acknowledged: if result.inserted_id: return {'_id': result.inserted_id} - return {"hi" : "bye"} + else: + self.abort(404, 'Search not created') def get_all(self): log.debug(self.uid) @@ -36,6 +56,7 @@ def get(self, sid): result = storage.get_el(sid) if result is None: self.abort(404, 'Element {} not found'.format(sid)) + unstring_filters(result) return result def delete(self, sid): @@ -52,6 +73,7 @@ def replace_search(self, sid): payload = self.request.json_body payload = self._scrub_replace(payload) validators.validate_data(payload, 'search-input.json', 'input', 'POST') + payload = string_filters(payload) payload['_id'] = bson.ObjectId(sid) search = storage.get_container(sid) payload['permissions'] = search['permissions'] @@ -74,3 +96,4 @@ def _scrub_replace(self, payload): if payload.get('creator'): del(payload['creator']) return payload + diff --git a/raml/examples/output/search-list.json b/raml/examples/output/search-list.json new file mode 100644 index 000000000..6d4ccf812 --- /dev/null +++ b/raml/examples/output/search-list.json @@ -0,0 +1,4 @@ +[{ + "label": "Test Search", + "_id": "57e452791cff88b85f9f9c23" +}] \ No newline at end of file diff --git a/raml/examples/output/search.json b/raml/examples/output/search.json new file mode 100644 index 000000000..189e06ce7 --- /dev/null +++ b/raml/examples/output/search.json @@ -0,0 +1,10 @@ +{ + "label": "Test Search", + "_id": "57e452791cff88b85f9f9c23", + "search": { + "return_type": "file", + "filters": [{"terms": {"file.type":["nifti"]}}] + }, + "permissions": [{"access": "admin", "_id": "harshakethineni@invenshure.com"}], + "creator": "harshakethineni@invenshure.com" +} \ No newline at end of file diff --git a/raml/resources/savesearch.raml b/raml/resources/savesearch.raml index 2e6751783..49dde59b3 100644 --- a/raml/resources/savesearch.raml +++ b/raml/resources/savesearch.raml @@ -5,6 +5,7 @@ get: 200: body: application/json: + example: !include ../examples/output/search-list.json schema: !include ../schemas/output/search-list.json post: body: @@ -18,6 +19,15 @@ post: body: application/json: schema: !include ../schemas/output/search-output.json + post: + description: Replace saved search with a new search + body: + application/json: + schema: !include ../schemas/input/search-input.json + /permissions: + type: permissions-list + /{UserId}: + type: permissions-item delete: description: Delete a saved search responses: @@ -26,4 +36,3 @@ post: application/json: schema: !include ../schemas/output/container-delete.json example: !include ../examples/output/container-delete.json - diff --git a/raml/schemas/output/search-list.json b/raml/schemas/output/search-list.json index 058612f9c..529c9744c 100644 --- a/raml/schemas/output/search-list.json +++ b/raml/schemas/output/search-list.json @@ -5,7 +5,7 @@ "type":"object", "allOf":[{"$ref":"../definitions/search.json#/definitions/search-output"}], "required":[ - "_id", "label", "permissions", "search" + "_id", "label" ] } } \ No newline at end of file diff --git a/tests/integration_tests/abao/abao_test_hooks.js b/tests/integration_tests/abao/abao_test_hooks.js index b39380ccd..94f0aea60 100644 --- a/tests/integration_tests/abao/abao_test_hooks.js +++ b/tests/integration_tests/abao/abao_test_hooks.js @@ -24,6 +24,8 @@ var delete_project_id = ''; var device_id = 'bootstrapper_Bootstrapper'; var injected_api_key = 'XZpXI40Uk85eozjQkU1zHJ6yZHpix+j0mo1TMeGZ4dPzIqVPVGPmyfeK'; var search_id = ''; +var delete_search_id = ''; +var user_id = 'user@user.com' // Tests we're skipping, fix these @@ -1474,7 +1476,7 @@ hooks.before("POST /savesearches -> 200", function(test, done) { done(); }) hooks.after("POST /savesearches -> 200", function(test, done) { - search_id = test.response.body['_id']; + delete_search_id = test.response.body['_id']; done(); }) @@ -1485,6 +1487,11 @@ hooks.before("POST /savesearches -> 400", function(test, done) { done(); }) +hooks.after("GET /savesearches -> 200", function(test, done) { + search_id = test.response.body[0]._id; + done(); +}) + hooks.before("GET /savesearches/{SearchId} -> 200", function(test, done) { test.request.params = { SearchId: search_id @@ -1516,9 +1523,69 @@ hooks.before("POST /savesearches/{SearchId} -> 400", function(test, done) { done(); }) -hooks.before("DELETE /savesearches/{SearchId} -> 200", function(test, done) { +hooks.before("POST /savesearches/{SearchId}/permissions -> 200", function(test, done) { + test.request.params = { + SearchId: search_id + }; + test.request.body = { + "access" : "admin", + "_id": user_id + }; + done(); +}) + +hooks.before("POST /savesearches/{SearchId}/permissions -> 400", function(test, done) { test.request.params = { SearchId: search_id }; + test.request.body = { + "not-access" : "admin", + "not_id": user_id + }; + done(); +}) + +hooks.before("GET /savesearches/{SearchId}/permissions/{UserId} -> 200", function(test, done) { + test.request.params = { + SearchId: search_id, + UserId: user_id + }; + done(); +}) + +hooks.before("PUT /savesearches/{SearchId}/permissions/{UserId} -> 200", function(test, done) { + test.request.params = { + SearchId: search_id, + UserId: user_id + }; + test.request.body = { + "access" : "ro" + }; + done(); +}) + +hooks.before("PUT /savesearches/{SearchId}/permissions/{UserId} -> 400", function(test, done) { + test.request.params = { + SearchId: search_id, + UserId: user_id + }; + test.request.body = { + "access" : "not_an_access_level" + }; + done(); +}) + +hooks.before("DELETE /savesearches/{SearchId}/permissions/{UserId} -> 200", function(test, done) { + test.request.params = { + SearchId: search_id, + UserId: user_id + }; + done(); +}) + +hooks.before("DELETE /savesearches/{SearchId} -> 200", function(test, done) { + test.request.params = { + SearchId: delete_search_id + }; done(); }) diff --git a/tests/integration_tests/abao/load_fixture.py b/tests/integration_tests/abao/load_fixture.py index 32c036bdc..3db3cbc42 100644 --- a/tests/integration_tests/abao/load_fixture.py +++ b/tests/integration_tests/abao/load_fixture.py @@ -81,6 +81,19 @@ def main(): }) assert r.ok + # create a saved search + r = as_root.post('/savesearches', json={ + "label": "Test Search", + "search": { + "return_type": "file", + "filters": [{"terms": {"file.type":["nifti"]}}] + }, + }) + assert r.ok + r = as_root.get('/savesearches') + assert r.ok + assert r.json()[0]['label'] == 'Test Search' + # list projects # depends on 'upload file to test-project-1/test-session-1/test-acquisition-1' r = as_root.get('/projects') From 2253541ea2b7abcd97506e78a8d86ee91c9e4677 Mon Sep 17 00:00:00 2001 From: Harsha Kethineni Date: Thu, 21 Dec 2017 15:19:28 -0600 Subject: [PATCH 5/5] added example json for search input/output --- api/dao/containerstorage.py | 4 +--- raml/examples/input/search-input.json | 7 +++++++ raml/examples/output/{search.json => search-output.json} | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 raml/examples/input/search-input.json rename raml/examples/output/{search.json => search-output.json} (99%) diff --git a/api/dao/containerstorage.py b/api/dao/containerstorage.py index 17ed90464..24ac359a1 100644 --- a/api/dao/containerstorage.py +++ b/api/dao/containerstorage.py @@ -441,10 +441,8 @@ def __init__(self): super(SearchStorage, self).__init__('savesearches', use_object_id=True) def create_el(self, payload): - log.debug(payload) - payload = self._to_mongo(payload) try: - result = self.dbc.insert_one(payload, bypass_document_validation=True) + result = self.dbc.insert_one(payload) except pymongo.errors.DuplicateKeyError: raise APIConflictException('Object with id {} already exists.'.format(payload['_id'])) return result diff --git a/raml/examples/input/search-input.json b/raml/examples/input/search-input.json new file mode 100644 index 000000000..4ce20dd6c --- /dev/null +++ b/raml/examples/input/search-input.json @@ -0,0 +1,7 @@ +{ +"search": { + "return_type": "file", + "filters": [{"terms": {"file.type":["nifti"]}}] +}, +"label" : "Test Search" +} diff --git a/raml/examples/output/search.json b/raml/examples/output/search-output.json similarity index 99% rename from raml/examples/output/search.json rename to raml/examples/output/search-output.json index 189e06ce7..a005e15c2 100644 --- a/raml/examples/output/search.json +++ b/raml/examples/output/search-output.json @@ -7,4 +7,4 @@ }, "permissions": [{"access": "admin", "_id": "harshakethineni@invenshure.com"}], "creator": "harshakethineni@invenshure.com" -} \ No newline at end of file +}