Skip to content

Commit 97b6b07

Browse files
committed
Merge branch 'add-account-unlock-on-password-reset' into deploy-debug
* add-account-unlock-on-password-reset: moved changelog entry to correct position Added docs entry added changelog entry added account policy option added account unlock on password reset fix: upgrade ws from 7.4.1 to 7.4.2 (parse-community#7132) Supporting patterns in classNames for Live Queries (parse-community#7131) add api mail adapter to mail adapter list (parse-community#7126) # Conflicts: # CHANGELOG.md # src/Config.js # src/Options/Definitions.js
2 parents a2378fa + fbc8748 commit 97b6b07

13 files changed

+201
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ __BREAKING CHANGES:__
77
- NEW: Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html). [#7071](https://github.com/parse-community/parse-server/pull/7071). Thanks to [dblythy](https://github.com/dblythy).
88
___
99
- NEW (EXPERIMENTAL): Added page localization for password reset and email verification. **Caution, this is an experimental feature that may not be appropriate for production.** [#6891](https://github.com/parse-community/parse-server/issues/6891). Thanks to [Manuel Trezza](https://github.com/mtrezza).
10+
- IMPROVE: Added new account lockout policy option `accountLockout.unlockOnPasswordReset` to automatically unlock account on password reset. [#7146](https://github.com/parse-community/parse-server/pull/7146). Thanks to [Manuel Trezza](https://github.com/mtrezza).
1011
- IMPROVE: Optimize queries on classes with pointer permissions. [#7061](https://github.com/parse-community/parse-server/pull/7061). Thanks to [Pedro Diaz](https://github.com/pdiaz)
1112
- FIX: request.context for afterFind triggers. [#7078](https://github.com/parse-community/parse-server/pull/7078). Thanks to [dblythy](https://github.com/dblythy)
1213
- NEW: Added convenience method Parse.Cloud.sendEmail(...) to send email via email adapter in Cloud Code. [#7089](https://github.com/parse-community/parse-server/pull/7089). Thanks to [dblythy](https://github.com/dblythy)
1314
- FIX: Winston Logger interpolating stdout to console [#7114](https://github.com/parse-community/parse-server/pull/7114). Thanks to [dplewis](https://github.com/dplewis)
1415
- NEW: LiveQuery support for $and, $nor, $containedBy, $geoWithin, $geoIntersects queries [#7113](https://github.com/parse-community/parse-server/pull/7113). Thanks to [dplewis](https://github.com/dplewis)
16+
- NEW: Supporting patterns in LiveQuery server's config parameter `classNames` [#7131](https://github.com/parse-community/parse-server/pull/7131). Thanks to [Nes-si](https://github.com/Nes-si)
1517

1618
### 4.5.0
1719
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0)

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ var server = ParseServer({
317317
accountLockout: {
318318
duration: 5, // duration policy setting determines the number of minutes that a locked-out account remains locked out before automatically becoming unlocked. Set it to a value greater than 0 and less than 100000.
319319
threshold: 3, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000.
320+
unlockOnPasswordReset: true, // Is true if the account lock should be removed after a successful password reset. Default: false.
321+
}
320322
},
321323
// optional settings to enforce password policies
322324
passwordPolicy: {
@@ -347,6 +349,7 @@ You can also use other email adapters contributed by the community such as:
347349
- [parse-server-mailjet-adapter](https://www.npmjs.com/package/parse-server-mailjet-adapter)
348350
- [simple-parse-smtp-adapter](https://www.npmjs.com/package/simple-parse-smtp-adapter)
349351
- [parse-server-generic-email-adapter](https://www.npmjs.com/package/parse-server-generic-email-adapter)
352+
- [parse-server-api-mail-adapter](https://www.npmjs.com/package/parse-server-api-mail-adapter)
350353

351354
### Custom Pages
352355

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"uuid": "8.3.2",
5959
"winston": "3.3.3",
6060
"winston-daily-rotate-file": "4.5.0",
61-
"ws": "7.4.1"
61+
"ws": "7.4.2"
6262
},
6363
"devDependencies": {
6464
"@babel/cli": "7.10.0",

spec/AccountLockoutPolicy.spec.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use strict';
22

33
const Config = require('../lib/Config');
4+
const Definitions = require('../lib/Options/Definitions');
5+
const request = require('../lib/request');
46

57
const loginWithWrongCredentialsShouldFail = function (username, password) {
68
return new Promise((resolve, reject) => {
@@ -340,3 +342,118 @@ describe('Account Lockout Policy: ', () => {
340342
});
341343
});
342344
});
345+
346+
describe('lockout with password reset option', () => {
347+
let sendPasswordResetEmail;
348+
349+
async function setup(options = {}) {
350+
const accountLockout = Object.assign(
351+
{
352+
duration: 10000,
353+
threshold: 1,
354+
}, options
355+
);
356+
const config = {
357+
appName: 'exampleApp',
358+
accountLockout: accountLockout,
359+
publicServerURL: 'http://localhost:8378/1',
360+
emailAdapter: {
361+
sendVerificationEmail: () => Promise.resolve(),
362+
sendPasswordResetEmail: () => Promise.resolve(),
363+
sendMail: () => {},
364+
},
365+
};
366+
await reconfigureServer(config);
367+
368+
sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough();
369+
}
370+
371+
it('accepts valid unlockOnPasswordReset option', async () => {
372+
const values = [true, false];
373+
374+
for (const value of values) {
375+
await expectAsync(setup({ unlockOnPasswordReset: value })).toBeResolved();
376+
}
377+
});
378+
379+
it('rejects invalid unlockOnPasswordReset option', async () => {
380+
const values = ["a", 0, {}, [], null];
381+
382+
for (const value of values) {
383+
await expectAsync(setup({ unlockOnPasswordReset: value })).toBeRejected();
384+
}
385+
});
386+
387+
it('uses default value if unlockOnPasswordReset is not set', async () => {
388+
await expectAsync(setup({ unlockOnPasswordReset: undefined })).toBeResolved();
389+
390+
const parseConfig = Config.get(Parse.applicationId);
391+
expect(parseConfig.accountLockout.unlockOnPasswordReset).toBe(Definitions.AccountLockoutOptions.unlockOnPasswordReset.default);
392+
});
393+
394+
it('allow login for locked account after password reset', async () => {
395+
await setup({ unlockOnPasswordReset: true });
396+
const config = Config.get(Parse.applicationId);
397+
398+
const user = new Parse.User();
399+
const username = 'exampleUsername';
400+
const password = 'examplePassword';
401+
user.setUsername(username);
402+
user.setPassword(password);
403+
user.setEmail('mail@example.com');
404+
await user.signUp();
405+
406+
await expectAsync(Parse.User.logIn(username, 'incorrectPassword')).toBeRejected();
407+
await expectAsync(Parse.User.logIn(username, password)).toBeRejected();
408+
409+
await Parse.User.requestPasswordReset(user.getEmail());
410+
const link = sendPasswordResetEmail.calls.all()[0].args[0].link;
411+
const linkUrl = new URL(link);
412+
const token = linkUrl.searchParams.get('token');
413+
const newPassword = 'newPassword';
414+
await request({
415+
method: 'POST',
416+
url: `${config.publicServerURL}/apps/test/request_password_reset`,
417+
body: `new_password=${newPassword}&token=${token}&username=${username}`,
418+
headers: {
419+
'Content-Type': 'application/x-www-form-urlencoded',
420+
},
421+
followRedirects: false,
422+
});
423+
424+
await expectAsync(Parse.User.logIn(username, newPassword)).toBeResolved();
425+
});
426+
427+
it('reject login for locked account after password reset (default)', async () => {
428+
await setup();
429+
const config = Config.get(Parse.applicationId);
430+
431+
const user = new Parse.User();
432+
const username = 'exampleUsername';
433+
const password = 'examplePassword';
434+
user.setUsername(username);
435+
user.setPassword(password);
436+
user.setEmail('mail@example.com');
437+
await user.signUp();
438+
439+
await expectAsync(Parse.User.logIn(username, 'incorrectPassword')).toBeRejected();
440+
await expectAsync(Parse.User.logIn(username, password)).toBeRejected();
441+
442+
await Parse.User.requestPasswordReset(user.getEmail());
443+
const link = sendPasswordResetEmail.calls.all()[0].args[0].link;
444+
const linkUrl = new URL(link);
445+
const token = linkUrl.searchParams.get('token');
446+
const newPassword = 'newPassword';
447+
await request({
448+
method: 'POST',
449+
url: `${config.publicServerURL}/apps/test/request_password_reset`,
450+
body: `new_password=${newPassword}&token=${token}&username=${username}`,
451+
headers: {
452+
'Content-Type': 'application/x-www-form-urlencoded',
453+
},
454+
followRedirects: false,
455+
});
456+
457+
await expectAsync(Parse.User.logIn(username, newPassword)).toBeRejected();
458+
});
459+
});

spec/ParseLiveQuery.spec.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,29 @@ describe('ParseLiveQuery', function () {
5656
await object.save();
5757
});
5858

59+
it('can use patterns in className', async done => {
60+
await reconfigureServer({
61+
liveQuery: {
62+
classNames: ['Test.*'],
63+
},
64+
startLiveQueryServer: true,
65+
verbose: false,
66+
silent: true,
67+
});
68+
const object = new TestObject();
69+
await object.save();
70+
71+
const query = new Parse.Query(TestObject);
72+
query.equalTo('objectId', object.id);
73+
const subscription = await query.subscribe();
74+
subscription.on('update', object => {
75+
expect(object.get('foo')).toBe('bar');
76+
done();
77+
});
78+
object.set({ foo: 'bar' });
79+
await object.save();
80+
});
81+
5982
it('expect afterEvent create', async done => {
6083
await reconfigureServer({
6184
liveQuery: {

src/AccountLockout.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,23 @@ export class AccountLockout {
158158
}
159159
});
160160
}
161+
162+
/**
163+
* Removes the account lockout.
164+
*/
165+
unlockAccount() {
166+
if (!this._config.accountLockout || !this._config.accountLockout.unlockOnPasswordReset) {
167+
return Promise.resolve();
168+
}
169+
return this._config.database.update(
170+
'_User',
171+
{ username: this._user.username },
172+
{
173+
_failed_login_count: { __op: 'Delete' },
174+
_account_lockout_expires_at: { __op: 'Delete' },
175+
},
176+
);
177+
}
161178
}
162179

163180
export default AccountLockout;

src/Config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
IdempotencyOptions,
1111
FileUploadOptions,
1212
PagesOptions,
13+
AccountLockoutOptions,
1314
} from './Options/Definitions';
1415
import { isBoolean, isString } from 'lodash';
1516

@@ -186,6 +187,12 @@ export class Config {
186187
) {
187188
throw 'Account lockout threshold should be an integer greater than 0 and less than 1000';
188189
}
190+
191+
if (accountLockout.unlockOnPasswordReset === undefined) {
192+
accountLockout.unlockOnPasswordReset = AccountLockoutOptions.unlockOnPasswordReset.default;
193+
} else if (!isBoolean(accountLockout.unlockOnPasswordReset)) {
194+
throw 'Parse Server option accountLockout.unlockOnPasswordReset must be a boolean.';
195+
}
189196
}
190197
}
191198

src/Controllers/LiveQueryController.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ export class LiveQueryController {
99
if (!config || !config.classNames) {
1010
this.classNames = new Set();
1111
} else if (config.classNames instanceof Array) {
12-
this.classNames = new Set(config.classNames);
12+
const classNames = config.classNames
13+
.map(name => new RegExp("^" + name + "$"));
14+
this.classNames = new Set(classNames);
1315
} else {
1416
throw 'liveQuery.classes should be an array of string';
1517
}
@@ -43,7 +45,12 @@ export class LiveQueryController {
4345
}
4446

4547
hasLiveQuery(className: string): boolean {
46-
return this.classNames.has(className);
48+
for (const name of this.classNames) {
49+
if (name.test(className)) {
50+
return true;
51+
}
52+
}
53+
return false;
4754
}
4855

4956
_makePublisherRequest(currentObject: any, originalObject: any, classLevelPermissions: ?any): any {

src/Controllers/UserController.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import AdaptableController from './AdaptableController';
44
import MailAdapter from '../Adapters/Email/MailAdapter';
55
import rest from '../rest';
66
import Parse from 'parse/node';
7+
import AccountLockout from '../AccountLockout';
78

89
var RestQuery = require('../RestQuery');
910
var Auth = require('../Auth');
@@ -258,7 +259,11 @@ export class UserController extends AdaptableController {
258259

259260
updatePassword(username, token, password) {
260261
return this.checkResetTokenValidity(username, token)
261-
.then(user => updateUserPassword(user.objectId, password, this.config))
262+
.then(user => updateUserPassword(user, password, this.config))
263+
.then(user => {
264+
const accountLockoutPolicy = new AccountLockout(user, this.config);
265+
return accountLockoutPolicy.unlockAccount();
266+
})
262267
.catch(error => {
263268
if (error && error.message) {
264269
// in case of Parse.Error, fail with the error message only
@@ -302,16 +307,16 @@ export class UserController extends AdaptableController {
302307
}
303308

304309
// Mark this private
305-
function updateUserPassword(userId, password, config) {
310+
function updateUserPassword(user, password, config) {
306311
return rest.update(
307312
config,
308313
Auth.master(config),
309314
'_User',
310-
{ objectId: userId },
315+
{ objectId: user.objectId },
311316
{
312317
password: password,
313318
}
314-
);
319+
).then(() => user);
315320
}
316321

317322
function buildEmailLink(destination, username, token, config) {

0 commit comments

Comments
 (0)