UserMembershipMiddleware.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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: ({ entityName, adminCapability }) => [
  34. AuthenticationController.requireLogin(),
  35. fetchEntityConfig(entityName),
  36. fetchEntity(),
  37. requireEntity(),
  38. allowAccessIfAny(
  39. [
  40. UserMembershipAuthorization.hasEntityAccess(),
  41. adminCapability &&
  42. UserMembershipAuthorization.hasAdminCapability(adminCapability),
  43. ].filter(Boolean)
  44. ),
  45. ],
  46. requireEntityAccessOrAdminAccess: entityName => [
  47. AuthenticationController.requireLogin(),
  48. fetchEntityConfig(entityName),
  49. fetchEntity(),
  50. requireEntity(),
  51. allowAccessIfAny([
  52. UserMembershipAuthorization.hasEntityAccess(),
  53. UserMembershipAuthorization.hasAdminCapability('modify-group'),
  54. ]),
  55. ],
  56. requireGroupMemberManagement: entityName => [
  57. AuthenticationController.requireLogin(),
  58. fetchEntityConfig(entityName),
  59. fetchEntity(),
  60. requireEntity(),
  61. useAdminCapabilities,
  62. allowAccessIfAny([
  63. UserMembershipAuthorization.hasEntityAccess(),
  64. UserMembershipAuthorization.hasModifyGroupMemberCapability,
  65. ]),
  66. ],
  67. requireGroupMetricsAccess: [
  68. AuthenticationController.requireLogin(),
  69. fetchEntityConfig('group'),
  70. fetchEntity(),
  71. requireEntity(),
  72. allowAccessIfAny([
  73. UserMembershipAuthorization.hasEntityAccess(),
  74. UserMembershipAuthorization.hasAdminAccess,
  75. ]),
  76. ],
  77. requireInstitutionMetricsAccess: [
  78. AuthenticationController.requireLogin(),
  79. fetchEntityConfig('institution'),
  80. fetchEntity(),
  81. requireEntityOrCreate(),
  82. allowAccessIfAny([
  83. UserMembershipAuthorization.hasEntityAccess(),
  84. UserMembershipAuthorization.hasAdminAccess,
  85. ]),
  86. ],
  87. requireInstitutionManagementAccess: [
  88. AuthenticationController.requireLogin(),
  89. fetchEntityConfig('institution'),
  90. fetchEntity(),
  91. requireEntityOrCreate(),
  92. useAdminCapabilities,
  93. allowAccessIfAny([
  94. UserMembershipAuthorization.hasEntityAccess(),
  95. UserMembershipAuthorization.hasAdminCapability(
  96. 'modify-institution-manager'
  97. ),
  98. ]),
  99. ],
  100. requireInstitutionAIAccess: [
  101. AuthenticationController.requireLogin(),
  102. fetchEntityConfig('institution'),
  103. fetchEntity(),
  104. requireEntityOrCreate(),
  105. allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
  106. ],
  107. requireInstitutionStaffHubAccess: [
  108. AuthenticationController.requireLogin(),
  109. fetchEntityConfig('institution'),
  110. fetchEntity(),
  111. requireEntityOrCreate(),
  112. allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
  113. ],
  114. requirePublisherMetricsAccess: [
  115. AuthenticationController.requireLogin(),
  116. fetchEntityConfig('publisher'),
  117. fetchEntity(),
  118. requireEntityOrCreate(),
  119. allowAccessIfAny([
  120. UserMembershipAuthorization.hasEntityAccess(),
  121. UserMembershipAuthorization.hasAdminAccess,
  122. ]),
  123. ],
  124. requirePublisherManagementAccess: [
  125. AuthenticationController.requireLogin(),
  126. fetchEntityConfig('publisher'),
  127. fetchEntity(),
  128. requireEntityOrCreate(),
  129. useAdminCapabilities,
  130. allowAccessIfAny([
  131. UserMembershipAuthorization.hasEntityAccess(),
  132. UserMembershipAuthorization.hasAdminCapability(
  133. 'modify-publisher-manager'
  134. ),
  135. ]),
  136. ],
  137. requireConversionMetricsAccess: [
  138. AuthenticationController.requireLogin(),
  139. fetchEntityConfig('publisher'),
  140. fetchEntity(),
  141. requireEntityOrCreate(),
  142. allowAccessIfAny([
  143. UserMembershipAuthorization.hasEntityAccess(),
  144. UserMembershipAuthorization.hasAdminAccess,
  145. ]),
  146. ],
  147. requireAdminMetricsAccess: [
  148. AuthenticationController.requireLogin(),
  149. allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
  150. ],
  151. requireTemplateMetricsAccess: [
  152. AuthenticationController.requireLogin(),
  153. fetchV1Template(),
  154. requireV1Template(),
  155. fetchEntityConfig('publisher'),
  156. fetchPublisherFromTemplate(),
  157. allowAccessIfAny([
  158. UserMembershipAuthorization.hasEntityAccess(),
  159. UserMembershipAuthorization.hasAdminAccess,
  160. ]),
  161. ],
  162. requirePublisherCreationAccess: [
  163. AuthenticationController.requireLogin(),
  164. allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
  165. fetchEntityConfig('publisher'),
  166. ],
  167. requireInstitutionCreationAccess: [
  168. AuthenticationController.requireLogin(),
  169. allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
  170. fetchEntityConfig('institution'),
  171. ],
  172. requireSplitTestMetricsAccess: [
  173. AuthenticationController.requireLogin(),
  174. useAdminCapabilities,
  175. allowAccessIfAny([
  176. UserMembershipAuthorization.hasAdminCapability('view-split-test'),
  177. ]),
  178. ],
  179. requireSplitTestManagementAccess: [
  180. AuthenticationController.requireLogin(),
  181. useAdminCapabilities,
  182. allowAccessIfAny([
  183. UserMembershipAuthorization.hasAdminCapability('modify-split-test'),
  184. ]),
  185. ],
  186. requireGraphAccess,
  187. }
  188. export default UserMembershipMiddleware
  189. // fetch entity config and set it in the request
  190. function fetchEntityConfig(entityName) {
  191. return (req, res, next) => {
  192. const entityConfig = EntityConfigs[entityName]
  193. req.entityName = entityName
  194. req.entityConfig = entityConfig
  195. next()
  196. }
  197. }
  198. const SlugEntitySchema = z.object({
  199. entityName: z.literal('publisher'),
  200. params: z.object({
  201. id: z.string(), // slug
  202. }),
  203. })
  204. const PostgresIdEntitySchema = z.object({
  205. entityName: z.literal(['institution', 'team']),
  206. params: z.object({
  207. id: z.coerce.number().positive(),
  208. }),
  209. })
  210. const ObjectIdEntitySchema = z.object({
  211. entityName: z.literal([
  212. 'group',
  213. 'groupAdmin',
  214. 'groupManagers',
  215. 'groupMember',
  216. ]),
  217. params: z.object({
  218. id: zz.coercedObjectId(),
  219. }),
  220. })
  221. const fetchEntitySchema = z.discriminatedUnion('entityName', [
  222. SlugEntitySchema,
  223. ObjectIdEntitySchema,
  224. PostgresIdEntitySchema,
  225. ])
  226. // graphs access is an edge-case:
  227. // - the entity id is in `req.query.resource_id`. It must be set as
  228. // `req.params.id`
  229. // - the entity name is in `req.query.resource_type` and is used to find the
  230. // require middleware depending on the entity name
  231. function requireGraphAccess(req, res, next) {
  232. const entityName = req.query.resource_type
  233. if (!entityName) {
  234. return HttpErrorHandler.notFound(req, res, 'resource_type param missing')
  235. }
  236. const middleWareName =
  237. entityName.charAt(0).toUpperCase() + entityName.slice(1)
  238. const middlewares =
  239. UserMembershipMiddleware[`require${middleWareName}MetricsAccess`]
  240. if (!middlewares) {
  241. return HttpErrorHandler.notFound(
  242. req,
  243. res,
  244. `incorrect entity name: ${middleWareName}`
  245. )
  246. }
  247. // call next router with fixed params to pass it to the correct middleware chain
  248. const { graph } = req.params
  249. const entityRoute = entityName === 'splitTest' ? 'split-test' : entityName
  250. req.url = `/graphs/${entityRoute}/${graph}/${req.query.resource_id}`
  251. next('route')
  252. }
  253. // fetch the entity with id and config, and set it in the request
  254. function fetchEntity() {
  255. return expressify(async (req, res, next) => {
  256. const { params } = parseReq(req, fetchEntitySchema)
  257. req.entity =
  258. await UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck(
  259. params.id,
  260. req.entityConfig
  261. )
  262. next()
  263. })
  264. }
  265. function fetchPublisherFromTemplate() {
  266. return (req, res, next) => {
  267. if (req.template.brand.slug) {
  268. // set the id as the publisher's id as it's the entity used for access
  269. // control
  270. req.params.id = req.template.brand.slug
  271. return fetchEntity()(req, res, next)
  272. } else {
  273. return next()
  274. }
  275. }
  276. }
  277. // ensure an entity was found, or fail with 404
  278. function requireEntity() {
  279. return (req, res, next) => {
  280. if (req.entity) {
  281. return next()
  282. }
  283. throw new Errors.NotFoundError(
  284. `no '${req.entityName}' entity with '${req.params.id}'`
  285. )
  286. }
  287. }
  288. // ensure an entity was found or redirect to entity creation page if the user
  289. // has permissions to create the entity, or fail with 404
  290. function requireEntityOrCreate() {
  291. return (req, res, next) => {
  292. if (req.entity) {
  293. return next()
  294. }
  295. if (UserMembershipAuthorization.hasAdminAccess(req)) {
  296. res.redirect(`/entities/${req.entityName}/create/${req.params.id}`)
  297. return
  298. }
  299. throw new Errors.NotFoundError(
  300. `no '${req.entityName}' entity with '${req.params.id}'`
  301. )
  302. }
  303. }
  304. // fetch the template from v1, and set it in the request
  305. function fetchV1Template() {
  306. return expressify(async (req, res, next) => {
  307. const templateId = req.params.id
  308. const body = await TemplatesManager.promises.fetchFromV1(templateId)
  309. req.template = {
  310. id: body.id,
  311. title: body.title,
  312. brand: body.brand,
  313. }
  314. next()
  315. })
  316. }
  317. // ensure a template was found, or fail with 404
  318. function requireV1Template() {
  319. return (req, res, next) => {
  320. if (req.template.id) {
  321. return next()
  322. }
  323. throw new Errors.NotFoundError('no template found')
  324. }
  325. }
  326. // run a serie of synchronous access functions and call `next` if any of the
  327. // retur values is truly. Redirect to restricted otherwise
  328. function allowAccessIfAny(accessFunctions) {
  329. return (req, res, next) => {
  330. for (const accessFunction of accessFunctions) {
  331. if (accessFunction(req)) {
  332. return next()
  333. }
  334. }
  335. HttpErrorHandler.forbidden(req, res)
  336. }
  337. }