Skip to content

Commit 71f68b6

Browse files
authored
Merge c8b6129 into 80b987d
2 parents 80b987d + c8b6129 commit 71f68b6

File tree

5 files changed

+323
-14
lines changed

5 files changed

+323
-14
lines changed

spec/DatabaseController.spec.js

+254
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const Config = require('../lib/Config');
12
const DatabaseController = require('../lib/Controllers/DatabaseController.js');
23
const validateQuery = DatabaseController._validateQuery;
34

@@ -361,6 +362,259 @@ describe('DatabaseController', function () {
361362
done();
362363
});
363364
});
365+
366+
describe('enableCollationCaseComparison', () => {
367+
const dummyStorageAdapter = {
368+
find: () => Promise.resolve([]),
369+
watch: () => Promise.resolve(),
370+
getAllClasses: () => Promise.resolve([]),
371+
};
372+
373+
beforeEach(() => {
374+
Config.get(Parse.applicationId).schemaCache.clear();
375+
});
376+
377+
it('should force caseInsensitive to false with enableCollationCaseComparison option', async () => {
378+
const databaseController = new DatabaseController(dummyStorageAdapter, {
379+
enableCollationCaseComparison: true,
380+
});
381+
const spy = spyOn(dummyStorageAdapter, 'find');
382+
spy.and.callThrough();
383+
await databaseController.find('SomeClass', {}, { caseInsensitive: true });
384+
expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(false);
385+
});
386+
387+
it('should support caseInsensitive without enableCollationCaseComparison option', async () => {
388+
const databaseController = new DatabaseController(dummyStorageAdapter, {});
389+
const spy = spyOn(dummyStorageAdapter, 'find');
390+
spy.and.callThrough();
391+
await databaseController.find('_User', {}, { caseInsensitive: true });
392+
expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(true);
393+
});
394+
395+
it_only_db('mongo')(
396+
'should create insensitive indexes without enableCollationCaseComparison',
397+
async () => {
398+
await reconfigureServer({
399+
databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonFalse',
400+
databaseAdapter: undefined,
401+
});
402+
const user = new Parse.User();
403+
await user.save({
404+
username: 'example',
405+
password: 'password',
406+
407+
});
408+
const schemas = await Parse.Schema.all();
409+
const UserSchema = schemas.find(({ className }) => className === '_User');
410+
expect(UserSchema.indexes).toEqual({
411+
_id_: { _id: 1 },
412+
username_1: { username: 1 },
413+
case_insensitive_username: { username: 1 },
414+
case_insensitive_email: { email: 1 },
415+
email_1: { email: 1 },
416+
});
417+
}
418+
);
419+
420+
it_only_db('mongo')(
421+
'should not create insensitive indexes with enableCollationCaseComparison',
422+
async () => {
423+
await reconfigureServer({
424+
enableCollationCaseComparison: true,
425+
databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonTrue',
426+
databaseAdapter: undefined,
427+
});
428+
const user = new Parse.User();
429+
await user.save({
430+
username: 'example',
431+
password: 'password',
432+
433+
});
434+
const schemas = await Parse.Schema.all();
435+
const UserSchema = schemas.find(({ className }) => className === '_User');
436+
expect(UserSchema.indexes).toEqual({
437+
_id_: { _id: 1 },
438+
username_1: { username: 1 },
439+
email_1: { email: 1 },
440+
});
441+
}
442+
);
443+
});
444+
445+
describe('convertEmailToLowercase', () => {
446+
const dummyStorageAdapter = {
447+
createObject: () => Promise.resolve({ ops: [{}] }),
448+
findOneAndUpdate: () => Promise.resolve({}),
449+
watch: () => Promise.resolve(),
450+
getAllClasses: () =>
451+
Promise.resolve([
452+
{
453+
className: '_User',
454+
fields: { email: 'String' },
455+
indexes: {},
456+
classLevelPermissions: { protectedFields: {} },
457+
},
458+
]),
459+
};
460+
const dates = {
461+
createdAt: { iso: undefined, __type: 'Date' },
462+
updatedAt: { iso: undefined, __type: 'Date' },
463+
};
464+
465+
it('should not transform email to lower case without convertEmailToLowercase option on create', async () => {
466+
const databaseController = new DatabaseController(dummyStorageAdapter, {});
467+
const spy = spyOn(dummyStorageAdapter, 'createObject');
468+
spy.and.callThrough();
469+
await databaseController.create('_User', {
470+
471+
});
472+
expect(spy.calls.all()[0].args[2]).toEqual({
473+
474+
...dates,
475+
});
476+
});
477+
478+
it('should transform email to lower case with convertEmailToLowercase option on create', async () => {
479+
const databaseController = new DatabaseController(dummyStorageAdapter, {
480+
convertEmailToLowercase: true,
481+
});
482+
const spy = spyOn(dummyStorageAdapter, 'createObject');
483+
spy.and.callThrough();
484+
await databaseController.create('_User', {
485+
486+
});
487+
expect(spy.calls.all()[0].args[2]).toEqual({
488+
489+
...dates,
490+
});
491+
});
492+
493+
it('should not transform email to lower case without convertEmailToLowercase option on update', async () => {
494+
const databaseController = new DatabaseController(dummyStorageAdapter, {});
495+
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
496+
spy.and.callThrough();
497+
await databaseController.update('_User', { id: 'example' }, { email: '[email protected]' });
498+
expect(spy.calls.all()[0].args[3]).toEqual({
499+
500+
});
501+
});
502+
503+
it('should transform email to lower case with convertEmailToLowercase option on update', async () => {
504+
const databaseController = new DatabaseController(dummyStorageAdapter, {
505+
convertEmailToLowercase: true,
506+
});
507+
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
508+
spy.and.callThrough();
509+
await databaseController.update('_User', { id: 'example' }, { email: '[email protected]' });
510+
expect(spy.calls.all()[0].args[3]).toEqual({
511+
512+
});
513+
});
514+
515+
it('should not find a case insensitive user by email with convertEmailToLowercase', async () => {
516+
await reconfigureServer({ convertEmailToLowercase: true });
517+
const user = new Parse.User();
518+
await user.save({ username: 'EXAMPLE', email: '[email protected]', password: 'password' });
519+
520+
const query = new Parse.Query(Parse.User);
521+
query.equalTo('email', '[email protected]');
522+
const result = await query.find({ useMasterKey: true });
523+
expect(result.length).toEqual(0);
524+
525+
const query2 = new Parse.Query(Parse.User);
526+
query2.equalTo('email', '[email protected]');
527+
const result2 = await query2.find({ useMasterKey: true });
528+
expect(result2.length).toEqual(1);
529+
});
530+
});
531+
532+
describe('convertUsernameToLowercase', () => {
533+
const dummyStorageAdapter = {
534+
createObject: () => Promise.resolve({ ops: [{}] }),
535+
findOneAndUpdate: () => Promise.resolve({}),
536+
watch: () => Promise.resolve(),
537+
getAllClasses: () =>
538+
Promise.resolve([
539+
{
540+
className: '_User',
541+
fields: { username: 'String' },
542+
indexes: {},
543+
classLevelPermissions: { protectedFields: {} },
544+
},
545+
]),
546+
};
547+
const dates = {
548+
createdAt: { iso: undefined, __type: 'Date' },
549+
updatedAt: { iso: undefined, __type: 'Date' },
550+
};
551+
552+
it('should not transform username to lower case without convertUsernameToLowercase option on create', async () => {
553+
const databaseController = new DatabaseController(dummyStorageAdapter, {});
554+
const spy = spyOn(dummyStorageAdapter, 'createObject');
555+
spy.and.callThrough();
556+
await databaseController.create('_User', {
557+
username: 'EXAMPLE',
558+
});
559+
expect(spy.calls.all()[0].args[2]).toEqual({
560+
username: 'EXAMPLE',
561+
...dates,
562+
});
563+
});
564+
565+
it('should transform username to lower case with convertUsernameToLowercase option on create', async () => {
566+
const databaseController = new DatabaseController(dummyStorageAdapter, {
567+
convertUsernameToLowercase: true,
568+
});
569+
const spy = spyOn(dummyStorageAdapter, 'createObject');
570+
spy.and.callThrough();
571+
await databaseController.create('_User', {
572+
username: 'EXAMPLE',
573+
});
574+
expect(spy.calls.all()[0].args[2]).toEqual({
575+
username: 'example',
576+
...dates,
577+
});
578+
});
579+
580+
it('should not transform username to lower case without convertUsernameToLowercase option on update', async () => {
581+
const databaseController = new DatabaseController(dummyStorageAdapter, {});
582+
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
583+
spy.and.callThrough();
584+
await databaseController.update('_User', { id: 'example' }, { username: 'EXAMPLE' });
585+
expect(spy.calls.all()[0].args[3]).toEqual({
586+
username: 'EXAMPLE',
587+
});
588+
});
589+
590+
it('should transform username to lower case with convertUsernameToLowercase option on update', async () => {
591+
const databaseController = new DatabaseController(dummyStorageAdapter, {
592+
convertUsernameToLowercase: true,
593+
});
594+
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
595+
spy.and.callThrough();
596+
await databaseController.update('_User', { id: 'example' }, { username: 'EXAMPLE' });
597+
expect(spy.calls.all()[0].args[3]).toEqual({
598+
username: 'example',
599+
});
600+
});
601+
602+
it('should not find a case insensitive user by username with convertUsernameToLowercase', async () => {
603+
await reconfigureServer({ convertUsernameToLowercase: true });
604+
const user = new Parse.User();
605+
await user.save({ username: 'EXAMPLE', password: 'password' });
606+
607+
const query = new Parse.Query(Parse.User);
608+
query.equalTo('username', 'EXAMPLE');
609+
const result = await query.find({ useMasterKey: true });
610+
expect(result.length).toEqual(0);
611+
612+
const query2 = new Parse.Query(Parse.User);
613+
query2.equalTo('username', 'example');
614+
const result2 = await query2.find({ useMasterKey: true });
615+
expect(result2.length).toEqual(1);
616+
});
617+
});
364618
});
365619

366620
function buildCLP(pointerNames) {

src/Controllers/DatabaseController.js

+36-14
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,22 @@ const relationSchema = {
368368
fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } },
369369
};
370370

371+
const convertEmailToLowercase = (object, className, options) => {
372+
if (className === '_User' && options.convertEmailToLowercase) {
373+
if (typeof object['email'] === 'string') {
374+
object['email'] = object['email'].toLowerCase();
375+
}
376+
}
377+
};
378+
379+
const convertUsernameToLowercase = (object, className, options) => {
380+
if (className === '_User' && options.convertUsernameToLowercase) {
381+
if (typeof object['username'] === 'string') {
382+
object['username'] = object['username'].toLowerCase();
383+
}
384+
}
385+
};
386+
371387
class DatabaseController {
372388
adapter: StorageAdapter;
373389
schemaCache: any;
@@ -573,6 +589,8 @@ class DatabaseController {
573589
}
574590
}
575591
update = transformObjectACL(update);
592+
convertEmailToLowercase(update, className, this.options);
593+
convertUsernameToLowercase(update, className, this.options);
576594
transformAuthData(className, update, schema);
577595
if (validateOnly) {
578596
return this.adapter.find(className, schema, query, {}).then(result => {
@@ -822,6 +840,8 @@ class DatabaseController {
822840
const originalObject = object;
823841
object = transformObjectACL(object);
824842

843+
convertEmailToLowercase(object, className, this.options);
844+
convertUsernameToLowercase(object, className, this.options);
825845
object.createdAt = { iso: object.createdAt, __type: 'Date' };
826846
object.updatedAt = { iso: object.updatedAt, __type: 'Date' };
827847

@@ -1215,7 +1235,7 @@ class DatabaseController {
12151235
keys,
12161236
readPreference,
12171237
hint,
1218-
caseInsensitive,
1238+
caseInsensitive: this.options.enableCollationCaseComparison ? false : caseInsensitive,
12191239
explain,
12201240
};
12211241
Object.keys(sort).forEach(fieldName => {
@@ -1719,25 +1739,27 @@ class DatabaseController {
17191739
throw error;
17201740
});
17211741

1722-
await this.adapter
1723-
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
1724-
.catch(error => {
1725-
logger.warn('Unable to create case insensitive username index: ', error);
1726-
throw error;
1727-
});
1742+
if (!this.options.enableCollationCaseComparison) {
1743+
await this.adapter
1744+
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
1745+
.catch(error => {
1746+
logger.warn('Unable to create case insensitive username index: ', error);
1747+
throw error;
1748+
});
1749+
1750+
await this.adapter
1751+
.ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true)
1752+
.catch(error => {
1753+
logger.warn('Unable to create case insensitive email index: ', error);
1754+
throw error;
1755+
});
1756+
}
17281757

17291758
await this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']).catch(error => {
17301759
logger.warn('Unable to ensure uniqueness for user email addresses: ', error);
17311760
throw error;
17321761
});
17331762

1734-
await this.adapter
1735-
.ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true)
1736-
.catch(error => {
1737-
logger.warn('Unable to create case insensitive email index: ', error);
1738-
throw error;
1739-
});
1740-
17411763
await this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']).catch(error => {
17421764
logger.warn('Unable to ensure uniqueness for role name: ', error);
17431765
throw error;

src/Options/Definitions.js

+21
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,20 @@ module.exports.ParseServerOptions = {
139139
help: 'A collection prefix for the classes',
140140
default: '',
141141
},
142+
convertEmailToLowercase: {
143+
env: 'PARSE_SERVER_CONVERT_EMAIL_TO_LOWERCASE',
144+
help:
145+
'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`.',
146+
action: parsers.booleanParser,
147+
default: false,
148+
},
149+
convertUsernameToLowercase: {
150+
env: 'PARSE_SERVER_CONVERT_USERNAME_TO_LOWERCASE',
151+
help:
152+
'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`.',
153+
action: parsers.booleanParser,
154+
default: false,
155+
},
142156
customPages: {
143157
env: 'PARSE_SERVER_CUSTOM_PAGES',
144158
help: 'custom pages for password validation and reset',
@@ -203,6 +217,13 @@ module.exports.ParseServerOptions = {
203217
action: parsers.booleanParser,
204218
default: true,
205219
},
220+
enableCollationCaseComparison: {
221+
env: 'PARSE_SERVER_ENABLE_COLLATION_CASE_COMPARISON',
222+
help:
223+
'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`.',
224+
action: parsers.booleanParser,
225+
default: false,
226+
},
206227
enableExpressErrorHandler: {
207228
env: 'PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER',
208229
help: 'Enables the default express error handler for all errors',

0 commit comments

Comments
 (0)