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:
Brian Gough
2025-08-19 09:12:57 +01:00
committed by Copybot
parent 0138ae0dff
commit f5dbbadf79
18 changed files with 908 additions and 392 deletions

View File

@@ -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 }

View File

@@ -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',

View File

@@ -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()
})
})
})
})