refactor: Parse Pointer allows to access internal Parse Server classes and circumvent beforeFind query trigger (#8734)
This commit is contained in:
@@ -2423,6 +2423,35 @@ describe('beforeFind hooks', () => {
|
||||
})
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
it('should run beforeFind on pointers and array of pointers from an object', async () => {
|
||||
const obj1 = new Parse.Object('TestObject');
|
||||
const obj2 = new Parse.Object('TestObject2');
|
||||
const obj3 = new Parse.Object('TestObject');
|
||||
obj2.set('aField', 'aFieldValue');
|
||||
await obj2.save();
|
||||
obj1.set('pointerField', obj2);
|
||||
obj3.set('pointerFieldArray', [obj2]);
|
||||
await obj1.save();
|
||||
await obj3.save();
|
||||
const spy = jasmine.createSpy('beforeFindSpy');
|
||||
Parse.Cloud.beforeFind('TestObject2', spy);
|
||||
const query = new Parse.Query('TestObject');
|
||||
await query.get(obj1.id);
|
||||
// Pointer not included in query so we don't expect beforeFind to be called
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
const query2 = new Parse.Query('TestObject');
|
||||
query2.include('pointerField');
|
||||
const res = await query2.get(obj1.id);
|
||||
expect(res.get('pointerField').get('aField')).toBe('aFieldValue');
|
||||
// Pointer included in query so we expect beforeFind to be called
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const query3 = new Parse.Query('TestObject');
|
||||
query3.include('pointerFieldArray');
|
||||
const res2 = await query3.get(obj3.id);
|
||||
expect(res2.get('pointerFieldArray')[0].get('aField')).toBe('aFieldValue');
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterFind hooks', () => {
|
||||
|
||||
@@ -5275,7 +5275,6 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
it('should only count', async () => {
|
||||
await prepareData();
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const where = {
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('Parse Role testing', () => {
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
const restExecute = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const restExecute = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
|
||||
let user, auth, getAllRolesSpy;
|
||||
createTestUser()
|
||||
|
||||
@@ -399,15 +399,16 @@ describe('RestQuery.each', () => {
|
||||
}
|
||||
const config = Config.get('test');
|
||||
await Parse.Object.saveAll(objects);
|
||||
const query = new RestQuery(
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config,
|
||||
auth.master(config),
|
||||
'Object',
|
||||
{ value: { $gt: 2 } },
|
||||
{ limit: 2 }
|
||||
);
|
||||
auth: auth.master(config),
|
||||
className: 'Object',
|
||||
restWhere: { value: { $gt: 2 } },
|
||||
restOptions: { limit: 2 },
|
||||
});
|
||||
const spy = spyOn(query, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
const results = [];
|
||||
await query.each(result => {
|
||||
expect(result.value).toBeGreaterThan(2);
|
||||
@@ -438,34 +439,37 @@ describe('RestQuery.each', () => {
|
||||
* Two queries needed since objectId are sorted and we can't know which one
|
||||
* going to be the first and then skip by the $gt added by each
|
||||
*/
|
||||
const queryOne = new RestQuery(
|
||||
const queryOne = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth.master(config),
|
||||
'Letter',
|
||||
{
|
||||
auth: auth.master(config),
|
||||
className: 'Letter',
|
||||
restWhere: {
|
||||
numbers: {
|
||||
__type: 'Pointer',
|
||||
className: 'Number',
|
||||
objectId: object1.id,
|
||||
},
|
||||
},
|
||||
{ limit: 1 }
|
||||
);
|
||||
const queryTwo = new RestQuery(
|
||||
restOptions: { limit: 1 },
|
||||
});
|
||||
|
||||
const queryTwo = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth.master(config),
|
||||
'Letter',
|
||||
{
|
||||
auth: auth.master(config),
|
||||
className: 'Letter',
|
||||
restWhere: {
|
||||
numbers: {
|
||||
__type: 'Pointer',
|
||||
className: 'Number',
|
||||
objectId: object2.id,
|
||||
},
|
||||
},
|
||||
{ limit: 1 }
|
||||
);
|
||||
restOptions: { limit: 1 },
|
||||
});
|
||||
|
||||
const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
const resultsOne = [];
|
||||
const resultsTwo = [];
|
||||
await queryOne.each(result => {
|
||||
|
||||
@@ -660,6 +660,38 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get object in volatileClasses if not masterKey through pointer', async () => {
|
||||
const masterKeyOnlyClassObject = new Parse.Object('_PushStatus');
|
||||
await masterKeyOnlyClassObject.save(null, { useMasterKey: true });
|
||||
const obj2 = new Parse.Object('TestObject');
|
||||
// Anyone is can basically create a pointer to any object
|
||||
// or some developers can use master key in some hook to link
|
||||
// private objects to standard objects
|
||||
obj2.set('pointer', masterKeyOnlyClassObject);
|
||||
await obj2.save();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.include('pointer');
|
||||
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
|
||||
"Clients aren't allowed to perform the get operation on the _PushStatus collection."
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot get object in _GlobalConfig if not masterKey through pointer', async () => {
|
||||
await Parse.Config.save({ privateData: 'secret' }, { privateData: true });
|
||||
const obj2 = new Parse.Object('TestObject');
|
||||
obj2.set('globalConfigPointer', {
|
||||
__type: 'Pointer',
|
||||
className: '_GlobalConfig',
|
||||
objectId: 1,
|
||||
});
|
||||
await obj2.save();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.include('globalConfigPointer');
|
||||
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
|
||||
"Clients aren't allowed to perform the get operation on the _GlobalConfig collection."
|
||||
);
|
||||
});
|
||||
|
||||
it('locks down session', done => {
|
||||
let currentUser;
|
||||
Parse.User.signUp('foo', 'bar')
|
||||
|
||||
61
src/Auth.js
61
src/Auth.js
@@ -77,13 +77,16 @@ const renewSessionIfNeeded = async ({ config, session, sessionToken }) => {
|
||||
throttle[sessionToken] = setTimeout(async () => {
|
||||
try {
|
||||
if (!session) {
|
||||
const { results } = await new RestQuery(
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
master(config),
|
||||
'_Session',
|
||||
{ sessionToken },
|
||||
{ limit: 1 }
|
||||
).execute();
|
||||
auth: master(config),
|
||||
runBeforeFind: false,
|
||||
className: '_Session',
|
||||
restWhere: { sessionToken },
|
||||
restOptions: { limit: 1 },
|
||||
});
|
||||
const { results } = await query.execute();
|
||||
session = results[0];
|
||||
}
|
||||
const lastUpdated = new Date(session?.updatedAt);
|
||||
@@ -140,7 +143,15 @@ const getAuthForSessionToken = async function ({
|
||||
include: 'user',
|
||||
};
|
||||
const RestQuery = require('./RestQuery');
|
||||
const query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions);
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
runBeforeFind: false,
|
||||
auth: master(config),
|
||||
className: '_Session',
|
||||
restWhere: { sessionToken },
|
||||
restOptions,
|
||||
});
|
||||
results = (await query.execute()).results;
|
||||
} else {
|
||||
results = (
|
||||
@@ -179,12 +190,20 @@ const getAuthForSessionToken = async function ({
|
||||
});
|
||||
};
|
||||
|
||||
var getAuthForLegacySessionToken = function ({ config, sessionToken, installationId }) {
|
||||
var getAuthForLegacySessionToken = async function ({ config, sessionToken, installationId }) {
|
||||
var restOptions = {
|
||||
limit: 1,
|
||||
};
|
||||
const RestQuery = require('./RestQuery');
|
||||
var query = new RestQuery(config, master(config), '_User', { sessionToken }, restOptions);
|
||||
var query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
runBeforeFind: false,
|
||||
auth: master(config),
|
||||
className: '_User',
|
||||
restWhere: { _session_token: sessionToken },
|
||||
restOptions,
|
||||
});
|
||||
return query.execute().then(response => {
|
||||
var results = response.results;
|
||||
if (results.length !== 1) {
|
||||
@@ -229,9 +248,15 @@ Auth.prototype.getRolesForUser = async function () {
|
||||
},
|
||||
};
|
||||
const RestQuery = require('./RestQuery');
|
||||
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
|
||||
results.push(result)
|
||||
);
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
runBeforeFind: false,
|
||||
config: this.config,
|
||||
auth: master(this.config),
|
||||
className: '_Role',
|
||||
restWhere,
|
||||
});
|
||||
await query.each(result => results.push(result));
|
||||
} else {
|
||||
await new Parse.Query(Parse.Role)
|
||||
.equalTo('users', this.user)
|
||||
@@ -323,9 +348,15 @@ Auth.prototype.getRolesByIds = async function (ins) {
|
||||
});
|
||||
const restWhere = { roles: { $in: roles } };
|
||||
const RestQuery = require('./RestQuery');
|
||||
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
|
||||
results.push(result)
|
||||
);
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
runBeforeFind: false,
|
||||
auth: master(this.config),
|
||||
className: '_Role',
|
||||
restWhere,
|
||||
});
|
||||
await query.each(result => results.push(result));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
@@ -58,9 +58,16 @@ export class PushController {
|
||||
|
||||
// Force filtering on only valid device tokens
|
||||
const updateWhere = applyDeviceTokenExists(where);
|
||||
badgeUpdate = () => {
|
||||
badgeUpdate = async () => {
|
||||
// Build a real RestQuery so we can use it in RestWrite
|
||||
const restQuery = new RestQuery(config, master(config), '_Installation', updateWhere);
|
||||
const restQuery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config,
|
||||
runBeforeFind: false,
|
||||
auth: master(config),
|
||||
className: '_Installation',
|
||||
restWhere: updateWhere,
|
||||
});
|
||||
return restQuery.buildRestWhere().then(() => {
|
||||
const write = new RestWrite(
|
||||
config,
|
||||
|
||||
@@ -61,7 +61,7 @@ export class UserController extends AdaptableController {
|
||||
return true;
|
||||
}
|
||||
|
||||
verifyEmail(username, token) {
|
||||
async verifyEmail(username, token) {
|
||||
if (!this.shouldVerifyEmails) {
|
||||
// Trying to verify email when not enabled
|
||||
// TODO: Better error here.
|
||||
@@ -83,8 +83,14 @@ export class UserController extends AdaptableController {
|
||||
updateFields._email_verify_token_expires_at = { __op: 'Delete' };
|
||||
}
|
||||
const maintenanceAuth = Auth.maintenance(this.config);
|
||||
var findUserForEmailVerification = new RestQuery(this.config, maintenanceAuth, '_User', {
|
||||
username,
|
||||
var findUserForEmailVerification = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config: this.config,
|
||||
auth: maintenanceAuth,
|
||||
className: '_User',
|
||||
restWhere: {
|
||||
username,
|
||||
},
|
||||
});
|
||||
return findUserForEmailVerification.execute().then(result => {
|
||||
if (result.results.length && result.results[0].emailVerified) {
|
||||
@@ -123,7 +129,7 @@ export class UserController extends AdaptableController {
|
||||
});
|
||||
}
|
||||
|
||||
getUserIfNeeded(user) {
|
||||
async getUserIfNeeded(user) {
|
||||
if (user.username && user.email) {
|
||||
return Promise.resolve(user);
|
||||
}
|
||||
@@ -135,7 +141,14 @@ export class UserController extends AdaptableController {
|
||||
where.email = user.email;
|
||||
}
|
||||
|
||||
var query = new RestQuery(this.config, Auth.master(this.config), '_User', where);
|
||||
var query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config: this.config,
|
||||
runBeforeFind: false,
|
||||
auth: Auth.master(this.config),
|
||||
className: '_User',
|
||||
restWhere: where,
|
||||
});
|
||||
return query.execute().then(function (result) {
|
||||
if (result.results.length != 1) {
|
||||
throw undefined;
|
||||
|
||||
197
src/RestQuery.js
197
src/RestQuery.js
@@ -6,6 +6,8 @@ var Parse = require('parse/node').Parse;
|
||||
const triggers = require('./triggers');
|
||||
const { continueWhile } = require('parse/lib/node/promiseUtils');
|
||||
const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
|
||||
const { enforceRoleSecurity } = require('./SharedRest');
|
||||
|
||||
// restOptions can include:
|
||||
// skip
|
||||
// limit
|
||||
@@ -18,7 +20,80 @@ const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
|
||||
// readPreference
|
||||
// includeReadPreference
|
||||
// subqueryReadPreference
|
||||
function RestQuery(
|
||||
/**
|
||||
* Use to perform a query on a class. It will run security checks and triggers.
|
||||
* @param options
|
||||
* @param options.method {RestQuery.Method} The type of query to perform
|
||||
* @param options.config {ParseServerConfiguration} The server configuration
|
||||
* @param options.auth {Auth} The auth object for the request
|
||||
* @param options.className {string} The name of the class to query
|
||||
* @param options.restWhere {object} The where object for the query
|
||||
* @param options.restOptions {object} The options object for the query
|
||||
* @param options.clientSDK {string} The client SDK that is performing the query
|
||||
* @param options.runAfterFind {boolean} Whether to run the afterFind trigger
|
||||
* @param options.runBeforeFind {boolean} Whether to run the beforeFind trigger
|
||||
* @param options.context {object} The context object for the query
|
||||
* @returns {Promise<_UnsafeRestQuery>} A promise that is resolved with the _UnsafeRestQuery object
|
||||
*/
|
||||
async function RestQuery({
|
||||
method,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere = {},
|
||||
restOptions = {},
|
||||
clientSDK,
|
||||
runAfterFind = true,
|
||||
runBeforeFind = true,
|
||||
context,
|
||||
}) {
|
||||
if (![RestQuery.Method.find, RestQuery.Method.get].includes(method)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad query type');
|
||||
}
|
||||
enforceRoleSecurity(method, className, auth);
|
||||
const result = runBeforeFind
|
||||
? await triggers.maybeRunQueryTrigger(
|
||||
triggers.Types.beforeFind,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
config,
|
||||
auth,
|
||||
context,
|
||||
method === RestQuery.Method.get
|
||||
)
|
||||
: Promise.resolve({ restWhere, restOptions });
|
||||
|
||||
return new _UnsafeRestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
result.restWhere || restWhere,
|
||||
result.restOptions || restOptions,
|
||||
clientSDK,
|
||||
runAfterFind,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
RestQuery.Method = Object.freeze({
|
||||
get: 'get',
|
||||
find: 'find',
|
||||
});
|
||||
|
||||
/**
|
||||
* _UnsafeRestQuery is meant for specific internal usage only. When you need to skip security checks or some triggers.
|
||||
* Don't use it if you don't know what you are doing.
|
||||
* @param config
|
||||
* @param auth
|
||||
* @param className
|
||||
* @param restWhere
|
||||
* @param restOptions
|
||||
* @param clientSDK
|
||||
* @param runAfterFind
|
||||
* @param context
|
||||
*/
|
||||
function _UnsafeRestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
@@ -197,7 +272,7 @@ function RestQuery(
|
||||
// Returns a promise for the response - an object with optional keys
|
||||
// 'results' and 'count'.
|
||||
// TODO: consolidate the replaceX functions
|
||||
RestQuery.prototype.execute = function (executeOptions) {
|
||||
_UnsafeRestQuery.prototype.execute = function (executeOptions) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return this.buildRestWhere();
|
||||
@@ -231,7 +306,7 @@ RestQuery.prototype.execute = function (executeOptions) {
|
||||
});
|
||||
};
|
||||
|
||||
RestQuery.prototype.each = function (callback) {
|
||||
_UnsafeRestQuery.prototype.each = function (callback) {
|
||||
const { config, auth, className, restWhere, restOptions, clientSDK } = this;
|
||||
// if the limit is set, use it
|
||||
restOptions.limit = restOptions.limit || 100;
|
||||
@@ -243,7 +318,9 @@ RestQuery.prototype.each = function (callback) {
|
||||
return !finished;
|
||||
},
|
||||
async () => {
|
||||
const query = new RestQuery(
|
||||
// Safe here to use _UnsafeRestQuery because the security was already
|
||||
// checked during "await RestQuery()"
|
||||
const query = new _UnsafeRestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
@@ -265,7 +342,7 @@ RestQuery.prototype.each = function (callback) {
|
||||
);
|
||||
};
|
||||
|
||||
RestQuery.prototype.buildRestWhere = function () {
|
||||
_UnsafeRestQuery.prototype.buildRestWhere = function () {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return this.getUserAndRoleACL();
|
||||
@@ -294,7 +371,7 @@ RestQuery.prototype.buildRestWhere = function () {
|
||||
};
|
||||
|
||||
// Uses the Auth object to get the list of roles, adds the user id
|
||||
RestQuery.prototype.getUserAndRoleACL = function () {
|
||||
_UnsafeRestQuery.prototype.getUserAndRoleACL = function () {
|
||||
if (this.auth.isMaster) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -313,7 +390,7 @@ RestQuery.prototype.getUserAndRoleACL = function () {
|
||||
|
||||
// Changes the className if redirectClassNameForKey is set.
|
||||
// Returns a promise.
|
||||
RestQuery.prototype.redirectClassNameForKey = function () {
|
||||
_UnsafeRestQuery.prototype.redirectClassNameForKey = function () {
|
||||
if (!this.redirectKey) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -328,7 +405,7 @@ RestQuery.prototype.redirectClassNameForKey = function () {
|
||||
};
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestQuery.prototype.validateClientClassCreation = function () {
|
||||
_UnsafeRestQuery.prototype.validateClientClassCreation = function () {
|
||||
if (
|
||||
this.config.allowClientClassCreation === false &&
|
||||
!this.auth.isMaster &&
|
||||
@@ -371,7 +448,7 @@ function transformInQuery(inQueryObject, className, results) {
|
||||
// $inQuery clause.
|
||||
// The $inQuery clause turns into an $in with values that are just
|
||||
// pointers to the objects returned in the subquery.
|
||||
RestQuery.prototype.replaceInQuery = function () {
|
||||
_UnsafeRestQuery.prototype.replaceInQuery = async function () {
|
||||
var inQueryObject = findObjectWithKey(this.restWhere, '$inQuery');
|
||||
if (!inQueryObject) {
|
||||
return;
|
||||
@@ -394,13 +471,14 @@ RestQuery.prototype.replaceInQuery = function () {
|
||||
additionalOptions.readPreference = this.restOptions.readPreference;
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config,
|
||||
this.auth,
|
||||
inQueryValue.className,
|
||||
inQueryValue.where,
|
||||
additionalOptions
|
||||
);
|
||||
const subquery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: this.auth,
|
||||
className: inQueryValue.className,
|
||||
restWhere: inQueryValue.where,
|
||||
restOptions: additionalOptions,
|
||||
});
|
||||
return subquery.execute().then(response => {
|
||||
transformInQuery(inQueryObject, subquery.className, response.results);
|
||||
// Recurse to repeat
|
||||
@@ -429,7 +507,7 @@ function transformNotInQuery(notInQueryObject, className, results) {
|
||||
// $notInQuery clause.
|
||||
// The $notInQuery clause turns into a $nin with values that are just
|
||||
// pointers to the objects returned in the subquery.
|
||||
RestQuery.prototype.replaceNotInQuery = function () {
|
||||
_UnsafeRestQuery.prototype.replaceNotInQuery = async function () {
|
||||
var notInQueryObject = findObjectWithKey(this.restWhere, '$notInQuery');
|
||||
if (!notInQueryObject) {
|
||||
return;
|
||||
@@ -452,13 +530,15 @@ RestQuery.prototype.replaceNotInQuery = function () {
|
||||
additionalOptions.readPreference = this.restOptions.readPreference;
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config,
|
||||
this.auth,
|
||||
notInQueryValue.className,
|
||||
notInQueryValue.where,
|
||||
additionalOptions
|
||||
);
|
||||
const subquery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: this.auth,
|
||||
className: notInQueryValue.className,
|
||||
restWhere: notInQueryValue.where,
|
||||
restOptions: additionalOptions,
|
||||
});
|
||||
|
||||
return subquery.execute().then(response => {
|
||||
transformNotInQuery(notInQueryObject, subquery.className, response.results);
|
||||
// Recurse to repeat
|
||||
@@ -492,7 +572,7 @@ const transformSelect = (selectObject, key, objects) => {
|
||||
// The $select clause turns into an $in with values selected out of
|
||||
// the subquery.
|
||||
// Returns a possible-promise.
|
||||
RestQuery.prototype.replaceSelect = function () {
|
||||
_UnsafeRestQuery.prototype.replaceSelect = async function () {
|
||||
var selectObject = findObjectWithKey(this.restWhere, '$select');
|
||||
if (!selectObject) {
|
||||
return;
|
||||
@@ -522,13 +602,15 @@ RestQuery.prototype.replaceSelect = function () {
|
||||
additionalOptions.readPreference = this.restOptions.readPreference;
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config,
|
||||
this.auth,
|
||||
selectValue.query.className,
|
||||
selectValue.query.where,
|
||||
additionalOptions
|
||||
);
|
||||
const subquery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: this.auth,
|
||||
className: selectValue.query.className,
|
||||
restWhere: selectValue.query.where,
|
||||
restOptions: additionalOptions,
|
||||
});
|
||||
|
||||
return subquery.execute().then(response => {
|
||||
transformSelect(selectObject, selectValue.key, response.results);
|
||||
// Keep replacing $select clauses
|
||||
@@ -554,7 +636,7 @@ const transformDontSelect = (dontSelectObject, key, objects) => {
|
||||
// The $dontSelect clause turns into an $nin with values selected out of
|
||||
// the subquery.
|
||||
// Returns a possible-promise.
|
||||
RestQuery.prototype.replaceDontSelect = function () {
|
||||
_UnsafeRestQuery.prototype.replaceDontSelect = async function () {
|
||||
var dontSelectObject = findObjectWithKey(this.restWhere, '$dontSelect');
|
||||
if (!dontSelectObject) {
|
||||
return;
|
||||
@@ -582,13 +664,15 @@ RestQuery.prototype.replaceDontSelect = function () {
|
||||
additionalOptions.readPreference = this.restOptions.readPreference;
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config,
|
||||
this.auth,
|
||||
dontSelectValue.query.className,
|
||||
dontSelectValue.query.where,
|
||||
additionalOptions
|
||||
);
|
||||
const subquery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: this.auth,
|
||||
className: dontSelectValue.query.className,
|
||||
restWhere: dontSelectValue.query.where,
|
||||
restOptions: additionalOptions,
|
||||
});
|
||||
|
||||
return subquery.execute().then(response => {
|
||||
transformDontSelect(dontSelectObject, dontSelectValue.key, response.results);
|
||||
// Keep replacing $dontSelect clauses
|
||||
@@ -596,7 +680,7 @@ RestQuery.prototype.replaceDontSelect = function () {
|
||||
});
|
||||
};
|
||||
|
||||
RestQuery.prototype.cleanResultAuthData = function (result) {
|
||||
_UnsafeRestQuery.prototype.cleanResultAuthData = function (result) {
|
||||
delete result.password;
|
||||
if (result.authData) {
|
||||
Object.keys(result.authData).forEach(provider => {
|
||||
@@ -635,7 +719,7 @@ const replaceEqualityConstraint = constraint => {
|
||||
return constraint;
|
||||
};
|
||||
|
||||
RestQuery.prototype.replaceEquality = function () {
|
||||
_UnsafeRestQuery.prototype.replaceEquality = function () {
|
||||
if (typeof this.restWhere !== 'object') {
|
||||
return;
|
||||
}
|
||||
@@ -646,7 +730,7 @@ RestQuery.prototype.replaceEquality = function () {
|
||||
|
||||
// Returns a promise for whether it was successful.
|
||||
// Populates this.response with an object that only has 'results'.
|
||||
RestQuery.prototype.runFind = function (options = {}) {
|
||||
_UnsafeRestQuery.prototype.runFind = function (options = {}) {
|
||||
if (this.findOptions.limit === 0) {
|
||||
this.response = { results: [] };
|
||||
return Promise.resolve();
|
||||
@@ -682,7 +766,7 @@ RestQuery.prototype.runFind = function (options = {}) {
|
||||
|
||||
// Returns a promise for whether it was successful.
|
||||
// Populates this.response.count with the count
|
||||
RestQuery.prototype.runCount = function () {
|
||||
_UnsafeRestQuery.prototype.runCount = function () {
|
||||
if (!this.doCount) {
|
||||
return;
|
||||
}
|
||||
@@ -694,7 +778,7 @@ RestQuery.prototype.runCount = function () {
|
||||
});
|
||||
};
|
||||
|
||||
RestQuery.prototype.denyProtectedFields = async function () {
|
||||
_UnsafeRestQuery.prototype.denyProtectedFields = async function () {
|
||||
if (this.auth.isMaster) {
|
||||
return;
|
||||
}
|
||||
@@ -719,7 +803,7 @@ RestQuery.prototype.denyProtectedFields = async function () {
|
||||
};
|
||||
|
||||
// Augments this.response with all pointers on an object
|
||||
RestQuery.prototype.handleIncludeAll = function () {
|
||||
_UnsafeRestQuery.prototype.handleIncludeAll = function () {
|
||||
if (!this.includeAll) {
|
||||
return;
|
||||
}
|
||||
@@ -748,7 +832,7 @@ RestQuery.prototype.handleIncludeAll = function () {
|
||||
};
|
||||
|
||||
// Updates property `this.keys` to contain all keys but the ones unselected.
|
||||
RestQuery.prototype.handleExcludeKeys = function () {
|
||||
_UnsafeRestQuery.prototype.handleExcludeKeys = function () {
|
||||
if (!this.excludeKeys) {
|
||||
return;
|
||||
}
|
||||
@@ -766,7 +850,7 @@ RestQuery.prototype.handleExcludeKeys = function () {
|
||||
};
|
||||
|
||||
// Augments this.response with data at the paths provided in this.include.
|
||||
RestQuery.prototype.handleInclude = function () {
|
||||
_UnsafeRestQuery.prototype.handleInclude = function () {
|
||||
if (this.include.length == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -793,7 +877,7 @@ RestQuery.prototype.handleInclude = function () {
|
||||
};
|
||||
|
||||
//Returns a promise of a processed set of results
|
||||
RestQuery.prototype.runAfterFindTrigger = function () {
|
||||
_UnsafeRestQuery.prototype.runAfterFindTrigger = function () {
|
||||
if (!this.response) {
|
||||
return;
|
||||
}
|
||||
@@ -845,7 +929,7 @@ RestQuery.prototype.runAfterFindTrigger = function () {
|
||||
});
|
||||
};
|
||||
|
||||
RestQuery.prototype.handleAuthAdapters = async function () {
|
||||
_UnsafeRestQuery.prototype.handleAuthAdapters = async function () {
|
||||
if (this.className !== '_User' || this.findOptions.explain) {
|
||||
return;
|
||||
}
|
||||
@@ -927,7 +1011,7 @@ function includePath(config, auth, response, path, restOptions = {}) {
|
||||
includeRestOptions.readPreference = restOptions.readPreference;
|
||||
}
|
||||
|
||||
const queryPromises = Object.keys(pointersHash).map(className => {
|
||||
const queryPromises = Object.keys(pointersHash).map(async className => {
|
||||
const objectIds = Array.from(pointersHash[className]);
|
||||
let where;
|
||||
if (objectIds.length === 1) {
|
||||
@@ -935,7 +1019,14 @@ function includePath(config, auth, response, path, restOptions = {}) {
|
||||
} else {
|
||||
where = { objectId: { $in: objectIds } };
|
||||
}
|
||||
var query = new RestQuery(config, auth, className, where, includeRestOptions);
|
||||
const query = await RestQuery({
|
||||
method: objectIds.length === 1 ? RestQuery.Method.get : RestQuery.Method.find,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere: where,
|
||||
restOptions: includeRestOptions,
|
||||
});
|
||||
return query.execute({ op: 'get' }).then(results => {
|
||||
results.className = className;
|
||||
return Promise.resolve(results);
|
||||
@@ -1066,3 +1157,5 @@ function findObjectWithKey(root, key) {
|
||||
}
|
||||
|
||||
module.exports = RestQuery;
|
||||
// For tests
|
||||
module.exports._UnsafeRestQuery = _UnsafeRestQuery;
|
||||
|
||||
@@ -620,7 +620,7 @@ RestWrite.prototype.checkRestrictedFields = async function () {
|
||||
};
|
||||
|
||||
// The non-third-party parts of User transformation
|
||||
RestWrite.prototype.transformUser = function () {
|
||||
RestWrite.prototype.transformUser = async function () {
|
||||
var promise = Promise.resolve();
|
||||
if (this.className !== '_User') {
|
||||
return promise;
|
||||
@@ -630,19 +630,25 @@ RestWrite.prototype.transformUser = function () {
|
||||
if (this.query && this.objectId()) {
|
||||
// If we're updating a _User object, we need to clear out the cache for that user. Find all their
|
||||
// session tokens, and remove them from the cache.
|
||||
promise = new RestQuery(this.config, Auth.master(this.config), '_Session', {
|
||||
user: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.objectId(),
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: Auth.master(this.config),
|
||||
className: '_Session',
|
||||
runBeforeFind: false,
|
||||
restWhere: {
|
||||
user: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.objectId(),
|
||||
},
|
||||
},
|
||||
})
|
||||
.execute()
|
||||
.then(results => {
|
||||
results.results.forEach(session =>
|
||||
this.config.cacheController.user.del(session.sessionToken)
|
||||
);
|
||||
});
|
||||
});
|
||||
promise = query.execute().then(results => {
|
||||
results.results.forEach(session =>
|
||||
this.config.cacheController.user.del(session.sessionToken)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return promise
|
||||
|
||||
37
src/SharedRest.js
Normal file
37
src/SharedRest.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const classesWithMasterOnlyAccess = [
|
||||
'_JobStatus',
|
||||
'_PushStatus',
|
||||
'_Hooks',
|
||||
'_GlobalConfig',
|
||||
'_JobSchedule',
|
||||
'_Idempotency',
|
||||
];
|
||||
// Disallowing access to the _Role collection except by master key
|
||||
function enforceRoleSecurity(method, className, auth) {
|
||||
if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) {
|
||||
if (method === 'delete' || method === 'find') {
|
||||
const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`;
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||
}
|
||||
}
|
||||
|
||||
//all volatileClasses are masterKey only
|
||||
if (
|
||||
classesWithMasterOnlyAccess.indexOf(className) >= 0 &&
|
||||
!auth.isMaster &&
|
||||
!auth.isMaintenance
|
||||
) {
|
||||
const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`;
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||
}
|
||||
|
||||
// readOnly masterKey is not allowed
|
||||
if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) {
|
||||
const error = `read-only masterKey isn't allowed to perform the ${method} operation.`;
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
enforceRoleSecurity,
|
||||
};
|
||||
184
src/rest.js
184
src/rest.js
@@ -12,6 +12,7 @@ var Parse = require('parse/node').Parse;
|
||||
var RestQuery = require('./RestQuery');
|
||||
var RestWrite = require('./RestWrite');
|
||||
var triggers = require('./triggers');
|
||||
const { enforceRoleSecurity } = require('./SharedRest');
|
||||
|
||||
function checkTriggers(className, config, types) {
|
||||
return types.some(triggerType => {
|
||||
@@ -24,65 +25,34 @@ function checkLiveQuery(className, config) {
|
||||
}
|
||||
|
||||
// Returns a promise for an object with optional keys 'results' and 'count'.
|
||||
function find(config, auth, className, restWhere, restOptions, clientSDK, context) {
|
||||
enforceRoleSecurity('find', className, auth);
|
||||
return triggers
|
||||
.maybeRunQueryTrigger(
|
||||
triggers.Types.beforeFind,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
config,
|
||||
auth,
|
||||
context
|
||||
)
|
||||
.then(result => {
|
||||
restWhere = result.restWhere || restWhere;
|
||||
restOptions = result.restOptions || restOptions;
|
||||
const query = new RestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
clientSDK,
|
||||
true,
|
||||
context
|
||||
);
|
||||
return query.execute();
|
||||
});
|
||||
}
|
||||
const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => {
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
clientSDK,
|
||||
context,
|
||||
});
|
||||
return query.execute();
|
||||
};
|
||||
|
||||
// get is just like find but only queries an objectId.
|
||||
const get = (config, auth, className, objectId, restOptions, clientSDK, context) => {
|
||||
const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => {
|
||||
var restWhere = { objectId };
|
||||
enforceRoleSecurity('get', className, auth);
|
||||
return triggers
|
||||
.maybeRunQueryTrigger(
|
||||
triggers.Types.beforeFind,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
config,
|
||||
auth,
|
||||
context,
|
||||
true
|
||||
)
|
||||
.then(result => {
|
||||
restWhere = result.restWhere || restWhere;
|
||||
restOptions = result.restOptions || restOptions;
|
||||
const query = new RestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
clientSDK,
|
||||
true,
|
||||
context
|
||||
);
|
||||
return query.execute();
|
||||
});
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
clientSDK,
|
||||
context,
|
||||
});
|
||||
return query.execute();
|
||||
};
|
||||
|
||||
// Returns a promise that doesn't resolve to any useful value.
|
||||
@@ -101,35 +71,40 @@ function del(config, auth, className, objectId, context) {
|
||||
let schemaController;
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
const hasTriggers = checkTriggers(className, config, ['beforeDelete', 'afterDelete']);
|
||||
const hasLiveQuery = checkLiveQuery(className, config);
|
||||
if (hasTriggers || hasLiveQuery || className == '_Session') {
|
||||
return new RestQuery(config, auth, className, { objectId })
|
||||
.execute({ op: 'delete' })
|
||||
.then(response => {
|
||||
if (response && response.results && response.results.length) {
|
||||
const firstResult = response.results[0];
|
||||
firstResult.className = className;
|
||||
if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) {
|
||||
if (!auth.user || firstResult.user.objectId !== auth.user.id) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||
}
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere: { objectId },
|
||||
});
|
||||
return query.execute({ op: 'delete' }).then(response => {
|
||||
if (response && response.results && response.results.length) {
|
||||
const firstResult = response.results[0];
|
||||
firstResult.className = className;
|
||||
if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) {
|
||||
if (!auth.user || firstResult.user.objectId !== auth.user.id) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||
}
|
||||
var cacheAdapter = config.cacheController;
|
||||
cacheAdapter.user.del(firstResult.sessionToken);
|
||||
inflatedObject = Parse.Object.fromJSON(firstResult);
|
||||
return triggers.maybeRunTrigger(
|
||||
triggers.Types.beforeDelete,
|
||||
auth,
|
||||
inflatedObject,
|
||||
null,
|
||||
config,
|
||||
context
|
||||
);
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found for delete.');
|
||||
});
|
||||
var cacheAdapter = config.cacheController;
|
||||
cacheAdapter.user.del(firstResult.sessionToken);
|
||||
inflatedObject = Parse.Object.fromJSON(firstResult);
|
||||
return triggers.maybeRunTrigger(
|
||||
triggers.Types.beforeDelete,
|
||||
auth,
|
||||
inflatedObject,
|
||||
null,
|
||||
config,
|
||||
context
|
||||
);
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found for delete.');
|
||||
});
|
||||
}
|
||||
return Promise.resolve({});
|
||||
})
|
||||
@@ -193,21 +168,22 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte
|
||||
enforceRoleSecurity('update', className, auth);
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
const hasTriggers = checkTriggers(className, config, ['beforeSave', 'afterSave']);
|
||||
const hasLiveQuery = checkLiveQuery(className, config);
|
||||
if (hasTriggers || hasLiveQuery) {
|
||||
// Do not use find, as it runs the before finds
|
||||
return new RestQuery(
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
context
|
||||
).execute({
|
||||
runAfterFind: false,
|
||||
runBeforeFind: false,
|
||||
context,
|
||||
});
|
||||
return query.execute({
|
||||
op: 'update',
|
||||
});
|
||||
}
|
||||
@@ -248,40 +224,6 @@ function handleSessionMissingError(error, className, auth) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const classesWithMasterOnlyAccess = [
|
||||
'_JobStatus',
|
||||
'_PushStatus',
|
||||
'_Hooks',
|
||||
'_GlobalConfig',
|
||||
'_JobSchedule',
|
||||
'_Idempotency',
|
||||
];
|
||||
// Disallowing access to the _Role collection except by master key
|
||||
function enforceRoleSecurity(method, className, auth) {
|
||||
if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) {
|
||||
if (method === 'delete' || method === 'find') {
|
||||
const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`;
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||
}
|
||||
}
|
||||
|
||||
//all volatileClasses are masterKey only
|
||||
if (
|
||||
classesWithMasterOnlyAccess.indexOf(className) >= 0 &&
|
||||
!auth.isMaster &&
|
||||
!auth.isMaintenance
|
||||
) {
|
||||
const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`;
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||
}
|
||||
|
||||
// readOnly masterKey is not allowed
|
||||
if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) {
|
||||
const error = `read-only masterKey isn't allowed to perform the ${method} operation.`;
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
del,
|
||||
|
||||
Reference in New Issue
Block a user