diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index d89ed40fa1..bd8ede56ca 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -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; \ No newline at end of file diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 00e5bac206..e12e54e2bd 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -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 }); -}; +}; \ No newline at end of file diff --git a/packages/accounts-base/accounts_tests.js b/packages/accounts-base/accounts_tests.js index fb4165a96c..7a079dca17 100644 --- a/packages/accounts-base/accounts_tests.js +++ b/packages/accounts-base/accounts_tests.js @@ -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; } -); +); \ No newline at end of file