From 866b08b52cb9e422c226b365ae9c6af749b0a060 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Tue, 4 Jun 2019 14:27:28 -0700 Subject: [PATCH 01/33] Always delete data after each, even for mongo. --- spec/helper.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index 844ca6777a..7a4074ffd4 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -228,13 +228,7 @@ afterEach(function(done) { 'There were open connections to the server left after the test finished' ); } - on_db( - 'postgres', - () => { - TestUtils.destroyAllDataPermanently(true).then(done, done); - }, - done - ); + TestUtils.destroyAllDataPermanently(true).then(done, done); }; Parse.Cloud._removeAllHooks(); databaseAdapter From 70e3210df82f1020f38e455a69286a7caebb500b Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Tue, 4 Jun 2019 14:49:58 -0700 Subject: [PATCH 02/33] Add failing simple case test --- spec/ParseUser.spec.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index f21403e89a..c81c80c41e 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -2165,6 +2165,25 @@ describe('Parse.User testing', () => { ); }); + describe('case insensitive signup not allowed', () => { + fit('user should fail with duplicate insensitive username', async () => { + const user = new Parse.User(); + user.set('username', 'test1'); + user.set('password', 'test'); + await user.signUp(); + + const user2 = new Parse.User(); + user2.set('username', 'Test1'); + user2.set('password', 'test'); + await expectAsync(user2.signUp()).toBeRejectedWith( + new Parse.Error( + Parse.Error.USERNAME_TAKEN, + 'Account already exists for this username.' + ) + ); + }); + }); + it('user cannot update email to existing user', done => { const user = new Parse.User(); user.set('username', 'test1'); From 8d0210bae348ac399b933d9e414ac3025edea02b Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Tue, 4 Jun 2019 17:15:37 -0700 Subject: [PATCH 03/33] run all tests --- 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 c81c80c41e..5ddd9cd1e3 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -2166,7 +2166,7 @@ describe('Parse.User testing', () => { }); describe('case insensitive signup not allowed', () => { - fit('user should fail with duplicate insensitive username', async () => { + it('user should fail with duplicate insensitive username', async () => { const user = new Parse.User(); user.set('username', 'test1'); user.set('password', 'test'); From e9f700156f79d77e306433f5c097791f5896db5c Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Tue, 4 Jun 2019 17:20:25 -0700 Subject: [PATCH 04/33] 1. when validating username be case insensitive 2. add _auth_data_anonymous to specialQueryKeys...whatever that is! --- src/Controllers/DatabaseController.js | 1 + src/RestWrite.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 473fcf0266..b912ef2418 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -63,6 +63,7 @@ const specialQuerykeys = [ '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count', + '_auth_data_anonymous', ]; const isSpecialQueryKey = key => { diff --git a/src/RestWrite.js b/src/RestWrite.js index 998d357617..88ebe517ec 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -618,7 +618,11 @@ RestWrite.prototype._validateUserName = function() { return this.config.database .find( this.className, - { username: this.data.username, objectId: { $ne: this.objectId() } }, + { + username: { $regex: `^${this.data.username}$`, $options: 'i' }, + objectId: { $ne: this.objectId() }, + _auth_data_anonymous: null, + }, { limit: 1 }, {}, this.validSchemaController From 6149c86fa2d50f9428c20c7f8b0bab40db32a483 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Fri, 21 Jun 2019 09:23:44 -0700 Subject: [PATCH 05/33] More case sensitivity 1. also make email validation case insensitive 2. update comments to reflect what this change does --- spec/ParseUser.spec.js | 60 +++++++++++++++++++++++++++++++++++++++++- src/RestWrite.js | 32 +++++++++++++++++++--- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 5ddd9cd1e3..2e1ec07999 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -2166,7 +2166,7 @@ describe('Parse.User testing', () => { }); describe('case insensitive signup not allowed', () => { - it('user should fail with duplicate insensitive username', async () => { + it('signup should fail with duplicate case insensitive username with basic setter', async () => { const user = new Parse.User(); user.set('username', 'test1'); user.set('password', 'test'); @@ -2182,6 +2182,64 @@ describe('Parse.User testing', () => { ) ); }); + + it('signup should fail with duplicate case insensitive username with field specific setter', async () => { + const user = new Parse.User(); + user.setUsername('test1'); + user.setPassword('test'); + await user.signUp(); + + const user2 = new Parse.User(); + user2.setUsername('Test1'); + user2.setPassword('test'); + await expectAsync(user2.signUp()).toBeRejectedWith( + new Parse.Error( + Parse.Error.USERNAME_TAKEN, + 'Account already exists for this username.' + ) + ); + }); + + it('signup should fail with duplicate case insensitive email', async() => { + const user = new Parse.User(); + user.setUsername('test1'); + user.setPassword('test'); + user.setEmail('test@example.com'); + await user.signUp(); + + const user2 = new Parse.User(); + user2.setUsername('test2'); + user2.setPassword('test'); + user2.setEmail('Test@Example.Com'); + await expectAsync(user2.signUp()).toBeRejectedWith( + new Parse.Error( + Parse.Error.EMAIL_TAKEN, + 'Account already exists for this email address.' + ) + ); + }); + + it('edit should fail with duplicate case insensitive email', async() => { + const user = new Parse.User(); + user.setUsername('test1'); + user.setPassword('test'); + user.setEmail('test@example.com'); + await user.signUp(); + + const user2 = new Parse.User(); + user2.setUsername('test2'); + user2.setPassword('test'); + user2.setEmail('Foo@Example.Com'); + await user2.signUp(); + + user2.setEmail('Test@Example.Com') + await expectAsync(user2.save()).toBeRejectedWith( + new Parse.Error( + Parse.Error.EMAIL_TAKEN, + 'Account already exists for this email address.' + ) + ); + }); }); it('user cannot update email to existing user', done => { diff --git a/src/RestWrite.js b/src/RestWrite.js index ad81648e70..b7b3532fa4 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -619,8 +619,18 @@ RestWrite.prototype._validateUserName = function() { } return Promise.resolve(); } - // We need to a find to check for duplicate username in case they are missing the unique index on usernames - // TODO: Check if there is a unique index, and if so, skip this query. + /* + Username's should be unique when compared case insensitively + + User's should be able to make case sensitive usernames and + login using the case they entered. I.e. 'Snoopy' should preclude + 'snoopy' as a valid username. + + Users that use authentication adapters should enforce unique ids + through a unique index on username. Failure to enforce through an index + allows for a potential collision for adapter users (a low probability outcome) + but more importantly will have poor performance on this validation. + */ return this.config.database .find( this.className, @@ -644,6 +654,18 @@ RestWrite.prototype._validateUserName = function() { }); }; +/* + As with username's, parse should not allow case insensitive collisions of email + unlike with usernames (which can have case insensitive collisions) emails should + never have a case insensitive collision. + + This behavior can be enforced through a properly configured index see: + https://docs.mongodb.com/manual/core/index-case-insensitive/#create-a-case-insensitive-index + which could be implemented instead of this code based validation. + + Given that this lookup should be a relatively low use case and that the case sensitive + unique index will be used by the db for the query, this is an adequate solution. +*/ RestWrite.prototype._validateEmail = function() { if (!this.data.email || this.data.email.__op === 'Delete') { return Promise.resolve(); @@ -657,11 +679,13 @@ RestWrite.prototype._validateEmail = function() { ) ); } - // Same problem for email as above for username + // Case insensitive match, see note above function. return this.config.database .find( this.className, - { email: this.data.email, objectId: { $ne: this.objectId() } }, + { + email: { $regex: `^${this.data.email}$`, $options: 'i' }, + objectId: { $ne: this.objectId() } }, { limit: 1 }, {}, this.validSchemaController From 7d0d14c363b637a6c2355f59ef207ae9f91a6d63 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Fri, 21 Jun 2019 09:31:32 -0700 Subject: [PATCH 06/33] wordsmithery and grammar --- src/RestWrite.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index b7b3532fa4..fc64d45433 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -620,16 +620,23 @@ RestWrite.prototype._validateUserName = function() { return Promise.resolve(); } /* - Username's should be unique when compared case insensitively + Usernames should be unique when compared case insensitively - User's should be able to make case sensitive usernames and + Users should be able to make case sensitive usernames and login using the case they entered. I.e. 'Snoopy' should preclude 'snoopy' as a valid username. - Users that use authentication adapters should enforce unique ids - through a unique index on username. Failure to enforce through an index - allows for a potential collision for adapter users (a low probability outcome) - but more importantly will have poor performance on this validation. + However, authentication adapters require a looser check that takes + case into consideration when determining uniqueness. + + The username field should have a unique index on the database as + Failure to enforce through an index allows for a potential collision + for adapter users (a low probability outcome) but more importantly + will have poor performance on this validation. + + The check below has the potential to not allow a valid + username for an adapter other than anonymous, this should + be fixed. */ return this.config.database .find( @@ -655,9 +662,9 @@ RestWrite.prototype._validateUserName = function() { }; /* - As with username's, parse should not allow case insensitive collisions of email - unlike with usernames (which can have case insensitive collisions) emails should - never have a case insensitive collision. + As with usernames, Parse should not allow case insensitive collisions of email. + unlike with usernames (which can have case insensitive collisions in the case of + auth adapters), emails should never have a case insensitive collision. This behavior can be enforced through a properly configured index see: https://docs.mongodb.com/manual/core/index-case-insensitive/#create-a-case-insensitive-index @@ -685,7 +692,8 @@ RestWrite.prototype._validateEmail = function() { this.className, { email: { $regex: `^${this.data.email}$`, $options: 'i' }, - objectId: { $ne: this.objectId() } }, + objectId: { $ne: this.objectId() }, + }, { limit: 1 }, {}, this.validSchemaController From bbc1d73fe769aaf1659b718c16bf708bdc768b09 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Sun, 23 Jun 2019 10:51:08 -0400 Subject: [PATCH 07/33] first pass at a preformant case insensitive query. mongo only so far. --- src/Adapters/Storage/Mongo/MongoCollection.js | 16 ++++++++++++++-- .../Storage/Mongo/MongoStorageAdapter.js | 3 ++- src/Controllers/DatabaseController.js | 11 ++++++++++- src/RestWrite.js | 8 ++++---- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index 50e7a41123..8c1c7256c1 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -13,7 +13,10 @@ export default class MongoCollection { // none, then build the geoindex. // This could be improved a lot but it's not clear if that's a good // idea. Or even if this behavior is a good idea. - find(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) { + find( + query, + { skip, limit, sort, keys, maxTimeMS, readPreference, insensitive } = {} + ) { // Support for Full Text Search - $text if (keys && keys.$score) { delete keys.$score; @@ -26,6 +29,7 @@ export default class MongoCollection { keys, maxTimeMS, readPreference, + insensitive, }).catch(error => { // Check for "no geoindex" error if ( @@ -54,13 +58,17 @@ export default class MongoCollection { keys, maxTimeMS, readPreference, + insensitive, }) ) ); }); } - _rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) { + _rawFind( + query, + { skip, limit, sort, keys, maxTimeMS, readPreference, insensitive } = {} + ) { let findOperation = this._mongoCollection.find(query, { skip, limit, @@ -72,6 +80,10 @@ export default class MongoCollection { findOperation = findOperation.project(keys); } + if (insensitive) { + findOperation = findOperation.collation({ locale: 'en_US', strength: 2 }); + } + if (maxTimeMS) { findOperation = findOperation.maxTimeMS(maxTimeMS); } diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 33282ce2b9..f5e9265685 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -592,7 +592,7 @@ export class MongoStorageAdapter implements StorageAdapter { className: string, schema: SchemaType, query: QueryType, - { skip, limit, sort, keys, readPreference }: QueryOptions + { skip, limit, sort, keys, readPreference, insensitive }: QueryOptions ): Promise { schema = convertParseSchemaToMongoSchema(schema); const mongoWhere = transformWhere(className, query, schema); @@ -624,6 +624,7 @@ export class MongoStorageAdapter implements StorageAdapter { keys: mongoKeys, maxTimeMS: this._maxTimeMS, readPreference, + insensitive, }) ) .then(objects => diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 3ab503e027..420e7227c7 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1210,6 +1210,7 @@ class DatabaseController { // acl restrict this operation with an ACL for the provided array // of user objectIds and roles. acl: null means no user. // when this field is not present, don't do anything regarding ACLs. + // insensitive make string comparisons case insensitive // TODO: make userIds not needed here. The db adapter shouldn't know // anything about users, ideally. Then, improve the format of the ACL // arg to work like the others. @@ -1227,6 +1228,7 @@ class DatabaseController { distinct, pipeline, readPreference, + insensitive = false, }: any = {}, auth: any = {}, validSchemaController: SchemaController.SchemaController @@ -1271,7 +1273,14 @@ class DatabaseController { sort.updatedAt = sort._updated_at; delete sort._updated_at; } - const queryOptions = { skip, limit, sort, keys, readPreference }; + const queryOptions = { + skip, + limit, + sort, + keys, + readPreference, + insensitive, + }; Object.keys(sort).forEach(fieldName => { if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { throw new Parse.Error( diff --git a/src/RestWrite.js b/src/RestWrite.js index fc64d45433..9128b4c1a1 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -642,11 +642,11 @@ RestWrite.prototype._validateUserName = function() { .find( this.className, { - username: { $regex: `^${this.data.username}$`, $options: 'i' }, + username: this.data.username, objectId: { $ne: this.objectId() }, _auth_data_anonymous: null, }, - { limit: 1 }, + { limit: 1, insensitive: true }, {}, this.validSchemaController ) @@ -691,10 +691,10 @@ RestWrite.prototype._validateEmail = function() { .find( this.className, { - email: { $regex: `^${this.data.email}$`, $options: 'i' }, + email: this.data.email, objectId: { $ne: this.objectId() }, }, - { limit: 1 }, + { limit: 1, insensitive: true }, {}, this.validSchemaController ) From 0052c6bb8aa2b8bb5d90d1374361dd349dabcdde Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Sun, 23 Jun 2019 12:19:34 -0400 Subject: [PATCH 08/33] change name of parameter from insensitive to caseInsensitive --- src/Adapters/Storage/Mongo/MongoCollection.js | 10 +++++----- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 4 ++-- src/Controllers/DatabaseController.js | 8 ++++---- src/RestWrite.js | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index 8c1c7256c1..48700515ac 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -15,7 +15,7 @@ export default class MongoCollection { // idea. Or even if this behavior is a good idea. find( query, - { skip, limit, sort, keys, maxTimeMS, readPreference, insensitive } = {} + { skip, limit, sort, keys, maxTimeMS, readPreference, caseInsensitive } = {} ) { // Support for Full Text Search - $text if (keys && keys.$score) { @@ -29,7 +29,7 @@ export default class MongoCollection { keys, maxTimeMS, readPreference, - insensitive, + caseInsensitive, }).catch(error => { // Check for "no geoindex" error if ( @@ -58,7 +58,7 @@ export default class MongoCollection { keys, maxTimeMS, readPreference, - insensitive, + caseInsensitive, }) ) ); @@ -67,7 +67,7 @@ export default class MongoCollection { _rawFind( query, - { skip, limit, sort, keys, maxTimeMS, readPreference, insensitive } = {} + { skip, limit, sort, keys, maxTimeMS, readPreference, caseInsensitive } = {} ) { let findOperation = this._mongoCollection.find(query, { skip, @@ -80,7 +80,7 @@ export default class MongoCollection { findOperation = findOperation.project(keys); } - if (insensitive) { + if (caseInsensitive) { findOperation = findOperation.collation({ locale: 'en_US', strength: 2 }); } diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index f5e9265685..5bc92fbf33 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -592,7 +592,7 @@ export class MongoStorageAdapter implements StorageAdapter { className: string, schema: SchemaType, query: QueryType, - { skip, limit, sort, keys, readPreference, insensitive }: QueryOptions + { skip, limit, sort, keys, readPreference, caseInsensitive }: QueryOptions ): Promise { schema = convertParseSchemaToMongoSchema(schema); const mongoWhere = transformWhere(className, query, schema); @@ -624,7 +624,7 @@ export class MongoStorageAdapter implements StorageAdapter { keys: mongoKeys, maxTimeMS: this._maxTimeMS, readPreference, - insensitive, + caseInsensitive, }) ) .then(objects => diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 420e7227c7..967f2d03fb 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1,4 +1,4 @@ -// @flow +// @flow // A database adapter that works with data exported from the hosted // Parse database. @@ -1210,7 +1210,7 @@ class DatabaseController { // acl restrict this operation with an ACL for the provided array // of user objectIds and roles. acl: null means no user. // when this field is not present, don't do anything regarding ACLs. - // insensitive make string comparisons case insensitive + // caseInsensitive make string comparisons case insensitive // TODO: make userIds not needed here. The db adapter shouldn't know // anything about users, ideally. Then, improve the format of the ACL // arg to work like the others. @@ -1228,7 +1228,7 @@ class DatabaseController { distinct, pipeline, readPreference, - insensitive = false, + caseInsensitive = false, }: any = {}, auth: any = {}, validSchemaController: SchemaController.SchemaController @@ -1279,7 +1279,7 @@ class DatabaseController { sort, keys, readPreference, - insensitive, + caseInsensitive, }; Object.keys(sort).forEach(fieldName => { if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { diff --git a/src/RestWrite.js b/src/RestWrite.js index 9128b4c1a1..63bf5bce9c 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -646,7 +646,7 @@ RestWrite.prototype._validateUserName = function() { objectId: { $ne: this.objectId() }, _auth_data_anonymous: null, }, - { limit: 1, insensitive: true }, + { limit: 1, caseInsensitive: true }, {}, this.validSchemaController ) @@ -694,7 +694,7 @@ RestWrite.prototype._validateEmail = function() { email: this.data.email, objectId: { $ne: this.objectId() }, }, - { limit: 1, insensitive: true }, + { limit: 1, caseInsensitive: true }, {}, this.validSchemaController ) From 972cc3763caf669d9324501134e426e47950773d Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Mon, 24 Jun 2019 17:21:44 -0500 Subject: [PATCH 09/33] Postgres support --- .../Postgres/PostgresStorageAdapter.js | 61 ++++++++++++++++--- src/Adapters/Storage/StorageAdapter.js | 1 + 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index dda05ae659..d84dbecd06 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -252,7 +252,12 @@ interface WhereClause { sorts: Array; } -const buildWhereClause = ({ schema, query, index }): WhereClause => { +const buildWhereClause = ({ + schema, + query, + index, + insensitive, +}): WhereClause => { const patterns = []; let values = []; const sorts = []; @@ -274,7 +279,18 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { } } - if (fieldName.indexOf('.') >= 0) { + const authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/); + if (authDataMatch) { + // TODO: Handle querying by _auth_data_provider, authData is stored in authData field + continue; + } else if ( + insensitive && + (fieldName === 'username' || fieldName === 'email') + ) { + patterns.push(`LOWER($${index}:name) = LOWER($${index + 1})`); + values.push(fieldName, fieldValue); + index += 2; + } else if (fieldName.indexOf('.') >= 0) { let name = transformDotField(fieldName); if (fieldValue === null) { patterns.push(`${name} IS NULL`); @@ -333,7 +349,12 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { const clauses = []; const clauseValues = []; fieldValue.forEach(subQuery => { - const clause = buildWhereClause({ schema, query: subQuery, index }); + const clause = buildWhereClause({ + schema, + query: subQuery, + index, + insensitive, + }); if (clause.pattern.length > 0) { clauses.push(clause.pattern); clauseValues.push(...clause.values); @@ -1391,7 +1412,12 @@ export class PostgresStorageAdapter implements StorageAdapter { debug('deleteObjectsByQuery', className, query); const values = [className]; const index = 2; - const where = buildWhereClause({ schema, index, query }); + const where = buildWhereClause({ + schema, + index, + query, + insensitive: false, + }); values.push(...where.values); if (Object.keys(query).length === 0) { where.pattern = 'TRUE'; @@ -1685,7 +1711,12 @@ export class PostgresStorageAdapter implements StorageAdapter { } } - const where = buildWhereClause({ schema, index, query }); + const where = buildWhereClause({ + schema, + index, + query, + insensitive: false, + }); values.push(...where.values); const whereClause = @@ -1717,13 +1748,13 @@ export class PostgresStorageAdapter implements StorageAdapter { className: string, schema: SchemaType, query: QueryType, - { skip, limit, sort, keys }: QueryOptions + { skip, limit, sort, keys, insensitive }: QueryOptions ) { - debug('find', className, query, { skip, limit, sort, keys }); + debug('find', className, query, { skip, limit, sort, keys, insensitive }); const hasLimit = limit !== undefined; const hasSkip = skip !== undefined; let values = [className]; - const where = buildWhereClause({ schema, query, index: 2 }); + const where = buildWhereClause({ schema, query, index: 2, insensitive }); values.push(...where.values); const wherePattern = @@ -1949,7 +1980,12 @@ export class PostgresStorageAdapter implements StorageAdapter { ) { debug('count', className, query, readPreference, estimate); const values = [className]; - const where = buildWhereClause({ schema, query, index: 2 }); + const where = buildWhereClause({ + schema, + query, + index: 2, + insensitive: false, + }); values.push(...where.values); const wherePattern = @@ -2002,7 +2038,12 @@ export class PostgresStorageAdapter implements StorageAdapter { schema.fields[fieldName] && schema.fields[fieldName].type === 'Pointer'; const values = [field, column, className]; - const where = buildWhereClause({ schema, query, index: 4 }); + const where = buildWhereClause({ + schema, + query, + index: 4, + insensitive: false, + }); values.push(...where.values); const wherePattern = diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index 31afe569c9..c9e56b8c97 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -13,6 +13,7 @@ export type QueryOptions = { op?: string, distinct?: boolean, pipeline?: any, + insensitive?: boolean, readPreference?: ?string, }; From 9dd7d77e25a390a9bf8b6655ac57d3c1768fcc8a Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Mon, 24 Jun 2019 17:57:03 -0500 Subject: [PATCH 10/33] properly handle auth data null --- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 5 ++++- src/RestWrite.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index d84dbecd06..bb9d017d12 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -293,7 +293,10 @@ const buildWhereClause = ({ } else if (fieldName.indexOf('.') >= 0) { let name = transformDotField(fieldName); if (fieldValue === null) { - patterns.push(`${name} IS NULL`); + patterns.push(`$${index}:raw IS NULL`); + values.push(name); + index += 1; + continue; } else { if (fieldValue.$in) { const inPatterns = []; diff --git a/src/RestWrite.js b/src/RestWrite.js index 9128b4c1a1..fcd4cb0711 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -644,7 +644,7 @@ RestWrite.prototype._validateUserName = function() { { username: this.data.username, objectId: { $ne: this.objectId() }, - _auth_data_anonymous: null, + 'authData.anonymous.id': null, }, { limit: 1, insensitive: true }, {}, From 43aa5dca9306721123b187a63a118d4f0103bff0 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Wed, 10 Jul 2019 09:15:51 -0400 Subject: [PATCH 11/33] wip --- spec/UserController.spec.js | 14 ++++++++++++++ src/Adapters/Storage/StorageAdapter.js | 3 ++- src/Controllers/DatabaseController.js | 16 +++++++++++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 1e4d5e48c3..8be6dd687e 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -3,6 +3,20 @@ const UserController = require('../lib/Controllers/UserController') const emailAdapter = require('./MockEmailAdapter'); const AppCache = require('../lib/cache').AppCache; +fdescribe('boo', function() { + it('boo2', async () => { + const user = await Parse.User.signUp('test', 'test'); + expect(user.getSessionToken).toBeDefined(); + + const fetchedUser = await new Parse.Query(Parse.User).get(user.id); + expect(fetchedUser.getSessionToken()).toBeUndefined(); + + const fetchedUser2 = await new Parse.Query(Parse.User).get(user.id, { + sessionToken: user.getSessionToken(), + }); + expect(fetchedUser2.getSessionToken()).toBeDefined(); + }); +}); describe('UserController', () => { const user = { _email_verify_token: 'testToken', diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index c9e56b8c97..65c4d5cc82 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -81,7 +81,8 @@ export interface StorageAdapter { ensureUniqueness( className: string, schema: SchemaType, - fieldNames: Array + fieldNames: Array, + caseInsensitive?: boolean ): Promise; count( className: string, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 967f2d03fb..baff9ea633 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1,4 +1,4 @@ -// @flow +// @flow // A database adapter that works with data exported from the hosted // Parse database. @@ -1566,7 +1566,12 @@ class DatabaseController { const usernameUniqueness = userClassPromise .then(() => - this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']) + this.adapter.ensureUniqueness( + '_User', + requiredUserFields, + ['username'], + true + ) ) .catch(error => { logger.warn('Unable to ensure uniqueness for usernames: ', error); @@ -1575,7 +1580,12 @@ class DatabaseController { const emailUniqueness = userClassPromise .then(() => - this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']) + this.adapter.ensureUniqueness( + '_User', + requiredUserFields, + ['email'], + true + ) ) .catch(error => { logger.warn( From d58107d3d10f9e5dec2e38f87287b6ab9f6844d4 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Thu, 6 Feb 2020 17:50:11 -0800 Subject: [PATCH 12/33] use 'caseInsensitive' instead of 'insensitive' in all places. --- .../Postgres/PostgresStorageAdapter.js | 58 +++++++++++++------ src/Adapters/Storage/StorageAdapter.js | 2 +- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index b0bf0a4503..2c87eac1ed 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -258,7 +258,7 @@ const buildWhereClause = ({ schema, query, index, - insensitive, + caseInsensitive, }): WhereClause => { const patterns = []; let values = []; @@ -286,7 +286,7 @@ const buildWhereClause = ({ // TODO: Handle querying by _auth_data_provider, authData is stored in authData field continue; } else if ( - insensitive && + caseInsensitive && (fieldName === 'username' || fieldName === 'email') ) { patterns.push(`LOWER($${index}:name) = LOWER($${index + 1})`); @@ -348,7 +348,7 @@ const buildWhereClause = ({ schema, query: subQuery, index, - insensitive, + caseInsensitive, }); if (clause.pattern.length > 0) { clauses.push(clause.pattern); @@ -488,10 +488,16 @@ const buildWhereClause = ({ } }; if (fieldValue.$in) { - createConstraint(_.flatMap(fieldValue.$in, elt => elt), false); + createConstraint( + _.flatMap(fieldValue.$in, elt => elt), + false + ); } if (fieldValue.$nin) { - createConstraint(_.flatMap(fieldValue.$nin, elt => elt), true); + createConstraint( + _.flatMap(fieldValue.$nin, elt => elt), + true + ); } } else if (typeof fieldValue.$in !== 'undefined') { throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $in value'); @@ -1465,7 +1471,7 @@ export class PostgresStorageAdapter implements StorageAdapter { schema, index, query, - insensitive: false, + caseInsensitive: false, }); values.push(...where.values); if (Object.keys(query).length === 0) { @@ -1777,7 +1783,7 @@ export class PostgresStorageAdapter implements StorageAdapter { schema, index, query, - insensitive: false, + caseInsensitive: false, }); values.push(...where.values); @@ -1829,13 +1835,24 @@ export class PostgresStorageAdapter implements StorageAdapter { className: string, schema: SchemaType, query: QueryType, - { skip, limit, sort, keys, insensitive }: QueryOptions + { skip, limit, sort, keys, caseInsensitive }: QueryOptions ) { - debug('find', className, query, { skip, limit, sort, keys, insensitive }); + debug('find', className, query, { + skip, + limit, + sort, + keys, + caseInsensitive, + }); const hasLimit = limit !== undefined; const hasSkip = skip !== undefined; let values = [className]; - const where = buildWhereClause({ schema, query, index: 2, insensitive }); + const where = buildWhereClause({ + schema, + query, + index: 2, + caseInsensitive, + }); values.push(...where.values); const wherePattern = @@ -2065,7 +2082,7 @@ export class PostgresStorageAdapter implements StorageAdapter { schema, query, index: 2, - insensitive: false, + caseInsensitive: false, }); values.push(...where.values); @@ -2123,7 +2140,7 @@ export class PostgresStorageAdapter implements StorageAdapter { schema, query, index: 4, - insensitive: false, + caseInsensitive: false, }); values.push(...where.values); @@ -2408,7 +2425,11 @@ export class PostgresStorageAdapter implements StorageAdapter { }); } - async createIndexes(className: string, indexes: any, conn: ?any): Promise { + async createIndexes( + className: string, + indexes: any, + conn: ?any + ): Promise { return (conn || this._client).tx(t => t.batch( indexes.map(i => { @@ -2428,10 +2449,13 @@ export class PostgresStorageAdapter implements StorageAdapter { type: any, conn: ?any ): Promise { - await (conn || this._client).none( - 'CREATE INDEX $1:name ON $2:name ($3:name)', - [fieldName, className, type] - ); + await ( + conn || this._client + ).none('CREATE INDEX $1:name ON $2:name ($3:name)', [ + fieldName, + className, + type, + ]); } async dropIndexes(className: string, indexes: any, conn: any): Promise { diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index 8747593258..24e4e0c8df 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -13,7 +13,7 @@ export type QueryOptions = { op?: string, distinct?: boolean, pipeline?: any, - insensitive?: boolean, + caseInsensitive?: boolean, readPreference?: ?string, hint?: ?mixed, explain?: Boolean, From 319c06ee4687b2f542ebc41402c9110458fa54e3 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Fri, 7 Feb 2020 12:11:24 -0800 Subject: [PATCH 13/33] update commenet to reclect current plan --- src/RestWrite.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index f3ee6f1a51..38a795bb4d 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -710,18 +710,6 @@ RestWrite.prototype._validateUserName = function() { Users should be able to make case sensitive usernames and login using the case they entered. I.e. 'Snoopy' should preclude 'snoopy' as a valid username. - - However, authentication adapters require a looser check that takes - case into consideration when determining uniqueness. - - The username field should have a unique index on the database as - Failure to enforce through an index allows for a potential collision - for adapter users (a low probability outcome) but more importantly - will have poor performance on this validation. - - The check below has the potential to not allow a valid - username for an adapter other than anonymous, this should - be fixed. */ return this.config.database .find( @@ -729,7 +717,6 @@ RestWrite.prototype._validateUserName = function() { { username: this.data.username, objectId: { $ne: this.objectId() }, - 'authData.anonymous.id': null, }, { limit: 1, caseInsensitive: true }, {}, From e593666815bcaae1fcd7019a0698d693171c8b11 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Fri, 7 Feb 2020 12:15:01 -0800 Subject: [PATCH 14/33] skip the mystery test for now --- spec/UserController.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 8be6dd687e..122c1022c9 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -3,7 +3,7 @@ const UserController = require('../lib/Controllers/UserController') const emailAdapter = require('./MockEmailAdapter'); const AppCache = require('../lib/cache').AppCache; -fdescribe('boo', function() { +xdescribe('boo', function() { it('boo2', async () => { const user = await Parse.User.signUp('test', 'test'); expect(user.getSessionToken).toBeDefined(); From 75cc1f51104c5db960cb3118af92b50b895fa7d2 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Fri, 7 Feb 2020 16:35:32 -0800 Subject: [PATCH 15/33] create case insensitive indecies for mongo to support case insensitive checks for email and username --- .../Storage/Mongo/MongoStorageAdapter.js | 52 ++++++++++++++++++- src/Adapters/Storage/StorageAdapter.js | 10 +++- src/Controllers/DatabaseController.js | 35 +++++++++++++ 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index ac574296ca..44af171d82 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -620,7 +620,16 @@ export class MongoStorageAdapter implements StorageAdapter { className: string, schema: SchemaType, query: QueryType, - { skip, limit, sort, keys, readPreference, hint, explain, caseInsensitive }: QueryOptions + { + skip, + limit, + sort, + keys, + readPreference, + hint, + explain, + caseInsensitive, + }: QueryOptions ): Promise { schema = convertParseSchemaToMongoSchema(schema); const mongoWhere = transformWhere(className, query, schema); @@ -668,6 +677,47 @@ export class MongoStorageAdapter implements StorageAdapter { .catch(err => this.handleError(err)); } + ensureIndex( + className: string, + schema: SchemaType, + fieldNames: string[], + indexName: ?string, + caseInsensitive: boolean = false + ): Promise { + schema = convertParseSchemaToMongoSchema(schema); + const indexCreationRequest = {}; + const mongoFieldNames = fieldNames.map(fieldName => + transformKey(className, fieldName, schema) + ); + mongoFieldNames.forEach(fieldName => { + indexCreationRequest[fieldName] = 1; + }); + + const defaultOptions = { background: true, sparse: true }; + const indexNameOptions = indexName ? { name: indexName } : {}; + const caseInsensitiveOptions = caseInsensitive + ? { collation: { locale: 'en_US', strength: 2 } } + : {}; + const indexOptions = { + ...defaultOptions, + ...caseInsensitiveOptions, + ...indexNameOptions, + }; + + return this._adaptiveCollection(className) + .then( + collection => + new Promise((resolve, reject) => + collection._mongoCollection.createIndex( + indexCreationRequest, + indexOptions, + error => (error ? reject(error) : resolve()) + ) + ) + ) + .catch(err => this.handleError(err)); + } + // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't // currently know which fields are nullable and which aren't, we ignore that criteria. // As such, we shouldn't expose this function to users of parse until we have an out-of-band diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index 24e4e0c8df..f5fd4600f9 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -87,11 +87,17 @@ export interface StorageAdapter { query: QueryType, options: QueryOptions ): Promise<[any]>; + ensureIndex( + className: string, + schema: SchemaType, + fieldNames: string[], + indexName?: string, + caseSensitive?: boolean + ): Promise; ensureUniqueness( className: string, schema: SchemaType, - fieldNames: Array, - caseInsensitive?: boolean + fieldNames: Array ): Promise; count( className: string, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 44043e47f1..7a526a0d78 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1732,6 +1732,24 @@ class DatabaseController { throw error; }); + const usernameCaseInsensitiveIndex = userClassPromise + .then(() => + this.adapter.ensureIndex( + '_User', + requiredUserFields, + ['username'], + 'case_insensitive_username', + true + ) + ) + .catch(error => { + logger.warn( + 'Unable to create case insensitive username index: ', + error + ); + throw error; + }); + const emailUniqueness = userClassPromise .then(() => this.adapter.ensureUniqueness( @@ -1749,6 +1767,21 @@ class DatabaseController { throw error; }); + const emailCaseInsensitiveIndex = userClassPromise + .then(() => + this.adapter.ensureIndex( + '_User', + requiredUserFields, + ['email'], + 'case_insensitive_email', + true + ) + ) + .catch(error => { + logger.warn('Unable to create case insensitive email index: ', error); + throw error; + }); + const roleUniqueness = roleClassPromise .then(() => this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']) @@ -1766,7 +1799,9 @@ class DatabaseController { }); return Promise.all([ usernameUniqueness, + usernameCaseInsensitiveIndex, emailUniqueness, + emailCaseInsensitiveIndex, roleUniqueness, adapterInit, indexPromise, From 4323eac656468e5289a65b3b94166ab45e9c7266 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Mon, 10 Feb 2020 12:01:12 -0800 Subject: [PATCH 16/33] remove unneeded specialKey --- src/Controllers/DatabaseController.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 7a526a0d78..ccf7b96da6 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -63,7 +63,6 @@ const specialQuerykeys = [ '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count', - '_auth_data_anonymous', ]; const isSpecialQueryKey = key => { From 90211f83b22a965b794b7508ee8008e873c7f349 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Mon, 10 Feb 2020 12:01:49 -0800 Subject: [PATCH 17/33] pull collation out to a function. --- src/Adapters/Storage/Mongo/MongoCollection.js | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index fb2f22505d..a31deee6b5 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -15,7 +15,17 @@ export default class MongoCollection { // idea. Or even if this behavior is a good idea. find( query, - { skip, limit, sort, keys, maxTimeMS, readPreference, hint, explain, caseInsensitive } = {} + { + skip, + limit, + sort, + keys, + maxTimeMS, + readPreference, + hint, + explain, + caseInsensitive, + } = {} ) { // Support for Full Text Search - $text if (keys && keys.$score) { @@ -69,9 +79,26 @@ export default class MongoCollection { }); } + /** + * Collation to support case insensitive queries + */ + _caseInsensitiveCollation() { + return { locale: 'en_US', strength: 2 }; + } + _rawFind( query, - { skip, limit, sort, keys, maxTimeMS, readPreference, hint, explain, caseInsensitive } = {} + { + skip, + limit, + sort, + keys, + maxTimeMS, + readPreference, + hint, + explain, + caseInsensitive, + } = {} ) { let findOperation = this._mongoCollection.find(query, { skip, @@ -86,7 +113,7 @@ export default class MongoCollection { } if (caseInsensitive) { - findOperation = findOperation.collation({ locale: 'en_US', strength: 2 }); + findOperation = findOperation.collation(this._caseInsensitiveCollation()); } if (maxTimeMS) { From cb82cf0699156e456298139649bc0c439d980234 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Mon, 10 Feb 2020 12:03:56 -0800 Subject: [PATCH 18/33] not sure what i planned to do with this test. removing. --- spec/UserController.spec.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 122c1022c9..1e4d5e48c3 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -3,20 +3,6 @@ const UserController = require('../lib/Controllers/UserController') const emailAdapter = require('./MockEmailAdapter'); const AppCache = require('../lib/cache').AppCache; -xdescribe('boo', function() { - it('boo2', async () => { - const user = await Parse.User.signUp('test', 'test'); - expect(user.getSessionToken).toBeDefined(); - - const fetchedUser = await new Parse.Query(Parse.User).get(user.id); - expect(fetchedUser.getSessionToken()).toBeUndefined(); - - const fetchedUser2 = await new Parse.Query(Parse.User).get(user.id, { - sessionToken: user.getSessionToken(), - }); - expect(fetchedUser2.getSessionToken()).toBeDefined(); - }); -}); describe('UserController', () => { const user = { _email_verify_token: 'testToken', From 7626468d7e90b1a90cf7bafc5c00329c86da3656 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Mon, 10 Feb 2020 12:16:11 -0800 Subject: [PATCH 19/33] remove typo --- src/Controllers/DatabaseController.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index ccf7b96da6..e35dc17e25 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1751,12 +1751,7 @@ class DatabaseController { const emailUniqueness = userClassPromise .then(() => - this.adapter.ensureUniqueness( - '_User', - requiredUserFields, - ['email'], - true - ) + this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']) ) .catch(error => { logger.warn( From a40e3a238eabdd0cf77e5f3dccea60ca4d8bba6e Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Mon, 10 Feb 2020 12:20:41 -0800 Subject: [PATCH 20/33] remove another unused flag --- src/Controllers/DatabaseController.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index e35dc17e25..04b25e14b0 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1719,12 +1719,7 @@ class DatabaseController { const usernameUniqueness = userClassPromise .then(() => - this.adapter.ensureUniqueness( - '_User', - requiredUserFields, - ['username'], - true - ) + this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']) ) .catch(error => { logger.warn('Unable to ensure uniqueness for usernames: ', error); From 85a2398fb2a9a30e1caba6a7ca336a9d26a59485 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Mon, 10 Feb 2020 12:23:53 -0800 Subject: [PATCH 21/33] maintain order --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 44af171d82..45037c63ce 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -661,9 +661,9 @@ export class MongoStorageAdapter implements StorageAdapter { keys: mongoKeys, maxTimeMS: this._maxTimeMS, readPreference, - caseInsensitive, hint, explain, + caseInsensitive, }) ) .then(objects => { From 8d5be2c66b72f96d5aa88423ee694216fca26b79 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Mon, 10 Feb 2020 12:25:31 -0800 Subject: [PATCH 22/33] maintain order of params --- src/Adapters/Storage/StorageAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index f5fd4600f9..ca4b406b37 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -13,10 +13,10 @@ export type QueryOptions = { op?: string, distinct?: boolean, pipeline?: any, - caseInsensitive?: boolean, readPreference?: ?string, hint?: ?mixed, explain?: Boolean, + caseInsensitive?: boolean, action?: string, addsField?: boolean, }; From cdb73d2dff6781ebc343e4730205a68f1089d732 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Mon, 10 Feb 2020 12:30:16 -0800 Subject: [PATCH 23/33] boil the ocean on param sequence i like having explain last cause it seems like something you would change/remove after getting what you want from the explain? --- src/Adapters/Storage/Mongo/MongoCollection.js | 8 ++++---- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 4 ++-- src/Controllers/DatabaseController.js | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index a31deee6b5..1b9ec130ea 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -23,8 +23,8 @@ export default class MongoCollection { maxTimeMS, readPreference, hint, - explain, caseInsensitive, + explain, } = {} ) { // Support for Full Text Search - $text @@ -39,8 +39,8 @@ export default class MongoCollection { keys, maxTimeMS, readPreference, - caseInsensitive, hint, + caseInsensitive, explain, }).catch(error => { // Check for "no geoindex" error @@ -70,8 +70,8 @@ export default class MongoCollection { keys, maxTimeMS, readPreference, - caseInsensitive, hint, + caseInsensitive, explain, }) ) @@ -96,8 +96,8 @@ export default class MongoCollection { maxTimeMS, readPreference, hint, - explain, caseInsensitive, + explain, } = {} ) { let findOperation = this._mongoCollection.find(query, { diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 45037c63ce..5bcc38c408 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -627,8 +627,8 @@ export class MongoStorageAdapter implements StorageAdapter { keys, readPreference, hint, - explain, caseInsensitive, + explain, }: QueryOptions ): Promise { schema = convertParseSchemaToMongoSchema(schema); @@ -662,8 +662,8 @@ export class MongoStorageAdapter implements StorageAdapter { maxTimeMS: this._maxTimeMS, readPreference, hint, - explain, caseInsensitive, + explain, }) ) .then(objects => { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 04b25e14b0..4b69d05e29 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1317,8 +1317,8 @@ class DatabaseController { distinct, pipeline, readPreference, - caseInsensitive = false, hint, + caseInsensitive = false, explain, }: any = {}, auth: any = {}, @@ -1369,8 +1369,8 @@ class DatabaseController { sort, keys, readPreference, - caseInsensitive, hint, + caseInsensitive, explain, }; Object.keys(sort).forEach(fieldName => { From 78c63c41d926493e0c9bbb6120a7d465654c700c Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Mon, 10 Feb 2020 14:49:11 -0800 Subject: [PATCH 24/33] add test to verify creation and use of caseInsensitive index --- spec/MongoStorageAdapter.spec.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index 456c3a4661..99370ef93a 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -318,6 +318,38 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { ); }); + it('should use index for caseInsensitive query', async () => { + const user = new Parse.User(); + user.set('username', 'Bugs'); + user.set('password', 'Bunny'); + await user.signUp(); + + const database = Config.get(Parse.applicationId).database; + const preIndexPlan = await database.find( + '_User', + { username: 'bugs' }, + { caseInsensitive: true, explain: true } + ); + + const schema = await new Parse.Schema('_User').get(); + + await database.adapter.ensureIndex( + '_User', + schema, + ['username'], + 'case_insensitive_username', + true + ); + + const postIndexPlan = await database.find( + '_User', + { username: 'bugs' }, + { caseInsensitive: true, explain: true } + ); + expect(preIndexPlan.executionStats.executionStages.stage).toBe('COLLSCAN'); + expect(postIndexPlan.executionStats.executionStages.stage).toBe('FETCH'); + }); + if ( process.env.MONGODB_VERSION === '4.0.4' && process.env.MONGODB_TOPOLOGY === 'replicaset' && From a1c012ae912aad15b09aaad6e4ead4bd17e35e1b Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Mon, 10 Feb 2020 15:19:32 -0800 Subject: [PATCH 25/33] add no op func to prostgress --- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 2c87eac1ed..0fc5d4c820 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -2512,6 +2512,11 @@ export class PostgresStorageAdapter implements StorageAdapter { ); return result; } + + // TODO: implement? + ensureIndex(): Promise { + return Promise.resolve(); + } } function convertPolygonToSQL(polygon) { From 7913cd20981ffd7d00ae842240308371e4111cf1 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Mon, 10 Feb 2020 15:54:46 -0800 Subject: [PATCH 26/33] get collation object from mongocollection make flow lint happy by declaring things Object. --- src/Adapters/Storage/Mongo/MongoCollection.js | 4 ++-- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index 1b9ec130ea..248ee8e06b 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -82,7 +82,7 @@ export default class MongoCollection { /** * Collation to support case insensitive queries */ - _caseInsensitiveCollation() { + static caseInsensitiveCollation() { return { locale: 'en_US', strength: 2 }; } @@ -113,7 +113,7 @@ export default class MongoCollection { } if (caseInsensitive) { - findOperation = findOperation.collation(this._caseInsensitiveCollation()); + findOperation = findOperation.collation(this.caseInsensitiveCollation()); } if (maxTimeMS) { diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 5bcc38c408..b9970fd353 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -693,12 +693,12 @@ export class MongoStorageAdapter implements StorageAdapter { indexCreationRequest[fieldName] = 1; }); - const defaultOptions = { background: true, sparse: true }; - const indexNameOptions = indexName ? { name: indexName } : {}; - const caseInsensitiveOptions = caseInsensitive - ? { collation: { locale: 'en_US', strength: 2 } } + const defaultOptions: Object = { background: true, sparse: true }; + const indexNameOptions: Object = indexName ? { name: indexName } : {}; + const caseInsensitiveOptions: Object = caseInsensitive + ? { collation: MongoCollection.caseInsensitiveCollation } : {}; - const indexOptions = { + const indexOptions: Object = { ...defaultOptions, ...caseInsensitiveOptions, ...indexNameOptions, From 0a04f2ad4108b665049cbf6b672a7ced9df09884 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Tue, 11 Feb 2020 00:21:29 +0000 Subject: [PATCH 27/33] fix typo --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index b9970fd353..b6902fd764 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -696,7 +696,7 @@ export class MongoStorageAdapter implements StorageAdapter { const defaultOptions: Object = { background: true, sparse: true }; const indexNameOptions: Object = indexName ? { name: indexName } : {}; const caseInsensitiveOptions: Object = caseInsensitive - ? { collation: MongoCollection.caseInsensitiveCollation } + ? { collation: MongoCollection.caseInsensitiveCollation() } : {}; const indexOptions: Object = { ...defaultOptions, From 15e2b54e43d010554da8092f5890c1ed30bb9d76 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Tue, 11 Feb 2020 00:28:28 +0000 Subject: [PATCH 28/33] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 625becbbdc..78716e9c8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### master [Full Changelog](https://github.com/parse-community/parse-server/compare/3.10.0...master) +- FIX: Prevent user sign up if username or email already exist when compared case insensitively. For example if a user sign up with the name 'Jane', sign up will fail if there is already a user with the `username` 'jane'. [#5634](https://github.com/parse-community/parse-server/pull/5634). Thanks to [Arthur Cinader](https://github.com/acinader) ### 3.10.0 [Full Changelog](https://github.com/parse-community/parse-server/compare/3.9.0...3.10.0) From d5deca80c29dad0eecaa388322df448c7cf56da0 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Tue, 11 Feb 2020 00:51:11 +0000 Subject: [PATCH 29/33] kick travis From 3ec5995080d92c39fe71be69b07b376f7dcb3fdd Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Tue, 11 Feb 2020 10:32:15 -0800 Subject: [PATCH 30/33] properly reference static method --- src/Adapters/Storage/Mongo/MongoCollection.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index 248ee8e06b..8ab24fc29b 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -113,7 +113,9 @@ export default class MongoCollection { } if (caseInsensitive) { - findOperation = findOperation.collation(this.caseInsensitiveCollation()); + findOperation = findOperation.collation( + MongoCollection.caseInsensitiveCollation() + ); } if (maxTimeMS) { From 201839a6a67541bd251adf14730dbc49067f30f7 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Thu, 13 Feb 2020 17:50:42 -0800 Subject: [PATCH 31/33] add a test to confirm that anonymous users with unique username that do collide when compared insensitively can still be created. --- spec/ParseUser.spec.js | 49 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index e7586e1a11..772dd305c5 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -12,6 +12,7 @@ const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageA const request = require('../lib/request'); const passwordCrypto = require('../lib/password'); const Config = require('../lib/Config'); +const cryptoUtils = require('../lib/cryptoUtils'); function verifyACL(user) { const ACL = user.getACL(); @@ -2279,7 +2280,7 @@ describe('Parse.User testing', () => { ); }); - it('signup should fail with duplicate case insensitive email', async() => { + it('signup should fail with duplicate case insensitive email', async () => { const user = new Parse.User(); user.setUsername('test1'); user.setPassword('test'); @@ -2298,7 +2299,7 @@ describe('Parse.User testing', () => { ); }); - it('edit should fail with duplicate case insensitive email', async() => { + it('edit should fail with duplicate case insensitive email', async () => { const user = new Parse.User(); user.setUsername('test1'); user.setPassword('test'); @@ -2311,7 +2312,7 @@ describe('Parse.User testing', () => { user2.setEmail('Foo@Example.Com'); await user2.signUp(); - user2.setEmail('Test@Example.Com') + user2.setEmail('Test@Example.Com'); await expectAsync(user2.save()).toBeRejectedWith( new Parse.Error( Parse.Error.EMAIL_TAKEN, @@ -2319,6 +2320,48 @@ describe('Parse.User testing', () => { ) ); }); + + describe('anonymous users', () => { + beforeEach(() => { + const insensitiveCollisions = [ + 'abcdefghijklmnop', + 'Abcdefghijklmnop', + 'ABcdefghijklmnop', + 'ABCdefghijklmnop', + 'ABCDefghijklmnop', + 'ABCDEfghijklmnop', + 'ABCDEFghijklmnop', + 'ABCDEFGhijklmnop', + 'ABCDEFGHijklmnop', + 'ABCDEFGHIjklmnop', + 'ABCDEFGHIJklmnop', + 'ABCDEFGHIJKlmnop', + 'ABCDEFGHIJKLmnop', + 'ABCDEFGHIJKLMnop', + 'ABCDEFGHIJKLMnop', + 'ABCDEFGHIJKLMNop', + 'ABCDEFGHIJKLMNOp', + 'ABCDEFGHIJKLMNOP', + ]; + // Random String gets called a lot before we get to relevant code + spyOn(cryptoUtils, 'randomString').and.returnValues( + ...insensitiveCollisions + ); + }); + + it('should not fail on case insensitive matches', async () => { + const user1 = await Parse.AnonymousUtils.logIn(); + const username1 = user1.get('username'); + expect(username1).not.toBeUndefined(); + + const user2 = await Parse.AnonymousUtils.logIn(); + const username2 = user2.get('username'); + expect(username2).not.toBeUndefined(); + + expect(username2).not.toBe(username1); + expect(username2.toLowerCase()).toBe(username1.toLowerCase()); + }); + }); }); it('user cannot update email to existing user', done => { From 5d0e0990a81b275873cf8cd5913b9c0260526fbe Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Thu, 13 Feb 2020 22:00:40 -0800 Subject: [PATCH 32/33] minot doc nits --- CHANGELOG.md | 2 +- spec/ParseUser.spec.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78716e9c8b..7a370f8b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ### master [Full Changelog](https://github.com/parse-community/parse-server/compare/3.10.0...master) -- FIX: Prevent user sign up if username or email already exist when compared case insensitively. For example if a user sign up with the name 'Jane', sign up will fail if there is already a user with the `username` 'jane'. [#5634](https://github.com/parse-community/parse-server/pull/5634). Thanks to [Arthur Cinader](https://github.com/acinader) +- FIX: Prevent user sign up if username or email already exist when compared case insensitively. For example if a user tries to sign up with the name 'Jane', sign up will fail if there is already a user with the `username` 'jane'. [#5634](https://github.com/parse-community/parse-server/pull/5634). Thanks to [Arthur Cinader](https://github.com/acinader) ### 3.10.0 [Full Changelog](https://github.com/parse-community/parse-server/compare/3.9.0...3.10.0) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 772dd305c5..1cde919766 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -2343,7 +2343,8 @@ describe('Parse.User testing', () => { 'ABCDEFGHIJKLMNOp', 'ABCDEFGHIJKLMNOP', ]; - // Random String gets called a lot before we get to relevant code + + // need a bunch of spare random strings per api request spyOn(cryptoUtils, 'randomString').and.returnValues( ...insensitiveCollisions ); From 8227dcab46b29fd91a58ea92bc9fd39a8d22d053 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Fri, 14 Feb 2020 09:26:48 -0800 Subject: [PATCH 33/33] add a few tests to make sure our spy is working as expected wordsmith the changelog --- CHANGELOG.md | 2 +- spec/ParseUser.spec.js | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a370f8b7b..1488539480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ### master [Full Changelog](https://github.com/parse-community/parse-server/compare/3.10.0...master) -- FIX: Prevent user sign up if username or email already exist when compared case insensitively. For example if a user tries to sign up with the name 'Jane', sign up will fail if there is already a user with the `username` 'jane'. [#5634](https://github.com/parse-community/parse-server/pull/5634). Thanks to [Arthur Cinader](https://github.com/acinader) +- FIX: FIX: Prevent new usernames or emails that clash with existing users' email or username if it only differs by case. For example, don't allow a new user with the name 'Jane' if we already have a user 'jane'. [#5634](https://github.com/parse-community/parse-server/pull/5634). Thanks to [Arthur Cinader](https://github.com/acinader) ### 3.10.0 [Full Changelog](https://github.com/parse-community/parse-server/compare/3.9.0...3.10.0) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 1cde919766..8484cb4b17 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -2353,14 +2353,16 @@ describe('Parse.User testing', () => { it('should not fail on case insensitive matches', async () => { const user1 = await Parse.AnonymousUtils.logIn(); const username1 = user1.get('username'); - expect(username1).not.toBeUndefined(); const user2 = await Parse.AnonymousUtils.logIn(); const username2 = user2.get('username'); - expect(username2).not.toBeUndefined(); + expect(username1).not.toBeUndefined(); + expect(username2).not.toBeUndefined(); + expect(username1.toLowerCase()).toBe('abcdefghijklmnop'); + expect(username2.toLowerCase()).toBe('abcdefghijklmnop'); expect(username2).not.toBe(username1); - expect(username2.toLowerCase()).toBe(username1.toLowerCase()); + expect(username2.toLowerCase()).toBe(username1.toLowerCase()); // this is redundant :). }); }); });