feat: Add Parse Server option enableSanitizedErrorResponse to remove detailed error messages from responses sent to clients (#9944)
This commit is contained in:
@@ -767,13 +767,11 @@ describe('Parse.File testing', () => {
|
||||
|
||||
describe('getting files', () => {
|
||||
it('does not crash on file request with invalid app ID', async () => {
|
||||
loggerErrorSpy.calls.reset();
|
||||
const res1 = await request({
|
||||
url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt',
|
||||
}).catch(e => e);
|
||||
expect(res1.status).toBe(403);
|
||||
expect(res1.data).toEqual({ code: 119, error: 'Permission denied' });
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid application ID.'));
|
||||
expect(res1.data).toEqual({ code: 119, error: 'Invalid application ID.' });
|
||||
// Ensure server did not crash
|
||||
const res2 = await request({ url: 'http://localhost:8378/1/health' });
|
||||
expect(res2.status).toEqual(200);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const Utils = require('../src/Utils');
|
||||
const Utils = require('../lib/Utils');
|
||||
const { createSanitizedError, createSanitizedHttpError } = require("../lib/Error")
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('encodeForUrl', () => {
|
||||
@@ -173,4 +174,42 @@ describe('Utils', () => {
|
||||
expect(Utils.getNestedProperty(obj, 'database.name')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSanitizedError', () => {
|
||||
it('should return "Permission denied" when enableSanitizedErrorResponse is true', () => {
|
||||
const config = { enableSanitizedErrorResponse: true };
|
||||
const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config);
|
||||
expect(error.message).toBe('Permission denied');
|
||||
});
|
||||
|
||||
it('should not crash with config undefined', () => {
|
||||
const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', undefined);
|
||||
expect(error.message).toBe('Permission denied');
|
||||
});
|
||||
|
||||
it('should return the detailed message when enableSanitizedErrorResponse is false', () => {
|
||||
const config = { enableSanitizedErrorResponse: false };
|
||||
const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config);
|
||||
expect(error.message).toBe('Detailed error message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSanitizedHttpError', () => {
|
||||
it('should return "Permission denied" when enableSanitizedErrorResponse is true', () => {
|
||||
const config = { enableSanitizedErrorResponse: true };
|
||||
const error = createSanitizedHttpError(403, 'Detailed error message', config);
|
||||
expect(error.message).toBe('Permission denied');
|
||||
});
|
||||
|
||||
it('should not crash with config undefined', () => {
|
||||
const error = createSanitizedHttpError(403, 'Detailed error message', undefined);
|
||||
expect(error.message).toBe('Permission denied');
|
||||
});
|
||||
|
||||
it('should return the detailed message when enableSanitizedErrorResponse is false', () => {
|
||||
const config = { enableSanitizedErrorResponse: false };
|
||||
const error = createSanitizedHttpError(403, 'Detailed error message', config);
|
||||
expect(error.message).toBe('Detailed error message');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1399,6 +1399,7 @@ export default class SchemaController {
|
||||
return true;
|
||||
}
|
||||
const perms = classPermissions[operation];
|
||||
const config = Config.get(Parse.applicationId)
|
||||
// If only for authenticated users
|
||||
// make sure we have an aclGroup
|
||||
if (perms['requiresAuthentication']) {
|
||||
@@ -1406,12 +1407,14 @@ export default class SchemaController {
|
||||
if (!aclGroup || aclGroup.length == 0) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Permission denied, user needs to be authenticated.'
|
||||
'Permission denied, user needs to be authenticated.',
|
||||
config
|
||||
);
|
||||
} else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Permission denied, user needs to be authenticated.'
|
||||
'Permission denied, user needs to be authenticated.',
|
||||
config
|
||||
);
|
||||
}
|
||||
// requiresAuthentication passed, just move forward
|
||||
@@ -1428,7 +1431,8 @@ export default class SchemaController {
|
||||
if (permissionField == 'writeUserFields' && operation == 'create') {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
`Permission denied for action ${operation} on class ${className}.`
|
||||
`Permission denied for action ${operation} on class ${className}.`,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1451,7 +1455,8 @@ export default class SchemaController {
|
||||
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
`Permission denied for action ${operation} on class ${className}.`
|
||||
`Permission denied for action ${operation} on class ${className}.`,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import defaultLogger from './logger';
|
||||
* @param {string} detailedMessage - The detailed error message to log server-side
|
||||
* @returns {Parse.Error} A Parse.Error with sanitized message
|
||||
*/
|
||||
function createSanitizedError(errorCode, detailedMessage) {
|
||||
function createSanitizedError(errorCode, detailedMessage, config) {
|
||||
// On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file
|
||||
if (process.env.TESTING) {
|
||||
defaultLogger.error('Sanitized error:', detailedMessage);
|
||||
@@ -16,7 +16,7 @@ function createSanitizedError(errorCode, detailedMessage) {
|
||||
defaultLogger.error(detailedMessage);
|
||||
}
|
||||
|
||||
return new Parse.Error(errorCode, 'Permission denied');
|
||||
return new Parse.Error(errorCode, config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,7 +27,7 @@ function createSanitizedError(errorCode, detailedMessage) {
|
||||
* @param {string} detailedMessage - The detailed error message to log server-side
|
||||
* @returns {Error} An Error with sanitized message
|
||||
*/
|
||||
function createSanitizedHttpError(statusCode, detailedMessage) {
|
||||
function createSanitizedHttpError(statusCode, detailedMessage, config) {
|
||||
// On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file
|
||||
if (process.env.TESTING) {
|
||||
defaultLogger.error('Sanitized error:', detailedMessage);
|
||||
@@ -37,7 +37,7 @@ function createSanitizedHttpError(statusCode, detailedMessage) {
|
||||
|
||||
const error = new Error();
|
||||
error.status = statusCode;
|
||||
error.message = 'Permission denied';
|
||||
error.message = config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage;
|
||||
return error;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,13 @@ const load = parseGraphQLSchema => {
|
||||
const { name, schemaFields } = deepcopy(args);
|
||||
const { config, auth } = context;
|
||||
|
||||
enforceMasterKeyAccess(auth);
|
||||
enforceMasterKeyAccess(auth, config);
|
||||
|
||||
if (auth.isReadOnly) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
"read-only masterKey isn't allowed to create a schema.",
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,12 +81,13 @@ const load = parseGraphQLSchema => {
|
||||
const { name, schemaFields } = deepcopy(args);
|
||||
const { config, auth } = context;
|
||||
|
||||
enforceMasterKeyAccess(auth);
|
||||
enforceMasterKeyAccess(auth, config);
|
||||
|
||||
if (auth.isReadOnly) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
"read-only masterKey isn't allowed to update a schema."
|
||||
"read-only masterKey isn't allowed to update a schema.",
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,12 +133,13 @@ const load = parseGraphQLSchema => {
|
||||
const { name } = deepcopy(args);
|
||||
const { config, auth } = context;
|
||||
|
||||
enforceMasterKeyAccess(auth);
|
||||
enforceMasterKeyAccess(auth, config);
|
||||
|
||||
if (auth.isReadOnly) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
"read-only masterKey isn't allowed to delete a schema.",
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ const load = parseGraphQLSchema => {
|
||||
const { name } = deepcopy(args);
|
||||
const { config, auth } = context;
|
||||
|
||||
enforceMasterKeyAccess(auth);
|
||||
enforceMasterKeyAccess(auth, config);
|
||||
|
||||
const schema = await config.database.loadSchema({ clearCache: true });
|
||||
const parseClass = await getClass(name, schema);
|
||||
@@ -57,7 +57,7 @@ const load = parseGraphQLSchema => {
|
||||
try {
|
||||
const { config, auth } = context;
|
||||
|
||||
enforceMasterKeyAccess(auth);
|
||||
enforceMasterKeyAccess(auth, config);
|
||||
|
||||
const schema = await config.database.loadSchema({ clearCache: true });
|
||||
return (await schema.getAllClasses(true)).map(parseClass => ({
|
||||
|
||||
@@ -9,7 +9,7 @@ import { createSanitizedError } from '../../Error';
|
||||
const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) => {
|
||||
const { info, config } = context;
|
||||
if (!info || !info.sessionToken) {
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
|
||||
}
|
||||
const sessionToken = info.sessionToken;
|
||||
const selectedFields = getFieldNames(queryInfo)
|
||||
@@ -63,7 +63,7 @@ const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) =
|
||||
info.context
|
||||
);
|
||||
if (!response.results || response.results.length == 0) {
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
|
||||
} else {
|
||||
const user = response.results[0];
|
||||
return {
|
||||
|
||||
@@ -2,11 +2,12 @@ import Parse from 'parse/node';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { createSanitizedError } from '../Error';
|
||||
|
||||
export function enforceMasterKeyAccess(auth) {
|
||||
export function enforceMasterKeyAccess(auth, config) {
|
||||
if (!auth.isMaster) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
'unauthorized: master key is required',
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,13 @@ module.exports.ParseServerOptions = {
|
||||
action: parsers.booleanParser,
|
||||
default: true,
|
||||
},
|
||||
enableSanitizedErrorResponse: {
|
||||
env: 'PARSE_SERVER_ENABLE_SANITIZED_ERROR_RESPONSE',
|
||||
help:
|
||||
'If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`.',
|
||||
action: parsers.booleanParser,
|
||||
default: true,
|
||||
},
|
||||
encodeParseObjectInCloudFunction: {
|
||||
env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION',
|
||||
help:
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
* @property {Boolean} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.
|
||||
* @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
|
||||
* @property {Boolean} enableInsecureAuthAdapters Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.
|
||||
* @property {Boolean} enableSanitizedErrorResponse If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`.
|
||||
* @property {Boolean} encodeParseObjectInCloudFunction If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`. <br><br>ℹ️ The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.
|
||||
* @property {String} encryptionKey Key for encrypting your files
|
||||
* @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.
|
||||
|
||||
@@ -347,6 +347,9 @@ export interface ParseServerOptions {
|
||||
rateLimit: ?(RateLimitOptions[]);
|
||||
/* Options to customize the request context using inversion of control/dependency injection.*/
|
||||
requestContextMiddleware: ?(req: any, res: any, next: any) => void;
|
||||
/* If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`.
|
||||
:DEFAULT: true */
|
||||
enableSanitizedErrorResponse: ?boolean;
|
||||
}
|
||||
|
||||
export interface RateLimitOptions {
|
||||
|
||||
@@ -52,7 +52,7 @@ async function RestQuery({
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad query type');
|
||||
}
|
||||
const isGet = method === RestQuery.Method.get;
|
||||
enforceRoleSecurity(method, className, auth);
|
||||
enforceRoleSecurity(method, className, auth, config);
|
||||
const result = runBeforeFind
|
||||
? await triggers.maybeRunQueryTrigger(
|
||||
triggers.Types.beforeFind,
|
||||
@@ -121,7 +121,7 @@ function _UnsafeRestQuery(
|
||||
if (!this.auth.isMaster) {
|
||||
if (this.className == '_Session') {
|
||||
if (!this.auth.user) {
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
|
||||
}
|
||||
this.restWhere = {
|
||||
$and: [
|
||||
@@ -424,7 +424,8 @@ _UnsafeRestQuery.prototype.validateClientClassCreation = function () {
|
||||
if (hasClass !== true) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
'This user is not allowed to access ' + 'non-existent class: ' + this.className
|
||||
'This user is not allowed to access ' + 'non-existent class: ' + this.className,
|
||||
this.config
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -803,7 +804,8 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () {
|
||||
if (this.restWhere[key]) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
`This user is not allowed to query ${key} on class ${this.className}`
|
||||
`This user is not allowed to query ${key} on class ${this.className}`,
|
||||
this.config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
'Cannot perform a write operation when using readOnlyMasterKey',
|
||||
config
|
||||
);
|
||||
}
|
||||
this.config = config;
|
||||
@@ -203,6 +204,7 @@ RestWrite.prototype.validateClientClassCreation = function () {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
'This user is not allowed to access non-existent class: ' + this.className,
|
||||
this.config
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -662,7 +664,8 @@ RestWrite.prototype.checkRestrictedFields = async function () {
|
||||
if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
"Clients aren't allowed to manually update email verification."
|
||||
"Clients aren't allowed to manually update email verification.",
|
||||
this.config
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1454,7 +1457,8 @@ RestWrite.prototype.runDatabaseOperation = function () {
|
||||
if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.SESSION_MISSING,
|
||||
`Cannot modify user ${this.query.objectId}.`
|
||||
`Cannot modify user ${this.query.objectId}.`,
|
||||
this.config
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ export class ClassesRouter extends PromiseRouter {
|
||||
typeof req.body?.objectId === 'string' &&
|
||||
req.body.objectId.startsWith('role:')
|
||||
) {
|
||||
throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.');
|
||||
throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.', req.config);
|
||||
}
|
||||
return rest.create(
|
||||
req.config,
|
||||
|
||||
@@ -5,7 +5,6 @@ import Config from '../Config';
|
||||
import logger from '../logger';
|
||||
const triggers = require('../triggers');
|
||||
const Utils = require('../Utils');
|
||||
import { createSanitizedError } from '../Error';
|
||||
|
||||
export class FilesRouter {
|
||||
expressRouter({ maxUploadSize = '20Mb' } = {}) {
|
||||
@@ -44,8 +43,7 @@ export class FilesRouter {
|
||||
const config = Config.get(req.params.appId);
|
||||
if (!config) {
|
||||
res.status(403);
|
||||
const err = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid application ID.');
|
||||
res.json({ code: err.code, error: err.message });
|
||||
res.json({ code: Parse.Error.OPERATION_FORBIDDEN, error: 'Invalid application ID.' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ export class GlobalConfigRouter extends PromiseRouter {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
"read-only masterKey isn't allowed to update the config.",
|
||||
req.config
|
||||
);
|
||||
}
|
||||
const params = req.body.params || {};
|
||||
|
||||
@@ -18,6 +18,7 @@ export class GraphQLRouter extends PromiseRouter {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
"read-only masterKey isn't allowed to update the GraphQL config.",
|
||||
req.config
|
||||
);
|
||||
}
|
||||
const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {});
|
||||
|
||||
@@ -9,6 +9,7 @@ export class PurgeRouter extends PromiseRouter {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
"read-only masterKey isn't allowed to purge a schema.",
|
||||
req.config
|
||||
);
|
||||
}
|
||||
return req.config.database
|
||||
|
||||
@@ -13,6 +13,7 @@ export class PushRouter extends PromiseRouter {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
"read-only masterKey isn't allowed to send push notifications.",
|
||||
req.config
|
||||
);
|
||||
}
|
||||
const pushController = req.config.pushController;
|
||||
|
||||
@@ -76,6 +76,7 @@ async function createSchema(req) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
"read-only masterKey isn't allowed to create a schema.",
|
||||
req.config
|
||||
);
|
||||
}
|
||||
if (req.params.className && req.body?.className) {
|
||||
@@ -98,6 +99,7 @@ function modifySchema(req) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
"read-only masterKey isn't allowed to update a schema.",
|
||||
req.config
|
||||
);
|
||||
}
|
||||
if (req.body?.className && req.body.className != req.params.className) {
|
||||
@@ -113,6 +115,7 @@ const deleteSchema = req => {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
"read-only masterKey isn't allowed to delete a schema.",
|
||||
req.config
|
||||
);
|
||||
}
|
||||
if (!SchemaController.classNameIsValid(req.params.className)) {
|
||||
|
||||
@@ -172,7 +172,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
|
||||
handleMe(req) {
|
||||
if (!req.info || !req.info.sessionToken) {
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
|
||||
}
|
||||
const sessionToken = req.info.sessionToken;
|
||||
return rest
|
||||
@@ -187,7 +187,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
)
|
||||
.then(response => {
|
||||
if (!response.results || response.results.length == 0 || !response.results[0].user) {
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
|
||||
} else {
|
||||
const user = response.results[0].user;
|
||||
// Send token back on the login, because SDKs expect that.
|
||||
@@ -338,6 +338,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
'master key is required',
|
||||
req.config
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,13 @@ const classesWithMasterOnlyAccess = [
|
||||
const { createSanitizedError } = require('./Error');
|
||||
|
||||
// Disallowing access to the _Role collection except by master key
|
||||
function enforceRoleSecurity(method, className, auth) {
|
||||
function enforceRoleSecurity(method, className, auth, config) {
|
||||
if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) {
|
||||
if (method === 'delete' || method === 'find') {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
`Clients aren't allowed to perform the ${method} operation on the installation collection.`
|
||||
`Clients aren't allowed to perform the ${method} operation on the installation collection.`,
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +28,8 @@ function enforceRoleSecurity(method, className, auth) {
|
||||
) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
`Clients aren't allowed to perform the ${method} operation on the ${className} collection.`
|
||||
`Clients aren't allowed to perform the ${method} operation on the ${className} collection.`,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,7 +37,8 @@ function enforceRoleSecurity(method, className, auth) {
|
||||
if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
`read-only masterKey isn't allowed to perform the ${method} operation.`
|
||||
`read-only masterKey isn't allowed to perform the ${method} operation.`,
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,7 +502,7 @@ export function handleParseErrors(err, req, res, next) {
|
||||
|
||||
export function enforceMasterKeyAccess(req, res, next) {
|
||||
if (!req.auth.isMaster) {
|
||||
const error = createSanitizedHttpError(403, 'unauthorized: master key is required');
|
||||
const error = createSanitizedHttpError(403, 'unauthorized: master key is required', req.config);
|
||||
res.status(error.status);
|
||||
res.end(`{"error":"${error.message}"}`);
|
||||
return;
|
||||
@@ -512,7 +512,7 @@ export function enforceMasterKeyAccess(req, res, next) {
|
||||
|
||||
export function promiseEnforceMasterKeyAccess(request) {
|
||||
if (!request.auth.isMaster) {
|
||||
throw createSanitizedHttpError(403, 'unauthorized: master key is required');
|
||||
throw createSanitizedHttpError(403, 'unauthorized: master key is required', request.config);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
20
src/rest.js
20
src/rest.js
@@ -135,7 +135,7 @@ async function runFindTriggers(
|
||||
|
||||
// Returns a promise for an object with optional keys 'results' and 'count'.
|
||||
const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => {
|
||||
enforceRoleSecurity('find', className, auth);
|
||||
enforceRoleSecurity('find', className, auth, config);
|
||||
return runFindTriggers(
|
||||
config,
|
||||
auth,
|
||||
@@ -150,7 +150,7 @@ const find = async (config, auth, className, restWhere, restOptions, clientSDK,
|
||||
|
||||
// get is just like find but only queries an objectId.
|
||||
const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => {
|
||||
enforceRoleSecurity('get', className, auth);
|
||||
enforceRoleSecurity('get', className, auth, config);
|
||||
return runFindTriggers(
|
||||
config,
|
||||
auth,
|
||||
@@ -173,7 +173,7 @@ function del(config, auth, className, objectId, context) {
|
||||
throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth to delete user');
|
||||
}
|
||||
|
||||
enforceRoleSecurity('delete', className, auth);
|
||||
enforceRoleSecurity('delete', className, auth, config);
|
||||
|
||||
let inflatedObject;
|
||||
let schemaController;
|
||||
@@ -196,7 +196,7 @@ function del(config, auth, className, objectId, context) {
|
||||
firstResult.className = className;
|
||||
if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) {
|
||||
if (!auth.user || firstResult.user.objectId !== auth.user.id) {
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
|
||||
}
|
||||
}
|
||||
var cacheAdapter = config.cacheController;
|
||||
@@ -258,13 +258,13 @@ function del(config, auth, className, objectId, context) {
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
handleSessionMissingError(error, className, auth);
|
||||
handleSessionMissingError(error, className, auth, config);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise for a {response, status, location} object.
|
||||
function create(config, auth, className, restObject, clientSDK, context) {
|
||||
enforceRoleSecurity('create', className, auth);
|
||||
enforceRoleSecurity('create', className, auth, config);
|
||||
var write = new RestWrite(config, auth, className, null, restObject, null, clientSDK, context);
|
||||
return write.execute();
|
||||
}
|
||||
@@ -273,7 +273,7 @@ function create(config, auth, className, restObject, clientSDK, context) {
|
||||
// REST API is supposed to return.
|
||||
// Usually, this is just updatedAt.
|
||||
function update(config, auth, className, restWhere, restObject, clientSDK, context) {
|
||||
enforceRoleSecurity('update', className, auth);
|
||||
enforceRoleSecurity('update', className, auth, config);
|
||||
|
||||
return Promise.resolve()
|
||||
.then(async () => {
|
||||
@@ -315,11 +315,11 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte
|
||||
).execute();
|
||||
})
|
||||
.catch(error => {
|
||||
handleSessionMissingError(error, className, auth);
|
||||
handleSessionMissingError(error, className, auth, config);
|
||||
});
|
||||
}
|
||||
|
||||
function handleSessionMissingError(error, className, auth) {
|
||||
function handleSessionMissingError(error, className, auth, config) {
|
||||
// If we're trying to update a user without / with bad session token
|
||||
if (
|
||||
className === '_User' &&
|
||||
@@ -327,7 +327,7 @@ function handleSessionMissingError(error, className, auth) {
|
||||
!auth.isMaster &&
|
||||
!auth.isMaintenance
|
||||
) {
|
||||
throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.');
|
||||
throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.', config);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user