TokenAccessController.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. const AuthenticationController = require('../Authentication/AuthenticationController')
  2. const SessionManager = require('../Authentication/SessionManager')
  3. const TokenAccessHandler = require('./TokenAccessHandler')
  4. const Errors = require('../Errors/Errors')
  5. const logger = require('@overleaf/logger')
  6. const settings = require('@overleaf/settings')
  7. const OError = require('@overleaf/o-error')
  8. const { expressify } = require('@overleaf/promise-utils')
  9. const AuthorizationManager = require('../Authorization/AuthorizationManager')
  10. const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
  11. const {
  12. handleAdminDomainRedirect,
  13. } = require('../Authorization/AuthorizationMiddleware')
  14. const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
  15. const orderedPrivilegeLevels = [
  16. PrivilegeLevels.NONE,
  17. PrivilegeLevels.READ_ONLY,
  18. PrivilegeLevels.READ_AND_WRITE,
  19. PrivilegeLevels.OWNER,
  20. ]
  21. async function _userAlreadyHasHigherPrivilege(userId, projectId, tokenType) {
  22. if (!Object.values(TokenAccessHandler.TOKEN_TYPES).includes(tokenType)) {
  23. throw new Error('bad token type')
  24. }
  25. if (!userId) {
  26. return false
  27. }
  28. const privilegeLevel =
  29. await AuthorizationManager.promises.getPrivilegeLevelForProject(
  30. userId,
  31. projectId
  32. )
  33. return (
  34. orderedPrivilegeLevels.indexOf(privilegeLevel) >=
  35. orderedPrivilegeLevels.indexOf(tokenType)
  36. )
  37. }
  38. const makePostUrl = token => {
  39. if (TokenAccessHandler.isReadAndWriteToken(token)) {
  40. return `/${token}/grant`
  41. } else if (TokenAccessHandler.isReadOnlyToken(token)) {
  42. return `/read/${token}/grant`
  43. } else {
  44. throw new Error('invalid token type')
  45. }
  46. }
  47. async function _handleV1Project(token, userId) {
  48. if (!userId) {
  49. return { v1Import: { status: 'mustLogin' } }
  50. } else {
  51. const docInfo = await TokenAccessHandler.promises.getV1DocInfo(
  52. token,
  53. userId
  54. )
  55. // This should not happen anymore, but it does show
  56. // a nice "contact support" message, so it can stay
  57. if (!docInfo) {
  58. return { v1Import: { status: 'cannotImport' } }
  59. }
  60. if (!docInfo.exists) {
  61. return null
  62. }
  63. if (docInfo.exported) {
  64. return null
  65. }
  66. return {
  67. v1Import: {
  68. status: 'canDownloadZip',
  69. projectId: token,
  70. hasOwner: docInfo.has_owner,
  71. name: docInfo.name || 'Untitled',
  72. brandInfo: docInfo.brand_info,
  73. },
  74. }
  75. }
  76. }
  77. async function tokenAccessPage(req, res, next) {
  78. const { token } = req.params
  79. if (!TokenAccessHandler.isValidToken(token)) {
  80. return next(new Errors.NotFoundError())
  81. }
  82. if (handleAdminDomainRedirect(req, res)) {
  83. // Admin users do not join the project, but view it on the admin domain.
  84. return
  85. }
  86. try {
  87. if (TokenAccessHandler.isReadOnlyToken(token)) {
  88. const docPublishedInfo =
  89. await TokenAccessHandler.promises.getV1DocPublishedInfo(token)
  90. if (docPublishedInfo.allow === false) {
  91. return res.redirect(302, docPublishedInfo.published_path)
  92. }
  93. }
  94. res.render('project/token/access', {
  95. postUrl: makePostUrl(token),
  96. })
  97. } catch (err) {
  98. return next(
  99. OError.tag(err, 'error while rendering token access page', { token })
  100. )
  101. }
  102. }
  103. async function checkAndGetProjectOrResponseAction(
  104. tokenType,
  105. token,
  106. userId,
  107. tokenHashPrefix,
  108. req,
  109. res,
  110. next
  111. ) {
  112. const isAnonymousUser = !userId
  113. if (
  114. isAnonymousUser &&
  115. tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE &&
  116. !TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED
  117. ) {
  118. logger.warn('[TokenAccess] deny anonymous read-and-write token access')
  119. let projectUrlWithToken = TokenAccessHandler.makeTokenUrl(token)
  120. if (tokenHashPrefix && tokenHashPrefix.startsWith('#')) {
  121. projectUrlWithToken += `${tokenHashPrefix}`
  122. }
  123. AuthenticationController.setRedirectInSession(req, projectUrlWithToken)
  124. return [
  125. null,
  126. () => {
  127. res.json({
  128. redirect: '/restricted',
  129. anonWriteAccessDenied: true,
  130. })
  131. },
  132. { action: 'denied anonymous read-and-write token access' },
  133. ]
  134. }
  135. // Try to get the project, and/or an alternative action to take.
  136. // Returns a tuple of [project, action]
  137. const project = await TokenAccessHandler.promises.getProjectByToken(
  138. tokenType,
  139. token
  140. )
  141. if (!project) {
  142. if (settings.overleaf) {
  143. const v1ImportData = await _handleV1Project(token, userId)
  144. return [
  145. null,
  146. () => {
  147. if (v1ImportData) {
  148. res.json(v1ImportData)
  149. } else {
  150. res.sendStatus(404)
  151. }
  152. },
  153. { action: v1ImportData ? 'import v1' : '404' },
  154. ]
  155. } else {
  156. return [null, null, { action: '404' }]
  157. }
  158. }
  159. const projectId = project._id
  160. const tokenAccessEnabled =
  161. TokenAccessHandler.tokenAccessEnabledForProject(project)
  162. if (isAnonymousUser && tokenAccessEnabled) {
  163. if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
  164. if (TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED) {
  165. logger.debug({ projectId }, 'granting read-write anonymous access')
  166. TokenAccessHandler.grantSessionTokenAccess(req, projectId, token)
  167. return [
  168. null,
  169. () => {
  170. res.json({
  171. redirect: `/project/${projectId}`,
  172. grantAnonymousAccess: tokenType,
  173. })
  174. },
  175. { projectId, action: 'granting read-write anonymous access' },
  176. ]
  177. } else {
  178. // anonymous read-and-write token access should have been denied already
  179. throw new Error(
  180. 'unreachable: anonymous read-and-write token access bug'
  181. )
  182. }
  183. } else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
  184. logger.debug({ projectId }, 'granting read-only anonymous access')
  185. TokenAccessHandler.grantSessionTokenAccess(req, projectId, token)
  186. return [
  187. null,
  188. () => {
  189. res.json({
  190. redirect: `/project/${projectId}`,
  191. grantAnonymousAccess: tokenType,
  192. })
  193. },
  194. { projectId, action: 'granting read-only anonymous access' },
  195. ]
  196. } else {
  197. throw new Error('unreachable')
  198. }
  199. }
  200. const userHasPrivilege = await _userAlreadyHasHigherPrivilege(
  201. userId,
  202. projectId,
  203. tokenType
  204. )
  205. if (userHasPrivilege) {
  206. return [
  207. null,
  208. () => {
  209. res.json({ redirect: `/project/${project._id}`, higherAccess: true })
  210. },
  211. { projectId, action: 'user already has higher or same privilege' },
  212. ]
  213. }
  214. if (!tokenAccessEnabled) {
  215. return [
  216. null,
  217. () => {
  218. next(new Errors.NotFoundError())
  219. },
  220. { projectId, action: 'token access not enabled' },
  221. ]
  222. }
  223. return [project, null, { projectId, action: 'continue' }]
  224. }
  225. async function grantTokenAccessReadAndWrite(req, res, next) {
  226. const { token } = req.params
  227. const { confirmedByUser, tokenHashPrefix } = req.body
  228. const userId = SessionManager.getLoggedInUserId(req.session)
  229. if (!TokenAccessHandler.isReadAndWriteToken(token)) {
  230. return res.sendStatus(400)
  231. }
  232. const tokenType = TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE
  233. try {
  234. const [project, action, logData] = await checkAndGetProjectOrResponseAction(
  235. tokenType,
  236. token,
  237. userId,
  238. tokenHashPrefix,
  239. req,
  240. res,
  241. next
  242. )
  243. TokenAccessHandler.checkTokenHashPrefix(
  244. token,
  245. tokenHashPrefix,
  246. tokenType,
  247. userId,
  248. logData
  249. )
  250. if (action) {
  251. return action()
  252. }
  253. if (!project) {
  254. return next(new Errors.NotFoundError())
  255. }
  256. if (!confirmedByUser) {
  257. return res.json({
  258. requireAccept: {
  259. projectName: project.name,
  260. },
  261. })
  262. }
  263. if (!project.tokenAccessReadAndWrite_refs.some(id => id.equals(userId))) {
  264. await ProjectAuditLogHandler.promises.addEntry(
  265. project._id,
  266. 'join-via-token',
  267. userId,
  268. req.ip,
  269. { privileges: 'readAndWrite' }
  270. )
  271. }
  272. await TokenAccessHandler.promises.addReadAndWriteUserToProject(
  273. userId,
  274. project._id
  275. )
  276. return res.json({
  277. redirect: `/project/${project._id}`,
  278. tokenAccessGranted: tokenType,
  279. })
  280. } catch (err) {
  281. return next(
  282. OError.tag(
  283. err,
  284. 'error while trying to grant read-and-write token access',
  285. { token }
  286. )
  287. )
  288. }
  289. }
  290. async function grantTokenAccessReadOnly(req, res, next) {
  291. const { token } = req.params
  292. const { confirmedByUser, tokenHashPrefix } = req.body
  293. const userId = SessionManager.getLoggedInUserId(req.session)
  294. if (!TokenAccessHandler.isReadOnlyToken(token)) {
  295. return res.sendStatus(400)
  296. }
  297. const tokenType = TokenAccessHandler.TOKEN_TYPES.READ_ONLY
  298. const docPublishedInfo =
  299. await TokenAccessHandler.promises.getV1DocPublishedInfo(token)
  300. if (docPublishedInfo.allow === false) {
  301. return res.json({ redirect: docPublishedInfo.published_path })
  302. }
  303. try {
  304. const [project, action, logData] = await checkAndGetProjectOrResponseAction(
  305. tokenType,
  306. token,
  307. userId,
  308. tokenHashPrefix,
  309. req,
  310. res,
  311. next
  312. )
  313. TokenAccessHandler.checkTokenHashPrefix(
  314. token,
  315. tokenHashPrefix,
  316. tokenType,
  317. userId,
  318. logData
  319. )
  320. if (action) {
  321. return action()
  322. }
  323. if (!project) {
  324. return next(new Errors.NotFoundError())
  325. }
  326. if (!confirmedByUser) {
  327. return res.json({
  328. requireAccept: {
  329. projectName: project.name,
  330. },
  331. })
  332. }
  333. if (!project.tokenAccessReadOnly_refs.some(id => id.equals(userId))) {
  334. await ProjectAuditLogHandler.promises.addEntry(
  335. project._id,
  336. 'join-via-token',
  337. userId,
  338. req.ip,
  339. { privileges: 'readOnly' }
  340. )
  341. }
  342. await TokenAccessHandler.promises.addReadOnlyUserToProject(
  343. userId,
  344. project._id
  345. )
  346. return res.json({
  347. redirect: `/project/${project._id}`,
  348. tokenAccessGranted: tokenType,
  349. })
  350. } catch (err) {
  351. return next(
  352. OError.tag(err, 'error while trying to grant read-only token access', {
  353. token,
  354. })
  355. )
  356. }
  357. }
  358. module.exports = {
  359. READ_ONLY_TOKEN_PATTERN: TokenAccessHandler.READ_ONLY_TOKEN_PATTERN,
  360. READ_AND_WRITE_TOKEN_PATTERN: TokenAccessHandler.READ_AND_WRITE_TOKEN_PATTERN,
  361. tokenAccessPage: expressify(tokenAccessPage),
  362. grantTokenAccessReadOnly: expressify(grantTokenAccessReadOnly),
  363. grantTokenAccessReadAndWrite: expressify(grantTokenAccessReadAndWrite),
  364. }