Ver código fonte

Merge pull request #13875 from overleaf/msm-hotfix-4-0-4

[CE/SP] Hotfixes 3.5.9 / 4.0.4

GitOrigin-RevId: 89f5b3832b2a069a2ee31b2ddc5cde9a4bbb5464
Miguel Serrano 3 anos atrás
pai
commit
22e232242c

+ 10 - 0
server-ce/hotfix/3.5.9/Dockerfile

@@ -0,0 +1,10 @@
+FROM sharelatex/sharelatex:3.5.8
+
+# Node update
+RUN curl -sSL https://deb.nodesource.com/setup_16.x | bash - \
+    && apt-get install -y nodejs
+
+# Patch: fetch access tokens via endpoint
+COPY pr_13485.patch .
+RUN patch -p0 < pr_13485.patch
+RUN node genScript compile | bash

+ 389 - 0
server-ce/hotfix/3.5.9/pr_13485.patch

@@ -0,0 +1,389 @@
+--- services/web/app/src/Features/Collaborators/CollaboratorsController.js
++++ services/web/app/src/Features/Collaborators/CollaboratorsController.js
+@@ -11,6 +11,7 @@ const Errors = require('../Errors/Errors')
+ const logger = require('@overleaf/logger')
+ const { expressify } = require('../../util/promises')
+ const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
++const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
+
+ module.exports = {
+   removeUserFromProject: expressify(removeUserFromProject),
+@@ -18,6 +19,7 @@ module.exports = {
+   getAllMembers: expressify(getAllMembers),
+   setCollaboratorInfo: expressify(setCollaboratorInfo),
+   transferOwnership: expressify(transferOwnership),
++  getShareTokens: expressify(getShareTokens),
+ }
+
+ async function removeUserFromProject(req, res, next) {
+@@ -114,3 +116,37 @@ async function _removeUserIdFromProject(projectId, userId) {
+   )
+   await TagsHandler.promises.removeProjectFromAllTags(userId, projectId)
+ }
++
++async function getShareTokens(req, res) {
++  const projectId = req.params.Project_id
++  const userId = SessionManager.getLoggedInUserId(req.session)
++
++  let tokens
++  if (userId) {
++    tokens = await CollaboratorsGetter.promises.getPublicShareTokens(
++      ObjectId(userId),
++      ObjectId(projectId)
++    )
++  } else {
++    // anonymous access, the token is already available in the session
++    const readOnly = TokenAccessHandler.getRequestToken(req, projectId)
++    tokens = { readOnly }
++  }
++  if (!tokens) {
++    return res.sendStatus(403)
++  }
++
++  if (tokens.readOnly || tokens.readAndWrite) {
++    logger.info(
++      {
++        projectId,
++        userId: userId || 'anonymous',
++        ip: req.ip,
++        tokens: Object.keys(tokens),
++      },
++      'project tokens accessed'
++    )
++  }
++
++  res.json(tokens)
++}
+--- services/web/app/src/Features/Collaborators/CollaboratorsGetter.js
++++ services/web/app/src/Features/Collaborators/CollaboratorsGetter.js
+@@ -25,6 +25,7 @@ module.exports = {
+   getInvitedCollaboratorCount: callbackify(getInvitedCollaboratorCount),
+   getProjectsUserIsMemberOf: callbackify(getProjectsUserIsMemberOf),
+   isUserInvitedMemberOfProject: callbackify(isUserInvitedMemberOfProject),
++  getPublicShareTokens: callbackify(getPublicShareTokens),
+   userIsTokenMember: callbackify(userIsTokenMember),
+   getAllInvitedMembers: callbackify(getAllInvitedMembers),
+   promises: {
+@@ -37,6 +38,7 @@ module.exports = {
+     getInvitedCollaboratorCount,
+     getProjectsUserIsMemberOf,
+     isUserInvitedMemberOfProject,
++    getPublicShareTokens,
+     userIsTokenMember,
+     getAllInvitedMembers,
+   },
+@@ -133,6 +135,40 @@ async function isUserInvitedMemberOfProject(userId, projectId) {
+   return false
+ }
+
++async function getPublicShareTokens(userId, projectId) {
++  const memberInfo = await Project.findOne(
++    {
++      _id: projectId,
++    },
++    {
++      isOwner: { $eq: ['$owner_ref', userId] },
++      hasTokenReadOnlyAccess: {
++        $and: [
++          { $in: [userId, '$tokenAccessReadOnly_refs'] },
++          { $eq: ['$publicAccesLevel', PublicAccessLevels.TOKEN_BASED] },
++        ],
++      },
++      tokens: 1,
++    }
++  )
++    .lean()
++    .exec()
++
++  if (!memberInfo) {
++    return null
++  }
++
++  if (memberInfo.isOwner) {
++    return memberInfo.tokens
++  } else if (memberInfo.hasTokenReadOnlyAccess) {
++    return {
++      readOnly: memberInfo.tokens.readOnly,
++    }
++  } else {
++    return {}
++  }
++}
++
+ async function getProjectsUserIsMemberOf(userId, fields) {
+   const limit = pLimit(2)
+   const [readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly] =
+--- services/web/app/src/Features/Collaborators/CollaboratorsRouter.js
++++ services/web/app/src/Features/Collaborators/CollaboratorsRouter.js
+@@ -22,6 +22,10 @@ const rateLimiters = {
+     points: 200,
+     duration: 60 * 10,
+   }),
++  getProjectTokens: new RateLimiter('get-project-tokens', {
++    points: 200,
++    duration: 60 * 10,
++  }),
+ }
+
+ module.exports = {
+@@ -139,5 +143,12 @@ module.exports = {
+       CollaboratorsInviteController.acceptInvite,
+       AnalyticsRegistrationSourceMiddleware.clearSource()
+     )
++
++    webRouter.get(
++      '/project/:Project_id/tokens',
++      RateLimiterMiddleware.rateLimit(rateLimiters.getProjectTokens),
++      AuthorizationMiddleware.ensureUserCanReadProject,
++      CollaboratorsController.getShareTokens
++    )
+   },
+ }
+--- services/web/app/src/Features/Editor/EditorController.js
++++ services/web/app/src/Features/Editor/EditorController.js
+@@ -581,20 +581,7 @@ const EditorController = {
+           { newAccessLevel }
+         )
+         if (newAccessLevel === PublicAccessLevels.TOKEN_BASED) {
+-          ProjectDetailsHandler.ensureTokensArePresent(
+-            projectId,
+-            function (err, tokens) {
+-              if (err) {
+-                return callback(err)
+-              }
+-              EditorRealTimeController.emitToRoom(
+-                projectId,
+-                'project:tokens:changed',
+-                { tokens }
+-              )
+-              callback()
+-            }
+-          )
++          ProjectDetailsHandler.ensureTokensArePresent(projectId, callback)
+         } else {
+           callback()
+         }
+--- services/web/app/src/Features/Editor/EditorHttpController.js
++++ services/web/app/src/Features/Editor/EditorHttpController.js
+@@ -67,8 +67,6 @@ async function joinProject(req, res, next) {
+   if (!project) {
+     return res.sendStatus(403)
+   }
+-  // Hide access tokens if this is not the project owner
+-  TokenAccessHandler.protectTokens(project, privilegeLevel)
+   // Hide sensitive data if the user is restricted
+   if (isRestrictedUser) {
+     project.owner = { _id: project.owner._id }
+--- services/web/app/src/Features/Project/ProjectController.js
++++ services/web/app/src/Features/Project/ProjectController.js
+@@ -343,7 +343,7 @@ const ProjectController = {
+     const userId = SessionManager.getLoggedInUserId(req.session)
+     ProjectGetter.findAllUsersProjects(
+       userId,
+-      'name lastUpdated publicAccesLevel archived trashed owner_ref tokens',
++      'name lastUpdated publicAccesLevel archived trashed owner_ref',
+       (err, projects) => {
+         if (err != null) {
+           return next(err)
+@@ -1072,7 +1072,6 @@ const ProjectController = {
+     // If a project is simultaneously trashed and archived, we will consider it archived but not trashed.
+     const trashed = ProjectHelper.isTrashed(project, userId) && !archived
+
+-    TokenAccessHandler.protectTokens(project, accessLevel)
+     const model = {
+       id: project._id,
+       name: project.name,
+--- services/web/app/src/Features/Project/ProjectDetailsHandler.js
++++ services/web/app/src/Features/Project/ProjectDetailsHandler.js
+@@ -207,14 +207,13 @@ async function ensureTokensArePresent(projectId) {
+     project.tokens.readOnly != null &&
+     project.tokens.readAndWrite != null
+   ) {
+-    return project.tokens
++    return
+   }
+   await _generateTokens(project)
+   await Project.updateOne(
+     { _id: projectId },
+     { $set: { tokens: project.tokens } }
+   ).exec()
+-  return project.tokens
+ }
+
+ async function clearTokens(projectId) {
+--- services/web/app/src/Features/Project/ProjectEditorHandler.js
++++ services/web/app/src/Features/Project/ProjectEditorHandler.js
+@@ -49,7 +49,6 @@ module.exports = ProjectEditorHandler = {
+       ),
+       members: [],
+       invites,
+-      tokens: project.tokens,
+       imageName:
+         project.imageName != null
+           ? Path.basename(project.imageName)
+--- services/web/app/src/Features/TokenAccess/TokenAccessHandler.js
++++ services/web/app/src/Features/TokenAccess/TokenAccessHandler.js
+@@ -246,22 +246,6 @@ const TokenAccessHandler = {
+     })
+   },
+
+-  protectTokens(project, privilegeLevel) {
+-    if (!project || !project.tokens) {
+-      return
+-    }
+-    if (privilegeLevel === PrivilegeLevels.OWNER) {
+-      return
+-    }
+-    if (privilegeLevel !== PrivilegeLevels.READ_AND_WRITE) {
+-      project.tokens.readAndWrite = ''
+-      project.tokens.readAndWritePrefix = ''
+-    }
+-    if (privilegeLevel !== PrivilegeLevels.READ_ONLY) {
+-      project.tokens.readOnly = ''
+-    }
+-  },
+-
+   getV1DocPublishedInfo(token, callback) {
+     // default to allowing access
+     if (!Settings.apis.v1 || !Settings.apis.v1.url) {
+@@ -304,7 +288,6 @@ TokenAccessHandler.promises = promisifyAll(TokenAccessHandler, {
+     '_projectFindOne',
+     'grantSessionTokenAccess',
+     'getRequestToken',
+-    'protectTokens',
+   ],
+   multiResult: {
+     validateTokenForAnonymousAccess: ['isValidReadAndWrite', 'isValidReadOnly'],
+--- services/web/frontend/js/features/share-project-modal/components/link-sharing.js
++++ services/web/frontend/js/features/share-project-modal/components/link-sharing.js
+@@ -1,4 +1,4 @@
+-import { useCallback, useState } from 'react'
++import { useCallback, useState, useEffect } from 'react'
+ import PropTypes from 'prop-types'
+ import { Button, Col, Row } from 'react-bootstrap'
+ import { Trans } from 'react-i18next'
+@@ -10,6 +10,8 @@ import CopyLink from '../../../shared/components/copy-link'
+ import { useProjectContext } from '../../../shared/context/project-context'
+ import * as eventTracking from '../../../infrastructure/event-tracking'
+ import { useUserContext } from '../../../shared/context/user-context'
++import { getJSON } from '../../../infrastructure/fetch-json'
++import useAbortController from '../../../shared/hooks/use-abort-controller'
+
+ export default function LinkSharing({ canAddCollaborators }) {
+   const [inflight, setInflight] = useState(false)
+@@ -27,8 +29,7 @@ export default function LinkSharing({ canAddCollaborators }) {
+       )
+         .then(() => {
+           // NOTE: not calling `updateProject` here as it receives data via
+-          // project:publicAccessLevel:changed and project:tokens:changed
+-          // over the websocket connection
++          // project:publicAccessLevel:changed over the websocket connection
+           // TODO: eventTracking.sendMB('project-make-token-based') when newPublicAccessLevel is 'tokenBased'
+         })
+         .finally(() => {
+@@ -106,7 +107,17 @@ PrivateSharing.propTypes = {
+ }
+
+ function TokenBasedSharing({ setAccessLevel, inflight, canAddCollaborators }) {
+-  const { tokens } = useProjectContext()
++  const { _id: projectId } = useProjectContext()
++
++  const [tokens, setTokens] = useState(null)
++
++  const { signal } = useAbortController()
++
++  useEffect(() => {
++    getJSON(`/project/${projectId}/tokens`, { signal })
++      .then(data => setTokens(data))
++      .catch(error => console.error(error))
++  }, [projectId, signal])
+
+   return (
+     <Row className="public-access-level">
+@@ -194,7 +205,17 @@ LegacySharing.propTypes = {
+ }
+
+ export function ReadOnlyTokenLink() {
+-  const { tokens } = useProjectContext()
++  const { _id: projectId } = useProjectContext()
++
++  const [tokens, setTokens] = useState(null)
++
++  const { signal } = useAbortController()
++
++  useEffect(() => {
++    getJSON(`/project/${projectId}/tokens`, { signal })
++      .then(data => setTokens(data))
++      .catch(error => console.error(error))
++  }, [projectId, signal])
+
+   return (
+     <Row className="public-access-level">
+--- services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
++++ services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
+@@ -31,16 +31,6 @@ export default App.controller(
+       })
+     }
+
+-    /* tokens */
+-
+-    ide.socket.on('project:tokens:changed', data => {
+-      if (data.tokens != null) {
+-        $scope.$applyAsync(() => {
+-          $scope.project.tokens = data.tokens
+-        })
+-      }
+-    })
+-
+     ide.socket.on('project:membership:changed', data => {
+       if (data.members) {
+         listProjectMembers($scope.project._id)
+--- services/web/frontend/js/shared/context/mock/mock-ide.js
++++ services/web/frontend/js/shared/context/mock/mock-ide.js
+@@ -27,10 +27,6 @@ export const getMockIde = () => {
+           zotero: false,
+         },
+         publicAccessLevel: '',
+-        tokens: {
+-          readOnly: '',
+-          readAndWrite: '',
+-        },
+         owner: {
+           _id: '',
+           email: '',
+--- services/web/frontend/js/shared/context/project-context.js
++++ services/web/frontend/js/shared/context/project-context.js
+@@ -28,10 +28,6 @@ export const projectShape = {
+     versioning: PropTypes.bool,
+   }),
+   publicAccessLevel: PropTypes.string,
+-  tokens: PropTypes.shape({
+-    readOnly: PropTypes.string,
+-    readAndWrite: PropTypes.string,
+-  }),
+   owner: PropTypes.shape({
+     _id: PropTypes.string.isRequired,
+     email: PropTypes.string.isRequired,
+@@ -81,7 +77,6 @@ export function ProjectProvider({ children }) {
+     invites,
+     features,
+     publicAccesLevel: publicAccessLevel,
+-    tokens,
+     owner,
+   } = project || projectFallback
+
+@@ -94,7 +89,6 @@ export function ProjectProvider({ children }) {
+       invites,
+       features,
+       publicAccessLevel,
+-      tokens,
+       owner,
+     }
+   }, [
+@@ -105,7 +99,6 @@ export function ProjectProvider({ children }) {
+     invites,
+     features,
+     publicAccessLevel,
+-    tokens,
+     owner,
+   ])

+ 10 - 0
server-ce/hotfix/4.0.4/Dockerfile

@@ -0,0 +1,10 @@
+FROM sharelatex/sharelatex:4.0.3
+
+# Node update
+RUN curl -sSL https://deb.nodesource.com/setup_16.x | bash - \
+    && apt-get install -y nodejs
+
+# Patch: fetch access tokens via endpoint
+COPY pr_13485.patch .
+RUN patch -p0 < pr_13485.patch
+RUN node genScript compile | bash

+ 389 - 0
server-ce/hotfix/4.0.4/pr_13485.patch

@@ -0,0 +1,389 @@
+--- services/web/app/src/Features/Collaborators/CollaboratorsController.js
++++ services/web/app/src/Features/Collaborators/CollaboratorsController.js
+@@ -11,6 +11,7 @@ const Errors = require('../Errors/Errors')
+ const logger = require('@overleaf/logger')
+ const { expressify } = require('../../util/promises')
+ const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
++const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
+
+ module.exports = {
+   removeUserFromProject: expressify(removeUserFromProject),
+@@ -18,6 +19,7 @@ module.exports = {
+   getAllMembers: expressify(getAllMembers),
+   setCollaboratorInfo: expressify(setCollaboratorInfo),
+   transferOwnership: expressify(transferOwnership),
++  getShareTokens: expressify(getShareTokens),
+ }
+
+ async function removeUserFromProject(req, res, next) {
+@@ -114,3 +116,37 @@ async function _removeUserIdFromProject(projectId, userId) {
+   )
+   await TagsHandler.promises.removeProjectFromAllTags(userId, projectId)
+ }
++
++async function getShareTokens(req, res) {
++  const projectId = req.params.Project_id
++  const userId = SessionManager.getLoggedInUserId(req.session)
++
++  let tokens
++  if (userId) {
++    tokens = await CollaboratorsGetter.promises.getPublicShareTokens(
++      ObjectId(userId),
++      ObjectId(projectId)
++    )
++  } else {
++    // anonymous access, the token is already available in the session
++    const readOnly = TokenAccessHandler.getRequestToken(req, projectId)
++    tokens = { readOnly }
++  }
++  if (!tokens) {
++    return res.sendStatus(403)
++  }
++
++  if (tokens.readOnly || tokens.readAndWrite) {
++    logger.info(
++      {
++        projectId,
++        userId: userId || 'anonymous',
++        ip: req.ip,
++        tokens: Object.keys(tokens),
++      },
++      'project tokens accessed'
++    )
++  }
++
++  res.json(tokens)
++}
+--- services/web/app/src/Features/Collaborators/CollaboratorsGetter.js
++++ services/web/app/src/Features/Collaborators/CollaboratorsGetter.js
+@@ -25,6 +25,7 @@ module.exports = {
+   getInvitedCollaboratorCount: callbackify(getInvitedCollaboratorCount),
+   getProjectsUserIsMemberOf: callbackify(getProjectsUserIsMemberOf),
+   isUserInvitedMemberOfProject: callbackify(isUserInvitedMemberOfProject),
++  getPublicShareTokens: callbackify(getPublicShareTokens),
+   userIsTokenMember: callbackify(userIsTokenMember),
+   getAllInvitedMembers: callbackify(getAllInvitedMembers),
+   promises: {
+@@ -37,6 +38,7 @@ module.exports = {
+     getInvitedCollaboratorCount,
+     getProjectsUserIsMemberOf,
+     isUserInvitedMemberOfProject,
++    getPublicShareTokens,
+     userIsTokenMember,
+     getAllInvitedMembers,
+   },
+@@ -133,6 +135,40 @@ async function isUserInvitedMemberOfProject(userId, projectId) {
+   return false
+ }
+
++async function getPublicShareTokens(userId, projectId) {
++  const memberInfo = await Project.findOne(
++    {
++      _id: projectId,
++    },
++    {
++      isOwner: { $eq: ['$owner_ref', userId] },
++      hasTokenReadOnlyAccess: {
++        $and: [
++          { $in: [userId, '$tokenAccessReadOnly_refs'] },
++          { $eq: ['$publicAccesLevel', PublicAccessLevels.TOKEN_BASED] },
++        ],
++      },
++      tokens: 1,
++    }
++  )
++    .lean()
++    .exec()
++
++  if (!memberInfo) {
++    return null
++  }
++
++  if (memberInfo.isOwner) {
++    return memberInfo.tokens
++  } else if (memberInfo.hasTokenReadOnlyAccess) {
++    return {
++      readOnly: memberInfo.tokens.readOnly,
++    }
++  } else {
++    return {}
++  }
++}
++
+ async function getProjectsUserIsMemberOf(userId, fields) {
+   const limit = pLimit(2)
+   const [readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly] =
+--- services/web/app/src/Features/Collaborators/CollaboratorsRouter.js
++++ services/web/app/src/Features/Collaborators/CollaboratorsRouter.js
+@@ -22,6 +22,10 @@ const rateLimiters = {
+     points: 200,
+     duration: 60 * 10,
+   }),
++  getProjectTokens: new RateLimiter('get-project-tokens', {
++    points: 200,
++    duration: 60 * 10,
++  }),
+ }
+
+ module.exports = {
+@@ -139,5 +143,12 @@ module.exports = {
+       CollaboratorsInviteController.acceptInvite,
+       AnalyticsRegistrationSourceMiddleware.clearSource()
+     )
++
++    webRouter.get(
++      '/project/:Project_id/tokens',
++      RateLimiterMiddleware.rateLimit(rateLimiters.getProjectTokens),
++      AuthorizationMiddleware.ensureUserCanReadProject,
++      CollaboratorsController.getShareTokens
++    )
+   },
+ }
+--- services/web/app/src/Features/Editor/EditorController.js
++++ services/web/app/src/Features/Editor/EditorController.js
+@@ -581,20 +581,7 @@ const EditorController = {
+           { newAccessLevel }
+         )
+         if (newAccessLevel === PublicAccessLevels.TOKEN_BASED) {
+-          ProjectDetailsHandler.ensureTokensArePresent(
+-            projectId,
+-            function (err, tokens) {
+-              if (err) {
+-                return callback(err)
+-              }
+-              EditorRealTimeController.emitToRoom(
+-                projectId,
+-                'project:tokens:changed',
+-                { tokens }
+-              )
+-              callback()
+-            }
+-          )
++          ProjectDetailsHandler.ensureTokensArePresent(projectId, callback)
+         } else {
+           callback()
+         }
+--- services/web/app/src/Features/Editor/EditorHttpController.js
++++ services/web/app/src/Features/Editor/EditorHttpController.js
+@@ -67,8 +67,6 @@ async function joinProject(req, res, next) {
+   if (!project) {
+     return res.sendStatus(403)
+   }
+-  // Hide access tokens if this is not the project owner
+-  TokenAccessHandler.protectTokens(project, privilegeLevel)
+   // Hide sensitive data if the user is restricted
+   if (isRestrictedUser) {
+     project.owner = { _id: project.owner._id }
+--- services/web/app/src/Features/Project/ProjectController.js
++++ services/web/app/src/Features/Project/ProjectController.js
+@@ -343,7 +343,7 @@ const ProjectController = {
+     const userId = SessionManager.getLoggedInUserId(req.session)
+     ProjectGetter.findAllUsersProjects(
+       userId,
+-      'name lastUpdated publicAccesLevel archived trashed owner_ref tokens',
++      'name lastUpdated publicAccesLevel archived trashed owner_ref',
+       (err, projects) => {
+         if (err != null) {
+           return next(err)
+@@ -1072,7 +1072,6 @@ const ProjectController = {
+     // If a project is simultaneously trashed and archived, we will consider it archived but not trashed.
+     const trashed = ProjectHelper.isTrashed(project, userId) && !archived
+
+-    TokenAccessHandler.protectTokens(project, accessLevel)
+     const model = {
+       id: project._id,
+       name: project.name,
+--- services/web/app/src/Features/Project/ProjectDetailsHandler.js
++++ services/web/app/src/Features/Project/ProjectDetailsHandler.js
+@@ -207,14 +207,13 @@ async function ensureTokensArePresent(projectId) {
+     project.tokens.readOnly != null &&
+     project.tokens.readAndWrite != null
+   ) {
+-    return project.tokens
++    return
+   }
+   await _generateTokens(project)
+   await Project.updateOne(
+     { _id: projectId },
+     { $set: { tokens: project.tokens } }
+   ).exec()
+-  return project.tokens
+ }
+
+ async function clearTokens(projectId) {
+--- services/web/app/src/Features/Project/ProjectEditorHandler.js
++++ services/web/app/src/Features/Project/ProjectEditorHandler.js
+@@ -49,7 +49,6 @@ module.exports = ProjectEditorHandler = {
+       ),
+       members: [],
+       invites,
+-      tokens: project.tokens,
+       imageName:
+         project.imageName != null
+           ? Path.basename(project.imageName)
+--- services/web/app/src/Features/TokenAccess/TokenAccessHandler.js
++++ services/web/app/src/Features/TokenAccess/TokenAccessHandler.js
+@@ -246,22 +246,6 @@ const TokenAccessHandler = {
+     })
+   },
+
+-  protectTokens(project, privilegeLevel) {
+-    if (!project || !project.tokens) {
+-      return
+-    }
+-    if (privilegeLevel === PrivilegeLevels.OWNER) {
+-      return
+-    }
+-    if (privilegeLevel !== PrivilegeLevels.READ_AND_WRITE) {
+-      project.tokens.readAndWrite = ''
+-      project.tokens.readAndWritePrefix = ''
+-    }
+-    if (privilegeLevel !== PrivilegeLevels.READ_ONLY) {
+-      project.tokens.readOnly = ''
+-    }
+-  },
+-
+   getV1DocPublishedInfo(token, callback) {
+     // default to allowing access
+     if (!Settings.apis.v1 || !Settings.apis.v1.url) {
+@@ -304,7 +288,6 @@ TokenAccessHandler.promises = promisifyAll(TokenAccessHandler, {
+     '_projectFindOne',
+     'grantSessionTokenAccess',
+     'getRequestToken',
+-    'protectTokens',
+   ],
+   multiResult: {
+     validateTokenForAnonymousAccess: ['isValidReadAndWrite', 'isValidReadOnly'],
+--- services/web/frontend/js/features/share-project-modal/components/link-sharing.js
++++ services/web/frontend/js/features/share-project-modal/components/link-sharing.js
+@@ -1,4 +1,4 @@
+-import { useCallback, useState } from 'react'
++import { useCallback, useState, useEffect } from 'react'
+ import PropTypes from 'prop-types'
+ import { Button, Col, Row } from 'react-bootstrap'
+ import { Trans } from 'react-i18next'
+@@ -10,6 +10,8 @@ import CopyLink from '../../../shared/components/copy-link'
+ import { useProjectContext } from '../../../shared/context/project-context'
+ import * as eventTracking from '../../../infrastructure/event-tracking'
+ import { useUserContext } from '../../../shared/context/user-context'
++import { getJSON } from '../../../infrastructure/fetch-json'
++import useAbortController from '../../../shared/hooks/use-abort-controller'
+
+ export default function LinkSharing({ canAddCollaborators }) {
+   const [inflight, setInflight] = useState(false)
+@@ -27,8 +29,7 @@ export default function LinkSharing({ canAddCollaborators }) {
+       )
+         .then(() => {
+           // NOTE: not calling `updateProject` here as it receives data via
+-          // project:publicAccessLevel:changed and project:tokens:changed
+-          // over the websocket connection
++          // project:publicAccessLevel:changed over the websocket connection
+           // TODO: eventTracking.sendMB('project-make-token-based') when newPublicAccessLevel is 'tokenBased'
+         })
+         .finally(() => {
+@@ -106,7 +107,17 @@ PrivateSharing.propTypes = {
+ }
+
+ function TokenBasedSharing({ setAccessLevel, inflight, canAddCollaborators }) {
+-  const { tokens } = useProjectContext()
++  const { _id: projectId } = useProjectContext()
++
++  const [tokens, setTokens] = useState(null)
++
++  const { signal } = useAbortController()
++
++  useEffect(() => {
++    getJSON(`/project/${projectId}/tokens`, { signal })
++      .then(data => setTokens(data))
++      .catch(error => console.error(error))
++  }, [projectId, signal])
+
+   return (
+     <Row className="public-access-level">
+@@ -194,7 +205,17 @@ LegacySharing.propTypes = {
+ }
+
+ export function ReadOnlyTokenLink() {
+-  const { tokens } = useProjectContext()
++  const { _id: projectId } = useProjectContext()
++
++  const [tokens, setTokens] = useState(null)
++
++  const { signal } = useAbortController()
++
++  useEffect(() => {
++    getJSON(`/project/${projectId}/tokens`, { signal })
++      .then(data => setTokens(data))
++      .catch(error => console.error(error))
++  }, [projectId, signal])
+
+   return (
+     <Row className="public-access-level">
+--- services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
++++ services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
+@@ -31,16 +31,6 @@ export default App.controller(
+       })
+     }
+
+-    /* tokens */
+-
+-    ide.socket.on('project:tokens:changed', data => {
+-      if (data.tokens != null) {
+-        $scope.$applyAsync(() => {
+-          $scope.project.tokens = data.tokens
+-        })
+-      }
+-    })
+-
+     ide.socket.on('project:membership:changed', data => {
+       if (data.members) {
+         listProjectMembers($scope.project._id)
+--- services/web/frontend/js/shared/context/mock/mock-ide.js
++++ services/web/frontend/js/shared/context/mock/mock-ide.js
+@@ -27,10 +27,6 @@ export const getMockIde = () => {
+           zotero: false,
+         },
+         publicAccessLevel: '',
+-        tokens: {
+-          readOnly: '',
+-          readAndWrite: '',
+-        },
+         owner: {
+           _id: '',
+           email: '',
+--- services/web/frontend/js/shared/context/project-context.js
++++ services/web/frontend/js/shared/context/project-context.js
+@@ -28,10 +28,6 @@ export const projectShape = {
+     versioning: PropTypes.bool,
+   }),
+   publicAccessLevel: PropTypes.string,
+-  tokens: PropTypes.shape({
+-    readOnly: PropTypes.string,
+-    readAndWrite: PropTypes.string,
+-  }),
+   owner: PropTypes.shape({
+     _id: PropTypes.string.isRequired,
+     email: PropTypes.string.isRequired,
+@@ -81,7 +77,6 @@ export function ProjectProvider({ children }) {
+     invites,
+     features,
+     publicAccesLevel: publicAccessLevel,
+-    tokens,
+     owner,
+   } = project || projectFallback
+
+@@ -94,7 +89,6 @@ export function ProjectProvider({ children }) {
+       invites,
+       features,
+       publicAccessLevel,
+-      tokens,
+       owner,
+     }
+   }, [
+@@ -105,7 +99,6 @@ export function ProjectProvider({ children }) {
+     invites,
+     features,
+     publicAccessLevel,
+-    tokens,
+     owner,
+   ])