Skip to content

Commit

Permalink
Merge c8b6129 into 80b987d
Browse files Browse the repository at this point in the history
  • Loading branch information
mattia1208 authored Nov 12, 2023
2 parents 80b987d + c8b6129 commit 71f68b6
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 14 deletions.
254 changes: 254 additions & 0 deletions spec/DatabaseController.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Config = require('../lib/Config');
const DatabaseController = require('../lib/Controllers/DatabaseController.js');
const validateQuery = DatabaseController._validateQuery;

Expand Down Expand Up @@ -361,6 +362,259 @@ describe('DatabaseController', function () {
done();
});
});

describe('enableCollationCaseComparison', () => {
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 enableCollationCaseComparison option', async () => {
const databaseController = new DatabaseController(dummyStorageAdapter, {
enableCollationCaseComparison: 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 enableCollationCaseComparison 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 enableCollationCaseComparison',
async () => {
await reconfigureServer({
databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonFalse',
databaseAdapter: undefined,
});
const user = new Parse.User();
await user.save({
username: 'example',
password: 'password',
email: '[email protected]',
});
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: '[email protected]',
});
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('convertEmailToLowercase', () => {
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 convertEmailToLowercase option on create', async () => {
const databaseController = new DatabaseController(dummyStorageAdapter, {});
const spy = spyOn(dummyStorageAdapter, 'createObject');
spy.and.callThrough();
await databaseController.create('_User', {
email: '[email protected]',
});
expect(spy.calls.all()[0].args[2]).toEqual({
email: '[email protected]',
...dates,
});
});

it('should transform email to lower case with convertEmailToLowercase option on create', async () => {
const databaseController = new DatabaseController(dummyStorageAdapter, {
convertEmailToLowercase: true,
});
const spy = spyOn(dummyStorageAdapter, 'createObject');
spy.and.callThrough();
await databaseController.create('_User', {
email: '[email protected]',
});
expect(spy.calls.all()[0].args[2]).toEqual({
email: '[email protected]',
...dates,
});
});

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();
await databaseController.update('_User', { id: 'example' }, { email: '[email protected]' });
expect(spy.calls.all()[0].args[3]).toEqual({
email: '[email protected]',
});
});

it('should transform email to lower case with convertEmailToLowercase option on update', async () => {
const databaseController = new DatabaseController(dummyStorageAdapter, {
convertEmailToLowercase: true,
});
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
spy.and.callThrough();
await databaseController.update('_User', { id: 'example' }, { email: '[email protected]' });
expect(spy.calls.all()[0].args[3]).toEqual({
email: '[email protected]',
});
});

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({ username: 'EXAMPLE', email: '[email protected]', password: 'password' });

const query = new Parse.Query(Parse.User);
query.equalTo('email', '[email protected]');
const result = await query.find({ useMasterKey: true });
expect(result.length).toEqual(0);

const query2 = new Parse.Query(Parse.User);
query2.equalTo('email', '[email protected]');
const result2 = await query2.find({ useMasterKey: true });
expect(result2.length).toEqual(1);
});
});

describe('convertUsernameToLowercase', () => {
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 convertUsernameToLowercase 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 convertUsernameToLowercase option on create', async () => {
const databaseController = new DatabaseController(dummyStorageAdapter, {
convertUsernameToLowercase: 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 convertUsernameToLowercase 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 convertUsernameToLowercase option on update', async () => {
const databaseController = new DatabaseController(dummyStorageAdapter, {
convertUsernameToLowercase: 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 convertUsernameToLowercase', async () => {
await reconfigureServer({ convertUsernameToLowercase: 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) {
Expand Down
50 changes: 36 additions & 14 deletions src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,22 @@ const relationSchema = {
fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } },
};

const convertEmailToLowercase = (object, className, options) => {
if (className === '_User' && options.convertEmailToLowercase) {
if (typeof object['email'] === 'string') {
object['email'] = object['email'].toLowerCase();
}
}
};

const convertUsernameToLowercase = (object, className, options) => {
if (className === '_User' && options.convertUsernameToLowercase) {
if (typeof object['username'] === 'string') {
object['username'] = object['username'].toLowerCase();
}
}
};

class DatabaseController {
adapter: StorageAdapter;
schemaCache: any;
Expand Down Expand Up @@ -573,6 +589,8 @@ class DatabaseController {
}
}
update = transformObjectACL(update);
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 => {
Expand Down Expand Up @@ -822,6 +840,8 @@ class DatabaseController {
const originalObject = object;
object = transformObjectACL(object);

convertEmailToLowercase(object, className, this.options);
convertUsernameToLowercase(object, className, this.options);
object.createdAt = { iso: object.createdAt, __type: 'Date' };
object.updatedAt = { iso: object.updatedAt, __type: 'Date' };

Expand Down Expand Up @@ -1215,7 +1235,7 @@ class DatabaseController {
keys,
readPreference,
hint,
caseInsensitive,
caseInsensitive: this.options.enableCollationCaseComparison ? false : caseInsensitive,
explain,
};
Object.keys(sort).forEach(fieldName => {
Expand Down Expand Up @@ -1719,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;

Check warning on line 1747 in src/Controllers/DatabaseController.js

View check run for this annotation

Codecov / codecov/patch

src/Controllers/DatabaseController.js#L1746-L1747

Added lines #L1746 - L1747 were not covered by tests
});

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;

Check warning on line 1754 in src/Controllers/DatabaseController.js

View check run for this annotation

Codecov / codecov/patch

src/Controllers/DatabaseController.js#L1753-L1754

Added lines #L1753 - L1754 were not covered by tests
});
}

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;
Expand Down
21 changes: 21 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -203,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',
Expand Down
Loading

0 comments on commit 71f68b6

Please sign in to comment.