[web] Add User logs to Group Audit Logs view (#29480)

* Revert "Revert "[web] Add User logs to Group Audit Logs view (#29155)" (#29479)"

This reverts commit 40a1516ab9cec690d0487a0a870b9fab17598d60.

* Fix `managedUsersEnabled` flag in frontend

GitOrigin-RevId: ae3edf5bcbc01ec46bc18028e758d3364072c307
This commit is contained in:
Miguel Serrano
2025-11-04 12:50:31 +01:00
committed by Copybot
parent 78d7178c08
commit a4d9d5789a
5 changed files with 113 additions and 21 deletions

View File

@@ -98,6 +98,13 @@ const SubscriptionLocator = {
)
},
async getUniqueManagedSubscriptionMemberOf(userId) {
return await Subscription.findOne(
{ member_ids: userId, managedUsersEnabled: true },
{ _id: 1 }
)
},
async getGroupsWithEmailInvite(email) {
return await Subscription.find({ invited_emails: email }).exec()
},

View File

@@ -1,6 +1,8 @@
const OError = require('@overleaf/o-error')
const logger = require('@overleaf/logger')
const { UserAuditLogEntry } = require('../../models/UserAuditLogEntry')
const { callbackify } = require('util')
const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
function _canHaveNoIpAddressId(operation, info) {
if (operation === 'join-group-subscription') return true
@@ -27,6 +29,9 @@ function _canHaveNoInitiatorId(operation, info) {
if (operation === 'release-managed-user' && info.script) return true
}
// events that are visible to managed user admins in Group Audit Logs view
const MANAGED_GROUP_USER_EVENTS = ['login', 'reset-password', 'update-password']
/**
* Add an audit log entry
*
@@ -68,10 +73,25 @@ async function addEntry(userId, operation, initiatorId, ipAddress, info = {}) {
ipAddress,
}
if (MANAGED_GROUP_USER_EVENTS.includes(operation)) {
try {
const managedSubscription =
await SubscriptionLocator.promises.getUniqueManagedSubscriptionMemberOf(
userId
)
if (managedSubscription) {
entry.managedSubscriptionId = managedSubscription._id
}
} catch (err) {
logger.error({ err, userId }, 'failed to lookup managed subscription')
}
}
await UserAuditLogEntry.create(entry)
}
const UserAuditLogHandler = {
MANAGED_GROUP_USER_EVENTS,
addEntry: callbackify(addEntry),
promises: {
addEntry,

View File

@@ -4,6 +4,7 @@ const { Schema } = mongoose
const UserAuditLogEntrySchema = new Schema(
{
userId: { type: Schema.Types.ObjectId, index: true },
managedSubscriptionId: { type: Schema.Types.ObjectId, index: true },
info: { type: Object },
initiatorId: { type: Schema.Types.ObjectId },
ipAddress: { type: String },

View File

@@ -10,6 +10,7 @@ describe('UserAuditLogHandler', function () {
beforeEach(function () {
this.userId = new ObjectId()
this.initiatorId = new ObjectId()
this.subscriptionId = new ObjectId()
this.action = {
operation: 'clear-sessions',
initiatorId: this.initiatorId,
@@ -24,9 +25,16 @@ describe('UserAuditLogHandler', function () {
ip: '0:0:0:0',
}
this.UserAuditLogEntryMock = sinon.mock(UserAuditLogEntry)
this.getUniqueManagedSubscriptionMemberOfMock = sinon.stub().resolves()
this.UserAuditLogHandler = SandboxedModule.require(MODULE_PATH, {
requires: {
'../../models/UserAuditLogEntry': { UserAuditLogEntry },
'../Subscription/SubscriptionLocator': {
promises: {
getUniqueManagedSubscriptionMemberOf:
this.getUniqueManagedSubscriptionMemberOfMock,
},
},
},
})
})
@@ -53,37 +61,33 @@ describe('UserAuditLogHandler', function () {
this.UserAuditLogEntryMock.verify()
})
it('updates the log for password reset operation witout a initiatorId', async function () {
await expect(
this.UserAuditLogHandler.promises.addEntry(
this.userId,
'reset-password',
undefined,
this.action.ip,
this.action.info
)
it('updates the log for password reset operation without a initiatorId', async function () {
await this.UserAuditLogHandler.promises.addEntry(
this.userId,
'reset-password',
undefined,
this.action.ip,
this.action.info
)
this.UserAuditLogEntryMock.verify()
})
it('updates the log for a email removal via script', async function () {
await expect(
this.UserAuditLogHandler.promises.addEntry(
this.userId,
'remove-email',
undefined,
this.action.ip,
{
removedEmail: 'foo',
script: true,
}
)
await this.UserAuditLogHandler.promises.addEntry(
this.userId,
'remove-email',
undefined,
this.action.ip,
{
removedEmail: 'foo',
script: true,
}
)
this.UserAuditLogEntryMock.verify()
})
it('updates the log when no ip address or initiatorId is specified for a group join event', async function () {
this.UserAuditLogHandler.promises.addEntry(
await this.UserAuditLogHandler.promises.addEntry(
this.userId,
'join-group-subscription',
undefined,
@@ -92,6 +96,31 @@ describe('UserAuditLogHandler', function () {
subscriptionId: 'foo',
}
)
this.UserAuditLogEntryMock.verify()
})
it('includes managedSubscriptionId for managed group user events ', async function () {
await this.UserAuditLogHandler.promises.addEntry(
this.userId,
'reset-password',
undefined,
this.action.ip
)
this.UserAuditLogEntryMock.verify()
expect(this.getUniqueManagedSubscriptionMemberOfMock).to.have.been
.called
})
it('does not includes managedSubscriptionId for events not in the managed group event list', async function () {
await this.UserAuditLogHandler.promises.addEntry(
this.userId,
'foo',
this.action.initiatorId,
this.action.ip
)
this.UserAuditLogEntryMock.verify()
expect(this.getUniqueManagedSubscriptionMemberOfMock).not.to.have.been
.called
})
})

View File

@@ -0,0 +1,35 @@
/* eslint-disable no-unused-vars */
import Helpers from './lib/helpers.mjs'
const tags = ['saas']
const indexes = [
{
key: {
managedSubscriptionId: 1,
timestamp: 1,
},
name: 'managedSubscriptionId_1_timestamp_1',
},
]
const migrate = async client => {
const { db } = client
await Helpers.addIndexesToCollection(db.userAuditLogEntries, indexes)
}
const rollback = async client => {
const { db } = client
try {
await Helpers.dropIndexesFromCollection(db.userAuditLogEntries, indexes)
} catch (err) {
console.error('Something went wrong rolling back the migrations', err)
}
}
export default {
tags,
migrate,
rollback,
}