mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #9617 from overleaf/msm-audit-log-collections
Move project/user audit logs to their own collections GitOrigin-RevId: f6f89b3e2815c0fe5691a79eceb35b77b3c370d8
This commit is contained in:
parent
d9850487f8
commit
74f44e655a
19 changed files with 298 additions and 156 deletions
|
@ -1,7 +1,4 @@
|
|||
const OError = require('@overleaf/o-error')
|
||||
const { Project } = require('../../models/Project')
|
||||
|
||||
const MAX_AUDIT_LOG_ENTRIES = 200
|
||||
const { ProjectAuditLogEntry } = require('../../models/ProjectAuditLogEntry')
|
||||
|
||||
module.exports = {
|
||||
promises: {
|
||||
|
@ -19,22 +16,11 @@ module.exports = {
|
|||
* - message: a string detailing what happened
|
||||
*/
|
||||
async function addEntry(projectId, operation, initiatorId, info = {}) {
|
||||
const timestamp = new Date()
|
||||
const entry = {
|
||||
projectId,
|
||||
operation,
|
||||
initiatorId,
|
||||
timestamp,
|
||||
info,
|
||||
}
|
||||
const result = await Project.updateOne(
|
||||
{ _id: projectId },
|
||||
{
|
||||
$push: {
|
||||
auditLog: { $each: [entry], $slice: -MAX_AUDIT_LOG_ENTRIES },
|
||||
},
|
||||
}
|
||||
).exec()
|
||||
if (result.nModified === 0) {
|
||||
throw new OError('project not found', { projectId })
|
||||
}
|
||||
await ProjectAuditLogEntry.create(entry)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ const { db, ObjectId } = require('../../infrastructure/mongodb')
|
|||
const { callbackify } = require('util')
|
||||
const { Project } = require('../../models/Project')
|
||||
const { DeletedProject } = require('../../models/DeletedProject')
|
||||
const { ProjectAuditLogEntry } = require('../../models/ProjectAuditLogEntry')
|
||||
const Errors = require('../Errors/Errors')
|
||||
const logger = require('@overleaf/logger')
|
||||
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||
|
@ -349,6 +350,7 @@ async function expireDeletedProject(projectId) {
|
|||
await DeletedProject.deleteOne({
|
||||
'deleterData.deletedProjectId': projectId,
|
||||
})
|
||||
await ProjectAuditLogEntry.deleteMany({ projectId })
|
||||
return
|
||||
}
|
||||
const deletedProject = await DeletedProject.findOne({
|
||||
|
@ -386,6 +388,7 @@ async function expireDeletedProject(projectId) {
|
|||
}),
|
||||
ChatApiHandler.promises.destroyProject(deletedProject.project._id),
|
||||
hardDeleteDeletedFiles(deletedProject.project._id),
|
||||
ProjectAuditLogEntry.deleteMany({ projectId }),
|
||||
])
|
||||
|
||||
await DeletedProject.updateOne(
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
const OError = require('@overleaf/o-error')
|
||||
const { User } = require('../../models/User')
|
||||
const { UserAuditLogEntry } = require('../../models/UserAuditLogEntry')
|
||||
const { callbackify } = require('util')
|
||||
|
||||
const MAX_AUDIT_LOG_ENTRIES = 200
|
||||
|
||||
function _canHaveNoInitiatorId(operation, info) {
|
||||
if (operation === 'reset-password') return true
|
||||
if (operation === 'unlink-sso' && info.providerId === 'collabratec')
|
||||
|
@ -38,25 +36,15 @@ async function addEntry(userId, operation, initiatorId, ipAddress, info = {}) {
|
|||
})
|
||||
}
|
||||
|
||||
const timestamp = new Date()
|
||||
const entry = {
|
||||
userId,
|
||||
operation,
|
||||
initiatorId,
|
||||
info,
|
||||
ipAddress,
|
||||
timestamp,
|
||||
}
|
||||
const result = await User.updateOne(
|
||||
{ _id: userId },
|
||||
{
|
||||
$push: {
|
||||
auditLog: { $each: [entry], $slice: -MAX_AUDIT_LOG_ENTRIES },
|
||||
},
|
||||
}
|
||||
).exec()
|
||||
if (result.nModified === 0) {
|
||||
throw new OError('user not found', { userId })
|
||||
}
|
||||
|
||||
await UserAuditLogEntry.create(entry)
|
||||
}
|
||||
|
||||
const UserAuditLogHandler = {
|
||||
|
|
|
@ -3,6 +3,7 @@ const logger = require('@overleaf/logger')
|
|||
const moment = require('moment')
|
||||
const { User } = require('../../models/User')
|
||||
const { DeletedUser } = require('../../models/DeletedUser')
|
||||
const { UserAuditLogEntry } = require('../../models/UserAuditLogEntry')
|
||||
const NewsletterManager = require('../Newsletter/NewsletterManager')
|
||||
const ProjectDeleter = require('../Project/ProjectDeleter')
|
||||
const SubscriptionHandler = require('../Subscription/SubscriptionHandler')
|
||||
|
@ -87,7 +88,9 @@ async function expireDeletedUsersAfterDuration() {
|
|||
}
|
||||
|
||||
for (let i = 0; i < deletedUsers.length; i++) {
|
||||
await expireDeletedUser(deletedUsers[i].deleterData.deletedUserId)
|
||||
const deletedUserId = deletedUsers[i].deleterData.deletedUserId
|
||||
await expireDeletedUser(deletedUserId)
|
||||
await UserAuditLogEntry.deleteMany({ userId: deletedUserId }).exec()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ async function setupDb() {
|
|||
db.oauthAccessTokens = internalDb.collection('oauthAccessTokens')
|
||||
db.oauthApplications = internalDb.collection('oauthApplications')
|
||||
db.oauthAuthorizationCodes = internalDb.collection('oauthAuthorizationCodes')
|
||||
db.projectAuditLogEntries = internalDb.collection('projectAuditLogEntries')
|
||||
db.projectHistoryFailures = internalDb.collection('projectHistoryFailures')
|
||||
db.projectHistoryLabels = internalDb.collection('projectHistoryLabels')
|
||||
db.projectHistoryMetaData = internalDb.collection('projectHistoryMetaData')
|
||||
|
@ -78,6 +79,7 @@ async function setupDb() {
|
|||
db.teamInvites = internalDb.collection('teamInvites')
|
||||
db.templates = internalDb.collection('templates')
|
||||
db.tokens = internalDb.collection('tokens')
|
||||
db.userAuditLogEntries = internalDb.collection('userAuditLogEntries')
|
||||
db.users = internalDb.collection('users')
|
||||
db.userstubs = internalDb.collection('userstubs')
|
||||
}
|
||||
|
|
21
services/web/app/src/models/ProjectAuditLogEntry.js
Normal file
21
services/web/app/src/models/ProjectAuditLogEntry.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
const mongoose = require('../infrastructure/Mongoose')
|
||||
const { Schema } = mongoose
|
||||
|
||||
const ProjectAuditLogEntrySchema = new Schema(
|
||||
{
|
||||
projectId: { type: Schema.Types.ObjectId, index: true },
|
||||
operation: { type: String },
|
||||
initiatorId: { type: Schema.Types.ObjectId },
|
||||
timestamp: { type: Date, default: Date.now },
|
||||
info: { type: Object },
|
||||
},
|
||||
{
|
||||
collection: 'projectAuditLogEntries',
|
||||
}
|
||||
)
|
||||
|
||||
exports.ProjectAuditLogEntry = mongoose.model(
|
||||
'ProjectAuditLogEntry',
|
||||
ProjectAuditLogEntrySchema
|
||||
)
|
||||
exports.ProjectAuditLogEntrySchema = ProjectAuditLogEntrySchema
|
22
services/web/app/src/models/UserAuditLogEntry.js
Normal file
22
services/web/app/src/models/UserAuditLogEntry.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
const mongoose = require('../infrastructure/Mongoose')
|
||||
const { Schema } = mongoose
|
||||
|
||||
const UserAuditLogEntrySchema = new Schema(
|
||||
{
|
||||
userId: { type: Schema.Types.ObjectId, index: true },
|
||||
info: { type: Object },
|
||||
initiatorId: { type: Schema.Types.ObjectId },
|
||||
ipAddress: { type: String },
|
||||
operation: { type: String },
|
||||
timestamp: { type: Date, default: Date.now },
|
||||
},
|
||||
{
|
||||
collection: 'userAuditLogEntries',
|
||||
}
|
||||
)
|
||||
|
||||
exports.UserAuditLogEntry = mongoose.model(
|
||||
'UserAuditLogEntry',
|
||||
UserAuditLogEntrySchema
|
||||
)
|
||||
exports.UserAuditLogEntrySchema = UserAuditLogEntrySchema
|
|
@ -0,0 +1,28 @@
|
|||
const Helpers = require('./lib/helpers')
|
||||
|
||||
exports.tags = ['server-ce', 'server-pro', 'saas']
|
||||
|
||||
const indexes = [
|
||||
{
|
||||
// expire after 2.5 years
|
||||
expireAfterSeconds: 60 * 60 * 24 * 365 * 2.5,
|
||||
key: {
|
||||
timestamp: 1,
|
||||
},
|
||||
name: 'timestamp_1',
|
||||
},
|
||||
]
|
||||
|
||||
exports.migrate = async client => {
|
||||
const { db } = client
|
||||
await Helpers.addIndexesToCollection(db.projectAuditLogEntries, indexes)
|
||||
await Helpers.addIndexesToCollection(db.userAuditLogEntries, indexes)
|
||||
}
|
||||
|
||||
exports.rollback = async client => {
|
||||
const { db } = client
|
||||
await Promise.all([
|
||||
Helpers.dropIndexesFromCollection(db.userAuditLogEntries, indexes),
|
||||
Helpers.dropIndexesFromCollection(db.projectAuditLogEntries, indexes),
|
||||
])
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
const runScript = require('../scripts/migrate_audit_logs.js')
|
||||
|
||||
exports.tags = ['server-ce', 'server-pro']
|
||||
|
||||
exports.migrate = async () => {
|
||||
const options = {
|
||||
writeConcurrency: 10,
|
||||
dryRun: false,
|
||||
}
|
||||
await runScript(options)
|
||||
}
|
||||
|
||||
exports.rollback = async () => {}
|
123
services/web/scripts/migrate_audit_logs.js
Normal file
123
services/web/scripts/migrate_audit_logs.js
Normal file
|
@ -0,0 +1,123 @@
|
|||
const { batchedUpdate } = require('./helpers/batchedUpdate')
|
||||
const { promiseMapWithLimit, promisify } = require('../app/src/util/promises')
|
||||
const { db, ObjectId, waitForDb } = require('../app/src/infrastructure/mongodb')
|
||||
const sleep = promisify(setTimeout)
|
||||
const _ = require('lodash')
|
||||
|
||||
async function main(options) {
|
||||
if (!options) {
|
||||
options = {}
|
||||
}
|
||||
_.defaults(options, {
|
||||
dryRun: process.env.DRY_RUN !== 'false',
|
||||
projectId: process.env.PROJECT_ID,
|
||||
userId: process.env.USER_ID,
|
||||
writeConcurrency: parseInt(process.env.WRITE_CONCURRENCY, 10) || 10,
|
||||
letUserDoubleCheckInputsFor: parseInt(
|
||||
process.env.LET_USER_DOUBLE_CHECK_INPUTS_FOR || 10 * 1000,
|
||||
10
|
||||
),
|
||||
})
|
||||
|
||||
await letUserDoubleCheckInputs(options)
|
||||
|
||||
if (options.projectId) {
|
||||
console.log('migrating projectId=' + options.projectId)
|
||||
const project = await db.projects.findOne(
|
||||
{ _id: ObjectId(options.projectId) },
|
||||
{ _id: 1, auditLog: 1 }
|
||||
)
|
||||
await processProject(project, options)
|
||||
} else if (options.userId) {
|
||||
console.log('migrating userId=' + options.userId)
|
||||
const user = await db.users.findOne(
|
||||
{ _id: ObjectId(options.userId) },
|
||||
{ _id: 1, auditLog: 1 }
|
||||
)
|
||||
await processUser(user, options)
|
||||
} else {
|
||||
await batchedUpdate(
|
||||
'users',
|
||||
{ auditLog: { $exists: true } },
|
||||
async (_, users) => {
|
||||
await processUsersBatch(users, options)
|
||||
},
|
||||
{ _id: 1, auditLog: 1 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function processUsersBatch(users, options) {
|
||||
await promiseMapWithLimit(options.writeConcurrency, users, async user => {
|
||||
await processUser(user, options)
|
||||
})
|
||||
}
|
||||
|
||||
async function processUser(user, options) {
|
||||
const entries = user.auditLog.map(log => ({ ...log, userId: user._id }))
|
||||
if (!options.dryRun && entries?.length > 0) {
|
||||
await db.userAuditLogEntries.insertMany(entries)
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
await db.users.updateOne({ _id: user._id }, { $unset: { auditLog: 1 } })
|
||||
}
|
||||
|
||||
const projects = await db.projects.find(
|
||||
{ owner_ref: user._id, auditLog: { $exists: true } },
|
||||
{ _id: 1, auditLog: 1 }
|
||||
)
|
||||
projects.forEach(project => processProject(project, options))
|
||||
}
|
||||
|
||||
async function processProject(project, options) {
|
||||
const entries = project.auditLog.map(log => ({
|
||||
...log,
|
||||
projectId: project._id,
|
||||
}))
|
||||
|
||||
if (!options.dryRun && entries?.length > 0) {
|
||||
await db.projectAuditLogEntries.insertMany(entries)
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
await db.projects.updateOne(
|
||||
{ _id: project._id },
|
||||
{ $unset: { auditLog: 1 } }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function letUserDoubleCheckInputs(options) {
|
||||
const allOptions = {
|
||||
...options,
|
||||
// batchedUpdate() environment variables
|
||||
BATCH_DESCENDING: process.env.BATCH_DESCENDING,
|
||||
BATCH_SIZE: process.env.BATCH_SIZE,
|
||||
VERBOSE_LOGGING: process.env.VERBOSE_LOGGING,
|
||||
BATCH_LAST_ID: process.env.BATCH_LAST_ID,
|
||||
BATCH_RANGE_END: process.env.BATCH_RANGE_END,
|
||||
}
|
||||
console.error('Options:', JSON.stringify(allOptions, null, 2))
|
||||
console.error(
|
||||
'Waiting for you to double check inputs for',
|
||||
options.letUserDoubleCheckInputsFor,
|
||||
'ms'
|
||||
)
|
||||
await sleep(options.letUserDoubleCheckInputsFor)
|
||||
}
|
||||
|
||||
module.exports = main
|
||||
|
||||
if (require.main === module) {
|
||||
waitForDb()
|
||||
.then(main)
|
||||
.then(() => {
|
||||
console.log('Done.')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error({ error })
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
|
@ -59,18 +59,18 @@ describe('Authentication', function () {
|
|||
})
|
||||
|
||||
it('should emit an user auditLog entry for the login', async function () {
|
||||
const {
|
||||
auditLog: [auditLogEntry],
|
||||
} = await user.get()
|
||||
const auditLog = await user.getAuditLog()
|
||||
const auditLogEntry = auditLog[0]
|
||||
expect(auditLogEntry).to.exist
|
||||
expect(auditLogEntry.timestamp).to.exist
|
||||
delete auditLogEntry.timestamp
|
||||
expect(auditLogEntry).to.deep.equal({
|
||||
operation: 'login',
|
||||
ipAddress: '127.0.0.1',
|
||||
initiatorId: ObjectId(user.id),
|
||||
info: { method: 'Password login', captcha: 'solved' },
|
||||
expect(auditLogEntry.initiatorId).to.deep.equal(ObjectId(user.id))
|
||||
expect(auditLogEntry.userId).to.deep.equal(ObjectId(user.id))
|
||||
expect(auditLogEntry.operation).to.equal('login')
|
||||
expect(auditLogEntry.info).to.deep.equal({
|
||||
method: 'Password login',
|
||||
captcha: 'solved',
|
||||
})
|
||||
expect(auditLogEntry.ipAddress).to.equal('127.0.0.1')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -79,7 +79,8 @@ describe('Captcha', function () {
|
|||
const { response, body } = await loginWithCaptcha('valid')
|
||||
expectSuccessfulLogin(response, body)
|
||||
|
||||
expect((await user.get()).auditLog.pop().info).to.deep.equal({
|
||||
const auditLog = await user.getAuditLog()
|
||||
expect(auditLog[0].info).to.deep.equal({
|
||||
captcha: 'solved',
|
||||
method: 'Password login',
|
||||
})
|
||||
|
@ -104,7 +105,8 @@ describe('Captcha', function () {
|
|||
const { response, body } = await loginWithCaptcha('')
|
||||
expectSuccessfulLogin(response, body)
|
||||
|
||||
expect((await user.get()).auditLog.pop().info).to.deep.equal({
|
||||
const auditLog = await user.getAuditLog()
|
||||
expect(auditLog[1].info).to.deep.equal({
|
||||
captcha: 'skipped',
|
||||
method: 'Password login',
|
||||
})
|
||||
|
|
|
@ -44,14 +44,26 @@ class User {
|
|||
db.users.findOne({ _id: ObjectId(this._id) }, callback)
|
||||
}
|
||||
|
||||
getAuditLogWithoutNoise(callback) {
|
||||
getAuditLog(callback) {
|
||||
this.get((error, user) => {
|
||||
if (error) return callback(error)
|
||||
if (!user) return callback(new Error('User not found'))
|
||||
|
||||
db.userAuditLogEntries
|
||||
.find({ userId: ObjectId(this._id) })
|
||||
.toArray((error, auditLog) => {
|
||||
if (error) return callback(error)
|
||||
callback(null, auditLog || [])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getAuditLogWithoutNoise(callback) {
|
||||
this.getAuditLog((error, auditLog) => {
|
||||
if (error) return callback(error)
|
||||
callback(
|
||||
null,
|
||||
(user.auditLog || []).filter(entry => {
|
||||
auditLog.filter(entry => {
|
||||
return entry.operation !== 'login'
|
||||
})
|
||||
)
|
||||
|
|
|
@ -9,6 +9,9 @@ const moment = require('moment')
|
|||
const request = require('request-promise-native')
|
||||
const { db } = require('../../../../app/src/infrastructure/mongodb')
|
||||
const { ObjectId } = require('mongodb')
|
||||
const {
|
||||
UserAuditLogEntry,
|
||||
} = require('../../../../app/src/models/UserAuditLogEntry')
|
||||
|
||||
let globalUserNum = Settings.test.counterInit
|
||||
|
||||
|
@ -201,6 +204,12 @@ class UserHelper {
|
|||
throw new Error(`no user found for args: ${JSON.stringify([...args])}`)
|
||||
}
|
||||
|
||||
user.auditLog = await UserAuditLogEntry.find(
|
||||
{ userId: user._id },
|
||||
{},
|
||||
{ sort: { timestamp: 'asc' } }
|
||||
).exec()
|
||||
|
||||
return new UserHelper(user)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const { ObjectId } = require('mongodb')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const { Project } = require('../helpers/models/Project')
|
||||
|
||||
const MODULE_PATH =
|
||||
'../../../../app/src/Features/Project/ProjectAuditLogHandler'
|
||||
|
||||
describe('ProjectAuditLogHandler', function () {
|
||||
beforeEach(function () {
|
||||
this.projectId = ObjectId()
|
||||
this.userId = ObjectId()
|
||||
this.ProjectMock = sinon.mock(Project)
|
||||
this.ProjectAuditLogHandler = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'../../models/Project': { Project },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.ProjectMock.restore()
|
||||
})
|
||||
|
||||
describe('addEntry', function () {
|
||||
describe('success', function () {
|
||||
beforeEach(async function () {
|
||||
this.dbUpdate = this.ProjectMock.expects('updateOne').withArgs(
|
||||
{ _id: this.projectId },
|
||||
{
|
||||
$push: {
|
||||
auditLog: {
|
||||
$each: [
|
||||
{
|
||||
operation: 'translate',
|
||||
initiatorId: this.userId,
|
||||
info: { destinationLanguage: 'tagalog' },
|
||||
timestamp: sinon.match.typeOf('date'),
|
||||
},
|
||||
],
|
||||
$slice: -200,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
this.dbUpdate.chain('exec').resolves({ nModified: 1 })
|
||||
this.operationId = await this.ProjectAuditLogHandler.promises.addEntry(
|
||||
this.projectId,
|
||||
'translate',
|
||||
this.userId,
|
||||
{ destinationLanguage: 'tagalog' }
|
||||
)
|
||||
})
|
||||
|
||||
it('writes a log', async function () {
|
||||
this.ProjectMock.verify()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the project does not exist', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectMock.expects('updateOne')
|
||||
.chain('exec')
|
||||
.resolves({ nModified: 0 })
|
||||
})
|
||||
|
||||
it('throws an error', async function () {
|
||||
await expect(
|
||||
this.ProjectAuditLogHandler.promises.addEntry(
|
||||
this.projectId,
|
||||
'translate',
|
||||
this.userId,
|
||||
{ destinationLanguage: 'tagalog' }
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -140,7 +140,9 @@ describe('ProjectDeleter', function () {
|
|||
destroyProject: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
this.ProjectAuditLogEntry = {
|
||||
deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
|
||||
}
|
||||
this.ProjectDeleter = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'../../infrastructure/Features': this.Features,
|
||||
|
@ -160,6 +162,9 @@ describe('ProjectDeleter', function () {
|
|||
'./ProjectDetailsHandler': this.ProjectDetailsHandler,
|
||||
'../../infrastructure/mongodb': { db: this.db, ObjectId },
|
||||
'../History/HistoryManager': this.HistoryManager,
|
||||
'../../models/ProjectAuditLogEntry': {
|
||||
ProjectAuditLogEntry: this.ProjectAuditLogEntry,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -500,6 +505,12 @@ describe('ProjectDeleter', function () {
|
|||
this.ChatApiHandler.promises.destroyProject
|
||||
).to.have.been.calledWith(this.deletedProjects[0].project._id)
|
||||
})
|
||||
|
||||
it('should delete audit logs', async function () {
|
||||
expect(this.ProjectAuditLogEntry.deleteMany).to.have.been.calledWith({
|
||||
projectId: this.deletedProjects[0].project._id,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when history-v1 is not available', function () {
|
||||
|
|
|
@ -2,7 +2,7 @@ const sinon = require('sinon')
|
|||
const { expect } = require('chai')
|
||||
const { ObjectId } = require('mongodb')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const { User } = require('../helpers/models/User')
|
||||
const { UserAuditLogEntry } = require('../helpers/models/UserAuditLogEntry')
|
||||
|
||||
const MODULE_PATH = '../../../../app/src/Features/User/UserAuditLogHandler'
|
||||
|
||||
|
@ -23,22 +23,22 @@ describe('UserAuditLogHandler', function () {
|
|||
},
|
||||
ip: '0:0:0:0',
|
||||
}
|
||||
this.UserMock = sinon.mock(User)
|
||||
this.UserAuditLogEntryMock = sinon.mock(UserAuditLogEntry)
|
||||
this.UserAuditLogHandler = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'../../models/User': { User },
|
||||
'../../models/UserAuditLogEntry': { UserAuditLogEntry },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.UserMock.restore()
|
||||
this.UserAuditLogEntryMock.restore()
|
||||
})
|
||||
|
||||
describe('addEntry', function () {
|
||||
describe('success', function () {
|
||||
beforeEach(function () {
|
||||
this.dbUpdate = this.UserMock.expects('updateOne')
|
||||
this.dbUpdate = this.UserAuditLogEntryMock.expects('create')
|
||||
.chain('exec')
|
||||
.resolves({ nModified: 1 })
|
||||
})
|
||||
|
@ -50,7 +50,7 @@ describe('UserAuditLogHandler', function () {
|
|||
this.action.ip,
|
||||
this.action.info
|
||||
)
|
||||
this.UserMock.verify()
|
||||
this.UserAuditLogEntryMock.verify()
|
||||
})
|
||||
|
||||
it('updates the log for password reset operation witout a initiatorId', async function () {
|
||||
|
@ -63,7 +63,7 @@ describe('UserAuditLogHandler', function () {
|
|||
this.action.info
|
||||
)
|
||||
)
|
||||
this.UserMock.verify()
|
||||
this.UserAuditLogEntryMock.verify()
|
||||
})
|
||||
|
||||
it('updates the log for a email removal via script', async function () {
|
||||
|
@ -79,31 +79,11 @@ describe('UserAuditLogHandler', function () {
|
|||
}
|
||||
)
|
||||
)
|
||||
this.UserMock.verify()
|
||||
this.UserAuditLogEntryMock.verify()
|
||||
})
|
||||
})
|
||||
|
||||
describe('errors', function () {
|
||||
describe('when the user does not exist', function () {
|
||||
beforeEach(function () {
|
||||
this.UserMock.expects('updateOne')
|
||||
.chain('exec')
|
||||
.resolves({ nModified: 0 })
|
||||
})
|
||||
|
||||
it('throws an error', async function () {
|
||||
await expect(
|
||||
this.UserAuditLogHandler.promises.addEntry(
|
||||
this.userId,
|
||||
this.action.operation,
|
||||
this.action.initiatorId,
|
||||
this.action.ip,
|
||||
this.action.info
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('missing parameters', function () {
|
||||
it('throws an error when no operation', async function () {
|
||||
await expect(
|
||||
|
|
|
@ -84,6 +84,10 @@ describe('UserDeleter', function () {
|
|||
},
|
||||
}
|
||||
|
||||
this.UserAuditLogEntry = {
|
||||
deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
|
||||
}
|
||||
|
||||
this.UserDeleter = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'../../models/User': { User },
|
||||
|
@ -96,6 +100,9 @@ describe('UserDeleter', function () {
|
|||
'../UserMembership/UserMembershipsHandler': this.UserMembershipsHandler,
|
||||
'../Project/ProjectDeleter': this.ProjectDeleter,
|
||||
'../Institutions/InstitutionsAPI': this.InstitutionsApi,
|
||||
'../../models/UserAuditLogEntry': {
|
||||
UserAuditLogEntry: this.UserAuditLogEntry,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -420,6 +427,15 @@ describe('UserDeleter', function () {
|
|||
expect(deletedUser.save.called).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
it('deletes audit logs for all deleted users', async function () {
|
||||
await this.UserDeleter.promises.expireDeletedUsersAfterDuration()
|
||||
for (const deletedUser of this.deletedUsers) {
|
||||
expect(this.UserAuditLogEntry.deleteMany).to.have.been.calledWith({
|
||||
userId: deletedUser.deleterData.deletedUserId,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('expireDeletedUser', function () {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
const mockModel = require('../MockModel')
|
||||
|
||||
module.exports = mockModel('UserAuditLogEntry')
|
Loading…
Reference in a new issue