Просмотр исходного кода

Merge pull request #15095 from overleaf/em-invite-audit-logs-3

Write audit logs when a user joins a project via token

GitOrigin-RevId: 083fb7301ac2193c276a35bbf4dbf4f37d0ffa3b
Eric Mc Sween 2 лет назад
Родитель
Сommit
863a645dfb

+ 23 - 0
services/web/app/src/Features/TokenAccess/TokenAccessController.js

@@ -11,6 +11,7 @@ const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
 const {
   handleAdminDomainRedirect,
 } = require('../Authorization/AuthorizationMiddleware')
+const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
 
 const orderedPrivilegeLevels = [
   PrivilegeLevels.NONE,
@@ -252,6 +253,17 @@ async function grantTokenAccessReadAndWrite(req, res, next) {
         },
       })
     }
+
+    if (!project.tokenAccessReadAndWrite_refs.some(id => id.equals(userId))) {
+      await ProjectAuditLogHandler.promises.addEntry(
+        project._id,
+        'join-via-token',
+        userId,
+        req.ip,
+        { privileges: 'readAndWrite' }
+      )
+    }
+
     await TokenAccessHandler.promises.addReadAndWriteUserToProject(
       userId,
       project._id
@@ -306,6 +318,17 @@ async function grantTokenAccessReadOnly(req, res, next) {
         },
       })
     }
+
+    if (!project.tokenAccessReadOnly_refs.some(id => id.equals(userId))) {
+      await ProjectAuditLogHandler.promises.addEntry(
+        project._id,
+        'join-via-token',
+        userId,
+        req.ip,
+        { privileges: 'readOnly' }
+      )
+    }
+
     await TokenAccessHandler.promises.addReadOnlyUserToProject(
       userId,
       project._id

+ 2 - 0
services/web/app/src/Features/TokenAccess/TokenAccessHandler.js

@@ -93,6 +93,8 @@ const TokenAccessHandler = {
         publicAccesLevel: 1,
         owner_ref: 1,
         name: 1,
+        tokenAccessReadOnly_refs: 1,
+        tokenAccessReadAndWrite_refs: 1,
       },
       callback
     )

+ 180 - 0
services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js

@@ -0,0 +1,180 @@
+const SandboxedModule = require('sandboxed-module')
+const sinon = require('sinon')
+const { expect } = require('chai')
+const { ObjectId } = require('mongodb')
+const MockRequest = require('../helpers/MockRequest')
+const MockResponse = require('../helpers/MockResponse')
+const PrivilegeLevels = require('../../../../app/src/Features/Authorization/PrivilegeLevels')
+
+const MODULE_PATH =
+  '../../../../app/src/Features/TokenAccess/TokenAccessController'
+
+describe('TokenAccessController', function () {
+  beforeEach(function () {
+    this.user = { _id: ObjectId() }
+    this.project = {
+      _id: ObjectId(),
+      tokenAccessReadAndWrite_refs: [],
+      tokenAccessReadOnly_refs: [],
+    }
+    this.req = new MockRequest()
+    this.res = new MockResponse()
+
+    this.Settings = {}
+    this.TokenAccessHandler = {
+      TOKEN_TYPES: {
+        READ_ONLY: 'readOnly',
+        READ_AND_WRITE: 'readAndWrite',
+      },
+      isReadAndWriteToken: sinon.stub().returns(true),
+      isReadOnlyToken: sinon.stub().returns(true),
+      tokenAccessEnabledForProject: sinon.stub().returns(true),
+      promises: {
+        addReadAndWriteUserToProject: sinon.stub().resolves(),
+        addReadOnlyUserToProject: sinon.stub().resolves(),
+        getProjectByToken: sinon.stub().resolves(this.project),
+        getV1DocPublishedInfo: sinon.stub().resolves({ allow: true }),
+      },
+    }
+
+    this.SessionManager = {
+      getLoggedInUserId: sinon.stub().returns(this.user._id),
+    }
+
+    this.AuthenticationController = {}
+
+    this.AuthorizationManager = {
+      promises: {
+        getPrivilegeLevelForProject: sinon
+          .stub()
+          .resolves(PrivilegeLevels.NONE),
+      },
+    }
+
+    this.AuthorizationMiddleware = {}
+
+    this.ProjectAuditLogHandler = {
+      promises: {
+        addEntry: sinon.stub().resolves(),
+      },
+    }
+
+    this.TokenAccessController = SandboxedModule.require(MODULE_PATH, {
+      requires: {
+        '@overleaf/settings': this.Settings,
+        './TokenAccessHandler': this.TokenAccessHandler,
+        '../Authentication/AuthenticationController':
+          this.AuthenticationController,
+        '../Authentication/SessionManager': this.SessionManager,
+        '../Authorization/AuthorizationManager': this.AuthorizationManager,
+        '../Authorization/AuthorizationMiddleware':
+          this.AuthorizationMiddleware,
+        '../Project/ProjectAuditLogHandler': this.ProjectAuditLogHandler,
+      },
+    })
+  })
+
+  describe('grantTokenAccessReadAndWrite', function () {
+    describe('normal case', function () {
+      beforeEach(function (done) {
+        this.req.params = { token: this.token }
+        this.req.body = { confirmedByUser: true }
+        this.res.callback = done
+        this.TokenAccessController.grantTokenAccessReadAndWrite(
+          this.req,
+          this.res,
+          done
+        )
+      })
+
+      it('grants read and write access', function () {
+        expect(
+          this.TokenAccessHandler.promises.addReadAndWriteUserToProject
+        ).to.have.been.calledWith(this.user._id, this.project._id)
+      })
+
+      it('writes a project audit log', function () {
+        expect(
+          this.ProjectAuditLogHandler.promises.addEntry
+        ).to.have.been.calledWith(
+          this.project._id,
+          'join-via-token',
+          this.user._id,
+          this.req.ip,
+          { privileges: 'readAndWrite' }
+        )
+      })
+    })
+
+    describe('when the access was already granted', function () {
+      beforeEach(function (done) {
+        this.project.tokenAccessReadAndWrite_refs.push(this.user._id)
+        this.req.params = { token: this.token }
+        this.req.body = { confirmedByUser: true }
+        this.res.callback = done
+        this.TokenAccessController.grantTokenAccessReadAndWrite(
+          this.req,
+          this.res,
+          done
+        )
+      })
+
+      it("doesn't write a project audit log", function () {
+        expect(this.ProjectAuditLogHandler.promises.addEntry).to.not.have.been
+          .called
+      })
+    })
+  })
+
+  describe('grantTokenAccessReadOnly', function () {
+    describe('normal case', function () {
+      beforeEach(function (done) {
+        this.req.params = { token: this.token }
+        this.req.body = { confirmedByUser: true }
+        this.res.callback = done
+        this.TokenAccessController.grantTokenAccessReadOnly(
+          this.req,
+          this.res,
+          done
+        )
+      })
+
+      it('grants read-only access', function () {
+        expect(
+          this.TokenAccessHandler.promises.addReadOnlyUserToProject
+        ).to.have.been.calledWith(this.user._id, this.project._id)
+      })
+
+      it('writes a project audit log', function () {
+        expect(
+          this.ProjectAuditLogHandler.promises.addEntry
+        ).to.have.been.calledWith(
+          this.project._id,
+          'join-via-token',
+          this.user._id,
+          this.req.ip,
+          { privileges: 'readOnly' }
+        )
+      })
+    })
+
+    describe('when the access was already granted', function () {
+      beforeEach(function (done) {
+        this.project.tokenAccessReadOnly_refs.push(this.user._id)
+        this.req.params = { token: this.token }
+        this.req.body = { confirmedByUser: true }
+        this.res.callback = done
+        this.TokenAccessController.grantTokenAccessReadOnly(
+          this.req,
+          this.res,
+          done
+        )
+      })
+
+      it("doesn't write a project audit log", function () {
+        expect(this.ProjectAuditLogHandler.promises.addEntry).to.not.have.been
+          .called
+      })
+    })
+  })
+})