From a540036f0c83fd965e35cf49a5bf07e509237622 Mon Sep 17 00:00:00 2001 From: Marco129 Date: Sat, 11 Jun 2016 21:18:40 +0800 Subject: [PATCH 1/4] Endpoint for purging all objects in class --- spec/ParseAPI.spec.js | 51 +++++++++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 4 ++ src/Controllers/DatabaseController.js | 4 ++ src/Routers/ClassesRouter.js | 5 ++ src/Routers/FeaturesRouter.js | 2 +- src/rest.js | 12 +++++ 6 files changed, 77 insertions(+), 1 deletion(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 6696568110..3ee8b84726 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -5,6 +5,7 @@ var DatabaseAdapter = require('../src/DatabaseAdapter'); const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); var request = require('request'); +const rp = require('request-promise'); const Parse = require("parse/node"); let Config = require('../src/Config'); let defaultColumns = require('../src/Controllers/SchemaController').defaultColumns; @@ -1363,4 +1364,54 @@ describe('miscellaneous', function() { done(); }); }); + + it('purge all objects in class', (done) => { + let object = new Parse.Object('TestObject'); + object.set('foo', 'bar'); + let object2 = new Parse.Object('TestObject'); + object2.set('alice', 'wonderland'); + Parse.Object.saveAll([object, object2]) + .then(() => { + let query = new Parse.Query(TestObject); + return query.count() + }).then((count) => { + expect(count).toBe(2); + let headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test' + }; + request.del({ + headers: headers, + url: 'http://localhost:8378/1/classes/TestObject', + json: true + }, (err, res, body) => { + expect(err).toBe(null); + let query = new Parse.Query(TestObject); + return query.count().then((count) => { + expect(count).toBe(0); + done(); + }); + }); + }); + }); + + it('fail on purge all objects in class without master key', (done) => { + let headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + rp({ + method: 'DELETE', + headers: headers, + uri: 'http://localhost:8378/1/classes/TestObject', + json: true + }).then(body => { + fail('Should not succeed') + }).catch(err => { + expect(err.error.error).toEqual('unauthorized: master key is required'); + done(); + }); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index c3bb30d7e8..cecc5df6bf 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -95,6 +95,10 @@ export class MongoStorageAdapter { }); } + purgeCollection(name: string) { + return this.collection(this._collectionPrefix + name).then(collection => collection.remove({})); + } + // Deletes a schema. Resolve if successful. If the schema doesn't // exist, resolve with undefined. If schema exists, but can't be deleted for some other reason, // reject with INTERNAL_SERVER_ERROR. diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 0da314f5d2..366594540e 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -75,6 +75,10 @@ DatabaseController.prototype.collectionExists = function(className) { return this.adapter.collectionExists(className); }; +DatabaseController.prototype.purgeCollection = function(className) { + return this.adapter.purgeCollection(className); +}; + DatabaseController.prototype.validateClassName = function(className) { if (this.skipValidation) { return Promise.resolve(); diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 515719a59d..2358fddfc3 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -1,5 +1,6 @@ import PromiseRouter from '../PromiseRouter'; +import * as middleware from '../middlewares'; import rest from '../rest'; import url from 'url'; @@ -107,6 +108,9 @@ export class ClassesRouter extends PromiseRouter { } handleDelete(req) { + if (!req.params.objectId) { + req.params.objectId = '*'; + } return rest.del(req.config, req.auth, req.params.className, req.params.objectId) .then(() => { return {response: {}}; @@ -131,6 +135,7 @@ export class ClassesRouter extends PromiseRouter { this.route('POST', '/classes/:className', (req) => { return this.handleCreate(req); }); this.route('PUT', '/classes/:className/:objectId', (req) => { return this.handleUpdate(req); }); this.route('DELETE', '/classes/:className/:objectId', (req) => { return this.handleDelete(req); }); + this.route('DELETE', '/classes/:className', middleware.promiseEnforceMasterKeyAccess, (req) => { return this.handleDelete(req); }); } } diff --git a/src/Routers/FeaturesRouter.js b/src/Routers/FeaturesRouter.js index e96f48ce4a..03bf12e74a 100644 --- a/src/Routers/FeaturesRouter.js +++ b/src/Routers/FeaturesRouter.js @@ -36,7 +36,7 @@ export class FeaturesRouter extends PromiseRouter { removeField: true, addClass: true, removeClass: true, - clearAllDataFromClass: false, + clearAllDataFromClass: true, exportClass: false, editClassLevelPermissions: true, editPointerPermissions: true, diff --git a/src/rest.js b/src/rest.js index 45f0d7db74..f0ed1fa73a 100644 --- a/src/rest.js +++ b/src/rest.js @@ -42,6 +42,18 @@ function del(config, auth, className, objectId) { enforceRoleSecurity('delete', className, auth); + if (objectId === '*') { + return Promise.resolve().then(() => { + return config.database.purgeCollection(className); + }).then(() => { + if (className == '_Session') { + var cacheAdapter = config.cacheController; + cacheAdapter.user.clear(); + } + return Promise.resolve({}); + }); + } + var inflatedObject; return Promise.resolve().then(() => { From 4f9ebf23eaca5a64a9c3698699b0c2a28f0338a3 Mon Sep 17 00:00:00 2001 From: Marco129 Date: Sun, 12 Jun 2016 03:08:31 +0800 Subject: [PATCH 2/4] Use deleteObjectsByQuery --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 4 ---- src/Controllers/DatabaseController.js | 8 +++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index cecc5df6bf..c3bb30d7e8 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -95,10 +95,6 @@ export class MongoStorageAdapter { }); } - purgeCollection(name: string) { - return this.collection(this._collectionPrefix + name).then(collection => collection.remove({})); - } - // Deletes a schema. Resolve if successful. If the schema doesn't // exist, resolve with undefined. If schema exists, but can't be deleted for some other reason, // reject with INTERNAL_SERVER_ERROR. diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 366594540e..9bc2d85df3 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -76,7 +76,13 @@ DatabaseController.prototype.collectionExists = function(className) { }; DatabaseController.prototype.purgeCollection = function(className) { - return this.adapter.purgeCollection(className); + return this.loadSchema() + .then((schema) => { + schema.getOneSchema(className) + }) + .then((schema) => { + this.adapter.deleteObjectsByQuery(className, {}, schema); + }); }; DatabaseController.prototype.validateClassName = function(className) { From 8a98954d393206665f83862be18c57b4ecff91c0 Mon Sep 17 00:00:00 2001 From: Marco129 Date: Sun, 12 Jun 2016 03:50:36 +0800 Subject: [PATCH 3/4] Standalone handling function and purge cache --- spec/ParseAPI.spec.js | 59 +++++++++++++++++++++++++++++++++++- src/Routers/ClassesRouter.js | 18 ++++++++--- src/rest.js | 12 -------- 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 3ee8b84726..80dd38da4a 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1408,10 +1408,67 @@ describe('miscellaneous', function() { uri: 'http://localhost:8378/1/classes/TestObject', json: true }).then(body => { - fail('Should not succeed') + fail('Should not succeed'); }).catch(err => { expect(err.error.error).toEqual('unauthorized: master key is required'); done(); }); }); + + it('purge all objects in _Role also purge cache', (done) => { + let headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test' + }; + var user, object; + createTestUser().then((x) => { + user = x; + let acl = new Parse.ACL(); + acl.setPublicReadAccess(true); + acl.setPublicWriteAccess(false); + let role = new Parse.Object('_Role'); + role.set('name', 'TestRole'); + role.setACL(acl); + let users = role.relation('users'); + users.add(user); + return role.save({}, { useMasterKey: true }); + }).then((x) => { + let query = new Parse.Query('_Role'); + return query.find({ useMasterKey: true }); + }).then((x) => { + expect(x.length).toEqual(1); + let relation = x[0].relation('users').query(); + return relation.first({ useMasterKey: true }); + }).then((x) => { + expect(x.id).toEqual(user.id); + object = new Parse.Object('TestObject'); + let acl = new Parse.ACL(); + acl.setPublicReadAccess(false); + acl.setPublicWriteAccess(false); + acl.setRoleReadAccess('TestRole', true); + acl.setRoleWriteAccess('TestRole', true); + object.setACL(acl); + return object.save(); + }).then((x) => { + let query = new Parse.Query('TestObject'); + return query.find({ sessionToken: user.getSessionToken() }); + }).then((x) => { + expect(x.length).toEqual(1); + return rp({ + method: 'DELETE', + headers: headers, + uri: 'http://localhost:8378/1/classes/_Role', + json: true + }); + }).then((x) => { + let query = new Parse.Query('TestObject'); + return query.get(object.id, { sessionToken: user.getSessionToken() }); + }).then((x) => { + fail('Should not succeed'); + }, (e) => { + expect(e.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + }); + }); }); diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 2358fddfc3..0c6594413b 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -108,15 +108,25 @@ export class ClassesRouter extends PromiseRouter { } handleDelete(req) { - if (!req.params.objectId) { - req.params.objectId = '*'; - } return rest.del(req.config, req.auth, req.params.className, req.params.objectId) .then(() => { return {response: {}}; }); } + handlePurge(req) { + return req.config.database.purgeCollection(req.params.className) + .then(() => { + var cacheAdapter = req.config.cacheController; + if (req.params.className == '_Session') { + cacheAdapter.user.clear(); + } else if (req.params.className == '_Role') { + cacheAdapter.role.clear(); + } + return {response: {}}; + }); + } + static JSONFromQuery(query) { let json = {}; for (let [key, value] of Object.entries(query)) { @@ -135,7 +145,7 @@ export class ClassesRouter extends PromiseRouter { this.route('POST', '/classes/:className', (req) => { return this.handleCreate(req); }); this.route('PUT', '/classes/:className/:objectId', (req) => { return this.handleUpdate(req); }); this.route('DELETE', '/classes/:className/:objectId', (req) => { return this.handleDelete(req); }); - this.route('DELETE', '/classes/:className', middleware.promiseEnforceMasterKeyAccess, (req) => { return this.handleDelete(req); }); + this.route('DELETE', '/classes/:className', middleware.promiseEnforceMasterKeyAccess, (req) => { return this.handlePurge(req); }); } } diff --git a/src/rest.js b/src/rest.js index f0ed1fa73a..45f0d7db74 100644 --- a/src/rest.js +++ b/src/rest.js @@ -42,18 +42,6 @@ function del(config, auth, className, objectId) { enforceRoleSecurity('delete', className, auth); - if (objectId === '*') { - return Promise.resolve().then(() => { - return config.database.purgeCollection(className); - }).then(() => { - if (className == '_Session') { - var cacheAdapter = config.cacheController; - cacheAdapter.user.clear(); - } - return Promise.resolve({}); - }); - } - var inflatedObject; return Promise.resolve().then(() => { From 4d2be8a1c929254be942042ca302070024d90212 Mon Sep 17 00:00:00 2001 From: Marco129 Date: Wed, 15 Jun 2016 01:26:02 +0800 Subject: [PATCH 4/4] Change endpoint url --- spec/ParseAPI.spec.js | 6 +++--- src/ParseServer.js | 2 ++ src/Routers/ClassesRouter.js | 15 --------------- src/Routers/PurgeRouter.js | 24 ++++++++++++++++++++++++ 4 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 src/Routers/PurgeRouter.js diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 80dd38da4a..edbfda0be6 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1383,7 +1383,7 @@ describe('miscellaneous', function() { }; request.del({ headers: headers, - url: 'http://localhost:8378/1/classes/TestObject', + url: 'http://localhost:8378/1/purge/TestObject', json: true }, (err, res, body) => { expect(err).toBe(null); @@ -1405,7 +1405,7 @@ describe('miscellaneous', function() { rp({ method: 'DELETE', headers: headers, - uri: 'http://localhost:8378/1/classes/TestObject', + uri: 'http://localhost:8378/1/purge/TestObject', json: true }).then(body => { fail('Should not succeed'); @@ -1458,7 +1458,7 @@ describe('miscellaneous', function() { return rp({ method: 'DELETE', headers: headers, - uri: 'http://localhost:8378/1/classes/_Role', + uri: 'http://localhost:8378/1/purge/_Role', json: true }); }).then((x) => { diff --git a/src/ParseServer.js b/src/ParseServer.js index b823b00615..68c2d46eb7 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -50,6 +50,7 @@ import { SchemasRouter } from './Routers/SchemasRouter'; import { SessionsRouter } from './Routers/SessionsRouter'; import { UserController } from './Controllers/UserController'; import { UsersRouter } from './Routers/UsersRouter'; +import { PurgeRouter } from './Routers/PurgeRouter'; import DatabaseController from './Controllers/DatabaseController'; const SchemaController = require('./Controllers/SchemaController'); @@ -291,6 +292,7 @@ class ParseServer { new IAPValidationRouter(), new FeaturesRouter(), new GlobalConfigRouter(), + new PurgeRouter(), ]; if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) { diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 0c6594413b..515719a59d 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -1,6 +1,5 @@ import PromiseRouter from '../PromiseRouter'; -import * as middleware from '../middlewares'; import rest from '../rest'; import url from 'url'; @@ -114,19 +113,6 @@ export class ClassesRouter extends PromiseRouter { }); } - handlePurge(req) { - return req.config.database.purgeCollection(req.params.className) - .then(() => { - var cacheAdapter = req.config.cacheController; - if (req.params.className == '_Session') { - cacheAdapter.user.clear(); - } else if (req.params.className == '_Role') { - cacheAdapter.role.clear(); - } - return {response: {}}; - }); - } - static JSONFromQuery(query) { let json = {}; for (let [key, value] of Object.entries(query)) { @@ -145,7 +131,6 @@ export class ClassesRouter extends PromiseRouter { this.route('POST', '/classes/:className', (req) => { return this.handleCreate(req); }); this.route('PUT', '/classes/:className/:objectId', (req) => { return this.handleUpdate(req); }); this.route('DELETE', '/classes/:className/:objectId', (req) => { return this.handleDelete(req); }); - this.route('DELETE', '/classes/:className', middleware.promiseEnforceMasterKeyAccess, (req) => { return this.handlePurge(req); }); } } diff --git a/src/Routers/PurgeRouter.js b/src/Routers/PurgeRouter.js new file mode 100644 index 0000000000..1f0eed816f --- /dev/null +++ b/src/Routers/PurgeRouter.js @@ -0,0 +1,24 @@ +import PromiseRouter from '../PromiseRouter'; +import * as middleware from '../middlewares'; + +export class PurgeRouter extends PromiseRouter { + + handlePurge(req) { + return req.config.database.purgeCollection(req.params.className) + .then(() => { + var cacheAdapter = req.config.cacheController; + if (req.params.className == '_Session') { + cacheAdapter.user.clear(); + } else if (req.params.className == '_Role') { + cacheAdapter.role.clear(); + } + return {response: {}}; + }); + } + + mountRoutes() { + this.route('DELETE', '/purge/:className', middleware.promiseEnforceMasterKeyAccess, (req) => { return this.handlePurge(req); }); + } +} + +export default PurgeRouter;