From 19507722c7ed8042b614d4541e4bdd8a59aded03 Mon Sep 17 00:00:00 2001 From: Mattia Faraci Date: Fri, 10 Nov 2023 16:47:05 +0100 Subject: [PATCH 1/5] feat: disable collation, transformEmailToLowerCase and transformUsernameToLowerCase --- spec/DatabaseController.spec.js | 248 ++++++++++++++++++++++++++ src/Controllers/DatabaseController.js | 20 +++ src/Options/Definitions.js | 18 ++ src/Options/docs.js | 2 + src/Options/index.js | 6 + 5 files changed, 294 insertions(+) diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js index 98103ce6e4c..31c7329710d 100644 --- a/spec/DatabaseController.spec.js +++ b/spec/DatabaseController.spec.js @@ -1,3 +1,4 @@ +const Config = require('../lib/Config'); const DatabaseController = require('../lib/Controllers/DatabaseController.js'); const validateQuery = DatabaseController._validateQuery; @@ -361,6 +362,253 @@ describe('DatabaseController', function () { done(); }); }); + + describe('disableCollation', () => { + const dummyStorageAdapter = { + find: () => Promise.resolve([]), + watch: () => Promise.resolve(), + getAllClasses: () => Promise.resolve([]), + }; + + beforeEach(() => { + Config.get(Parse.applicationId).schemaCache.clear(); + }); + + it('should force caseInsensitive to false with disableCollation option', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, { + disableCollation: true, + }); + const spy = spyOn(dummyStorageAdapter, 'find'); + spy.and.callThrough(); + await databaseController.find('SomeClass', {}, { caseInsensitive: true }); + expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(false); + }); + + it('should support caseInsensitive without disableCollation option', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, {}); + const spy = spyOn(dummyStorageAdapter, 'find'); + spy.and.callThrough(); + await databaseController.find('_User', {}, { caseInsensitive: true }); + expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(true); + }); + + it_only_db('mongo')('should create insensitive indexes without disableCollation', async () => { + await reconfigureServer({ + databaseURI: 'mongodb://localhost:27017/disableCollationFalse', + databaseAdapter: undefined, + }); + const user = new Parse.User(); + await user.save({ + username: 'example', + password: 'password', + email: 'example@example.com', + }); + const schemas = await Parse.Schema.all(); + const UserSchema = schemas.find(({ className }) => className === '_User'); + expect(UserSchema.indexes).toEqual({ + _id_: { _id: 1 }, + username_1: { username: 1 }, + case_insensitive_username: { username: 1 }, + case_insensitive_email: { email: 1 }, + email_1: { email: 1 }, + }); + }); + + it_only_db('mongo')('should not create insensitive indexes with disableCollation', async () => { + await reconfigureServer({ + disableCollation: true, + databaseURI: 'mongodb://localhost:27017/disableCollationTrue', + databaseAdapter: undefined, + }); + const user = new Parse.User(); + await user.save({ + username: 'example', + password: 'password', + email: 'example@example.com', + }); + const schemas = await Parse.Schema.all(); + const UserSchema = schemas.find(({ className }) => className === '_User'); + expect(UserSchema.indexes).toEqual({ + _id_: { _id: 1 }, + username_1: { username: 1 }, + email_1: { email: 1 }, + }); + }); + }); + + describe('transformEmailToLowerCase', () => { + const dummyStorageAdapter = { + createObject: () => Promise.resolve({ ops: [{}] }), + findOneAndUpdate: () => Promise.resolve({}), + watch: () => Promise.resolve(), + getAllClasses: () => + Promise.resolve([ + { + className: '_User', + fields: { email: 'String' }, + indexes: {}, + classLevelPermissions: { protectedFields: {} }, + }, + ]), + }; + const dates = { + createdAt: { iso: undefined, __type: 'Date' }, + updatedAt: { iso: undefined, __type: 'Date' }, + }; + + it('should not transform email to lower case without transformEmailToLowerCase option on create', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, {}); + const spy = spyOn(dummyStorageAdapter, 'createObject'); + spy.and.callThrough(); + await databaseController.create('_User', { + email: 'EXAMPLE@EXAMPLE.COM', + }); + expect(spy.calls.all()[0].args[2]).toEqual({ + email: 'EXAMPLE@EXAMPLE.COM', + ...dates, + }); + }); + + it('should transform email to lower case with transformEmailToLowerCase option on create', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, { + transformEmailToLowerCase: true, + }); + const spy = spyOn(dummyStorageAdapter, 'createObject'); + spy.and.callThrough(); + await databaseController.create('_User', { + email: 'EXAMPLE@EXAMPLE.COM', + }); + expect(spy.calls.all()[0].args[2]).toEqual({ + email: 'example@example.com', + ...dates, + }); + }); + + it('should not transform email to lower case without transformEmailToLowerCase option on update', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, {}); + const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate'); + spy.and.callThrough(); + await databaseController.update('_User', { id: 'example' }, { email: 'EXAMPLE@EXAMPLE.COM' }); + expect(spy.calls.all()[0].args[3]).toEqual({ + email: 'EXAMPLE@EXAMPLE.COM', + }); + }); + + it('should transform email to lower case with transformEmailToLowerCase option on update', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, { + transformEmailToLowerCase: true, + }); + const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate'); + spy.and.callThrough(); + await databaseController.update('_User', { id: 'example' }, { email: 'EXAMPLE@EXAMPLE.COM' }); + expect(spy.calls.all()[0].args[3]).toEqual({ + email: 'example@example.com', + }); + }); + + it('should not find a case insensitive user by email with transformEmailToLowerCase', async () => { + await reconfigureServer({ transformEmailToLowerCase: true }); + const user = new Parse.User(); + await user.save({ email: 'EXAMPLE@EXAMPLE.COM', password: 'password' }); + + const query = new Parse.Query(Parse.User); + query.equalTo('email', 'EXAMPLE@EXAMPLE.COM'); + const result = await query.find({ useMasterKey: true }); + expect(result.length).toEqual(0); + + const query2 = new Parse.Query(Parse.User); + query2.equalTo('email', 'example@example.com'); + const result2 = await query2.find({ useMasterKey: true }); + expect(result2.length).toEqual(1); + }); + }); + + describe('transformUsernameToLowerCase', () => { + const dummyStorageAdapter = { + createObject: () => Promise.resolve({ ops: [{}] }), + findOneAndUpdate: () => Promise.resolve({}), + watch: () => Promise.resolve(), + getAllClasses: () => + Promise.resolve([ + { + className: '_User', + fields: { username: 'String' }, + indexes: {}, + classLevelPermissions: { protectedFields: {} }, + }, + ]), + }; + const dates = { + createdAt: { iso: undefined, __type: 'Date' }, + updatedAt: { iso: undefined, __type: 'Date' }, + }; + + it('should not transform username to lower case without transformUsernameToLowerCase option on create', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, {}); + const spy = spyOn(dummyStorageAdapter, 'createObject'); + spy.and.callThrough(); + await databaseController.create('_User', { + username: 'EXAMPLE', + }); + expect(spy.calls.all()[0].args[2]).toEqual({ + username: 'EXAMPLE', + ...dates, + }); + }); + + it('should transform username to lower case with transformUsernameToLowerCase option on create', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, { + transformUsernameToLowerCase: true, + }); + const spy = spyOn(dummyStorageAdapter, 'createObject'); + spy.and.callThrough(); + await databaseController.create('_User', { + username: 'EXAMPLE', + }); + expect(spy.calls.all()[0].args[2]).toEqual({ + username: 'example', + ...dates, + }); + }); + + it('should not transform username to lower case without transformUsernameToLowerCase option on update', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, {}); + const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate'); + spy.and.callThrough(); + await databaseController.update('_User', { id: 'example' }, { username: 'EXAMPLE' }); + expect(spy.calls.all()[0].args[3]).toEqual({ + username: 'EXAMPLE', + }); + }); + + it('should transform username to lower case with transformUsernameToLowerCase option on update', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, { + transformUsernameToLowerCase: true, + }); + const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate'); + spy.and.callThrough(); + await databaseController.update('_User', { id: 'example' }, { username: 'EXAMPLE' }); + expect(spy.calls.all()[0].args[3]).toEqual({ + username: 'example', + }); + }); + + it('should not find a case insensitive user by username with transformUsernameToLowerCase', async () => { + await reconfigureServer({ transformUsernameToLowerCase: true }); + const user = new Parse.User(); + await user.save({ username: 'EXAMPLE', password: 'password' }); + + const query = new Parse.Query(Parse.User); + query.equalTo('username', 'EXAMPLE'); + const result = await query.find({ useMasterKey: true }); + expect(result.length).toEqual(0); + + const query2 = new Parse.Query(Parse.User); + query2.equalTo('username', 'example'); + const result2 = await query2.find({ useMasterKey: true }); + expect(result2.length).toEqual(1); + }); + }); }); function buildCLP(pointerNames) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index defb7976c4e..be26d287aee 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -368,6 +368,22 @@ const relationSchema = { fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } }, }; +const transformEmailToLowerCase = (object, className, options) => { + if (className === '_User' && options.transformEmailToLowerCase) { + if (typeof object['email'] === 'string') { + object['email'] = object['email'].toLowerCase(); + } + } +}; + +const transformUsernameToLowerCase = (object, className, options) => { + if (className === '_User' && options.transformUsernameToLowerCase) { + if (typeof object['username'] === 'string') { + object['username'] = object['username'].toLowerCase(); + } + } +}; + class DatabaseController { adapter: StorageAdapter; schemaCache: any; @@ -573,6 +589,8 @@ class DatabaseController { } } update = transformObjectACL(update); + transformEmailToLowerCase(update, className, this.options); + transformUsernameToLowerCase(update, className, this.options); transformAuthData(className, update, schema); if (validateOnly) { return this.adapter.find(className, schema, query, {}).then(result => { @@ -822,6 +840,8 @@ class DatabaseController { const originalObject = object; object = transformObjectACL(object); + transformEmailToLowerCase(object, className, this.options); + transformUsernameToLowerCase(object, className, this.options); object.createdAt = { iso: object.createdAt, __type: 'Date' }; object.updatedAt = { iso: object.updatedAt, __type: 'Date' }; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index f35300b2870..aaf130a095c 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -175,6 +175,12 @@ module.exports.ParseServerOptions = { action: parsers.booleanParser, default: true, }, + disableCollation: { + env: 'PARSE_SERVER_DISABLE_COLLATION', + help: + 'Disable case insensitivity (collation) on queries and indexes, needed if you use MongoDB serverless or AWS DocumentDB.', + action: parsers.booleanParser, + }, dotNetKey: { env: 'PARSE_SERVER_DOT_NET_KEY', help: 'Key for Unity and .Net SDK', @@ -533,6 +539,18 @@ module.exports.ParseServerOptions = { help: 'Starts the liveQuery server', action: parsers.booleanParser, }, + transformEmailToLowerCase: { + env: 'PARSE_SERVER_TRANSFORM_EMAIL_TO_LOWER_CASE', + help: + 'Transform Email to lowercase on create/update/login/signup. On queries client needs to ensure to send Email in lowercase format.', + action: parsers.booleanParser, + }, + transformUsernameToLowerCase: { + env: 'PARSE_SERVER_TRANSFORM_USERNAME_TO_LOWER_CASE', + help: + 'Transform Username to lowercase on create/update/login/signup. On queries client needs to ensure to send Username in lowercase format.', + action: parsers.booleanParser, + }, trustProxy: { env: 'PARSE_SERVER_TRUST_PROXY', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index cf672cc6cb9..5d0e74c466b 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -96,6 +96,8 @@ * @property {Number} sessionLength Session duration, in seconds, defaults to 1 year * @property {Boolean} silent Disables console output * @property {Boolean} startLiveQueryServer Starts the liveQuery server + * @property {Boolean} transformEmailToLowerCase Transform Email to lowercase on create/update/login/signup. On queries client needs to ensure to send Email in lowercase format. + * @property {Boolean} transformUsernameToLowerCase Transform Username to lowercase on create/update/login/signup. On queries client needs to ensure to send Username in lowercase format. * @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 diff --git a/src/Options/index.js b/src/Options/index.js index 996512e36ea..2d57a4a4983 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -103,6 +103,12 @@ export interface ParseServerOptions { databaseOptions: ?DatabaseOptions; /* Adapter module for the database; any options that are not explicitly described here are passed directly to the database client. */ databaseAdapter: ?Adapter; + /* Disable case insensitivity (collation) on queries and indexes, needed if you use MongoDB serverless or AWS DocumentDB. */ + disableCollation: ?boolean; + /* Transform Email to lowercase on create/update/login/signup. On queries client needs to ensure to send Email in lowercase format. */ + transformEmailToLowerCase: ?boolean; + /* Transform Username to lowercase on create/update/login/signup. On queries client needs to ensure to send Username in lowercase format. */ + transformUsernameToLowerCase: ?boolean; /* Full path to your cloud code main.js */ cloud: ?string; /* A collection prefix for the classes From f292d28d3ad35f2b3c46bc2b38420d872d3bd7d6 Mon Sep 17 00:00:00 2001 From: Mattia Faraci <82614156+mattia1208@users.noreply.github.com> Date: Sat, 11 Nov 2023 15:42:35 +0100 Subject: [PATCH 2/5] Update src/Options/index.js Co-authored-by: Manuel <5673677+mtrezza@users.noreply.github.com> Signed-off-by: Mattia Faraci <82614156+mattia1208@users.noreply.github.com> --- src/Options/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Options/index.js b/src/Options/index.js index 2d57a4a4983..958ffe33de8 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -103,8 +103,9 @@ export interface ParseServerOptions { databaseOptions: ?DatabaseOptions; /* Adapter module for the database; any options that are not explicitly described here are passed directly to the database client. */ databaseAdapter: ?Adapter; - /* Disable case insensitivity (collation) on queries and indexes, needed if you use MongoDB serverless or AWS DocumentDB. */ - disableCollation: ?boolean; + /* Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`. + :DEFAULT: false */ + enableCollationCaseComparison: ?boolean; /* Transform Email to lowercase on create/update/login/signup. On queries client needs to ensure to send Email in lowercase format. */ transformEmailToLowerCase: ?boolean; /* Transform Username to lowercase on create/update/login/signup. On queries client needs to ensure to send Username in lowercase format. */ From 622dffac8f231391394936ff7760f6738d451918 Mon Sep 17 00:00:00 2001 From: Mattia Faraci <82614156+mattia1208@users.noreply.github.com> Date: Sat, 11 Nov 2023 15:42:50 +0100 Subject: [PATCH 3/5] Update src/Options/index.js Co-authored-by: Manuel <5673677+mtrezza@users.noreply.github.com> Signed-off-by: Mattia Faraci <82614156+mattia1208@users.noreply.github.com> --- src/Options/index.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Options/index.js b/src/Options/index.js index 958ffe33de8..9af83ecd829 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -106,10 +106,12 @@ export interface ParseServerOptions { /* Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`. :DEFAULT: false */ enableCollationCaseComparison: ?boolean; - /* Transform Email to lowercase on create/update/login/signup. On queries client needs to ensure to send Email in lowercase format. */ - transformEmailToLowerCase: ?boolean; - /* Transform Username to lowercase on create/update/login/signup. On queries client needs to ensure to send Username in lowercase format. */ - transformUsernameToLowerCase: ?boolean; + /* Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`. + :DEFAULT: false */ + convertEmailToLowercase: ?boolean; + /* Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`. + :DEFAULT: false */ + convertUsernameToLowercase: ?boolean; /* Full path to your cloud code main.js */ cloud: ?string; /* A collection prefix for the classes From 7bff03cd3ec611bbe1de2f88aca99b1f5ba9a45f Mon Sep 17 00:00:00 2001 From: Mattia Faraci Date: Sat, 11 Nov 2023 16:57:53 +0100 Subject: [PATCH 4/5] Change options name --- spec/DatabaseController.spec.js | 134 ++++++++++++++------------ src/Controllers/DatabaseController.js | 16 +-- src/Options/Definitions.js | 39 ++++---- src/Options/docs.js | 5 +- 4 files changed, 102 insertions(+), 92 deletions(-) diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js index 31c7329710d..4633f1c26f7 100644 --- a/spec/DatabaseController.spec.js +++ b/spec/DatabaseController.spec.js @@ -363,7 +363,7 @@ describe('DatabaseController', function () { }); }); - describe('disableCollation', () => { + describe('enableCollationCaseComparison', () => { const dummyStorageAdapter = { find: () => Promise.resolve([]), watch: () => Promise.resolve(), @@ -374,9 +374,9 @@ describe('DatabaseController', function () { Config.get(Parse.applicationId).schemaCache.clear(); }); - it('should force caseInsensitive to false with disableCollation option', async () => { + it('should force caseInsensitive to false with enableCollationCaseComparison option', async () => { const databaseController = new DatabaseController(dummyStorageAdapter, { - disableCollation: true, + enableCollationCaseComparison: true, }); const spy = spyOn(dummyStorageAdapter, 'find'); spy.and.callThrough(); @@ -384,7 +384,7 @@ describe('DatabaseController', function () { expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(false); }); - it('should support caseInsensitive without disableCollation option', async () => { + it('should support caseInsensitive without enableCollationCaseComparison option', async () => { const databaseController = new DatabaseController(dummyStorageAdapter, {}); const spy = spyOn(dummyStorageAdapter, 'find'); spy.and.callThrough(); @@ -392,51 +392,57 @@ describe('DatabaseController', function () { expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(true); }); - it_only_db('mongo')('should create insensitive indexes without disableCollation', async () => { - await reconfigureServer({ - databaseURI: 'mongodb://localhost:27017/disableCollationFalse', - databaseAdapter: undefined, - }); - const user = new Parse.User(); - await user.save({ - username: 'example', - password: 'password', - email: 'example@example.com', - }); - const schemas = await Parse.Schema.all(); - const UserSchema = schemas.find(({ className }) => className === '_User'); - expect(UserSchema.indexes).toEqual({ - _id_: { _id: 1 }, - username_1: { username: 1 }, - case_insensitive_username: { username: 1 }, - case_insensitive_email: { email: 1 }, - email_1: { email: 1 }, - }); - }); - - it_only_db('mongo')('should not create insensitive indexes with disableCollation', async () => { - await reconfigureServer({ - disableCollation: true, - databaseURI: 'mongodb://localhost:27017/disableCollationTrue', - databaseAdapter: undefined, - }); - const user = new Parse.User(); - await user.save({ - username: 'example', - password: 'password', - email: 'example@example.com', - }); - const schemas = await Parse.Schema.all(); - const UserSchema = schemas.find(({ className }) => className === '_User'); - expect(UserSchema.indexes).toEqual({ - _id_: { _id: 1 }, - username_1: { username: 1 }, - email_1: { email: 1 }, - }); - }); + it_only_db('mongo')( + 'should create insensitive indexes without enableCollationCaseComparison', + async () => { + await reconfigureServer({ + databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonFalse', + databaseAdapter: undefined, + }); + const user = new Parse.User(); + await user.save({ + username: 'example', + password: 'password', + email: 'example@example.com', + }); + const schemas = await Parse.Schema.all(); + const UserSchema = schemas.find(({ className }) => className === '_User'); + expect(UserSchema.indexes).toEqual({ + _id_: { _id: 1 }, + username_1: { username: 1 }, + case_insensitive_username: { username: 1 }, + case_insensitive_email: { email: 1 }, + email_1: { email: 1 }, + }); + } + ); + + it_only_db('mongo')( + 'should not create insensitive indexes with enableCollationCaseComparison', + async () => { + await reconfigureServer({ + enableCollationCaseComparison: true, + databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonTrue', + databaseAdapter: undefined, + }); + const user = new Parse.User(); + await user.save({ + username: 'example', + password: 'password', + email: 'example@example.com', + }); + const schemas = await Parse.Schema.all(); + const UserSchema = schemas.find(({ className }) => className === '_User'); + expect(UserSchema.indexes).toEqual({ + _id_: { _id: 1 }, + username_1: { username: 1 }, + email_1: { email: 1 }, + }); + } + ); }); - describe('transformEmailToLowerCase', () => { + describe('convertEmailToLowercase', () => { const dummyStorageAdapter = { createObject: () => Promise.resolve({ ops: [{}] }), findOneAndUpdate: () => Promise.resolve({}), @@ -456,7 +462,7 @@ describe('DatabaseController', function () { updatedAt: { iso: undefined, __type: 'Date' }, }; - it('should not transform email to lower case without transformEmailToLowerCase option on create', async () => { + it('should not transform email to lower case without convertEmailToLowercase option on create', async () => { const databaseController = new DatabaseController(dummyStorageAdapter, {}); const spy = spyOn(dummyStorageAdapter, 'createObject'); spy.and.callThrough(); @@ -469,9 +475,9 @@ describe('DatabaseController', function () { }); }); - it('should transform email to lower case with transformEmailToLowerCase option on create', async () => { + it('should transform email to lower case with convertEmailToLowercase option on create', async () => { const databaseController = new DatabaseController(dummyStorageAdapter, { - transformEmailToLowerCase: true, + convertEmailToLowercase: true, }); const spy = spyOn(dummyStorageAdapter, 'createObject'); spy.and.callThrough(); @@ -484,7 +490,7 @@ describe('DatabaseController', function () { }); }); - it('should not transform email to lower case without transformEmailToLowerCase option on update', async () => { + it('should not transform email to lower case without convertEmailToLowercase option on update', async () => { const databaseController = new DatabaseController(dummyStorageAdapter, {}); const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate'); spy.and.callThrough(); @@ -494,9 +500,9 @@ describe('DatabaseController', function () { }); }); - it('should transform email to lower case with transformEmailToLowerCase option on update', async () => { + it('should transform email to lower case with convertEmailToLowercase option on update', async () => { const databaseController = new DatabaseController(dummyStorageAdapter, { - transformEmailToLowerCase: true, + convertEmailToLowercase: true, }); const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate'); spy.and.callThrough(); @@ -506,8 +512,8 @@ describe('DatabaseController', function () { }); }); - it('should not find a case insensitive user by email with transformEmailToLowerCase', async () => { - await reconfigureServer({ transformEmailToLowerCase: true }); + it('should not find a case insensitive user by email with convertEmailToLowercase', async () => { + await reconfigureServer({ convertEmailToLowercase: true }); const user = new Parse.User(); await user.save({ email: 'EXAMPLE@EXAMPLE.COM', password: 'password' }); @@ -523,7 +529,7 @@ describe('DatabaseController', function () { }); }); - describe('transformUsernameToLowerCase', () => { + describe('convertUsernameToLowercase', () => { const dummyStorageAdapter = { createObject: () => Promise.resolve({ ops: [{}] }), findOneAndUpdate: () => Promise.resolve({}), @@ -543,7 +549,7 @@ describe('DatabaseController', function () { updatedAt: { iso: undefined, __type: 'Date' }, }; - it('should not transform username to lower case without transformUsernameToLowerCase option on create', async () => { + it('should not transform username to lower case without convertUsernameToLowercase option on create', async () => { const databaseController = new DatabaseController(dummyStorageAdapter, {}); const spy = spyOn(dummyStorageAdapter, 'createObject'); spy.and.callThrough(); @@ -556,9 +562,9 @@ describe('DatabaseController', function () { }); }); - it('should transform username to lower case with transformUsernameToLowerCase option on create', async () => { + it('should transform username to lower case with convertUsernameToLowercase option on create', async () => { const databaseController = new DatabaseController(dummyStorageAdapter, { - transformUsernameToLowerCase: true, + convertUsernameToLowercase: true, }); const spy = spyOn(dummyStorageAdapter, 'createObject'); spy.and.callThrough(); @@ -571,7 +577,7 @@ describe('DatabaseController', function () { }); }); - it('should not transform username to lower case without transformUsernameToLowerCase option on update', async () => { + it('should not transform username to lower case without convertUsernameToLowercase option on update', async () => { const databaseController = new DatabaseController(dummyStorageAdapter, {}); const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate'); spy.and.callThrough(); @@ -581,9 +587,9 @@ describe('DatabaseController', function () { }); }); - it('should transform username to lower case with transformUsernameToLowerCase option on update', async () => { + it('should transform username to lower case with convertUsernameToLowercase option on update', async () => { const databaseController = new DatabaseController(dummyStorageAdapter, { - transformUsernameToLowerCase: true, + convertUsernameToLowercase: true, }); const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate'); spy.and.callThrough(); @@ -593,8 +599,8 @@ describe('DatabaseController', function () { }); }); - it('should not find a case insensitive user by username with transformUsernameToLowerCase', async () => { - await reconfigureServer({ transformUsernameToLowerCase: true }); + it('should not find a case insensitive user by username with convertUsernameToLowercase', async () => { + await reconfigureServer({ convertUsernameToLowercase: true }); const user = new Parse.User(); await user.save({ username: 'EXAMPLE', password: 'password' }); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index be26d287aee..c1dbfdacfe4 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -368,16 +368,16 @@ const relationSchema = { fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } }, }; -const transformEmailToLowerCase = (object, className, options) => { - if (className === '_User' && options.transformEmailToLowerCase) { +const convertEmailToLowercase = (object, className, options) => { + if (className === '_User' && options.convertEmailToLowercase) { if (typeof object['email'] === 'string') { object['email'] = object['email'].toLowerCase(); } } }; -const transformUsernameToLowerCase = (object, className, options) => { - if (className === '_User' && options.transformUsernameToLowerCase) { +const convertUsernameToLowercase = (object, className, options) => { + if (className === '_User' && options.convertUsernameToLowercase) { if (typeof object['username'] === 'string') { object['username'] = object['username'].toLowerCase(); } @@ -589,8 +589,8 @@ class DatabaseController { } } update = transformObjectACL(update); - transformEmailToLowerCase(update, className, this.options); - transformUsernameToLowerCase(update, className, this.options); + convertEmailToLowercase(update, className, this.options); + convertUsernameToLowercase(update, className, this.options); transformAuthData(className, update, schema); if (validateOnly) { return this.adapter.find(className, schema, query, {}).then(result => { @@ -840,8 +840,8 @@ class DatabaseController { const originalObject = object; object = transformObjectACL(object); - transformEmailToLowerCase(object, className, this.options); - transformUsernameToLowerCase(object, className, this.options); + convertEmailToLowercase(object, className, this.options); + convertUsernameToLowercase(object, className, this.options); object.createdAt = { iso: object.createdAt, __type: 'Date' }; object.updatedAt = { iso: object.updatedAt, __type: 'Date' }; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index aaf130a095c..c37e6adfc3e 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -139,6 +139,20 @@ module.exports.ParseServerOptions = { help: 'A collection prefix for the classes', default: '', }, + convertEmailToLowercase: { + env: 'PARSE_SERVER_CONVERT_EMAIL_TO_LOWERCASE', + help: + 'Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`.', + action: parsers.booleanParser, + default: false, + }, + convertUsernameToLowercase: { + env: 'PARSE_SERVER_CONVERT_USERNAME_TO_LOWERCASE', + help: + 'Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`.', + action: parsers.booleanParser, + default: false, + }, customPages: { env: 'PARSE_SERVER_CUSTOM_PAGES', help: 'custom pages for password validation and reset', @@ -175,12 +189,6 @@ module.exports.ParseServerOptions = { action: parsers.booleanParser, default: true, }, - disableCollation: { - env: 'PARSE_SERVER_DISABLE_COLLATION', - help: - 'Disable case insensitivity (collation) on queries and indexes, needed if you use MongoDB serverless or AWS DocumentDB.', - action: parsers.booleanParser, - }, dotNetKey: { env: 'PARSE_SERVER_DOT_NET_KEY', help: 'Key for Unity and .Net SDK', @@ -209,6 +217,13 @@ module.exports.ParseServerOptions = { action: parsers.booleanParser, default: true, }, + enableCollationCaseComparison: { + env: 'PARSE_SERVER_ENABLE_COLLATION_CASE_COMPARISON', + help: + 'Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.', + action: parsers.booleanParser, + default: false, + }, enableExpressErrorHandler: { env: 'PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER', help: 'Enables the default express error handler for all errors', @@ -539,18 +554,6 @@ module.exports.ParseServerOptions = { help: 'Starts the liveQuery server', action: parsers.booleanParser, }, - transformEmailToLowerCase: { - env: 'PARSE_SERVER_TRANSFORM_EMAIL_TO_LOWER_CASE', - help: - 'Transform Email to lowercase on create/update/login/signup. On queries client needs to ensure to send Email in lowercase format.', - action: parsers.booleanParser, - }, - transformUsernameToLowerCase: { - env: 'PARSE_SERVER_TRANSFORM_USERNAME_TO_LOWER_CASE', - help: - 'Transform Username to lowercase on create/update/login/signup. On queries client needs to ensure to send Username in lowercase format.', - action: parsers.booleanParser, - }, trustProxy: { env: 'PARSE_SERVER_TRUST_PROXY', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index 5d0e74c466b..cef358ad63b 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -28,6 +28,8 @@ * @property {String} cloud Full path to your cloud code main.js * @property {Number|Boolean} cluster Run with cluster, optionally set the number of processes default to os.cpus().length * @property {String} collectionPrefix A collection prefix for the classes + * @property {Boolean} convertEmailToLowercase Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`. + * @property {Boolean} convertUsernameToLowercase Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`. * @property {CustomPagesOptions} customPages custom pages for password validation and reset * @property {Adapter} databaseAdapter Adapter module for the database; any options that are not explicitly described here are passed directly to the database client. * @property {DatabaseOptions} databaseOptions Options to pass to the database client @@ -39,6 +41,7 @@ * @property {Boolean} emailVerifyTokenReuseIfValid Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`. * @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} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`. * @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors * @property {Boolean} 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 @@ -96,8 +99,6 @@ * @property {Number} sessionLength Session duration, in seconds, defaults to 1 year * @property {Boolean} silent Disables console output * @property {Boolean} startLiveQueryServer Starts the liveQuery server - * @property {Boolean} transformEmailToLowerCase Transform Email to lowercase on create/update/login/signup. On queries client needs to ensure to send Email in lowercase format. - * @property {Boolean} transformUsernameToLowerCase Transform Username to lowercase on create/update/login/signup. On queries client needs to ensure to send Username in lowercase format. * @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 From c8b6129bce87f4f4d42bc49d315f7394b4788c4f Mon Sep 17 00:00:00 2001 From: Mattia Faraci Date: Sun, 12 Nov 2023 01:07:44 +0100 Subject: [PATCH 5/5] Fix enableCollationCaseComparison, caseInsensitive and test --- spec/DatabaseController.spec.js | 2 +- src/Controllers/DatabaseController.js | 30 ++++++++++++++------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js index 4633f1c26f7..e1b50a5a527 100644 --- a/spec/DatabaseController.spec.js +++ b/spec/DatabaseController.spec.js @@ -515,7 +515,7 @@ describe('DatabaseController', function () { it('should not find a case insensitive user by email with convertEmailToLowercase', async () => { await reconfigureServer({ convertEmailToLowercase: true }); const user = new Parse.User(); - await user.save({ email: 'EXAMPLE@EXAMPLE.COM', password: 'password' }); + await user.save({ username: 'EXAMPLE', email: 'EXAMPLE@EXAMPLE.COM', password: 'password' }); const query = new Parse.Query(Parse.User); query.equalTo('email', 'EXAMPLE@EXAMPLE.COM'); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index c1dbfdacfe4..5975e940536 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1235,7 +1235,7 @@ class DatabaseController { keys, readPreference, hint, - caseInsensitive, + caseInsensitive: this.options.enableCollationCaseComparison ? false : caseInsensitive, explain, }; Object.keys(sort).forEach(fieldName => { @@ -1739,25 +1739,27 @@ 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; - }); + if (!this.options.enableCollationCaseComparison) { + 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, ['email'], 'case_insensitive_email', true) + .catch(error => { + logger.warn('Unable to create case insensitive email index: ', error); + throw error; + }); + } await this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']).catch(error => { logger.warn('Unable to ensure uniqueness for user email addresses: ', error); throw error; }); - await this.adapter - .ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true) - .catch(error => { - logger.warn('Unable to create case insensitive email index: ', error); - throw error; - }); - await this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']).catch(error => { logger.warn('Unable to ensure uniqueness for role name: ', error); throw error;