AuthorizationManager.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import { callbackify } from 'node:util'
  2. import mongodb from 'mongodb-legacy'
  3. import Features from '../../infrastructure/Features.js'
  4. import CollaboratorsGetter from '../Collaborators/CollaboratorsGetter.mjs'
  5. import CollaboratorsHandler from '../Collaborators/CollaboratorsHandler.mjs'
  6. import ProjectGetter from '../Project/ProjectGetter.mjs'
  7. import { User } from '../../models/User.mjs'
  8. import PrivilegeLevels from './PrivilegeLevels.js'
  9. import TokenAccessHandler from '../TokenAccess/TokenAccessHandler.mjs'
  10. import PublicAccessLevels from './PublicAccessLevels.mjs'
  11. import Errors from '../Errors/Errors.js'
  12. import AdminAuthorizationHelper from '../Helpers/AdminAuthorizationHelper.mjs'
  13. import Settings from '@overleaf/settings'
  14. import ChatApiHandler from '../Chat/ChatApiHandler.mjs'
  15. const { hasAdminAccess, getAdminCapabilities } = AdminAuthorizationHelper
  16. const { ObjectId } = mongodb
  17. function isRestrictedUser(
  18. userId,
  19. privilegeLevel,
  20. isTokenMember,
  21. isInvitedMember
  22. ) {
  23. if (privilegeLevel === PrivilegeLevels.NONE) {
  24. return true
  25. }
  26. return (
  27. privilegeLevel === PrivilegeLevels.READ_ONLY &&
  28. (isTokenMember || !userId) &&
  29. !isInvitedMember
  30. )
  31. }
  32. async function isRestrictedUserForProject(userId, projectId, token) {
  33. const privilegeLevel = await getPrivilegeLevelForProject(
  34. userId,
  35. projectId,
  36. token
  37. )
  38. const isTokenMember = await CollaboratorsHandler.promises.userIsTokenMember(
  39. userId,
  40. projectId
  41. )
  42. const isInvitedMember =
  43. await CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
  44. userId,
  45. projectId
  46. )
  47. return isRestrictedUser(
  48. userId,
  49. privilegeLevel,
  50. isTokenMember,
  51. isInvitedMember
  52. )
  53. }
  54. async function getPublicAccessLevel(projectId) {
  55. if (!ObjectId.isValid(projectId)) {
  56. throw new Error('invalid project id')
  57. }
  58. // Note, the Project property in the DB is `publicAccesLevel`, without the second `s`
  59. const project = await ProjectGetter.promises.getProject(projectId, {
  60. publicAccesLevel: 1,
  61. })
  62. if (!project) {
  63. throw new Errors.NotFoundError(`no project found with id ${projectId}`)
  64. }
  65. return project.publicAccesLevel
  66. }
  67. /**
  68. * Get the privilege level that the user has for the project.
  69. *
  70. * @param userId - The id of the user that wants to access the project.
  71. * @param projectId - The id of the project to be accessed.
  72. * @param {string} token
  73. * @param {Object} opts
  74. * @param {boolean} opts.ignoreSiteAdmin - Do not consider whether the user is
  75. * a site admin.
  76. * @param {boolean} opts.ignorePublicAccess - Do not consider the project is
  77. * publicly accessible.
  78. *
  79. * @returns {string|boolean} The privilege level. One of "owner",
  80. * "readAndWrite", "readOnly" or false.
  81. */
  82. async function getPrivilegeLevelForProject(
  83. userId,
  84. projectId,
  85. token,
  86. opts = {}
  87. ) {
  88. if (userId) {
  89. return await getPrivilegeLevelForProjectWithUser(
  90. userId,
  91. projectId,
  92. null,
  93. opts
  94. )
  95. } else {
  96. return await getPrivilegeLevelForProjectWithoutUser(projectId, token, opts)
  97. }
  98. }
  99. /**
  100. * Get the privilege level that the user has for the project.
  101. *
  102. * @param userId - The id of the user that wants to access the project.
  103. * @param projectId - The id of the project to be accessed.
  104. * @param {string} token
  105. * @param {ProjectAccess} projectAccess
  106. * @param {Object} opts
  107. * @param {boolean} opts.ignoreSiteAdmin - Do not consider whether the user is
  108. * a site admin.
  109. * @param {boolean} opts.ignorePublicAccess - Do not consider the project is
  110. * publicly accessible.
  111. *
  112. * @returns {string|boolean} The privilege level. One of "owner",
  113. * "readAndWrite", "readOnly" or false.
  114. */
  115. async function getPrivilegeLevelForProjectWithProjectAccess(
  116. userId,
  117. projectId,
  118. token,
  119. projectAccess,
  120. opts = {}
  121. ) {
  122. if (userId) {
  123. return await getPrivilegeLevelForProjectWithUser(
  124. userId,
  125. projectId,
  126. projectAccess,
  127. opts
  128. )
  129. } else {
  130. return await _getPrivilegeLevelForProjectWithoutUserWithPublicAccessLevel(
  131. projectId,
  132. token,
  133. projectAccess.publicAccessLevel(),
  134. opts
  135. )
  136. }
  137. }
  138. // User is present, get their privilege level from database
  139. async function getPrivilegeLevelForProjectWithUser(
  140. userId,
  141. projectId,
  142. projectAccess,
  143. opts = {}
  144. ) {
  145. let adminReadOnly = false
  146. if (!opts.ignoreSiteAdmin && (await isUserSiteAdmin(userId))) {
  147. if (!Settings.adminRolesEnabled) {
  148. return PrivilegeLevels.OWNER
  149. }
  150. const { adminCapabilities } = await getAdminCapabilities({ _id: userId })
  151. if (adminCapabilities.includes('modify-project-content')) {
  152. return PrivilegeLevels.OWNER
  153. }
  154. if (adminCapabilities.includes('view-project-content')) {
  155. adminReadOnly = true
  156. }
  157. }
  158. projectAccess =
  159. projectAccess ||
  160. (await CollaboratorsGetter.promises.getProjectAccess(projectId))
  161. const privilegeLevel = projectAccess.privilegeLevelForUser(userId)
  162. if (privilegeLevel && privilegeLevel !== PrivilegeLevels.NONE) {
  163. // The user has direct access
  164. return privilegeLevel
  165. }
  166. if (!opts.ignorePublicAccess) {
  167. // Legacy public-access system
  168. // User is present (not anonymous), but does not have direct access
  169. const publicAccessLevel = projectAccess.publicAccessLevel()
  170. if (publicAccessLevel === PublicAccessLevels.READ_ONLY) {
  171. return PrivilegeLevels.READ_ONLY
  172. }
  173. if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) {
  174. return PrivilegeLevels.READ_AND_WRITE
  175. }
  176. }
  177. if (adminReadOnly) {
  178. return PrivilegeLevels.READ_ONLY
  179. }
  180. return PrivilegeLevels.NONE
  181. }
  182. // User is Anonymous, Try Token-based access
  183. async function getPrivilegeLevelForProjectWithoutUser(
  184. projectId,
  185. token,
  186. opts = {}
  187. ) {
  188. return await _getPrivilegeLevelForProjectWithoutUserWithPublicAccessLevel(
  189. projectId,
  190. token,
  191. await getPublicAccessLevel(projectId),
  192. opts
  193. )
  194. }
  195. // User is Anonymous, Try Token-based access
  196. async function _getPrivilegeLevelForProjectWithoutUserWithPublicAccessLevel(
  197. projectId,
  198. token,
  199. publicAccessLevel,
  200. opts = {}
  201. ) {
  202. if (!Features.hasFeature('link-sharing')) {
  203. // Link sharing disabled globally.
  204. return PrivilegeLevels.NONE
  205. }
  206. if (!opts.ignorePublicAccess) {
  207. if (publicAccessLevel === PublicAccessLevels.READ_ONLY) {
  208. // Legacy public read-only access for anonymous user
  209. return PrivilegeLevels.READ_ONLY
  210. }
  211. if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) {
  212. // Legacy public read-write access for anonymous user
  213. return PrivilegeLevels.READ_AND_WRITE
  214. }
  215. }
  216. if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) {
  217. return await getPrivilegeLevelForProjectWithToken(projectId, token)
  218. }
  219. // Deny anonymous user access
  220. return PrivilegeLevels.NONE
  221. }
  222. async function getPrivilegeLevelForProjectWithToken(projectId, token) {
  223. // Anonymous users can have read-only access to token-based projects,
  224. // while read-write access must be logged in,
  225. // unless the `enableAnonymousReadAndWriteSharing` setting is enabled
  226. const { isValidReadAndWrite, isValidReadOnly } =
  227. await TokenAccessHandler.promises.validateTokenForAnonymousAccess(
  228. projectId,
  229. token
  230. )
  231. if (isValidReadOnly) {
  232. // Grant anonymous user read-only access
  233. return PrivilegeLevels.READ_ONLY
  234. }
  235. if (isValidReadAndWrite) {
  236. // Grant anonymous user read-and-write access
  237. return PrivilegeLevels.READ_AND_WRITE
  238. }
  239. // Deny anonymous access
  240. return PrivilegeLevels.NONE
  241. }
  242. async function canUserReadProject(userId, projectId, token) {
  243. const privilegeLevel = await getPrivilegeLevelForProject(
  244. userId,
  245. projectId,
  246. token,
  247. { ignoreSiteAdmin: true }
  248. )
  249. return (
  250. [
  251. PrivilegeLevels.OWNER,
  252. PrivilegeLevels.READ_AND_WRITE,
  253. PrivilegeLevels.READ_ONLY,
  254. PrivilegeLevels.REVIEW,
  255. ].includes(privilegeLevel) ||
  256. (await hasAdminProjectCapability(userId, 'view-project-content'))
  257. )
  258. }
  259. async function canUserWriteProjectContent(userId, projectId, token) {
  260. const privilegeLevel = await getPrivilegeLevelForProject(
  261. userId,
  262. projectId,
  263. token,
  264. { ignoreSiteAdmin: true }
  265. )
  266. return (
  267. [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
  268. privilegeLevel
  269. ) || (await hasAdminProjectCapability(userId, 'modify-project-content'))
  270. )
  271. }
  272. async function canUserWriteOrReviewProjectContent(userId, projectId, token) {
  273. const privilegeLevel = await getPrivilegeLevelForProject(
  274. userId,
  275. projectId,
  276. token,
  277. { ignoreSiteAdmin: true }
  278. )
  279. return (
  280. privilegeLevel === PrivilegeLevels.OWNER ||
  281. privilegeLevel === PrivilegeLevels.READ_AND_WRITE ||
  282. privilegeLevel === PrivilegeLevels.REVIEW ||
  283. (await hasAdminProjectCapability(userId, 'modify-project-content'))
  284. )
  285. }
  286. async function canUserWriteProjectSettings(userId, projectId, token) {
  287. const privilegeLevel = await getPrivilegeLevelForProject(
  288. userId,
  289. projectId,
  290. token,
  291. { ignorePublicAccess: true, ignoreSiteAdmin: true }
  292. )
  293. return (
  294. [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
  295. privilegeLevel
  296. ) || (await hasAdminProjectCapability(userId, 'modify-project-setting'))
  297. )
  298. }
  299. async function canUserRenameProject(userId, projectId, token) {
  300. const privilegeLevel = await getPrivilegeLevelForProject(
  301. userId,
  302. projectId,
  303. token,
  304. { ignoreSiteAdmin: true }
  305. )
  306. return (
  307. privilegeLevel === PrivilegeLevels.OWNER ||
  308. (await hasAdminProjectCapability(userId, 'modify-project-setting'))
  309. )
  310. }
  311. async function canUserAdminProject(userId, projectId, token) {
  312. const privilegeLevel = await getPrivilegeLevelForProject(
  313. userId,
  314. projectId,
  315. token,
  316. { ignoreSiteAdmin: true }
  317. )
  318. return (
  319. privilegeLevel === PrivilegeLevels.OWNER ||
  320. (await hasAdminProjectCapability(userId, 'modify-project-setting'))
  321. )
  322. }
  323. async function isUserSiteAdmin(userId) {
  324. if (!userId) {
  325. return false
  326. }
  327. if (!Settings.adminPrivilegeAvailable) return false
  328. const user = await User.findOne({ _id: userId }, { isAdmin: 1 }).exec()
  329. return hasAdminAccess(user)
  330. }
  331. /**
  332. * @param {string} userId
  333. * @param {'view-project-setting'|'view-project-content'|'modify-project-setting'|'modify-project-content'} adminCapability
  334. */
  335. async function hasAdminProjectCapability(userId, adminCapability) {
  336. if (!Settings.adminPrivilegeAvailable || !(await isUserSiteAdmin(userId))) {
  337. return false
  338. }
  339. if (!Settings.adminRolesEnabled) {
  340. return true
  341. }
  342. const { adminCapabilities } = await getAdminCapabilities({ _id: userId })
  343. return adminCapabilities.includes(adminCapability)
  344. }
  345. async function canUserDeleteOrResolveThread(
  346. userId,
  347. projectId,
  348. threadId,
  349. token
  350. ) {
  351. const privilegeLevel = await getPrivilegeLevelForProject(
  352. userId,
  353. projectId,
  354. token,
  355. { ignorePublicAccess: true }
  356. )
  357. if (
  358. privilegeLevel === PrivilegeLevels.OWNER ||
  359. privilegeLevel === PrivilegeLevels.READ_AND_WRITE
  360. ) {
  361. return true
  362. }
  363. if (privilegeLevel !== PrivilegeLevels.REVIEW) {
  364. return false
  365. }
  366. try {
  367. const thread = await ChatApiHandler.promises.getThread(projectId, threadId)
  368. // Check if the user created the thread (first message)
  369. return thread.messages.length > 0 && thread.messages[0].user_id === userId
  370. } catch (error) {
  371. // If thread doesn't exist or other error, deny access
  372. return false
  373. }
  374. }
  375. export default {
  376. canUserReadProject: callbackify(canUserReadProject),
  377. canUserWriteProjectContent: callbackify(canUserWriteProjectContent),
  378. canUserWriteOrReviewProjectContent: callbackify(
  379. canUserWriteOrReviewProjectContent
  380. ),
  381. canUserDeleteOrResolveThread: callbackify(canUserDeleteOrResolveThread),
  382. canUserWriteProjectSettings: callbackify(canUserWriteProjectSettings),
  383. canUserRenameProject: callbackify(canUserRenameProject),
  384. canUserAdminProject: callbackify(canUserAdminProject),
  385. getPrivilegeLevelForProject: callbackify(getPrivilegeLevelForProject),
  386. isRestrictedUser,
  387. isRestrictedUserForProject: callbackify(isRestrictedUserForProject),
  388. isUserSiteAdmin: callbackify(isUserSiteAdmin),
  389. promises: {
  390. canUserReadProject,
  391. canUserWriteProjectContent,
  392. canUserWriteOrReviewProjectContent,
  393. canUserDeleteOrResolveThread,
  394. canUserWriteProjectSettings,
  395. canUserRenameProject,
  396. canUserAdminProject,
  397. getPrivilegeLevelForProject,
  398. getPrivilegeLevelForProjectWithProjectAccess,
  399. isRestrictedUserForProject,
  400. isUserSiteAdmin,
  401. },
  402. }