pr_13485.patch 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. --- services/web/app/src/Features/Collaborators/CollaboratorsController.js
  2. +++ services/web/app/src/Features/Collaborators/CollaboratorsController.js
  3. @@ -11,6 +11,7 @@ const Errors = require('../Errors/Errors')
  4. const logger = require('@overleaf/logger')
  5. const { expressify } = require('../../util/promises')
  6. const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
  7. +const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
  8. module.exports = {
  9. removeUserFromProject: expressify(removeUserFromProject),
  10. @@ -18,6 +19,7 @@ module.exports = {
  11. getAllMembers: expressify(getAllMembers),
  12. setCollaboratorInfo: expressify(setCollaboratorInfo),
  13. transferOwnership: expressify(transferOwnership),
  14. + getShareTokens: expressify(getShareTokens),
  15. }
  16. async function removeUserFromProject(req, res, next) {
  17. @@ -114,3 +116,37 @@ async function _removeUserIdFromProject(projectId, userId) {
  18. )
  19. await TagsHandler.promises.removeProjectFromAllTags(userId, projectId)
  20. }
  21. +
  22. +async function getShareTokens(req, res) {
  23. + const projectId = req.params.Project_id
  24. + const userId = SessionManager.getLoggedInUserId(req.session)
  25. +
  26. + let tokens
  27. + if (userId) {
  28. + tokens = await CollaboratorsGetter.promises.getPublicShareTokens(
  29. + ObjectId(userId),
  30. + ObjectId(projectId)
  31. + )
  32. + } else {
  33. + // anonymous access, the token is already available in the session
  34. + const readOnly = TokenAccessHandler.getRequestToken(req, projectId)
  35. + tokens = { readOnly }
  36. + }
  37. + if (!tokens) {
  38. + return res.sendStatus(403)
  39. + }
  40. +
  41. + if (tokens.readOnly || tokens.readAndWrite) {
  42. + logger.info(
  43. + {
  44. + projectId,
  45. + userId: userId || 'anonymous',
  46. + ip: req.ip,
  47. + tokens: Object.keys(tokens),
  48. + },
  49. + 'project tokens accessed'
  50. + )
  51. + }
  52. +
  53. + res.json(tokens)
  54. +}
  55. --- services/web/app/src/Features/Collaborators/CollaboratorsGetter.js
  56. +++ services/web/app/src/Features/Collaborators/CollaboratorsGetter.js
  57. @@ -25,6 +25,7 @@ module.exports = {
  58. getInvitedCollaboratorCount: callbackify(getInvitedCollaboratorCount),
  59. getProjectsUserIsMemberOf: callbackify(getProjectsUserIsMemberOf),
  60. isUserInvitedMemberOfProject: callbackify(isUserInvitedMemberOfProject),
  61. + getPublicShareTokens: callbackify(getPublicShareTokens),
  62. userIsTokenMember: callbackify(userIsTokenMember),
  63. getAllInvitedMembers: callbackify(getAllInvitedMembers),
  64. promises: {
  65. @@ -37,6 +38,7 @@ module.exports = {
  66. getInvitedCollaboratorCount,
  67. getProjectsUserIsMemberOf,
  68. isUserInvitedMemberOfProject,
  69. + getPublicShareTokens,
  70. userIsTokenMember,
  71. getAllInvitedMembers,
  72. },
  73. @@ -133,6 +135,40 @@ async function isUserInvitedMemberOfProject(userId, projectId) {
  74. return false
  75. }
  76. +async function getPublicShareTokens(userId, projectId) {
  77. + const memberInfo = await Project.findOne(
  78. + {
  79. + _id: projectId,
  80. + },
  81. + {
  82. + isOwner: { $eq: ['$owner_ref', userId] },
  83. + hasTokenReadOnlyAccess: {
  84. + $and: [
  85. + { $in: [userId, '$tokenAccessReadOnly_refs'] },
  86. + { $eq: ['$publicAccesLevel', PublicAccessLevels.TOKEN_BASED] },
  87. + ],
  88. + },
  89. + tokens: 1,
  90. + }
  91. + )
  92. + .lean()
  93. + .exec()
  94. +
  95. + if (!memberInfo) {
  96. + return null
  97. + }
  98. +
  99. + if (memberInfo.isOwner) {
  100. + return memberInfo.tokens
  101. + } else if (memberInfo.hasTokenReadOnlyAccess) {
  102. + return {
  103. + readOnly: memberInfo.tokens.readOnly,
  104. + }
  105. + } else {
  106. + return {}
  107. + }
  108. +}
  109. +
  110. async function getProjectsUserIsMemberOf(userId, fields) {
  111. const limit = pLimit(2)
  112. const [readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly] =
  113. --- services/web/app/src/Features/Collaborators/CollaboratorsRouter.js
  114. +++ services/web/app/src/Features/Collaborators/CollaboratorsRouter.js
  115. @@ -22,6 +22,10 @@ const rateLimiters = {
  116. points: 200,
  117. duration: 60 * 10,
  118. }),
  119. + getProjectTokens: new RateLimiter('get-project-tokens', {
  120. + points: 200,
  121. + duration: 60 * 10,
  122. + }),
  123. }
  124. module.exports = {
  125. @@ -139,5 +143,12 @@ module.exports = {
  126. CollaboratorsInviteController.acceptInvite,
  127. AnalyticsRegistrationSourceMiddleware.clearSource()
  128. )
  129. +
  130. + webRouter.get(
  131. + '/project/:Project_id/tokens',
  132. + RateLimiterMiddleware.rateLimit(rateLimiters.getProjectTokens),
  133. + AuthorizationMiddleware.ensureUserCanReadProject,
  134. + CollaboratorsController.getShareTokens
  135. + )
  136. },
  137. }
  138. --- services/web/app/src/Features/Editor/EditorController.js
  139. +++ services/web/app/src/Features/Editor/EditorController.js
  140. @@ -581,20 +581,7 @@ const EditorController = {
  141. { newAccessLevel }
  142. )
  143. if (newAccessLevel === PublicAccessLevels.TOKEN_BASED) {
  144. - ProjectDetailsHandler.ensureTokensArePresent(
  145. - projectId,
  146. - function (err, tokens) {
  147. - if (err) {
  148. - return callback(err)
  149. - }
  150. - EditorRealTimeController.emitToRoom(
  151. - projectId,
  152. - 'project:tokens:changed',
  153. - { tokens }
  154. - )
  155. - callback()
  156. - }
  157. - )
  158. + ProjectDetailsHandler.ensureTokensArePresent(projectId, callback)
  159. } else {
  160. callback()
  161. }
  162. --- services/web/app/src/Features/Editor/EditorHttpController.js
  163. +++ services/web/app/src/Features/Editor/EditorHttpController.js
  164. @@ -67,8 +67,6 @@ async function joinProject(req, res, next) {
  165. if (!project) {
  166. return res.sendStatus(403)
  167. }
  168. - // Hide access tokens if this is not the project owner
  169. - TokenAccessHandler.protectTokens(project, privilegeLevel)
  170. // Hide sensitive data if the user is restricted
  171. if (isRestrictedUser) {
  172. project.owner = { _id: project.owner._id }
  173. --- services/web/app/src/Features/Project/ProjectController.js
  174. +++ services/web/app/src/Features/Project/ProjectController.js
  175. @@ -343,7 +343,7 @@ const ProjectController = {
  176. const userId = SessionManager.getLoggedInUserId(req.session)
  177. ProjectGetter.findAllUsersProjects(
  178. userId,
  179. - 'name lastUpdated publicAccesLevel archived trashed owner_ref tokens',
  180. + 'name lastUpdated publicAccesLevel archived trashed owner_ref',
  181. (err, projects) => {
  182. if (err != null) {
  183. return next(err)
  184. @@ -1072,7 +1072,6 @@ const ProjectController = {
  185. // If a project is simultaneously trashed and archived, we will consider it archived but not trashed.
  186. const trashed = ProjectHelper.isTrashed(project, userId) && !archived
  187. - TokenAccessHandler.protectTokens(project, accessLevel)
  188. const model = {
  189. id: project._id,
  190. name: project.name,
  191. --- services/web/app/src/Features/Project/ProjectDetailsHandler.js
  192. +++ services/web/app/src/Features/Project/ProjectDetailsHandler.js
  193. @@ -207,14 +207,13 @@ async function ensureTokensArePresent(projectId) {
  194. project.tokens.readOnly != null &&
  195. project.tokens.readAndWrite != null
  196. ) {
  197. - return project.tokens
  198. + return
  199. }
  200. await _generateTokens(project)
  201. await Project.updateOne(
  202. { _id: projectId },
  203. { $set: { tokens: project.tokens } }
  204. ).exec()
  205. - return project.tokens
  206. }
  207. async function clearTokens(projectId) {
  208. --- services/web/app/src/Features/Project/ProjectEditorHandler.js
  209. +++ services/web/app/src/Features/Project/ProjectEditorHandler.js
  210. @@ -49,7 +49,6 @@ module.exports = ProjectEditorHandler = {
  211. ),
  212. members: [],
  213. invites,
  214. - tokens: project.tokens,
  215. imageName:
  216. project.imageName != null
  217. ? Path.basename(project.imageName)
  218. --- services/web/app/src/Features/TokenAccess/TokenAccessHandler.js
  219. +++ services/web/app/src/Features/TokenAccess/TokenAccessHandler.js
  220. @@ -246,22 +246,6 @@ const TokenAccessHandler = {
  221. })
  222. },
  223. - protectTokens(project, privilegeLevel) {
  224. - if (!project || !project.tokens) {
  225. - return
  226. - }
  227. - if (privilegeLevel === PrivilegeLevels.OWNER) {
  228. - return
  229. - }
  230. - if (privilegeLevel !== PrivilegeLevels.READ_AND_WRITE) {
  231. - project.tokens.readAndWrite = ''
  232. - project.tokens.readAndWritePrefix = ''
  233. - }
  234. - if (privilegeLevel !== PrivilegeLevels.READ_ONLY) {
  235. - project.tokens.readOnly = ''
  236. - }
  237. - },
  238. -
  239. getV1DocPublishedInfo(token, callback) {
  240. // default to allowing access
  241. if (!Settings.apis.v1 || !Settings.apis.v1.url) {
  242. @@ -304,7 +288,6 @@ TokenAccessHandler.promises = promisifyAll(TokenAccessHandler, {
  243. '_projectFindOne',
  244. 'grantSessionTokenAccess',
  245. 'getRequestToken',
  246. - 'protectTokens',
  247. ],
  248. multiResult: {
  249. validateTokenForAnonymousAccess: ['isValidReadAndWrite', 'isValidReadOnly'],
  250. --- services/web/frontend/js/features/share-project-modal/components/link-sharing.js
  251. +++ services/web/frontend/js/features/share-project-modal/components/link-sharing.js
  252. @@ -1,4 +1,4 @@
  253. -import { useCallback, useState } from 'react'
  254. +import { useCallback, useState, useEffect } from 'react'
  255. import PropTypes from 'prop-types'
  256. import { Button, Col, Row } from 'react-bootstrap'
  257. import { Trans } from 'react-i18next'
  258. @@ -10,6 +10,8 @@ import CopyLink from '../../../shared/components/copy-link'
  259. import { useProjectContext } from '../../../shared/context/project-context'
  260. import * as eventTracking from '../../../infrastructure/event-tracking'
  261. import { useUserContext } from '../../../shared/context/user-context'
  262. +import { getJSON } from '../../../infrastructure/fetch-json'
  263. +import useAbortController from '../../../shared/hooks/use-abort-controller'
  264. export default function LinkSharing({ canAddCollaborators }) {
  265. const [inflight, setInflight] = useState(false)
  266. @@ -27,8 +29,7 @@ export default function LinkSharing({ canAddCollaborators }) {
  267. )
  268. .then(() => {
  269. // NOTE: not calling `updateProject` here as it receives data via
  270. - // project:publicAccessLevel:changed and project:tokens:changed
  271. - // over the websocket connection
  272. + // project:publicAccessLevel:changed over the websocket connection
  273. // TODO: eventTracking.sendMB('project-make-token-based') when newPublicAccessLevel is 'tokenBased'
  274. })
  275. .finally(() => {
  276. @@ -106,7 +107,17 @@ PrivateSharing.propTypes = {
  277. }
  278. function TokenBasedSharing({ setAccessLevel, inflight, canAddCollaborators }) {
  279. - const { tokens } = useProjectContext()
  280. + const { _id: projectId } = useProjectContext()
  281. +
  282. + const [tokens, setTokens] = useState(null)
  283. +
  284. + const { signal } = useAbortController()
  285. +
  286. + useEffect(() => {
  287. + getJSON(`/project/${projectId}/tokens`, { signal })
  288. + .then(data => setTokens(data))
  289. + .catch(error => console.error(error))
  290. + }, [projectId, signal])
  291. return (
  292. <Row className="public-access-level">
  293. @@ -194,7 +205,17 @@ LegacySharing.propTypes = {
  294. }
  295. export function ReadOnlyTokenLink() {
  296. - const { tokens } = useProjectContext()
  297. + const { _id: projectId } = useProjectContext()
  298. +
  299. + const [tokens, setTokens] = useState(null)
  300. +
  301. + const { signal } = useAbortController()
  302. +
  303. + useEffect(() => {
  304. + getJSON(`/project/${projectId}/tokens`, { signal })
  305. + .then(data => setTokens(data))
  306. + .catch(error => console.error(error))
  307. + }, [projectId, signal])
  308. return (
  309. <Row className="public-access-level">
  310. --- services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
  311. +++ services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
  312. @@ -31,16 +31,6 @@ export default App.controller(
  313. })
  314. }
  315. - /* tokens */
  316. -
  317. - ide.socket.on('project:tokens:changed', data => {
  318. - if (data.tokens != null) {
  319. - $scope.$applyAsync(() => {
  320. - $scope.project.tokens = data.tokens
  321. - })
  322. - }
  323. - })
  324. -
  325. ide.socket.on('project:membership:changed', data => {
  326. if (data.members) {
  327. listProjectMembers($scope.project._id)
  328. --- services/web/frontend/js/shared/context/mock/mock-ide.js
  329. +++ services/web/frontend/js/shared/context/mock/mock-ide.js
  330. @@ -27,10 +27,6 @@ export const getMockIde = () => {
  331. zotero: false,
  332. },
  333. publicAccessLevel: '',
  334. - tokens: {
  335. - readOnly: '',
  336. - readAndWrite: '',
  337. - },
  338. owner: {
  339. _id: '',
  340. email: '',
  341. --- services/web/frontend/js/shared/context/project-context.js
  342. +++ services/web/frontend/js/shared/context/project-context.js
  343. @@ -28,10 +28,6 @@ export const projectShape = {
  344. versioning: PropTypes.bool,
  345. }),
  346. publicAccessLevel: PropTypes.string,
  347. - tokens: PropTypes.shape({
  348. - readOnly: PropTypes.string,
  349. - readAndWrite: PropTypes.string,
  350. - }),
  351. owner: PropTypes.shape({
  352. _id: PropTypes.string.isRequired,
  353. email: PropTypes.string.isRequired,
  354. @@ -81,7 +77,6 @@ export function ProjectProvider({ children }) {
  355. invites,
  356. features,
  357. publicAccesLevel: publicAccessLevel,
  358. - tokens,
  359. owner,
  360. } = project || projectFallback
  361. @@ -94,7 +89,6 @@ export function ProjectProvider({ children }) {
  362. invites,
  363. features,
  364. publicAccessLevel,
  365. - tokens,
  366. owner,
  367. }
  368. }, [
  369. @@ -105,7 +99,6 @@ export function ProjectProvider({ children }) {
  370. invites,
  371. features,
  372. publicAccessLevel,
  373. - tokens,
  374. owner,
  375. ])