mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
add option to disable link sharing (#27626)
* add option to remove link-sharing from backend * restrict make link-sharing in the frontend based on capability * extend e2e project-sharing tests to cover OVERLEAF_DISABLE_LINK_SHARING=true * throw an error when link sharing is disabled in TokenAccessHandler * throw errors when attempting to add users to projects with link sharing disabled * Update server-ce/test/project-sharing.spec.ts Co-authored-by: Jakob Ackermann <jakob.ackermann@overleaf.com> * add tests for existing access when link sharing is disabled * update tests to specify access restrictions for read-only and read-write link shared projects * [web] block access to legacy public project with link-sharing disabled --------- Co-authored-by: Jakob Ackermann <jakob.ackermann@overleaf.com> GitOrigin-RevId: 5f194dbcb790e973e427c58a3a4a738a5dd74cb4
This commit is contained in:
@@ -68,4 +68,44 @@ export function startWith({
|
||||
})
|
||||
}
|
||||
|
||||
// Allow reloading the server in other places, e.g. beforeEach hooks.
|
||||
export async function reloadWith({
|
||||
pro = false,
|
||||
version = 'latest',
|
||||
vars = {},
|
||||
varsFn = () => ({}),
|
||||
withDataDir = false,
|
||||
resetData = false,
|
||||
mongoVersion = '',
|
||||
}) {
|
||||
Object.assign(vars, varsFn())
|
||||
const cfg = JSON.stringify({
|
||||
pro,
|
||||
version,
|
||||
vars,
|
||||
withDataDir,
|
||||
resetData,
|
||||
mongoVersion,
|
||||
})
|
||||
if (resetData) {
|
||||
resetCreatedUsersCache()
|
||||
resetActivateUserRateLimit()
|
||||
// no return here, always reconfigure when resetting data
|
||||
} else if (previousConfigFrontend === cfg) {
|
||||
return
|
||||
}
|
||||
const { previousConfigServer } = await reconfigure({
|
||||
pro,
|
||||
version,
|
||||
vars,
|
||||
withDataDir,
|
||||
resetData,
|
||||
mongoVersion,
|
||||
})
|
||||
if (previousConfigServer !== cfg) {
|
||||
await Cypress.session.clearAllSavedSessions()
|
||||
}
|
||||
previousConfigFrontend = cfg
|
||||
}
|
||||
|
||||
export { reconfigure }
|
||||
|
||||
@@ -203,6 +203,7 @@ const allowedVars = Joi.object(
|
||||
'OVERLEAF_NEW_PROJECT_TEMPLATE_LINKS',
|
||||
'OVERLEAF_ALLOW_PUBLIC_ACCESS',
|
||||
'OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING',
|
||||
'OVERLEAF_DISABLE_LINK_SHARING',
|
||||
'EXTERNAL_AUTH',
|
||||
'OVERLEAF_SAML_ENTRYPOINT',
|
||||
'OVERLEAF_SAML_CALLBACK_URL',
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { isExcludedBySharding, startWith } from './helpers/config'
|
||||
import {
|
||||
isExcludedBySharding,
|
||||
startWith,
|
||||
reloadWith,
|
||||
STARTUP_TIMEOUT,
|
||||
} from './helpers/config'
|
||||
import { ensureUserExists, login } from './helpers/login'
|
||||
import {
|
||||
createProject,
|
||||
@@ -358,5 +363,116 @@ describe('Project Sharing', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with OVERLEAF_DISABLE_LINK_SHARING=true', () => {
|
||||
const email = 'collaborator-email@example.com'
|
||||
ensureUserExists({ email })
|
||||
|
||||
const invitedEmail = 'invited-email@example.com'
|
||||
ensureUserExists({ email: invitedEmail })
|
||||
|
||||
const retainedViewerEmail = 'collaborator-retained-viewer@example.com'
|
||||
ensureUserExists({ email: retainedViewerEmail })
|
||||
|
||||
const retainedEditorEmail = 'collaborator-retained-editor@example.com'
|
||||
ensureUserExists({ email: retainedEditorEmail })
|
||||
|
||||
// Link-sharing urls have to be created before disabling link sharing.
|
||||
// We use the `beforeEach` hook to reload the server with link sharing
|
||||
// disabled **after** the initial setup which happens in the `before`
|
||||
// block. The `before` hook always runs prior to the `beforeEach` hook.
|
||||
|
||||
// Set up retained access before disabling link sharing
|
||||
before(function () {
|
||||
// Set up retained viewer access
|
||||
login(retainedViewerEmail)
|
||||
openProjectViaLinkSharingAsUser(
|
||||
linkSharingReadOnly,
|
||||
projectName,
|
||||
retainedViewerEmail
|
||||
)
|
||||
|
||||
// Set up retained editor access
|
||||
login(retainedEditorEmail)
|
||||
openProjectViaLinkSharingAsUser(
|
||||
linkSharingReadAndWrite,
|
||||
projectName,
|
||||
retainedEditorEmail
|
||||
)
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
this.timeout(STARTUP_TIMEOUT) // Increase timeout for server reload
|
||||
|
||||
return cy.wrap(
|
||||
reloadWith({
|
||||
pro: true,
|
||||
vars: {
|
||||
OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
|
||||
OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
|
||||
OVERLEAF_DISABLE_LINK_SHARING: 'true',
|
||||
},
|
||||
withDataDir: true,
|
||||
}),
|
||||
{ timeout: STARTUP_TIMEOUT }
|
||||
)
|
||||
})
|
||||
|
||||
it('should not display link sharing in the sharing modal', () => {
|
||||
login('user@example.com')
|
||||
openProjectByName(projectName)
|
||||
cy.findByText('Share').click()
|
||||
cy.findByText('Turn on link sharing').should('not.exist')
|
||||
})
|
||||
|
||||
it('should block new access to read-only link shared projects', () => {
|
||||
login(email)
|
||||
|
||||
// Test read-only link returns 404
|
||||
cy.request({
|
||||
url: linkSharingReadOnly,
|
||||
failOnStatusCode: false,
|
||||
}).then(response => {
|
||||
expect(response.status).to.eq(404)
|
||||
})
|
||||
})
|
||||
|
||||
it('should block new access to read-write link shared projects', () => {
|
||||
login(email)
|
||||
|
||||
// Test read-write link returns 404
|
||||
cy.request({
|
||||
url: linkSharingReadAndWrite,
|
||||
failOnStatusCode: false,
|
||||
}).then(response => {
|
||||
expect(response.status).to.eq(404)
|
||||
})
|
||||
})
|
||||
|
||||
it('should continue to allow email sharing', () => {
|
||||
login('user@example.com')
|
||||
shareProjectByEmailAndAcceptInviteViaEmail(
|
||||
projectName,
|
||||
invitedEmail,
|
||||
'Viewer'
|
||||
)
|
||||
expectFullReadOnlyAccess()
|
||||
expectProjectDashboardEntry()
|
||||
})
|
||||
|
||||
it('should retain read-only access when project was joined via link before link sharing was turned off', () => {
|
||||
login(retainedViewerEmail)
|
||||
openProjectByName(projectName)
|
||||
expectRestrictedReadOnlyAccess()
|
||||
expectProjectDashboardEntry()
|
||||
})
|
||||
|
||||
it('should retain read-write access when project was joined via link before link sharing was turned off', () => {
|
||||
login(retainedEditorEmail)
|
||||
openProjectByName(projectName)
|
||||
expectFullReadAndWriteAccess()
|
||||
expectProjectDashboardEntry()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { callbackify } = require('util')
|
||||
const { ObjectId } = require('mongodb-legacy')
|
||||
const Features = require('../../infrastructure/Features')
|
||||
const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
|
||||
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
|
||||
const ProjectGetter = require('../Project/ProjectGetter')
|
||||
@@ -213,6 +214,10 @@ async function _getPrivilegeLevelForProjectWithoutUserWithPublicAccessLevel(
|
||||
publicAccessLevel,
|
||||
opts = {}
|
||||
) {
|
||||
if (!Features.hasFeature('link-sharing')) {
|
||||
// Link sharing disabled globally.
|
||||
return PrivilegeLevels.NONE
|
||||
}
|
||||
if (!opts.ignorePublicAccess) {
|
||||
if (publicAccessLevel === PublicAccessLevels.READ_ONLY) {
|
||||
// Legacy public read-only access for anonymous user
|
||||
|
||||
@@ -14,6 +14,7 @@ import { hasAdminAccess } from '../Helpers/AdminAuthorizationHelper.js'
|
||||
import TokenAccessHandler from '../TokenAccess/TokenAccessHandler.js'
|
||||
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.js'
|
||||
import LimitationsManager from '../Subscription/LimitationsManager.js'
|
||||
import Features from '../../infrastructure/Features.js'
|
||||
|
||||
const ObjectId = mongodb.ObjectId
|
||||
|
||||
@@ -159,6 +160,10 @@ async function getShareTokens(req, res) {
|
||||
const projectId = req.params.Project_id
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
|
||||
if (!Features.hasFeature('link-sharing')) {
|
||||
return res.sendStatus(403) // return Forbidden if link sharing is not enabled
|
||||
}
|
||||
|
||||
let tokens
|
||||
if (userId) {
|
||||
tokens = await CollaboratorsGetter.promises.getPublicShareTokens(
|
||||
|
||||
@@ -107,6 +107,9 @@ const _ProjectController = {
|
||||
async updateProjectAdminSettings(req, res) {
|
||||
const projectId = req.params.Project_id
|
||||
const user = SessionManager.getSessionUser(req.session)
|
||||
if (!Features.hasFeature('link-sharing')) {
|
||||
return res.sendStatus(403) // return Forbidden if link sharing is not enabled
|
||||
}
|
||||
const publicAccessLevel = req.body.publicAccessLevel
|
||||
const publicAccessLevels = [
|
||||
PublicAccessLevels.READ_ONLY,
|
||||
@@ -694,6 +697,15 @@ const _ProjectController = {
|
||||
capabilities.push('chat')
|
||||
}
|
||||
|
||||
// Note: this is not part of the default capabilities in the backend.
|
||||
// See services/web/modules/group-settings/app/src/DefaultGroupPolicy.mjs.
|
||||
// We are only using it on the frontend at the moment.
|
||||
// Add !Features.hasFeature('saas') to the conditional, as for chat above
|
||||
// if you define the capability in the backend.
|
||||
if (Features.hasFeature('link-sharing')) {
|
||||
capabilities.push('link-sharing')
|
||||
}
|
||||
|
||||
const isOverleafAssistBundleEnabled =
|
||||
splitTestAssignments['overleaf-assist-bundle']?.variant === 'enabled'
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const V1Api = require('../V1/V1Api')
|
||||
const crypto = require('crypto')
|
||||
const { callbackifyAll } = require('@overleaf/promise-utils')
|
||||
const Analytics = require('../Analytics/AnalyticsManager')
|
||||
|
||||
const Features = require('../../infrastructure/Features')
|
||||
const READ_AND_WRITE_TOKEN_PATTERN = '([0-9]+[a-z]{6,12})'
|
||||
const READ_ONLY_TOKEN_PATTERN = '([a-z]{12})'
|
||||
|
||||
@@ -152,6 +152,9 @@ const TokenAccessHandler = {
|
||||
},
|
||||
|
||||
async addReadOnlyUserToProject(userId, projectId, ownerId) {
|
||||
if (!Features.hasFeature('link-sharing')) {
|
||||
throw new Error('link sharing is disabled')
|
||||
}
|
||||
userId = new ObjectId(userId.toString())
|
||||
projectId = new ObjectId(projectId.toString())
|
||||
Analytics.recordEventForUserInBackground(userId, 'project-joined', {
|
||||
@@ -202,6 +205,9 @@ const TokenAccessHandler = {
|
||||
},
|
||||
|
||||
grantSessionTokenAccess(req, projectId, token) {
|
||||
if (!Features.hasFeature('link-sharing')) {
|
||||
throw new Error('link sharing is disabled')
|
||||
}
|
||||
if (!req.session) {
|
||||
return
|
||||
}
|
||||
@@ -213,6 +219,7 @@ const TokenAccessHandler = {
|
||||
|
||||
getRequestToken(req, projectId) {
|
||||
const token =
|
||||
Features.hasFeature('link-sharing') &&
|
||||
req.session &&
|
||||
req.session.anonTokenAccess &&
|
||||
req.session.anonTokenAccess[projectId.toString()]
|
||||
@@ -220,7 +227,7 @@ const TokenAccessHandler = {
|
||||
},
|
||||
|
||||
async validateTokenForAnonymousAccess(projectId, token, callback) {
|
||||
if (!token) {
|
||||
if (!Features.hasFeature('link-sharing') || !token) {
|
||||
return { isValidReadAndWrite: false, isValidReadOnly: false }
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,8 @@ const Features = {
|
||||
return Boolean(Settings.overleaf)
|
||||
case 'chat':
|
||||
return Boolean(Settings.disableChat) === false
|
||||
case 'link-sharing':
|
||||
return Boolean(Settings.disableLinkSharing) === false
|
||||
case 'github-sync':
|
||||
return Boolean(Settings.enableGithubSync)
|
||||
case 'git-bridge':
|
||||
|
||||
@@ -1226,39 +1226,41 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
||||
res.sendStatus(204)
|
||||
})
|
||||
|
||||
webRouter.get(
|
||||
`/read/:token(${TokenAccessController.READ_ONLY_TOKEN_PATTERN})`,
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.readOnlyToken),
|
||||
AnalyticsRegistrationSourceMiddleware.setSource(
|
||||
'collaboration',
|
||||
'link-sharing'
|
||||
),
|
||||
TokenAccessController.tokenAccessPage,
|
||||
AnalyticsRegistrationSourceMiddleware.clearSource()
|
||||
)
|
||||
if (Features.hasFeature('link-sharing')) {
|
||||
webRouter.get(
|
||||
`/read/:token(${TokenAccessController.READ_ONLY_TOKEN_PATTERN})`,
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.readOnlyToken),
|
||||
AnalyticsRegistrationSourceMiddleware.setSource(
|
||||
'collaboration',
|
||||
'link-sharing'
|
||||
),
|
||||
TokenAccessController.tokenAccessPage,
|
||||
AnalyticsRegistrationSourceMiddleware.clearSource()
|
||||
)
|
||||
|
||||
webRouter.get(
|
||||
`/:token(${TokenAccessController.READ_AND_WRITE_TOKEN_PATTERN})`,
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.readAndWriteToken),
|
||||
AnalyticsRegistrationSourceMiddleware.setSource(
|
||||
'collaboration',
|
||||
'link-sharing'
|
||||
),
|
||||
TokenAccessController.tokenAccessPage,
|
||||
AnalyticsRegistrationSourceMiddleware.clearSource()
|
||||
)
|
||||
webRouter.get(
|
||||
`/:token(${TokenAccessController.READ_AND_WRITE_TOKEN_PATTERN})`,
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.readAndWriteToken),
|
||||
AnalyticsRegistrationSourceMiddleware.setSource(
|
||||
'collaboration',
|
||||
'link-sharing'
|
||||
),
|
||||
TokenAccessController.tokenAccessPage,
|
||||
AnalyticsRegistrationSourceMiddleware.clearSource()
|
||||
)
|
||||
|
||||
webRouter.post(
|
||||
`/:token(${TokenAccessController.READ_AND_WRITE_TOKEN_PATTERN})/grant`,
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.grantTokenAccessReadWrite),
|
||||
TokenAccessController.grantTokenAccessReadAndWrite
|
||||
)
|
||||
webRouter.post(
|
||||
`/:token(${TokenAccessController.READ_AND_WRITE_TOKEN_PATTERN})/grant`,
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.grantTokenAccessReadWrite),
|
||||
TokenAccessController.grantTokenAccessReadAndWrite
|
||||
)
|
||||
|
||||
webRouter.post(
|
||||
`/read/:token(${TokenAccessController.READ_ONLY_TOKEN_PATTERN})/grant`,
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.grantTokenAccessReadOnly),
|
||||
TokenAccessController.grantTokenAccessReadOnly
|
||||
)
|
||||
webRouter.post(
|
||||
`/read/:token(${TokenAccessController.READ_ONLY_TOKEN_PATTERN})/grant`,
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.grantTokenAccessReadOnly),
|
||||
TokenAccessController.grantTokenAccessReadOnly
|
||||
)
|
||||
}
|
||||
|
||||
webRouter.get('/unsupported-browser', renderUnsupportedBrowserPage)
|
||||
|
||||
|
||||
@@ -432,6 +432,7 @@ module.exports = {
|
||||
],
|
||||
|
||||
disableChat: process.env.OVERLEAF_DISABLE_CHAT === 'true',
|
||||
disableLinkSharing: process.env.OVERLEAF_DISABLE_LINK_SHARING === 'true',
|
||||
enableSubscriptions: false,
|
||||
restrictedCountries: [],
|
||||
enableOnboardingEmails: process.env.ENABLE_ONBOARDING_EMAILS === 'true',
|
||||
|
||||
@@ -30,6 +30,8 @@ type AccessLevel = 'private' | 'tokenBased' | 'readAndWrite' | 'readOnly'
|
||||
export default function LinkSharing() {
|
||||
const [inflight, setInflight] = useState(false)
|
||||
const [showLinks, setShowLinks] = useState(true)
|
||||
const linkSharingEnabled =
|
||||
getMeta('ol-capabilities')?.includes('link-sharing')
|
||||
|
||||
const { monitorRequest } = useShareProjectContext()
|
||||
|
||||
@@ -58,6 +60,10 @@ export default function LinkSharing() {
|
||||
[monitorRequest, projectId]
|
||||
)
|
||||
|
||||
if (!linkSharingEnabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (publicAccessLevel) {
|
||||
// Private (with token-access available)
|
||||
case 'private':
|
||||
|
||||
@@ -93,7 +93,7 @@ export interface Meta {
|
||||
'ol-cannot-link-other-third-party-sso': boolean
|
||||
'ol-cannot-reactivate-subscription': boolean
|
||||
'ol-cannot-use-ai': boolean
|
||||
'ol-capabilities': Array<'dropbox' | 'chat' | 'use-ai'>
|
||||
'ol-capabilities': Array<'dropbox' | 'chat' | 'use-ai' | 'link-sharing'>
|
||||
'ol-compileSettings': {
|
||||
reducedTimeoutWarning: string
|
||||
compileTimeout: number
|
||||
|
||||
@@ -5,6 +5,8 @@ import request from './helpers/request.js'
|
||||
import settings from '@overleaf/settings'
|
||||
import { db } from '../../../app/src/infrastructure/mongodb.js'
|
||||
import expectErrorResponse from './helpers/expectErrorResponse.mjs'
|
||||
import logger from '@overleaf/logger'
|
||||
import sinon from 'sinon'
|
||||
|
||||
const tryEditorAccess = (user, projectId, test, callback) =>
|
||||
async.series(
|
||||
@@ -810,6 +812,106 @@ describe('TokenAccess', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('link sharing disabled', function () {
|
||||
const previous = settings.disableLinkSharing
|
||||
let loggerStub
|
||||
beforeEach(function () {
|
||||
settings.disableLinkSharing = true
|
||||
loggerStub = sinon.spy(logger, 'error')
|
||||
})
|
||||
afterEach(function () {
|
||||
settings.disableLinkSharing = previous
|
||||
loggerStub.restore()
|
||||
})
|
||||
|
||||
it('should deny access to project', function (done) {
|
||||
async.series(
|
||||
[
|
||||
cb =>
|
||||
tryEditorAccess(
|
||||
this.anon,
|
||||
this.projectId,
|
||||
expectErrorResponse.restricted.html,
|
||||
cb
|
||||
),
|
||||
// should not allow the user to access read-only token
|
||||
cb =>
|
||||
tryReadOnlyTokenAccess(
|
||||
this.anon,
|
||||
this.tokens.readOnly,
|
||||
(response, body) => {
|
||||
// NOTE: This would be 404 when recreating the router. The Server Pro E2E tests cover this.
|
||||
expect(response.statusCode).to.equal(200)
|
||||
},
|
||||
(response, body) => {
|
||||
// NOTE: This would be 404 when recreating the router. The Server Pro E2E tests cover this.
|
||||
expect(response.statusCode).to.equal(500)
|
||||
expect(loggerStub).to.have.been.calledWithMatch(
|
||||
{
|
||||
err: { message: 'link sharing is disabled' },
|
||||
},
|
||||
'%s %s',
|
||||
'POST',
|
||||
`/read/${this.tokens.readOnly}/grant`
|
||||
)
|
||||
},
|
||||
cb
|
||||
),
|
||||
// still no access
|
||||
cb =>
|
||||
tryEditorAccess(
|
||||
this.anon,
|
||||
this.projectId,
|
||||
expectErrorResponse.restricted.html,
|
||||
cb
|
||||
),
|
||||
// should not allow the user to join the project
|
||||
cb =>
|
||||
tryAnonContentAccess(
|
||||
this.anon,
|
||||
this.projectId,
|
||||
this.tokens.readOnly,
|
||||
(response, body) => {
|
||||
expect(response.statusCode).to.equal(403)
|
||||
expect(body).to.equal('Forbidden')
|
||||
},
|
||||
cb
|
||||
),
|
||||
],
|
||||
done
|
||||
)
|
||||
})
|
||||
|
||||
it('should deny access to access tokens', function (done) {
|
||||
tryFetchProjectTokens(this.anon, this.projectId, (error, response) => {
|
||||
expect(error).to.equal(null)
|
||||
expect(response.statusCode).to.equal(403)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should deny access to legacy public project', function (done) {
|
||||
async.series(
|
||||
[
|
||||
cb => this.owner.makePublic(this.projectId, 'readOnly', cb),
|
||||
|
||||
cb =>
|
||||
tryAnonContentAccess(
|
||||
this.anon,
|
||||
this.projectId,
|
||||
this.tokens.readOnly,
|
||||
(response, body) => {
|
||||
expect(response.statusCode).to.equal(403)
|
||||
expect(body).to.equal('Forbidden')
|
||||
},
|
||||
cb
|
||||
),
|
||||
],
|
||||
done
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('anonymous read-and-write token, disabled (feature is deprecated)', function () {
|
||||
|
||||
@@ -182,7 +182,11 @@ export function EditorProviders({
|
||||
merge({}, defaultUserSettings, userSettings)
|
||||
)
|
||||
|
||||
window.metaAttributesCache.set('ol-capabilities', ['chat', 'dropbox'])
|
||||
window.metaAttributesCache.set('ol-capabilities', [
|
||||
'chat',
|
||||
'dropbox',
|
||||
'link-sharing',
|
||||
])
|
||||
|
||||
const scope = merge(
|
||||
{
|
||||
|
||||
@@ -2,7 +2,11 @@ export function resetMeta() {
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-projectHistoryBlobsEnabled', true)
|
||||
window.metaAttributesCache.set('ol-i18n', { currentLangCode: 'en' })
|
||||
window.metaAttributesCache.set('ol-capabilities', ['chat', 'dropbox'])
|
||||
window.metaAttributesCache.set('ol-capabilities', [
|
||||
'chat',
|
||||
'dropbox',
|
||||
'link-sharing',
|
||||
])
|
||||
window.metaAttributesCache.set('ol-ExposedSettings', {
|
||||
appName: 'Overleaf',
|
||||
maxEntitiesPerProject: 10,
|
||||
|
||||
@@ -63,6 +63,7 @@ describe('AuthorizationManager', function () {
|
||||
passwordStrengthOptions: {},
|
||||
adminPrivilegeAvailable: true,
|
||||
adminRolesEnabled: false,
|
||||
moduleImportSequence: [],
|
||||
}
|
||||
this.AuthorizationManager = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
@@ -448,6 +449,28 @@ describe('AuthorizationManager', function () {
|
||||
expect(this.result).to.equal('readAndWrite')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with link-sharing disabled', function () {
|
||||
beforeEach(async function () {
|
||||
this.settings.disableLinkSharing = true
|
||||
this.result =
|
||||
await this.AuthorizationManager.promises.getPrivilegeLevelForProject(
|
||||
null,
|
||||
this.project._id,
|
||||
this.token
|
||||
)
|
||||
})
|
||||
|
||||
it('should not call CollaboratorsGetter.getProjectAccess', function () {
|
||||
this.CollaboratorsGetter.promises.getProjectAccess.called.should.equal(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('should return false', function () {
|
||||
expect(this.result).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("when the project doesn't exist", function () {
|
||||
|
||||
@@ -382,6 +382,7 @@ describe('ProjectController', function () {
|
||||
|
||||
describe('updateProjectAdminSettings', function () {
|
||||
it('should update the public access level', function (done) {
|
||||
this.Features.hasFeature.withArgs('link-sharing').returns(true)
|
||||
this.EditorController.promises.setPublicAccessLevel = sinon
|
||||
.stub()
|
||||
.resolves()
|
||||
@@ -399,6 +400,7 @@ describe('ProjectController', function () {
|
||||
})
|
||||
|
||||
it('should record the change in the project audit log', function (done) {
|
||||
this.Features.hasFeature.withArgs('link-sharing').returns(true)
|
||||
this.EditorController.promises.setPublicAccessLevel = sinon
|
||||
.stub()
|
||||
.resolves()
|
||||
@@ -422,6 +424,24 @@ describe('ProjectController', function () {
|
||||
}
|
||||
this.ProjectController.updateProjectAdminSettings(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should refuse to update the public access level when link sharing is disabled', function (done) {
|
||||
this.Features.hasFeature.withArgs('link-sharing').returns(false)
|
||||
this.EditorController.promises.setPublicAccessLevel = sinon
|
||||
.stub()
|
||||
.resolves()
|
||||
this.req.body = {
|
||||
publicAccessLevel: 'readOnly',
|
||||
}
|
||||
this.res.sendStatus = code => {
|
||||
this.EditorController.promises.setPublicAccessLevel.called.should.equal(
|
||||
false
|
||||
)
|
||||
code.should.equal(403) // Forbidden
|
||||
done()
|
||||
}
|
||||
this.ProjectController.updateProjectAdminSettings(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteProject', function () {
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('TokenAccessHandler', function () {
|
||||
'mongodb-legacy': { ObjectId },
|
||||
'../../models/Project': { Project: (this.Project = {}) },
|
||||
'@overleaf/metrics': (this.Metrics = { inc: sinon.stub() }),
|
||||
'@overleaf/settings': (this.settings = {}),
|
||||
'@overleaf/settings': (this.settings = { disableLinkSharing: false }),
|
||||
'../V1/V1Api': (this.V1Api = {
|
||||
promises: {
|
||||
request: sinon.stub(),
|
||||
@@ -35,388 +35,211 @@ describe('TokenAccessHandler', function () {
|
||||
'../Analytics/AnalyticsManager': (this.Analytics = {
|
||||
recordEventForUserInBackground: sinon.stub(),
|
||||
}),
|
||||
'../../infrastructure/Features': (this.Features = {}),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('getTokenType', function () {
|
||||
it('should determine tokens correctly', function () {
|
||||
const specs = {
|
||||
abcdefabcdef: 'readOnly',
|
||||
aaaaaabbbbbb: 'readOnly',
|
||||
'54325aaaaaa': 'readAndWrite',
|
||||
'54325aaaaaabbbbbb': 'readAndWrite',
|
||||
'': null,
|
||||
abc123def: null,
|
||||
}
|
||||
for (const token of Object.keys(specs)) {
|
||||
expect(this.TokenAccessHandler.getTokenType(token)).to.equal(
|
||||
specs[token]
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('getProjectByReadOnlyToken', function () {
|
||||
describe('when link sharing is enabled', function () {
|
||||
beforeEach(function () {
|
||||
this.token = 'abcdefabcdef'
|
||||
this.Project.findOne = sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(this.project),
|
||||
})
|
||||
this.Features.hasFeature = sinon
|
||||
.stub()
|
||||
.withArgs('link-sharing')
|
||||
.returns(true)
|
||||
})
|
||||
|
||||
it('should get the project', async function () {
|
||||
const project =
|
||||
await this.TokenAccessHandler.promises.getProjectByReadOnlyToken(
|
||||
this.token
|
||||
)
|
||||
expect(project).to.exist
|
||||
expect(this.Project.findOne.callCount).to.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getProjectByReadAndWriteToken', function () {
|
||||
beforeEach(function () {
|
||||
sinon.spy(this.Crypto, 'timingSafeEqual')
|
||||
this.token = '1234abcdefabcdef'
|
||||
this.project.tokens = {
|
||||
readAndWrite: this.token,
|
||||
readAndWritePrefix: '1234',
|
||||
}
|
||||
this.Project.findOne = sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(this.project),
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.Crypto.timingSafeEqual.restore()
|
||||
})
|
||||
|
||||
it('should get the project and do timing-safe comparison', async function () {
|
||||
const project =
|
||||
await this.TokenAccessHandler.promises.getProjectByReadAndWriteToken(
|
||||
this.token
|
||||
)
|
||||
expect(project).to.exist
|
||||
expect(this.Crypto.timingSafeEqual.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Crypto.timingSafeEqual.calledWith(Buffer.from(this.token))
|
||||
).to.equal(true)
|
||||
expect(this.Project.findOne.callCount).to.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addReadOnlyUserToProject', function () {
|
||||
beforeEach(function () {
|
||||
this.Project.updateOne = sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(null),
|
||||
})
|
||||
})
|
||||
|
||||
it('should call Project.updateOne', async function () {
|
||||
await this.TokenAccessHandler.promises.addReadOnlyUserToProject(
|
||||
this.userId,
|
||||
this.projectId,
|
||||
this.project.owner_ref
|
||||
)
|
||||
expect(this.Project.updateOne.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Project.updateOne.calledWith({
|
||||
_id: this.projectId,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(this.Project.updateOne.lastCall.args[1].$addToSet).to.have.keys(
|
||||
'tokenAccessReadOnly_refs'
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.Analytics.recordEventForUserInBackground,
|
||||
this.userId,
|
||||
'project-joined',
|
||||
{
|
||||
mode: 'view',
|
||||
role: PrivilegeLevels.READ_ONLY,
|
||||
projectId: this.projectId.toString(),
|
||||
ownerId: this.project.owner_ref.toString(),
|
||||
source: 'link-sharing',
|
||||
describe('getTokenType', function () {
|
||||
it('should determine tokens correctly', function () {
|
||||
const specs = {
|
||||
abcdefabcdef: 'readOnly',
|
||||
aaaaaabbbbbb: 'readOnly',
|
||||
'54325aaaaaa': 'readAndWrite',
|
||||
'54325aaaaaabbbbbb': 'readAndWrite',
|
||||
'': null,
|
||||
abc123def: null,
|
||||
}
|
||||
)
|
||||
for (const token of Object.keys(specs)) {
|
||||
expect(this.TokenAccessHandler.getTokenType(token)).to.equal(
|
||||
specs[token]
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('when Project.updateOne produces an error', function () {
|
||||
describe('getProjectByReadOnlyToken', function () {
|
||||
beforeEach(function () {
|
||||
this.token = 'abcdefabcdef'
|
||||
this.Project.findOne = sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(this.project),
|
||||
})
|
||||
})
|
||||
|
||||
it('should get the project', async function () {
|
||||
const project =
|
||||
await this.TokenAccessHandler.promises.getProjectByReadOnlyToken(
|
||||
this.token
|
||||
)
|
||||
expect(project).to.exist
|
||||
expect(this.Project.findOne.callCount).to.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getProjectByReadAndWriteToken', function () {
|
||||
beforeEach(function () {
|
||||
sinon.spy(this.Crypto, 'timingSafeEqual')
|
||||
this.token = '1234abcdefabcdef'
|
||||
this.project.tokens = {
|
||||
readAndWrite: this.token,
|
||||
readAndWritePrefix: '1234',
|
||||
}
|
||||
this.Project.findOne = sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(this.project),
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.Crypto.timingSafeEqual.restore()
|
||||
})
|
||||
|
||||
it('should get the project and do timing-safe comparison', async function () {
|
||||
const project =
|
||||
await this.TokenAccessHandler.promises.getProjectByReadAndWriteToken(
|
||||
this.token
|
||||
)
|
||||
expect(project).to.exist
|
||||
expect(this.Crypto.timingSafeEqual.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Crypto.timingSafeEqual.calledWith(Buffer.from(this.token))
|
||||
).to.equal(true)
|
||||
expect(this.Project.findOne.callCount).to.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addReadOnlyUserToProject', function () {
|
||||
beforeEach(function () {
|
||||
this.Project.updateOne = sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(null),
|
||||
})
|
||||
})
|
||||
|
||||
it('should call Project.updateOne', async function () {
|
||||
await this.TokenAccessHandler.promises.addReadOnlyUserToProject(
|
||||
this.userId,
|
||||
this.projectId,
|
||||
this.project.owner_ref
|
||||
)
|
||||
expect(this.Project.updateOne.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Project.updateOne.calledWith({
|
||||
_id: this.projectId,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(this.Project.updateOne.lastCall.args[1].$addToSet).to.have.keys(
|
||||
'tokenAccessReadOnly_refs'
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.Analytics.recordEventForUserInBackground,
|
||||
this.userId,
|
||||
'project-joined',
|
||||
{
|
||||
mode: 'view',
|
||||
role: PrivilegeLevels.READ_ONLY,
|
||||
projectId: this.projectId.toString(),
|
||||
ownerId: this.project.owner_ref.toString(),
|
||||
source: 'link-sharing',
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('when Project.updateOne produces an error', function () {
|
||||
beforeEach(function () {
|
||||
this.Project.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().rejects(new Error('woops')) })
|
||||
})
|
||||
|
||||
it('should be rejected', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.addReadOnlyUserToProject(
|
||||
this.userId,
|
||||
this.projectId
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeReadAndWriteUserFromProject', function () {
|
||||
beforeEach(function () {
|
||||
this.Project.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().rejects(new Error('woops')) })
|
||||
.returns({ exec: sinon.stub().resolves(null) })
|
||||
})
|
||||
|
||||
it('should be rejected', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.addReadOnlyUserToProject(
|
||||
this.userId,
|
||||
this.projectId
|
||||
)
|
||||
).to.be.rejected
|
||||
it('should call Project.updateOne', async function () {
|
||||
await this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject(
|
||||
this.userId,
|
||||
this.projectId
|
||||
)
|
||||
|
||||
expect(this.Project.updateOne.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Project.updateOne.calledWith({
|
||||
_id: this.projectId,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(this.Project.updateOne.lastCall.args[1].$pull).to.have.keys(
|
||||
'tokenAccessReadAndWrite_refs'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeReadAndWriteUserFromProject', function () {
|
||||
beforeEach(function () {
|
||||
this.Project.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves(null) })
|
||||
})
|
||||
|
||||
it('should call Project.updateOne', async function () {
|
||||
await this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject(
|
||||
this.userId,
|
||||
this.projectId
|
||||
)
|
||||
|
||||
expect(this.Project.updateOne.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Project.updateOne.calledWith({
|
||||
_id: this.projectId,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(this.Project.updateOne.lastCall.args[1].$pull).to.have.keys(
|
||||
'tokenAccessReadAndWrite_refs'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('moveReadAndWriteUserToReadOnly', function () {
|
||||
beforeEach(function () {
|
||||
this.Project.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves(null) })
|
||||
})
|
||||
|
||||
it('should call Project.updateOne', async function () {
|
||||
await this.TokenAccessHandler.promises.moveReadAndWriteUserToReadOnly(
|
||||
this.userId,
|
||||
this.projectId
|
||||
)
|
||||
|
||||
expect(this.Project.updateOne.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Project.updateOne.calledWith({
|
||||
_id: this.projectId,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(this.Project.updateOne.lastCall.args[1].$pull).to.have.keys(
|
||||
'tokenAccessReadAndWrite_refs'
|
||||
)
|
||||
expect(this.Project.updateOne.lastCall.args[1].$addToSet).to.have.keys(
|
||||
'tokenAccessReadOnly_refs'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('grantSessionTokenAccess', function () {
|
||||
beforeEach(function () {
|
||||
this.req = { session: {}, headers: {} }
|
||||
})
|
||||
|
||||
it('should add the token to the session', function () {
|
||||
this.TokenAccessHandler.promises.grantSessionTokenAccess(
|
||||
this.req,
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
expect(
|
||||
this.req.session.anonTokenAccess[this.projectId.toString()]
|
||||
).to.equal(this.token)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateTokenForAnonymousAccess', function () {
|
||||
describe('when a read-only project is found', function () {
|
||||
describe('moveReadAndWriteUserToReadOnly', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.getTokenType = sinon.stub().returns('readOnly')
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
this.Project.updateOne = sinon
|
||||
.stub()
|
||||
.resolves(this.project)
|
||||
.returns({ exec: sinon.stub().resolves(null) })
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
it('should call Project.updateOne', async function () {
|
||||
await this.TokenAccessHandler.promises.moveReadAndWriteUserToReadOnly(
|
||||
this.userId,
|
||||
this.projectId
|
||||
)
|
||||
|
||||
expect(this.Project.updateOne.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Project.updateOne.calledWith({
|
||||
_id: this.projectId,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(this.Project.updateOne.lastCall.args[1].$pull).to.have.keys(
|
||||
'tokenAccessReadAndWrite_refs'
|
||||
)
|
||||
expect(this.Project.updateOne.lastCall.args[1].$addToSet).to.have.keys(
|
||||
'tokenAccessReadOnly_refs'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('grantSessionTokenAccess', function () {
|
||||
beforeEach(function () {
|
||||
this.req = { session: {}, headers: {} }
|
||||
})
|
||||
|
||||
it('should add the token to the session', function () {
|
||||
this.TokenAccessHandler.promises.grantSessionTokenAccess(
|
||||
this.req,
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should allow read-only access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(true)
|
||||
this.req.session.anonTokenAccess[this.projectId.toString()]
|
||||
).to.equal(this.token)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a read-and-write project is found', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.promises.getTokenType = sinon
|
||||
.stub()
|
||||
.returns('readAndWrite')
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.resolves(this.project)
|
||||
})
|
||||
|
||||
describe('when Anonymous token access is not enabled', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should not allow read-and-write access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when anonymous token access is enabled', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.promises.ANONYMOUS_READ_AND_WRITE_ENABLED = true
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should allow read-and-write access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(true)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when no project is found', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.resolves(null)
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should not allow any access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when findProject produces an error', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.rejects(new Error('woops'))
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
).to.be.rejected
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should produce an error and not allow access', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('when project is not set to token-based access', function () {
|
||||
beforeEach(function () {
|
||||
this.project.publicAccesLevel = 'private'
|
||||
})
|
||||
|
||||
describe('for read-and-write project', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.getTokenType = sinon
|
||||
.stub()
|
||||
.returns('readAndWrite')
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.resolves(this.project)
|
||||
})
|
||||
|
||||
it('should not allow any access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('for read-only project', function () {
|
||||
describe('validateTokenForAnonymousAccess', function () {
|
||||
describe('when a read-only project is found', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.getTokenType = sinon
|
||||
.stub()
|
||||
@@ -426,6 +249,114 @@ describe('TokenAccessHandler', function () {
|
||||
.resolves(this.project)
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should allow read-only access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a read-and-write project is found', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.promises.getTokenType = sinon
|
||||
.stub()
|
||||
.returns('readAndWrite')
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.resolves(this.project)
|
||||
})
|
||||
|
||||
describe('when Anonymous token access is not enabled', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should not allow read-and-write access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when anonymous token access is enabled', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.promises.ANONYMOUS_READ_AND_WRITE_ENABLED = true
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should allow read-and-write access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(true)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when no project is found', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.resolves(null)
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should not allow any access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
@@ -438,13 +369,248 @@ describe('TokenAccessHandler', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('with nothing', function () {
|
||||
describe('when findProject produces an error', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.rejects(new Error('woops'))
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
).to.be.rejected
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should produce an error and not allow access', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('when project is not set to token-based access', function () {
|
||||
beforeEach(function () {
|
||||
this.project.publicAccesLevel = 'private'
|
||||
})
|
||||
|
||||
describe('for read-and-write project', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.getTokenType = sinon
|
||||
.stub()
|
||||
.returns('readAndWrite')
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.resolves(this.project)
|
||||
})
|
||||
|
||||
it('should not allow any access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('for read-only project', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.getTokenType = sinon
|
||||
.stub()
|
||||
.returns('readOnly')
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.resolves(this.project)
|
||||
})
|
||||
|
||||
it('should not allow any access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with nothing', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.resolves(null)
|
||||
})
|
||||
|
||||
it('should not allow any access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when link sharing is disabled', function () {
|
||||
beforeEach(function () {
|
||||
this.Features.hasFeature = sinon
|
||||
.stub()
|
||||
.withArgs('link-sharing')
|
||||
.returns(false)
|
||||
})
|
||||
|
||||
describe('addReadOnlyUserToProject', function () {
|
||||
beforeEach(function () {
|
||||
this.Project.updateOne = sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(null),
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.addReadOnlyUserToProject(
|
||||
this.userId,
|
||||
this.projectId,
|
||||
this.project.owner_ref
|
||||
)
|
||||
).to.be.rejectedWith('link sharing is disabled')
|
||||
expect(this.Project.updateOne.callCount).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('grantSessionTokenAccess', function () {
|
||||
beforeEach(function () {
|
||||
this.req = { session: {}, headers: {} }
|
||||
})
|
||||
|
||||
it('should throw an error', function () {
|
||||
expect(() => {
|
||||
this.TokenAccessHandler.promises.grantSessionTokenAccess(
|
||||
this.req,
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
}).to.throw('link sharing is disabled')
|
||||
expect(this.req.session.anonTokenAccess).to.be.undefined
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateTokenForAnonymousAccess', function () {
|
||||
describe('when a read-only project is found', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.getTokenType = sinon
|
||||
.stub()
|
||||
.returns('readOnly')
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.resolves(this.project)
|
||||
})
|
||||
|
||||
it('should refuse access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a read-and-write project is found', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.promises.getTokenType = sinon
|
||||
.stub()
|
||||
.returns('readAndWrite')
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.resolves(this.project)
|
||||
})
|
||||
|
||||
describe('when Anonymous token access is not enabled', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false
|
||||
})
|
||||
|
||||
it('should refuse access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when anonymous token access is enabled', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.promises.ANONYMOUS_READ_AND_WRITE_ENABLED = true
|
||||
})
|
||||
|
||||
it('should not try to find any projects', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(0)
|
||||
})
|
||||
|
||||
it('should refuse access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when no project is found', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.resolves(null)
|
||||
})
|
||||
|
||||
it('should not try to find any projects ', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(0)
|
||||
})
|
||||
|
||||
it('should not allow any access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
|
||||
Reference in New Issue
Block a user