Skip to content

Case insensitive signup #5634

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Feb 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
866b08b
Always delete data after each, even for mongo.
acinader Jun 4, 2019
70e3210
Add failing simple case test
acinader Jun 4, 2019
8d0210b
run all tests
acinader Jun 5, 2019
e9f7001
1. when validating username be case insensitive
acinader Jun 5, 2019
1dc0e34
Merge branch 'master' into case-insensitive-signup
acinader Jun 11, 2019
e1a2140
Merge branch 'master' into case-insensitive-signup
acinader Jun 11, 2019
00b5773
Merge branch 'master' into case-insensitive-signup
acinader Jun 12, 2019
0c91c3c
Merge branch 'master' into case-insensitive-signup
acinader Jun 21, 2019
6149c86
More case sensitivity
acinader Jun 21, 2019
7d0d14c
wordsmithery and grammar
acinader Jun 21, 2019
bbc1d73
first pass at a preformant case insensitive query. mongo only so far.
acinader Jun 23, 2019
0052c6b
change name of parameter from insensitive to
acinader Jun 23, 2019
972cc37
Postgres support
dplewis Jun 24, 2019
9dd7d77
properly handle auth data null
dplewis Jun 24, 2019
df4fea4
Merge branch 'case-insensitive-signup' of github.com:acinader/parse-s…
acinader Jul 9, 2019
43aa5dc
wip
acinader Jul 10, 2019
a94fa63
Merge branch 'master' into case-insensitive-signup
acinader Feb 7, 2020
d58107d
use 'caseInsensitive' instead of 'insensitive' in all places.
acinader Feb 7, 2020
319c06e
update commenet to reclect current plan
acinader Feb 7, 2020
e593666
skip the mystery test for now
acinader Feb 7, 2020
75cc1f5
create case insensitive indecies for
acinader Feb 8, 2020
4323eac
remove unneeded specialKey
acinader Feb 10, 2020
90211f8
pull collation out to a function.
acinader Feb 10, 2020
cb82cf0
not sure what i planned
acinader Feb 10, 2020
7626468
remove typo
acinader Feb 10, 2020
a40e3a2
remove another unused flag
acinader Feb 10, 2020
85a2398
maintain order
acinader Feb 10, 2020
8d5be2c
maintain order of params
acinader Feb 10, 2020
cdb73d2
boil the ocean on param sequence
acinader Feb 10, 2020
78c63c4
add test to verify creation
acinader Feb 10, 2020
a1c012a
add no op func to prostgress
acinader Feb 10, 2020
7913cd2
get collation object from mongocollection
acinader Feb 10, 2020
0a04f2a
fix typo
acinader Feb 11, 2020
15e2b54
add changelog
acinader Feb 11, 2020
d5deca8
kick travis
acinader Feb 11, 2020
bccdb16
Merge branch 'master' into case-insensitive-signup
acinader Feb 11, 2020
3ec5995
properly reference static method
acinader Feb 11, 2020
201839a
add a test to confirm that anonymous users with
acinader Feb 14, 2020
5d0e099
minot doc nits
acinader Feb 14, 2020
8227dca
add a few tests to make sure our spy is working as expected
acinader Feb 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### master

[Full Changelog](https://github.com/parse-community/parse-server/compare/3.10.0...master)
- 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)
Expand Down
32 changes: 32 additions & 0 deletions spec/MongoStorageAdapter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' &&
Expand Down
123 changes: 123 additions & 0 deletions spec/ParseUser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -2244,6 +2245,128 @@ describe('Parse.User testing', () => {
);
});

describe('case insensitive signup not allowed', () => {
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');
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('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('[email protected]');
await user.signUp();

const user2 = new Parse.User();
user2.setUsername('test2');
user2.setPassword('test');
user2.setEmail('[email protected]');
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('[email protected]');
await user.signUp();

const user2 = new Parse.User();
user2.setUsername('test2');
user2.setPassword('test');
user2.setEmail('[email protected]');
await user2.signUp();

user2.setEmail('[email protected]');
await expectAsync(user2.save()).toBeRejectedWith(
new Parse.Error(
Parse.Error.EMAIL_TAKEN,
'Account already exists for this email address.'
)
);
});

describe('anonymous users', () => {
beforeEach(() => {
const insensitiveCollisions = [
'abcdefghijklmnop',
'Abcdefghijklmnop',
'ABcdefghijklmnop',
'ABCdefghijklmnop',
'ABCDefghijklmnop',
'ABCDEfghijklmnop',
'ABCDEFghijklmnop',
'ABCDEFGhijklmnop',
'ABCDEFGHijklmnop',
'ABCDEFGHIjklmnop',
'ABCDEFGHIJklmnop',
'ABCDEFGHIJKlmnop',
'ABCDEFGHIJKLmnop',
'ABCDEFGHIJKLMnop',
'ABCDEFGHIJKLMnop',
'ABCDEFGHIJKLMNop',
'ABCDEFGHIJKLMNOp',
'ABCDEFGHIJKLMNOP',
];

// need a bunch of spare random strings per api request
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');

const user2 = await Parse.AnonymousUtils.logIn();
const username2 = user2.get('username');

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()); // this is redundant :).
});
});
});

it('user cannot update email to existing user', done => {
const user = new Parse.User();
user.set('username', 'test1');
Expand Down
8 changes: 1 addition & 7 deletions spec/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,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
Expand Down
39 changes: 37 additions & 2 deletions src/Adapters/Storage/Mongo/MongoCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {}
{
skip,
limit,
sort,
keys,
maxTimeMS,
readPreference,
hint,
caseInsensitive,
explain,
} = {}
) {
// Support for Full Text Search - $text
if (keys && keys.$score) {
Expand All @@ -30,6 +40,7 @@ export default class MongoCollection {
maxTimeMS,
readPreference,
hint,
caseInsensitive,
explain,
}).catch(error => {
// Check for "no geoindex" error
Expand Down Expand Up @@ -60,16 +71,34 @@ export default class MongoCollection {
maxTimeMS,
readPreference,
hint,
caseInsensitive,
explain,
})
)
);
});
}

/**
* Collation to support case insensitive queries
*/
static caseInsensitiveCollation() {
return { locale: 'en_US', strength: 2 };
}

_rawFind(
query,
{ skip, limit, sort, keys, maxTimeMS, readPreference, hint, explain } = {}
{
skip,
limit,
sort,
keys,
maxTimeMS,
readPreference,
hint,
caseInsensitive,
explain,
} = {}
) {
let findOperation = this._mongoCollection.find(query, {
skip,
Expand All @@ -83,6 +112,12 @@ export default class MongoCollection {
findOperation = findOperation.project(keys);
}

if (caseInsensitive) {
findOperation = findOperation.collation(
MongoCollection.caseInsensitiveCollation()
);
}

if (maxTimeMS) {
findOperation = findOperation.maxTimeMS(maxTimeMS);
}
Expand Down
53 changes: 52 additions & 1 deletion src/Adapters/Storage/Mongo/MongoStorageAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,16 @@ export class MongoStorageAdapter implements StorageAdapter {
className: string,
schema: SchemaType,
query: QueryType,
{ skip, limit, sort, keys, readPreference, hint, explain }: QueryOptions
{
skip,
limit,
sort,
keys,
readPreference,
hint,
caseInsensitive,
explain,
}: QueryOptions
): Promise<any> {
schema = convertParseSchemaToMongoSchema(schema);
const mongoWhere = transformWhere(className, query, schema);
Expand Down Expand Up @@ -653,6 +662,7 @@ export class MongoStorageAdapter implements StorageAdapter {
maxTimeMS: this._maxTimeMS,
readPreference,
hint,
caseInsensitive,
explain,
})
)
Expand All @@ -667,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<any> {
schema = convertParseSchemaToMongoSchema(schema);
const indexCreationRequest = {};
const mongoFieldNames = fieldNames.map(fieldName =>
transformKey(className, fieldName, schema)
);
mongoFieldNames.forEach(fieldName => {
indexCreationRequest[fieldName] = 1;
});

const defaultOptions: Object = { background: true, sparse: true };
const indexNameOptions: Object = indexName ? { name: indexName } : {};
const caseInsensitiveOptions: Object = caseInsensitive
? { collation: MongoCollection.caseInsensitiveCollation() }
: {};
const indexOptions: Object = {
...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
Expand Down
Loading