| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- // @ts-check
- import { expressify } from '@overleaf/promise-utils'
- import UserMembershipAuthorization from './UserMembershipAuthorization.mjs'
- import AuthenticationController from '../Authentication/AuthenticationController.mjs'
- import UserMembershipHandler from './UserMembershipHandler.mjs'
- import EntityConfigs from './UserMembershipEntityConfigs.mjs'
- import Errors from '../Errors/Errors.js'
- import HttpErrorHandler from '../Errors/HttpErrorHandler.mjs'
- import TemplatesManager from '../Templates/TemplatesManager.mjs'
- import { z, zz, parseReq } from '../../infrastructure/Validation.mjs'
- import AdminAuthorizationHelper from '../Helpers/AdminAuthorizationHelper.mjs'
- const { useAdminCapabilities } = AdminAuthorizationHelper
- // set of middleware arrays or functions that checks user access to an entity
- // (publisher, institution, group, template, etc.)
- const UserMembershipMiddleware = {
- requireTeamMetricsAccess: [
- AuthenticationController.requireLogin(),
- fetchEntityConfig('team'),
- fetchEntity(),
- requireEntity(),
- allowAccessIfAny([
- UserMembershipAuthorization.hasEntityAccess(),
- UserMembershipAuthorization.hasAdminAccess,
- ]),
- ],
- requireGroup: [fetchEntityConfig('group'), fetchEntity(), requireEntity()],
- requireGroupAccess: [
- AuthenticationController.requireLogin(),
- fetchEntityConfig('group'),
- fetchEntity(),
- requireEntity(),
- ],
- requireEntityAccess: (
- /** @type {{ entityName: any; adminCapability?: any }} */ {
- entityName,
- adminCapability,
- }
- ) => [
- AuthenticationController.requireLogin(),
- fetchEntityConfig(entityName),
- fetchEntity(),
- requireEntity(),
- allowAccessIfAny(
- [
- UserMembershipAuthorization.hasEntityAccess(),
- adminCapability &&
- UserMembershipAuthorization.hasAdminCapability(adminCapability),
- ].filter(Boolean)
- ),
- ],
- requireEntityAccessOrAdminAccess: (/** @type {any} */ entityName) => [
- AuthenticationController.requireLogin(),
- fetchEntityConfig(entityName),
- fetchEntity(),
- requireEntity(),
- allowAccessIfAny([
- UserMembershipAuthorization.hasEntityAccess(),
- UserMembershipAuthorization.hasAdminCapability('modify-group'),
- ]),
- ],
- requireGroupMemberManagement: (/** @type {any} */ entityName) => [
- AuthenticationController.requireLogin(),
- fetchEntityConfig(entityName),
- fetchEntity(),
- requireEntity(),
- useAdminCapabilities,
- allowAccessIfAny([
- UserMembershipAuthorization.hasEntityAccess(),
- UserMembershipAuthorization.hasModifyGroupMemberCapability,
- ]),
- ],
- requireGroupMetricsAccess: [
- AuthenticationController.requireLogin(),
- fetchEntityConfig('group'),
- fetchEntity(),
- requireEntity(),
- allowAccessIfAny([
- UserMembershipAuthorization.hasEntityAccess(),
- UserMembershipAuthorization.hasAdminAccess,
- ]),
- ],
- requireInstitutionMetricsAccess: [
- AuthenticationController.requireLogin(),
- fetchEntityConfig('institution'),
- fetchEntity(),
- requireEntityOrCreate(),
- allowAccessIfAny([
- UserMembershipAuthorization.hasEntityAccess(),
- UserMembershipAuthorization.hasAdminAccess,
- ]),
- ],
- requireInstitutionManagerAccess: [
- AuthenticationController.requireLogin(),
- fetchEntityConfig('institution'),
- fetchEntity(),
- requireEntityOrCreate(),
- useAdminCapabilities,
- allowAccessIfAny([
- UserMembershipAuthorization.hasEntityAccess(),
- UserMembershipAuthorization.hasAdminAccess,
- ]),
- ],
- requireInstitutionManagerManagement: [
- AuthenticationController.requireLogin(),
- fetchEntityConfig('institution'),
- fetchEntity(),
- requireEntityOrCreate(),
- useAdminCapabilities,
- allowAccessIfAny([
- UserMembershipAuthorization.hasEntityAccess(),
- UserMembershipAuthorization.hasAdminCapability(
- 'modify-institution-manager'
- ),
- ]),
- ],
- requireInstitutionAIAccess: [
- AuthenticationController.requireLogin(),
- fetchEntityConfig('institution'),
- fetchEntity(),
- requireEntityOrCreate(),
- allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
- ],
- requireInstitutionStaffHubAccess: [
- AuthenticationController.requireLogin(),
- fetchEntityConfig('institution'),
- fetchEntity(),
- requireEntityOrCreate(),
- allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
- ],
- requirePublisherMetricsAccess: [
- AuthenticationController.requireLogin(),
- fetchEntityConfig('publisher'),
- fetchEntity(),
- requireEntityOrCreate(),
- allowAccessIfAny([
- UserMembershipAuthorization.hasEntityAccess(),
- UserMembershipAuthorization.hasAdminAccess,
- ]),
- ],
- requirePublisherManagementAccess: [
- AuthenticationController.requireLogin(),
- fetchEntityConfig('publisher'),
- fetchEntity(),
- requireEntityOrCreate(),
- useAdminCapabilities,
- allowAccessIfAny([
- UserMembershipAuthorization.hasEntityAccess(),
- UserMembershipAuthorization.hasAdminCapability(
- 'modify-publisher-manager'
- ),
- ]),
- ],
- requireConversionMetricsAccess: [
- AuthenticationController.requireLogin(),
- fetchEntityConfig('publisher'),
- fetchEntity(),
- requireEntityOrCreate(),
- allowAccessIfAny([
- UserMembershipAuthorization.hasEntityAccess(),
- UserMembershipAuthorization.hasAdminAccess,
- ]),
- ],
- requireAdminMetricsAccess: [
- AuthenticationController.requireLogin(),
- allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
- ],
- requireTemplateMetricsAccess: [
- AuthenticationController.requireLogin(),
- fetchV1Template(),
- requireV1Template(),
- fetchEntityConfig('publisher'),
- fetchPublisherFromTemplate(),
- allowAccessIfAny([
- UserMembershipAuthorization.hasEntityAccess(),
- UserMembershipAuthorization.hasAdminAccess,
- ]),
- ],
- requirePublisherCreationAccess: [
- AuthenticationController.requireLogin(),
- allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
- fetchEntityConfig('publisher'),
- ],
- requireInstitutionCreationAccess: [
- AuthenticationController.requireLogin(),
- allowAccessIfAny([UserMembershipAuthorization.hasAdminAccess]),
- fetchEntityConfig('institution'),
- ],
- requireSplitTestMetricsAccess: [
- AuthenticationController.requireLogin(),
- useAdminCapabilities,
- allowAccessIfAny([
- UserMembershipAuthorization.hasAdminCapability('view-split-test'),
- ]),
- ],
- requireSplitTestManagementAccess: [
- AuthenticationController.requireLogin(),
- useAdminCapabilities,
- allowAccessIfAny([
- UserMembershipAuthorization.hasAdminCapability('modify-split-test'),
- ]),
- ],
- requireGraphAccess,
- }
- export default UserMembershipMiddleware
- /**
- * fetch entity config and set it in the request
- *
- * @param {any} entityName
- */
- function fetchEntityConfig(entityName) {
- return (
- /** @type {any} */ req,
- /** @type {any} */ res,
- /** @type {any} */ next
- ) => {
- const entityConfig = /** @type {Record<string, any>} */ (EntityConfigs)[
- entityName
- ]
- req.entityName = entityName
- req.entityConfig = entityConfig
- next()
- }
- }
- const SlugEntitySchema = z.object({
- entityName: z.literal('publisher'),
- params: z.object({
- id: z.string(), // slug
- }),
- })
- const PostgresIdEntitySchema = z.object({
- entityName: z.literal(['institution', 'team']),
- params: z.object({
- id: z.coerce.number().positive(),
- }),
- })
- const ObjectIdEntitySchema = z.object({
- entityName: z.literal([
- 'group',
- 'groupAdmin',
- 'groupManagers',
- 'groupMember',
- 'groupUsers',
- ]),
- params: z.object({
- id: zz.coercedObjectId(),
- }),
- })
- const fetchEntitySchema = z.discriminatedUnion('entityName', [
- SlugEntitySchema,
- ObjectIdEntitySchema,
- PostgresIdEntitySchema,
- ])
- // graphs access is an edge-case:
- // - the entity id is in `req.query.resource_id`. It must be set as
- // `req.params.id`
- // - the entity name is in `req.query.resource_type` and is used to find the
- // require middleware depending on the entity name
- /**
- * @param {any} req
- * @param {any} res
- * @param {any} next
- */
- function requireGraphAccess(req, res, next) {
- const entityName = req.query.resource_type
- if (!entityName) {
- return HttpErrorHandler.notFound(req, res, 'resource_type param missing')
- }
- const middleWareName =
- entityName.charAt(0).toUpperCase() + entityName.slice(1)
- const middlewares = /** @type {Record<string, any>} */ (
- UserMembershipMiddleware
- )[`require${middleWareName}MetricsAccess`]
- if (!middlewares) {
- return HttpErrorHandler.notFound(
- req,
- res,
- `incorrect entity name: ${middleWareName}`
- )
- }
- // call next router with fixed params to pass it to the correct middleware chain
- const { graph } = req.params
- const entityRoute = entityName === 'splitTest' ? 'split-test' : entityName
- // all other routes go through analytics, which map undefined graph type to index (to fetch all graphs)
- // conversion still goes directly to v1, which can not handle an undefined graph param
- if (!graph?.length && entityRoute === 'conversion') {
- req.url = `/graphs/conversion/index/${req.query.resource_id}`
- } else {
- req.url = `/graphs/${entityRoute}/${graph}/${req.query.resource_id}`
- }
- next('route')
- }
- // fetch the entity with id and config, and set it in the request
- function fetchEntity() {
- return expressify(
- async (
- /** @type {any} */ req,
- /** @type {any} */ res,
- /** @type {any} */ next
- ) => {
- const { params } = parseReq(req, fetchEntitySchema)
- req.entity =
- await UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck(
- params.id,
- req.entityConfig
- )
- next()
- }
- )
- }
- function fetchPublisherFromTemplate() {
- return (
- /** @type {any} */ req,
- /** @type {any} */ res,
- /** @type {any} */ next
- ) => {
- if (req.template.brand.slug) {
- // set the id as the publisher's id as it's the entity used for access
- // control
- req.params.id = req.template.brand.slug
- return fetchEntity()(req, res, next)
- } else {
- return next()
- }
- }
- }
- // ensure an entity was found, or fail with 404
- function requireEntity() {
- return (
- /** @type {any} */ req,
- /** @type {any} */ res,
- /** @type {any} */ next
- ) => {
- if (req.entity) {
- return next()
- }
- throw new Errors.NotFoundError(
- `no '${req.entityName}' entity with '${req.params.id}'`
- )
- }
- }
- /**
- * ensure an entity was found or redirect to entity creation page if the user
- * has permissions to create the entity, or fail with 404
- */
- function requireEntityOrCreate() {
- return (
- /** @type {any} */ req,
- /** @type {any} */ res,
- /** @type {any} */ next
- ) => {
- if (req.entity) {
- return next()
- }
- if (UserMembershipAuthorization.hasAdminAccess(req)) {
- res.redirect(`/entities/${req.entityName}/create/${req.params.id}`)
- return
- }
- throw new Errors.NotFoundError(
- `no '${req.entityName}' entity with '${req.params.id}'`
- )
- }
- }
- // fetch the template from v1, and set it in the request
- function fetchV1Template() {
- return expressify(
- async (
- /** @type {any} */ req,
- /** @type {any} */ res,
- /** @type {any} */ next
- ) => {
- const templateId = req.params.id
- const body = await TemplatesManager.promises.fetchFromV1(templateId)
- req.template = {
- id: body.id,
- title: body.title,
- brand: body.brand,
- }
- next()
- }
- )
- }
- // ensure a template was found, or fail with 404
- function requireV1Template() {
- return (
- /** @type {any} */ req,
- /** @type {any} */ res,
- /** @type {any} */ next
- ) => {
- if (req.template.id) {
- return next()
- }
- throw new Errors.NotFoundError('no template found')
- }
- }
- /**
- * run a series of synchronous access functions and call `next` if any of the
- * return values is truly. Redirect to restricted otherwise
- *
- * @param {any} accessFunctions
- */
- function allowAccessIfAny(accessFunctions) {
- return (
- /** @type {any} */ req,
- /** @type {any} */ res,
- /** @type {any} */ next
- ) => {
- for (const accessFunction of accessFunctions) {
- if (accessFunction(req)) {
- return next()
- }
- }
- HttpErrorHandler.forbidden(req, res)
- }
- }
|