Merge pull request #2105 from overleaf/ta-user-membership-refactor

UserMembershipAuthorization Refactor

GitOrigin-RevId: 7711cda4a134823cbacee42731319fbb8aa648d0
This commit is contained in:
Timothée Alby 2019-09-24 10:44:13 +02:00 committed by sharelatex
parent 44d3b8b92e
commit a23ecc9bf8
14 changed files with 427 additions and 930 deletions

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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
}
}

View file

@ -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) {}

View file

@ -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({}))
}
}

View file

@ -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
)
}

View file

@ -0,0 +1,5 @@
module.exports = function expressify(fn) {
return (req, res, next) => {
fn(req, res, next).catch(next)
}
}

View file

@ -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",

View file

@ -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",

View file

@ -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
)

View file

@ -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()
})
})
})
})

View file

@ -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()
})
})
})
})

View file

@ -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() {