TokenAccessHandler.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. const { Project } = require('../../models/Project')
  2. const PublicAccessLevels = require('../Authorization/PublicAccessLevels')
  3. const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
  4. const { ObjectId } = require('mongodb')
  5. const Settings = require('@overleaf/settings')
  6. const logger = require('@overleaf/logger')
  7. const V1Api = require('../V1/V1Api')
  8. const crypto = require('crypto')
  9. const { promisifyAll } = require('../../util/promises')
  10. const Analytics = require('../Analytics/AnalyticsManager')
  11. const READ_AND_WRITE_TOKEN_PATTERN = '([0-9]+[a-z]{6,12})'
  12. const READ_ONLY_TOKEN_PATTERN = '([a-z]{12})'
  13. const TokenAccessHandler = {
  14. TOKEN_TYPES: {
  15. READ_ONLY: PrivilegeLevels.READ_ONLY,
  16. READ_AND_WRITE: PrivilegeLevels.READ_AND_WRITE,
  17. },
  18. ANONYMOUS_READ_AND_WRITE_ENABLED:
  19. Settings.allowAnonymousReadAndWriteSharing === true,
  20. READ_AND_WRITE_TOKEN_PATTERN,
  21. READ_AND_WRITE_TOKEN_REGEX: new RegExp(`^${READ_AND_WRITE_TOKEN_PATTERN}$`),
  22. READ_AND_WRITE_URL_REGEX: new RegExp(`^/${READ_AND_WRITE_TOKEN_PATTERN}$`),
  23. READ_ONLY_TOKEN_PATTERN,
  24. READ_ONLY_TOKEN_REGEX: new RegExp(`^${READ_ONLY_TOKEN_PATTERN}$`),
  25. READ_ONLY_URL_REGEX: new RegExp(`^/read/${READ_ONLY_TOKEN_PATTERN}$`),
  26. makeReadAndWriteTokenUrl(token) {
  27. return `/${token}`
  28. },
  29. makeReadOnlyTokenUrl(token) {
  30. return `/read/${token}`
  31. },
  32. makeTokenUrl(token) {
  33. const tokenType = TokenAccessHandler.getTokenType(token)
  34. if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
  35. return TokenAccessHandler.makeReadAndWriteTokenUrl(token)
  36. } else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
  37. return TokenAccessHandler.makeReadOnlyTokenUrl(token)
  38. } else {
  39. throw new Error('invalid token type')
  40. }
  41. },
  42. getTokenType(token) {
  43. if (!token) {
  44. return null
  45. }
  46. if (token.match(`^${TokenAccessHandler.READ_ONLY_TOKEN_PATTERN}$`)) {
  47. return TokenAccessHandler.TOKEN_TYPES.READ_ONLY
  48. } else if (
  49. token.match(`^${TokenAccessHandler.READ_AND_WRITE_TOKEN_PATTERN}$`)
  50. ) {
  51. return TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE
  52. }
  53. return null
  54. },
  55. isReadOnlyToken(token) {
  56. return (
  57. TokenAccessHandler.getTokenType(token) ===
  58. TokenAccessHandler.TOKEN_TYPES.READ_ONLY
  59. )
  60. },
  61. isReadAndWriteToken(token) {
  62. return (
  63. TokenAccessHandler.getTokenType(token) ===
  64. TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE
  65. )
  66. },
  67. isValidToken(token) {
  68. return TokenAccessHandler.getTokenType(token) != null
  69. },
  70. tokenAccessEnabledForProject(project) {
  71. return project.publicAccesLevel === PublicAccessLevels.TOKEN_BASED
  72. },
  73. _projectFindOne(query, callback) {
  74. Project.findOne(
  75. query,
  76. {
  77. _id: 1,
  78. tokens: 1,
  79. publicAccesLevel: 1,
  80. owner_ref: 1,
  81. name: 1,
  82. },
  83. callback
  84. )
  85. },
  86. getProjectByReadOnlyToken(token, callback) {
  87. TokenAccessHandler._projectFindOne({ 'tokens.readOnly': token }, callback)
  88. },
  89. _extractNumericPrefix(token) {
  90. return token.match(/^(\d+)\w+/)
  91. },
  92. _extractStringSuffix(token) {
  93. return token.match(/^\d+(\w+)/)
  94. },
  95. getProjectByReadAndWriteToken(token, callback) {
  96. const numericPrefixMatch = TokenAccessHandler._extractNumericPrefix(token)
  97. if (!numericPrefixMatch) {
  98. return callback(null, null)
  99. }
  100. const numerics = numericPrefixMatch[1]
  101. TokenAccessHandler._projectFindOne(
  102. {
  103. 'tokens.readAndWritePrefix': numerics,
  104. },
  105. function (err, project) {
  106. if (err != null) {
  107. return callback(err)
  108. }
  109. if (project == null) {
  110. return callback(null, null)
  111. }
  112. try {
  113. if (
  114. !crypto.timingSafeEqual(
  115. Buffer.from(token),
  116. Buffer.from(project.tokens.readAndWrite)
  117. )
  118. ) {
  119. logger.err(
  120. { projectId: project._id },
  121. 'read-and-write token match on numeric section, but not on full token'
  122. )
  123. callback(null, null)
  124. } else {
  125. callback(null, project)
  126. }
  127. } catch (error) {
  128. err = error
  129. logger.err(
  130. { projectId: project._id, cryptoErr: err },
  131. 'error comparing tokens'
  132. )
  133. callback(null, null)
  134. }
  135. }
  136. )
  137. },
  138. getProjectByToken(tokenType, token, callback) {
  139. if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
  140. TokenAccessHandler.getProjectByReadOnlyToken(token, callback)
  141. } else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
  142. TokenAccessHandler.getProjectByReadAndWriteToken(token, callback)
  143. } else {
  144. callback(new Error('invalid token type'))
  145. }
  146. },
  147. addReadOnlyUserToProject(userId, projectId, callback) {
  148. userId = ObjectId(userId.toString())
  149. projectId = ObjectId(projectId.toString())
  150. Analytics.recordEventForUser(userId, 'project-joined', {
  151. mode: 'read-only',
  152. })
  153. Project.updateOne(
  154. {
  155. _id: projectId,
  156. },
  157. {
  158. $addToSet: { tokenAccessReadOnly_refs: userId },
  159. },
  160. callback
  161. )
  162. },
  163. addReadAndWriteUserToProject(userId, projectId, callback) {
  164. userId = ObjectId(userId.toString())
  165. projectId = ObjectId(projectId.toString())
  166. Analytics.recordEventForUser(userId, 'project-joined', {
  167. mode: 'read-write',
  168. })
  169. Project.updateOne(
  170. {
  171. _id: projectId,
  172. },
  173. {
  174. $addToSet: { tokenAccessReadAndWrite_refs: userId },
  175. },
  176. callback
  177. )
  178. },
  179. grantSessionTokenAccess(req, projectId, token) {
  180. if (!req.session) {
  181. return
  182. }
  183. if (!req.session.anonTokenAccess) {
  184. req.session.anonTokenAccess = {}
  185. }
  186. req.session.anonTokenAccess[projectId.toString()] = token
  187. },
  188. getRequestToken(req, projectId) {
  189. const token =
  190. (req.session &&
  191. req.session.anonTokenAccess &&
  192. req.session.anonTokenAccess[projectId.toString()]) ||
  193. req.headers['x-sl-anonymous-access-token']
  194. return token
  195. },
  196. validateTokenForAnonymousAccess(projectId, token, callback) {
  197. if (!token) {
  198. return callback(null, false, false)
  199. }
  200. const tokenType = TokenAccessHandler.getTokenType(token)
  201. if (!tokenType) {
  202. return callback(new Error('invalid token type'))
  203. }
  204. TokenAccessHandler.getProjectByToken(tokenType, token, (err, project) => {
  205. if (err) {
  206. return callback(err)
  207. }
  208. if (
  209. !project ||
  210. !TokenAccessHandler.tokenAccessEnabledForProject(project) ||
  211. project._id.toString() !== projectId.toString()
  212. ) {
  213. return callback(null, false, false)
  214. }
  215. // TODO: think about cleaning up this interface and its usage in AuthorizationManager
  216. callback(
  217. null,
  218. tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE &&
  219. TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED,
  220. tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY
  221. )
  222. })
  223. },
  224. protectTokens(project, privilegeLevel) {
  225. if (!project || !project.tokens) {
  226. return
  227. }
  228. if (privilegeLevel === PrivilegeLevels.OWNER) {
  229. return
  230. }
  231. if (privilegeLevel !== PrivilegeLevels.READ_AND_WRITE) {
  232. project.tokens.readAndWrite = ''
  233. project.tokens.readAndWritePrefix = ''
  234. }
  235. if (privilegeLevel !== PrivilegeLevels.READ_ONLY) {
  236. project.tokens.readOnly = ''
  237. }
  238. },
  239. getV1DocPublishedInfo(token, callback) {
  240. // default to allowing access
  241. if (!Settings.apis.v1 || !Settings.apis.v1.url) {
  242. return callback(null, { allow: true })
  243. }
  244. V1Api.request(
  245. { url: `/api/v1/sharelatex/docs/${token}/is_published` },
  246. function (err, response, body) {
  247. if (err != null) {
  248. return callback(err)
  249. }
  250. callback(null, body)
  251. }
  252. )
  253. },
  254. getV1DocInfo(token, v2UserId, callback) {
  255. if (!Settings.apis || !Settings.apis.v1) {
  256. return callback(null, {
  257. exists: true,
  258. exported: false,
  259. })
  260. }
  261. const v1Url = `/api/v1/sharelatex/docs/${token}/info`
  262. V1Api.request({ url: v1Url }, function (err, response, body) {
  263. if (err != null) {
  264. return callback(err)
  265. }
  266. callback(null, body)
  267. })
  268. },
  269. }
  270. TokenAccessHandler.promises = promisifyAll(TokenAccessHandler, {
  271. without: [
  272. 'getTokenType',
  273. 'tokenAccessEnabledForProject',
  274. '_extractNumericPrefix',
  275. '_extractStringSuffix',
  276. '_projectFindOne',
  277. 'grantSessionTokenAccess',
  278. 'getRequestToken',
  279. 'protectTokens',
  280. ],
  281. multiResult: {
  282. validateTokenForAnonymousAccess: ['isValidReadAndWrite', 'isValidReadOnly'],
  283. },
  284. })
  285. module.exports = TokenAccessHandler