mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 06:33:55 +01:00
Merge pull request #35312 from overleaf/ii-anonymous-project-access
[web] Restore anonymous read-only access for "Anyone with the link" sharing GitOrigin-RevId: b17e30259f73d43f0febffeea8908eb47c98b7ee
This commit is contained in:
@@ -3,6 +3,7 @@ import mongodb from 'mongodb-legacy'
|
|||||||
import Features from '../../infrastructure/Features.mjs'
|
import Features from '../../infrastructure/Features.mjs'
|
||||||
import CollaboratorsGetter from '../Collaborators/CollaboratorsGetter.mjs'
|
import CollaboratorsGetter from '../Collaborators/CollaboratorsGetter.mjs'
|
||||||
import CollaboratorsHandler from '../Collaborators/CollaboratorsHandler.mjs'
|
import CollaboratorsHandler from '../Collaborators/CollaboratorsHandler.mjs'
|
||||||
|
import CollaboratorsInviteGetter from '../Collaborators/CollaboratorsInviteGetter.mjs'
|
||||||
import ProjectGetter from '../Project/ProjectGetter.mjs'
|
import ProjectGetter from '../Project/ProjectGetter.mjs'
|
||||||
import { User } from '../../models/User.mjs'
|
import { User } from '../../models/User.mjs'
|
||||||
import PrivilegeLevels from './PrivilegeLevels.mjs'
|
import PrivilegeLevels from './PrivilegeLevels.mjs'
|
||||||
@@ -232,10 +233,23 @@ async function _getPrivilegeLevelForProjectWithoutUserWithPublicAccessLevel(
|
|||||||
return await getPrivilegeLevelForProjectWithToken(projectId, token)
|
return await getPrivilegeLevelForProjectWithToken(projectId, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!opts.ignorePublicAccess && token) {
|
||||||
|
return await getAnonymousSharingLinkPrivilegeLevel(projectId, token)
|
||||||
|
}
|
||||||
|
|
||||||
// Deny anonymous user access
|
// Deny anonymous user access
|
||||||
return PrivilegeLevels.NONE
|
return PrivilegeLevels.NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getAnonymousSharingLinkPrivilegeLevel(projectId, token) {
|
||||||
|
const invite =
|
||||||
|
await CollaboratorsInviteGetter.promises.getUsableAnonymousSharingLinkInvite(
|
||||||
|
projectId,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
return invite ? PrivilegeLevels.READ_ONLY : PrivilegeLevels.NONE
|
||||||
|
}
|
||||||
|
|
||||||
async function getPrivilegeLevelForProjectWithToken(projectId, token) {
|
async function getPrivilegeLevelForProjectWithToken(projectId, token) {
|
||||||
// Anonymous users can have read-only access to token-based projects,
|
// Anonymous users can have read-only access to token-based projects,
|
||||||
// while read-write access must be logged in,
|
// while read-write access must be logged in,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import PrivilegeLevels, {
|
|||||||
import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
|
import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
|
||||||
import SubscriptionGroupHandler from '../Subscription/SubscriptionGroupHandler.mjs'
|
import SubscriptionGroupHandler from '../Subscription/SubscriptionGroupHandler.mjs'
|
||||||
import SubscriptionLocator from '../Subscription/SubscriptionLocator.mjs'
|
import SubscriptionLocator from '../Subscription/SubscriptionLocator.mjs'
|
||||||
|
import TokenAccessHandler from '../TokenAccess/TokenAccessHandler.mjs'
|
||||||
|
|
||||||
// This rate limiter allows a different number of requests depending on the
|
// This rate limiter allows a different number of requests depending on the
|
||||||
// number of callaborators a user is allowed. This is implemented by providing
|
// number of callaborators a user is allowed. This is implemented by providing
|
||||||
@@ -374,8 +375,17 @@ async function viewSharingLink(req, res) {
|
|||||||
|
|
||||||
const currentUser = SessionManager.getSessionUser(req.session)
|
const currentUser = SessionManager.getSessionUser(req.session)
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
AuthenticationController.setRedirectInSession(req)
|
const invite =
|
||||||
return res.redirect('/register')
|
await CollaboratorsInviteGetter.promises.getSharingLinkInvite(projectId)
|
||||||
|
const isPublicSharingLink =
|
||||||
|
invite != null &&
|
||||||
|
invite.privileges !== PrivilegeLevels.NONE &&
|
||||||
|
!invite.subscriptionId
|
||||||
|
|
||||||
|
if (!isPublicSharingLink) {
|
||||||
|
AuthenticationController.setRedirectInSession(req)
|
||||||
|
return res.redirect('/register')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup if set for register page
|
// cleanup if set for register page
|
||||||
@@ -645,7 +655,19 @@ async function validateSharingLink(req, res) {
|
|||||||
|
|
||||||
const currentUser = SessionManager.getSessionUser(req.session)
|
const currentUser = SessionManager.getSessionUser(req.session)
|
||||||
|
|
||||||
if (invite == null || !currentUser) {
|
if (invite == null) {
|
||||||
|
return res.json({ valid: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentUser) {
|
||||||
|
if (
|
||||||
|
invite.reusable &&
|
||||||
|
invite.privileges !== PrivilegeLevels.NONE &&
|
||||||
|
!invite.subscriptionId
|
||||||
|
) {
|
||||||
|
TokenAccessHandler.grantSessionTokenAccess(req, projectId, token)
|
||||||
|
return res.json({ valid: true, redirect: true })
|
||||||
|
}
|
||||||
return res.json({ valid: false })
|
return res.json({ valid: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,11 +58,23 @@ async function getSharingLinkInvite(projectId) {
|
|||||||
return invite
|
return invite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getUsableAnonymousSharingLinkInvite(projectId, tokenString) {
|
||||||
|
const invite = await ProjectInvite.findOne({
|
||||||
|
projectId,
|
||||||
|
reusable: true,
|
||||||
|
tokenHmac: CollaboratorsInviteHelper.hashInviteToken(tokenString),
|
||||||
|
subscriptionId: null,
|
||||||
|
privileges: { $ne: PrivilegeLevels.NONE },
|
||||||
|
}).exec()
|
||||||
|
return invite
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
promises: {
|
promises: {
|
||||||
getAllInvites,
|
getAllInvites,
|
||||||
getEditInviteCount,
|
getEditInviteCount,
|
||||||
getInviteByToken,
|
getInviteByToken,
|
||||||
getSharingLinkInvite,
|
getSharingLinkInvite,
|
||||||
|
getUsableAnonymousSharingLinkInvite,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ export default {
|
|||||||
'collaboration',
|
'collaboration',
|
||||||
'project-invite'
|
'project-invite'
|
||||||
),
|
),
|
||||||
AuthenticationController.requireLogin(),
|
|
||||||
RateLimiterMiddleware.rateLimit(rateLimiters.viewProjectInvite),
|
RateLimiterMiddleware.rateLimit(rateLimiters.viewProjectInvite),
|
||||||
CollaboratorsInviteController.viewSharingLink,
|
CollaboratorsInviteController.viewSharingLink,
|
||||||
AnalyticsRegistrationSourceMiddleware.clearSource()
|
AnalyticsRegistrationSourceMiddleware.clearSource()
|
||||||
@@ -179,7 +178,6 @@ export default {
|
|||||||
'collaboration',
|
'collaboration',
|
||||||
'project-invite'
|
'project-invite'
|
||||||
),
|
),
|
||||||
AuthenticationController.requireLogin(),
|
|
||||||
RateLimiterMiddleware.rateLimit(rateLimiters.validateSharingLink),
|
RateLimiterMiddleware.rateLimit(rateLimiters.validateSharingLink),
|
||||||
CollaboratorsInviteController.validateSharingLink,
|
CollaboratorsInviteController.validateSharingLink,
|
||||||
AnalyticsRegistrationSourceMiddleware.clearSource()
|
AnalyticsRegistrationSourceMiddleware.clearSource()
|
||||||
|
|||||||
@@ -1145,5 +1145,162 @@ describe('ProjectInviteTests', function () {
|
|||||||
done
|
done
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('anonymous (logged-out) access', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
this.anonymousUser = new User()
|
||||||
|
this.anonymousUser.getCsrfToken(done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('grants read-only access via a public sharing link, regardless of the link privilege', function (done) {
|
||||||
|
Async.series(
|
||||||
|
[
|
||||||
|
cb => {
|
||||||
|
// An "anyone with the link" editor link should still only grant
|
||||||
|
// anonymous users read-only access.
|
||||||
|
updateSharingLink(
|
||||||
|
this.sendingUser,
|
||||||
|
this.projectId,
|
||||||
|
'readAndWrite',
|
||||||
|
(err, link) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
this.sharingLink = link
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => expectNoProjectAccess(this.anonymousUser, this.projectId, cb),
|
||||||
|
cb => {
|
||||||
|
validateSharingLink(
|
||||||
|
this.anonymousUser,
|
||||||
|
this.projectId,
|
||||||
|
this.sharingLink.token,
|
||||||
|
(err, response, body) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
expect(body.valid).to.equal(true)
|
||||||
|
expect(body.redirect).to.equal(true)
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => expectProjectAccess(this.anonymousUser, this.projectId, cb),
|
||||||
|
],
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the invite page for a public sharing link', function (done) {
|
||||||
|
Async.series(
|
||||||
|
[
|
||||||
|
cb => {
|
||||||
|
updateSharingLink(
|
||||||
|
this.sendingUser,
|
||||||
|
this.projectId,
|
||||||
|
'readOnly',
|
||||||
|
(err, link) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
this.sharingLink = link
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb =>
|
||||||
|
expectInvitePage(
|
||||||
|
this.anonymousUser,
|
||||||
|
`${settings.siteUrl}/project/${this.projectId}/share`,
|
||||||
|
cb
|
||||||
|
),
|
||||||
|
],
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects to register when there is no public sharing link', function (done) {
|
||||||
|
tryFollowInviteLink(
|
||||||
|
this.anonymousUser,
|
||||||
|
`${settings.siteUrl}/project/${this.projectId}/share`,
|
||||||
|
(err, response) => {
|
||||||
|
if (err) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
expect(response.statusCode).to.equal(302)
|
||||||
|
expect(response.headers.location).to.match(/^\/register/)
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not grant access via a group-restricted sharing link', function (done) {
|
||||||
|
if (!Features.hasFeature('saas')) {
|
||||||
|
this.skip()
|
||||||
|
}
|
||||||
|
const subscription = new Subscription({
|
||||||
|
adminId: this.sendingUser._id,
|
||||||
|
memberIds: [this.sendingUser._id],
|
||||||
|
groupPlan: true,
|
||||||
|
planCode: 'group_professional',
|
||||||
|
paymentProvider: { state: 'active' },
|
||||||
|
})
|
||||||
|
Async.series(
|
||||||
|
[
|
||||||
|
cb => subscription.ensureExists(cb),
|
||||||
|
cb => {
|
||||||
|
updateSharingLink(
|
||||||
|
this.sendingUser,
|
||||||
|
this.projectId,
|
||||||
|
'readOnly',
|
||||||
|
{ subscriptionId: subscription._id.toString() },
|
||||||
|
(err, link) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
this.sharingLink = link
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
validateSharingLink(
|
||||||
|
this.anonymousUser,
|
||||||
|
this.projectId,
|
||||||
|
this.sharingLink.token,
|
||||||
|
(err, response, body) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
expect(body.valid).to.equal(false)
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => expectNoProjectAccess(this.anonymousUser, this.projectId, cb),
|
||||||
|
cb => {
|
||||||
|
// Group-restricted links send logged-out users to register.
|
||||||
|
tryFollowInviteLink(
|
||||||
|
this.anonymousUser,
|
||||||
|
`${settings.siteUrl}/project/${this.projectId}/share`,
|
||||||
|
(err, response) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
expect(response.statusCode).to.equal(302)
|
||||||
|
expect(response.headers.location).to.match(/^\/register/)
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ describe('AuthorizationManager', function () {
|
|||||||
|
|
||||||
ctx.CollaboratorsHandler = {}
|
ctx.CollaboratorsHandler = {}
|
||||||
|
|
||||||
|
ctx.CollaboratorsInviteGetter = {
|
||||||
|
promises: {
|
||||||
|
getUsableAnonymousSharingLinkInvite: sinon.stub().resolves(null),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
ctx.User = {
|
ctx.User = {
|
||||||
findOne: sinon.stub().returns({ exec: sinon.stub().resolves(null) }),
|
findOne: sinon.stub().returns({ exec: sinon.stub().resolves(null) }),
|
||||||
}
|
}
|
||||||
@@ -92,6 +98,13 @@ describe('AuthorizationManager', function () {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
vi.doMock(
|
||||||
|
'../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter',
|
||||||
|
() => ({
|
||||||
|
default: ctx.CollaboratorsInviteGetter,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
|
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
|
||||||
default: ctx.ProjectGetter,
|
default: ctx.ProjectGetter,
|
||||||
}))
|
}))
|
||||||
@@ -395,6 +408,50 @@ describe('AuthorizationManager', function () {
|
|||||||
expect(ctx.result).to.equal(false)
|
expect(ctx.result).to.equal(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('with no user (anonymous) and a public sharing link', function () {
|
||||||
|
beforeEach(async function (ctx) {
|
||||||
|
ctx.CollaboratorsInviteGetter.promises.getUsableAnonymousSharingLinkInvite
|
||||||
|
.withArgs(ctx.project._id, ctx.token)
|
||||||
|
.resolves({ _id: new ObjectId(), privileges: 'readAndWrite' })
|
||||||
|
ctx.result =
|
||||||
|
await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject(
|
||||||
|
null,
|
||||||
|
ctx.project._id,
|
||||||
|
ctx.token
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should look up the sharing-link invite by token', function (ctx) {
|
||||||
|
ctx.CollaboratorsInviteGetter.promises.getUsableAnonymousSharingLinkInvite.should.have.been.calledWith(
|
||||||
|
ctx.project._id,
|
||||||
|
ctx.token
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should give read-only access regardless of the link privilege', function (ctx) {
|
||||||
|
expect(ctx.result).to.equal('readOnly')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with no user (anonymous) and ignorePublicAccess', function () {
|
||||||
|
beforeEach(async function (ctx) {
|
||||||
|
ctx.CollaboratorsInviteGetter.promises.getUsableAnonymousSharingLinkInvite
|
||||||
|
.withArgs(ctx.project._id, ctx.token)
|
||||||
|
.resolves({ _id: new ObjectId(), privileges: 'readAndWrite' })
|
||||||
|
ctx.result =
|
||||||
|
await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject(
|
||||||
|
null,
|
||||||
|
ctx.project._id,
|
||||||
|
ctx.token,
|
||||||
|
{ ignorePublicAccess: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not grant sharing-link access', function (ctx) {
|
||||||
|
expect(ctx.result).to.equal(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with a public project', function () {
|
describe('with a public project', function () {
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ describe('CollaboratorsInviteController', function () {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.TokenAccessHandler = {
|
||||||
|
grantSessionTokenAccess: sinon.stub(),
|
||||||
|
}
|
||||||
|
|
||||||
ctx.SplitTestHandler = {
|
ctx.SplitTestHandler = {
|
||||||
promises: {
|
promises: {
|
||||||
getAssignment: sinon.stub().resolves({ variant: 'default' }),
|
getAssignment: sinon.stub().resolves({ variant: 'default' }),
|
||||||
@@ -270,6 +274,13 @@ describe('CollaboratorsInviteController', function () {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
vi.doMock(
|
||||||
|
'../../../../app/src/Features/TokenAccess/TokenAccessHandler.mjs',
|
||||||
|
() => ({
|
||||||
|
default: ctx.TokenAccessHandler,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
ctx.CollaboratorsInviteController = (await import(MODULE_PATH)).default
|
ctx.CollaboratorsInviteController = (await import(MODULE_PATH)).default
|
||||||
|
|
||||||
ctx.res = new MockResponse(vi)
|
ctx.res = new MockResponse(vi)
|
||||||
@@ -1978,6 +1989,62 @@ describe('CollaboratorsInviteController', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('viewSharingLink', function () {
|
||||||
|
beforeEach(function (ctx) {
|
||||||
|
ctx.req.params = { Project_id: ctx.projectId }
|
||||||
|
ctx.ProjectGetter.promises.getProject.resolves({ name: 'Project' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the invite page for a logged-in user', async function (ctx) {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
ctx.res.callback = () => resolve()
|
||||||
|
ctx.CollaboratorsInviteController.viewSharingLink(ctx.req, ctx.res)
|
||||||
|
})
|
||||||
|
expect(ctx.res.renderedTemplate).to.equal('project/invite/show')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('for a logged-out (anonymous) user', function () {
|
||||||
|
beforeEach(function (ctx) {
|
||||||
|
ctx.SessionManager.getSessionUser.returns(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the invite page when there is a public sharing link', async function (ctx) {
|
||||||
|
ctx.CollaboratorsInviteGetter.promises.getSharingLinkInvite.resolves({
|
||||||
|
privileges: 'readOnly',
|
||||||
|
subscriptionId: undefined,
|
||||||
|
})
|
||||||
|
await new Promise(resolve => {
|
||||||
|
ctx.res.callback = () => resolve()
|
||||||
|
ctx.CollaboratorsInviteController.viewSharingLink(ctx.req, ctx.res)
|
||||||
|
})
|
||||||
|
expect(ctx.res.renderedTemplate).to.equal('project/invite/show')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects to register for a group-restricted sharing link', async function (ctx) {
|
||||||
|
ctx.CollaboratorsInviteGetter.promises.getSharingLinkInvite.resolves({
|
||||||
|
privileges: 'readOnly',
|
||||||
|
subscriptionId: new ObjectId().toString(),
|
||||||
|
})
|
||||||
|
await new Promise(resolve => {
|
||||||
|
ctx.res.callback = () => resolve()
|
||||||
|
ctx.CollaboratorsInviteController.viewSharingLink(ctx.req, ctx.res)
|
||||||
|
})
|
||||||
|
expect(ctx.res.redirectedTo).to.equal('/register')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects to register when there is no sharing link', async function (ctx) {
|
||||||
|
ctx.CollaboratorsInviteGetter.promises.getSharingLinkInvite.resolves(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
await new Promise(resolve => {
|
||||||
|
ctx.res.callback = () => resolve()
|
||||||
|
ctx.CollaboratorsInviteController.viewSharingLink(ctx.req, ctx.res)
|
||||||
|
})
|
||||||
|
expect(ctx.res.redirectedTo).to.equal('/register')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('validateSharingLink', function () {
|
describe('validateSharingLink', function () {
|
||||||
beforeEach(function (ctx) {
|
beforeEach(function (ctx) {
|
||||||
ctx.req.params = { Project_id: ctx.projectId }
|
ctx.req.params = { Project_id: ctx.projectId }
|
||||||
@@ -2018,6 +2085,73 @@ describe('CollaboratorsInviteController', function () {
|
|||||||
})
|
})
|
||||||
expect(ctx.res.json).toHaveBeenCalledWith({ valid: false })
|
expect(ctx.res.json).toHaveBeenCalledWith({ valid: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('for a logged-out (anonymous) user', function () {
|
||||||
|
beforeEach(function (ctx) {
|
||||||
|
ctx.SessionManager.getSessionUser.returns(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('grants read-only access and redirects for a public sharing link', async function (ctx) {
|
||||||
|
ctx.invite.reusable = true
|
||||||
|
ctx.invite.privileges = 'readAndWrite'
|
||||||
|
ctx.invite.subscriptionId = undefined
|
||||||
|
await new Promise(resolve => {
|
||||||
|
ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(
|
||||||
|
ctx.invite
|
||||||
|
)
|
||||||
|
ctx.res.callback = () => resolve()
|
||||||
|
ctx.CollaboratorsInviteController.validateSharingLink(
|
||||||
|
ctx.req,
|
||||||
|
ctx.res
|
||||||
|
)
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
ctx.TokenAccessHandler.grantSessionTokenAccess
|
||||||
|
).to.have.been.calledWith(ctx.req, ctx.projectId, ctx.token)
|
||||||
|
expect(ctx.res.json).toHaveBeenCalledWith({
|
||||||
|
valid: true,
|
||||||
|
redirect: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns valid false for a group-restricted sharing link', async function (ctx) {
|
||||||
|
ctx.invite.reusable = true
|
||||||
|
ctx.invite.privileges = 'readOnly'
|
||||||
|
ctx.invite.subscriptionId = new ObjectId().toString()
|
||||||
|
await new Promise(resolve => {
|
||||||
|
ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(
|
||||||
|
ctx.invite
|
||||||
|
)
|
||||||
|
ctx.res.callback = () => resolve()
|
||||||
|
ctx.CollaboratorsInviteController.validateSharingLink(
|
||||||
|
ctx.req,
|
||||||
|
ctx.res
|
||||||
|
)
|
||||||
|
})
|
||||||
|
expect(ctx.TokenAccessHandler.grantSessionTokenAccess).to.not.have.been
|
||||||
|
.called
|
||||||
|
expect(ctx.res.json).toHaveBeenCalledWith({ valid: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns valid false for a one-time (non-reusable) invite', async function (ctx) {
|
||||||
|
ctx.invite.reusable = false
|
||||||
|
ctx.invite.privileges = 'readOnly'
|
||||||
|
ctx.invite.subscriptionId = undefined
|
||||||
|
await new Promise(resolve => {
|
||||||
|
ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(
|
||||||
|
ctx.invite
|
||||||
|
)
|
||||||
|
ctx.res.callback = () => resolve()
|
||||||
|
ctx.CollaboratorsInviteController.validateSharingLink(
|
||||||
|
ctx.req,
|
||||||
|
ctx.res
|
||||||
|
)
|
||||||
|
})
|
||||||
|
expect(ctx.TokenAccessHandler.grantSessionTokenAccess).to.not.have.been
|
||||||
|
.called
|
||||||
|
expect(ctx.res.json).toHaveBeenCalledWith({ valid: false })
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('_checkShouldInviteEmail', function () {
|
describe('_checkShouldInviteEmail', function () {
|
||||||
|
|||||||
@@ -247,4 +247,47 @@ describe('CollaboratorsInviteGetter', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('getUsableAnonymousSharingLinkInvite', function () {
|
||||||
|
beforeEach(function (ctx) {
|
||||||
|
ctx.ProjectInvite.findOne.returns({
|
||||||
|
exec: sinon.stub().resolves(ctx.fakeInvite),
|
||||||
|
})
|
||||||
|
ctx.call = async () => {
|
||||||
|
return await ctx.CollaboratorsInviteGetter.promises.getUsableAnonymousSharingLinkInvite(
|
||||||
|
ctx.projectId,
|
||||||
|
ctx.token
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the invite', async function (ctx) {
|
||||||
|
const invite = await ctx.call()
|
||||||
|
expect(invite).to.deep.equal(ctx.fakeInvite)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('queries for an active, non-group reusable invite matching the token hash', async function (ctx) {
|
||||||
|
await ctx.call()
|
||||||
|
ctx.ProjectInvite.findOne.should.have.been.calledWith({
|
||||||
|
projectId: ctx.projectId,
|
||||||
|
reusable: true,
|
||||||
|
tokenHmac: ctx.tokenHmac,
|
||||||
|
subscriptionId: null,
|
||||||
|
privileges: { $ne: false },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when no matching invite exists', function () {
|
||||||
|
beforeEach(function (ctx) {
|
||||||
|
ctx.ProjectInvite.findOne.returns({
|
||||||
|
exec: sinon.stub().resolves(null),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null', async function (ctx) {
|
||||||
|
const invite = await ctx.call()
|
||||||
|
expect(invite).to.be.null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user