TokenAccessHandler.js 8.1 KB

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