mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #13875 from overleaf/msm-hotfix-4-0-4
[CE/SP] Hotfixes 3.5.9 / 4.0.4 GitOrigin-RevId: 89f5b3832b2a069a2ee31b2ddc5cde9a4bbb5464
This commit is contained in:
parent
38d1e749fb
commit
22e232242c
4 changed files with 798 additions and 0 deletions
10
server-ce/hotfix/3.5.9/Dockerfile
Normal file
10
server-ce/hotfix/3.5.9/Dockerfile
Normal file
|
@ -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
server-ce/hotfix/3.5.9/pr_13485.patch
Normal file
389
server-ce/hotfix/3.5.9/pr_13485.patch
Normal file
|
@ -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
server-ce/hotfix/4.0.4/Dockerfile
Normal file
10
server-ce/hotfix/4.0.4/Dockerfile
Normal file
|
@ -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
server-ce/hotfix/4.0.4/pr_13485.patch
Normal file
389
server-ce/hotfix/4.0.4/pr_13485.patch
Normal file
|
@ -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,
|
||||||
|
])
|
Loading…
Reference in a new issue