UserMembershipMiddleware.mjs 12 KB

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