diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 6696568110..edbfda0be6 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,111 @@ 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/purge/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/purge/TestObject', + json: true + }).then(body => { + 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/purge/_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/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 0da314f5d2..9bc2d85df3 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -75,6 +75,16 @@ DatabaseController.prototype.collectionExists = function(className) { return this.adapter.collectionExists(className); }; +DatabaseController.prototype.purgeCollection = function(className) { + return this.loadSchema() + .then((schema) => { + schema.getOneSchema(className) + }) + .then((schema) => { + this.adapter.deleteObjectsByQuery(className, {}, schema); + }); +}; + DatabaseController.prototype.validateClassName = function(className) { if (this.skipValidation) { return Promise.resolve(); 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/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/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;