Files
overleaf-cep/services/web/test/unit/src/Authorization/AuthorizationManagerTests.js
T
Mathias Jakobsen b5e2604041 [web] Upgrade restricted user access if they are invited members (#9401)
* [web] Upgrade restricted user access if they are invited members

Previously, if a user joined a project via a read-only link and later on
joined the project via an invite, we would still treat them as
restricted users, disabling chat and commenting. This patch changes
that, so that we do *not* consider an invited user restricted.

GitOrigin-RevId: e2acdfd29cc0687cb7276310a9c96d697087b21a
2022-09-28 08:06:44 +00:00

626 lines
20 KiB
JavaScript

const sinon = require('sinon')
const { expect } = require('chai')
const modulePath =
'../../../../app/src/Features/Authorization/AuthorizationManager.js'
const SandboxedModule = require('sandboxed-module')
const Errors = require('../../../../app/src/Features/Errors/Errors')
const PrivilegeLevels = require('../../../../app/src/Features/Authorization/PrivilegeLevels')
const PublicAccessLevels = require('../../../../app/src/Features/Authorization/PublicAccessLevels')
const { ObjectId } = require('mongodb')
describe('AuthorizationManager', function () {
beforeEach(function () {
this.user = { _id: new ObjectId() }
this.project = { _id: new ObjectId() }
this.token = 'some-token'
this.ProjectGetter = {
promises: {
getProject: sinon.stub().resolves(null),
},
}
this.ProjectGetter.promises.getProject
.withArgs(this.project._id)
.resolves(this.project)
this.CollaboratorsGetter = {
promises: {
getMemberIdPrivilegeLevel: sinon.stub().resolves(PrivilegeLevels.NONE),
},
}
this.CollaboratorsHandler = {}
this.User = {
findOne: sinon.stub().returns({ exec: sinon.stub().resolves(null) }),
}
this.User.findOne
.withArgs({ _id: this.user._id })
.returns({ exec: sinon.stub().resolves(this.user) })
this.TokenAccessHandler = {
promises: {
validateTokenForAnonymousAccess: sinon
.stub()
.resolves({ isValidReadAndWrite: false, isValidReadOnly: false }),
},
}
this.AuthorizationManager = SandboxedModule.require(modulePath, {
requires: {
mongodb: { ObjectId },
'../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
'../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
'../Project/ProjectGetter': this.ProjectGetter,
'../../models/User': { User: this.User },
'../TokenAccess/TokenAccessHandler': this.TokenAccessHandler,
'@overleaf/settings': {
passwordStrengthOptions: {},
adminPrivilegeAvailable: true,
},
},
})
})
describe('isRestrictedUser', function () {
it('should produce the correct values', function () {
const notRestrictedScenarios = [
[null, 'readAndWrite', false, false],
['id', 'readAndWrite', true, false],
['id', 'readAndWrite', true, true],
['id', 'readOnly', false, false],
['id', 'readOnly', false, true],
]
const restrictedScenarios = [
[null, 'readOnly', false, false],
['id', 'readOnly', true, false],
[null, false, true, false],
[null, false, false, false],
['id', false, true, false],
['id', false, false, false],
]
for (const notRestrictedArgs of notRestrictedScenarios) {
expect(
this.AuthorizationManager.isRestrictedUser(...notRestrictedArgs)
).to.equal(false)
}
for (const restrictedArgs of restrictedScenarios) {
expect(
this.AuthorizationManager.isRestrictedUser(...restrictedArgs)
).to.equal(true)
}
})
})
describe('getPrivilegeLevelForProject', function () {
describe('with a token-based project', function () {
beforeEach(function () {
this.project.publicAccesLevel = 'tokenBased'
})
describe('with a user id with a privilege level', function () {
beforeEach(async function () {
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel
.withArgs(this.user._id, this.project._id)
.resolves(PrivilegeLevels.READ_ONLY)
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
this.user._id,
this.project._id,
this.token
)
})
it("should return the user's privilege level", function () {
expect(this.result).to.equal('readOnly')
})
})
describe('with a user id with no privilege level', function () {
beforeEach(async function () {
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
this.user._id,
this.project._id,
this.token
)
})
it('should return false', function () {
expect(this.result).to.equal(false)
})
})
describe('with a user id who is an admin', function () {
beforeEach(async function () {
this.user.isAdmin = true
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
this.user._id,
this.project._id,
this.token
)
})
it('should return the user as an owner', function () {
expect(this.result).to.equal('owner')
})
})
describe('with no user (anonymous)', function () {
describe('when the token is not valid', function () {
beforeEach(async function () {
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
null,
this.project._id,
this.token
)
})
it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function () {
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.called.should.equal(
false
)
})
it('should check if the token is valid', function () {
this.TokenAccessHandler.promises.validateTokenForAnonymousAccess.should.have.been.calledWith(
this.project._id,
this.token
)
})
it('should return false', function () {
expect(this.result).to.equal(false)
})
})
describe('when the token is valid for read-and-write', function () {
beforeEach(async function () {
this.TokenAccessHandler.promises.validateTokenForAnonymousAccess =
sinon
.stub()
.withArgs(this.project._id, this.token)
.resolves({ isValidReadAndWrite: true, isValidReadOnly: false })
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
null,
this.project._id,
this.token
)
})
it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function () {
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.called.should.equal(
false
)
})
it('should check if the token is valid', function () {
this.TokenAccessHandler.promises.validateTokenForAnonymousAccess.should.have.been.calledWith(
this.project._id,
this.token
)
})
it('should give read-write access', function () {
expect(this.result).to.equal('readAndWrite')
})
})
describe('when the token is valid for read-only', function () {
beforeEach(async function () {
this.TokenAccessHandler.promises.validateTokenForAnonymousAccess =
sinon
.stub()
.withArgs(this.project._id, this.token)
.resolves({ isValidReadAndWrite: false, isValidReadOnly: true })
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
null,
this.project._id,
this.token
)
})
it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function () {
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.called.should.equal(
false
)
})
it('should check if the token is valid', function () {
this.TokenAccessHandler.promises.validateTokenForAnonymousAccess.should.have.been.calledWith(
this.project._id,
this.token
)
})
it('should give read-only access', function () {
expect(this.result).to.equal('readOnly')
})
})
})
})
describe('with a private project', function () {
beforeEach(function () {
this.project.publicAccesLevel = 'private'
})
describe('with a user id with a privilege level', function () {
beforeEach(async function () {
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel
.withArgs(this.user._id, this.project._id)
.resolves(PrivilegeLevels.READ_ONLY)
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
this.user._id,
this.project._id,
this.token
)
})
it("should return the user's privilege level", function () {
expect(this.result).to.equal('readOnly')
})
})
describe('with a user id with no privilege level', function () {
beforeEach(async function () {
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
this.user._id,
this.project._id,
this.token
)
})
it('should return false', function () {
expect(this.result).to.equal(false)
})
})
describe('with a user id who is an admin', function () {
beforeEach(async function () {
this.user.isAdmin = true
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
this.user._id,
this.project._id,
this.token
)
})
it('should return the user as an owner', function () {
expect(this.result).to.equal('owner')
})
})
describe('with no user (anonymous)', function () {
beforeEach(async function () {
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
null,
this.project._id,
this.token
)
})
it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function () {
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.called.should.equal(
false
)
})
it('should return false', function () {
expect(this.result).to.equal(false)
})
})
})
describe('with a public project', function () {
beforeEach(function () {
this.project.publicAccesLevel = 'readAndWrite'
})
describe('with a user id with a privilege level', function () {
beforeEach(async function () {
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel
.withArgs(this.user._id, this.project._id)
.resolves(PrivilegeLevels.READ_ONLY)
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
this.user._id,
this.project._id,
this.token
)
})
it("should return the user's privilege level", function () {
expect(this.result).to.equal('readOnly')
})
})
describe('with a user id with no privilege level', function () {
beforeEach(async function () {
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
this.user._id,
this.project._id,
this.token
)
})
it('should return the public privilege level', function () {
expect(this.result).to.equal('readAndWrite')
})
})
describe('with a user id who is an admin', function () {
beforeEach(async function () {
this.user.isAdmin = true
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
this.user._id,
this.project._id,
this.token
)
})
it('should return the user as an owner', function () {
expect(this.result).to.equal('owner')
})
})
describe('with no user (anonymous)', function () {
beforeEach(async function () {
this.result =
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
null,
this.project._id,
this.token
)
})
it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function () {
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.called.should.equal(
false
)
})
it('should return the public privilege level', function () {
expect(this.result).to.equal('readAndWrite')
})
})
})
describe("when the project doesn't exist", function () {
it('should return a NotFoundError', async function () {
const someOtherId = new ObjectId()
await expect(
this.AuthorizationManager.promises.getPrivilegeLevelForProject(
this.user._id,
someOtherId,
this.token
)
).to.be.rejectedWith(Errors.NotFoundError)
})
})
describe('when the project id is not valid', function () {
beforeEach(function () {
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel
.withArgs(this.user._id, this.project._id)
.resolves(PrivilegeLevels.READ_ONLY)
})
it('should return a error', async function () {
await expect(
this.AuthorizationManager.promises.getPrivilegeLevelForProject(
undefined,
'not project id',
this.token
)
).to.be.rejected
})
})
})
testPermission('canUserReadProject', {
siteAdmin: true,
owner: true,
readAndWrite: true,
readOnly: true,
publicReadAndWrite: true,
publicReadOnly: true,
tokenReadAndWrite: true,
tokenReadOnly: true,
})
testPermission('canUserWriteProjectContent', {
siteAdmin: true,
owner: true,
readAndWrite: true,
publicReadAndWrite: true,
tokenReadAndWrite: true,
})
testPermission('canUserWriteProjectSettings', {
siteAdmin: true,
owner: true,
readAndWrite: true,
tokenReadAndWrite: true,
})
testPermission('canUserRenameProject', {
siteAdmin: true,
owner: true,
})
testPermission('canUserAdminProject', { siteAdmin: true, owner: true })
describe('isUserSiteAdmin', function () {
describe('when user is admin', function () {
beforeEach(function () {
this.user.isAdmin = true
})
it('should return true', async function () {
const isAdmin =
await this.AuthorizationManager.promises.isUserSiteAdmin(
this.user._id
)
expect(isAdmin).to.equal(true)
})
})
describe('when user is not admin', function () {
it('should return false', async function () {
const isAdmin =
await this.AuthorizationManager.promises.isUserSiteAdmin(
this.user._id
)
expect(isAdmin).to.equal(false)
})
})
describe('when user is not found', function () {
it('should return false', async function () {
const someOtherId = new ObjectId()
const isAdmin =
await this.AuthorizationManager.promises.isUserSiteAdmin(someOtherId)
expect(isAdmin).to.equal(false)
})
})
describe('when no user is passed', function () {
it('should return false', async function () {
const isAdmin =
await this.AuthorizationManager.promises.isUserSiteAdmin(null)
expect(isAdmin).to.equal(false)
})
})
})
})
function testPermission(permission, privilegeLevels) {
describe(permission, function () {
describe('when authenticated', function () {
describe('when user is site admin', function () {
beforeEach('set user as site admin', function () {
this.user.isAdmin = true
})
expectPermission(permission, privilegeLevels.siteAdmin || false)
})
describe('when user is owner', function () {
setupUserPrivilegeLevel(PrivilegeLevels.OWNER)
expectPermission(permission, privilegeLevels.owner || false)
})
describe('when user has read-write access', function () {
setupUserPrivilegeLevel(PrivilegeLevels.READ_AND_WRITE)
expectPermission(permission, privilegeLevels.readAndWrite || false)
})
describe('when user has read-only access', function () {
setupUserPrivilegeLevel(PrivilegeLevels.READ_ONLY)
expectPermission(permission, privilegeLevels.readOnly || false)
})
describe('when user has read-write access as the public', function () {
setupPublicAccessLevel(PublicAccessLevels.READ_AND_WRITE)
expectPermission(
permission,
privilegeLevels.publicReadAndWrite || false
)
})
describe('when user has read-only access as the public', function () {
setupPublicAccessLevel(PublicAccessLevels.READ_ONLY)
expectPermission(permission, privilegeLevels.publicReadOnly || false)
})
describe('when user is not found', function () {
it('should return false', async function () {
const otherUserId = new ObjectId()
const value = await this.AuthorizationManager.promises[permission](
otherUserId,
this.project._id,
this.token
)
expect(value).to.equal(false)
})
})
})
describe('when anonymous', function () {
beforeEach(function () {
this.user = null
})
describe('with read-write access through a token', function () {
setupTokenAccessLevel('readAndWrite')
expectPermission(permission, privilegeLevels.tokenReadAndWrite || false)
})
describe('with read-only access through a token', function () {
setupTokenAccessLevel('readOnly')
expectPermission(permission, privilegeLevels.tokenReadOnly || false)
})
describe('with public read-write access', function () {
setupPublicAccessLevel(PublicAccessLevels.READ_AND_WRITE)
expectPermission(
permission,
privilegeLevels.publicReadAndWrite || false
)
})
describe('with public read-only access', function () {
setupPublicAccessLevel(PublicAccessLevels.READ_ONLY)
expectPermission(permission, privilegeLevels.publicReadOnly || false)
})
})
})
}
function setupUserPrivilegeLevel(privilegeLevel) {
beforeEach(`set user privilege level to ${privilegeLevel}`, function () {
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel
.withArgs(this.user._id, this.project._id)
.resolves(privilegeLevel)
})
}
function setupPublicAccessLevel(level) {
beforeEach(`set public access level to ${level}`, function () {
this.project.publicAccesLevel = level
})
}
function setupTokenAccessLevel(level) {
beforeEach(`set token access level to ${level}`, function () {
this.project.publicAccesLevel = PublicAccessLevels.TOKEN_BASED
this.TokenAccessHandler.promises.validateTokenForAnonymousAccess
.withArgs(this.project._id, this.token)
.resolves({
isValidReadAndWrite: level === 'readAndWrite',
isValidReadOnly: level === 'readOnly',
})
})
}
function expectPermission(permission, expectedValue) {
it(`should return ${expectedValue}`, async function () {
const value = await this.AuthorizationManager.promises[permission](
this.user && this.user._id,
this.project._id,
this.token
)
expect(value).to.equal(expectedValue)
})
}