mirror of
https://github.com/overleaf/overleaf.git
synced 2025-03-15 04:41:34 +00:00
Merge pull request #2105 from overleaf/ta-user-membership-refactor
UserMembershipAuthorization Refactor GitOrigin-RevId: 7711cda4a134823cbacee42731319fbb8aa648d0
This commit is contained in:
parent
44d3b8b92e
commit
a23ecc9bf8
14 changed files with 427 additions and 930 deletions
|
@ -10,7 +10,6 @@
|
|||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let TemplatesManager
|
||||
const { Project } = require('../../models/Project')
|
||||
const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
|
||||
const ProjectOptionsHandler = require('../Project/ProjectOptionsHandler')
|
||||
|
@ -19,12 +18,15 @@ const ProjectUploadManager = require('../Uploads/ProjectUploadManager')
|
|||
const FileWriter = require('../../infrastructure/FileWriter')
|
||||
const async = require('async')
|
||||
const fs = require('fs')
|
||||
const util = require('util')
|
||||
const logger = require('logger-sharelatex')
|
||||
const request = require('request')
|
||||
const requestPromise = require('request-promise-native')
|
||||
const settings = require('settings-sharelatex')
|
||||
const uuid = require('uuid')
|
||||
const Errors = require('../Errors/Errors')
|
||||
|
||||
module.exports = TemplatesManager = {
|
||||
const TemplatesManager = {
|
||||
createProjectFromV1Template(
|
||||
brandVariationId,
|
||||
compiler,
|
||||
|
@ -158,5 +160,42 @@ module.exports = TemplatesManager = {
|
|||
brandVariationId,
|
||||
callback
|
||||
)
|
||||
},
|
||||
|
||||
promises: {
|
||||
async fetchFromV1(templateId, callback) {
|
||||
let { body, statusCode } = await requestPromise({
|
||||
baseUrl: settings.apis.v1.url,
|
||||
url: `/api/v2/templates/${templateId}`,
|
||||
method: 'GET',
|
||||
auth: {
|
||||
user: settings.apis.v1.user,
|
||||
pass: settings.apis.v1.pass,
|
||||
sendImmediately: true
|
||||
},
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
json: true
|
||||
})
|
||||
|
||||
if (statusCode === 404) {
|
||||
throw new Errors.NotFoundError()
|
||||
}
|
||||
|
||||
if (statusCode !== 200) {
|
||||
logger.warn(
|
||||
{ templateId },
|
||||
"[TemplateMetrics] Couldn't fetch template data from v1"
|
||||
)
|
||||
throw new Error("Couldn't fetch template data from v1")
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TemplatesManager.fetchFromV1 = util.callbackify(
|
||||
TemplatesManager.promises.fetchFromV1
|
||||
)
|
||||
module.exports = TemplatesManager
|
||||
|
|
|
@ -1,360 +1,29 @@
|
|||
/* eslint-disable
|
||||
handle-callback-err,
|
||||
max-len,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let UserMembershipAuthorization
|
||||
const AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
const AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware')
|
||||
const UserMembershipHandler = require('./UserMembershipHandler')
|
||||
const EntityConfigs = require('./UserMembershipEntityConfigs')
|
||||
const Errors = require('../Errors/Errors')
|
||||
const logger = require('logger-sharelatex')
|
||||
const settings = require('settings-sharelatex')
|
||||
const request = require('request')
|
||||
|
||||
module.exports = UserMembershipAuthorization = {
|
||||
requireTeamMetricsAccess(req, res, next) {
|
||||
return requireAccessToEntity(
|
||||
'team',
|
||||
req.params.id,
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
'groupMetrics'
|
||||
)
|
||||
},
|
||||
|
||||
requireGroupManagementAccess(req, res, next) {
|
||||
return requireAccessToEntity(
|
||||
'group',
|
||||
req.params.id,
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
'groupManagement'
|
||||
)
|
||||
},
|
||||
|
||||
requireGroupMetricsAccess(req, res, next) {
|
||||
return requireAccessToEntity(
|
||||
'group',
|
||||
req.params.id,
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
'groupMetrics'
|
||||
)
|
||||
},
|
||||
|
||||
requireGroupManagersManagementAccess(req, res, next) {
|
||||
return requireAccessToEntity(
|
||||
'groupManagers',
|
||||
req.params.id,
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
'groupManagement'
|
||||
)
|
||||
},
|
||||
|
||||
requireInstitutionMetricsAccess(req, res, next) {
|
||||
return requireAccessToEntity(
|
||||
'institution',
|
||||
req.params.id,
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
'institutionMetrics'
|
||||
)
|
||||
},
|
||||
|
||||
requireInstitutionManagementAccess(req, res, next) {
|
||||
return requireAccessToEntity(
|
||||
'institution',
|
||||
req.params.id,
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
'institutionManagement'
|
||||
)
|
||||
},
|
||||
|
||||
requireInstitutionManagementStaffAccess(req, res, next) {
|
||||
return requireAccessToEntity(
|
||||
'institution',
|
||||
req.params.id,
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
'institutionManagement',
|
||||
true
|
||||
)
|
||||
},
|
||||
|
||||
requirePublisherMetricsAccess(req, res, next) {
|
||||
return requireAccessToEntity(
|
||||
'publisher',
|
||||
req.params.id,
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
'publisherMetrics'
|
||||
)
|
||||
},
|
||||
|
||||
requirePublisherManagementAccess(req, res, next) {
|
||||
return requireAccessToEntity(
|
||||
'publisher',
|
||||
req.params.id,
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
'publisherManagement'
|
||||
)
|
||||
},
|
||||
|
||||
requireAdminMetricsStaffAccess(req, res, next) {
|
||||
return requireAccessToEntity(
|
||||
'admin',
|
||||
'admin',
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
'adminMetrics',
|
||||
true
|
||||
)
|
||||
},
|
||||
|
||||
requireTemplateMetricsAccess(req, res, next) {
|
||||
const templateId = req.params.id
|
||||
return request(
|
||||
{
|
||||
baseUrl: settings.apis.v1.url,
|
||||
url: `/api/v2/templates/${templateId}`,
|
||||
method: 'GET',
|
||||
auth: {
|
||||
user: settings.apis.v1.user,
|
||||
pass: settings.apis.v1.pass,
|
||||
sendImmediately: true
|
||||
}
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (response.statusCode === 404) {
|
||||
return next(new Errors.NotFoundError())
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
logger.warn(
|
||||
{ templateId },
|
||||
"[TemplateMetrics] Couldn't fetch template data from v1"
|
||||
)
|
||||
return next(new Error("Couldn't fetch template data from v1"))
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
try {
|
||||
body = JSON.parse(body)
|
||||
} catch (error1) {
|
||||
error = error1
|
||||
return next(error)
|
||||
}
|
||||
|
||||
req.template = {
|
||||
id: body.id,
|
||||
title: body.title
|
||||
}
|
||||
if (__guard__(body != null ? body.brand : undefined, x => x.slug)) {
|
||||
req.params.id = body.brand.slug
|
||||
return UserMembershipAuthorization.requirePublisherMetricsAccess(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
)
|
||||
} else {
|
||||
return AuthorizationMiddleware.ensureUserIsSiteAdmin(req, res, next)
|
||||
}
|
||||
let UserMembershipAuthorization = {
|
||||
hasStaffAccess(requiredStaffAccess) {
|
||||
return req => {
|
||||
if (!req.user) {
|
||||
return false
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
requireGraphAccess(req, res, next) {
|
||||
req.params.id = req.query.resource_id
|
||||
if (req.query.resource_type === 'template') {
|
||||
return UserMembershipAuthorization.requireTemplateMetricsAccess(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
)
|
||||
} else if (req.query.resource_type === 'institution') {
|
||||
return UserMembershipAuthorization.requireInstitutionMetricsAccess(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
)
|
||||
} else if (req.query.resource_type === 'group') {
|
||||
return UserMembershipAuthorization.requireGroupMetricsAccess(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
)
|
||||
} else if (req.query.resource_type === 'team') {
|
||||
return UserMembershipAuthorization.requireTeamMetricsAccess(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
)
|
||||
} else if (req.query.resource_type === 'admin') {
|
||||
return UserMembershipAuthorization.requireAdminMetricsStaffAccess(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
if (req.user.isAdmin) {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
requiredStaffAccess &&
|
||||
req.user.staffAccess &&
|
||||
req.user.staffAccess[requiredStaffAccess]
|
||||
)
|
||||
}
|
||||
return requireAccessToEntity(
|
||||
req.query.resource_type,
|
||||
req.query.resource_id,
|
||||
req,
|
||||
res,
|
||||
next
|
||||
)
|
||||
},
|
||||
|
||||
requireEntityCreationAccess(req, res, next) {
|
||||
const loggedInUser = AuthenticationController.getSessionUser(req)
|
||||
if (!loggedInUser || !hasEntityCreationAccess(loggedInUser)) {
|
||||
return AuthorizationMiddleware.redirectToRestricted(req, res, next)
|
||||
hasEntityAccess() {
|
||||
return req => {
|
||||
if (!req.entity) {
|
||||
return false
|
||||
}
|
||||
return req.entity[req.entityConfig.fields.access].some(accessUserId =>
|
||||
accessUserId.equals(req.user._id)
|
||||
)
|
||||
}
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
var requireAccessToEntity = function(
|
||||
entityName,
|
||||
entityId,
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
requiredStaffAccess = null,
|
||||
asStaff
|
||||
) {
|
||||
if (asStaff == null) {
|
||||
asStaff = false
|
||||
}
|
||||
const loggedInUser = AuthenticationController.getSessionUser(req)
|
||||
if (!loggedInUser) {
|
||||
return AuthorizationMiddleware.redirectToRestricted(req, res, next)
|
||||
}
|
||||
|
||||
if (asStaff) {
|
||||
if (
|
||||
!loggedInUser.isAdmin &&
|
||||
!(loggedInUser.staffAccess != null
|
||||
? loggedInUser.staffAccess[requiredStaffAccess]
|
||||
: undefined)
|
||||
) {
|
||||
return AuthorizationMiddleware.redirectToRestricted(req, res, next)
|
||||
}
|
||||
}
|
||||
|
||||
return getEntity(
|
||||
entityName,
|
||||
entityId,
|
||||
loggedInUser,
|
||||
requiredStaffAccess,
|
||||
function(error, entity, entityConfig, entityExists) {
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
|
||||
if (entity != null) {
|
||||
req.entity = entity
|
||||
req.entityConfig = entityConfig
|
||||
return next()
|
||||
}
|
||||
|
||||
if (entityExists) {
|
||||
// user doesn't have access to entity
|
||||
return AuthorizationMiddleware.redirectToRestricted(req, res, next)
|
||||
}
|
||||
|
||||
if (hasEntityCreationAccess(loggedInUser) && entityConfig.canCreate) {
|
||||
// entity doesn't exists, admin can create it
|
||||
return res.redirect(`/entities/${entityName}/create/${entityId}`)
|
||||
}
|
||||
|
||||
return next(new Errors.NotFoundError())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var getEntity = function(
|
||||
entityName,
|
||||
entityId,
|
||||
user,
|
||||
requiredStaffAccess,
|
||||
callback
|
||||
) {
|
||||
if (callback == null) {
|
||||
callback = function(error, entity, entityConfig, entityExists) {}
|
||||
}
|
||||
const entityConfig = EntityConfigs[entityName]
|
||||
if (!entityConfig) {
|
||||
return callback(new Errors.NotFoundError(`No such entity: ${entityName}`))
|
||||
}
|
||||
if (!entityConfig.modelName) {
|
||||
return callback(null, { id: entityName }, entityConfig, true)
|
||||
}
|
||||
|
||||
return UserMembershipHandler.getEntity(
|
||||
entityId,
|
||||
entityConfig,
|
||||
user,
|
||||
requiredStaffAccess,
|
||||
function(error, entity) {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
if (entity != null) {
|
||||
return callback(null, entity, entityConfig, true)
|
||||
}
|
||||
|
||||
// no access to entity. Check if entity exists
|
||||
return UserMembershipHandler.getEntityWithoutAuthorizationCheck(
|
||||
entityId,
|
||||
entityConfig,
|
||||
function(error, entity) {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return callback(null, null, entityConfig, entity != null)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var hasEntityCreationAccess = user =>
|
||||
user.isAdmin ||
|
||||
(user.staffAccess != null
|
||||
? user.staffAccess['institutionManagement']
|
||||
: undefined) ||
|
||||
(user.staffAccess != null
|
||||
? user.staffAccess['publisherManagement']
|
||||
: undefined)
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
||||
module.exports = UserMembershipAuthorization
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
*/
|
||||
const AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
const UserMembershipHandler = require('./UserMembershipHandler')
|
||||
const EntityConfigs = require('./UserMembershipEntityConfigs')
|
||||
const Errors = require('../Errors/Errors')
|
||||
const EmailHelper = require('../Helpers/EmailHelper')
|
||||
const logger = require('logger-sharelatex')
|
||||
|
@ -161,15 +160,8 @@ module.exports = {
|
|||
},
|
||||
|
||||
create(req, res, next) {
|
||||
const entityName = req.params.name
|
||||
const entityId = req.params.id
|
||||
const entityConfig = EntityConfigs[entityName]
|
||||
if (!entityConfig) {
|
||||
return next(new Errors.NotFoundError(`No such entity: ${entityName}`))
|
||||
}
|
||||
if (!entityConfig.canCreate) {
|
||||
return next(new Errors.NotFoundError(`Cannot create new ${entityName}`))
|
||||
}
|
||||
const entityConfig = req.entityConfig
|
||||
|
||||
return UserMembershipHandler.createEntity(entityId, entityConfig, function(
|
||||
error,
|
||||
|
|
|
@ -1,10 +1,3 @@
|
|||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Sanity-check the conversion and remove this comment.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
module.exports = {
|
||||
group: {
|
||||
modelName: 'Subscription',
|
||||
|
@ -74,7 +67,6 @@ module.exports = {
|
|||
|
||||
institution: {
|
||||
modelName: 'Institution',
|
||||
canCreate: true,
|
||||
fields: {
|
||||
primaryKey: 'v1Id',
|
||||
read: ['managerIds'],
|
||||
|
@ -98,7 +90,6 @@ module.exports = {
|
|||
|
||||
publisher: {
|
||||
modelName: 'Publisher',
|
||||
canCreate: true,
|
||||
fields: {
|
||||
primaryKey: 'slug',
|
||||
read: ['managerIds'],
|
||||
|
@ -118,19 +109,5 @@ module.exports = {
|
|||
removeMember: `/manage/publishers/${id}/managers`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
conversion: {
|
||||
// for metrics only
|
||||
modelName: 'Publisher',
|
||||
fields: {
|
||||
primaryKey: 'slug',
|
||||
access: 'managerIds'
|
||||
}
|
||||
},
|
||||
|
||||
admin: {
|
||||
// for metrics only
|
||||
modelName: null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
const { ObjectId } = require('mongoose').Types
|
||||
const async = require('async')
|
||||
const { promisifyAll } = require('../../util/promises')
|
||||
const Errors = require('../Errors/Errors')
|
||||
const EntityModels = {
|
||||
Institution: require('../../models/Institution').Institution,
|
||||
|
@ -26,29 +27,7 @@ const UserGetter = require('../User/UserGetter')
|
|||
const logger = require('logger-sharelatex')
|
||||
const UserMembershipEntityConfigs = require('./UserMembershipEntityConfigs')
|
||||
|
||||
module.exports = {
|
||||
getEntity(
|
||||
entityId,
|
||||
entityConfig,
|
||||
loggedInUser,
|
||||
requiredStaffAccess,
|
||||
callback
|
||||
) {
|
||||
if (callback == null) {
|
||||
callback = function(error, entity) {}
|
||||
}
|
||||
const query = buildEntityQuery(entityId, entityConfig)
|
||||
if (
|
||||
!loggedInUser.isAdmin &&
|
||||
!(loggedInUser.staffAccess != null
|
||||
? loggedInUser.staffAccess[requiredStaffAccess]
|
||||
: undefined)
|
||||
) {
|
||||
query[entityConfig.fields.access] = ObjectId(loggedInUser._id)
|
||||
}
|
||||
return EntityModels[entityConfig.modelName].findOne(query, callback)
|
||||
},
|
||||
|
||||
const UserMembershipHandler = {
|
||||
getEntityWithoutAuthorizationCheck(entityId, entityConfig, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, entity) {}
|
||||
|
@ -107,6 +86,9 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
|
||||
UserMembershipHandler.promises = promisifyAll(UserMembershipHandler)
|
||||
module.exports = UserMembershipHandler
|
||||
|
||||
var getPopulatedListOfMembers = function(entity, attributes, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, users) {}
|
||||
|
|
|
@ -0,0 +1,273 @@
|
|||
const expressify = require('../../util/expressify')
|
||||
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 HttpErrors = require('@overleaf/o-error/http')
|
||||
const TemplatesManager = require('../Templates/TemplatesManager')
|
||||
|
||||
// set of middleware arrays or functions that checks user access to an entity
|
||||
// (publisher, institution, group, template, etc.)
|
||||
let UserMembershipMiddleware = {
|
||||
requireTeamMetricsAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
fetchEntityConfig('team'),
|
||||
fetchEntity(),
|
||||
requireEntity(),
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasEntityAccess(),
|
||||
UserMembershipAuthorization.hasStaffAccess('groupMetrics')
|
||||
])
|
||||
],
|
||||
|
||||
requireGroupManagementAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
fetchEntityConfig('group'),
|
||||
fetchEntity(),
|
||||
requireEntity(),
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasEntityAccess(),
|
||||
UserMembershipAuthorization.hasStaffAccess('groupManagement')
|
||||
])
|
||||
],
|
||||
|
||||
requireGroupMetricsAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
fetchEntityConfig('group'),
|
||||
fetchEntity(),
|
||||
requireEntity(),
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasEntityAccess(),
|
||||
UserMembershipAuthorization.hasStaffAccess('groupMetrics')
|
||||
])
|
||||
],
|
||||
|
||||
requireGroupManagersManagementAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
fetchEntityConfig('groupManagers'),
|
||||
fetchEntity(),
|
||||
requireEntity(),
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasEntityAccess(),
|
||||
UserMembershipAuthorization.hasStaffAccess('groupManagement')
|
||||
])
|
||||
],
|
||||
|
||||
requireInstitutionMetricsAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
fetchEntityConfig('institution'),
|
||||
fetchEntity(),
|
||||
requireEntityOrCreate('institutionManagement'),
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasEntityAccess(),
|
||||
UserMembershipAuthorization.hasStaffAccess('institutionMetrics')
|
||||
])
|
||||
],
|
||||
|
||||
requireInstitutionManagementAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
fetchEntityConfig('institution'),
|
||||
fetchEntity(),
|
||||
requireEntityOrCreate('institutionManagement'),
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasEntityAccess(),
|
||||
UserMembershipAuthorization.hasStaffAccess('institutionManagement')
|
||||
])
|
||||
],
|
||||
|
||||
requireInstitutionManagementStaffAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasStaffAccess('institutionManagement')
|
||||
]),
|
||||
fetchEntityConfig('institution'),
|
||||
fetchEntity(),
|
||||
requireEntityOrCreate('institutionManagement')
|
||||
],
|
||||
|
||||
requirePublisherMetricsAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
fetchEntityConfig('publisher'),
|
||||
fetchEntity(),
|
||||
requireEntityOrCreate('publisherManagement'),
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasEntityAccess(),
|
||||
UserMembershipAuthorization.hasStaffAccess('publisherMetrics')
|
||||
])
|
||||
],
|
||||
|
||||
requirePublisherManagementAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
fetchEntityConfig('publisher'),
|
||||
fetchEntity(),
|
||||
requireEntityOrCreate('publisherManagement'),
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasEntityAccess(),
|
||||
UserMembershipAuthorization.hasStaffAccess('publisherManagement')
|
||||
])
|
||||
],
|
||||
|
||||
requireConversionMetricsAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
fetchEntityConfig('publisher'),
|
||||
fetchEntity(),
|
||||
requireEntityOrCreate('publisherManagement'),
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasEntityAccess(),
|
||||
UserMembershipAuthorization.hasStaffAccess('publisherMetrics')
|
||||
])
|
||||
],
|
||||
|
||||
requireAdminMetricsAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
restrictAccess([UserMembershipAuthorization.hasStaffAccess('adminMetrics')])
|
||||
],
|
||||
|
||||
requireTemplateMetricsAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
fetchV1Template(),
|
||||
requireV1Template(),
|
||||
fetchEntityConfig('publisher'),
|
||||
fetchEntity(), // at this point the entity is the template's publisher, if any
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasEntityAccess(),
|
||||
UserMembershipAuthorization.hasStaffAccess('publisherMetrics')
|
||||
])
|
||||
],
|
||||
|
||||
requirePublisherCreationAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasStaffAccess('publisherManagement')
|
||||
]),
|
||||
fetchEntityConfig('publisher')
|
||||
],
|
||||
|
||||
requireInstitutionCreationAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
restrictAccess([
|
||||
UserMembershipAuthorization.hasStaffAccess('institutionManagement')
|
||||
]),
|
||||
fetchEntityConfig('institution')
|
||||
],
|
||||
|
||||
// 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
|
||||
entityName = entityName.charAt(0).toUpperCase() + entityName.slice(1)
|
||||
|
||||
// run the list of middleware functions in series. This is essencially
|
||||
// a poor man's middleware runner
|
||||
async.eachSeries(
|
||||
UserMembershipMiddleware[`require${entityName}MetricsAccess`],
|
||||
(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) => {
|
||||
let entity = await UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck(
|
||||
req.params.id,
|
||||
req.entityConfig
|
||||
)
|
||||
req.entity = entity
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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 restrictAccess(accessFunctions) {
|
||||
return (req, res, next) => {
|
||||
for (let accessFunction of accessFunctions) {
|
||||
if (accessFunction(req)) {
|
||||
return next()
|
||||
}
|
||||
}
|
||||
next(new HttpErrors.ForbiddenError({}))
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const UserMembershipAuthorization = require('./UserMembershipAuthorization')
|
||||
const UserMembershipMiddleware = require('./UserMembershipMiddleware')
|
||||
const UserMembershipController = require('./UserMembershipController')
|
||||
const SubscriptionGroupController = require('../Subscription/SubscriptionGroupController')
|
||||
const TeamInvitesController = require('../Subscription/TeamInvitesController')
|
||||
|
@ -16,12 +16,12 @@ module.exports = {
|
|||
// group members routes
|
||||
webRouter.get(
|
||||
'/manage/groups/:id/members',
|
||||
UserMembershipAuthorization.requireGroupManagementAccess,
|
||||
UserMembershipMiddleware.requireGroupManagementAccess,
|
||||
UserMembershipController.index
|
||||
)
|
||||
webRouter.post(
|
||||
'/manage/groups/:id/invites',
|
||||
UserMembershipAuthorization.requireGroupManagementAccess,
|
||||
UserMembershipMiddleware.requireGroupManagementAccess,
|
||||
RateLimiterMiddleware.rateLimit({
|
||||
endpointName: 'create-team-invite',
|
||||
maxRequests: 100,
|
||||
|
@ -31,17 +31,17 @@ module.exports = {
|
|||
)
|
||||
webRouter.delete(
|
||||
'/manage/groups/:id/user/:user_id',
|
||||
UserMembershipAuthorization.requireGroupManagementAccess,
|
||||
UserMembershipMiddleware.requireGroupManagementAccess,
|
||||
SubscriptionGroupController.removeUserFromGroup
|
||||
)
|
||||
webRouter.delete(
|
||||
'/manage/groups/:id/invites/:email',
|
||||
UserMembershipAuthorization.requireGroupManagementAccess,
|
||||
UserMembershipMiddleware.requireGroupManagementAccess,
|
||||
TeamInvitesController.revokeInvite
|
||||
)
|
||||
webRouter.get(
|
||||
'/manage/groups/:id/members/export',
|
||||
UserMembershipAuthorization.requireGroupManagementAccess,
|
||||
UserMembershipMiddleware.requireGroupManagementAccess,
|
||||
RateLimiterMiddleware.rateLimit({
|
||||
endpointName: 'export-team-csv',
|
||||
maxRequests: 30,
|
||||
|
@ -53,63 +53,75 @@ module.exports = {
|
|||
// group managers routes
|
||||
webRouter.get(
|
||||
'/manage/groups/:id/managers',
|
||||
UserMembershipAuthorization.requireGroupManagersManagementAccess,
|
||||
UserMembershipMiddleware.requireGroupManagersManagementAccess,
|
||||
UserMembershipController.index
|
||||
)
|
||||
webRouter.post(
|
||||
'/manage/groups/:id/managers',
|
||||
UserMembershipAuthorization.requireGroupManagersManagementAccess,
|
||||
UserMembershipMiddleware.requireGroupManagersManagementAccess,
|
||||
UserMembershipController.add
|
||||
)
|
||||
webRouter.delete(
|
||||
'/manage/groups/:id/managers/:userId',
|
||||
UserMembershipAuthorization.requireGroupManagersManagementAccess,
|
||||
UserMembershipMiddleware.requireGroupManagersManagementAccess,
|
||||
UserMembershipController.remove
|
||||
)
|
||||
|
||||
// institution members routes
|
||||
webRouter.get(
|
||||
'/manage/institutions/:id/managers',
|
||||
UserMembershipAuthorization.requireInstitutionManagementAccess,
|
||||
UserMembershipMiddleware.requireInstitutionManagementAccess,
|
||||
UserMembershipController.index
|
||||
)
|
||||
webRouter.post(
|
||||
'/manage/institutions/:id/managers',
|
||||
UserMembershipAuthorization.requireInstitutionManagementAccess,
|
||||
UserMembershipMiddleware.requireInstitutionManagementAccess,
|
||||
UserMembershipController.add
|
||||
)
|
||||
webRouter.delete(
|
||||
'/manage/institutions/:id/managers/:userId',
|
||||
UserMembershipAuthorization.requireInstitutionManagementAccess,
|
||||
UserMembershipMiddleware.requireInstitutionManagementAccess,
|
||||
UserMembershipController.remove
|
||||
)
|
||||
|
||||
// publisher members routes
|
||||
webRouter.get(
|
||||
'/manage/publishers/:id/managers',
|
||||
UserMembershipAuthorization.requirePublisherManagementAccess,
|
||||
UserMembershipMiddleware.requirePublisherManagementAccess,
|
||||
UserMembershipController.index
|
||||
)
|
||||
webRouter.post(
|
||||
'/manage/publishers/:id/managers',
|
||||
UserMembershipAuthorization.requirePublisherManagementAccess,
|
||||
UserMembershipMiddleware.requirePublisherManagementAccess,
|
||||
UserMembershipController.add
|
||||
)
|
||||
webRouter.delete(
|
||||
'/manage/publishers/:id/managers/:userId',
|
||||
UserMembershipAuthorization.requirePublisherManagementAccess,
|
||||
UserMembershipMiddleware.requirePublisherManagementAccess,
|
||||
UserMembershipController.remove
|
||||
)
|
||||
|
||||
// create new entitites
|
||||
// publisher creation routes
|
||||
webRouter.get(
|
||||
'/entities/:name/create/:id',
|
||||
UserMembershipAuthorization.requireEntityCreationAccess,
|
||||
'/entities/publisher/create/:id',
|
||||
UserMembershipMiddleware.requirePublisherCreationAccess,
|
||||
UserMembershipController.new
|
||||
)
|
||||
return webRouter.post(
|
||||
'/entities/:name/create/:id',
|
||||
UserMembershipAuthorization.requireEntityCreationAccess,
|
||||
webRouter.post(
|
||||
'/entities/publisher/create/:id',
|
||||
UserMembershipMiddleware.requirePublisherCreationAccess,
|
||||
UserMembershipController.create
|
||||
)
|
||||
|
||||
// institution creation routes
|
||||
webRouter.get(
|
||||
'/entities/institution/create/:id',
|
||||
UserMembershipMiddleware.requireInstitutionCreationAccess,
|
||||
UserMembershipController.new
|
||||
)
|
||||
webRouter.post(
|
||||
'/entities/institution/create/:id',
|
||||
UserMembershipMiddleware.requireInstitutionCreationAccess,
|
||||
UserMembershipController.create
|
||||
)
|
||||
}
|
||||
|
|
5
services/web/app/src/util/expressify.js
Normal file
5
services/web/app/src/util/expressify.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = function expressify(fn) {
|
||||
return (req, res, next) => {
|
||||
fn(req, res, next).catch(next)
|
||||
}
|
||||
}
|
30
services/web/package-lock.json
generated
30
services/web/package-lock.json
generated
|
@ -17110,6 +17110,31 @@
|
|||
"uuid": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"request-promise-core": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz",
|
||||
"integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"request-promise-native": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz",
|
||||
"integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==",
|
||||
"requires": {
|
||||
"request-promise-core": "1.1.2",
|
||||
"stealthy-require": "^1.1.1",
|
||||
"tough-cookie": "^2.3.3"
|
||||
}
|
||||
},
|
||||
"requestretry": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/requestretry/-/requestretry-1.13.0.tgz",
|
||||
|
@ -18581,6 +18606,11 @@
|
|||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
|
||||
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
|
||||
},
|
||||
"stealthy-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
||||
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
|
||||
},
|
||||
"stream-browserify": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
|
||||
|
|
|
@ -98,6 +98,7 @@
|
|||
"react-dom": "^15.4.2",
|
||||
"redis-sharelatex": "^1.0.9",
|
||||
"request": "^2.69.0",
|
||||
"request-promise-native": "^1.0.7",
|
||||
"requestretry": "^1.13.0",
|
||||
"rimraf": "2.2.6",
|
||||
"rolling-rate-limiter": "git+https://github.com/ShaneKilkelly/rolling-rate-limiter.git#master",
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
const url = `/metrics/teams/123`
|
||||
async.series(
|
||||
[
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
cb => this.subscription.setManagerIds([this.user._id], cb),
|
||||
expectAccess(this.user, url, 200)
|
||||
],
|
||||
|
@ -62,7 +62,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
const url = `/manage/groups/${this.subscription._id}/members`
|
||||
async.series(
|
||||
[
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
cb => this.subscription.setManagerIds([this.user._id], cb),
|
||||
expectAccess(this.user, url, 200)
|
||||
],
|
||||
|
@ -76,7 +76,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
const url = `/metrics/groups/${this.subscription._id}`
|
||||
async.series(
|
||||
[
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
cb => this.subscription.setManagerIds([this.user._id], cb),
|
||||
expectAccess(this.user, url, 200)
|
||||
],
|
||||
|
@ -95,7 +95,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
const url = `/manage/groups/${this.subscription._id}/managers`
|
||||
async.series(
|
||||
[
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
cb => this.subscription.setManagerIds([this.user._id], cb),
|
||||
expectAccess(this.user, url, 200)
|
||||
],
|
||||
|
@ -151,10 +151,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
it('should not allow users without access', function(done) {
|
||||
const url = `/metrics/institutions/${this.institution.v1Id}`
|
||||
async.series(
|
||||
[
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 302, /\/restricted/)
|
||||
],
|
||||
[this.user.login.bind(this.user), expectAccess(this.user, url, 403)],
|
||||
done
|
||||
)
|
||||
})
|
||||
|
@ -166,7 +163,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
async.series(
|
||||
[
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
cb => this.institution.setManagerIds([this.user._id], cb),
|
||||
expectAccess(this.user, url, 200)
|
||||
],
|
||||
|
@ -181,7 +178,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
async.series(
|
||||
[
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
cb => this.institution.setManagerIds([this.user._id], cb),
|
||||
expectAccess(this.user, url, 200)
|
||||
],
|
||||
|
@ -196,7 +193,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
async.series(
|
||||
[
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
cb => this.user.ensureStaffAccess('institutionManagement', cb),
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 200)
|
||||
|
@ -224,7 +221,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
const url = `/metrics/conversions/${this.publisher.slug}`
|
||||
async.series(
|
||||
[
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
cb => this.publisher.setManagerIds([this.user._id], cb),
|
||||
expectAccess(this.user, url, 200)
|
||||
],
|
||||
|
@ -238,7 +235,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
const url = `/manage/publishers/${this.publisher.slug}/managers`
|
||||
async.series(
|
||||
[
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
cb => this.publisher.setManagerIds([this.user._id], cb),
|
||||
expectAccess(this.user, url, 200)
|
||||
],
|
||||
|
@ -266,7 +263,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
const url = `/entities/publisher/create/foo`
|
||||
async.series(
|
||||
[
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
cb => this.user.ensureStaffAccess('publisherManagement', cb),
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 200)
|
||||
|
@ -300,7 +297,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
const url = '/metrics/templates/123'
|
||||
async.series(
|
||||
[
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
cb => this.publisher.setManagerIds([this.user._id], cb),
|
||||
expectAccess(this.user, url, 200)
|
||||
],
|
||||
|
@ -319,7 +316,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
const url = '/metrics/templates/456'
|
||||
async.series(
|
||||
[
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
this.user.ensure_admin.bind(this.user),
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 200)
|
||||
|
@ -347,7 +344,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
async.series(
|
||||
[
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 302, /\/restricted/),
|
||||
expectAccess(this.user, url, 403),
|
||||
this.user.ensure_admin.bind(this.user),
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 200)
|
||||
|
@ -359,14 +356,14 @@ describe('UserMembershipAuthorization', function() {
|
|||
|
||||
describe('admin metrics', function() {
|
||||
it('should not allow anonymous users', function(done) {
|
||||
expectAccess(this.user, '/metrics/admin', 302, /\/restricted/)(done)
|
||||
expectAccess(this.user, '/metrics/admin', 302, /\/login/)(done)
|
||||
})
|
||||
|
||||
it('should not allow all users', function(done) {
|
||||
async.series(
|
||||
[
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, '/metrics/admin', 302, /\/restricted/)
|
||||
expectAccess(this.user, '/metrics/admin', 403)
|
||||
],
|
||||
done
|
||||
)
|
||||
|
|
|
@ -1,339 +0,0 @@
|
|||
/* eslint-disable
|
||||
handle-callback-err,
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const sinon = require('sinon')
|
||||
const chai = require('chai')
|
||||
const { expect } = require('chai')
|
||||
const modulePath =
|
||||
'../../../../app/src/Features/UserMembership/UserMembershipAuthorization.js'
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const MockRequest = require('../helpers/MockRequest')
|
||||
const EntityConfigs = require('../../../../app/src/Features/UserMembership/UserMembershipEntityConfigs')
|
||||
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
||||
|
||||
describe('UserMembershipAuthorization', function() {
|
||||
beforeEach(function() {
|
||||
this.req = new MockRequest()
|
||||
this.req.params.id = 'mock-entity-id'
|
||||
this.user = { _id: 'mock-user-id' }
|
||||
this.subscription = { _id: 'mock-subscription-id' }
|
||||
|
||||
this.AuthenticationController = {
|
||||
getSessionUser: sinon.stub().returns(this.user)
|
||||
}
|
||||
this.UserMembershipHandler = {
|
||||
getEntity: sinon.stub().yields(null, this.subscription),
|
||||
getEntityWithoutAuthorizationCheck: sinon
|
||||
.stub()
|
||||
.yields(null, this.subscription)
|
||||
}
|
||||
this.AuthorizationMiddleware = {
|
||||
redirectToRestricted: sinon.stub().yields(),
|
||||
ensureUserIsSiteAdmin: sinon.stub().yields()
|
||||
}
|
||||
return (this.UserMembershipAuthorization = SandboxedModule.require(
|
||||
modulePath,
|
||||
{
|
||||
globals: {
|
||||
console: console
|
||||
},
|
||||
requires: {
|
||||
'../Authentication/AuthenticationController': this
|
||||
.AuthenticationController,
|
||||
'../Authorization/AuthorizationMiddleware': this
|
||||
.AuthorizationMiddleware,
|
||||
'./UserMembershipHandler': this.UserMembershipHandler,
|
||||
'./EntityConfigs': EntityConfigs,
|
||||
'../Errors/Errors': Errors,
|
||||
request: (this.request = sinon.stub().yields(null, null, {})),
|
||||
'logger-sharelatex': {
|
||||
log() {},
|
||||
warn() {},
|
||||
err() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
})
|
||||
|
||||
describe('requireAccessToEntity', function() {
|
||||
it('get entity', function(done) {
|
||||
return this.UserMembershipAuthorization.requireGroupMetricsAccess(
|
||||
this.req,
|
||||
null,
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
sinon.assert.calledWithMatch(
|
||||
this.UserMembershipHandler.getEntity,
|
||||
this.req.params.id,
|
||||
{ modelName: 'Subscription' },
|
||||
this.user
|
||||
)
|
||||
expect(this.req.entity).to.equal(this.subscription)
|
||||
expect(this.req.entityConfig).to.exist
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle entity not found as non-admin', function(done) {
|
||||
this.UserMembershipHandler.getEntity.yields(null, null)
|
||||
this.UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(
|
||||
null,
|
||||
null
|
||||
)
|
||||
return this.UserMembershipAuthorization.requireGroupMetricsAccess(
|
||||
this.req,
|
||||
null,
|
||||
error => {
|
||||
expect(error).to.exist
|
||||
expect(error).to.be.instanceof(Error)
|
||||
expect(error.constructor.name).to.equal('NotFoundError')
|
||||
sinon.assert.called(this.UserMembershipHandler.getEntity)
|
||||
expect(this.req.entity).to.not.exist
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle entity not found an admin can create', function(done) {
|
||||
this.user.isAdmin = true
|
||||
this.UserMembershipHandler.getEntity.yields(null, null)
|
||||
this.UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(
|
||||
null,
|
||||
null
|
||||
)
|
||||
return this.UserMembershipAuthorization.requirePublisherMetricsAccess(
|
||||
this.req,
|
||||
{
|
||||
redirect: path => {
|
||||
expect(path).to.exist
|
||||
expect(path).to.match(/create/)
|
||||
return done()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle entity not found a non-admin can create', function(done) {
|
||||
this.user.staffAccess = { institutionManagement: true }
|
||||
this.UserMembershipHandler.getEntity.yields(null, null)
|
||||
this.UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(
|
||||
null,
|
||||
null
|
||||
)
|
||||
return this.UserMembershipAuthorization.requirePublisherMetricsAccess(
|
||||
this.req,
|
||||
{
|
||||
redirect: path => {
|
||||
expect(path).to.exist
|
||||
expect(path).to.match(/create/)
|
||||
return done()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle entity not found an admin cannot create', function(done) {
|
||||
this.user.isAdmin = true
|
||||
this.UserMembershipHandler.getEntity.yields(null, null)
|
||||
this.UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(
|
||||
null,
|
||||
null
|
||||
)
|
||||
return this.UserMembershipAuthorization.requireGroupMetricsAccess(
|
||||
this.req,
|
||||
null,
|
||||
error => {
|
||||
expect(error).to.exist
|
||||
expect(error).to.be.instanceof(Error)
|
||||
expect(error.constructor.name).to.equal('NotFoundError')
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle entity no access', function(done) {
|
||||
this.UserMembershipHandler.getEntity.yields(null, null)
|
||||
return this.UserMembershipAuthorization.requireGroupMetricsAccess(
|
||||
this.req,
|
||||
null,
|
||||
error => {
|
||||
sinon.assert.called(this.AuthorizationMiddleware.redirectToRestricted)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle anonymous user', function(done) {
|
||||
this.AuthenticationController.getSessionUser.returns(null)
|
||||
return this.UserMembershipAuthorization.requireGroupMetricsAccess(
|
||||
this.req,
|
||||
null,
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
sinon.assert.called(this.AuthorizationMiddleware.redirectToRestricted)
|
||||
sinon.assert.notCalled(this.UserMembershipHandler.getEntity)
|
||||
expect(this.req.entity).to.not.exist
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('checks user is staff if required', function(done) {
|
||||
return this.UserMembershipAuthorization.requireInstitutionManagementStaffAccess(
|
||||
this.req,
|
||||
null,
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
sinon.assert.called(this.AuthorizationMiddleware.redirectToRestricted)
|
||||
sinon.assert.notCalled(this.UserMembershipHandler.getEntity)
|
||||
expect(this.req.entity).to.not.exist
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('requireEntityAccess', function() {
|
||||
it('handle team access', function(done) {
|
||||
return this.UserMembershipAuthorization.requireTeamMetricsAccess(
|
||||
this.req,
|
||||
null,
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
sinon.assert.calledWithMatch(
|
||||
this.UserMembershipHandler.getEntity,
|
||||
this.req.params.id,
|
||||
{ fields: { primaryKey: 'overleaf.id' } }
|
||||
)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle group access', function(done) {
|
||||
return this.UserMembershipAuthorization.requireGroupMetricsAccess(
|
||||
this.req,
|
||||
null,
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
sinon.assert.calledWithMatch(
|
||||
this.UserMembershipHandler.getEntity,
|
||||
this.req.params.id,
|
||||
{ translations: { title: 'group_account' } }
|
||||
)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle group managers access', function(done) {
|
||||
return this.UserMembershipAuthorization.requireGroupManagersManagementAccess(
|
||||
this.req,
|
||||
null,
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
sinon.assert.calledWithMatch(
|
||||
this.UserMembershipHandler.getEntity,
|
||||
this.req.params.id,
|
||||
{ translations: { subtitle: 'managers_management' } }
|
||||
)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle institution access', function(done) {
|
||||
return this.UserMembershipAuthorization.requireInstitutionMetricsAccess(
|
||||
this.req,
|
||||
null,
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
sinon.assert.calledWithMatch(
|
||||
this.UserMembershipHandler.getEntity,
|
||||
this.req.params.id,
|
||||
{ modelName: 'Institution' }
|
||||
)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle template with brand access', function(done) {
|
||||
const templateData = {
|
||||
id: 123,
|
||||
title: 'Template Title',
|
||||
brand: { slug: 'brand-slug' }
|
||||
}
|
||||
this.request.yields(
|
||||
null,
|
||||
{ statusCode: 200 },
|
||||
JSON.stringify(templateData)
|
||||
)
|
||||
return this.UserMembershipAuthorization.requireTemplateMetricsAccess(
|
||||
this.req,
|
||||
null,
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
sinon.assert.calledWithMatch(
|
||||
this.UserMembershipHandler.getEntity,
|
||||
'brand-slug',
|
||||
{ modelName: 'Publisher' }
|
||||
)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle template without brand access', function(done) {
|
||||
const templateData = {
|
||||
id: 123,
|
||||
title: 'Template Title',
|
||||
brand: null
|
||||
}
|
||||
this.request.yields(
|
||||
null,
|
||||
{ statusCode: 200 },
|
||||
JSON.stringify(templateData)
|
||||
)
|
||||
return this.UserMembershipAuthorization.requireTemplateMetricsAccess(
|
||||
this.req,
|
||||
null,
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
sinon.assert.notCalled(this.UserMembershipHandler.getEntity)
|
||||
sinon.assert.calledOnce(
|
||||
this.AuthorizationMiddleware.ensureUserIsSiteAdmin
|
||||
)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle graph access', function(done) {
|
||||
this.req.query.resource_id = 'mock-resource-id'
|
||||
this.req.query.resource_type = 'institution'
|
||||
const middleware = this.UserMembershipAuthorization.requireGraphAccess
|
||||
return middleware(this.req, null, error => {
|
||||
expect(error).to.not.exist
|
||||
sinon.assert.calledWithMatch(
|
||||
this.UserMembershipHandler.getEntity,
|
||||
this.req.query.resource_id,
|
||||
{ modelName: 'Institution' }
|
||||
)
|
||||
return done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -336,6 +336,7 @@ describe('UserMembershipController', function() {
|
|||
describe('create', function() {
|
||||
beforeEach(function() {
|
||||
this.req.params.name = 'institution'
|
||||
this.req.entityConfig = EntityConfigs['institution']
|
||||
return (this.req.params.id = 123)
|
||||
})
|
||||
|
||||
|
@ -352,15 +353,5 @@ describe('UserMembershipController', function() {
|
|||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('checks canCreate', function(done) {
|
||||
this.req.params.name = 'group'
|
||||
return this.UserMembershipController.create(this.req, null, error => {
|
||||
expect(error).to.extist
|
||||
expect(error).to.be.an.instanceof(Errors.NotFoundError)
|
||||
sinon.assert.notCalled(this.UserMembershipHandler.createEntity)
|
||||
return done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -92,81 +92,6 @@ describe('UserMembershipHandler', function() {
|
|||
}))
|
||||
})
|
||||
|
||||
describe('getEntity', function() {
|
||||
describe('group subscriptions', function() {
|
||||
it('get subscription', function(done) {
|
||||
return this.UserMembershipHandler.getEntity(
|
||||
this.fakeEntityId,
|
||||
EntityConfigs.group,
|
||||
this.user,
|
||||
null,
|
||||
(error, subscription) => {
|
||||
should.not.exist(error)
|
||||
const expectedQuery = {
|
||||
groupPlan: true,
|
||||
_id: this.fakeEntityId,
|
||||
manager_ids: ObjectId(this.user._id)
|
||||
}
|
||||
assertCalledWith(this.Subscription.findOne, expectedQuery)
|
||||
expect(subscription).to.equal(this.subscription)
|
||||
expect(subscription.membersLimit).to.equal(10)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('get for admin', function(done) {
|
||||
return this.UserMembershipHandler.getEntity(
|
||||
this.fakeEntityId,
|
||||
EntityConfigs.group,
|
||||
{ isAdmin: true },
|
||||
null,
|
||||
(error, subscription) => {
|
||||
should.not.exist(error)
|
||||
const expectedQuery = {
|
||||
groupPlan: true,
|
||||
_id: this.fakeEntityId
|
||||
}
|
||||
assertCalledWith(this.Subscription.findOne, expectedQuery)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('get with staffAccess field', function(done) {
|
||||
return this.UserMembershipHandler.getEntity(
|
||||
this.fakeEntityId,
|
||||
EntityConfigs.group,
|
||||
{ staffAccess: { institutionMetrics: true } },
|
||||
'institutionMetrics',
|
||||
(error, subscription) => {
|
||||
should.not.exist(error)
|
||||
const expectedQuery = {
|
||||
groupPlan: true,
|
||||
_id: this.fakeEntityId
|
||||
}
|
||||
assertCalledWith(this.Subscription.findOne, expectedQuery)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle error', function(done) {
|
||||
this.Subscription.findOne.yields(new Error('some error'))
|
||||
return this.UserMembershipHandler.getEntity(
|
||||
this.fakeEntityId,
|
||||
EntityConfigs.group,
|
||||
this.user._id,
|
||||
null,
|
||||
(error, subscription) => {
|
||||
should.exist(error)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getEntityWithoutAuthorizationCheck', function() {
|
||||
it('get publisher', function(done) {
|
||||
return this.UserMembershipHandler.getEntityWithoutAuthorizationCheck(
|
||||
|
@ -181,63 +106,6 @@ describe('UserMembershipHandler', function() {
|
|||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('institutions', function() {
|
||||
it('get institution', function(done) {
|
||||
return this.UserMembershipHandler.getEntity(
|
||||
this.institution.v1Id,
|
||||
EntityConfigs.institution,
|
||||
this.user,
|
||||
null,
|
||||
(error, institution) => {
|
||||
should.not.exist(error)
|
||||
const expectedQuery = {
|
||||
v1Id: this.institution.v1Id,
|
||||
managerIds: ObjectId(this.user._id)
|
||||
}
|
||||
assertCalledWith(this.Institution.findOne, expectedQuery)
|
||||
expect(institution).to.equal(this.institution)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('handle errors', function(done) {
|
||||
this.Institution.findOne.yields(new Error('nope'))
|
||||
return this.UserMembershipHandler.getEntity(
|
||||
this.fakeEntityId,
|
||||
EntityConfigs.institution,
|
||||
this.user._id,
|
||||
null,
|
||||
(error, institution) => {
|
||||
should.exist(error)
|
||||
expect(error).to.not.be.an.instanceof(Errors.NotFoundError)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('publishers', function() {
|
||||
it('get publisher', function(done) {
|
||||
return this.UserMembershipHandler.getEntity(
|
||||
this.publisher.slug,
|
||||
EntityConfigs.publisher,
|
||||
this.user,
|
||||
null,
|
||||
(error, institution) => {
|
||||
should.not.exist(error)
|
||||
const expectedQuery = {
|
||||
slug: this.publisher.slug,
|
||||
managerIds: ObjectId(this.user._id)
|
||||
}
|
||||
assertCalledWith(this.Publisher.findOne, expectedQuery)
|
||||
expect(institution).to.equal(this.publisher)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getUsers', function() {
|
||||
|
|
Loading…
Reference in a new issue