From 45301a67e8b265693b37759d9e2622470df32a8a Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 18 Jun 2023 03:19:07 +0200 Subject: [PATCH 01/34] docs: Fix broken logo link in API docs (#8642) --- jsdoc-conf.json | 2 +- package-lock.json | 7 ++++++- release_docs.sh | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/jsdoc-conf.json b/jsdoc-conf.json index 4a1e5de846..efbaa0a37c 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 20ad3e754d..c6130f0e07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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.1", "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" } diff --git a/release_docs.sh b/release_docs.sh index a7bb26324c..a9cc5bf3bc 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}/" From 9674d4a2c0a9d0cda112056a6a2b1629931f37a3 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 18 Jun 2023 01:20:54 +0000 Subject: [PATCH 02/34] chore(release): 6.3.0-alpha.1 [skip ci] # [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)) --- changelogs/CHANGELOG_alpha.md | 25 +++++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index b9b1925b78..c7a229e5a7 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,28 @@ +# [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/package-lock.json b/package-lock.json index c6130f0e07..146ac05e7e 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.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.3.0-beta.1", + "version": "6.3.0-alpha.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index e0b943eab4..7a58f71970 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.1", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 3710da737909f7877a00ad431ad942084ecb808c Mon Sep 17 00:00:00 2001 From: Corey Date: Tue, 20 Jun 2023 06:07:10 -0400 Subject: [PATCH 03/34] refactor: Replace deprecated `substr` with `substring` (#8644) --- postinstall.js | 2 +- src/Adapters/Files/GridFSBucketAdapter.js | 71 ++++++------------- .../Postgres/PostgresStorageAdapter.js | 14 ++-- src/Config.js | 2 +- 4 files changed, 32 insertions(+), 57 deletions(-) diff --git a/postinstall.js b/postinstall.js index fef1fb31ff..fe1fc96bae 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/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 451165789d..76a8f25d1b 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 3e8e867799..3ad59ec77f 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 5e3a49bb35..8fe10a9a1c 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; } From 44acd6d9ed157ad4842200c9d01f9c77a05fec3a Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 20 Jun 2023 20:10:25 +1000 Subject: [PATCH 04/34] feat: Add conditional email verification via dynamic Parse Server options `verifyUserEmails`, `sendUserEmailVerification` that now accept functions (#8425) --- resources/buildConfigDefinitions.js | 11 +- spec/EmailVerificationToken.spec.js | 178 ++++++++++++++++++++++++++++ spec/UserController.spec.js | 21 ++-- src/Controllers/UserController.js | 100 ++++++++++------ src/Options/Definitions.js | 9 +- src/Options/docs.js | 3 +- src/Options/index.js | 10 +- src/RestWrite.js | 51 ++++++-- src/Routers/PagesRouter.js | 2 +- src/Routers/PublicAPIRouter.js | 2 +- src/Routers/UsersRouter.js | 34 +++--- 11 files changed, 340 insertions(+), 81 deletions(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index e0d33daa4b..0be6e0085d 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/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index e21a049719..a7a59b893e 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 6bcc454baf..7b98367702 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/Controllers/UserController.js b/src/Controllers/UserController.js index 6871add987..7618f500bf 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); + 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() - ); - } + 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 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 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', 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/Options/Definitions.js b/src/Options/Definitions.js index 3815902c51..b067412d26 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -496,6 +496,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 +548,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 847e7df944..2e1390345d 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -89,6 +89,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 +98,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 87813147f7..2008301c7d 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. */ diff --git a/src/RestWrite.js b/src/RestWrite.js index f7c6a53592..003a4a7d0a 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/PagesRouter.js b/src/Routers/PagesRouter.js index 5d5a1467a7..79a487b6e4 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 5009ee7d22..ddef76a5b8 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 feca46e802..e58f3dda6d 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 e6bd2baf718a7dee36ae67291bb4be64cf4034bf Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 20 Jun 2023 10:12:02 +0000 Subject: [PATCH 05/34] chore(release): 6.3.0-alpha.2 [skip ci] # [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)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index c7a229e5a7..14170fdf58 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index 146ac05e7e..4d04dca6cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "6.3.0-alpha.1", + "version": "6.3.0-alpha.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.3.0-alpha.1", + "version": "6.3.0-alpha.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 7a58f71970..1d57c96e8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.3.0-alpha.1", + "version": "6.3.0-alpha.2", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 1850be45b17abdccae129811157ddef6feb52a34 Mon Sep 17 00:00:00 2001 From: Parse Platform <90459499+parseplatformorg@users.noreply.github.com> Date: Thu, 22 Jun 2023 00:42:42 +0200 Subject: [PATCH 06/34] refactor: Security upgrade semver from 7.5.1 to 7.5.2 (#8650) --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d04dca6cc..0ddcbdbeb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -18178,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" }, @@ -34406,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 1d57c96e8b..9ffb91d2a6 100644 --- a/package.json +++ b/package.json @@ -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", From 068fb9e777944cf66389996b8ec0579c5e7729be Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 24 Jun 2023 00:29:54 +1000 Subject: [PATCH 07/34] refactor: Add option to convert `Parse.Object` to instance in Cloud Function payload (#8646) --- DEPRECATIONS.md | 1 + spec/CloudCode.spec.js | 20 ++++++++++++++++++++ src/Deprecator/Deprecations.js | 1 + src/Options/Definitions.js | 7 +++++++ src/Options/docs.js | 1 + src/Options/index.js | 3 +++ src/Routers/FunctionsRouter.js | 14 +++++++------- 7 files changed, 40 insertions(+), 7 deletions(-) diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 56359c937e..59b728a944 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/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index a8795a4e84..a2e623551a 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/src/Deprecator/Deprecations.js b/src/Deprecator/Deprecations.js index 0afd98ff0c..2f698ad33e 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 b067412d26..6477836eed 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', diff --git a/src/Options/docs.js b/src/Options/docs.js index 2e1390345d..fdb62bb590 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. diff --git a/src/Options/index.js b/src/Options/index.js index 2008301c7d..a8414c658c 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -202,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/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index da69d54e0c..bb4b959ebe 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, From 3ec3e40dc8bb4c157d82ba13ff301fc598bdaf7e Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:36:08 +0200 Subject: [PATCH 08/34] docs: Fix deprecation date (#8657) --- DEPRECATIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 59b728a944..b825e50b2f 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -13,7 +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 | - | +| DEPPS10 | Config option `encodeParseObjectInCloudFunction` defaults to `true` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 8.0.0 (2025) | deprecated | - | [i_deprecation]: ## "The version and date of the deprecation." [i_removal]: ## "The version and date of the planned removal." From cc079a40f6849a0e9bc6fdc811e8649ecb67b589 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 24 Jun 2023 01:57:57 +1000 Subject: [PATCH 09/34] feat: Add TOTP authentication adapter (#8457) --- package-lock.json | 35 +++- package.json | 1 + spec/AuthenticationAdapters.spec.js | 295 ++++++++++++++++++++++++++++ src/Adapters/Auth/AuthAdapter.js | 4 +- src/Adapters/Auth/index.js | 21 +- src/Adapters/Auth/mfa.js | 212 ++++++++++++++++++++ src/Auth.js | 22 ++- src/RestWrite.js | 1 + src/Routers/UsersRouter.js | 7 +- src/index.js | 1 - 10 files changed, 580 insertions(+), 19 deletions(-) create mode 100644 src/Adapters/Auth/mfa.js diff --git a/package-lock.json b/package-lock.json index 0ddcbdbeb2..8664502816 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "mime": "3.0.0", "mongodb": "4.10.0", "mustache": "4.2.0", + "otpauth": "9.0.2", "parse": "4.1.0", "path-to-regexp": "6.2.1", "pg-monitor": "2.0.0", @@ -46,7 +47,7 @@ "pluralize": "8.0.0", "rate-limit-redis": "3.0.2", "redis": "4.6.6", - "semver": "^7.5.2", + "semver": "7.5.2", "subscriptions-transport-ws": "0.11.0", "tv4": "1.3.0", "uuid": "9.0.0", @@ -10220,6 +10221,14 @@ "extsprintf": "^1.2.0" } }, + "node_modules/jssha": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.0.tgz", + "integrity": "sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w==", + "engines": { + "node": "*" + } + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -15940,6 +15949,17 @@ "node": ">=8" } }, + "node_modules/otpauth": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.0.2.tgz", + "integrity": "sha512-0TzpkJYg24VvIK3/K91HKpTtMlwm73UoThhcGY8fZsXcwHDrqf008rfdOjj3NnQuyuT11+vHyyO//qRzi6OZ9A==", + "dependencies": { + "jssha": "~3.3.0" + }, + "funding": { + "url": "https://github.com/hectorm/otpauth?sponsor=1" + } + }, "node_modules/p-cancelable": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", @@ -28397,6 +28417,11 @@ } } }, + "jssha": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.0.tgz", + "integrity": "sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w==" + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -32712,6 +32737,14 @@ } } }, + "otpauth": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.0.2.tgz", + "integrity": "sha512-0TzpkJYg24VvIK3/K91HKpTtMlwm73UoThhcGY8fZsXcwHDrqf008rfdOjj3NnQuyuT11+vHyyO//qRzi6OZ9A==", + "requires": { + "jssha": "~3.3.0" + } + }, "p-cancelable": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", diff --git a/package.json b/package.json index 9ffb91d2a6..ed291c2028 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "mime": "3.0.0", "mongodb": "4.10.0", "mustache": "4.2.0", + "otpauth": "9.0.2", "parse": "4.1.0", "path-to-regexp": "6.2.1", "pg-monitor": "2.0.0", diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index bb89596cef..e11220a63c 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -2406,3 +2406,298 @@ describe('facebook limited auth adapter', () => { } }); }); + +describe('OTP TOTP auth adatper', () => { + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + beforeEach(async () => { + await reconfigureServer({ + auth: { + mfa: { + enabled: true, + options: ['TOTP'], + algorithm: 'SHA1', + digits: 6, + period: 30, + }, + }, + }); + }); + + it('can enroll', async () => { + const user = await Parse.User.signUp('username', 'password'); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + const token = totp.generate(); + await user.save( + { authData: { mfa: { secret: secret.base32, token } } }, + { sessionToken: user.getSessionToken() } + ); + const response = user.get('authDataResponse'); + expect(response.mfa).toBeDefined(); + expect(response.mfa.recovery).toBeDefined(); + expect(response.mfa.recovery.length).toEqual(2); + await user.fetch(); + expect(user.get('authData').mfa).toEqual({ enabled: true }); + }); + + it('can login with valid token', async () => { + const user = await Parse.User.signUp('username', 'password'); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + const token = totp.generate(); + await user.save( + { authData: { mfa: { secret: secret.base32, token } } }, + { sessionToken: user.getSessionToken() } + ); + const response = await request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/login', + body: JSON.stringify({ + username: 'username', + password: 'password', + authData: { + mfa: totp.generate(), + }, + }), + }).then(res => res.data); + expect(response.objectId).toEqual(user.id); + expect(response.sessionToken).toBeDefined(); + expect(response.authData).toEqual({ mfa: { enabled: true } }); + expect(Object.keys(response).sort()).toEqual( + [ + 'objectId', + 'username', + 'createdAt', + 'updatedAt', + 'authData', + 'ACL', + 'sessionToken', + 'authDataResponse', + ].sort() + ); + }); + + it('can change OTP with valid token', async () => { + const user = await Parse.User.signUp('username', 'password'); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + const token = totp.generate(); + await user.save( + { authData: { mfa: { secret: secret.base32, token } } }, + { sessionToken: user.getSessionToken() } + ); + + const new_secret = new OTPAuth.Secret(); + const new_totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret: new_secret, + }); + const new_token = new_totp.generate(); + await user.save( + { + authData: { mfa: { secret: new_secret.base32, token: new_token, old: totp.generate() } }, + }, + { sessionToken: user.getSessionToken() } + ); + await user.fetch({ useMasterKey: true }); + expect(user.get('authData').mfa.secret).toEqual(new_secret.base32); + }); + + it('future logins require TOTP token', async () => { + const user = await Parse.User.signUp('username', 'password'); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + const token = totp.generate(); + await user.save( + { authData: { mfa: { secret: secret.base32, token } } }, + { sessionToken: user.getSessionToken() } + ); + await expectAsync(Parse.User.logIn('username', 'password')).toBeRejectedWith( + new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing additional authData mfa') + ); + }); + + it('future logins reject incorrect TOTP token', async () => { + const user = await Parse.User.signUp('username', 'password'); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + const token = totp.generate(); + await user.save( + { authData: { mfa: { secret: secret.base32, token } } }, + { sessionToken: user.getSessionToken() } + ); + await expectAsync( + request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/login', + body: JSON.stringify({ + username: 'username', + password: 'password', + authData: { + mfa: 'abcd', + }, + }), + }).catch(e => { + throw e.data; + }) + ).toBeRejectedWith({ code: Parse.Error.SCRIPT_FAILED, error: 'Invalid MFA token' }); + }); +}); + +describe('OTP SMS auth adatper', () => { + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + let code; + let mobile; + const mfa = { + enabled: true, + options: ['SMS'], + sendSMS(smsCode, number) { + expect(smsCode).toBeDefined(); + expect(number).toBeDefined(); + expect(smsCode.length).toEqual(6); + code = smsCode; + mobile = number; + }, + digits: 6, + period: 30, + }; + beforeEach(async () => { + code = ''; + mobile = ''; + await reconfigureServer({ + auth: { + mfa, + }, + }); + }); + + it('can enroll', async () => { + const user = await Parse.User.signUp('username', 'password'); + const sessionToken = user.getSessionToken(); + const spy = spyOn(mfa, 'sendSMS').and.callThrough(); + await user.save({ authData: { mfa: { mobile: '+11111111111' } } }, { sessionToken }); + await user.fetch({ sessionToken }); + expect(user.get('authData')).toEqual({ mfa: { enabled: false } }); + expect(spy).toHaveBeenCalledWith(code, '+11111111111'); + await user.fetch({ useMasterKey: true }); + const authData = user.get('authData').mfa?.pending; + expect(authData).toBeDefined(); + expect(authData['+11111111111']).toBeDefined(); + expect(Object.keys(authData['+11111111111'])).toEqual(['token', 'expiry']); + + await user.save({ authData: { mfa: { mobile, token: code } } }, { sessionToken }); + await user.fetch({ sessionToken }); + expect(user.get('authData')).toEqual({ mfa: { enabled: true } }); + }); + + it('future logins require SMS code', async () => { + const user = await Parse.User.signUp('username', 'password'); + const spy = spyOn(mfa, 'sendSMS').and.callThrough(); + await user.save( + { authData: { mfa: { mobile: '+11111111111' } } }, + { sessionToken: user.getSessionToken() } + ); + + await user.save( + { authData: { mfa: { mobile, token: code } } }, + { sessionToken: user.getSessionToken() } + ); + + spy.calls.reset(); + + await expectAsync(Parse.User.logIn('username', 'password')).toBeRejectedWith( + new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing additional authData mfa') + ); + const res = await request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/login', + body: JSON.stringify({ + username: 'username', + password: 'password', + authData: { + mfa: true, + }, + }), + }).catch(e => e.data); + expect(res).toEqual({ code: Parse.Error.SCRIPT_FAILED, error: 'Please enter the token' }); + expect(spy).toHaveBeenCalledWith(code, '+11111111111'); + const response = await request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/login', + body: JSON.stringify({ + username: 'username', + password: 'password', + authData: { + mfa: code, + }, + }), + }).then(res => res.data); + expect(response.objectId).toEqual(user.id); + expect(response.sessionToken).toBeDefined(); + expect(response.authData).toEqual({ mfa: { enabled: true } }); + expect(Object.keys(response).sort()).toEqual( + [ + 'objectId', + 'username', + 'createdAt', + 'updatedAt', + 'authData', + 'ACL', + 'sessionToken', + 'authDataResponse', + ].sort() + ); + }); + + it('partially enrolled users can still login', async () => { + const user = await Parse.User.signUp('username', 'password'); + await user.save({ authData: { mfa: { mobile: '+11111111111' } } }); + const spy = spyOn(mfa, 'sendSMS').and.callThrough(); + await Parse.User.logIn('username', 'password'); + expect(spy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/Adapters/Auth/AuthAdapter.js b/src/Adapters/Auth/AuthAdapter.js index 5b18c75170..e739df3f54 100644 --- a/src/Adapters/Auth/AuthAdapter.js +++ b/src/Adapters/Auth/AuthAdapter.js @@ -21,7 +21,9 @@ export class AuthAdapter { * Usage policy * @type {AuthPolicy} */ - this.policy = 'default'; + if (!this.policy) { + this.policy = 'default'; + } } /** * @param appIds The specified app IDs in the configuration diff --git a/src/Adapters/Auth/index.js b/src/Adapters/Auth/index.js index 2defcb0dc0..da65b24ba5 100755 --- a/src/Adapters/Auth/index.js +++ b/src/Adapters/Auth/index.js @@ -9,6 +9,7 @@ const facebook = require('./facebook'); const instagram = require('./instagram'); const linkedin = require('./linkedin'); const meetup = require('./meetup'); +import mfa from './mfa'; const google = require('./google'); const github = require('./github'); const twitter = require('./twitter'); @@ -44,6 +45,7 @@ const providers = { instagram, linkedin, meetup, + mfa, google, github, twitter, @@ -75,7 +77,11 @@ function authDataValidator(provider, adapter, appIds, options) { if (appIds && typeof adapter.validateAppId === 'function') { await Promise.resolve(adapter.validateAppId(appIds, authData, options, requestObject)); } - if (adapter.policy && !authAdapterPolicies[adapter.policy]) { + if ( + adapter.policy && + !authAdapterPolicies[adapter.policy] && + typeof adapter.policy !== 'function' + ) { throw new Parse.Error( Parse.Error.OTHER_CAUSE, 'AuthAdapter policy is not configured correctly. The value must be either "solo", "additional", "default" or undefined (will be handled as "default")' @@ -225,17 +231,20 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) { if (!authAdapter) { return; } - const { - adapter: { afterFind }, - providerOptions, - } = authAdapter; + const { adapter, providerOptions } = authAdapter; + const afterFind = adapter.afterFind; if (afterFind && typeof afterFind === 'function') { const requestObject = { ip: req.config.ip, user: req.auth.user, master: req.auth.isMaster, }; - const result = afterFind(requestObject, authData[provider], providerOptions); + const result = afterFind.call( + adapter, + requestObject, + authData[provider], + providerOptions + ); if (result) { authData[provider] = result; } diff --git a/src/Adapters/Auth/mfa.js b/src/Adapters/Auth/mfa.js new file mode 100644 index 0000000000..efcff7b633 --- /dev/null +++ b/src/Adapters/Auth/mfa.js @@ -0,0 +1,212 @@ +import { TOTP, Secret } from 'otpauth'; +import { randomString } from '../../cryptoUtils'; +import AuthAdapter from './AuthAdapter'; +class MFAAdapter extends AuthAdapter { + validateOptions(opts) { + const validOptions = opts.options; + if (!Array.isArray(validOptions)) { + throw 'mfa.options must be an array'; + } + this.sms = validOptions.includes('SMS'); + this.totp = validOptions.includes('TOTP'); + if (!this.sms && !this.totp) { + throw 'mfa.options must include SMS or TOTP'; + } + const digits = opts.digits || 6; + const period = opts.period || 30; + if (typeof digits !== 'number') { + throw 'mfa.digits must be a number'; + } + if (typeof period !== 'number') { + throw 'mfa.period must be a number'; + } + if (digits < 4 || digits > 10) { + throw 'mfa.digits must be between 4 and 10'; + } + if (period < 10) { + throw 'mfa.period must be greater than 10'; + } + const sendSMS = opts.sendSMS; + if (this.sms && typeof sendSMS !== 'function') { + throw 'mfa.sendSMS callback must be defined when using SMS OTPs'; + } + this.smsCallback = sendSMS; + this.digits = digits; + this.period = period; + this.algorithm = opts.algorithm || 'SHA1'; + } + validateSetUp(mfaData) { + if (mfaData.mobile && this.sms) { + return this.setupMobileOTP(mfaData.mobile); + } + if (this.totp) { + return this.setupTOTP(mfaData); + } + throw 'Invalid MFA data'; + } + async validateLogin(token, _, req) { + const saveResponse = { + doNotSave: true, + }; + const auth = req.original.get('authData') || {}; + const { secret, recovery, mobile, token: saved, expiry } = auth.mfa || {}; + if (this.sms && mobile) { + if (typeof token === 'boolean') { + const { token: sendToken, expiry } = await this.sendSMS(mobile); + auth.mfa.token = sendToken; + auth.mfa.expiry = expiry; + req.object.set('authData', auth); + await req.object.save(null, { useMasterKey: true }); + throw 'Please enter the token'; + } + if (!saved || token !== saved) { + throw 'Invalid MFA token 1'; + } + if (new Date() > expiry) { + throw 'Invalid MFA token 2'; + } + delete auth.mfa.token; + delete auth.mfa.expiry; + return { + save: auth.mfa, + }; + } + if (this.totp) { + if (typeof token !== 'string') { + throw 'Invalid MFA token'; + } + if (!secret) { + return saveResponse; + } + if (recovery[0] === token || recovery[1] === token) { + return saveResponse; + } + const totp = new TOTP({ + algorithm: this.algorithm, + digits: this.digits, + period: this.period, + secret: Secret.fromBase32(secret), + }); + const valid = totp.validate({ + token, + }); + if (valid === null) { + throw 'Invalid MFA token'; + } + } + return saveResponse; + } + validateUpdate(authData, _, req) { + if (req.master) { + return; + } + if (authData.mobile && this.sms) { + if (!authData.token) { + throw 'MFA is already set up on this account'; + } + return this.confirmSMSOTP(authData, req.original.get('authData')?.mfa || {}); + } + if (this.totp) { + this.validateLogin(authData.old, null, req); + return this.validateSetUp(authData); + } + throw 'Invalid MFA data'; + } + afterFind(req, authData) { + if (req.master) { + return; + } + if (this.totp && authData.secret) { + return { + enabled: true, + }; + } + if (this.sms && authData.mobile) { + return { + enabled: true, + }; + } + return { + enabled: false, + }; + } + + policy(req, auth) { + if (this.sms && auth?.pending && Object.keys(auth).length === 1) { + return 'default'; + } + return 'additional'; + } + + async setupMobileOTP(mobile) { + const { token, expiry } = await this.sendSMS(mobile); + return { + save: { + pending: { + [mobile]: { + token, + expiry, + }, + }, + }, + }; + } + + async sendSMS(mobile) { + if (!/^[+]*[(]{0,1}[0-9]{1,3}[)]{0,1}[-\s\./0-9]*$/g.test(mobile)) { + throw 'Invalid mobile number.'; + } + let token = ''; + while (token.length < this.digits) { + token += randomString(10).replace(/\D/g, ''); + } + token = token.substring(0, this.digits); + await Promise.resolve(this.smsCallback(token, mobile)); + const expiry = new Date(new Date().getTime() + this.period * 1000); + return { token, expiry }; + } + + async confirmSMSOTP(inputData, authData) { + const { mobile, token } = inputData; + if (!authData.pending?.[mobile]) { + throw 'This number is not pending'; + } + const pendingData = authData.pending[mobile]; + if (token !== pendingData.token) { + throw 'Invalid MFA token'; + } + if (new Date() > pendingData.expiry) { + throw 'Invalid MFA token'; + } + delete authData.pending[mobile]; + authData.mobile = mobile; + return { + save: authData, + }; + } + + setupTOTP(mfaData) { + const { secret, token } = mfaData; + if (!secret || !token || secret.length < 20) { + throw 'Invalid MFA data'; + } + const totp = new TOTP({ + algorithm: this.algorithm, + digits: this.digits, + period: this.period, + secret: Secret.fromBase32(secret), + }); + const valid = totp.validate({ + token, + }); + if (valid === null) { + throw 'Invalid MFA token'; + } + const recovery = [randomString(30), randomString(30)]; + return { + response: { recovery }, + save: { secret, recovery }, + }; + } +} +export default new MFAAdapter(); diff --git a/src/Auth.js b/src/Auth.js index 96c99cbb1d..0fe3b54460 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -407,6 +407,7 @@ const hasMutatedAuthData = (authData, userAuthData) => { }; const checkIfUserHasProvidedConfiguredProvidersForLogin = ( + req = {}, authData = {}, userAuthData = {}, config @@ -430,7 +431,16 @@ const checkIfUserHasProvidedConfiguredProvidersForLogin = ( const additionProvidersNotFound = []; const hasProvidedAtLeastOneAdditionalProvider = savedUserProviders.some(provider => { - if (provider && provider.adapter && provider.adapter.policy === 'additional') { + let policy = provider.adapter.policy; + if (typeof policy === 'function') { + const requestObject = { + ip: req.config.ip, + user: req.auth.user, + master: req.auth.isMaster, + }; + policy = policy.call(provider.adapter, requestObject, userAuthData[provider.name]); + } + if (policy === 'additional') { if (authData[provider.name]) { return true; } else { @@ -467,14 +477,8 @@ const handleAuthDataValidation = async (authData, req, foundUser) => { await user.fetch({ useMasterKey: true }); } - const { originalObject, updatedObject } = req.buildParseObjects(); - const requestObject = getRequestObject( - undefined, - req.auth, - updatedObject, - originalObject || user, - req.config - ); + const { updatedObject } = req.buildParseObjects(); + const requestObject = getRequestObject(undefined, req.auth, updatedObject, user, req.config); // Perform validation as step-by-step pipeline for better error consistency // and also to avoid to trigger a provider (like OTP SMS) if another one fails const acc = { authData: {}, authDataResponse: {} }; diff --git a/src/RestWrite.js b/src/RestWrite.js index 003a4a7d0a..a707e27f7f 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -556,6 +556,7 @@ RestWrite.prototype.handleAuthData = async function (authData) { // we need to be sure that the user has provided // required authData Auth.checkIfUserHasProvidedConfiguredProvidersForLogin( + { config: this.config, auth: this.auth }, authData, userResult.authData, this.config diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index e58f3dda6d..14131cf5f1 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -189,7 +189,12 @@ export class UsersRouter extends ClassesRouter { const user = await this._authenticateUserFromRequest(req); const authData = req.body && req.body.authData; // Check if user has provided their required auth providers - Auth.checkIfUserHasProvidedConfiguredProvidersForLogin(authData, user.authData, req.config); + Auth.checkIfUserHasProvidedConfiguredProvidersForLogin( + req, + authData, + user.authData, + req.config + ); let authDataResponse; let validatedAuthData; diff --git a/src/index.js b/src/index.js index dcfe9b4c7e..0c9069d6b5 100644 --- a/src/index.js +++ b/src/index.js @@ -7,7 +7,6 @@ import LRUCacheAdapter from './Adapters/Cache/LRUCache.js'; import * as TestUtils from './TestUtils'; import * as SchemaMigrations from './SchemaMigrations/Migrations'; import AuthAdapter from './Adapters/Auth/AuthAdapter'; - import { useExternal } from './deprecated'; import { getLogger } from './logger'; import { PushWorker } from './Push/PushWorker'; From f8b5a99d54e7d74e8608be8f0fc5266929039d36 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 23 Jun 2023 15:59:00 +0000 Subject: [PATCH 10/34] chore(release): 6.3.0-alpha.3 [skip ci] # [6.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.2...6.3.0-alpha.3) (2023-06-23) ### Features * Add TOTP authentication adapter ([#8457](https://github.com/parse-community/parse-server/issues/8457)) ([cc079a4](https://github.com/parse-community/parse-server/commit/cc079a40f6849a0e9bc6fdc811e8649ecb67b589)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 14170fdf58..6290dc014f 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [6.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.2...6.3.0-alpha.3) (2023-06-23) + + +### Features + +* Add TOTP authentication adapter ([#8457](https://github.com/parse-community/parse-server/issues/8457)) ([cc079a4](https://github.com/parse-community/parse-server/commit/cc079a40f6849a0e9bc6fdc811e8649ecb67b589)) + # [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) diff --git a/package-lock.json b/package-lock.json index 8664502816..5c58f8999f 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-alpha.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.3.0-alpha.2", + "version": "6.3.0-alpha.3", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index ed291c2028..ca9f2369a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.3.0-alpha.2", + "version": "6.3.0-alpha.3", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 31805c96ec6a0ab9b33151e14b3e942f8ee86299 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Wed, 28 Jun 2023 23:38:14 +0200 Subject: [PATCH 11/34] 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) (#8676) --- .eslintrc.json | 3 ++ spec/vulnerabilities.spec.js | 65 +++++++++++++++++++++++++++ src/Controllers/DatabaseController.js | 10 +++++ src/RestWrite.js | 23 +++------- src/Routers/FilesRouter.js | 21 +++------ src/Utils.js | 12 +++++ 6 files changed, 101 insertions(+), 33 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 5995321633..c04e2d3109 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 5c83493c94..c499eb015f 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 e3ac5723ab..435095fb76 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 a707e27f7f..d35e52d6b5 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); + } }); }; @@ -1798,20 +1800,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 ed48a28a68..3b42d883d3 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -175,22 +175,13 @@ 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) { + try { // 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; - } - } + 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 d5a255a5ca..efeae58f3f 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 3f03bd3c6f27cc1edb7174d06645f517830790fd Mon Sep 17 00:00:00 2001 From: Corey Date: Thu, 29 Jun 2023 19:15:58 -0400 Subject: [PATCH 12/34] refactor: Remove duplicate user index creation (#8662) --- spec/DefinedSchemas.spec.js | 2 +- src/Controllers/DatabaseController.js | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/spec/DefinedSchemas.spec.js b/spec/DefinedSchemas.spec.js index 8d8853766e..a1c7e3dca5 100644 --- a/spec/DefinedSchemas.spec.js +++ b/spec/DefinedSchemas.spec.js @@ -554,7 +554,7 @@ describe('DefinedSchemas', () => { }); }); - it('should not delete automatically classes', async () => { + it('should not delete classes automatically', async () => { await reconfigureServer({ schema: { definitions: [{ className: '_User' }, { className: 'Test' }] }, }); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 435095fb76..f9c782db87 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1716,12 +1716,6 @@ class DatabaseController { throw error; }); - await this.adapter - .ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true) - .catch(error => { - logger.warn('Unable to create case insensitive username index: ', error); - throw error; - }); await this.adapter .ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true) .catch(error => { From 446cbb1a319ddf2e67c013fdb680faf9a3c22df2 Mon Sep 17 00:00:00 2001 From: Parse Platform <90459499+parseplatformorg@users.noreply.github.com> Date: Fri, 30 Jun 2023 02:29:08 +0200 Subject: [PATCH 13/34] refactor: Upgrade otpauth from 9.0.2 to 9.1.2 (#8668) --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5c58f8999f..4347039112 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "mime": "3.0.0", "mongodb": "4.10.0", "mustache": "4.2.0", - "otpauth": "9.0.2", + "otpauth": "^9.1.2", "parse": "4.1.0", "path-to-regexp": "6.2.1", "pg-monitor": "2.0.0", @@ -15950,9 +15950,9 @@ } }, "node_modules/otpauth": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.0.2.tgz", - "integrity": "sha512-0TzpkJYg24VvIK3/K91HKpTtMlwm73UoThhcGY8fZsXcwHDrqf008rfdOjj3NnQuyuT11+vHyyO//qRzi6OZ9A==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.1.2.tgz", + "integrity": "sha512-iI5nlVvMFP3aTPdjG/fnC4mhVJ/KZOSnBrvo/VnYHUwlTp9jVLjAe2B3i3pyCH+3/E5jYQRSvuHk/8oas3870g==", "dependencies": { "jssha": "~3.3.0" }, @@ -32738,9 +32738,9 @@ } }, "otpauth": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.0.2.tgz", - "integrity": "sha512-0TzpkJYg24VvIK3/K91HKpTtMlwm73UoThhcGY8fZsXcwHDrqf008rfdOjj3NnQuyuT11+vHyyO//qRzi6OZ9A==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.1.2.tgz", + "integrity": "sha512-iI5nlVvMFP3aTPdjG/fnC4mhVJ/KZOSnBrvo/VnYHUwlTp9jVLjAe2B3i3pyCH+3/E5jYQRSvuHk/8oas3870g==", "requires": { "jssha": "~3.3.0" } diff --git a/package.json b/package.json index ca9f2369a1..6d326313b0 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "mime": "3.0.0", "mongodb": "4.10.0", "mustache": "4.2.0", - "otpauth": "9.0.2", + "otpauth": "9.1.2", "parse": "4.1.0", "path-to-regexp": "6.2.1", "pg-monitor": "2.0.0", From 4e2000bc563324389584ace3c090a5c1a7796a64 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 4 Jul 2023 21:16:55 +1000 Subject: [PATCH 14/34] fix: Server does not start via CLI when `auth` option is set (#8666) --- spec/CLI.spec.js | 21 +++++++++++++++++++++ spec/configs/CLIConfigAuth.json | 11 +++++++++++ src/Options/Definitions.js | 1 - src/Options/docs.js | 2 +- src/Options/index.js | 2 +- 5 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 spec/configs/CLIConfigAuth.json diff --git a/spec/CLI.spec.js b/spec/CLI.spec.js index 20667fd349..d73b854a9b 100644 --- a/spec/CLI.spec.js +++ b/spec/CLI.spec.js @@ -302,4 +302,25 @@ describe('execution', () => { done.fail(data.toString()); }); }); + + it('can start Parse Server with auth via CLI', done => { + const env = { ...process.env }; + env.NODE_OPTIONS = '--dns-result-order=ipv4first'; + childProcess = spawn( + binPath, + ['--databaseURI', databaseURI, './spec/configs/CLIConfigAuth.json'], + { env } + ); + childProcess.stdout.on('data', data => { + data = data.toString(); + console.log(data); + if (data.includes('parse-server running on')) { + done(); + } + }); + childProcess.stderr.on('data', data => { + data = data.toString(); + done.fail(data.toString()); + }); + }); }); diff --git a/spec/configs/CLIConfigAuth.json b/spec/configs/CLIConfigAuth.json new file mode 100644 index 0000000000..37a2a5f373 --- /dev/null +++ b/spec/configs/CLIConfigAuth.json @@ -0,0 +1,11 @@ +{ + "appName": "test", + "appId": "test", + "masterKey": "test", + "logLevel": "error", + "auth": { + "facebook": { + "appIds": "test" + } + } +} diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 6477836eed..7a1e56bad0 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -103,7 +103,6 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_AUTH_PROVIDERS', help: 'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication', - action: parsers.arrayParser, }, cacheAdapter: { env: 'PARSE_SERVER_CACHE_ADAPTER', diff --git a/src/Options/docs.js b/src/Options/docs.js index fdb62bb590..09e6f5b3b4 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -20,7 +20,7 @@ * @property {Adapter} analyticsAdapter Adapter module for the analytics * @property {String} appId Your Parse Application ID * @property {String} appName Sets the app name - * @property {AuthAdapter[]} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication + * @property {Object} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication * @property {Adapter} cacheAdapter Adapter module for the cache * @property {Number} cacheMaxSize Sets the maximum size for the in memory cache, defaults to 10000 * @property {Number} cacheTTL Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds) diff --git a/src/Options/index.js b/src/Options/index.js index a8414c658c..d501b996dd 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -149,7 +149,7 @@ export interface ParseServerOptions { allowCustomObjectId: ?boolean; /* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication :ENV: PARSE_SERVER_AUTH_PROVIDERS */ - auth: ?(AuthAdapter[]); + auth: ?{ [string]: AuthAdapter }; /* Max file size for uploads, defaults to 20mb :DEFAULT: 20mb */ maxUploadSize: ?string; From a742656a90a3db73b5d2087c4643a144637ae6bb Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 4 Jul 2023 11:18:02 +0000 Subject: [PATCH 15/34] chore(release): 6.3.0-alpha.4 [skip ci] # [6.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.3...6.3.0-alpha.4) (2023-07-04) ### Bug Fixes * Server does not start via CLI when `auth` option is set ([#8666](https://github.com/parse-community/parse-server/issues/8666)) ([4e2000b](https://github.com/parse-community/parse-server/commit/4e2000bc563324389584ace3c090a5c1a7796a64)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 6290dc014f..2aa4524b40 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [6.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.3...6.3.0-alpha.4) (2023-07-04) + + +### Bug Fixes + +* Server does not start via CLI when `auth` option is set ([#8666](https://github.com/parse-community/parse-server/issues/8666)) ([4e2000b](https://github.com/parse-community/parse-server/commit/4e2000bc563324389584ace3c090a5c1a7796a64)) + # [6.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.2...6.3.0-alpha.3) (2023-06-23) diff --git a/package-lock.json b/package-lock.json index 4347039112..f69f710557 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "6.3.0-alpha.3", + "version": "6.3.0-alpha.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.3.0-alpha.3", + "version": "6.3.0-alpha.4", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 6d326313b0..c8f80b20a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.3.0-alpha.3", + "version": "6.3.0-alpha.4", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From a9d376b61f5b07806eafbda91c4e36c322f09298 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 6 Jul 2023 06:11:35 +1000 Subject: [PATCH 16/34] feat: Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code (#8670) --- spec/CloudCode.spec.js | 8 ++++++++ src/Config.js | 2 ++ 2 files changed, 10 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index a2e623551a..90ab313826 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -103,6 +103,14 @@ describe('Cloud Code', () => { expect(currentConfig.silent).toBeFalse(); }); + it('can get curent version', () => { + const version = require('../package.json').version; + const currentConfig = Config.get('test'); + expect(Parse.Server.version).toBeDefined(); + expect(currentConfig.version).toBeDefined(); + expect(Parse.Server.version).toEqual(version); + }); + it('show warning on duplicate cloud functions', done => { const logger = require('../lib/logger').logger; spyOn(logger, 'warn').and.callFake(() => {}); diff --git a/src/Config.js b/src/Config.js index 8fe10a9a1c..0e8cdda246 100644 --- a/src/Config.js +++ b/src/Config.js @@ -7,6 +7,7 @@ import net from 'net'; import AppCache from './cache'; import DatabaseController from './Controllers/DatabaseController'; import { logLevels as validLogLevels } from './Controllers/LoggerController'; +import { version } from '../package.json'; import { AccountLockoutOptions, DatabaseOptions, @@ -50,6 +51,7 @@ export class Config { config.generateEmailVerifyTokenExpiresAt = config.generateEmailVerifyTokenExpiresAt.bind( config ); + config.version = version; return config; } From 02f40fd89660272199d77da8af31246e88d42394 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 5 Jul 2023 20:12:35 +0000 Subject: [PATCH 17/34] chore(release): 6.3.0-alpha.5 [skip ci] # [6.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.4...6.3.0-alpha.5) (2023-07-05) ### Features * Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code ([#8670](https://github.com/parse-community/parse-server/issues/8670)) ([a9d376b](https://github.com/parse-community/parse-server/commit/a9d376b61f5b07806eafbda91c4e36c322f09298)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 2aa4524b40..850dc921a9 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [6.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.4...6.3.0-alpha.5) (2023-07-05) + + +### Features + +* Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code ([#8670](https://github.com/parse-community/parse-server/issues/8670)) ([a9d376b](https://github.com/parse-community/parse-server/commit/a9d376b61f5b07806eafbda91c4e36c322f09298)) + # [6.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.3...6.3.0-alpha.4) (2023-07-04) diff --git a/package-lock.json b/package-lock.json index f69f710557..becc9f864d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "6.3.0-alpha.4", + "version": "6.3.0-alpha.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.3.0-alpha.4", + "version": "6.3.0-alpha.5", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index c8f80b20a9..09c0d8bc60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.3.0-alpha.4", + "version": "6.3.0-alpha.5", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From c9b59719ec9648f9ba8bbef89db2a24a227ae55c Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 7 Jul 2023 01:22:18 +1000 Subject: [PATCH 18/34] refactor: Change response types of TOTP adapter to match existing adapters (#8661) --- spec/AuthenticationAdapters.spec.js | 64 ++++++++++++++++++++++++----- src/Adapters/Auth/mfa.js | 17 ++++---- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index e11220a63c..4a93ace3a3 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -2445,9 +2445,9 @@ describe('OTP TOTP auth adatper', () => { const response = user.get('authDataResponse'); expect(response.mfa).toBeDefined(); expect(response.mfa.recovery).toBeDefined(); - expect(response.mfa.recovery.length).toEqual(2); + expect(response.mfa.recovery.split(',').length).toEqual(2); await user.fetch(); - expect(user.get('authData').mfa).toEqual({ enabled: true }); + expect(user.get('authData').mfa).toEqual({ status: 'enabled' }); }); it('can login with valid token', async () => { @@ -2473,13 +2473,15 @@ describe('OTP TOTP auth adatper', () => { username: 'username', password: 'password', authData: { - mfa: totp.generate(), + mfa: { + token: totp.generate(), + }, }, }), }).then(res => res.data); expect(response.objectId).toEqual(user.id); expect(response.sessionToken).toBeDefined(); - expect(response.authData).toEqual({ mfa: { enabled: true } }); + expect(response.authData).toEqual({ mfa: { status: 'enabled' } }); expect(Object.keys(response).sort()).toEqual( [ 'objectId', @@ -2528,6 +2530,42 @@ describe('OTP TOTP auth adatper', () => { expect(user.get('authData').mfa.secret).toEqual(new_secret.base32); }); + it('cannot change OTP with invalid token', async () => { + const user = await Parse.User.signUp('username', 'password'); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + const token = totp.generate(); + await user.save( + { authData: { mfa: { secret: secret.base32, token } } }, + { sessionToken: user.getSessionToken() } + ); + + const new_secret = new OTPAuth.Secret(); + const new_totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret: new_secret, + }); + const new_token = new_totp.generate(); + await expectAsync( + user.save( + { + authData: { mfa: { secret: new_secret.base32, token: new_token, old: '123' } }, + }, + { sessionToken: user.getSessionToken() } + ) + ).toBeRejectedWith(new Parse.Error(Parse.Error.OTHER_CAUSE, 'Invalid MFA token')); + await user.fetch({ useMasterKey: true }); + expect(user.get('authData').mfa.secret).toEqual(secret.base32); + }); + it('future logins require TOTP token', async () => { const user = await Parse.User.signUp('username', 'password'); const OTPAuth = require('otpauth'); @@ -2572,7 +2610,9 @@ describe('OTP TOTP auth adatper', () => { username: 'username', password: 'password', authData: { - mfa: 'abcd', + mfa: { + token: 'abcd', + }, }, }), }).catch(e => { @@ -2619,7 +2659,7 @@ describe('OTP SMS auth adatper', () => { const spy = spyOn(mfa, 'sendSMS').and.callThrough(); await user.save({ authData: { mfa: { mobile: '+11111111111' } } }, { sessionToken }); await user.fetch({ sessionToken }); - expect(user.get('authData')).toEqual({ mfa: { enabled: false } }); + expect(user.get('authData')).toEqual({ mfa: { status: 'disabled' } }); expect(spy).toHaveBeenCalledWith(code, '+11111111111'); await user.fetch({ useMasterKey: true }); const authData = user.get('authData').mfa?.pending; @@ -2629,7 +2669,7 @@ describe('OTP SMS auth adatper', () => { await user.save({ authData: { mfa: { mobile, token: code } } }, { sessionToken }); await user.fetch({ sessionToken }); - expect(user.get('authData')).toEqual({ mfa: { enabled: true } }); + expect(user.get('authData')).toEqual({ mfa: { status: 'enabled' } }); }); it('future logins require SMS code', async () => { @@ -2658,7 +2698,9 @@ describe('OTP SMS auth adatper', () => { username: 'username', password: 'password', authData: { - mfa: true, + mfa: { + token: 'request', + }, }, }), }).catch(e => e.data); @@ -2672,13 +2714,15 @@ describe('OTP SMS auth adatper', () => { username: 'username', password: 'password', authData: { - mfa: code, + mfa: { + token: code, + }, }, }), }).then(res => res.data); expect(response.objectId).toEqual(user.id); expect(response.sessionToken).toBeDefined(); - expect(response.authData).toEqual({ mfa: { enabled: true } }); + expect(response.authData).toEqual({ mfa: { status: 'enabled' } }); expect(Object.keys(response).sort()).toEqual( [ 'objectId', diff --git a/src/Adapters/Auth/mfa.js b/src/Adapters/Auth/mfa.js index efcff7b633..a88eda99e7 100644 --- a/src/Adapters/Auth/mfa.js +++ b/src/Adapters/Auth/mfa.js @@ -44,14 +44,15 @@ class MFAAdapter extends AuthAdapter { } throw 'Invalid MFA data'; } - async validateLogin(token, _, req) { + async validateLogin(loginData, _, req) { const saveResponse = { doNotSave: true, }; + const token = loginData.token; const auth = req.original.get('authData') || {}; const { secret, recovery, mobile, token: saved, expiry } = auth.mfa || {}; if (this.sms && mobile) { - if (typeof token === 'boolean') { + if (token === 'request') { const { token: sendToken, expiry } = await this.sendSMS(mobile); auth.mfa.token = sendToken; auth.mfa.expiry = expiry; @@ -96,7 +97,7 @@ class MFAAdapter extends AuthAdapter { } return saveResponse; } - validateUpdate(authData, _, req) { + async validateUpdate(authData, _, req) { if (req.master) { return; } @@ -107,7 +108,7 @@ class MFAAdapter extends AuthAdapter { return this.confirmSMSOTP(authData, req.original.get('authData')?.mfa || {}); } if (this.totp) { - this.validateLogin(authData.old, null, req); + await this.validateLogin({ token: authData.old }, null, req); return this.validateSetUp(authData); } throw 'Invalid MFA data'; @@ -118,16 +119,16 @@ class MFAAdapter extends AuthAdapter { } if (this.totp && authData.secret) { return { - enabled: true, + status: 'enabled', }; } if (this.sms && authData.mobile) { return { - enabled: true, + status: 'enabled', }; } return { - enabled: false, + status: 'disabled', }; } @@ -204,7 +205,7 @@ class MFAAdapter extends AuthAdapter { } const recovery = [randomString(30), randomString(30)]; return { - response: { recovery }, + response: { recovery: recovery.join(', ') }, save: { secret, recovery }, }; } From 6a4a00ca7af1163ea74b047b85cd6817366b824b Mon Sep 17 00:00:00 2001 From: Bartosz Marganiec Date: Tue, 18 Jul 2023 00:34:46 +0200 Subject: [PATCH 19/34] fix: Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions (#8688) --- spec/ParseFile.spec.js | 26 +++++++++++++++++++++++++- src/Routers/FilesRouter.js | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index eeab537008..f083c90ae4 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -1368,7 +1368,7 @@ describe('Parse.File testing', () => { await reconfigureServer({ fileUpload: { enableForPublic: true, - fileExtensions: ['jpg'], + fileExtensions: ['jpg', 'wav'], }, }); await expectAsync( @@ -1387,6 +1387,30 @@ describe('Parse.File testing', () => { ).toBeRejectedWith( new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`) ); + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/files/file', + body: JSON.stringify({ + _ApplicationId: 'test', + _JavaScriptKey: 'test', + _ContentType: 'image/jpg', + base64: 'PGh0bWw+PC9odG1sPgo=', + }), + }) + ).toBeResolved(); + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/files/file', + body: JSON.stringify({ + _ApplicationId: 'test', + _JavaScriptKey: 'test', + _ContentType: 'audio/wav', + base64: 'UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA', + }), + }) + ).toBeResolved(); }); it('works with array without Content-Type', async () => { diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 3b42d883d3..cbb59fdcdd 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -147,7 +147,7 @@ export class FilesRouter { if (ext === '*') { return true; } - const regex = new RegExp(fileExtensions); + const regex = new RegExp(ext); if (regex.test(extension)) { return true; } From 95da5d64c182f8b05bf3962e8ee64a7c03b209db Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 17 Jul 2023 22:35:58 +0000 Subject: [PATCH 20/34] chore(release): 6.3.0-alpha.6 [skip ci] # [6.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.5...6.3.0-alpha.6) (2023-07-17) ### Bug Fixes * Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions ([#8688](https://github.com/parse-community/parse-server/issues/8688)) ([6a4a00c](https://github.com/parse-community/parse-server/commit/6a4a00ca7af1163ea74b047b85cd6817366b824b)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 850dc921a9..66e245f2e0 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [6.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.5...6.3.0-alpha.6) (2023-07-17) + + +### Bug Fixes + +* Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions ([#8688](https://github.com/parse-community/parse-server/issues/8688)) ([6a4a00c](https://github.com/parse-community/parse-server/commit/6a4a00ca7af1163ea74b047b85cd6817366b824b)) + # [6.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.4...6.3.0-alpha.5) (2023-07-05) diff --git a/package-lock.json b/package-lock.json index becc9f864d..940f05f87c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "6.3.0-alpha.5", + "version": "6.3.0-alpha.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.3.0-alpha.5", + "version": "6.3.0-alpha.6", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 09c0d8bc60..a505c743b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.3.0-alpha.5", + "version": "6.3.0-alpha.6", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 7a198f4eed2040f649614e748b57490d40c14ba3 Mon Sep 17 00:00:00 2001 From: Parse Platform <90459499+parseplatformorg@users.noreply.github.com> Date: Mon, 7 Aug 2023 01:39:05 +0200 Subject: [PATCH 21/34] refactor: Security upgrade @parse/push-adapter from 4.1.3 to 4.2.0 (#8707) --- package-lock.json | 165 ++++++++++------------------------------------ package.json | 2 +- 2 files changed, 35 insertions(+), 132 deletions(-) diff --git a/package-lock.json b/package-lock.json index 940f05f87c..a0f1298abb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@graphql-tools/utils": "8.12.0", "@graphql-yoga/node": "2.6.0", "@parse/fs-files-adapter": "1.2.2", - "@parse/push-adapter": "4.1.3", + "@parse/push-adapter": "^4.2.0", "bcryptjs": "2.4.3", "body-parser": "1.20.2", "commander": "10.0.1", @@ -39,7 +39,7 @@ "mime": "3.0.0", "mongodb": "4.10.0", "mustache": "4.2.0", - "otpauth": "^9.1.2", + "otpauth": "9.1.2", "parse": "4.1.0", "path-to-regexp": "6.2.1", "pg-monitor": "2.0.0", @@ -2731,13 +2731,13 @@ "integrity": "sha512-VUsVZXgt53FULqUd9xqGDW6RXes62qHXTNOeRSlS1MOemiCdtQOUGgLHgjdYQXnZ1hPLkxZKph96AluZUb953g==" }, "node_modules/@parse/node-apn": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.1.3.tgz", - "integrity": "sha512-Bwhmbm895lEIF2772PJ8dSvBjrtOG9/q/TDMxmX40IgZxQFoXS73+JUIKTq3CA7SUB/Szu5roJINQ0L2U/1MJw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.2.1.tgz", + "integrity": "sha512-dwVCDv+G9YV01Ad1XslWQImnmfFDSnaNwxI4l+vuCjL+DbjsCl6DuV4nMqZpEZOpViAY0pGCRHBKUygsf+aAGg==", "dependencies": { "debug": "4.3.3", - "jsonwebtoken": "8.5.1", - "node-forge": "1.3.0", + "jsonwebtoken": "9.0.0", + "node-forge": "1.3.1", "verror": "1.10.1" }, "engines": { @@ -2760,35 +2760,6 @@ } } }, - "node_modules/@parse/node-apn/node_modules/jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=4", - "npm": ">=1.4.28" - } - }, - "node_modules/@parse/node-apn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/@parse/node-gcm": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@parse/node-gcm/-/node-gcm-1.0.2.tgz", @@ -2811,11 +2782,11 @@ } }, "node_modules/@parse/push-adapter": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-4.1.3.tgz", - "integrity": "sha512-Oy53ag7DpUva5dUWwP6tNEsrxv2xU9QIk+rb84q1DIm1qVgo2yl4oXcZ3FPG2Ks/NYURbv4w+z9oaSgVfyBRfQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-4.2.0.tgz", + "integrity": "sha512-M6D9qk4KE9bJ2lMufTvgGmKOvsbj20lFhzg0kQRmHU10ootKt4XcL+QJRSTu/BmlRbIVZMGQEZ61UyUumWTOiQ==", "dependencies": { - "@parse/node-apn": "5.1.3", + "@parse/node-apn": "5.2.1", "@parse/node-gcm": "1.0.2", "npmlog": "4.1.2", "parse": "3.4.0" @@ -10800,41 +10771,23 @@ "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", "dev": true }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, "node_modules/lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "dev": true }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true }, "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true }, "node_modules/lodash.map": { "version": "4.6.0", @@ -10847,11 +10800,6 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, "node_modules/lodash.pad": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz", @@ -12493,9 +12441,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", - "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "engines": { "node": ">= 6.13.0" } @@ -22604,13 +22552,13 @@ "integrity": "sha512-VUsVZXgt53FULqUd9xqGDW6RXes62qHXTNOeRSlS1MOemiCdtQOUGgLHgjdYQXnZ1hPLkxZKph96AluZUb953g==" }, "@parse/node-apn": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.1.3.tgz", - "integrity": "sha512-Bwhmbm895lEIF2772PJ8dSvBjrtOG9/q/TDMxmX40IgZxQFoXS73+JUIKTq3CA7SUB/Szu5roJINQ0L2U/1MJw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.2.1.tgz", + "integrity": "sha512-dwVCDv+G9YV01Ad1XslWQImnmfFDSnaNwxI4l+vuCjL+DbjsCl6DuV4nMqZpEZOpViAY0pGCRHBKUygsf+aAGg==", "requires": { "debug": "4.3.3", - "jsonwebtoken": "8.5.1", - "node-forge": "1.3.0", + "jsonwebtoken": "9.0.0", + "node-forge": "1.3.1", "verror": "1.10.1" }, "dependencies": { @@ -22621,28 +22569,6 @@ "requires": { "ms": "2.1.2" } - }, - "jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", - "requires": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^5.6.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, @@ -22667,11 +22593,11 @@ } }, "@parse/push-adapter": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-4.1.3.tgz", - "integrity": "sha512-Oy53ag7DpUva5dUWwP6tNEsrxv2xU9QIk+rb84q1DIm1qVgo2yl4oXcZ3FPG2Ks/NYURbv4w+z9oaSgVfyBRfQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-4.2.0.tgz", + "integrity": "sha512-M6D9qk4KE9bJ2lMufTvgGmKOvsbj20lFhzg0kQRmHU10ootKt4XcL+QJRSTu/BmlRbIVZMGQEZ61UyUumWTOiQ==", "requires": { - "@parse/node-apn": "5.1.3", + "@parse/node-apn": "5.2.1", "@parse/node-gcm": "1.0.2", "npmlog": "4.1.2", "parse": "3.4.0" @@ -28885,41 +28811,23 @@ "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", "dev": true }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, "lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "dev": true }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true }, "lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true }, "lodash.map": { "version": "4.6.0", @@ -28932,11 +28840,6 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, "lodash.pad": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz", @@ -30231,9 +30134,9 @@ } }, "node-forge": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", - "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" }, "node-netstat": { "version": "1.8.0", diff --git a/package.json b/package.json index a505c743b5..18119f4bd6 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@graphql-tools/utils": "8.12.0", "@graphql-yoga/node": "2.6.0", "@parse/fs-files-adapter": "1.2.2", - "@parse/push-adapter": "4.1.3", + "@parse/push-adapter": "4.2.0", "bcryptjs": "2.4.3", "body-parser": "1.20.2", "commander": "10.0.1", From ae68f0c31b741eeb83379c905c7ddfaa124436ec Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 18 Aug 2023 02:11:24 +0200 Subject: [PATCH 22/34] fix: Remove config logging when launching Parse Server via CLI (#8710) --- src/cli/utils/runner.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/cli/utils/runner.js b/src/cli/utils/runner.js index d74a7a5928..4869111d64 100644 --- a/src/cli/utils/runner.js +++ b/src/cli/utils/runner.js @@ -1,13 +1,20 @@ import program from './commander'; function logStartupOptions(options) { + if (!options.verbose) { + return; + } + // Keys that may include sensitive information that will be redacted in logs + const keysToRedact = [ + 'databaseURI', + 'masterKey', + 'maintenanceKey', + 'push', + ]; for (const key in options) { let value = options[key]; - if (key == 'masterKey') { - value = '***REDACTED***'; - } - if (key == 'push' && options.verbose != true) { - value = '***REDACTED***'; + if (keysToRedact.includes(key)) { + value = ''; } if (typeof value === 'object') { try { From b9bdca452022ea31f0522f8be5d36d798b92c33c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 18 Aug 2023 00:24:55 +0000 Subject: [PATCH 23/34] chore(release): 6.3.0-alpha.7 [skip ci] # [6.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.6...6.3.0-alpha.7) (2023-08-18) ### Bug Fixes * Remove config logging when launching Parse Server via CLI ([#8710](https://github.com/parse-community/parse-server/issues/8710)) ([ae68f0c](https://github.com/parse-community/parse-server/commit/ae68f0c31b741eeb83379c905c7ddfaa124436ec)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 66e245f2e0..5260fa1a57 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [6.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.6...6.3.0-alpha.7) (2023-08-18) + + +### Bug Fixes + +* Remove config logging when launching Parse Server via CLI ([#8710](https://github.com/parse-community/parse-server/issues/8710)) ([ae68f0c](https://github.com/parse-community/parse-server/commit/ae68f0c31b741eeb83379c905c7ddfaa124436ec)) + # [6.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.5...6.3.0-alpha.6) (2023-07-17) diff --git a/package-lock.json b/package-lock.json index a0f1298abb..062b7760d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "6.3.0-alpha.6", + "version": "6.3.0-alpha.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.3.0-alpha.6", + "version": "6.3.0-alpha.7", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 18119f4bd6..429451f0a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.3.0-alpha.6", + "version": "6.3.0-alpha.7", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 42929e0e5e6b7e553e656d683edfa8316f684829 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 19:44:33 +0200 Subject: [PATCH 24/34] build(deps): Bump word-wrap from 1.2.3 to 1.2.5 (#8708) --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 062b7760d8..5be0e7194d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@graphql-tools/utils": "8.12.0", "@graphql-yoga/node": "2.6.0", "@parse/fs-files-adapter": "1.2.2", - "@parse/push-adapter": "^4.2.0", + "@parse/push-adapter": "4.2.0", "bcryptjs": "2.4.3", "body-parser": "1.20.2", "commander": "10.0.1", @@ -20385,9 +20385,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "engines": { "node": ">=0.10.0" } @@ -36103,9 +36103,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" }, "wordwrap": { "version": "1.0.0", From 2b3d4e5d3c85cd142f85af68dec51a8523548d49 Mon Sep 17 00:00:00 2001 From: Cory Imdieke Date: Tue, 29 Aug 2023 19:52:13 -0500 Subject: [PATCH 25/34] fix: Redis 4 does not reconnect after unhandled error (#8706) --- src/Adapters/Cache/RedisCacheAdapter.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Adapters/Cache/RedisCacheAdapter.js b/src/Adapters/Cache/RedisCacheAdapter.js index eba16694e3..a0345a1101 100644 --- a/src/Adapters/Cache/RedisCacheAdapter.js +++ b/src/Adapters/Cache/RedisCacheAdapter.js @@ -17,6 +17,10 @@ export class RedisCacheAdapter { this.ttl = isValidTTL(ttl) ? ttl : DEFAULT_REDIS_TTL; this.client = createClient(redisCtx); this.queue = new KeyPromiseQueue(); + this.client.on('error', err => { logger.error('RedisCacheAdapter client error', { error: err }) }); + this.client.on('connect', () => {}); + this.client.on('reconnecting', () => {}); + this.client.on('ready', () => {}); } async connect() { From 877eede075c6d1fe8187f6ababb327468f91670c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 30 Aug 2023 00:53:25 +0000 Subject: [PATCH 26/34] chore(release): 6.3.0-alpha.8 [skip ci] # [6.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.7...6.3.0-alpha.8) (2023-08-30) ### Bug Fixes * Redis 4 does not reconnect after unhandled error ([#8706](https://github.com/parse-community/parse-server/issues/8706)) ([2b3d4e5](https://github.com/parse-community/parse-server/commit/2b3d4e5d3c85cd142f85af68dec51a8523548d49)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 5260fa1a57..5f9add3062 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [6.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.7...6.3.0-alpha.8) (2023-08-30) + + +### Bug Fixes + +* Redis 4 does not reconnect after unhandled error ([#8706](https://github.com/parse-community/parse-server/issues/8706)) ([2b3d4e5](https://github.com/parse-community/parse-server/commit/2b3d4e5d3c85cd142f85af68dec51a8523548d49)) + # [6.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.6...6.3.0-alpha.7) (2023-08-18) diff --git a/package-lock.json b/package-lock.json index 5be0e7194d..a81e743ee5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "6.3.0-alpha.7", + "version": "6.3.0-alpha.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.3.0-alpha.7", + "version": "6.3.0-alpha.8", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 429451f0a4..d709e29784 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.3.0-alpha.7", + "version": "6.3.0-alpha.8", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 5954f0ffa0ebedcb2955b9cffd571a0190e32a29 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:01:02 +0200 Subject: [PATCH 27/34] refactor: Parse Pointer allows to access internal Parse Server classes and circumvent `beforeFind` query trigger (#8735) --- 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 90ab313826..9a39664614 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2431,6 +2431,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 87718da13a..022fb99fd2 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 47fed865fb..31de5b661e 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 24e22ac4f5..023d3b4790 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 02d2f5960b..61a5c728e4 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 0fe3b54460..6488b8427e 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 1a5b9bf491..04fb5c4fd0 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 7618f500bf..726dc279fa 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 fe3617eb1b..538d87d4c1 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 d35e52d6b5..a469936fd8 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -621,7 +621,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; @@ -631,19 +631,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 0000000000..0b4a07c320 --- /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 e1e53668a6..1f9dbacb73 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 977edeaf2827e91ed94b7a700b84a280e4265a42 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Wed, 6 Sep 2023 02:42:50 +0200 Subject: [PATCH 28/34] test: Add tests for `isGet` parameter in Cloud Code trigger `beforeFind` (#8738) --- spec/CloudCode.spec.js | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 9a39664614..14a64c4df4 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2398,6 +2398,56 @@ describe('beforeFind hooks', () => { }); }); + it('sets correct beforeFind trigger isGet parameter for Parse.Object.fetch request', async () => { + const hook = { + method: req => { + expect(req.isGet).toEqual(true); + return Promise.resolve(); + }, + }; + spyOn(hook, 'method').and.callThrough(); + Parse.Cloud.beforeFind('MyObject', hook.method); + const obj = new Parse.Object('MyObject'); + await obj.save(); + const getObj = await obj.fetch(); + expect(getObj).toBeInstanceOf(Parse.Object); + expect(hook.method).toHaveBeenCalledTimes(1); + }); + + it('sets correct beforeFind trigger isGet parameter for Parse.Query.get request', async () => { + const hook = { + method: req => { + expect(req.isGet).toEqual(false); + return Promise.resolve(); + }, + }; + spyOn(hook, 'method').and.callThrough(); + Parse.Cloud.beforeFind('MyObject', hook.method); + const obj = new Parse.Object('MyObject'); + await obj.save(); + const query = new Parse.Query('MyObject'); + const getObj = await query.get(obj.id); + expect(getObj).toBeInstanceOf(Parse.Object); + expect(hook.method).toHaveBeenCalledTimes(1); + }); + + it('sets correct beforeFind trigger isGet parameter for Parse.Query.find request', async () => { + const hook = { + method: req => { + expect(req.isGet).toEqual(false); + return Promise.resolve(); + }, + }; + spyOn(hook, 'method').and.callThrough(); + Parse.Cloud.beforeFind('MyObject', hook.method); + const obj = new Parse.Object('MyObject'); + await obj.save(); + const query = new Parse.Query('MyObject'); + const findObjs = await query.find(); + expect(findObjs?.[0]).toBeInstanceOf(Parse.Object); + expect(hook.method).toHaveBeenCalledTimes(1); + }); + it('should have request headers', done => { Parse.Cloud.beforeFind('MyObject', req => { expect(req.headers).toBeDefined(); From 45a3ed0fcf2c0170607505a1550fb15896e705fd Mon Sep 17 00:00:00 2001 From: Yechezkel Deren Date: Wed, 13 Sep 2023 15:16:33 +0300 Subject: [PATCH 29/34] perf: Improve performance of recursive pointer iterations (#8741) --- src/RestQuery.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/RestQuery.js b/src/RestQuery.js index 538d87d4c1..96a52ec17a 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -1066,11 +1066,7 @@ function includePath(config, auth, response, path, restOptions = {}) { // Returns a list of pointers in REST format. function findPointers(object, path) { if (object instanceof Array) { - var answer = []; - for (var x of object) { - answer = answer.concat(findPointers(x, path)); - } - return answer; + return object.map(x => findPointers(x, path)).flat(); } if (typeof object !== 'object' || !object) { From 6ea65f255f4cb5bd618907aac7bfe81990c49244 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 13 Sep 2023 12:17:51 +0000 Subject: [PATCH 30/34] chore(release): 6.3.0-alpha.9 [skip ci] # [6.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.8...6.3.0-alpha.9) (2023-09-13) ### Performance Improvements * Improve performance of recursive pointer iterations ([#8741](https://github.com/parse-community/parse-server/issues/8741)) ([45a3ed0](https://github.com/parse-community/parse-server/commit/45a3ed0fcf2c0170607505a1550fb15896e705fd)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 5f9add3062..376b57c90d 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [6.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.8...6.3.0-alpha.9) (2023-09-13) + + +### Performance Improvements + +* Improve performance of recursive pointer iterations ([#8741](https://github.com/parse-community/parse-server/issues/8741)) ([45a3ed0](https://github.com/parse-community/parse-server/commit/45a3ed0fcf2c0170607505a1550fb15896e705fd)) + # [6.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.7...6.3.0-alpha.8) (2023-08-30) diff --git a/package-lock.json b/package-lock.json index a81e743ee5..3feb7649e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "6.3.0-alpha.8", + "version": "6.3.0-alpha.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.3.0-alpha.8", + "version": "6.3.0-alpha.9", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index d709e29784..b45aba4c47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.3.0-alpha.8", + "version": "6.3.0-alpha.9", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 9c6cdf4904f7778b08f14bd113481548a325c03c Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 16 Sep 2023 03:25:46 +0200 Subject: [PATCH 31/34] release From 05939858af2ff830bffad1e58a6e09673267f3de Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 16 Sep 2023 02:52:34 +0000 Subject: [PATCH 32/34] chore(release): 6.4.0-beta.1 [skip ci] # [6.4.0-beta.1](https://github.com/parse-community/parse-server/compare/6.3.0...6.4.0-beta.1) (2023-09-16) ### Bug Fixes * Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions ([#8688](https://github.com/parse-community/parse-server/issues/8688)) ([6a4a00c](https://github.com/parse-community/parse-server/commit/6a4a00ca7af1163ea74b047b85cd6817366b824b)) * Redis 4 does not reconnect after unhandled error ([#8706](https://github.com/parse-community/parse-server/issues/8706)) ([2b3d4e5](https://github.com/parse-community/parse-server/commit/2b3d4e5d3c85cd142f85af68dec51a8523548d49)) * Remove config logging when launching Parse Server via CLI ([#8710](https://github.com/parse-community/parse-server/issues/8710)) ([ae68f0c](https://github.com/parse-community/parse-server/commit/ae68f0c31b741eeb83379c905c7ddfaa124436ec)) * Server does not start via CLI when `auth` option is set ([#8666](https://github.com/parse-community/parse-server/issues/8666)) ([4e2000b](https://github.com/parse-community/parse-server/commit/4e2000bc563324389584ace3c090a5c1a7796a64)) ### 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)) * Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code ([#8670](https://github.com/parse-community/parse-server/issues/8670)) ([a9d376b](https://github.com/parse-community/parse-server/commit/a9d376b61f5b07806eafbda91c4e36c322f09298)) * Add TOTP authentication adapter ([#8457](https://github.com/parse-community/parse-server/issues/8457)) ([cc079a4](https://github.com/parse-community/parse-server/commit/cc079a40f6849a0e9bc6fdc811e8649ecb67b589)) ### Performance Improvements * Improve performance of recursive pointer iterations ([#8741](https://github.com/parse-community/parse-server/issues/8741)) ([45a3ed0](https://github.com/parse-community/parse-server/commit/45a3ed0fcf2c0170607505a1550fb15896e705fd)) --- changelogs/CHANGELOG_beta.md | 20 ++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_beta.md b/changelogs/CHANGELOG_beta.md index 289b86d780..9a671aaab5 100644 --- a/changelogs/CHANGELOG_beta.md +++ b/changelogs/CHANGELOG_beta.md @@ -1,3 +1,23 @@ +# [6.4.0-beta.1](https://github.com/parse-community/parse-server/compare/6.3.0...6.4.0-beta.1) (2023-09-16) + + +### Bug Fixes + +* Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions ([#8688](https://github.com/parse-community/parse-server/issues/8688)) ([6a4a00c](https://github.com/parse-community/parse-server/commit/6a4a00ca7af1163ea74b047b85cd6817366b824b)) +* Redis 4 does not reconnect after unhandled error ([#8706](https://github.com/parse-community/parse-server/issues/8706)) ([2b3d4e5](https://github.com/parse-community/parse-server/commit/2b3d4e5d3c85cd142f85af68dec51a8523548d49)) +* Remove config logging when launching Parse Server via CLI ([#8710](https://github.com/parse-community/parse-server/issues/8710)) ([ae68f0c](https://github.com/parse-community/parse-server/commit/ae68f0c31b741eeb83379c905c7ddfaa124436ec)) +* Server does not start via CLI when `auth` option is set ([#8666](https://github.com/parse-community/parse-server/issues/8666)) ([4e2000b](https://github.com/parse-community/parse-server/commit/4e2000bc563324389584ace3c090a5c1a7796a64)) + +### 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)) +* Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code ([#8670](https://github.com/parse-community/parse-server/issues/8670)) ([a9d376b](https://github.com/parse-community/parse-server/commit/a9d376b61f5b07806eafbda91c4e36c322f09298)) +* Add TOTP authentication adapter ([#8457](https://github.com/parse-community/parse-server/issues/8457)) ([cc079a4](https://github.com/parse-community/parse-server/commit/cc079a40f6849a0e9bc6fdc811e8649ecb67b589)) + +### Performance Improvements + +* Improve performance of recursive pointer iterations ([#8741](https://github.com/parse-community/parse-server/issues/8741)) ([45a3ed0](https://github.com/parse-community/parse-server/commit/45a3ed0fcf2c0170607505a1550fb15896e705fd)) + # [6.3.0-beta.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.3.0-beta.1) (2023-06-10) diff --git a/package-lock.json b/package-lock.json index 3feb7649e5..5e831afe3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "6.3.0-alpha.9", + "version": "6.4.0-beta.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.3.0-alpha.9", + "version": "6.4.0-beta.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index b45aba4c47..4a7bb746dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.3.0-alpha.9", + "version": "6.4.0-beta.1", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From ea57a7706d75d21754d04985a580d040efe45939 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 21 Oct 2023 01:03:31 +0200 Subject: [PATCH 33/34] refactor: Server crash when uploading file without extension; fixes security vulnerability [GHSA-792q-q67h-w579](https://github.com/parse-community/parse-server/security/advisories/GHSA-792q-q67h-w579) (#8780) --- spec/ParseFile.spec.js | 28 ++++++++++++++++++++++++++++ src/Routers/FilesRouter.js | 4 ++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index f083c90ae4..5f5ab43c54 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -1364,6 +1364,34 @@ describe('Parse.File testing', () => { ); }); + it('allows file without extension', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'], + }, + }); + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + + const values = ['filenamewithoutextension']; + + for (const value of values) { + await expectAsync( + request({ + method: 'POST', + headers: headers, + url: `http://localhost:8378/1/files/${value}`, + body: '\n', + }).catch(e => { + throw new Error(e.data.error); + }) + ).toBeResolved(); + } + }); + it('works with array', async () => { await reconfigureServer({ fileUpload: { diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index cbb59fdcdd..165e0924ce 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -159,9 +159,9 @@ export class FilesRouter { } else if (contentType && contentType.includes('/')) { extension = contentType.split('/')[1]; } - extension = extension.split(' ').join(''); + extension = extension?.split(' ')?.join(''); - if (!isValidExtension(extension)) { + if (extension && !isValidExtension(extension)) { next( new Parse.Error( Parse.Error.FILE_SAVE_ERROR, From c6355cda73c204e1a29314f6085d00d5bc36c61a Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:58:24 +0100 Subject: [PATCH 34/34] release