TokenAccessHandler.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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('logger-sharelatex')
  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. { token },
  121. 'read-and-write token match on numeric section, but not on full token'
  122. )
  123. return callback(null, null)
  124. } else {
  125. return callback(null, project)
  126. }
  127. } catch (error) {
  128. err = error
  129. logger.err({ token, cryptoErr: err }, 'error comparing tokens')
  130. return callback(null, null)
  131. }
  132. }
  133. )
  134. },
  135. getProjectByToken(tokenType, token, callback) {
  136. if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
  137. TokenAccessHandler.getProjectByReadOnlyToken(token, callback)
  138. } else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
  139. TokenAccessHandler.getProjectByReadAndWriteToken(token, callback)
  140. } else {
  141. return callback(new Error('invalid token type'))
  142. }
  143. },
  144. addReadOnlyUserToProject(userId, projectId, callback) {
  145. userId = ObjectId(userId.toString())
  146. projectId = ObjectId(projectId.toString())
  147. Analytics.recordEvent(userId, 'project-joined', { mode: 'read-only' })
  148. Project.updateOne(
  149. {
  150. _id: projectId,
  151. },
  152. {
  153. $addToSet: { tokenAccessReadOnly_refs: userId },
  154. },
  155. callback
  156. )
  157. },
  158. addReadAndWriteUserToProject(userId, projectId, callback) {
  159. userId = ObjectId(userId.toString())
  160. projectId = ObjectId(projectId.toString())
  161. Analytics.recordEvent(userId, 'project-joined', { mode: 'read-write' })
  162. Project.updateOne(
  163. {
  164. _id: projectId,
  165. },
  166. {
  167. $addToSet: { tokenAccessReadAndWrite_refs: userId },
  168. },
  169. callback
  170. )
  171. },
  172. grantSessionTokenAccess(req, projectId, token) {
  173. if (!req.session) {
  174. return
  175. }
  176. if (!req.session.anonTokenAccess) {
  177. req.session.anonTokenAccess = {}
  178. }
  179. req.session.anonTokenAccess[projectId.toString()] = token
  180. },
  181. getRequestToken(req, projectId) {
  182. const token =
  183. (req.session &&
  184. req.session.anonTokenAccess &&
  185. req.session.anonTokenAccess[projectId.toString()]) ||
  186. req.headers['x-sl-anonymous-access-token']
  187. return token
  188. },
  189. validateTokenForAnonymousAccess(projectId, token, callback) {
  190. if (!token) {
  191. return callback(null, false, false)
  192. }
  193. const tokenType = TokenAccessHandler.getTokenType(token)
  194. if (!tokenType) {
  195. return callback(new Error('invalid token type'))
  196. }
  197. TokenAccessHandler.getProjectByToken(tokenType, token, (err, project) => {
  198. if (err) {
  199. return callback(err)
  200. }
  201. if (
  202. !project ||
  203. !TokenAccessHandler.tokenAccessEnabledForProject(project) ||
  204. project._id.toString() !== projectId.toString()
  205. ) {
  206. return callback(null, false, false)
  207. }
  208. // TODO: think about cleaning up this interface and its usage in AuthorizationManager
  209. return callback(
  210. null,
  211. tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE &&
  212. TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED,
  213. tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY
  214. )
  215. })
  216. },
  217. protectTokens(project, privilegeLevel) {
  218. if (!project || !project.tokens) {
  219. return
  220. }
  221. if (privilegeLevel === PrivilegeLevels.OWNER) {
  222. return
  223. }
  224. if (privilegeLevel !== PrivilegeLevels.READ_AND_WRITE) {
  225. project.tokens.readAndWrite = ''
  226. project.tokens.readAndWritePrefix = ''
  227. }
  228. if (privilegeLevel !== PrivilegeLevels.READ_ONLY) {
  229. project.tokens.readOnly = ''
  230. }
  231. },
  232. getV1DocPublishedInfo(token, callback) {
  233. // default to allowing access
  234. if (!Settings.apis.v1 || !Settings.apis.v1.url) {
  235. return callback(null, { allow: true })
  236. }
  237. V1Api.request(
  238. { url: `/api/v1/sharelatex/docs/${token}/is_published` },
  239. function (err, response, body) {
  240. if (err != null) {
  241. return callback(err)
  242. }
  243. callback(null, body)
  244. }
  245. )
  246. },
  247. getV1DocInfo(token, v2UserId, callback) {
  248. if (!Settings.apis || !Settings.apis.v1) {
  249. return callback(null, {
  250. exists: true,
  251. exported: false,
  252. })
  253. }
  254. const v1Url = `/api/v1/sharelatex/docs/${token}/info`
  255. V1Api.request({ url: v1Url }, function (err, response, body) {
  256. if (err != null) {
  257. return callback(err)
  258. }
  259. callback(null, body)
  260. })
  261. },
  262. }
  263. TokenAccessHandler.promises = promisifyAll(TokenAccessHandler, {
  264. without: [
  265. 'getTokenType',
  266. 'tokenAccessEnabledForProject',
  267. '_extractNumericPrefix',
  268. '_extractStringSuffix',
  269. '_projectFindOne',
  270. 'grantSessionTokenAccess',
  271. 'getRequestToken',
  272. 'protectTokens',
  273. 'validateTokenForAnonymousAccess',
  274. ],
  275. })
  276. module.exports = TokenAccessHandler