diff --git a/services/web/app/src/Features/Project/ProjectAuditLogHandler.js b/services/web/app/src/Features/Project/ProjectAuditLogHandler.js index da323cbc34..561cb51232 100644 --- a/services/web/app/src/Features/Project/ProjectAuditLogHandler.js +++ b/services/web/app/src/Features/Project/ProjectAuditLogHandler.js @@ -1,9 +1,11 @@ const { ProjectAuditLogEntry } = require('../../models/ProjectAuditLogEntry') +const { callbackify } = require('../../util/promises') module.exports = { promises: { addEntry, }, + addEntry: callbackify(addEntry), // callback version of adEntry } /** diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 410beebf9c..f204c40e69 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -45,6 +45,8 @@ const SubscriptionViewModelBuilder = require('../Subscription/SubscriptionViewMo const SurveyHandler = require('../Survey/SurveyHandler') const { expressify } = require('../../util/promises') const ProjectListController = require('./ProjectListController') +const ProjectAuditLogHandler = require('./ProjectAuditLogHandler') +const PublicAccessLevels = require('../Authorization/PublicAccessLevels') /** * @typedef {import("./types").GetProjectsRequest} GetProjectsRequest @@ -131,9 +133,21 @@ const ProjectController = { updateProjectAdminSettings(req, res, next) { const projectId = req.params.Project_id + const user = SessionManager.getSessionUser(req.session) + const publicAccessLevel = req.body.publicAccessLevel + const publicAccessLevels = [ + PublicAccessLevels.READ_ONLY, + PublicAccessLevels.READ_AND_WRITE, + PublicAccessLevels.PRIVATE, + PublicAccessLevels.TOKEN_BASED, + ] + + if ( + req.body.publicAccessLevel != null && + publicAccessLevels.includes(publicAccessLevel) + ) { + const jobs = [] - const jobs = [] - if (req.body.publicAccessLevel != null) { jobs.push(callback => EditorController.setPublicAccessLevel( projectId, @@ -141,14 +155,26 @@ const ProjectController = { callback ) ) - } - async.series(jobs, error => { - if (error != null) { - return next(error) - } - res.sendStatus(204) - }) + jobs.push(callback => + ProjectAuditLogHandler.addEntry( + projectId, + 'toggle-access-level', + user._id, + { publicAccessLevel: req.body.publicAccessLevel, status: 'OK' }, + callback + ) + ) + + async.series(jobs, error => { + if (error != null) { + return next(error) + } + res.sendStatus(204) + }) + } else { + res.sendStatus(500) + } }, deleteProject(req, res) { diff --git a/services/web/app/src/Features/Project/ProjectDetailsHandler.js b/services/web/app/src/Features/Project/ProjectDetailsHandler.js index 20d9c0401b..539bc6d742 100644 --- a/services/web/app/src/Features/Project/ProjectDetailsHandler.js +++ b/services/web/app/src/Features/Project/ProjectDetailsHandler.js @@ -200,6 +200,8 @@ async function setPublicAccessLevel(projectId, newAccessLevel) { { _id: projectId }, { publicAccesLevel: newAccessLevel } ).exec() + } else { + throw new Errors.InvalidError('unexpected access level') } } diff --git a/services/web/app/src/router.js b/services/web/app/src/router.js index 13918e9cce..1643fa7f38 100644 --- a/services/web/app/src/router.js +++ b/services/web/app/src/router.js @@ -64,6 +64,7 @@ const logger = require('@overleaf/logger') const _ = require('underscore') const { expressify } = require('./util/promises') const { plainTextResponse } = require('./infrastructure/Response') +const PublicAccessLevels = require('./Features/Authorization/PublicAccessLevels') module.exports = { initialize } @@ -401,7 +402,13 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) { ) webRouter.post( '/project/:Project_id/settings', - validate({ body: Joi.object() }), + validate({ + body: Joi.object({ + publicAccessLevel: Joi.string() + .valid(PublicAccessLevels.PRIVATE, PublicAccessLevels.TOKEN_BASED) + .optional(), + }), + }), AuthorizationMiddleware.ensureUserCanWriteProjectSettings, ProjectController.updateProjectSettings ) diff --git a/services/web/test/unit/src/Project/ProjectControllerTests.js b/services/web/test/unit/src/Project/ProjectControllerTests.js index b7d7b3d526..d5bc46abc0 100644 --- a/services/web/test/unit/src/Project/ProjectControllerTests.js +++ b/services/web/test/unit/src/Project/ProjectControllerTests.js @@ -144,6 +144,9 @@ describe('ProjectController', function () { this.SurveyHandler = { getSurvey: sinon.stub().yields(null, {}), } + this.ProjectAuditLogHandler = { + addEntry: sinon.stub().yields(), + } this.ProjectController = SandboxedModule.require(MODULE_PATH, { requires: { @@ -190,6 +193,7 @@ describe('ProjectController', function () { }, '../Institutions/InstitutionsFeatures': this.InstitutionsFeatures, '../Survey/SurveyHandler': this.SurveyHandler, + './ProjectAuditLogHandler': this.ProjectAuditLogHandler, }, }) @@ -293,17 +297,34 @@ describe('ProjectController', function () { it('should update the public access level', function (done) { this.EditorController.setPublicAccessLevel = sinon.stub().callsArg(2) this.req.body = { - publicAccessLevel: (this.publicAccessLevel = 'readonly'), + publicAccessLevel: 'readOnly', } this.res.sendStatus = code => { this.EditorController.setPublicAccessLevel - .calledWith(this.project_id, this.publicAccessLevel) + .calledWith(this.project_id, 'readOnly') .should.equal(true) code.should.equal(204) done() } this.ProjectController.updateProjectAdminSettings(this.req, this.res) }) + + it('should record the change in the project audit log', function (done) { + this.EditorController.setPublicAccessLevel = sinon.stub().callsArg(2) + this.req.body = { + publicAccessLevel: 'readOnly', + } + this.res.sendStatus = code => { + this.ProjectAuditLogHandler.addEntry + .calledWith(this.project_id, 'toggle-access-level', this.user._id, { + publicAccessLevel: 'readOnly', + status: 'OK', + }) + .should.equal(true) + done() + } + this.ProjectController.updateProjectAdminSettings(this.req, this.res) + }) }) describe('deleteProject', function () {