Added defaultFieldSelector and used it in Accounts callbacks. #10469

This commit is contained in:
Chris Morison
2019-12-15 08:46:04 +12:00
parent eba872966d
commit 21c1ca052d
3 changed files with 75 additions and 7 deletions

View File

@@ -132,6 +132,7 @@ export class AccountsCommon {
* @param {Number} options.passwordResetTokenExpirationInDays The number of days from when a link to reset password is sent until token expires and user can't reset password with the link anymore. Defaults to 3.
* @param {Number} options.passwordEnrollTokenExpirationInDays The number of days from when a link to set inital password is sent until token expires and user can't set password with the link anymore. Defaults to 30.
* @param {Boolean} options.ambiguousErrorMessages Return ambiguous error messages from login failures to prevent user enumeration. Defaults to false.
* @param {Object} options.defaultFieldSelector Default Mongo field selector, to exclude large custom fields from `Meteor.user()` & `Meteor.findUserBy...()` functions when called without a field selector and `onLogin`, `onLoginFailure` & `onLogout` callbacks. Example: `Accounts.config({ defaultFieldSelector: { myBigArray: 0 }})`.
*/
config(options) {
// We don't want users to accidentally only call Accounts.config on the
@@ -166,7 +167,8 @@ export class AccountsCommon {
// validate option keys
const VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation", "passwordEnrollTokenExpirationInDays",
"restrictCreationByEmailDomain", "loginExpirationInDays", "passwordResetTokenExpirationInDays",
"ambiguousErrorMessages", "bcryptRounds"];
"ambiguousErrorMessages", "bcryptRounds", "defaultFieldSelector"];
Object.keys(options).forEach(key => {
if (!VALID_KEYS.includes(key)) {
throw new Error(`Accounts.config: Invalid key: ${key}`);
@@ -321,4 +323,4 @@ export const EXPIRE_TOKENS_INTERVAL_MS = 600 * 1000; // 10 minutes
export const CONNECTION_CLOSE_DELAY_MS = 10 * 1000;
// A large number of expiration days (approximately 100 years worth) that is
// used when creating unexpiring tokens.
const LOGIN_UNEXPIRING_TOKEN_DAYS = 365 * 100;
const LOGIN_UNEXPIRING_TOKEN_DAYS = 365 * 100;

View File

@@ -188,7 +188,7 @@ export class AccountsServer extends AccountsCommon {
};
_successfulLogout(connection, userId) {
const user = userId && this.users.findOne(userId);
const user = userId && this.users.findOne(userId, {fields: this._options.defaultFieldSelector});
this._onLogoutHook.each(callback => {
callback({ user, connection });
return true;
@@ -317,7 +317,7 @@ export class AccountsServer extends AccountsCommon {
let user;
if (result.userId)
user = this.users.findOne(result.userId);
user = this.users.findOne(result.userId, {fields: this._options.defaultFieldSelector});
const attempt = {
type: result.type || "unknown",
@@ -398,7 +398,7 @@ export class AccountsServer extends AccountsCommon {
};
if (result.userId) {
attempt.user = this.users.findOne(result.userId);
attempt.user = this.users.findOne(result.userId, {fields: this._options.defaultFieldSelector});
}
this._validateLogin(methodInvocation.connection, attempt);
@@ -1576,4 +1576,4 @@ const setupUsersCollection = users => {
users._ensureIndex("services.resume.loginTokens.when", { sparse: 1 });
// For expiring password tokens
users._ensureIndex('services.password.reset.when', { sparse: 1 });
};
};

View File

@@ -57,6 +57,15 @@ Tinytest.add('accounts - config - default token lifetime', test => {
Accounts._options = options;
});
Tinytest.add('accounts - config - defaultFieldSelector', test => {
const options = Accounts._options;
Accounts._options = {};
const setValue = {bigArray: 0};
Accounts.config({defaultFieldSelector: setValue});
test.equal(Accounts._options.defaultFieldSelector, setValue);
Accounts._options = options;
});
const idsInValidateNewUser = {};
Accounts.validateNewUser(user => {
idsInValidateNewUser[user._id] = true;
@@ -451,6 +460,63 @@ Tinytest.add(
}
);
Tinytest.add(
'accounts - hook callbacks obey options.defaultFieldSelector',
test => {
const ignoreFieldName = "bigArray";
const userId = Accounts.insertUserDoc({}, { username: Random.id(), [ignoreFieldName]: [1] });
const stampedToken = Accounts._generateStampedLoginToken();
Accounts._insertLoginToken(userId, stampedToken);
const options = Accounts._options;
Accounts._options = {};
Accounts.config({defaultFieldSelector: {[ignoreFieldName]: 0}});
test.equal(Accounts._options.defaultFieldSelector, {[ignoreFieldName]: 0}, 'defaultFieldSelector');
const validateStopper = Accounts.validateLoginAttempt(attempt => {
test.isUndefined(allowLogin != 'bogus' ? attempt.user[ignoreFieldName] : attempt.user, "validateLoginAttempt")
return allowLogin;
});
const onLoginStopper = Accounts.onLogin(attempt =>
test.isUndefined(attempt.user[ignoreFieldName], "onLogin")
);
const onLogoutStopper = Accounts.onLogout(logoutContext =>
test.isUndefined(logoutContext.user[ignoreFieldName], "onLogout")
);
const onLoginFailureStopper = Accounts.onLoginFailure(attempt =>
test.isUndefined(allowLogin != 'bogus' ? attempt.user[ignoreFieldName] : attempt.user, "onLoginFailure")
);
const conn = DDP.connect(Meteor.absoluteUrl());
// test a new connection
let allowLogin = true;
conn.call('login', { resume: stampedToken.token });
// Now that the user is logged in on the connection, Meteor.userId() should
// return that user.
conn.call('login', { resume: stampedToken.token });
// Trigger onLoginFailure callbacks, this will not include the user object
allowLogin = 'bogus';
test.throws(() => conn.call('login', { resume: "bogus" }), '403');
// test a forced login fail which WILL include the user object
allowLogin = false;
test.throws(() => conn.call('login', { resume: stampedToken.token }), '403');
// Trigger onLogout callbacks
const onLogoutExpectedUserId = userId;
conn.call('logout');
Accounts._options = options;
conn.disconnect();
validateStopper.stop();
onLoginStopper.stop();
onLogoutStopper.stop();
onLoginFailureStopper.stop();
}
);
Tinytest.add(
'accounts - verify onExternalLogin hook can update oauth user profiles',
test => {
@@ -499,4 +565,4 @@ Tinytest.add(
Meteor.users.remove(uid2);
Accounts._onExternalLoginHook = null;
}
);
);