197 changes: 145 additions & 52 deletions src/RestQuery.js

Large diffs are not rendered by default.

32 changes: 19 additions & 13 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ RestWrite.prototype.handleAuthData = async function (authData) {
};

// 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;
Expand All @@ -618,19 +618,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
Expand Down
37 changes: 37 additions & 0 deletions src/SharedRest.js
Original file line number Diff line number Diff line change
@@ -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 changes: 63 additions & 121 deletions src/rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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.
Expand All @@ -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({});
})
Expand Down Expand Up @@ -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',
});
}
Expand Down Expand Up @@ -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,
Expand Down