From e212eb51954bfb5c49a59689fd785740489712fc Mon Sep 17 00:00:00 2001
From: Manuel <5673677+mtrezza@users.noreply.github.com>
Date: Fri, 23 Jun 2023 16:29:32 +0200
Subject: [PATCH 1/9] refactor: Add option to convert `Parse.Object` to
instance in Cloud Function payload (#8656)
---
DEPRECATIONS.md | 1 +
changelogs/CHANGELOG_alpha.md | 32 ++++
jsdoc-conf.json | 2 +-
package-lock.json | 23 ++-
package.json | 4 +-
postinstall.js | 2 +-
release_docs.sh | 5 +
resources/buildConfigDefinitions.js | 11 +-
spec/CloudCode.spec.js | 20 ++
spec/EmailVerificationToken.spec.js | 178 ++++++++++++++++++
spec/UserController.spec.js | 21 +--
src/Adapters/Files/GridFSBucketAdapter.js | 71 +++----
.../Postgres/PostgresStorageAdapter.js | 14 +-
src/Config.js | 2 +-
src/Controllers/UserController.js | 102 ++++++----
src/Deprecator/Deprecations.js | 1 +
src/Options/Definitions.js | 16 +-
src/Options/docs.js | 4 +-
src/Options/index.js | 13 +-
src/RestWrite.js | 51 ++++-
src/Routers/FunctionsRouter.js | 14 +-
src/Routers/PagesRouter.js | 2 +-
src/Routers/PublicAPIRouter.js | 2 +-
src/Routers/UsersRouter.js | 34 ++--
24 files changed, 467 insertions(+), 158 deletions(-)
diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md
index 56359c93..59b728a9 100644
--- a/DEPRECATIONS.md
+++ b/DEPRECATIONS.md
@@ -13,6 +13,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | deprecated | - |
+| DEPPS10 | Config option `encodeParseObjectInCloudFunction` defaults to `true` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 7.0.0 (2024) | deprecated | - |
[i_deprecation]: ## "The version and date of the deprecation."
[i_removal]: ## "The version and date of the planned removal."
diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md
index b9b1925b..14170fdf 100644
--- a/changelogs/CHANGELOG_alpha.md
+++ b/changelogs/CHANGELOG_alpha.md
@@ -1,3 +1,35 @@
+# [6.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.1...6.3.0-alpha.2) (2023-06-20)
+
+
+### Features
+
+* Add conditional email verification via dynamic Parse Server options `verifyUserEmails`, `sendUserEmailVerification` that now accept functions ([#8425](https://github.com/parse-community/parse-server/issues/8425)) ([44acd6d](https://github.com/parse-community/parse-server/commit/44acd6d9ed157ad4842200c9d01f9c77a05fec3a))
+
+# [6.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.3.0-alpha.1) (2023-06-18)
+
+
+### Bug Fixes
+
+* Cloud Code Trigger `afterSave` executes even if not set ([#8520](https://github.com/parse-community/parse-server/issues/8520)) ([afd0515](https://github.com/parse-community/parse-server/commit/afd0515e207bd947840579d3f245980dffa6f804))
+* GridFS file storage doesn't work with certain `enableSchemaHooks` settings ([#8467](https://github.com/parse-community/parse-server/issues/8467)) ([d4cda4b](https://github.com/parse-community/parse-server/commit/d4cda4b26c9bde8c812549b8780bea1cfabdb394))
+* Inaccurate table total row count for PostgreSQL ([#8511](https://github.com/parse-community/parse-server/issues/8511)) ([0823a02](https://github.com/parse-community/parse-server/commit/0823a02fbf80bc88dc403bc47e9f5c6597ea78b4))
+* LiveQuery server is not shut down properly when `handleShutdown` is called ([#8491](https://github.com/parse-community/parse-server/issues/8491)) ([967700b](https://github.com/parse-community/parse-server/commit/967700bdbc94c74f75ba84d2b3f4b9f3fd2dca0b))
+* Rate limit feature is incompatible with Node 14 ([#8578](https://github.com/parse-community/parse-server/issues/8578)) ([f911f2c](https://github.com/parse-community/parse-server/commit/f911f2cd3a8c45cd326272dcd681532764a3761e))
+* Unnecessary log entries by `extendSessionOnUse` ([#8562](https://github.com/parse-community/parse-server/issues/8562)) ([fd6a007](https://github.com/parse-community/parse-server/commit/fd6a0077f2e5cf83d65e52172ae5a950ab0f1eae))
+
+### Features
+
+* `extendSessionOnUse` to automatically renew Parse Sessions ([#8505](https://github.com/parse-community/parse-server/issues/8505)) ([6f885d3](https://github.com/parse-community/parse-server/commit/6f885d36b94902fdfea873fc554dee83589e6029))
+* Add new Parse Server option `preventSignupWithUnverifiedEmail` to prevent returning a user without session token on sign-up with unverified email address ([#8451](https://github.com/parse-community/parse-server/issues/8451)) ([82da308](https://github.com/parse-community/parse-server/commit/82da30842a55980aa90cb7680fbf6db37ee16dab))
+* Add option to change the log level of logs emitted by Cloud Functions ([#8530](https://github.com/parse-community/parse-server/issues/8530)) ([2caea31](https://github.com/parse-community/parse-server/commit/2caea310be412d82b04a85716bc769ccc410316d))
+* Add support for `$eq` query constraint in LiveQuery ([#8614](https://github.com/parse-community/parse-server/issues/8614)) ([656d673](https://github.com/parse-community/parse-server/commit/656d673cf5dea354e4f2b3d4dc2b29a41d311b3e))
+* Add zones for rate limiting by `ip`, `user`, `session`, `global` ([#8508](https://github.com/parse-community/parse-server/issues/8508)) ([03fba97](https://github.com/parse-community/parse-server/commit/03fba97e0549bfcaeee9f2fa4c9905dbcc91840e))
+* Allow `Parse.Object` pointers in Cloud Code arguments ([#8490](https://github.com/parse-community/parse-server/issues/8490)) ([28aeda3](https://github.com/parse-community/parse-server/commit/28aeda3f160efcbbcf85a85484a8d26567fa9761))
+
+### Reverts
+
+* fix: Inaccurate table total row count for PostgreSQL ([6722110](https://github.com/parse-community/parse-server/commit/6722110f203bc5fdcaa68cdf091cf9e7b48d1cff))
+
# [6.1.0-alpha.20](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.19...6.1.0-alpha.20) (2023-06-09)
diff --git a/jsdoc-conf.json b/jsdoc-conf.json
index 4a1e5de8..efbaa0a3 100644
--- a/jsdoc-conf.json
+++ b/jsdoc-conf.json
@@ -29,7 +29,7 @@
"template": "./node_modules/clean-jsdoc-theme",
"theme_opts": {
"default_theme": "dark",
- "title": "Parse Server",
+ "title": "
",
"create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }"
}
},
diff --git a/package-lock.json b/package-lock.json
index 20ad3e75..0ddcbdbe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "parse-server",
- "version": "6.3.0-beta.1",
+ "version": "6.3.0-alpha.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "parse-server",
- "version": "6.3.0-beta.1",
+ "version": "6.3.0-alpha.2",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -46,7 +46,7 @@
"pluralize": "8.0.0",
"rate-limit-redis": "3.0.2",
"redis": "4.6.6",
- "semver": "^7.5.1",
+ "semver": "^7.5.2",
"subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0",
"uuid": "9.0.0",
@@ -15213,6 +15213,11 @@
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
},
+ "bin": {
+ "sshpk-conv": "bin/sshpk-conv",
+ "sshpk-sign": "bin/sshpk-sign",
+ "sshpk-verify": "bin/sshpk-verify"
+ },
"engines": {
"node": ">=0.10.0"
}
@@ -18173,9 +18178,9 @@
}
},
"node_modules/semver": {
- "version": "7.5.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
- "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
+ "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -34401,9 +34406,9 @@
}
},
"semver": {
- "version": "7.5.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
- "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
+ "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"requires": {
"lru-cache": "^6.0.0"
},
diff --git a/package.json b/package.json
index e0b943ea..9ffb91d2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "parse-server",
- "version": "6.3.0-beta.1",
+ "version": "6.3.0-alpha.2",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -55,7 +55,7 @@
"pluralize": "8.0.0",
"rate-limit-redis": "3.0.2",
"redis": "4.6.6",
- "semver": "7.5.1",
+ "semver": "7.5.2",
"subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0",
"uuid": "9.0.0",
diff --git a/postinstall.js b/postinstall.js
index fef1fb31..fe1fc96b 100644
--- a/postinstall.js
+++ b/postinstall.js
@@ -1,6 +1,6 @@
const pkg = require('./package.json');
-const version = parseFloat(process.version.substr(1));
+const version = parseFloat(process.version.substring(1));
const minimum = parseFloat(pkg.engines.node.match(/\d+/g).join('.'));
module.exports = function () {
diff --git a/release_docs.sh b/release_docs.sh
index a7bb2632..a9cc5bf3 100755
--- a/release_docs.sh
+++ b/release_docs.sh
@@ -27,3 +27,8 @@ npm run docs
mkdir -p "docs/api/${DEST}"
cp -R out/* "docs/api/${DEST}"
+
+# Copy other resources
+RESOURCE_DIR=".github"
+mkdir -p "docs/${RESOURCE_DIR}"
+cp "./.github/parse-server-logo.png" "docs/${RESOURCE_DIR}/"
diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js
index e0d33daa..0be6e008 100644
--- a/resources/buildConfigDefinitions.js
+++ b/resources/buildConfigDefinitions.js
@@ -255,7 +255,16 @@ function inject(t, list) {
props.push(t.objectProperty(t.stringLiteral('action'), action));
}
if (elt.defaultValue) {
- const parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
+ let parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
+ if (!parsedValue) {
+ for (const type of elt.typeAnnotation.types) {
+ elt.type = type.type;
+ parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
+ if (parsedValue) {
+ break;
+ }
+ }
+ }
if (parsedValue) {
props.push(t.objectProperty(t.stringLiteral('default'), parsedValue));
} else {
diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js
index a8795a4e..a2e62355 100644
--- a/spec/CloudCode.spec.js
+++ b/spec/CloudCode.spec.js
@@ -1353,7 +1353,27 @@ describe('Cloud Code', () => {
});
});
+ it('should not encode Parse Objects', async () => {
+ const user = new Parse.User();
+ user.setUsername('username');
+ user.setPassword('password');
+ user.set('deleted', false);
+ await user.signUp();
+ Parse.Cloud.define(
+ 'deleteAccount',
+ async req => {
+ expect(req.params.object instanceof Parse.Object).not.toBeTrue();
+ return 'Object deleted';
+ },
+ {
+ requireMaster: true,
+ }
+ );
+ await Parse.Cloud.run('deleteAccount', { object: user.toPointer() }, { useMasterKey: true });
+ });
+
it('allow cloud to encode Parse Objects', async () => {
+ await reconfigureServer({ encodeParseObjectInCloudFunction: true });
const user = new Parse.User();
user.setUsername('username');
user.setPassword('password');
diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js
index e21a0497..a7a59b89 100644
--- a/spec/EmailVerificationToken.spec.js
+++ b/spec/EmailVerificationToken.spec.js
@@ -288,6 +288,184 @@ describe('Email Verification Token Expiration: ', () => {
});
});
+ it('can conditionally send emails', async () => {
+ let sendEmailOptions;
+ const emailAdapter = {
+ sendVerificationEmail: options => {
+ sendEmailOptions = options;
+ },
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => {},
+ };
+ const verifyUserEmails = {
+ method(req) {
+ expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip']);
+ return false;
+ },
+ };
+ const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
+ await reconfigureServer({
+ appName: 'emailVerifyToken',
+ verifyUserEmails: verifyUserEmails.method,
+ emailAdapter: emailAdapter,
+ emailVerifyTokenValidityDuration: 5, // 5 seconds
+ publicServerURL: 'http://localhost:8378/1',
+ });
+ const beforeSave = {
+ method(req) {
+ req.object.set('emailVerified', true);
+ },
+ };
+ const saveSpy = spyOn(beforeSave, 'method').and.callThrough();
+ const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
+ Parse.Cloud.beforeSave(Parse.User, beforeSave.method);
+ const user = new Parse.User();
+ user.setUsername('sets_email_verify_token_expires_at');
+ user.setPassword('expiringToken');
+ user.set('email', 'user@example.com');
+ await user.signUp();
+
+ const config = Config.get('test');
+ const results = await config.database.find(
+ '_User',
+ {
+ username: 'sets_email_verify_token_expires_at',
+ },
+ {},
+ Auth.maintenance(config)
+ );
+
+ expect(results.length).toBe(1);
+ const user_data = results[0];
+ expect(typeof user_data).toBe('object');
+ expect(user_data.emailVerified).toEqual(true);
+ expect(user_data._email_verify_token).toBeUndefined();
+ expect(user_data._email_verify_token_expires_at).toBeUndefined();
+ expect(emailSpy).not.toHaveBeenCalled();
+ expect(saveSpy).toHaveBeenCalled();
+ expect(sendEmailOptions).toBeUndefined();
+ expect(verifySpy).toHaveBeenCalled();
+ });
+
+ it('can conditionally send emails and allow conditional login', async () => {
+ let sendEmailOptions;
+ const emailAdapter = {
+ sendVerificationEmail: options => {
+ sendEmailOptions = options;
+ },
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => {},
+ };
+ const verifyUserEmails = {
+ method(req) {
+ expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip']);
+ if (req.object.get('username') === 'no_email') {
+ return false;
+ }
+ return true;
+ },
+ };
+ const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
+ await reconfigureServer({
+ appName: 'emailVerifyToken',
+ verifyUserEmails: verifyUserEmails.method,
+ preventLoginWithUnverifiedEmail: verifyUserEmails.method,
+ emailAdapter: emailAdapter,
+ emailVerifyTokenValidityDuration: 5, // 5 seconds
+ publicServerURL: 'http://localhost:8378/1',
+ });
+ const user = new Parse.User();
+ user.setUsername('no_email');
+ user.setPassword('expiringToken');
+ user.set('email', 'user@example.com');
+ await user.signUp();
+ expect(sendEmailOptions).toBeUndefined();
+ expect(user.getSessionToken()).toBeDefined();
+ expect(verifySpy).toHaveBeenCalledTimes(2);
+ const user2 = new Parse.User();
+ user2.setUsername('email');
+ user2.setPassword('expiringToken');
+ user2.set('email', 'user2@example.com');
+ await user2.signUp();
+ expect(user2.getSessionToken()).toBeUndefined();
+ expect(sendEmailOptions).toBeDefined();
+ expect(verifySpy).toHaveBeenCalledTimes(4);
+ });
+
+ it('can conditionally send user email verification', async () => {
+ const emailAdapter = {
+ sendVerificationEmail: () => {},
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => {},
+ };
+ const sendVerificationEmail = {
+ method(req) {
+ expect(req.user).toBeDefined();
+ expect(req.master).toBeDefined();
+ return false;
+ },
+ };
+ const sendSpy = spyOn(sendVerificationEmail, 'method').and.callThrough();
+ await reconfigureServer({
+ appName: 'emailVerifyToken',
+ verifyUserEmails: true,
+ emailAdapter: emailAdapter,
+ emailVerifyTokenValidityDuration: 5, // 5 seconds
+ publicServerURL: 'http://localhost:8378/1',
+ sendUserEmailVerification: sendVerificationEmail.method,
+ });
+ const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
+ const newUser = new Parse.User();
+ newUser.setUsername('unsets_email_verify_token_expires_at');
+ newUser.setPassword('expiringToken');
+ newUser.set('email', 'user@example.com');
+ await newUser.signUp();
+ await Parse.User.requestEmailVerification('user@example.com');
+ expect(sendSpy).toHaveBeenCalledTimes(2);
+ expect(emailSpy).toHaveBeenCalledTimes(0);
+ });
+
+ it('beforeSave options do not change existing behaviour', async () => {
+ let sendEmailOptions;
+ const emailAdapter = {
+ sendVerificationEmail: options => {
+ sendEmailOptions = options;
+ },
+ sendPasswordResetEmail: () => Promise.resolve(),
+ sendMail: () => {},
+ };
+ await reconfigureServer({
+ appName: 'emailVerifyToken',
+ verifyUserEmails: true,
+ emailAdapter: emailAdapter,
+ emailVerifyTokenValidityDuration: 5, // 5 seconds
+ publicServerURL: 'http://localhost:8378/1',
+ });
+ const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
+ const newUser = new Parse.User();
+ newUser.setUsername('unsets_email_verify_token_expires_at');
+ newUser.setPassword('expiringToken');
+ newUser.set('email', 'user@parse.com');
+ await newUser.signUp();
+ const response = await request({
+ url: sendEmailOptions.link,
+ followRedirects: false,
+ });
+ expect(response.status).toEqual(302);
+ const config = Config.get('test');
+ const results = await config.database.find('_User', {
+ username: 'unsets_email_verify_token_expires_at',
+ });
+
+ expect(results.length).toBe(1);
+ const user = results[0];
+ expect(typeof user).toBe('object');
+ expect(user.emailVerified).toEqual(true);
+ expect(typeof user._email_verify_token).toBe('undefined');
+ expect(typeof user._email_verify_token_expires_at).toBe('undefined');
+ expect(emailSpy).toHaveBeenCalled();
+ });
+
it('unsets the _email_verify_token_expires_at and _email_verify_token fields in the User class if email verification is successful', done => {
const user = new Parse.User();
let sendEmailOptions;
diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js
index 6bcc454b..7b983677 100644
--- a/spec/UserController.spec.js
+++ b/spec/UserController.spec.js
@@ -1,4 +1,3 @@
-const UserController = require('../lib/Controllers/UserController').UserController;
const emailAdapter = require('./support/MockEmailAdapter');
describe('UserController', () => {
@@ -11,11 +10,14 @@ describe('UserController', () => {
describe('sendVerificationEmail', () => {
describe('parseFrameURL not provided', () => {
it('uses publicServerURL', async done => {
- await reconfigureServer({
+ const server = await reconfigureServer({
publicServerURL: 'http://www.example.com',
customPages: {
parseFrameURL: undefined,
},
+ verifyUserEmails: true,
+ emailAdapter,
+ appName: 'test',
});
emailAdapter.sendVerificationEmail = options => {
expect(options.link).toEqual(
@@ -24,20 +26,20 @@ describe('UserController', () => {
emailAdapter.sendVerificationEmail = () => Promise.resolve();
done();
};
- const userController = new UserController(emailAdapter, 'test', {
- verifyUserEmails: true,
- });
- userController.sendVerificationEmail(user);
+ server.config.userController.sendVerificationEmail(user);
});
});
describe('parseFrameURL provided', () => {
it('uses parseFrameURL and includes the destination in the link parameter', async done => {
- await reconfigureServer({
+ const server = await reconfigureServer({
publicServerURL: 'http://www.example.com',
customPages: {
parseFrameURL: 'http://someother.example.com/handle-parse-iframe',
},
+ verifyUserEmails: true,
+ emailAdapter,
+ appName: 'test',
});
emailAdapter.sendVerificationEmail = options => {
expect(options.link).toEqual(
@@ -46,10 +48,7 @@ describe('UserController', () => {
emailAdapter.sendVerificationEmail = () => Promise.resolve();
done();
};
- const userController = new UserController(emailAdapter, 'test', {
- verifyUserEmails: true,
- });
- userController.sendVerificationEmail(user);
+ server.config.userController.sendVerificationEmail(user);
});
});
});
diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js
index 45116578..76a8f25d 100644
--- a/src/Adapters/Files/GridFSBucketAdapter.js
+++ b/src/Adapters/Files/GridFSBucketAdapter.js
@@ -28,7 +28,11 @@ export class GridFSBucketAdapter extends FilesAdapter {
this._algorithm = 'aes-256-gcm';
this._encryptionKey =
encryptionKey !== undefined
- ? crypto.createHash('sha256').update(String(encryptionKey)).digest('base64').substr(0, 32)
+ ? crypto
+ .createHash('sha256')
+ .update(String(encryptionKey))
+ .digest('base64')
+ .substring(0, 32)
: null;
const defaultMongoOptions = {
useNewUrlParser: true,
@@ -138,8 +142,8 @@ export class GridFSBucketAdapter extends FilesAdapter {
}
async rotateEncryptionKey(options = {}) {
- var fileNames = [];
- var oldKeyFileAdapter = {};
+ let fileNames = [];
+ let oldKeyFileAdapter = {};
const bucket = await this._getBucket();
if (options.oldKey !== undefined) {
oldKeyFileAdapter = new GridFSBucketAdapter(
@@ -158,51 +162,22 @@ export class GridFSBucketAdapter extends FilesAdapter {
fileNames.push(file.filename);
});
}
- return new Promise(resolve => {
- var fileNamesNotRotated = fileNames;
- var fileNamesRotated = [];
- var fileNameTotal = fileNames.length;
- var fileNameIndex = 0;
- fileNames.forEach(fileName => {
- oldKeyFileAdapter
- .getFileData(fileName)
- .then(plainTextData => {
- //Overwrite file with data encrypted with new key
- this.createFile(fileName, plainTextData)
- .then(() => {
- fileNamesRotated.push(fileName);
- fileNamesNotRotated = fileNamesNotRotated.filter(function (value) {
- return value !== fileName;
- });
- fileNameIndex += 1;
- if (fileNameIndex == fileNameTotal) {
- resolve({
- rotated: fileNamesRotated,
- notRotated: fileNamesNotRotated,
- });
- }
- })
- .catch(() => {
- fileNameIndex += 1;
- if (fileNameIndex == fileNameTotal) {
- resolve({
- rotated: fileNamesRotated,
- notRotated: fileNamesNotRotated,
- });
- }
- });
- })
- .catch(() => {
- fileNameIndex += 1;
- if (fileNameIndex == fileNameTotal) {
- resolve({
- rotated: fileNamesRotated,
- notRotated: fileNamesNotRotated,
- });
- }
- });
- });
- });
+ let fileNamesNotRotated = fileNames;
+ const fileNamesRotated = [];
+ for (const fileName of fileNames) {
+ try {
+ const plainTextData = await oldKeyFileAdapter.getFileData(fileName);
+ // Overwrite file with data encrypted with new key
+ await this.createFile(fileName, plainTextData);
+ fileNamesRotated.push(fileName);
+ fileNamesNotRotated = fileNamesNotRotated.filter(function (value) {
+ return value !== fileName;
+ });
+ } catch (err) {
+ continue;
+ }
+ }
+ return { rotated: fileNamesRotated, notRotated: fileNamesNotRotated };
}
getFileLocation(config, filename) {
diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js
index 3e8e8677..3ad59ec7 100644
--- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js
+++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js
@@ -231,7 +231,7 @@ const transformAggregateField = fieldName => {
if (fieldName === '$_updated_at') {
return 'updatedAt';
}
- return fieldName.substr(1);
+ return fieldName.substring(1);
};
const validateKeys = object => {
@@ -1921,14 +1921,14 @@ export class PostgresStorageAdapter implements StorageAdapter {
};
}
if (object[fieldName] && schema.fields[fieldName].type === 'Polygon') {
- let coords = object[fieldName];
- coords = coords.substr(2, coords.length - 4).split('),(');
- coords = coords.map(point => {
+ let coords = new String(object[fieldName]);
+ coords = coords.substring(2, coords.length - 2).split('),(');
+ const updatedCoords = coords.map(point => {
return [parseFloat(point.split(',')[1]), parseFloat(point.split(',')[0])];
});
object[fieldName] = {
__type: 'Polygon',
- coordinates: coords,
+ coordinates: updatedCoords,
};
}
if (object[fieldName] && schema.fields[fieldName].type === 'File') {
@@ -2634,7 +2634,7 @@ function literalizeRegexPart(s: string) {
const result1: any = s.match(matcher1);
if (result1 && result1.length > 1 && result1.index > -1) {
// process regex that has a beginning and an end specified for the literal text
- const prefix = s.substr(0, result1.index);
+ const prefix = s.substring(0, result1.index);
const remaining = result1[1];
return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
@@ -2644,7 +2644,7 @@ function literalizeRegexPart(s: string) {
const matcher2 = /\\Q((?!\\E).*)$/;
const result2: any = s.match(matcher2);
if (result2 && result2.length > 1 && result2.index > -1) {
- const prefix = s.substr(0, result2.index);
+ const prefix = s.substring(0, result2.index);
const remaining = result2[1];
return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
diff --git a/src/Config.js b/src/Config.js
index 5e3a49bb..8fe10a9a 100644
--- a/src/Config.js
+++ b/src/Config.js
@@ -25,7 +25,7 @@ function removeTrailingSlash(str) {
return str;
}
if (str.endsWith('/')) {
- str = str.substr(0, str.length - 1);
+ str = str.substring(0, str.length - 1);
}
return str;
}
diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js
index 6871add9..7618f500 100644
--- a/src/Controllers/UserController.js
+++ b/src/Controllers/UserController.js
@@ -32,20 +32,33 @@ export class UserController extends AdaptableController {
}
get shouldVerifyEmails() {
- return this.options.verifyUserEmails;
+ return (this.config || this.options).verifyUserEmails;
}
- setEmailVerifyToken(user) {
- if (this.shouldVerifyEmails) {
- user._email_verify_token = randomString(25);
- user.emailVerified = false;
-
- if (this.config.emailVerifyTokenValidityDuration) {
- user._email_verify_token_expires_at = Parse._encode(
- this.config.generateEmailVerifyTokenExpiresAt()
- );
- }
+ async setEmailVerifyToken(user, req, storage = {}) {
+ let shouldSendEmail = this.shouldVerifyEmails;
+ if (typeof shouldSendEmail === 'function') {
+ const response = await Promise.resolve(shouldSendEmail(req));
+ shouldSendEmail = response !== false;
}
+ if (!shouldSendEmail) {
+ return false;
+ }
+ storage.sendVerificationEmail = true;
+ user._email_verify_token = randomString(25);
+ if (
+ !storage.fieldsChangedByTrigger ||
+ !storage.fieldsChangedByTrigger.includes('emailVerified')
+ ) {
+ user.emailVerified = false;
+ }
+
+ if (this.config.emailVerifyTokenValidityDuration) {
+ user._email_verify_token_expires_at = Parse._encode(
+ this.config.generateEmailVerifyTokenExpiresAt()
+ );
+ }
+ return true;
}
verifyEmail(username, token) {
@@ -131,27 +144,39 @@ export class UserController extends AdaptableController {
});
}
- sendVerificationEmail(user) {
+ async sendVerificationEmail(user, req) {
if (!this.shouldVerifyEmails) {
return;
}
const token = encodeURIComponent(user._email_verify_token);
// We may need to fetch the user in case of update email
- this.getUserIfNeeded(user).then(user => {
- const username = encodeURIComponent(user.username);
+ const fetchedUser = await this.getUserIfNeeded(user);
+ let shouldSendEmail = this.config.sendUserEmailVerification;
+ if (typeof shouldSendEmail === 'function') {
+ const response = await Promise.resolve(
+ this.config.sendUserEmailVerification({
+ user: Parse.Object.fromJSON({ className: '_User', ...fetchedUser }),
+ master: req.auth?.isMaster,
+ })
+ );
+ shouldSendEmail = !!response;
+ }
+ if (!shouldSendEmail) {
+ return;
+ }
+ const username = encodeURIComponent(user.username);
- const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config);
- const options = {
- appName: this.config.appName,
- link: link,
- user: inflate('_User', user),
- };
- if (this.adapter.sendVerificationEmail) {
- this.adapter.sendVerificationEmail(options);
- } else {
- this.adapter.sendMail(this.defaultVerificationEmail(options));
- }
- });
+ const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config);
+ const options = {
+ appName: this.config.appName,
+ link: link,
+ user: inflate('_User', fetchedUser),
+ };
+ if (this.adapter.sendVerificationEmail) {
+ this.adapter.sendVerificationEmail(options);
+ } else {
+ this.adapter.sendMail(this.defaultVerificationEmail(options));
+ }
}
/**
@@ -160,7 +185,7 @@ export class UserController extends AdaptableController {
* @param user
* @returns {*}
*/
- regenerateEmailVerifyToken(user) {
+ async regenerateEmailVerifyToken(user, master) {
const { _email_verify_token } = user;
let { _email_verify_token_expires_at } = user;
if (_email_verify_token_expires_at && _email_verify_token_expires_at.__type === 'Date') {
@@ -174,19 +199,22 @@ export class UserController extends AdaptableController {
) {
return Promise.resolve();
}
- this.setEmailVerifyToken(user);
+ const shouldSend = await this.setEmailVerifyToken(user, { user, master });
+ if (!shouldSend) {
+ return;
+ }
return this.config.database.update('_User', { username: user.username }, user);
}
- resendVerificationEmail(username) {
- return this.getUserIfNeeded({ username: username }).then(aUser => {
- if (!aUser || aUser.emailVerified) {
- throw undefined;
- }
- return this.regenerateEmailVerifyToken(aUser).then(() => {
- this.sendVerificationEmail(aUser);
- });
- });
+ async resendVerificationEmail(username, req) {
+ const aUser = await this.getUserIfNeeded({ username: username });
+ if (!aUser || aUser.emailVerified) {
+ throw undefined;
+ }
+ const generate = await this.regenerateEmailVerifyToken(aUser, req.auth?.isMaster);
+ if (generate) {
+ this.sendVerificationEmail(aUser, req);
+ }
}
setPasswordResetToken(email) {
diff --git a/src/Deprecator/Deprecations.js b/src/Deprecator/Deprecations.js
index 0afd98ff..2f698ad3 100644
--- a/src/Deprecator/Deprecations.js
+++ b/src/Deprecator/Deprecations.js
@@ -18,4 +18,5 @@
module.exports = [
{ optionKey: 'allowClientClassCreation', changeNewDefault: 'false' },
{ optionKey: 'allowExpiredAuthDataToken', changeNewDefault: 'false' },
+ { optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
];
diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js
index 3815902c..6477836e 100644
--- a/src/Options/Definitions.js
+++ b/src/Options/Definitions.js
@@ -210,6 +210,13 @@ module.exports.ParseServerOptions = {
action: parsers.booleanParser,
default: false,
},
+ encodeParseObjectInCloudFunction: {
+ env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION',
+ help:
+ '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`.
\u2139\uFE0F 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`.',
+ action: parsers.booleanParser,
+ default: false,
+ },
encryptionKey: {
env: 'PARSE_SERVER_ENCRYPTION_KEY',
help: 'Key for encrypting your files',
@@ -496,6 +503,12 @@ module.exports.ParseServerOptions = {
action: parsers.objectParser,
default: {},
},
+ sendUserEmailVerification: {
+ env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION',
+ help:
+ 'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.
Default is `true`.
',
+ default: true,
+ },
serverCloseComplete: {
env: 'PARSE_SERVER_SERVER_CLOSE_COMPLETE',
help: 'Callback when server has closed',
@@ -542,8 +555,7 @@ module.exports.ParseServerOptions = {
verifyUserEmails: {
env: 'PARSE_SERVER_VERIFY_USER_EMAILS',
help:
- 'Set to `true` to require users to verify their email address to complete the sign-up process.
Default is `false`.',
- action: parsers.booleanParser,
+ 'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.
Default is `false`.',
default: false,
},
webhookKey: {
diff --git a/src/Options/docs.js b/src/Options/docs.js
index 847e7df9..fdb62bb5 100644
--- a/src/Options/docs.js
+++ b/src/Options/docs.js
@@ -40,6 +40,7 @@
* @property {Number} emailVerifyTokenValidityDuration Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.
For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).
Default is `undefined`.
Requires option `verifyUserEmails: true`.
* @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true
* @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
+ * @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`.
ℹ️ 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.
* @property {Boolean} expireInactiveSessions Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.
@@ -89,6 +90,7 @@
* @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false.
* @property {SchemaOptions} schema Defined schema
* @property {SecurityOptions} security The security options to identify and report weak security settings.
+ * @property {Boolean} sendUserEmailVerification Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.
Default is `true`.
* @property {Function} serverCloseComplete Callback when server has closed
* @property {String} serverURL URL to your parse server with http:// or https://.
* @property {Number} sessionLength Session duration, in seconds, defaults to 1 year
@@ -97,7 +99,7 @@
* @property {Any} trustProxy The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the express trust proxy settings documentation. Defaults to `false`.
* @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields
* @property {Boolean} verbose Set the logging to verbose
- * @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process.
Default is `false`.
+ * @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.
Default is `false`.
* @property {String} webhookKey Key sent with outgoing webhook calls
*/
diff --git a/src/Options/index.js b/src/Options/index.js
index 87813147..a8414c65 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -153,11 +153,11 @@ export interface ParseServerOptions {
/* Max file size for uploads, defaults to 20mb
:DEFAULT: 20mb */
maxUploadSize: ?string;
- /* Set to `true` to require users to verify their email address to complete the sign-up process.
+ /* Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.
Default is `false`.
:DEFAULT: false */
- verifyUserEmails: ?boolean;
+ verifyUserEmails: ?(boolean | void);
/* Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required.
Default is `false`.
@@ -188,6 +188,12 @@ export interface ParseServerOptions {
Requires option `verifyUserEmails: true`.
:DEFAULT: false */
emailVerifyTokenReuseIfValid: ?boolean;
+ /* Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.
+
+ Default is `true`.
+
+ :DEFAULT: true */
+ sendUserEmailVerification: ?(boolean | void);
/* The account lockout policy for failed login attempts. */
accountLockout: ?AccountLockoutOptions;
/* The password policy for enforcing password related rules. */
@@ -196,6 +202,9 @@ export interface ParseServerOptions {
cacheAdapter: ?Adapter;
/* Adapter module for email sending */
emailAdapter: ?Adapter;
+ /* 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`.
ℹ️ 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`.
+ :DEFAULT: false */
+ encodeParseObjectInCloudFunction: ?boolean;
/* Public URL to your parse server with http:// or https://.
:ENV: PARSE_PUBLIC_SERVER_URL */
publicServerURL: ?string;
diff --git a/src/RestWrite.js b/src/RestWrite.js
index f7c6a535..003a4a7d 100644
--- a/src/RestWrite.js
+++ b/src/RestWrite.js
@@ -113,6 +113,9 @@ RestWrite.prototype.execute = function () {
.then(() => {
return this.validateAuthData();
})
+ .then(() => {
+ return this.checkRestrictedFields();
+ })
.then(() => {
return this.runBeforeSaveTrigger();
})
@@ -603,17 +606,23 @@ RestWrite.prototype.handleAuthData = async function (authData) {
}
};
-// The non-third-party parts of User transformation
-RestWrite.prototype.transformUser = function () {
- var promise = Promise.resolve();
+RestWrite.prototype.checkRestrictedFields = async function () {
if (this.className !== '_User') {
- return promise;
+ return;
}
if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) {
const error = `Clients aren't allowed to manually update email verification.`;
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
}
+};
+
+// The non-third-party parts of User transformation
+RestWrite.prototype.transformUser = function () {
+ var promise = Promise.resolve();
+ if (this.className !== '_User') {
+ return promise;
+ }
// Do not cleanup session if objectId is not set
if (this.query && this.objectId()) {
@@ -751,8 +760,14 @@ RestWrite.prototype._validateEmail = function () {
Object.keys(this.data.authData)[0] === 'anonymous')
) {
// We updated the email, send a new validation
- this.storage['sendVerificationEmail'] = true;
- this.config.userController.setEmailVerifyToken(this.data);
+ const { originalObject, updatedObject } = this.buildParseObjects();
+ const request = {
+ original: originalObject,
+ object: updatedObject,
+ master: this.auth.isMaster,
+ ip: this.config.ip,
+ };
+ return this.config.userController.setEmailVerifyToken(this.data, request, this.storage);
}
});
};
@@ -864,7 +879,7 @@ RestWrite.prototype._validatePasswordHistory = function () {
return Promise.resolve();
};
-RestWrite.prototype.createSessionTokenIfNeeded = function () {
+RestWrite.prototype.createSessionTokenIfNeeded = async function () {
if (this.className !== '_User') {
return;
}
@@ -878,13 +893,31 @@ RestWrite.prototype.createSessionTokenIfNeeded = function () {
}
if (
!this.storage.authProvider && // signup call, with
- this.config.preventLoginWithUnverifiedEmail && // no login without verification
+ this.config.preventLoginWithUnverifiedEmail === true && // no login without verification
this.config.verifyUserEmails
) {
// verification is on
this.storage.rejectSignup = true;
return;
}
+ if (!this.storage.authProvider && this.config.verifyUserEmails) {
+ let shouldPreventUnverifedLogin = this.config.preventLoginWithUnverifiedEmail;
+ if (typeof this.config.preventLoginWithUnverifiedEmail === 'function') {
+ const { originalObject, updatedObject } = this.buildParseObjects();
+ const request = {
+ original: originalObject,
+ object: updatedObject,
+ master: this.auth.isMaster,
+ ip: this.config.ip,
+ };
+ shouldPreventUnverifedLogin = await Promise.resolve(
+ this.config.preventLoginWithUnverifiedEmail(request)
+ );
+ }
+ if (shouldPreventUnverifedLogin === true) {
+ return;
+ }
+ }
return this.createSessionToken();
};
@@ -1010,7 +1043,7 @@ RestWrite.prototype.handleFollowup = function () {
if (this.storage && this.storage['sendVerificationEmail']) {
delete this.storage['sendVerificationEmail'];
// Fire and forget!
- this.config.userController.sendVerificationEmail(this.data);
+ this.config.userController.sendVerificationEmail(this.data, { auth: this.auth });
return this.handleFollowup.bind(this);
}
};
diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js
index da69d54e..bb4b959e 100644
--- a/src/Routers/FunctionsRouter.js
+++ b/src/Routers/FunctionsRouter.js
@@ -9,7 +9,7 @@ import { jobStatusHandler } from '../StatusHandler';
import _ from 'lodash';
import { logger } from '../logger';
-function parseObject(obj) {
+function parseObject(obj, config) {
if (Array.isArray(obj)) {
return obj.map(item => {
return parseObject(item);
@@ -18,21 +18,21 @@ function parseObject(obj) {
return Object.assign(new Date(obj.iso), obj);
} else if (obj && obj.__type == 'File') {
return Parse.File.fromJSON(obj);
- } else if (obj && obj.__type == 'Pointer') {
+ } else if (obj && obj.__type == 'Pointer' && config.encodeParseObjectInCloudFunction) {
return Parse.Object.fromJSON({
__type: 'Pointer',
className: obj.className,
objectId: obj.objectId,
});
} else if (obj && typeof obj === 'object') {
- return parseParams(obj);
+ return parseParams(obj, config);
} else {
return obj;
}
}
-function parseParams(params) {
- return _.mapValues(params, parseObject);
+function parseParams(params, config) {
+ return _.mapValues(params, item => parseObject(item, config));
}
export class FunctionsRouter extends PromiseRouter {
@@ -66,7 +66,7 @@ export class FunctionsRouter extends PromiseRouter {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid job.');
}
let params = Object.assign({}, req.body, req.query);
- params = parseParams(params);
+ params = parseParams(params, req.config);
const request = {
params: params,
log: req.config.loggerController,
@@ -126,7 +126,7 @@ export class FunctionsRouter extends PromiseRouter {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, `Invalid function: "${functionName}"`);
}
let params = Object.assign({}, req.body, req.query);
- params = parseParams(params);
+ params = parseParams(params, req.config);
const request = {
params: params,
master: req.auth && req.auth.isMaster,
diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js
index 5d5a1467..79a487b6 100644
--- a/src/Routers/PagesRouter.js
+++ b/src/Routers/PagesRouter.js
@@ -125,7 +125,7 @@ export class PagesRouter extends PromiseRouter {
const userController = config.userController;
- return userController.resendVerificationEmail(username).then(
+ return userController.resendVerificationEmail(username, req).then(
() => {
return this.goToPage(req, pages.emailVerificationSendSuccess);
},
diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js
index 5009ee7d..ddef76a5 100644
--- a/src/Routers/PublicAPIRouter.js
+++ b/src/Routers/PublicAPIRouter.js
@@ -63,7 +63,7 @@ export class PublicAPIRouter extends PromiseRouter {
const userController = config.userController;
- return userController.resendVerificationEmail(username).then(
+ return userController.resendVerificationEmail(username, req).then(
() => {
return Promise.resolve({
status: 302,
diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js
index feca46e8..e58f3dda 100644
--- a/src/Routers/UsersRouter.js
+++ b/src/Routers/UsersRouter.js
@@ -447,7 +447,7 @@ export class UsersRouter extends ClassesRouter {
}
}
- handleVerificationEmailRequest(req) {
+ async handleVerificationEmailRequest(req) {
this._throwOnBadEmailConfig(req);
const { email } = req.body;
@@ -461,25 +461,25 @@ export class UsersRouter extends ClassesRouter {
);
}
- return req.config.database.find('_User', { email: email }).then(results => {
- if (!results.length || results.length < 1) {
- throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`);
- }
- const user = results[0];
+ const results = await req.config.database.find('_User', { email: email });
+ if (!results.length || results.length < 1) {
+ throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`);
+ }
+ const user = results[0];
- // remove password field, messes with saving on postgres
- delete user.password;
+ // remove password field, messes with saving on postgres
+ delete user.password;
- if (user.emailVerified) {
- throw new Parse.Error(Parse.Error.OTHER_CAUSE, `Email ${email} is already verified.`);
- }
+ if (user.emailVerified) {
+ throw new Parse.Error(Parse.Error.OTHER_CAUSE, `Email ${email} is already verified.`);
+ }
- const userController = req.config.userController;
- return userController.regenerateEmailVerifyToken(user).then(() => {
- userController.sendVerificationEmail(user);
- return { response: {} };
- });
- });
+ const userController = req.config.userController;
+ const send = await userController.regenerateEmailVerifyToken(user, req.auth.isMaster);
+ if (send) {
+ userController.sendVerificationEmail(user, req);
+ }
+ return { response: {} };
}
async handleChallenge(req) {
From 3dd99dd80e27e5e1d99b42844180546d90c7aa90 Mon Sep 17 00:00:00 2001
From: Manuel <5673677+mtrezza@users.noreply.github.com>
Date: Wed, 28 Jun 2023 22:57:25 +0200
Subject: [PATCH 2/9] fix: Remote code execution via MongoDB BSON parser
through prototype pollution; fixes security vulnerability
[GHSA-462x-c3jw-7vr6](https://github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6)
(#8674)
---
.eslintrc.json | 3 ++
spec/vulnerabilities.spec.js | 65 +++++++++++++++++++++++++++
src/Controllers/DatabaseController.js | 10 +++++
src/RestWrite.js | 23 +++-------
src/Routers/FilesRouter.js | 22 +++------
src/Utils.js | 12 +++++
6 files changed, 101 insertions(+), 34 deletions(-)
diff --git a/.eslintrc.json b/.eslintrc.json
index 59953216..c04e2d31 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -25,5 +25,8 @@
"space-infix-ops": "error",
"no-useless-escape": "off",
"require-atomic-updates": "off"
+ },
+ "globals": {
+ "Parse": true
}
}
diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js
index 5c83493c..c499eb01 100644
--- a/spec/vulnerabilities.spec.js
+++ b/spec/vulnerabilities.spec.js
@@ -138,6 +138,71 @@ describe('Vulnerabilities', () => {
);
});
+ it('denies creating global config with polluted data', async () => {
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'X-Parse-Application-Id': 'test',
+ 'X-Parse-Master-Key': 'test',
+ };
+ const params = {
+ method: 'PUT',
+ url: 'http://localhost:8378/1/config',
+ json: true,
+ body: {
+ params: {
+ welcomeMesssage: 'Welcome to Parse',
+ foo: { _bsontype: 'Code', code: 'shell' },
+ },
+ },
+ headers,
+ };
+ const response = await request(params).catch(e => e);
+ expect(response.status).toBe(400);
+ const text = JSON.parse(response.text);
+ expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
+ expect(text.error).toBe(
+ 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
+ );
+ });
+
+ it('denies direct database write wih prohibited keys', async () => {
+ const Config = require('../lib/Config');
+ const config = Config.get(Parse.applicationId);
+ const user = {
+ objectId: '1234567890',
+ username: 'hello',
+ password: 'pass',
+ _session_token: 'abc',
+ foo: { _bsontype: 'Code', code: 'shell' },
+ };
+ await expectAsync(config.database.create('_User', user)).toBeRejectedWith(
+ new Parse.Error(
+ Parse.Error.INVALID_KEY_NAME,
+ 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
+ )
+ );
+ });
+
+ it('denies direct database update wih prohibited keys', async () => {
+ const Config = require('../lib/Config');
+ const config = Config.get(Parse.applicationId);
+ const user = {
+ objectId: '1234567890',
+ username: 'hello',
+ password: 'pass',
+ _session_token: 'abc',
+ foo: { _bsontype: 'Code', code: 'shell' },
+ };
+ await expectAsync(
+ config.database.update('_User', { _id: user.objectId }, user)
+ ).toBeRejectedWith(
+ new Parse.Error(
+ Parse.Error.INVALID_KEY_NAME,
+ 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
+ )
+ );
+ });
+
it('denies creating a hook with polluted data', async () => {
const express = require('express');
const bodyParser = require('body-parser');
diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js
index e3ac5723..435095fb 100644
--- a/src/Controllers/DatabaseController.js
+++ b/src/Controllers/DatabaseController.js
@@ -475,6 +475,11 @@ class DatabaseController {
validateOnly: boolean = false,
validSchemaController: SchemaController.SchemaController
): Promise {
+ try {
+ Utils.checkProhibitedKeywords(this.options, update);
+ } catch (error) {
+ return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
+ }
const originalQuery = query;
const originalUpdate = update;
// Make a copy of the object, so we don't mutate the incoming data.
@@ -805,6 +810,11 @@ class DatabaseController {
validateOnly: boolean = false,
validSchemaController: SchemaController.SchemaController
): Promise {
+ try {
+ Utils.checkProhibitedKeywords(this.options, object);
+ } catch (error) {
+ return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
+ }
// Make a copy of the object, so we don't mutate the incoming data.
const originalObject = object;
object = transformObjectACL(object);
diff --git a/src/RestWrite.js b/src/RestWrite.js
index 3a8385e5..dd36eb37 100644
--- a/src/RestWrite.js
+++ b/src/RestWrite.js
@@ -64,8 +64,6 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
}
}
- this.checkProhibitedKeywords(data);
-
// When the operation is complete, this.response may have several
// fields.
// response: the actual data to be returned
@@ -298,7 +296,11 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
delete this.data.objectId;
}
}
- this.checkProhibitedKeywords(this.data);
+ try {
+ Utils.checkProhibitedKeywords(this.config, this.data);
+ } catch (error) {
+ throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, error);
+ }
});
};
@@ -1756,20 +1758,5 @@ RestWrite.prototype._updateResponseWithData = function (response, data) {
return response;
};
-RestWrite.prototype.checkProhibitedKeywords = function (data) {
- if (this.config.requestKeywordDenylist) {
- // Scan request data for denied keywords
- for (const keyword of this.config.requestKeywordDenylist) {
- const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
- if (match) {
- throw new Parse.Error(
- Parse.Error.INVALID_KEY_NAME,
- `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
- );
- }
- }
- }
-};
-
export default RestWrite;
module.exports = RestWrite;
diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js
index ed48a28a..a5322b4c 100644
--- a/src/Routers/FilesRouter.js
+++ b/src/Routers/FilesRouter.js
@@ -175,22 +175,12 @@ export class FilesRouter {
const base64 = req.body.toString('base64');
const file = new Parse.File(filename, { base64 }, contentType);
const { metadata = {}, tags = {} } = req.fileData || {};
- if (req.config && req.config.requestKeywordDenylist) {
- // Scan request data for denied keywords
- for (const keyword of req.config.requestKeywordDenylist) {
- const match =
- Utils.objectContainsKeyValue(metadata, keyword.key, keyword.value) ||
- Utils.objectContainsKeyValue(tags, keyword.key, keyword.value);
- if (match) {
- next(
- new Parse.Error(
- Parse.Error.INVALID_KEY_NAME,
- `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
- )
- );
- return;
- }
- }
+ try {
+ Utils.checkProhibitedKeywords(config, metadata);
+ Utils.checkProhibitedKeywords(config, tags);
+ } catch (error) {
+ next(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
+ return;
}
file.setTags(tags);
file.setMetadata(metadata);
diff --git a/src/Utils.js b/src/Utils.js
index d5a255a5..efeae58f 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -358,6 +358,18 @@ class Utils {
}
return false;
}
+
+ static checkProhibitedKeywords(config, data) {
+ if (config?.requestKeywordDenylist) {
+ // Scan request data for denied keywords
+ for (const keyword of config.requestKeywordDenylist) {
+ const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
+ if (match) {
+ throw `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`;
+ }
+ }
+ }
+ }
}
module.exports = Utils;
From 328918178f751cca1615878e8ab94c200edf804f Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Wed, 28 Jun 2023 20:58:52 +0000
Subject: [PATCH 3/9] chore(release): 6.2.1 [skip ci]
## [6.2.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.2.1) (2023-06-28)
### Bug Fixes
* Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-462x-c3jw-7vr6](https://github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6) ([#8674](https://github.com/parse-community/parse-server/issues/8674)) ([3dd99dd](https://github.com/parse-community/parse-server/commit/3dd99dd80e27e5e1d99b42844180546d90c7aa90))
---
changelogs/CHANGELOG_release.md | 7 +++++++
package-lock.json | 4 ++--
package.json | 2 +-
3 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md
index 4cd0131c..b92e3f61 100644
--- a/changelogs/CHANGELOG_release.md
+++ b/changelogs/CHANGELOG_release.md
@@ -1,3 +1,10 @@
+## [6.2.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.2.1) (2023-06-28)
+
+
+### Bug Fixes
+
+* Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-462x-c3jw-7vr6](https://github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6) ([#8674](https://github.com/parse-community/parse-server/issues/8674)) ([3dd99dd](https://github.com/parse-community/parse-server/commit/3dd99dd80e27e5e1d99b42844180546d90c7aa90))
+
# [6.2.0](https://github.com/parse-community/parse-server/compare/6.1.0...6.2.0) (2023-05-20)
diff --git a/package-lock.json b/package-lock.json
index c1d20e3b..648e63a4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "parse-server",
- "version": "6.2.0",
+ "version": "6.2.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "parse-server",
- "version": "6.2.0",
+ "version": "6.2.1",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
diff --git a/package.json b/package.json
index 16ae61cc..91f3be73 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "parse-server",
- "version": "6.2.0",
+ "version": "6.2.1",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
From d6b17baa32e7121b01faec375bb114f3ca6f393c Mon Sep 17 00:00:00 2001
From: Manuel <5673677+mtrezza@users.noreply.github.com>
Date: Wed, 28 Jun 2023 23:37:25 +0200
Subject: [PATCH 4/9] refactor: Remote code execution via MongoDB BSON parser
through prototype pollution; fixes security vulnerability
[GHSA-462x-c3jw-7vr6](https://github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6)
(#8677)
---
.eslintrc.json | 3 ++
spec/vulnerabilities.spec.js | 65 +++++++++++++++++++++++++++
src/Controllers/DatabaseController.js | 10 +++++
src/RestWrite.js | 23 +++-------
src/Routers/FilesRouter.js | 22 +++------
src/Utils.js | 12 +++++
6 files changed, 101 insertions(+), 34 deletions(-)
diff --git a/.eslintrc.json b/.eslintrc.json
index 59953216..c04e2d31 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -25,5 +25,8 @@
"space-infix-ops": "error",
"no-useless-escape": "off",
"require-atomic-updates": "off"
+ },
+ "globals": {
+ "Parse": true
}
}
diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js
index 5c83493c..c499eb01 100644
--- a/spec/vulnerabilities.spec.js
+++ b/spec/vulnerabilities.spec.js
@@ -138,6 +138,71 @@ describe('Vulnerabilities', () => {
);
});
+ it('denies creating global config with polluted data', async () => {
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'X-Parse-Application-Id': 'test',
+ 'X-Parse-Master-Key': 'test',
+ };
+ const params = {
+ method: 'PUT',
+ url: 'http://localhost:8378/1/config',
+ json: true,
+ body: {
+ params: {
+ welcomeMesssage: 'Welcome to Parse',
+ foo: { _bsontype: 'Code', code: 'shell' },
+ },
+ },
+ headers,
+ };
+ const response = await request(params).catch(e => e);
+ expect(response.status).toBe(400);
+ const text = JSON.parse(response.text);
+ expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
+ expect(text.error).toBe(
+ 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
+ );
+ });
+
+ it('denies direct database write wih prohibited keys', async () => {
+ const Config = require('../lib/Config');
+ const config = Config.get(Parse.applicationId);
+ const user = {
+ objectId: '1234567890',
+ username: 'hello',
+ password: 'pass',
+ _session_token: 'abc',
+ foo: { _bsontype: 'Code', code: 'shell' },
+ };
+ await expectAsync(config.database.create('_User', user)).toBeRejectedWith(
+ new Parse.Error(
+ Parse.Error.INVALID_KEY_NAME,
+ 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
+ )
+ );
+ });
+
+ it('denies direct database update wih prohibited keys', async () => {
+ const Config = require('../lib/Config');
+ const config = Config.get(Parse.applicationId);
+ const user = {
+ objectId: '1234567890',
+ username: 'hello',
+ password: 'pass',
+ _session_token: 'abc',
+ foo: { _bsontype: 'Code', code: 'shell' },
+ };
+ await expectAsync(
+ config.database.update('_User', { _id: user.objectId }, user)
+ ).toBeRejectedWith(
+ new Parse.Error(
+ Parse.Error.INVALID_KEY_NAME,
+ 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
+ )
+ );
+ });
+
it('denies creating a hook with polluted data', async () => {
const express = require('express');
const bodyParser = require('body-parser');
diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js
index e3ac5723..435095fb 100644
--- a/src/Controllers/DatabaseController.js
+++ b/src/Controllers/DatabaseController.js
@@ -475,6 +475,11 @@ class DatabaseController {
validateOnly: boolean = false,
validSchemaController: SchemaController.SchemaController
): Promise {
+ try {
+ Utils.checkProhibitedKeywords(this.options, update);
+ } catch (error) {
+ return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
+ }
const originalQuery = query;
const originalUpdate = update;
// Make a copy of the object, so we don't mutate the incoming data.
@@ -805,6 +810,11 @@ class DatabaseController {
validateOnly: boolean = false,
validSchemaController: SchemaController.SchemaController
): Promise {
+ try {
+ Utils.checkProhibitedKeywords(this.options, object);
+ } catch (error) {
+ return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
+ }
// Make a copy of the object, so we don't mutate the incoming data.
const originalObject = object;
object = transformObjectACL(object);
diff --git a/src/RestWrite.js b/src/RestWrite.js
index 003a4a7d..a624895e 100644
--- a/src/RestWrite.js
+++ b/src/RestWrite.js
@@ -64,8 +64,6 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
}
}
- this.checkProhibitedKeywords(data);
-
// When the operation is complete, this.response may have several
// fields.
// response: the actual data to be returned
@@ -304,7 +302,11 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
delete this.data.objectId;
}
}
- this.checkProhibitedKeywords(this.data);
+ try {
+ Utils.checkProhibitedKeywords(this.config, this.data);
+ } catch (error) {
+ throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, error);
+ }
});
};
@@ -1797,20 +1799,5 @@ RestWrite.prototype._updateResponseWithData = function (response, data) {
return response;
};
-RestWrite.prototype.checkProhibitedKeywords = function (data) {
- if (this.config.requestKeywordDenylist) {
- // Scan request data for denied keywords
- for (const keyword of this.config.requestKeywordDenylist) {
- const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
- if (match) {
- throw new Parse.Error(
- Parse.Error.INVALID_KEY_NAME,
- `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
- );
- }
- }
- }
-};
-
export default RestWrite;
module.exports = RestWrite;
diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js
index ed48a28a..a5322b4c 100644
--- a/src/Routers/FilesRouter.js
+++ b/src/Routers/FilesRouter.js
@@ -175,22 +175,12 @@ export class FilesRouter {
const base64 = req.body.toString('base64');
const file = new Parse.File(filename, { base64 }, contentType);
const { metadata = {}, tags = {} } = req.fileData || {};
- if (req.config && req.config.requestKeywordDenylist) {
- // Scan request data for denied keywords
- for (const keyword of req.config.requestKeywordDenylist) {
- const match =
- Utils.objectContainsKeyValue(metadata, keyword.key, keyword.value) ||
- Utils.objectContainsKeyValue(tags, keyword.key, keyword.value);
- if (match) {
- next(
- new Parse.Error(
- Parse.Error.INVALID_KEY_NAME,
- `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
- )
- );
- return;
- }
- }
+ try {
+ Utils.checkProhibitedKeywords(config, metadata);
+ Utils.checkProhibitedKeywords(config, tags);
+ } catch (error) {
+ next(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
+ return;
}
file.setTags(tags);
file.setMetadata(metadata);
diff --git a/src/Utils.js b/src/Utils.js
index d5a255a5..efeae58f 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -358,6 +358,18 @@ class Utils {
}
return false;
}
+
+ static checkProhibitedKeywords(config, data) {
+ if (config?.requestKeywordDenylist) {
+ // Scan request data for denied keywords
+ for (const keyword of config.requestKeywordDenylist) {
+ const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
+ if (match) {
+ throw `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`;
+ }
+ }
+ }
+ }
}
module.exports = Utils;
From be4c7e23c63a2fb690685665cebed0de26be05c5 Mon Sep 17 00:00:00 2001
From: Manuel <5673677+mtrezza@users.noreply.github.com>
Date: Mon, 4 Sep 2023 14:19:48 +0200
Subject: [PATCH 5/9] fix: Parse Pointer allows to access internal Parse Server
classes and circumvent `beforeFind` query trigger; fixes security
vulnerability
[GHSA-fcv6-fg5r-jm9q](https://github.com/parse-community/parse-server/security/advisories/GHSA-fcv6-fg5r-jm9q)
---
spec/CloudCode.spec.js | 29 +++++
spec/ParseGraphQLServer.spec.js | 1 -
spec/ParseRole.spec.js | 2 +-
spec/RestQuery.spec.js | 44 ++++---
spec/rest.spec.js | 32 +++++
src/Auth.js | 46 +++++--
src/Controllers/PushController.js | 11 +-
src/Controllers/UserController.js | 23 +++-
src/RestQuery.js | 197 ++++++++++++++++++++++--------
src/RestWrite.js | 32 +++--
src/SharedRest.js | 37 ++++++
src/rest.js | 184 ++++++++++------------------
12 files changed, 414 insertions(+), 224 deletions(-)
create mode 100644 src/SharedRest.js
diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js
index c02999ad..50e40485 100644
--- a/spec/CloudCode.spec.js
+++ b/spec/CloudCode.spec.js
@@ -2381,6 +2381,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', () => {
diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js
index 87718da1..022fb99f 100644
--- a/spec/ParseGraphQLServer.spec.js
+++ b/spec/ParseGraphQLServer.spec.js
@@ -5275,7 +5275,6 @@ describe('ParseGraphQLServer', () => {
it('should only count', async () => {
await prepareData();
-
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const where = {
diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js
index 47fed865..31de5b66 100644
--- a/spec/ParseRole.spec.js
+++ b/spec/ParseRole.spec.js
@@ -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()
diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js
index 24e22ac4..023d3b47 100644
--- a/spec/RestQuery.spec.js
+++ b/spec/RestQuery.spec.js
@@ -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 => {
diff --git a/spec/rest.spec.js b/spec/rest.spec.js
index 02d2f596..61a5c728 100644
--- a/spec/rest.spec.js
+++ b/spec/rest.spec.js
@@ -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')
diff --git a/src/Auth.js b/src/Auth.js
index abd14391..15179b01 100644
--- a/src/Auth.js
+++ b/src/Auth.js
@@ -97,7 +97,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 = (
@@ -134,12 +142,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) {
@@ -184,9 +200,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)
@@ -278,9 +300,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;
};
diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js
index 1a5b9bf4..04fb5c4f 100644
--- a/src/Controllers/PushController.js
+++ b/src/Controllers/PushController.js
@@ -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,
diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js
index 6871add9..51bc9878 100644
--- a/src/Controllers/UserController.js
+++ b/src/Controllers/UserController.js
@@ -48,7 +48,7 @@ export class UserController extends AdaptableController {
}
}
- verifyEmail(username, token) {
+ async verifyEmail(username, token) {
if (!this.shouldVerifyEmails) {
// Trying to verify email when not enabled
// TODO: Better error here.
@@ -70,8 +70,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) {
@@ -110,7 +116,7 @@ export class UserController extends AdaptableController {
});
}
- getUserIfNeeded(user) {
+ async getUserIfNeeded(user) {
if (user.username && user.email) {
return Promise.resolve(user);
}
@@ -122,7 +128,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;
diff --git a/src/RestQuery.js b/src/RestQuery.js
index fe3617eb..538d87d4 100644
--- a/src/RestQuery.js
+++ b/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;
diff --git a/src/RestWrite.js b/src/RestWrite.js
index dd36eb37..31e2ddd2 100644
--- a/src/RestWrite.js
+++ b/src/RestWrite.js
@@ -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;
@@ -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
diff --git a/src/SharedRest.js b/src/SharedRest.js
new file mode 100644
index 00000000..0b4a07c3
--- /dev/null
+++ b/src/SharedRest.js
@@ -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,
+};
diff --git a/src/rest.js b/src/rest.js
index e1e53668..1f9dbacb 100644
--- a/src/rest.js
+++ b/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,
From d141b822ad4473e4434e13639d126825a221e7a0 Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Mon, 4 Sep 2023 12:20:56 +0000
Subject: [PATCH 6/9] chore(release): 6.2.2 [skip ci]
## [6.2.2](https://github.com/parse-community/parse-server/compare/6.2.1...6.2.2) (2023-09-04)
### Bug Fixes
* Parse Pointer allows to access internal Parse Server classes and circumvent `beforeFind` query trigger; fixes security vulnerability [GHSA-fcv6-fg5r-jm9q](https://github.com/parse-community/parse-server/security/advisories/GHSA-fcv6-fg5r-jm9q) ([be4c7e2](https://github.com/parse-community/parse-server/commit/be4c7e23c63a2fb690685665cebed0de26be05c5))
---
changelogs/CHANGELOG_release.md | 7 +++++++
package-lock.json | 4 ++--
package.json | 2 +-
3 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md
index b92e3f61..665ba4f2 100644
--- a/changelogs/CHANGELOG_release.md
+++ b/changelogs/CHANGELOG_release.md
@@ -1,3 +1,10 @@
+## [6.2.2](https://github.com/parse-community/parse-server/compare/6.2.1...6.2.2) (2023-09-04)
+
+
+### Bug Fixes
+
+* Parse Pointer allows to access internal Parse Server classes and circumvent `beforeFind` query trigger; fixes security vulnerability [GHSA-fcv6-fg5r-jm9q](https://github.com/parse-community/parse-server/security/advisories/GHSA-fcv6-fg5r-jm9q) ([be4c7e2](https://github.com/parse-community/parse-server/commit/be4c7e23c63a2fb690685665cebed0de26be05c5))
+
## [6.2.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.2.1) (2023-06-28)
diff --git a/package-lock.json b/package-lock.json
index 648e63a4..a88c2d60 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "parse-server",
- "version": "6.2.1",
+ "version": "6.2.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "parse-server",
- "version": "6.2.1",
+ "version": "6.2.2",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
diff --git a/package.json b/package.json
index 91f3be73..d0ad2fca 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "parse-server",
- "version": "6.2.1",
+ "version": "6.2.2",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
From 739ffbed86758a4b7f60fd952aa9b52e4625fc22 Mon Sep 17 00:00:00 2001
From: Manuel <5673677+mtrezza@users.noreply.github.com>
Date: Mon, 4 Sep 2023 16:01:22 +0200
Subject: [PATCH 7/9] refactor: Parse Pointer allows to access internal Parse
Server classes and circumvent `beforeFind` query trigger (#8734)
---
spec/CloudCode.spec.js | 29 +++++
spec/ParseGraphQLServer.spec.js | 1 -
spec/ParseRole.spec.js | 2 +-
spec/RestQuery.spec.js | 44 ++++---
spec/rest.spec.js | 32 +++++
src/Auth.js | 61 ++++++---
src/Controllers/PushController.js | 11 +-
src/Controllers/UserController.js | 23 +++-
src/RestQuery.js | 197 ++++++++++++++++++++++--------
src/RestWrite.js | 32 +++--
src/SharedRest.js | 37 ++++++
src/rest.js | 184 ++++++++++------------------
12 files changed, 423 insertions(+), 230 deletions(-)
create mode 100644 src/SharedRest.js
diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js
index a2e62355..acc2230a 100644
--- a/spec/CloudCode.spec.js
+++ b/spec/CloudCode.spec.js
@@ -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', () => {
diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js
index 87718da1..022fb99f 100644
--- a/spec/ParseGraphQLServer.spec.js
+++ b/spec/ParseGraphQLServer.spec.js
@@ -5275,7 +5275,6 @@ describe('ParseGraphQLServer', () => {
it('should only count', async () => {
await prepareData();
-
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const where = {
diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js
index 47fed865..31de5b66 100644
--- a/spec/ParseRole.spec.js
+++ b/spec/ParseRole.spec.js
@@ -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()
diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js
index 24e22ac4..023d3b47 100644
--- a/spec/RestQuery.spec.js
+++ b/spec/RestQuery.spec.js
@@ -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 => {
diff --git a/spec/rest.spec.js b/spec/rest.spec.js
index 02d2f596..61a5c728 100644
--- a/spec/rest.spec.js
+++ b/spec/rest.spec.js
@@ -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')
diff --git a/src/Auth.js b/src/Auth.js
index 96c99cbb..b68ebc40 100644
--- a/src/Auth.js
+++ b/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;
};
diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js
index 1a5b9bf4..04fb5c4f 100644
--- a/src/Controllers/PushController.js
+++ b/src/Controllers/PushController.js
@@ -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,
diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js
index 7618f500..726dc279 100644
--- a/src/Controllers/UserController.js
+++ b/src/Controllers/UserController.js
@@ -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;
diff --git a/src/RestQuery.js b/src/RestQuery.js
index fe3617eb..538d87d4 100644
--- a/src/RestQuery.js
+++ b/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;
diff --git a/src/RestWrite.js b/src/RestWrite.js
index a624895e..4fff0e3a 100644
--- a/src/RestWrite.js
+++ b/src/RestWrite.js
@@ -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
diff --git a/src/SharedRest.js b/src/SharedRest.js
new file mode 100644
index 00000000..0b4a07c3
--- /dev/null
+++ b/src/SharedRest.js
@@ -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,
+};
diff --git a/src/rest.js b/src/rest.js
index e1e53668..1f9dbacb 100644
--- a/src/rest.js
+++ b/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,
From 3602ecb169ccd875cb3363c20ad346f98b99ea7b Mon Sep 17 00:00:00 2001
From: semantic-release-bot
Date: Sat, 16 Sep 2023 01:08:49 +0000
Subject: [PATCH 9/9] chore(release): 6.3.0 [skip ci]
# [6.3.0](https://github.com/parse-community/parse-server/compare/6.2.2...6.3.0) (2023-09-16)
### Bug Fixes
* Cloud Code Trigger `afterSave` executes even if not set ([#8520](https://github.com/parse-community/parse-server/issues/8520)) ([afd0515](https://github.com/parse-community/parse-server/commit/afd0515e207bd947840579d3f245980dffa6f804))
* GridFS file storage doesn't work with certain `enableSchemaHooks` settings ([#8467](https://github.com/parse-community/parse-server/issues/8467)) ([d4cda4b](https://github.com/parse-community/parse-server/commit/d4cda4b26c9bde8c812549b8780bea1cfabdb394))
* Inaccurate table total row count for PostgreSQL ([#8511](https://github.com/parse-community/parse-server/issues/8511)) ([0823a02](https://github.com/parse-community/parse-server/commit/0823a02fbf80bc88dc403bc47e9f5c6597ea78b4))
* LiveQuery server is not shut down properly when `handleShutdown` is called ([#8491](https://github.com/parse-community/parse-server/issues/8491)) ([967700b](https://github.com/parse-community/parse-server/commit/967700bdbc94c74f75ba84d2b3f4b9f3fd2dca0b))
* Rate limit feature is incompatible with Node 14 ([#8578](https://github.com/parse-community/parse-server/issues/8578)) ([f911f2c](https://github.com/parse-community/parse-server/commit/f911f2cd3a8c45cd326272dcd681532764a3761e))
* Unnecessary log entries by `extendSessionOnUse` ([#8562](https://github.com/parse-community/parse-server/issues/8562)) ([fd6a007](https://github.com/parse-community/parse-server/commit/fd6a0077f2e5cf83d65e52172ae5a950ab0f1eae))
### Features
* `extendSessionOnUse` to automatically renew Parse Sessions ([#8505](https://github.com/parse-community/parse-server/issues/8505)) ([6f885d3](https://github.com/parse-community/parse-server/commit/6f885d36b94902fdfea873fc554dee83589e6029))
* Add new Parse Server option `preventSignupWithUnverifiedEmail` to prevent returning a user without session token on sign-up with unverified email address ([#8451](https://github.com/parse-community/parse-server/issues/8451)) ([82da308](https://github.com/parse-community/parse-server/commit/82da30842a55980aa90cb7680fbf6db37ee16dab))
* Add option to change the log level of logs emitted by Cloud Functions ([#8530](https://github.com/parse-community/parse-server/issues/8530)) ([2caea31](https://github.com/parse-community/parse-server/commit/2caea310be412d82b04a85716bc769ccc410316d))
* Add support for `$eq` query constraint in LiveQuery ([#8614](https://github.com/parse-community/parse-server/issues/8614)) ([656d673](https://github.com/parse-community/parse-server/commit/656d673cf5dea354e4f2b3d4dc2b29a41d311b3e))
* Add zones for rate limiting by `ip`, `user`, `session`, `global` ([#8508](https://github.com/parse-community/parse-server/issues/8508)) ([03fba97](https://github.com/parse-community/parse-server/commit/03fba97e0549bfcaeee9f2fa4c9905dbcc91840e))
* Allow `Parse.Object` pointers in Cloud Code arguments ([#8490](https://github.com/parse-community/parse-server/issues/8490)) ([28aeda3](https://github.com/parse-community/parse-server/commit/28aeda3f160efcbbcf85a85484a8d26567fa9761))
### Reverts
* fix: Inaccurate table total row count for PostgreSQL ([6722110](https://github.com/parse-community/parse-server/commit/6722110f203bc5fdcaa68cdf091cf9e7b48d1cff))
---
changelogs/CHANGELOG_release.md | 25 +++++++++++++++++++++++++
package-lock.json | 4 ++--
package.json | 2 +-
3 files changed, 28 insertions(+), 3 deletions(-)
diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md
index 665ba4f2..b93450cc 100644
--- a/changelogs/CHANGELOG_release.md
+++ b/changelogs/CHANGELOG_release.md
@@ -1,3 +1,28 @@
+# [6.3.0](https://github.com/parse-community/parse-server/compare/6.2.2...6.3.0) (2023-09-16)
+
+
+### Bug Fixes
+
+* Cloud Code Trigger `afterSave` executes even if not set ([#8520](https://github.com/parse-community/parse-server/issues/8520)) ([afd0515](https://github.com/parse-community/parse-server/commit/afd0515e207bd947840579d3f245980dffa6f804))
+* GridFS file storage doesn't work with certain `enableSchemaHooks` settings ([#8467](https://github.com/parse-community/parse-server/issues/8467)) ([d4cda4b](https://github.com/parse-community/parse-server/commit/d4cda4b26c9bde8c812549b8780bea1cfabdb394))
+* Inaccurate table total row count for PostgreSQL ([#8511](https://github.com/parse-community/parse-server/issues/8511)) ([0823a02](https://github.com/parse-community/parse-server/commit/0823a02fbf80bc88dc403bc47e9f5c6597ea78b4))
+* LiveQuery server is not shut down properly when `handleShutdown` is called ([#8491](https://github.com/parse-community/parse-server/issues/8491)) ([967700b](https://github.com/parse-community/parse-server/commit/967700bdbc94c74f75ba84d2b3f4b9f3fd2dca0b))
+* Rate limit feature is incompatible with Node 14 ([#8578](https://github.com/parse-community/parse-server/issues/8578)) ([f911f2c](https://github.com/parse-community/parse-server/commit/f911f2cd3a8c45cd326272dcd681532764a3761e))
+* Unnecessary log entries by `extendSessionOnUse` ([#8562](https://github.com/parse-community/parse-server/issues/8562)) ([fd6a007](https://github.com/parse-community/parse-server/commit/fd6a0077f2e5cf83d65e52172ae5a950ab0f1eae))
+
+### Features
+
+* `extendSessionOnUse` to automatically renew Parse Sessions ([#8505](https://github.com/parse-community/parse-server/issues/8505)) ([6f885d3](https://github.com/parse-community/parse-server/commit/6f885d36b94902fdfea873fc554dee83589e6029))
+* Add new Parse Server option `preventSignupWithUnverifiedEmail` to prevent returning a user without session token on sign-up with unverified email address ([#8451](https://github.com/parse-community/parse-server/issues/8451)) ([82da308](https://github.com/parse-community/parse-server/commit/82da30842a55980aa90cb7680fbf6db37ee16dab))
+* Add option to change the log level of logs emitted by Cloud Functions ([#8530](https://github.com/parse-community/parse-server/issues/8530)) ([2caea31](https://github.com/parse-community/parse-server/commit/2caea310be412d82b04a85716bc769ccc410316d))
+* Add support for `$eq` query constraint in LiveQuery ([#8614](https://github.com/parse-community/parse-server/issues/8614)) ([656d673](https://github.com/parse-community/parse-server/commit/656d673cf5dea354e4f2b3d4dc2b29a41d311b3e))
+* Add zones for rate limiting by `ip`, `user`, `session`, `global` ([#8508](https://github.com/parse-community/parse-server/issues/8508)) ([03fba97](https://github.com/parse-community/parse-server/commit/03fba97e0549bfcaeee9f2fa4c9905dbcc91840e))
+* Allow `Parse.Object` pointers in Cloud Code arguments ([#8490](https://github.com/parse-community/parse-server/issues/8490)) ([28aeda3](https://github.com/parse-community/parse-server/commit/28aeda3f160efcbbcf85a85484a8d26567fa9761))
+
+### Reverts
+
+* fix: Inaccurate table total row count for PostgreSQL ([6722110](https://github.com/parse-community/parse-server/commit/6722110f203bc5fdcaa68cdf091cf9e7b48d1cff))
+
## [6.2.2](https://github.com/parse-community/parse-server/compare/6.2.1...6.2.2) (2023-09-04)
diff --git a/package-lock.json b/package-lock.json
index 0ddcbdbe..51cb4ac7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "parse-server",
- "version": "6.3.0-alpha.2",
+ "version": "6.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "parse-server",
- "version": "6.3.0-alpha.2",
+ "version": "6.3.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
diff --git a/package.json b/package.json
index 9ffb91d2..aeefd288 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "parse-server",
- "version": "6.3.0-alpha.2",
+ "version": "6.3.0",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {