UserMembershipMiddleware.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. // @ts-check
  2. import { expressify } from '@overleaf/promise-utils'
  3. import UserMembershipAuthorization from './UserMembershipAuthorization.mjs'
  4. import AuthenticationController from '../Authentication/AuthenticationController.mjs'
  5. import UserMembershipHandler from './UserMembershipHandler.mjs'
  6. import EntityConfigs from './UserMembershipEntityConfigs.mjs'
  7. import Errors from '../Errors/Errors.js'
  8. import HttpErrorHandler from '../Errors/HttpErrorHandler.mjs'
  9. import TemplatesManager from '../Templates/TemplatesManager.mjs'
  10. import { z, zz, parseReq } from '../../infrastructure/Validation.mjs'
  11. import AdminAuthorizationHelper from '../Helpers/AdminAuthorizationHelper.mjs'
  12. const { useAdminCapabilities } = AdminAuthorizationHelper
  13. // set of middleware arrays or functions that checks user access to an entity
  14. // (publisher, institution, group, template, etc.)
  15. const UserMembershipMiddleware = {
  16. requireTeamMetricsAccess: [
  17. AuthenticationController.requireLogin(),
  18. fetchEntityConfig('team'),
  19. fetchEntity(),
  20. requireEntity(),
  21. allowAccessIfAny([
  22. UserMembershipAuthorization.hasEntityAccess(),
  23. UserMembershipAuthorization.hasAdminAccess,
  24. ]),
  25. ],
  26. requireGroup: [fetchEntityConfig('group'), fetchEntity(), requireEntity()],
  27. requireGroupAccess: [
  28. AuthenticationController.requireLogin(),
  29. fetchEntityConfig('group'),
  30. fetchEntity(),
  31. requireEntity(),
  32. ],
  33. requireEntityAccess: (
  34. /** @type {{ entityName: any; adminCapability?: any }} */ {
  35. entityName,
  36. adminCapability,
  37. }
  38. ) => [
  39. AuthenticationController.requireLogin(),
  40. fetchEntityConfig(entityName),
  41. fetchEntity(),
  42. requireEntity(),
  43. allowAccessIfAny(
  44. [
  45. UserMembershipAuthorization.hasEntityAccess(),
  46. adminCapability &&
  47. UserMembershipAuthorization.hasAdminCapability(adminCapability),
  48. ].filter(Boolean)
  49. ),
  50. ],
  51. requireEntityAccessOrAdminAccess: (/** @type {any} */ entityName) => [
  52. AuthenticationController.requireLogin(),
  53. fetchEntityConfig(entityName),
  54. fetchEntity(),
  55. requireEntity(),
  56. allowAccessIfAny([
  57. UserMembershipAuthorization.hasEntityAccess(),
  58. UserMembershipAuthorization.hasAdminCapability('modify-group'),
  59. ]),
  60. ],
  61. requireGroupMemberManagement: (/** @type {any} */ entityName) => [
  62. AuthenticationController.requireLogin(),
  63. fetchEntityConfig(entityName),
  64. fetchEntity(),
  65. requireEntity(),
  66. useAdminCapabilities,
  67. allowAccessIfAny([
  68. UserMembershipAuthorization.hasEntityAccess(),
  69. UserMembershipAuthorization.hasModifyGroupMemberCapability,
  70. ]),
  71. ],
  72. requireGroupMetricsAccess: [
  73. AuthenticationController.requireLogin(),
  74. fetchEntityConfig('group'),
  75. fetchEntity(),
  76. requireEntity(),
  77. allowAccessIfAny([
  78. UserMembershipAuthorization.hasEntityAccess(),
  79. UserMembershipAuthorization.hasAdminAccess,
  80. ]),
  81. ],
  82. requireInstitutionMetricsAccess: [
  83. AuthenticationController.requireLogin(),
  84. fetchEntityConfig('institution'),
  85. fetchEntity(),
  86. requireEntityOrCreate(),
  87. allowAccessIfAny([
  88. UserMembershipAuthorization.hasEntityAccess(),
  89. UserMembershipAuthorization.hasAdminAccess,
  90. ]),
  91. ],
  92. requireInstitutionManagerAccess: [
  93. AuthenticationController.requireLogin(),
  94. fetchEntityConfig('institution'),
  95. fetchEntity(),
  96. requireEntityOrCreate(),
  97. useAdminCapabilities,
  98. allowAccessIfAny([
  99. UserMembershipAuthorization.hasEntityAccess(),
  100. UserMembershipAuthorization.hasAdminAccess,
  101. ]),
  102. ],
  103. requireInstitutionManagerManagement: [
  104. AuthenticationController.requireLogin(),
  105. fetchEntityConfig('institution'),
  106. fetchEntity(),
  107. requireEntityOrCreate(),
  108. useAdminCapabilities,
  109. allowAccessIfAny([
  110. UserMembershipAuthorization.hasEntityAccess(),
  111. UserMembershipAuthorization.hasAdminCapability(
  112. 'modify-institution-manager'
  113. ),
  114. ]),
  115. ],
  116. requireInstitutionAIAccess: [
  117. AuthenticationController.requireLogin(),
  118. fetchEntityConfig('institution'),
  119. fetchEntity(),
  120. requireEntityOrCreate(),
  121. allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
  122. ],
  123. requireInstitutionStaffHubAccess: [
  124. AuthenticationController.requireLogin(),
  125. fetchEntityConfig('institution'),
  126. fetchEntity(),
  127. requireEntityOrCreate(),
  128. allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
  129. ],
  130. requirePublisherMetricsAccess: [
  131. AuthenticationController.requireLogin(),
  132. fetchEntityConfig('publisher'),
  133. fetchEntity(),
  134. requireEntityOrCreate(),
  135. allowAccessIfAny([
  136. UserMembershipAuthorization.hasEntityAccess(),
  137. UserMembershipAuthorization.hasAdminAccess,
  138. ]),
  139. ],
  140. requirePublisherManagementAccess: [
  141. AuthenticationController.requireLogin(),
  142. fetchEntityConfig('publisher'),
  143. fetchEntity(),
  144. requireEntityOrCreate(),
  145. useAdminCapabilities,
  146. allowAccessIfAny([
  147. UserMembershipAuthorization.hasEntityAccess(),
  148. UserMembershipAuthorization.hasAdminCapability(
  149. 'modify-publisher-manager'
  150. ),
  151. ]),
  152. ],
  153. requireConversionMetricsAccess: [
  154. AuthenticationController.requireLogin(),
  155. fetchEntityConfig('publisher'),
  156. fetchEntity(),
  157. requireEntityOrCreate(),
  158. allowAccessIfAny([
  159. UserMembershipAuthorization.hasEntityAccess(),
  160. UserMembershipAuthorization.hasAdminAccess,
  161. ]),
  162. ],
  163. requireAdminMetricsAccess: [
  164. AuthenticationController.requireLogin(),
  165. allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
  166. ],
  167. requireTemplateMetricsAccess: [
  168. AuthenticationController.requireLogin(),
  169. fetchV1Template(),
  170. requireV1Template(),
  171. fetchEntityConfig('publisher'),
  172. fetchPublisherFromTemplate(),
  173. allowAccessIfAny([
  174. UserMembershipAuthorization.hasEntityAccess(),
  175. UserMembershipAuthorization.hasAdminAccess,
  176. ]),
  177. ],
  178. requirePublisherCreationAccess: [
  179. AuthenticationController.requireLogin(),
  180. allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
  181. fetchEntityConfig('publisher'),
  182. ],
  183. requireInstitutionCreationAccess: [
  184. AuthenticationController.requireLogin(),
  185. allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
  186. fetchEntityConfig('institution'),
  187. ],
  188. requireSplitTestMetricsAccess: [
  189. AuthenticationController.requireLogin(),
  190. useAdminCapabilities,
  191. allowAccessIfAny([
  192. UserMembershipAuthorization.hasAdminCapability('view-split-test'),
  193. ]),
  194. ],
  195. requireSplitTestManagementAccess: [
  196. AuthenticationController.requireLogin(),
  197. useAdminCapabilities,
  198. allowAccessIfAny([
  199. UserMembershipAuthorization.hasAdminCapability('modify-split-test'),
  200. ]),
  201. ],
  202. requireGraphAccess,
  203. }
  204. export default UserMembershipMiddleware
  205. /**
  206. * fetch entity config and set it in the request
  207. *
  208. * @param {any} entityName
  209. */
  210. function fetchEntityConfig(entityName) {
  211. return (
  212. /** @type {any} */ req,
  213. /** @type {any} */ res,
  214. /** @type {any} */ next
  215. ) => {
  216. const entityConfig = /** @type {Record<string, any>} */ (EntityConfigs)[
  217. entityName
  218. ]
  219. req.entityName = entityName
  220. req.entityConfig = entityConfig
  221. next()
  222. }
  223. }
  224. const SlugEntitySchema = z.object({
  225. entityName: z.literal('publisher'),
  226. params: z.object({
  227. id: z.string(), // slug
  228. }),
  229. })
  230. const PostgresIdEntitySchema = z.object({
  231. entityName: z.literal(['institution', 'team']),
  232. params: z.object({
  233. id: z.coerce.number().positive(),
  234. }),
  235. })
  236. const ObjectIdEntitySchema = z.object({
  237. entityName: z.literal([
  238. 'group',
  239. 'groupAdmin',
  240. 'groupManagers',
  241. 'groupMember',
  242. 'groupUsers',
  243. ]),
  244. params: z.object({
  245. id: zz.coercedObjectId(),
  246. }),
  247. })
  248. const fetchEntitySchema = z.discriminatedUnion('entityName', [
  249. SlugEntitySchema,
  250. ObjectIdEntitySchema,
  251. PostgresIdEntitySchema,
  252. ])
  253. // graphs access is an edge-case:
  254. // - the entity id is in `req.query.resource_id`. It must be set as
  255. // `req.params.id`
  256. // - the entity name is in `req.query.resource_type` and is used to find the
  257. // require middleware depending on the entity name
  258. /**
  259. * @param {any} req
  260. * @param {any} res
  261. * @param {any} next
  262. */
  263. function requireGraphAccess(req, res, next) {
  264. const entityName = req.query.resource_type
  265. if (!entityName) {
  266. return HttpErrorHandler.notFound(req, res, 'resource_type param missing')
  267. }
  268. const middleWareName =
  269. entityName.charAt(0).toUpperCase() + entityName.slice(1)
  270. const middlewares = /** @type {Record<string, any>} */ (
  271. UserMembershipMiddleware
  272. )[`require${middleWareName}MetricsAccess`]
  273. if (!middlewares) {
  274. return HttpErrorHandler.notFound(
  275. req,
  276. res,
  277. `incorrect entity name: ${middleWareName}`
  278. )
  279. }
  280. // call next router with fixed params to pass it to the correct middleware chain
  281. const { graph } = req.params
  282. const entityRoute = entityName === 'splitTest' ? 'split-test' : entityName
  283. // all other routes go through analytics, which map undefined graph type to index (to fetch all graphs)
  284. // conversion still goes directly to v1, which can not handle an undefined graph param
  285. if (!graph?.length && entityRoute === 'conversion') {
  286. req.url = `/graphs/conversion/index/${req.query.resource_id}`
  287. } else {
  288. req.url = `/graphs/${entityRoute}/${graph}/${req.query.resource_id}`
  289. }
  290. next('route')
  291. }
  292. // fetch the entity with id and config, and set it in the request
  293. function fetchEntity() {
  294. return expressify(
  295. async (
  296. /** @type {any} */ req,
  297. /** @type {any} */ res,
  298. /** @type {any} */ next
  299. ) => {
  300. const { params } = parseReq(req, fetchEntitySchema)
  301. req.entity =
  302. await UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck(
  303. params.id,
  304. req.entityConfig
  305. )
  306. next()
  307. }
  308. )
  309. }
  310. function fetchPublisherFromTemplate() {
  311. return (
  312. /** @type {any} */ req,
  313. /** @type {any} */ res,
  314. /** @type {any} */ next
  315. ) => {
  316. if (req.template.brand.slug) {
  317. // set the id as the publisher's id as it's the entity used for access
  318. // control
  319. req.params.id = req.template.brand.slug
  320. return fetchEntity()(req, res, next)
  321. } else {
  322. return next()
  323. }
  324. }
  325. }
  326. // ensure an entity was found, or fail with 404
  327. function requireEntity() {
  328. return (
  329. /** @type {any} */ req,
  330. /** @type {any} */ res,
  331. /** @type {any} */ next
  332. ) => {
  333. if (req.entity) {
  334. return next()
  335. }
  336. throw new Errors.NotFoundError(
  337. `no '${req.entityName}' entity with '${req.params.id}'`
  338. )
  339. }
  340. }
  341. /**
  342. * ensure an entity was found or redirect to entity creation page if the user
  343. * has permissions to create the entity, or fail with 404
  344. */
  345. function requireEntityOrCreate() {
  346. return (
  347. /** @type {any} */ req,
  348. /** @type {any} */ res,
  349. /** @type {any} */ next
  350. ) => {
  351. if (req.entity) {
  352. return next()
  353. }
  354. if (UserMembershipAuthorization.hasAdminAccess(req)) {
  355. res.redirect(`/entities/${req.entityName}/create/${req.params.id}`)
  356. return
  357. }
  358. throw new Errors.NotFoundError(
  359. `no '${req.entityName}' entity with '${req.params.id}'`
  360. )
  361. }
  362. }
  363. // fetch the template from v1, and set it in the request
  364. function fetchV1Template() {
  365. return expressify(
  366. async (
  367. /** @type {any} */ req,
  368. /** @type {any} */ res,
  369. /** @type {any} */ next
  370. ) => {
  371. const templateId = req.params.id
  372. const body = await TemplatesManager.promises.fetchFromV1(templateId)
  373. req.template = {
  374. id: body.id,
  375. title: body.title,
  376. brand: body.brand,
  377. }
  378. next()
  379. }
  380. )
  381. }
  382. // ensure a template was found, or fail with 404
  383. function requireV1Template() {
  384. return (
  385. /** @type {any} */ req,
  386. /** @type {any} */ res,
  387. /** @type {any} */ next
  388. ) => {
  389. if (req.template.id) {
  390. return next()
  391. }
  392. throw new Errors.NotFoundError('no template found')
  393. }
  394. }
  395. /**
  396. * run a series of synchronous access functions and call `next` if any of the
  397. * return values is truly. Redirect to restricted otherwise
  398. *
  399. * @param {any} accessFunctions
  400. */
  401. function allowAccessIfAny(accessFunctions) {
  402. return (
  403. /** @type {any} */ req,
  404. /** @type {any} */ res,
  405. /** @type {any} */ next
  406. ) => {
  407. for (const accessFunction of accessFunctions) {
  408. if (accessFunction(req)) {
  409. return next()
  410. }
  411. }
  412. HttpErrorHandler.forbidden(req, res)
  413. }
  414. }