From dcd520d7ebb4d78c2abf2fa4c82e895dd01d6a9e Mon Sep 17 00:00:00 2001 From: Jessica Lawshe <5312836+lawshe@users.noreply.github.com> Date: Tue, 27 May 2025 09:39:02 -0500 Subject: [PATCH] Merge pull request #25360 from overleaf/jel-group-audit-log-join [web] Update group audit log when user joins GitOrigin-RevId: 81c0d5003cdde384cb5ff90b57f6aa8b8dae0ee2 --- .../Subscription/SubscriptionUpdater.js | 34 ++++++++++++++--- .../Subscription/TeamInvitesController.mjs | 7 +++- .../Subscription/TeamInvitesHandler.js | 10 ++++- .../Subscription/SubscriptionUpdaterTests.js | 38 +++++++++++++++++++ 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js index 482d81ff41..7b57e32619 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js +++ b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js @@ -12,6 +12,8 @@ const Features = require('../../infrastructure/Features') const UserAuditLogHandler = require('../User/UserAuditLogHandler') const AccountMappingHelper = require('../Analytics/AccountMappingHelper') const { SSOConfig } = require('../../models/SSOConfig') +const mongoose = require('../../infrastructure/Mongoose') +const Modules = require('../../infrastructure/Modules') /** * @typedef {import('../../../../types/subscription/dashboard/subscription').Subscription} Subscription @@ -65,7 +67,9 @@ async function syncSubscription( ) } -async function addUserToGroup(subscriptionId, userId) { +async function addUserToGroup(subscriptionId, userId, auditLog) { + const session = await mongoose.startSession() + await UserAuditLogHandler.promises.addEntry( userId, 'join-group-subscription', @@ -73,10 +77,30 @@ async function addUserToGroup(subscriptionId, userId) { undefined, { subscriptionId } ) - await Subscription.updateOne( - { _id: subscriptionId }, - { $addToSet: { member_ids: userId } } - ).exec() + + try { + await session.withTransaction(async () => { + await Subscription.updateOne( + { _id: subscriptionId }, + { $addToSet: { member_ids: userId } }, + { session } + ).exec() + + await Modules.promises.hooks.fire( + 'addGroupAuditLogEntry', + { + initiatorId: auditLog?.initiatorId, + ipAddress: auditLog?.ipAddress, + groupId: subscriptionId, + operation: 'join-group', + }, + session + ) + }) + } finally { + await session.endSession() + } + await FeaturesUpdater.promises.refreshFeatures(userId, 'add-to-group') await _sendUserGroupPlanCodeUserProperty(userId) await _sendSubscriptionEvent( diff --git a/services/web/app/src/Features/Subscription/TeamInvitesController.mjs b/services/web/app/src/Features/Subscription/TeamInvitesController.mjs index 2da67c3010..b2c9840de4 100644 --- a/services/web/app/src/Features/Subscription/TeamInvitesController.mjs +++ b/services/web/app/src/Features/Subscription/TeamInvitesController.mjs @@ -36,10 +36,15 @@ async function createInvite(req, res, next) { } try { + const auditLog = { + initiatorId: teamManagerId, + ipAddress: req.ip, + } const invitedUserData = await TeamInvitesHandler.promises.createInvite( teamManagerId, subscription, - email + email, + auditLog ) return res.json({ user: invitedUserData }) } catch (err) { diff --git a/services/web/app/src/Features/Subscription/TeamInvitesHandler.js b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js index 7312266ddf..a89f0612f2 100644 --- a/services/web/app/src/Features/Subscription/TeamInvitesHandler.js +++ b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js @@ -70,7 +70,11 @@ async function acceptInvite(token, userId, auditLog) { throw new Errors.NotFoundError('invite not found') } - await SubscriptionUpdater.promises.addUserToGroup(subscription._id, userId) + await SubscriptionUpdater.promises.addUserToGroup( + subscription._id, + userId, + auditLog + ) if (subscription.managedUsersEnabled) { await Modules.promises.hooks.fire( @@ -147,9 +151,11 @@ async function _createInvite(subscription, email, inviter) { emailData => emailData.email === email ) if (isInvitingSelf) { + const auditLog = { initiatorId: inviter._id } await SubscriptionUpdater.promises.addUserToGroup( subscription._id, - inviter._id + inviter._id, + auditLog ) // legacy: remove any invite that might have been created in the past diff --git a/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js b/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js index 09644bc7b1..a122f0e4b2 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js @@ -120,6 +120,18 @@ describe('SubscriptionUpdater', function () { }, }, ], + mongo: { + options: { + appname: 'web', + maxPoolSize: 100, + serverSelectionTimeoutMS: 60000, + socketTimeoutMS: 60000, + monitorCommands: true, + family: 4, + }, + url: 'mongodb://mongo/test-overleaf', + hasSecondaries: false, + }, } this.UserFeaturesUpdater = { @@ -181,6 +193,13 @@ describe('SubscriptionUpdater', function () { }), '../../infrastructure/Features': this.Features, '../User/UserAuditLogHandler': this.UserAuditLogHandler, + '../../infrastructure/Modules': (this.Modules = { + promises: { + hooks: { + fire: sinon.stub().resolves(), + }, + }, + }), }, }) }) @@ -486,6 +505,7 @@ describe('SubscriptionUpdater', function () { this.SubscriptionModel.updateOne .calledWith(searchOps, insertOperation) .should.equal(true) + expect(this.SubscriptionModel.updateOne.lastCall.args[2].session).to.exist sinon.assert.calledWith( this.AnalyticsManager.recordEventForUserInBackground, this.otherUserId, @@ -571,6 +591,24 @@ describe('SubscriptionUpdater', function () { } ) }) + + it('should add an entry to the group audit log when joining a group', async function () { + await this.SubscriptionUpdater.promises.addUserToGroup( + this.subscription._id, + this.otherUserId, + { ipAddress: '0:0:0:0', initiatorId: 'user123' } + ) + + expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + 'addGroupAuditLogEntry', + { + groupId: this.subscription._id, + initiatorId: 'user123', + ipAddress: '0:0:0:0', + operation: 'join-group', + } + ) + }) }) describe('removeUserFromGroup', function () {