TokenAccessController.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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. req,
  108. res,
  109. next
  110. ) {
  111. const isAnonymousUser = !userId
  112. if (
  113. isAnonymousUser &&
  114. tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE &&
  115. !TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED
  116. ) {
  117. logger.warn('[TokenAccess] deny anonymous read-and-write token access')
  118. AuthenticationController.setRedirectInSession(
  119. req,
  120. TokenAccessHandler.makeTokenUrl(token)
  121. )
  122. return [
  123. null,
  124. () => {
  125. res.json({
  126. redirect: '/restricted',
  127. anonWriteAccessDenied: true,
  128. })
  129. },
  130. ]
  131. }
  132. // Try to get the project, and/or an alternative action to take.
  133. // Returns a tuple of [project, action]
  134. const project = await TokenAccessHandler.promises.getProjectByToken(
  135. tokenType,
  136. token
  137. )
  138. if (!project) {
  139. if (settings.overleaf) {
  140. const v1ImportData = await _handleV1Project(token, userId)
  141. return [
  142. null,
  143. () => {
  144. if (v1ImportData) {
  145. res.json(v1ImportData)
  146. } else {
  147. res.sendStatus(404)
  148. }
  149. },
  150. ]
  151. } else {
  152. return [null, null]
  153. }
  154. }
  155. const projectId = project._id
  156. const tokenAccessEnabled =
  157. TokenAccessHandler.tokenAccessEnabledForProject(project)
  158. if (isAnonymousUser && tokenAccessEnabled) {
  159. if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
  160. if (TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED) {
  161. logger.debug({ projectId }, 'granting read-write anonymous access')
  162. TokenAccessHandler.grantSessionTokenAccess(req, projectId, token)
  163. return [
  164. null,
  165. () => {
  166. res.json({
  167. redirect: `/project/${projectId}`,
  168. grantAnonymousAccess: tokenType,
  169. })
  170. },
  171. ]
  172. } else {
  173. // anonymous read-and-write token access should have been denied already
  174. throw new Error(
  175. 'unreachable: anonymous read-and-write token access bug'
  176. )
  177. }
  178. } else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
  179. logger.debug({ projectId }, 'granting read-only anonymous access')
  180. TokenAccessHandler.grantSessionTokenAccess(req, projectId, token)
  181. return [
  182. null,
  183. () => {
  184. res.json({
  185. redirect: `/project/${projectId}`,
  186. grantAnonymousAccess: tokenType,
  187. })
  188. },
  189. ]
  190. } else {
  191. throw new Error('unreachable')
  192. }
  193. }
  194. const userHasPrivilege = await _userAlreadyHasHigherPrivilege(
  195. userId,
  196. projectId,
  197. tokenType
  198. )
  199. if (userHasPrivilege) {
  200. return [
  201. null,
  202. () => {
  203. res.json({ redirect: `/project/${project._id}`, higherAccess: true })
  204. },
  205. ]
  206. }
  207. if (!tokenAccessEnabled) {
  208. return [
  209. null,
  210. () => {
  211. next(new Errors.NotFoundError())
  212. },
  213. ]
  214. }
  215. return [project, null]
  216. }
  217. async function grantTokenAccessReadAndWrite(req, res, next) {
  218. const { token } = req.params
  219. const { confirmedByUser, tokenHashPrefix } = req.body
  220. const userId = SessionManager.getLoggedInUserId(req.session)
  221. if (!TokenAccessHandler.isReadAndWriteToken(token)) {
  222. return res.sendStatus(400)
  223. }
  224. const tokenType = TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE
  225. try {
  226. const [project, action] = await checkAndGetProjectOrResponseAction(
  227. tokenType,
  228. token,
  229. userId,
  230. req,
  231. res,
  232. next
  233. )
  234. TokenAccessHandler.checkTokenHashPrefix(
  235. token,
  236. tokenHashPrefix,
  237. tokenType,
  238. userId,
  239. project?._id
  240. )
  241. if (action) {
  242. return action()
  243. }
  244. if (!project) {
  245. return next(new Errors.NotFoundError())
  246. }
  247. if (!confirmedByUser) {
  248. return res.json({
  249. requireAccept: {
  250. projectName: project.name,
  251. },
  252. })
  253. }
  254. if (!project.tokenAccessReadAndWrite_refs.some(id => id.equals(userId))) {
  255. await ProjectAuditLogHandler.promises.addEntry(
  256. project._id,
  257. 'join-via-token',
  258. userId,
  259. req.ip,
  260. { privileges: 'readAndWrite' }
  261. )
  262. }
  263. await TokenAccessHandler.promises.addReadAndWriteUserToProject(
  264. userId,
  265. project._id
  266. )
  267. return res.json({
  268. redirect: `/project/${project._id}`,
  269. tokenAccessGranted: tokenType,
  270. })
  271. } catch (err) {
  272. return next(
  273. OError.tag(
  274. err,
  275. 'error while trying to grant read-and-write token access',
  276. { token }
  277. )
  278. )
  279. }
  280. }
  281. async function grantTokenAccessReadOnly(req, res, next) {
  282. const { token } = req.params
  283. const { confirmedByUser, tokenHashPrefix } = req.body
  284. const userId = SessionManager.getLoggedInUserId(req.session)
  285. if (!TokenAccessHandler.isReadOnlyToken(token)) {
  286. return res.sendStatus(400)
  287. }
  288. const tokenType = TokenAccessHandler.TOKEN_TYPES.READ_ONLY
  289. const docPublishedInfo =
  290. await TokenAccessHandler.promises.getV1DocPublishedInfo(token)
  291. if (docPublishedInfo.allow === false) {
  292. return res.json({ redirect: docPublishedInfo.published_path })
  293. }
  294. try {
  295. const [project, action] = await checkAndGetProjectOrResponseAction(
  296. tokenType,
  297. token,
  298. userId,
  299. req,
  300. res,
  301. next
  302. )
  303. TokenAccessHandler.checkTokenHashPrefix(
  304. token,
  305. tokenHashPrefix,
  306. tokenType,
  307. userId,
  308. project?._id
  309. )
  310. if (action) {
  311. return action()
  312. }
  313. if (!project) {
  314. return next(new Errors.NotFoundError())
  315. }
  316. if (!confirmedByUser) {
  317. return res.json({
  318. requireAccept: {
  319. projectName: project.name,
  320. },
  321. })
  322. }
  323. if (!project.tokenAccessReadOnly_refs.some(id => id.equals(userId))) {
  324. await ProjectAuditLogHandler.promises.addEntry(
  325. project._id,
  326. 'join-via-token',
  327. userId,
  328. req.ip,
  329. { privileges: 'readOnly' }
  330. )
  331. }
  332. await TokenAccessHandler.promises.addReadOnlyUserToProject(
  333. userId,
  334. project._id
  335. )
  336. return res.json({
  337. redirect: `/project/${project._id}`,
  338. tokenAccessGranted: tokenType,
  339. })
  340. } catch (err) {
  341. return next(
  342. OError.tag(err, 'error while trying to grant read-only token access', {
  343. token,
  344. })
  345. )
  346. }
  347. }
  348. module.exports = {
  349. READ_ONLY_TOKEN_PATTERN: TokenAccessHandler.READ_ONLY_TOKEN_PATTERN,
  350. READ_AND_WRITE_TOKEN_PATTERN: TokenAccessHandler.READ_AND_WRITE_TOKEN_PATTERN,
  351. tokenAccessPage: expressify(tokenAccessPage),
  352. grantTokenAccessReadOnly: expressify(grantTokenAccessReadOnly),
  353. grantTokenAccessReadAndWrite: expressify(grantTokenAccessReadAndWrite),
  354. }