Files
overleaf-cep/services/web/app/src/Features/UserMembership/UserMembershipMiddleware.js
Antoine Clausse ba97b96815 [web] Add admin permissions modify-group-member and modify-managed-group-member (#27665)
* Add capability `modify-managed-group-member` & `modify-group-member`

* Check `modify-managed-group-member` & `modify-group-member` (backend)

* Check `modify-managed-group-member` & `modify-group-member` (frontend)

* Update tests

* Update with `ol-hasWriteAccess` flag

* Update tests

* Move functions to AdminAuthorizationHelper.js

* Update import to fix build error

* Add `ol-hasWriteAccess` to types

* Use `hasAdminAccess()` instead of `req?.user?.isAdmin`

* Add tests on `/manage/groups/:id/invites` depending on admin roles

* Reuse `UserMembershipAuthorization.hasAdminCapability`

* Fix: Add entityAccess check

* Update unit test

* Rename `hasAdminGroupMemberCapability` to `hasModifyGroupMemberCapability`

* Remove useless and redundant `hasWriteAccess` check

* Restore stub in afterEach

GitOrigin-RevId: 4b6d83751121b43d4c19d0dbd82a4833cf7a6f24
2025-08-15 08:05:57 +00:00

376 lines
11 KiB
JavaScript

const { expressify } = require('@overleaf/promise-utils')
const async = require('async')
const UserMembershipAuthorization = require('./UserMembershipAuthorization')
const AuthenticationController = require('../Authentication/AuthenticationController')
const UserMembershipHandler = require('./UserMembershipHandler')
const EntityConfigs = require('./UserMembershipEntityConfigs')
const Errors = require('../Errors/Errors')
const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const TemplatesManager = require('../Templates/TemplatesManager')
const { useAdminCapabilities } = require('../Helpers/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.hasStaffAccess('groupMetrics'),
]),
],
requireGroup: [fetchEntityConfig('group'), fetchEntity(), requireEntity()],
requireGroupAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('group'),
fetchEntity(),
requireEntity(),
],
requireGroupMemberAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('groupMember'),
fetchEntity(),
requireEntity(),
allowAccessIfAny([UserMembershipAuthorization.hasEntityAccess()]),
],
requireGroupManagementAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('group'),
fetchEntity(),
requireEntity(),
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('groupManagement'),
]),
],
requireGroupMemberManagementAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('group'),
fetchEntity(),
requireEntity(),
useAdminCapabilities,
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('groupManagement'),
UserMembershipAuthorization.hasModifyGroupMemberCapability,
]),
],
requireGroupMetricsAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('group'),
fetchEntity(),
requireEntity(),
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('groupMetrics'),
]),
],
requireGroupManagersManagementAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('groupManagers'),
fetchEntity(),
requireEntity(),
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('groupManagement'),
]),
],
requireGroupAdminAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('groupAdmin'),
fetchEntity(),
requireEntity(),
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('groupManagement'),
]),
],
requireGroupSettingsReadAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('groupAdmin'),
fetchEntity(),
requireEntity(),
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('groupManagement'),
]),
],
requireGroupSettingsWriteAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('groupAdmin'),
fetchEntity(),
requireEntity(),
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('groupManagement'),
UserMembershipAuthorization.hasAdminCapability('modify-group-setting'),
]),
],
requireInstitutionMetricsAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('institution'),
fetchEntity(),
requireEntityOrCreate('institutionManagement'),
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('institutionMetrics'),
]),
],
requireInstitutionManagementAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('institution'),
fetchEntity(),
requireEntityOrCreate('institutionManagement'),
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('institutionManagement'),
]),
],
requireInstitutionManagementStaffAccess: [
AuthenticationController.requireLogin(),
allowAccessIfAny([
UserMembershipAuthorization.hasStaffAccess('institutionManagement'),
]),
fetchEntityConfig('institution'),
fetchEntity(),
requireEntityOrCreate('institutionManagement'),
],
requirePublisherMetricsAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('publisher'),
fetchEntity(),
requireEntityOrCreate('publisherManagement'),
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('publisherMetrics'),
]),
],
requirePublisherManagementAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('publisher'),
fetchEntity(),
requireEntityOrCreate('publisherManagement'),
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('publisherManagement'),
]),
],
requireConversionMetricsAccess: [
AuthenticationController.requireLogin(),
fetchEntityConfig('publisher'),
fetchEntity(),
requireEntityOrCreate('publisherManagement'),
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('publisherMetrics'),
]),
],
requireAdminMetricsAccess: [
AuthenticationController.requireLogin(),
allowAccessIfAny([
UserMembershipAuthorization.hasStaffAccess('adminMetrics'),
]),
],
requireTemplateMetricsAccess: [
AuthenticationController.requireLogin(),
fetchV1Template(),
requireV1Template(),
fetchEntityConfig('publisher'),
fetchPublisherFromTemplate(),
allowAccessIfAny([
UserMembershipAuthorization.hasEntityAccess(),
UserMembershipAuthorization.hasStaffAccess('publisherMetrics'),
]),
],
requirePublisherCreationAccess: [
AuthenticationController.requireLogin(),
allowAccessIfAny([
UserMembershipAuthorization.hasStaffAccess('publisherManagement'),
]),
fetchEntityConfig('publisher'),
],
requireInstitutionCreationAccess: [
AuthenticationController.requireLogin(),
allowAccessIfAny([
UserMembershipAuthorization.hasStaffAccess('institutionManagement'),
]),
fetchEntityConfig('institution'),
],
requireSplitTestMetricsAccess: [
AuthenticationController.requireLogin(),
useAdminCapabilities,
allowAccessIfAny([
UserMembershipAuthorization.hasStaffAccess('splitTestMetrics'),
UserMembershipAuthorization.hasStaffAccess('splitTestManagement'),
UserMembershipAuthorization.hasAdminCapability('view-split-test'),
]),
],
requireSplitTestManagementAccess: [
AuthenticationController.requireLogin(),
useAdminCapabilities,
allowAccessIfAny([
UserMembershipAuthorization.hasStaffAccess('splitTestManagement'),
UserMembershipAuthorization.hasAdminCapability('modify-split-test'),
]),
],
// 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
requireGraphAccess(req, res, next) {
req.params.id = req.query.resource_id
let entityName = req.query.resource_type
if (!entityName) {
return HttpErrorHandler.notFound(req, res, 'resource_type param missing')
}
entityName = entityName.charAt(0).toUpperCase() + entityName.slice(1)
const middleware =
UserMembershipMiddleware[`require${entityName}MetricsAccess`]
if (!middleware) {
return HttpErrorHandler.notFound(
req,
res,
`incorrect entity name: ${entityName}`
)
}
// run the list of middleware functions in series. This is essencially
// a poor man's middleware runner
async.eachSeries(middleware, (fn, callback) => fn(req, res, callback), next)
},
}
module.exports = UserMembershipMiddleware
// fetch entity config and set it in the request
function fetchEntityConfig(entityName) {
return (req, res, next) => {
const entityConfig = EntityConfigs[entityName]
req.entityName = entityName
req.entityConfig = entityConfig
next()
}
}
// fetch the entity with id and config, and set it in the request
function fetchEntity() {
return expressify(async (req, res, next) => {
req.entity =
await UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck(
req.params.id,
req.entityConfig
)
next()
})
}
function fetchPublisherFromTemplate() {
return (req, res, 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 (req, res, 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(creationStaffAccess) {
return (req, res, next) => {
if (req.entity) {
return next()
}
if (UserMembershipAuthorization.hasStaffAccess(creationStaffAccess)(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 (req, res, 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 (req, res, next) => {
if (req.template.id) {
return next()
}
throw new Errors.NotFoundError('no template found')
}
}
// run a serie of synchronous access functions and call `next` if any of the
// retur values is truly. Redirect to restricted otherwise
function allowAccessIfAny(accessFunctions) {
return (req, res, next) => {
for (const accessFunction of accessFunctions) {
if (accessFunction(req)) {
return next()
}
}
HttpErrorHandler.forbidden(req, res)
}
}