From 4bd135c4f083d8b8dd5ba88b030b600537428ad9 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 6 Oct 2022 21:18:24 +1100 Subject: [PATCH 01/25] fix: prevent stripping of user fields with masterKey --- src/Controllers/DatabaseController.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 7dbd789c3b..81edb3d7a7 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -194,6 +194,16 @@ const filterSensitiveData = ( } } + if (isUserClass) { + object.password = object._hashed_password; + delete object._hashed_password; + delete object.sessionToken; + } + + if (isMaster) { + return object; + } + const isUserClass = className === '_User'; /* special treat for the user class: don't filter protectedFields if currently loggedin user is @@ -208,15 +218,6 @@ const filterSensitiveData = ( perms.protectedFields.temporaryKeys.forEach(k => delete object[k]); } - if (isUserClass) { - object.password = object._hashed_password; - delete object._hashed_password; - delete object.sessionToken; - } - - if (isMaster) { - return object; - } for (const key in object) { if (key.charAt(0) === '_') { delete object[key]; From 906072d80fbf2410f8e5bf695456b98e9a1768d0 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 6 Oct 2022 22:20:44 +1100 Subject: [PATCH 02/25] feat: allow read and write of internal fields with masterKey --- spec/ParseUser.spec.js | 91 +++++++++++++++++++++------ src/Controllers/DatabaseController.js | 8 +-- src/Controllers/SchemaController.js | 13 ++-- src/RestWrite.js | 3 +- 4 files changed, 88 insertions(+), 27 deletions(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 92301316e4..dfa7cd5ce0 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -3448,40 +3448,95 @@ describe('Parse.User testing', () => { }); }); - it('should not allow updates to hidden fields', done => { + fit('should not allow updates to hidden fields', async () => { const emailAdapter = { sendVerificationEmail: () => {}, sendPasswordResetEmail: () => Promise.resolve(), sendMail: () => Promise.resolve(), }; - const user = new Parse.User(); user.set({ username: 'hello', password: 'world', email: 'test@email.com', }); + await reconfigureServer({ + appName: 'unused', + verifyUserEmails: true, + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + await user.signUp(); + user.set('_email_verify_token', 'bad', { ignoreValidation: true }); + await expectAsync(user.save()).toBeRejectedWith( + new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid field name: _email_verify_token.') + ); + }); - reconfigureServer({ + fit('should allow updates to fields with masterKey', async () => { + const emailAdapter = { + sendVerificationEmail: () => {}, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => Promise.resolve(), + }; + const user = new Parse.User(); + user.set({ + username: 'hello', + password: 'world', + email: 'test@email.com', + }); + await reconfigureServer({ appName: 'unused', verifyUserEmails: true, + emailVerifyTokenValidityDuration: 5, + accountLockout: { + duration: 1, + threshold: 1, + }, emailAdapter: emailAdapter, publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - return user.signUp(); - }) - .then(() => { - return Parse.User.current().set('_email_verify_token', 'bad').save(); - }) - .then(() => { - fail('Should not be able to update email verification token'); - done(); - }) - .catch(err => { - expect(err).toBeDefined(); - done(); - }); + silent: false, + }); + await user.signUp(); + for (let i = 0; i < 2; i++) { + try { + await Parse.User.logIn(user.getEmail(), 'abc'); + } catch (e) { + /* */ + } + } + await Parse.User.requestPasswordReset(user.getEmail()); + const userMaster = await new Parse.Query(Parse.User).first({ useMasterKey: true }); + expect(Object.keys(userMaster.toJSON()).sort()).toEqual( + [ + 'ACL', + '_account_lockout_expires_at', + '_email_verify_token', + '_email_verify_token_expires_at', + '_failed_login_count', + '_perishable_token', + 'createdAt', + 'email', + 'emailVerified', + 'objectId', + 'updatedAt', + 'username', + ].sort() + ); + const toSet = { + _account_lockout_expires_at: new Date().toISOString(), + _email_verify_token: 'abc', + _email_verify_token_expires_at: new Date().toISOString(), + _failed_login_count: 0, + _perishable_token_expires_at: new Date().toISOString(), + _perishable_token: 'abc', + }; + userMaster.set(toSet, { ignoreValidation: true }); + await userMaster.save(null, { useMasterKey: true }); + await userMaster.fetch({ useMasterKey: true }); + for (const key in toSet) { + expect(userMaster.get(key)).toEqual(toSet[key]); + } }); it('should revoke sessions when setting paswword with masterKey (#3289)', done => { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 81edb3d7a7..1cefba8510 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -194,6 +194,7 @@ const filterSensitiveData = ( } } + const isUserClass = className === '_User'; if (isUserClass) { object.password = object._hashed_password; delete object._hashed_password; @@ -204,8 +205,6 @@ const filterSensitiveData = ( return object; } - const isUserClass = className === '_User'; - /* special treat for the user class: don't filter protectedFields if currently loggedin user is the retrieved user */ if (!(isUserClass && userId && object.objectId === userId)) { @@ -440,7 +439,8 @@ class DatabaseController { className: string, object: any, query: any, - runOptions: QueryOptions + runOptions: QueryOptions, + master: Boolean ): Promise { let schema; const acl = runOptions.acl; @@ -455,7 +455,7 @@ class DatabaseController { return this.canAddField(schema, className, object, aclGroup, runOptions); }) .then(() => { - return schema.validateObject(className, object, query); + return schema.validateObject(className, object, query, master); }); } diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 179de37b6e..5e2ca4dfc5 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1071,14 +1071,19 @@ export default class SchemaController { className: string, fieldName: string, type: string | SchemaField, - isValidation?: boolean + isValidation?: boolean, + master: boolean ) { if (fieldName.indexOf('.') > 0) { // subdocument key (x.y) => ok if x is of type 'object' fieldName = fieldName.split('.')[0]; type = 'Object'; } - if (!fieldNameIsValid(fieldName, className)) { + let fieldNameToValidate = `${fieldName}`; + if (master && fieldNameToValidate.charAt(0) === '_') { + fieldNameToValidate = fieldNameToValidate.substring(1); + } + if (!fieldNameIsValid(fieldNameToValidate, className)) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); } @@ -1228,7 +1233,7 @@ export default class SchemaController { // Validates an object provided in REST format. // Returns a promise that resolves to the new schema if this object is // valid. - async validateObject(className: string, object: any, query: any) { + async validateObject(className: string, object: any, query: any, master: boolean) { let geocount = 0; const schema = await this.enforceClassExists(className); const promises = []; @@ -1258,7 +1263,7 @@ export default class SchemaController { // Every object has ACL implicitly. continue; } - promises.push(schema.enforceFieldExists(className, fieldName, expected, true)); + promises.push(schema.enforceFieldExists(className, fieldName, expected, true, master)); } const results = await Promise.all(promises); const enforceFields = results.filter(result => !!result); diff --git a/src/RestWrite.js b/src/RestWrite.js index 9cb735fa40..59450a2d2a 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -211,7 +211,8 @@ RestWrite.prototype.validateSchema = function () { this.className, this.data, this.query, - this.runOptions + this.runOptions, + this.auth.isMaster ); }; From 7e9bb672183ba347e69359187b308a8f85314a97 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 6 Oct 2022 22:22:07 +1100 Subject: [PATCH 03/25] Update ParseUser.spec.js --- spec/ParseUser.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index dfa7cd5ce0..aa5c8e781d 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -3448,7 +3448,7 @@ describe('Parse.User testing', () => { }); }); - fit('should not allow updates to hidden fields', async () => { + it('should not allow updates to hidden fields', async () => { const emailAdapter = { sendVerificationEmail: () => {}, sendPasswordResetEmail: () => Promise.resolve(), @@ -3473,7 +3473,7 @@ describe('Parse.User testing', () => { ); }); - fit('should allow updates to fields with masterKey', async () => { + it('should allow updates to fields with masterKey', async () => { const emailAdapter = { sendVerificationEmail: () => {}, sendPasswordResetEmail: () => Promise.resolve(), From 1be7ff54ae6c16de89e65a9f8b903585d4290ca3 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 6 Oct 2022 22:27:57 +1100 Subject: [PATCH 04/25] fix lint --- src/Controllers/DatabaseController.js | 2 +- src/Controllers/SchemaController.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 1cefba8510..87f8224337 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -440,7 +440,7 @@ class DatabaseController { object: any, query: any, runOptions: QueryOptions, - master: Boolean + master: boolean ): Promise { let schema; const acl = runOptions.acl; diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 5e2ca4dfc5..ca26decc7a 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1072,7 +1072,7 @@ export default class SchemaController { fieldName: string, type: string | SchemaField, isValidation?: boolean, - master: boolean + master?: boolean, ) { if (fieldName.indexOf('.') > 0) { // subdocument key (x.y) => ok if x is of type 'object' From 62c14eb2ee6bb7535dede509ac189c87ef52307e Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 7 Oct 2022 00:03:37 +1100 Subject: [PATCH 05/25] run prettier --- spec/ParseUser.spec.js | 13 ++++++++----- src/Controllers/SchemaController.js | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index aa5c8e781d..9ce6cb6186 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -3473,7 +3473,7 @@ describe('Parse.User testing', () => { ); }); - it('should allow updates to fields with masterKey', async () => { + fit('should allow updates to fields with masterKey', async () => { const emailAdapter = { sendVerificationEmail: () => {}, sendPasswordResetEmail: () => Promise.resolve(), @@ -3524,18 +3524,21 @@ describe('Parse.User testing', () => { ].sort() ); const toSet = { - _account_lockout_expires_at: new Date().toISOString(), + _account_lockout_expires_at: new Date(), _email_verify_token: 'abc', - _email_verify_token_expires_at: new Date().toISOString(), + _email_verify_token_expires_at: new Date(), _failed_login_count: 0, - _perishable_token_expires_at: new Date().toISOString(), + _perishable_token_expires_at: new Date(), _perishable_token: 'abc', }; userMaster.set(toSet, { ignoreValidation: true }); await userMaster.save(null, { useMasterKey: true }); await userMaster.fetch({ useMasterKey: true }); for (const key in toSet) { - expect(userMaster.get(key)).toEqual(toSet[key]); + const value = toSet[key]; + expect( + userMaster.get(key) === value || userMaster.get(key) === value.toISOString() + ).toBeTrue(); } }); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index ca26decc7a..c544354f22 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1072,7 +1072,7 @@ export default class SchemaController { fieldName: string, type: string | SchemaField, isValidation?: boolean, - master?: boolean, + master?: boolean ) { if (fieldName.indexOf('.') > 0) { // subdocument key (x.y) => ok if x is of type 'object' From 5797d43e905efd0cc792e12e5dc3b665a1b38b59 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 27 Oct 2022 21:25:47 +1100 Subject: [PATCH 06/25] add maintenance --- package-lock.json | 2 +- spec/Middlewares.spec.js | 11 ++++++++ spec/ParseUser.spec.js | 38 ++++++++++++++++++++------- spec/rest.spec.js | 7 +++++ src/Auth.js | 13 ++++++++- src/Config.js | 13 ++++++--- src/Controllers/DatabaseController.js | 34 ++++++++++++++---------- src/Controllers/SchemaController.js | 8 +++--- src/LiveQuery/ParseLiveQueryServer.js | 1 + src/Options/Definitions.js | 11 ++++++++ src/Options/docs.js | 2 ++ src/Options/index.js | 5 ++++ src/ParseServer.js | 3 +++ src/RestWrite.js | 24 ++++++++++------- src/middlewares.js | 20 ++++++++++++++ src/rest.js | 21 ++++++++++----- 16 files changed, 166 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index 26d5841908..134ff98a9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13354,7 +13354,7 @@ "postgres-bytea": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==" + "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" }, "postgres-date": { "version": "1.0.7", diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 2c380152ba..fc3ff75a33 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -145,6 +145,17 @@ describe('middlewares', () => { expect(fakeRes.status).toHaveBeenCalledWith(403); }); + it('should not succeed if the ip does not belong to maintenanceKeyIps list', () => { + AppCache.put(fakeReq.body._ApplicationId, { + maintenanceKey: 'masterKey', + maintenanceKeyIps: ['ip1', 'ip2'], + }); + fakeReq.ip = 'ip3'; + fakeReq.headers['x-parse-maintenance-key'] = 'masterKey'; + middlewares.handleParseHeaders(fakeReq, fakeRes); + expect(fakeRes.status).toHaveBeenCalledWith(403); + }); + it('should succeed if the ip does belong to masterKeyIps list', done => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 9ce6cb6186..b84a2de030 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -3473,7 +3473,7 @@ describe('Parse.User testing', () => { ); }); - fit('should allow updates to fields with masterKey', async () => { + it('should allow updates to fields with masterKey', async () => { const emailAdapter = { sendVerificationEmail: () => {}, sendPasswordResetEmail: () => Promise.resolve(), @@ -3487,6 +3487,7 @@ describe('Parse.User testing', () => { }); await reconfigureServer({ appName: 'unused', + maintenanceKey: 'test2', verifyUserEmails: true, emailVerifyTokenValidityDuration: 5, accountLockout: { @@ -3506,8 +3507,19 @@ describe('Parse.User testing', () => { } } await Parse.User.requestPasswordReset(user.getEmail()); - const userMaster = await new Parse.Query(Parse.User).first({ useMasterKey: true }); - expect(Object.keys(userMaster.toJSON()).sort()).toEqual( + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Rest-API-Key': 'test', + 'X-Parse-Maintenance-Key': 'test2', + 'Content-Type': 'application/json', + }; + const userMaster = await request({ + method: 'GET', + url: `http://localhost:8378/1/classes/_User`, + json: true, + headers, + }).then(res => res.data.results[0]); + expect(Object.keys(userMaster).sort()).toEqual( [ 'ACL', '_account_lockout_expires_at', @@ -3531,14 +3543,22 @@ describe('Parse.User testing', () => { _perishable_token_expires_at: new Date(), _perishable_token: 'abc', }; - userMaster.set(toSet, { ignoreValidation: true }); - await userMaster.save(null, { useMasterKey: true }); - await userMaster.fetch({ useMasterKey: true }); + await request({ + method: 'PUT', + headers, + url: Parse.serverURL + '/users/' + userMaster.objectId, + json: true, + body: toSet, + }).then(res => res.data); + const update = await request({ + method: 'GET', + url: `http://localhost:8378/1/classes/_User`, + json: true, + headers, + }).then(res => res.data.results[0]); for (const key in toSet) { const value = toSet[key]; - expect( - userMaster.get(key) === value || userMaster.get(key) === value.toISOString() - ).toBeTrue(); + expect(update[key] === value || update[key] === value.toISOString()).toBeTrue(); } }); diff --git a/spec/rest.spec.js b/spec/rest.spec.js index db3082ec74..30b000c740 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -859,6 +859,13 @@ describe('read-only masterKey', () => { await reconfigureServer(); }); + it('should throw when masterKey and maintenanceKey are the same', async () => { + await expectAsync(reconfigureServer({ + masterKey: 'yolo', + maintenanceKey: 'yolo', + })).toBeRejectedWith(new Error('masterKey and maintenanceKey should be different')); + }); + it('should throw when trying to create RestWrite', () => { const config = Config.get('test'); expect(() => { diff --git a/src/Auth.js b/src/Auth.js index ce5c71c860..9133640d8d 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -8,6 +8,7 @@ function Auth({ config, cacheController = undefined, isMaster = false, + isMaintenance = false, isReadOnly = false, user, installationId, @@ -16,6 +17,7 @@ function Auth({ this.cacheController = cacheController || (config && config.cacheController); this.installationId = installationId; this.isMaster = isMaster; + this.isMaintenance = isMaintenance; this.user = user; this.isReadOnly = isReadOnly; @@ -32,6 +34,9 @@ Auth.prototype.isUnauthenticated = function () { if (this.isMaster) { return false; } + if (this.isMaintenance) { + return false; + } if (this.user) { return false; } @@ -43,6 +48,11 @@ function master(config) { return new Auth({ config, isMaster: true }); } +// A helper to get a maintenance-level Auth object +function maintenance(config) { + return new Auth({ config, isMaintenance: true }); +} + // A helper to get a master-level Auth object function readOnly(config) { return new Auth({ config, isMaster: true, isReadOnly: true }); @@ -145,7 +155,7 @@ var getAuthForLegacySessionToken = function ({ config, sessionToken, installatio // Returns a promise that resolves to an array of role names Auth.prototype.getUserRoles = function () { - if (this.isMaster || !this.user) { + if (this.isMaster || this.isMaintenance || !this.user) { return Promise.resolve([]); } if (this.fetchedRoles) { @@ -310,6 +320,7 @@ Auth.prototype._getAllRolesNamesForRoleIds = function (roleIDs, names = [], quer module.exports = { Auth, master, + maintenance, nobody, readOnly, getAuthForSessionToken, diff --git a/src/Config.js b/src/Config.js index 9c5fb3bc83..89a8aeb8f6 100644 --- a/src/Config.js +++ b/src/Config.js @@ -71,6 +71,8 @@ export class Config { passwordPolicy, masterKeyIps, masterKey, + maintenanceKey, + maintenanceKeyIps, readOnlyMasterKey, allowHeaders, idempotencyOptions, @@ -86,6 +88,10 @@ export class Config { throw new Error('masterKey and readOnlyMasterKey should be different'); } + if (masterKey === maintenanceKey) { + throw new Error('masterKey and maintenanceKey should be different'); + } + const emailAdapter = userController.adapter; if (verifyUserEmails) { this.validateEmailConfiguration({ @@ -111,7 +117,8 @@ export class Config { } } this.validateSessionConfiguration(sessionLength, expireInactiveSessions); - this.validateMasterKeyIps(masterKeyIps); + this.validateIps('masterKeyIps', masterKeyIps); + this.validateIps('maintenanceKeyIps', maintenanceKeyIps); this.validateDefaultLimit(defaultLimit); this.validateMaxLimit(maxLimit); this.validateAllowHeaders(allowHeaders); @@ -426,10 +433,10 @@ export class Config { } } - static validateMasterKeyIps(masterKeyIps) { + static validateIps(field, masterKeyIps) { for (const ip of masterKeyIps) { if (!net.isIP(ip)) { - throw `Invalid ip in masterKeyIps: ${ip}`; + throw `Invalid ip in ${field}: ${ip}`; } } } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 87f8224337..00b9c3f14b 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -68,14 +68,17 @@ const specialMasterQueryKeys = [ '_password_history', ]; -const validateQuery = (query: any, isMaster: boolean, update: boolean): void => { +const validateQuery = (query: any, isMaster: boolean, isMaintenance: boolean, update: boolean): void => { + if (isMaintenance) { + isMaster = true; + } if (query.ACL) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); } if (query.$or) { if (query.$or instanceof Array) { - query.$or.forEach(value => validateQuery(value, isMaster, update)); + query.$or.forEach(value => validateQuery(value, isMaster, isMaintenance, update)); } else { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.'); } @@ -83,7 +86,7 @@ const validateQuery = (query: any, isMaster: boolean, update: boolean): void => if (query.$and) { if (query.$and instanceof Array) { - query.$and.forEach(value => validateQuery(value, isMaster, update)); + query.$and.forEach(value => validateQuery(value, isMaster, isMaintenance, update)); } else { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.'); } @@ -91,7 +94,7 @@ const validateQuery = (query: any, isMaster: boolean, update: boolean): void => if (query.$nor) { if (query.$nor instanceof Array && query.$nor.length > 0) { - query.$nor.forEach(value => validateQuery(value, isMaster, update)); + query.$nor.forEach(value => validateQuery(value, isMaster, isMaintenance, update)); } else { throw new Parse.Error( Parse.Error.INVALID_QUERY, @@ -124,6 +127,7 @@ const validateQuery = (query: any, isMaster: boolean, update: boolean): void => // Filters out any data that shouldn't be on this REST-formatted object. const filterSensitiveData = ( isMaster: boolean, + isMaintenance: boolean, aclGroup: any[], auth: any, operation: any, @@ -201,7 +205,7 @@ const filterSensitiveData = ( delete object.sessionToken; } - if (isMaster) { + if (isMaintenance) { return object; } @@ -223,7 +227,7 @@ const filterSensitiveData = ( } } - if (!isUserClass) { + if (!isUserClass || isMaster) { return object; } @@ -440,7 +444,7 @@ class DatabaseController { object: any, query: any, runOptions: QueryOptions, - master: boolean + maintenance: boolean ): Promise { let schema; const acl = runOptions.acl; @@ -455,7 +459,7 @@ class DatabaseController { return this.canAddField(schema, className, object, aclGroup, runOptions); }) .then(() => { - return schema.validateObject(className, object, query, master); + return schema.validateObject(className, object, query, maintenance); }); } @@ -513,7 +517,7 @@ class DatabaseController { if (acl) { query = addWriteACL(query, acl); } - validateQuery(query, isMaster, true); + validateQuery(query, isMaster, false, true); return schemaController .getOneSchema(className, true) .catch(error => { @@ -759,7 +763,7 @@ class DatabaseController { if (acl) { query = addWriteACL(query, acl); } - validateQuery(query, isMaster, false); + validateQuery(query, isMaster, false, false); return schemaController .getOneSchema(className) .catch(error => { @@ -1152,7 +1156,8 @@ class DatabaseController { auth: any = {}, validSchemaController: SchemaController.SchemaController ): Promise { - const isMaster = acl === undefined; + const isMaintenance = auth.isMaintenance; + const isMaster = acl === undefined || isMaintenance; const aclGroup = acl || []; op = op || (typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find'); @@ -1254,7 +1259,7 @@ class DatabaseController { query = addReadACL(query, aclGroup); } } - validateQuery(query, isMaster, false); + validateQuery(query, isMaster, isMaintenance, false); if (count) { if (!classExists) { return 0; @@ -1297,6 +1302,7 @@ class DatabaseController { object = untransformObjectACL(object); return filterSensitiveData( isMaster, + isMaintenance, aclGroup, auth, op, @@ -1810,8 +1816,8 @@ class DatabaseController { return Promise.resolve(response); } - static _validateQuery: (any, boolean, boolean) => void; - static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void; + static _validateQuery: (any, boolean, boolean, boolean) => void; + static filterSensitiveData: (boolean, boolean, any[], any, any, any, string, any[], any) => void; } module.exports = DatabaseController; diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index c544354f22..62757d251d 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1072,7 +1072,7 @@ export default class SchemaController { fieldName: string, type: string | SchemaField, isValidation?: boolean, - master?: boolean + maintenance?: boolean ) { if (fieldName.indexOf('.') > 0) { // subdocument key (x.y) => ok if x is of type 'object' @@ -1080,7 +1080,7 @@ export default class SchemaController { type = 'Object'; } let fieldNameToValidate = `${fieldName}`; - if (master && fieldNameToValidate.charAt(0) === '_') { + if (maintenance && fieldNameToValidate.charAt(0) === '_') { fieldNameToValidate = fieldNameToValidate.substring(1); } if (!fieldNameIsValid(fieldNameToValidate, className)) { @@ -1233,7 +1233,7 @@ export default class SchemaController { // Validates an object provided in REST format. // Returns a promise that resolves to the new schema if this object is // valid. - async validateObject(className: string, object: any, query: any, master: boolean) { + async validateObject(className: string, object: any, query: any, maintenance: boolean) { let geocount = 0; const schema = await this.enforceClassExists(className); const promises = []; @@ -1263,7 +1263,7 @@ export default class SchemaController { // Every object has ACL implicitly. continue; } - promises.push(schema.enforceFieldExists(className, fieldName, expected, true, master)); + promises.push(schema.enforceFieldExists(className, fieldName, expected, true, maintenance)); } const results = await Promise.all(promises); const enforceFields = results.filter(result => !!result); diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 02c6ad30a1..e42d51f29f 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -610,6 +610,7 @@ class ParseLiveQueryServer { } return DatabaseController.filterSensitiveData( client.hasMasterKey, + false, aclGroup, clientAuth, op, diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index d8e3f841dd..b46045f7e5 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -288,6 +288,17 @@ module.exports.ParseServerOptions = { help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging", default: './logs', }, + maintenanceKey: { + env: 'PARSE_SERVER_MAINTENANCE_KEY', + help: 'Your Parse Maintenance Key, used for updating internal fields', + required: true, + }, + maintenanceKeyIps: { + env: 'PARSE_SERVER_MAINTENANCE_KEY_IPS', + help: 'Restrict maintenanceKey to be used by only these ips, defaults to [] (allow all ips)', + action: parsers.arrayParser, + default: ['127.0.0.1'], + }, masterKey: { env: 'PARSE_SERVER_MASTER_KEY', help: 'Your Parse Master Key', diff --git a/src/Options/docs.js b/src/Options/docs.js index cbd06ecd25..7b5370fcbd 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -56,6 +56,8 @@ * @property {Adapter} loggerAdapter Adapter module for the logging sub-system * @property {String} logLevel Sets the level for logs * @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging + * @property {String} maintenanceKey Your Parse Maintenance Key, used for updating internal fields + * @property {String[]} maintenanceKeyIps Restrict maintenanceKey to be used by only these ips, defaults to [] (allow all ips) * @property {String} masterKey Your Parse Master Key * @property {String[]} masterKeyIps Restrict masterKey to be used by only these ips, defaults to [] (allow all ips) * @property {Number} maxLimit Max value for limit option on queries, defaults to unlimited diff --git a/src/Options/index.js b/src/Options/index.js index 2592e1e441..c27bde1284 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -46,12 +46,17 @@ export interface ParseServerOptions { appId: string; /* Your Parse Master Key */ masterKey: string; + /* Your Parse Maintenance Key, used for updating internal fields */ + maintenanceKey: string; /* URL to your parse server with http:// or https://. :ENV: PARSE_SERVER_URL */ serverURL: string; /* Restrict masterKey to be used by only these ips, defaults to [] (allow all ips) :DEFAULT: [] */ masterKeyIps: ?(string[]); + /* Restrict maintenanceKey to be used by only these ips, defaults to ["127.0.0.1"] (only allow current IP) + :DEFAULT: ["127.0.0.1"] */ + maintenanceKeyIps: ?(string[]); /* Sets the app name */ appName: ?string; /* Add headers to Access-Control-Allow-Headers */ diff --git a/src/ParseServer.js b/src/ParseServer.js index e6b30d1918..8d10674003 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -444,6 +444,9 @@ function injectDefaults(options: ParseServerOptions) { options.masterKeyIps = Array.from( new Set(options.masterKeyIps.concat(defaults.masterKeyIps, options.masterKeyIps)) ); + options.maintenanceKeyIps = Array.from( + new Set([...defaults.maintenanceKeyIps, options.maintenanceKeyIps]) + ); } // Those can't be tested as it requires a subprocess diff --git a/src/RestWrite.js b/src/RestWrite.js index 59450a2d2a..48e187a7b3 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -166,7 +166,7 @@ RestWrite.prototype.execute = function () { // Uses the Auth object to get the list of roles, adds the user id RestWrite.prototype.getUserAndRoleACL = function () { - if (this.auth.isMaster) { + if (this.auth.isMaster || this.auth.isMaintenance) { return Promise.resolve(); } @@ -187,6 +187,7 @@ RestWrite.prototype.validateClientClassCreation = function () { if ( this.config.allowClientClassCreation === false && !this.auth.isMaster && + !this.auth.isMaintenance && SchemaController.systemClasses.indexOf(this.className) === -1 ) { return this.config.database @@ -212,7 +213,7 @@ RestWrite.prototype.validateSchema = function () { this.data, this.query, this.runOptions, - this.auth.isMaster + this.auth.isMaintenance ); }; @@ -477,7 +478,7 @@ RestWrite.prototype.findUsersWithAuthData = function (authData) { }; RestWrite.prototype.filteredObjectsByACL = function (objects) { - if (this.auth.isMaster) { + if (this.auth.isMaster || this.auth.isMaintenance) { return objects; } return objects.filter(object => { @@ -593,7 +594,7 @@ RestWrite.prototype.transformUser = function () { return promise; } - if (!this.auth.isMaster && 'emailVerified' in this.data) { + 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); } @@ -628,7 +629,7 @@ RestWrite.prototype.transformUser = function () { if (this.query) { this.storage['clearSessions'] = true; // Generate a new session only if the user requested - if (!this.auth.isMaster) { + if (!this.auth.isMaster && !this.auth.isMaintenance) { this.storage['generateNewSession'] = true; } } @@ -1003,7 +1004,7 @@ RestWrite.prototype.handleSession = function () { return; } - if (!this.auth.user && !this.auth.isMaster) { + if (!this.auth.user && !this.auth.isMaster && !this.auth.isMaintenance) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token required.'); } @@ -1036,7 +1037,7 @@ RestWrite.prototype.handleSession = function () { } } - if (!this.query && !this.auth.isMaster) { + if (!this.query && !this.auth.isMaster && !this.auth.isMaintenance) { const additionalSessionData = {}; for (var key in this.data) { if (key === 'objectId' || key === 'user') { @@ -1103,7 +1104,7 @@ RestWrite.prototype.handleInstallation = function () { let installationId = this.data.installationId; // If data.installationId is not set and we're not master, we can lookup in auth - if (!installationId && !this.auth.isMaster) { + if (!installationId && !this.auth.isMaster && !this.auth.isMaintenance) { installationId = this.auth.installationId; } @@ -1367,7 +1368,12 @@ RestWrite.prototype.runDatabaseOperation = function () { if (this.query) { // Force the user to not lockout // Matched with parse.com - if (this.className === '_User' && this.data.ACL && this.auth.isMaster !== true) { + if ( + this.className === '_User' && + this.data.ACL && + this.auth.isMaster !== true && + this.auth.isMaintenance !== true + ) { this.data.ACL[this.query.objectId] = { read: true, write: true }; } // update password timestamp if user password is being changed diff --git a/src/middlewares.js b/src/middlewares.js index 37acf46821..ef8d6231bf 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -41,6 +41,7 @@ export function handleParseHeaders(req, res, next) { appId: req.get('X-Parse-Application-Id'), sessionToken: req.get('X-Parse-Session-Token'), masterKey: req.get('X-Parse-Master-Key'), + maintenanceKey: req.get('X-Parse-Maintenance-Key'), installationId: req.get('X-Parse-Installation-Id'), clientKey: req.get('X-Parse-Client-Key'), javascriptKey: req.get('X-Parse-Javascript-Key'), @@ -164,6 +165,25 @@ export function handleParseHeaders(req, res, next) { req.config.ip = clientIp; req.info = info; + if ( + info.maintenanceKey && + req.config.maintenanceKeyIps && + req.config.maintenanceKeyIps.length !== 0 && + req.config.maintenanceKeyIps.indexOf(clientIp) === -1 + ) { + return invalidRequest(req, res); + } + + if (req.config.maintenanceKey && info.maintenanceKey === req.config.maintenanceKey) { + req.auth = new auth.Auth({ + config: req.config, + installationId: info.installationId, + isMaintenance: true, + }); + next(); + return; + } + if ( info.masterKey && req.config.masterKeyIps && diff --git a/src/rest.js b/src/rest.js index fca3497a5d..e1e53668a6 100644 --- a/src/rest.js +++ b/src/rest.js @@ -111,7 +111,7 @@ function del(config, auth, className, objectId, context) { if (response && response.results && response.results.length) { const firstResult = response.results[0]; firstResult.className = className; - if (className === '_Session' && !auth.isMaster) { + 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'); } @@ -134,7 +134,7 @@ function del(config, auth, className, objectId, context) { return Promise.resolve({}); }) .then(() => { - if (!auth.isMaster) { + if (!auth.isMaster && !auth.isMaintenance) { return auth.getUserRoles(); } else { return; @@ -144,7 +144,7 @@ function del(config, auth, className, objectId, context) { .then(s => { schemaController = s; const options = {}; - if (!auth.isMaster) { + if (!auth.isMaster && !auth.isMaintenance) { options.acl = ['*']; if (auth.user) { options.acl.push(auth.user.id); @@ -237,7 +237,12 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte function handleSessionMissingError(error, className, auth) { // If we're trying to update a user without / with bad session token - if (className === '_User' && error.code === Parse.Error.OBJECT_NOT_FOUND && !auth.isMaster) { + if ( + className === '_User' && + error.code === Parse.Error.OBJECT_NOT_FOUND && + !auth.isMaster && + !auth.isMaintenance + ) { throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth.'); } throw error; @@ -253,7 +258,7 @@ const classesWithMasterOnlyAccess = [ ]; // Disallowing access to the _Role collection except by master key function enforceRoleSecurity(method, className, auth) { - if (className === '_Installation' && !auth.isMaster) { + 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); @@ -261,7 +266,11 @@ function enforceRoleSecurity(method, className, auth) { } //all volatileClasses are masterKey only - if (classesWithMasterOnlyAccess.indexOf(className) >= 0 && !auth.isMaster) { + 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); } From 7fc8227e2a4477f0efe25e13fb633c4c11783d78 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 27 Oct 2022 21:52:04 +1100 Subject: [PATCH 07/25] Update UsersRouter.js --- src/Routers/UsersRouter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index cdce6a1348..278e995140 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -97,7 +97,7 @@ export class UsersRouter extends ClassesRouter { query = { $or: [{ username }, { email: username }] }; } return req.config.database - .find('_User', query) + .find('_User', query, {}, Auth.maintenance(req.config)) .then(results => { if (!results.length) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); From d06ae11fed84501bb569e5e96972509b41f529c0 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 27 Oct 2022 23:12:43 +1100 Subject: [PATCH 08/25] fix tests --- spec/EmailVerificationToken.spec.js | 8 +++++--- spec/PasswordPolicy.spec.js | 30 ++++++++++++++++++++++++----- spec/RegexVulnerabilities.spec.js | 30 ++++++++++++++++++++++++----- src/RestWrite.js | 6 ++++-- 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 50b626de0d..8032d4b2f4 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -1,5 +1,6 @@ 'use strict'; +const Auth = require('../lib/Auth'); const Config = require('../lib/Config'); const request = require('../lib/request'); @@ -264,7 +265,7 @@ describe('Email Verification Token Expiration: ', () => { const config = Config.get('test'); return config.database.find('_User', { username: 'sets_email_verify_token_expires_at', - }); + }, {}, Auth.maintenance(config)); }) .then(results => { expect(results.length).toBe(1); @@ -499,7 +500,7 @@ describe('Email Verification Token Expiration: ', () => { .then(() => { const config = Config.get('test'); return config.database - .find('_User', { username: 'newEmailVerifyTokenOnEmailReset' }) + .find('_User', { username: 'newEmailVerifyTokenOnEmailReset' }, {}, Auth.maintenance(config)) .then(results => { return results[0]; }); @@ -582,7 +583,7 @@ describe('Email Verification Token Expiration: ', () => { // query for this user again const config = Config.get('test'); return config.database - .find('_User', { username: 'resends_verification_token' }) + .find('_User', { username: 'resends_verification_token' }, {}, Auth.maintenance(config)) .then(results => { return results[0]; }); @@ -599,6 +600,7 @@ describe('Email Verification Token Expiration: ', () => { done(); }) .catch(error => { + console.log(error); jfail(error); done(); }); diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 6d00ddfa28..4ea6ed2002 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -1677,12 +1677,19 @@ describe('Password Policy: ', () => { }); it('should not infinitely loop if maxPasswordHistory is 1 (#4918)', async () => { + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Rest-API-Key': 'test', + 'X-Parse-Maintenance-Key': 'test2', + 'Content-Type': 'application/json', + }; const user = new Parse.User(); const query = new Parse.Query(Parse.User); await reconfigureServer({ appName: 'passwordPolicy', verifyUserEmails: false, + maintenanceKey: 'test2', passwordPolicy: { maxPasswordHistory: 1, }, @@ -1696,15 +1703,28 @@ describe('Password Policy: ', () => { user.setPassword('user2'); await user.save(); - const result1 = await query.get(user.id, { useMasterKey: true }); - expect(result1.get('_password_history').length).toBe(1); + const user1 = await query.get(user.id, { useMasterKey: true }); + expect(user1.get('_password_history')).toBeUndefined(); + + const result1 = await request({ + method: 'GET', + url: `http://localhost:8378/1/classes/_User/${user.id}`, + json: true, + headers, + }).then(res => res.data); + expect(result1._password_history.length).toBe(1); user.setPassword('user3'); await user.save(); - const result2 = await query.get(user.id, { useMasterKey: true }); - expect(result2.get('_password_history').length).toBe(1); + const result2 = await request({ + method: 'GET', + url: `http://localhost:8378/1/classes/_User/${user.id}`, + json: true, + headers, + }).then(res => res.data); + expect(result2._password_history.length).toBe(1); - expect(result1.get('_password_history')).not.toEqual(result2.get('_password_history')); + expect(result1._password_history).not.toEqual(result2._password_history); }); }); diff --git a/spec/RegexVulnerabilities.spec.js b/spec/RegexVulnerabilities.spec.js index 62af4f04e2..008a653511 100644 --- a/spec/RegexVulnerabilities.spec.js +++ b/spec/RegexVulnerabilities.spec.js @@ -19,6 +19,7 @@ const publicServerURL = 'http://localhost:8378/1'; describe('Regex Vulnerabilities', function () { beforeEach(async function () { await reconfigureServer({ + maintenanceKey: 'test2', verifyUserEmails: true, emailAdapter, appName, @@ -98,11 +99,20 @@ describe('Regex Vulnerabilities', function () { it('should work with plain token', async function () { expect(this.user.get('emailVerified')).toEqual(false); + const current = await request({ + method: 'GET', + url: `http://localhost:8378/1/classes/_User/${this.user.id}`, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Rest-API-Key': 'test', + 'X-Parse-Maintenance-Key': 'test2', + 'Content-Type': 'application/json', + }, + }).then(res => res.data) // It should work await request({ - url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${this.user.get( - '_email_verify_token' - )}`, + url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${current._email_verify_token}`, method: 'GET', }); await this.user.fetch({ useMasterKey: true }); @@ -164,8 +174,18 @@ describe('Regex Vulnerabilities', function () { email: 'someemail@somedomain.com', }), }); - await this.user.fetch({ useMasterKey: true }); - const token = this.user.get('_perishable_token'); + const current = await request({ + method: 'GET', + url: `http://localhost:8378/1/classes/_User/${this.user.id}`, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Rest-API-Key': 'test', + 'X-Parse-Maintenance-Key': 'test2', + 'Content-Type': 'application/json', + }, + }).then(res => res.data) + const token = current._perishable_token; const passwordResetResponse = await request({ url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`, method: 'GET', diff --git a/src/RestWrite.js b/src/RestWrite.js index 48e187a7b3..1e79b09561 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -802,7 +802,8 @@ RestWrite.prototype._validatePasswordHistory = function () { .find( '_User', { objectId: this.objectId() }, - { keys: ['_password_history', '_hashed_password'] } + { keys: ['_password_history', '_hashed_password'] }, + Auth.maintenance(this.config) ) .then(results => { if (results.length != 1) { @@ -1400,7 +1401,8 @@ RestWrite.prototype.runDatabaseOperation = function () { .find( '_User', { objectId: this.objectId() }, - { keys: ['_password_history', '_hashed_password'] } + { keys: ['_password_history', '_hashed_password'] }, + Auth.maintenance(this.config) ) .then(results => { if (results.length != 1) { From 3f143737158bf488caddda10c09fe1e50cc1687c Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 27 Oct 2022 23:19:53 +1100 Subject: [PATCH 09/25] run lint --- spec/EmailVerificationToken.spec.js | 18 ++++++++++++++---- spec/ParseLiveQuery.spec.js | 13 ++++++++++--- spec/RegexVulnerabilities.spec.js | 4 ++-- spec/rest.spec.js | 10 ++++++---- src/Controllers/DatabaseController.js | 7 ++++++- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 8032d4b2f4..e21a049719 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -263,9 +263,14 @@ describe('Email Verification Token Expiration: ', () => { }) .then(() => { const config = Config.get('test'); - return config.database.find('_User', { - username: 'sets_email_verify_token_expires_at', - }, {}, Auth.maintenance(config)); + return config.database.find( + '_User', + { + username: 'sets_email_verify_token_expires_at', + }, + {}, + Auth.maintenance(config) + ); }) .then(results => { expect(results.length).toBe(1); @@ -500,7 +505,12 @@ describe('Email Verification Token Expiration: ', () => { .then(() => { const config = Config.get('test'); return config.database - .find('_User', { username: 'newEmailVerifyTokenOnEmailReset' }, {}, Auth.maintenance(config)) + .find( + '_User', + { username: 'newEmailVerifyTokenOnEmailReset' }, + {}, + Auth.maintenance(config) + ) .then(results => { return results[0]; }); diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index d112fceded..e4eff9021c 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -1,4 +1,5 @@ 'use strict'; +const Auth = require('../lib/Auth'); const UserController = require('../lib/Controllers/UserController').UserController; const Config = require('../lib/Config'); const validatorFail = () => { @@ -977,6 +978,7 @@ describe('ParseLiveQuery', function () { }; await reconfigureServer({ + maintenanceKey: 'test2', liveQuery: { classNames: [Parse.User], }, @@ -998,9 +1000,14 @@ describe('ParseLiveQuery', function () { .signUp() .then(() => { const config = Config.get('test'); - return config.database.find('_User', { - username: 'zxcv', - }); + return config.database.find( + '_User', + { + username: 'zxcv', + }, + {}, + Auth.maintenance(config) + ); }) .then(async results => { const foundUser = results[0]; diff --git a/spec/RegexVulnerabilities.spec.js b/spec/RegexVulnerabilities.spec.js index 008a653511..5d3bdf254d 100644 --- a/spec/RegexVulnerabilities.spec.js +++ b/spec/RegexVulnerabilities.spec.js @@ -109,7 +109,7 @@ describe('Regex Vulnerabilities', function () { 'X-Parse-Maintenance-Key': 'test2', 'Content-Type': 'application/json', }, - }).then(res => res.data) + }).then(res => res.data); // It should work await request({ url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${current._email_verify_token}`, @@ -184,7 +184,7 @@ describe('Regex Vulnerabilities', function () { 'X-Parse-Maintenance-Key': 'test2', 'Content-Type': 'application/json', }, - }).then(res => res.data) + }).then(res => res.data); const token = current._perishable_token; const passwordResetResponse = await request({ url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`, diff --git a/spec/rest.spec.js b/spec/rest.spec.js index 30b000c740..02d2f5960b 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -860,10 +860,12 @@ describe('read-only masterKey', () => { }); it('should throw when masterKey and maintenanceKey are the same', async () => { - await expectAsync(reconfigureServer({ - masterKey: 'yolo', - maintenanceKey: 'yolo', - })).toBeRejectedWith(new Error('masterKey and maintenanceKey should be different')); + await expectAsync( + reconfigureServer({ + masterKey: 'yolo', + maintenanceKey: 'yolo', + }) + ).toBeRejectedWith(new Error('masterKey and maintenanceKey should be different')); }); it('should throw when trying to create RestWrite', () => { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index ce0bd0ae05..b4c17beb02 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -68,7 +68,12 @@ const specialMasterQueryKeys = [ '_password_history', ]; -const validateQuery = (query: any, isMaster: boolean, isMaintenance: boolean, update: boolean): void => { +const validateQuery = ( + query: any, + isMaster: boolean, + isMaintenance: boolean, + update: boolean +): void => { if (isMaintenance) { isMaster = true; } From da1f4214c8fc1e13b619b28dbc5d6cb78790f8c2 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 27 Oct 2022 23:36:02 +1100 Subject: [PATCH 10/25] Update UserController.js --- src/Controllers/UserController.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 62c165c1e2..2ceaf2c4b7 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -69,12 +69,12 @@ export class UserController extends AdaptableController { updateFields._email_verify_token_expires_at = { __op: 'Delete' }; } - const masterAuth = Auth.master(this.config); + const maintenanceAuth = Auth.maintenance(this.config); var findUserForEmailVerification = new RestQuery( this.config, - Auth.master(this.config), + maintenanceAuth, '_User', - { username: username } + { username } ); return findUserForEmailVerification.execute().then(result => { if (result.results.length && result.results[0].emailVerified) { @@ -82,7 +82,7 @@ export class UserController extends AdaptableController { } else if (result.results.length) { query.objectId = result.results[0].objectId; } - return rest.update(this.config, masterAuth, '_User', query, updateFields); + return rest.update(this.config, maintenanceAuth, '_User', query, updateFields); }); } @@ -94,7 +94,8 @@ export class UserController extends AdaptableController { username: username, _perishable_token: token, }, - { limit: 1 } + { limit: 1 }, + Auth.maintenance(this.config) ) .then(results => { if (results.length != 1) { @@ -228,7 +229,8 @@ export class UserController extends AdaptableController { { username: email, email: { $exists: false }, _perishable_token: { $exists: true } }, ], }, - { limit: 1 } + { limit: 1 }, + Auth.maintenance(this.config) ); if (results.length == 1) { let expiresDate = results[0]._perishable_token_expires_at; From f2bcdcc0053a50fa65b575ea43236068bf2cf448 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 28 Oct 2022 00:15:05 +1100 Subject: [PATCH 11/25] fix tests --- spec/ParseUser.spec.js | 11 ++++++++--- src/Controllers/UserController.js | 9 +++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index b84a2de030..089e8689e2 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -3473,7 +3473,7 @@ describe('Parse.User testing', () => { ); }); - it('should allow updates to fields with masterKey', async () => { + it('should allow updates to fields with maintenanceKey', async () => { const emailAdapter = { sendVerificationEmail: () => {}, sendPasswordResetEmail: () => Promise.resolve(), @@ -3496,7 +3496,6 @@ describe('Parse.User testing', () => { }, emailAdapter: emailAdapter, publicServerURL: 'http://localhost:8378/1', - silent: false, }); await user.signUp(); for (let i = 0; i < 2; i++) { @@ -3558,7 +3557,13 @@ describe('Parse.User testing', () => { }).then(res => res.data.results[0]); for (const key in toSet) { const value = toSet[key]; - expect(update[key] === value || update[key] === value.toISOString()).toBeTrue(); + if (update[key] && update[key].iso) { + expect(update[key].iso).toEqual(value.toISOString()); + } else if (value.toISOString) { + expect(update[key]).toEqual(value.toISOString()); + } else { + expect(update[key]).toEqual(value); + } } }); diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 2ceaf2c4b7..6871add987 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -70,12 +70,9 @@ 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 = new RestQuery(this.config, maintenanceAuth, '_User', { + username, + }); return findUserForEmailVerification.execute().then(result => { if (result.results.length && result.results[0].emailVerified) { return Promise.resolve(result.results.length[0]); From 767e3a883a837f4270934661439e1cb47e51d167 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 23 Nov 2022 11:24:32 +1100 Subject: [PATCH 12/25] add ::1 --- src/Options/Definitions.js | 5 +++-- src/Options/docs.js | 2 +- src/Options/index.js | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index c208e83ad7..4d888f041b 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -302,9 +302,10 @@ module.exports.ParseServerOptions = { }, maintenanceKeyIps: { env: 'PARSE_SERVER_MAINTENANCE_KEY_IPS', - help: 'Restrict maintenanceKey to be used by only these ips, defaults to [] (allow all ips)', + help: + 'Restrict maintenanceKey to be used by only these ips, defaults to ["127.0.0.1", "::1"] (only allow current IP)', action: parsers.arrayParser, - default: ['127.0.0.1'], + default: ['127.0.0.1', '::1'], }, masterKey: { env: 'PARSE_SERVER_MASTER_KEY', diff --git a/src/Options/docs.js b/src/Options/docs.js index f6e8c6cd64..0e4bafa53f 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -58,7 +58,7 @@ * @property {String} logLevel Sets the level for logs * @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging * @property {String} maintenanceKey Your Parse Maintenance Key, used for updating internal fields - * @property {String[]} maintenanceKeyIps Restrict maintenanceKey to be used by only these ips, defaults to [] (allow all ips) + * @property {String[]} maintenanceKeyIps Restrict maintenanceKey to be used by only these ips, defaults to ["127.0.0.1", "::1"] (only allow current IP) * @property {String} masterKey Your Parse Master Key * @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1']` which means that only `localhost`, the server itself, is allowed to use the master key. * @property {Number} maxLimit Max value for limit option on queries, defaults to unlimited diff --git a/src/Options/index.js b/src/Options/index.js index 8e637a9fc0..5ac41dc040 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -54,8 +54,8 @@ export interface ParseServerOptions { /* (Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1']` which means that only `localhost`, the server itself, is allowed to use the master key. :DEFAULT: ["127.0.0.1"] */ masterKeyIps: ?(string[]); - /* Restrict maintenanceKey to be used by only these ips, defaults to ["127.0.0.1"] (only allow current IP) - :DEFAULT: ["127.0.0.1"] */ + /* Restrict maintenanceKey to be used by only these ips, defaults to ["127.0.0.1", "::1"] (only allow current IP) + :DEFAULT: ["127.0.0.1","::1"] */ maintenanceKeyIps: ?(string[]); /* Sets the app name */ appName: ?string; From d2c9e810724563bfbac63d40152f0c4cc00448ce Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 23 Nov 2022 11:29:17 +1100 Subject: [PATCH 13/25] prettier --- spec/Middlewares.spec.js | 1 - src/middlewares.js | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 16918f5064..6db32ae47a 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -158,7 +158,6 @@ describe('middlewares', () => { }); it('should succeed if the ip does belong to masterKeyIps list', async () => { - AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', masterKeyIps: ['10.0.0.1'], diff --git a/src/middlewares.js b/src/middlewares.js index 6117462ad0..8ee4e86988 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -165,8 +165,9 @@ export function handleParseHeaders(req, res, next) { req.config.headers = req.headers || {}; req.config.ip = clientIp; req.info = info; - - const isMaintenance = req.config.maintenanceKey && info.maintenanceKey === req.config.maintenanceKey; + + const isMaintenance = + req.config.maintenanceKey && info.maintenanceKey === req.config.maintenanceKey; if (isMaintenance && ipRangeCheck(clientIp, req.config.maintenanceKeyIps || [])) { req.auth = new auth.Auth({ config: req.config, From 518dec0e54653aa689dee5ca8614cdf36698051a Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 23 Nov 2022 12:55:02 +1100 Subject: [PATCH 14/25] Update package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index de92bbaf43..b60ceac132 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13384,7 +13384,7 @@ "postgres-bytea": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==" }, "postgres-date": { "version": "1.0.7", From 4dc463cc421d4567b6bfa0fe3417c4741685c1ba Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 27 Nov 2022 19:11:06 +1100 Subject: [PATCH 15/25] wip --- spec/helper.js | 1 - src/ParseServer.js | 7 ------- 2 files changed, 8 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index 40df5e627e..02a2dc4d72 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -110,7 +110,6 @@ const defaultConfiguration = { enableForAnonymousUser: true, enableForAuthenticatedUser: true, }, - masterKeyIps: ['127.0.0.1'], push: { android: { senderId: 'yolo', diff --git a/src/ParseServer.js b/src/ParseServer.js index 412d033d3c..eda6f4e1f2 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -467,13 +467,6 @@ function injectDefaults(options: ParseServerOptions) { }); } }); - - options.masterKeyIps = Array.from( - new Set(options.masterKeyIps.concat(defaults.masterKeyIps, options.masterKeyIps)) - ); - options.maintenanceKeyIps = Array.from( - new Set([...defaults.maintenanceKeyIps, options.maintenanceKeyIps]) - ); } // Those can't be tested as it requires a subprocess From dabd5013d78adf28b15e2f797f48ec8eaecf6fcf Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 15 Dec 2022 12:44:57 +1100 Subject: [PATCH 16/25] prettier --- spec/Middlewares.spec.js | 11 ++++++++--- src/middlewares.js | 22 ++++++++++++++-------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 6db32ae47a..57bd0891de 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -146,15 +146,20 @@ describe('middlewares', () => { expect(fakeReq.auth.isMaster).toBe(false); }); - it('should not succeed if the ip does not belong to maintenanceKeyIps list', () => { + it('should not succeed if the ip does not belong to maintenanceKeyIps list', async () => { + const logger = require('../lib/logger').logger; + spyOn(logger, 'error').and.callFake(() => {}); AppCache.put(fakeReq.body._ApplicationId, { maintenanceKey: 'masterKey', maintenanceKeyIps: ['ip1', 'ip2'], }); fakeReq.ip = 'ip3'; fakeReq.headers['x-parse-maintenance-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes); - expect(fakeRes.status).toHaveBeenCalledWith(403); + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaintenance).toBe(false); + expect(logger.error).toHaveBeenCalledWith( + `Request using maintenance key rejected as the request IP address 'ip3' is not set in Parse Server option 'maintenanceKeyIps'.` + ); }); it('should succeed if the ip does belong to masterKeyIps list', async () => { diff --git a/src/middlewares.js b/src/middlewares.js index 8ee4e86988..79c155f2ff 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -168,14 +168,20 @@ export function handleParseHeaders(req, res, next) { const isMaintenance = req.config.maintenanceKey && info.maintenanceKey === req.config.maintenanceKey; - if (isMaintenance && ipRangeCheck(clientIp, req.config.maintenanceKeyIps || [])) { - req.auth = new auth.Auth({ - config: req.config, - installationId: info.installationId, - isMaintenance: true, - }); - next(); - return; + if (isMaintenance) { + if (ipRangeCheck(clientIp, req.config.maintenanceKeyIps || [])) { + req.auth = new auth.Auth({ + config: req.config, + installationId: info.installationId, + isMaintenance: true, + }); + next(); + return; + } + const log = req.config?.loggerController || defaultLogger; + log.error( + `Request using maintenance key rejected as the request IP address '${clientIp}' is not set in Parse Server option 'maintenanceKeyIps'.` + ); } let isMaster = info.masterKey === req.config.masterKey; From 179879da9445af4e50ec6614bc3e5b1d14e51af2 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 17 Dec 2022 13:06:52 +0100 Subject: [PATCH 17/25] add access scopes to README --- README.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f33792296d..a6859252e6 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who --- -- [Flavors & Branches](#flavors--branches) +- [Flavors \& Branches](#flavors--branches) - [Long Term Support](#long-term-support) - [Getting Started](#getting-started) - [Running Parse Server](#running-parse-server) @@ -58,6 +58,7 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who - [Configuration](#configuration) - [Basic Options](#basic-options) - [Client Key Options](#client-key-options) + - [Access Scopes](#access-scopes) - [Email Verification and Password Reset](#email-verification-and-password-reset) - [Password and Account Policy](#password-and-account-policy) - [Custom Routes](#custom-routes) @@ -136,13 +137,13 @@ Parse Server is continuously tested with the most recent releases of Node.js to Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and [MongoDB lifecycle schedule](https://www.mongodb.com/support-policy/lifecycles) and only test against versions that are officially supported and have not reached their end-of-life date. We consider the end-of-life date of a MongoDB "rapid release" to be the same as its major version release. -| Version | Latest Version | End-of-Life | Compatible | -|-------------|----------------|---------------|--------------| -| MongoDB 4.0 | 4.0.28 | April 2022 | ✅ Yes | -| MongoDB 4.2 | 4.2.19 | April 2023 | ✅ Yes | -| MongoDB 4.4 | 4.4.13 | February 2024 | ✅ Yes | -| MongoDB 5 | 5.3.2 | October 2024 | ✅ Yes | -| MongoDB 6 | 6.0.2 | July 2025 | ✅ Yes | +| Version | Latest Version | End-of-Life | Compatible | +|-------------|----------------|---------------|------------| +| MongoDB 4.0 | 4.0.28 | April 2022 | ✅ Yes | +| MongoDB 4.2 | 4.2.19 | April 2023 | ✅ Yes | +| MongoDB 4.4 | 4.4.13 | February 2024 | ✅ Yes | +| MongoDB 5 | 5.3.2 | October 2024 | ✅ Yes | +| MongoDB 6 | 6.0.2 | July 2025 | ✅ Yes | #### PostgreSQL @@ -330,6 +331,15 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo * `restAPIKey` * `dotNetKey` +## Access Scopes + +| Scope | Internal data | Custom data | Restricted by CLP, ACL | Key | +|----------------|---------------|-------------|------------------------|---------------------| +| Internal | r/w | r/w | no | `maintenanceKey` | +| Master | -/- | r/w | no | `masterKey` | +| ReadOnlyMaster | -/- | r/- | no | `readOnlyMasterKey` | +| Session | -/- | r/w | yes | `sessionToken` | + ## Email Verification and Password Reset Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) for more details and a full list of available options. From 1987968a81d0471a71c848aad433f7d11bcc1eda Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 22 Dec 2022 15:27:03 +1100 Subject: [PATCH 18/25] Update Middlewares.spec.js --- spec/Middlewares.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 6db32ae47a..58dc449913 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -149,9 +149,9 @@ describe('middlewares', () => { it('should not succeed if the ip does not belong to maintenanceKeyIps list', () => { AppCache.put(fakeReq.body._ApplicationId, { maintenanceKey: 'masterKey', - maintenanceKeyIps: ['ip1', 'ip2'], + maintenanceKeyIps: ['10.0.0.0', '10.0.0.1'], }); - fakeReq.ip = 'ip3'; + fakeReq.ip = '10.0.0.2'; fakeReq.headers['x-parse-maintenance-key'] = 'masterKey'; middlewares.handleParseHeaders(fakeReq, fakeRes); expect(fakeRes.status).toHaveBeenCalledWith(403); From 57abc1f9301342f0578c8411cf8d6324efa0b02d Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 22 Dec 2022 15:28:26 +1100 Subject: [PATCH 19/25] Update ParseUser.spec.js --- spec/ParseUser.spec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index aaebb1096b..bf728cfcab 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -3576,13 +3576,14 @@ describe('Parse.User testing', () => { try { await Parse.User.logIn(user.getEmail(), 'abc'); } catch (e) { - /* */ + expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND); + expect(e.message === 'Invalid username/password.' || e.message === 'Your account is locked due to multiple failed login attempts. Please try again after 1 minute(s)').toBeTrue(); } } await Parse.User.requestPasswordReset(user.getEmail()); const headers = { 'X-Parse-Application-Id': 'test', - 'X-Parse-Rest-API-Key': 'test', + 'X-Parse-Rest-API-Key': 'rest', 'X-Parse-Maintenance-Key': 'test2', 'Content-Type': 'application/json', }; From 82bf8a910606ddbb14e641a89ff3a12f2c8814e6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 22 Dec 2022 15:28:52 +1100 Subject: [PATCH 20/25] Update src/Options/index.js Co-authored-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- src/Options/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Options/index.js b/src/Options/index.js index 42718f0ee4..0f9e0018bf 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -54,7 +54,7 @@ export interface ParseServerOptions { /* (Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key. :DEFAULT: ["127.0.0.1","::1"] */ masterKeyIps: ?(string[]); - /* Restrict maintenanceKey to be used by only these ips, defaults to ["127.0.0.1", "::1"] (only allow current IP) + /* (Optional) Restricts the use of maintenance key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `maintenanceKey` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the maintenance key can be used from any IP address.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the maintenance key. :DEFAULT: ["127.0.0.1","::1"] */ maintenanceKeyIps: ?(string[]); /* Sets the app name */ From c57efb1fc7eb9796a422368b032e14cf6f108c21 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 22 Dec 2022 15:29:00 +1100 Subject: [PATCH 21/25] Update src/Options/index.js Co-authored-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- src/Options/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Options/index.js b/src/Options/index.js index 0f9e0018bf..92f7074e22 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -46,7 +46,7 @@ export interface ParseServerOptions { appId: string; /* Your Parse Master Key */ masterKey: string; - /* Your Parse Maintenance Key, used for updating internal fields */ + /* (Optional) The maintenance key is used for modifying internal fields of Parse Server.

⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server. */ maintenanceKey: string; /* URL to your parse server with http:// or https://. :ENV: PARSE_SERVER_URL */ From 687893bbaf43d4ee3336ec553e2d43a4a985a6f1 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 22 Dec 2022 15:31:47 +1100 Subject: [PATCH 22/25] docs --- src/Options/Definitions.js | 7 ++++--- src/Options/docs.js | 6 +++--- src/Options/index.js | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index aa452aaacc..1e70cc9016 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -303,13 +303,14 @@ module.exports.ParseServerOptions = { }, maintenanceKey: { env: 'PARSE_SERVER_MAINTENANCE_KEY', - help: 'Your Parse Maintenance Key, used for updating internal fields', + help: + '(Optional) The maintenance key is used for modifying internal fields of Parse Server.

\u26A0\uFE0F This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.', required: true, }, maintenanceKeyIps: { env: 'PARSE_SERVER_MAINTENANCE_KEY_IPS', help: - 'Restrict maintenanceKey to be used by only these ips, defaults to ["127.0.0.1", "::1"] (only allow current IP)', + "(Optional) Restricts the use of maintenance key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `maintenanceKey` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the maintenance key can be used from any IP address.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the maintenance key.", action: parsers.arrayParser, default: ['127.0.0.1', '::1'], }, @@ -321,7 +322,7 @@ module.exports.ParseServerOptions = { masterKeyIps: { env: 'PARSE_SERVER_MASTER_KEY_IPS', help: - "(Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key.", + "(Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key.", action: parsers.arrayParser, default: ['127.0.0.1', '::1'], }, diff --git a/src/Options/docs.js b/src/Options/docs.js index 7ce8b4fbb1..51eb7f541c 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -58,10 +58,10 @@ * @property {String} logLevel Sets the level for logs * @property {LogLevels} logLevels (Optional) Overrides the log levels used internally by Parse Server to log events. * @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging - * @property {String} maintenanceKey Your Parse Maintenance Key, used for updating internal fields - * @property {String[]} maintenanceKeyIps Restrict maintenanceKey to be used by only these ips, defaults to ["127.0.0.1", "::1"] (only allow current IP) + * @property {String} maintenanceKey (Optional) The maintenance key is used for modifying internal fields of Parse Server.

⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server. + * @property {String[]} maintenanceKeyIps (Optional) Restricts the use of maintenance key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `maintenanceKey` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the maintenance key can be used from any IP address.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the maintenance key. * @property {String} masterKey Your Parse Master Key - * @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key. + * @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key. * @property {Number} maxLimit Max value for limit option on queries, defaults to unlimited * @property {Number|String} maxLogFiles Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null) * @property {String} maxUploadSize Max file size for uploads, defaults to 20mb diff --git a/src/Options/index.js b/src/Options/index.js index 92f7074e22..89644910cd 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -51,7 +51,7 @@ export interface ParseServerOptions { /* URL to your parse server with http:// or https://. :ENV: PARSE_SERVER_URL */ serverURL: string; - /* (Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key. + /* (Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key. :DEFAULT: ["127.0.0.1","::1"] */ masterKeyIps: ?(string[]); /* (Optional) Restricts the use of maintenance key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `maintenanceKey` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the maintenance key can be used from any IP address.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the maintenance key. From 5fce261f262d8af1b46cf9b90891b2d96ae5afbf Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 22 Dec 2022 15:46:04 +1100 Subject: [PATCH 23/25] Update Middlewares.spec.js --- spec/Middlewares.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 49983381ab..485d719f1d 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -174,7 +174,7 @@ describe('middlewares', () => { await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); expect(fakeReq.auth.isMaintenance).toBe(false); expect(logger.error).toHaveBeenCalledWith( - `Request using maintenance key rejected as the request IP address 'ip3' is not set in Parse Server option 'maintenanceKeyIps'.` + `Request using maintenance key rejected as the request IP address '10.0.0.2' is not set in Parse Server option 'maintenanceKeyIps'.` ); }); From 4e2469329a64e0732d51ab809a16f9dfcb4e5cdb Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 22 Dec 2022 16:02:55 +1100 Subject: [PATCH 24/25] Update ParseUser.spec.js --- spec/ParseUser.spec.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index bf728cfcab..5fb2f1e6cc 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -3577,7 +3577,11 @@ describe('Parse.User testing', () => { await Parse.User.logIn(user.getEmail(), 'abc'); } catch (e) { expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND); - expect(e.message === 'Invalid username/password.' || e.message === 'Your account is locked due to multiple failed login attempts. Please try again after 1 minute(s)').toBeTrue(); + expect( + e.message === 'Invalid username/password.' || + e.message === + 'Your account is locked due to multiple failed login attempts. Please try again after 1 minute(s)' + ).toBeTrue(); } } await Parse.User.requestPasswordReset(user.getEmail()); From 746c978b02682efb507e982b484488896e68575c Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 8 Jan 2023 17:42:55 +0100 Subject: [PATCH 25/25] use example domain --- spec/ParseUser.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 5fb2f1e6cc..4d3beaf349 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -3557,7 +3557,7 @@ describe('Parse.User testing', () => { user.set({ username: 'hello', password: 'world', - email: 'test@email.com', + email: 'test@example.com', }); await reconfigureServer({ appName: 'unused',