diff --git a/services/web/app/src/Features/Authorization/AuthorizationManager.js b/services/web/app/src/Features/Authorization/AuthorizationManager.js index 1a71d96598..796332b6ad 100644 --- a/services/web/app/src/Features/Authorization/AuthorizationManager.js +++ b/services/web/app/src/Features/Authorization/AuthorizationManager.js @@ -1,3 +1,5 @@ +const { callbackify } = require('util') +const { ObjectId } = require('mongodb') const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') const ProjectGetter = require('../Project/ProjectGetter') @@ -6,290 +8,237 @@ const PrivilegeLevels = require('./PrivilegeLevels') const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') const PublicAccessLevels = require('./PublicAccessLevels') const Errors = require('../Errors/Errors') -const { ObjectId } = require('mongodb') -const { promisifyAll } = require('../../util/promises') -const AuthorizationManager = { - isRestrictedUser(userId, privilegeLevel, isTokenMember) { - if (privilegeLevel === PrivilegeLevels.NONE) { - return true - } - return ( - privilegeLevel === PrivilegeLevels.READ_ONLY && (isTokenMember || !userId) - ) - }, - - isRestrictedUserForProject(userId, projectId, token, callback) { - AuthorizationManager.getPrivilegeLevelForProject( - userId, - projectId, - token, - (err, privilegeLevel) => { - if (err) { - return callback(err) - } - CollaboratorsHandler.userIsTokenMember( - userId, - projectId, - (err, isTokenMember) => { - if (err) { - return callback(err) - } - callback( - null, - AuthorizationManager.isRestrictedUser( - userId, - privilegeLevel, - isTokenMember - ) - ) - } - ) - } - ) - }, - - getPublicAccessLevel(projectId, callback) { - if (!ObjectId.isValid(projectId)) { - return callback(new Error('invalid project id')) - } - // Note, the Project property in the DB is `publicAccesLevel`, without the second `s` - ProjectGetter.getProject( - projectId, - { publicAccesLevel: 1 }, - function (error, project) { - if (error) { - return callback(error) - } - if (!project) { - return callback( - new Errors.NotFoundError(`no project found with id ${projectId}`) - ) - } - callback(null, project.publicAccesLevel) - } - ) - }, - - // Get the privilege level that the user has for the project - // Returns: - // * privilegeLevel: "owner", "readAndWrite", of "readOnly" if the user has - // access. false if the user does not have access - // * becausePublic: true if the access level is only because the project is public. - // * becauseSiteAdmin: true if access level is only because user is admin - getPrivilegeLevelForProject(userId, projectId, token, callback) { - if (userId) { - AuthorizationManager.getPrivilegeLevelForProjectWithUser( - userId, - projectId, - token, - callback - ) - } else { - AuthorizationManager.getPrivilegeLevelForProjectWithoutUser( - projectId, - token, - callback - ) - } - }, - - // User is present, get their privilege level from database - getPrivilegeLevelForProjectWithUser(userId, projectId, token, callback) { - CollaboratorsGetter.getMemberIdPrivilegeLevel( - userId, - projectId, - function (error, privilegeLevel) { - if (error) { - return callback(error) - } - if (privilegeLevel && privilegeLevel !== PrivilegeLevels.NONE) { - // The user has direct access - return callback(null, privilegeLevel, false, false) - } - AuthorizationManager.isUserSiteAdmin(userId, function (error, isAdmin) { - if (error) { - return callback(error) - } - if (isAdmin) { - return callback(null, PrivilegeLevels.OWNER, false, true) - } - // Legacy public-access system - // User is present (not anonymous), but does not have direct access - AuthorizationManager.getPublicAccessLevel( - projectId, - function (err, publicAccessLevel) { - if (err) { - return callback(err) - } - if (publicAccessLevel === PublicAccessLevels.READ_ONLY) { - return callback(null, PrivilegeLevels.READ_ONLY, true, false) - } - if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) { - return callback( - null, - PrivilegeLevels.READ_AND_WRITE, - true, - false - ) - } - callback(null, PrivilegeLevels.NONE, false, false) - } - ) - }) - } - ) - }, - - // User is Anonymous, Try Token-based access - getPrivilegeLevelForProjectWithoutUser(projectId, token, callback) { - AuthorizationManager.getPublicAccessLevel( - projectId, - function (err, publicAccessLevel) { - if (err) { - return callback(err) - } - if (publicAccessLevel === PublicAccessLevels.READ_ONLY) { - // Legacy public read-only access for anonymous user - return callback(null, PrivilegeLevels.READ_ONLY, true, false) - } - if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) { - // Legacy public read-write access for anonymous user - return callback(null, PrivilegeLevels.READ_AND_WRITE, true, false) - } - if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) { - return AuthorizationManager.getPrivilegeLevelForProjectWithToken( - projectId, - token, - callback - ) - } - // Deny anonymous user access - callback(null, PrivilegeLevels.NONE, false, false) - } - ) - }, - - getPrivilegeLevelForProjectWithToken(projectId, token, callback) { - // Anonymous users can have read-only access to token-based projects, - // while read-write access must be logged in, - // unless the `enableAnonymousReadAndWriteSharing` setting is enabled - TokenAccessHandler.validateTokenForAnonymousAccess( - projectId, - token, - function (err, isValidReadAndWrite, isValidReadOnly) { - if (err) { - return callback(err) - } - if (isValidReadOnly) { - // Grant anonymous user read-only access - return callback(null, PrivilegeLevels.READ_ONLY, false, false) - } - if (isValidReadAndWrite) { - // Grant anonymous user read-and-write access - return callback(null, PrivilegeLevels.READ_AND_WRITE, false, false) - } - // Deny anonymous access - callback(null, PrivilegeLevels.NONE, false, false) - } - ) - }, - - canUserReadProject(userId, projectId, token, callback) { - AuthorizationManager.getPrivilegeLevelForProject( - userId, - projectId, - token, - function (error, privilegeLevel) { - if (error) { - return callback(error) - } - callback( - null, - [ - PrivilegeLevels.OWNER, - PrivilegeLevels.READ_AND_WRITE, - PrivilegeLevels.READ_ONLY, - ].includes(privilegeLevel) - ) - } - ) - }, - - canUserWriteProjectContent(userId, projectId, token, callback) { - AuthorizationManager.getPrivilegeLevelForProject( - userId, - projectId, - token, - function (error, privilegeLevel) { - if (error) { - return callback(error) - } - callback( - null, - [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes( - privilegeLevel - ) - ) - } - ) - }, - - canUserWriteProjectSettings(userId, projectId, token, callback) { - AuthorizationManager.getPrivilegeLevelForProject( - userId, - projectId, - token, - function (error, privilegeLevel, becausePublic) { - if (error) { - return callback(error) - } - if (privilegeLevel === PrivilegeLevels.OWNER) { - return callback(null, true) - } - if ( - privilegeLevel === PrivilegeLevels.READ_AND_WRITE && - !becausePublic - ) { - return callback(null, true) - } - callback(null, false) - } - ) - }, - - canUserAdminProject(userId, projectId, token, callback) { - AuthorizationManager.getPrivilegeLevelForProject( - userId, - projectId, - token, - function (error, privilegeLevel, becausePublic, becauseSiteAdmin) { - if (error) { - return callback(error) - } - callback( - null, - privilegeLevel === PrivilegeLevels.OWNER, - becauseSiteAdmin - ) - } - ) - }, - - isUserSiteAdmin(userId, callback) { - if (!userId) { - return callback(null, false) - } - User.findOne({ _id: userId }, { isAdmin: 1 }, function (error, user) { - if (error) { - return callback(error) - } - callback(null, (user && user.isAdmin) === true) - }) - }, +function isRestrictedUser(userId, privilegeLevel, isTokenMember) { + if (privilegeLevel === PrivilegeLevels.NONE) { + return true + } + return ( + privilegeLevel === PrivilegeLevels.READ_ONLY && (isTokenMember || !userId) + ) } -module.exports = AuthorizationManager -module.exports.promises = promisifyAll(AuthorizationManager, { - without: 'isRestrictedUser', -}) +async function isRestrictedUserForProject(userId, projectId, token) { + const privilegeLevel = await getPrivilegeLevelForProject( + userId, + projectId, + token + ) + const isTokenMember = await CollaboratorsHandler.promises.userIsTokenMember( + userId, + projectId + ) + return isRestrictedUser(userId, privilegeLevel, isTokenMember) +} + +async function getPublicAccessLevel(projectId) { + if (!ObjectId.isValid(projectId)) { + throw new Error('invalid project id') + } + + // Note, the Project property in the DB is `publicAccesLevel`, without the second `s` + const project = await ProjectGetter.promises.getProject(projectId, { + publicAccesLevel: 1, + }) + if (!project) { + throw new Errors.NotFoundError(`no project found with id ${projectId}`) + } + return project.publicAccesLevel +} + +/** + * Get the privilege level that the user has for the project. + * + * @param userId - The id of the user that wants to access the project. + * @param projectId - The id of the project to be accessed. + * @param {Object} opts + * @param {boolean} opts.ignoreSiteAdmin - Do not consider whether the user is + * a site admin. + * @param {boolean} opts.ignorePublicAccess - Do not consider the project is + * publicly accessible. + * + * @returns {string|boolean} The privilege level. One of "owner", + * "readAndWrite", "readOnly" or false. + */ +async function getPrivilegeLevelForProject( + userId, + projectId, + token, + opts = {} +) { + if (userId) { + return getPrivilegeLevelForProjectWithUser(userId, projectId, token, opts) + } else { + return getPrivilegeLevelForProjectWithoutUser(projectId, token, opts) + } +} + +// User is present, get their privilege level from database +async function getPrivilegeLevelForProjectWithUser( + userId, + projectId, + token, + opts = {} +) { + const privilegeLevel = await CollaboratorsGetter.promises.getMemberIdPrivilegeLevel( + userId, + projectId + ) + if (privilegeLevel && privilegeLevel !== PrivilegeLevels.NONE) { + // The user has direct access + return privilegeLevel + } + + if (!opts.ignoreSiteAdmin) { + const isAdmin = await isUserSiteAdmin(userId) + if (isAdmin) { + return PrivilegeLevels.OWNER + } + } + + if (!opts.ignorePublicAccess) { + // Legacy public-access system + // User is present (not anonymous), but does not have direct access + const publicAccessLevel = await getPublicAccessLevel(projectId) + if (publicAccessLevel === PublicAccessLevels.READ_ONLY) { + return PrivilegeLevels.READ_ONLY + } + if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) { + return PrivilegeLevels.READ_AND_WRITE + } + } + + return PrivilegeLevels.NONE +} + +// User is Anonymous, Try Token-based access +async function getPrivilegeLevelForProjectWithoutUser( + projectId, + token, + opts = {} +) { + const publicAccessLevel = await getPublicAccessLevel(projectId) + if (!opts.ignorePublicAccess) { + if (publicAccessLevel === PublicAccessLevels.READ_ONLY) { + // Legacy public read-only access for anonymous user + return PrivilegeLevels.READ_ONLY + } + if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) { + // Legacy public read-write access for anonymous user + return PrivilegeLevels.READ_AND_WRITE + } + } + if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) { + return getPrivilegeLevelForProjectWithToken(projectId, token) + } + + // Deny anonymous user access + return PrivilegeLevels.NONE +} + +async function getPrivilegeLevelForProjectWithToken(projectId, token) { + // Anonymous users can have read-only access to token-based projects, + // while read-write access must be logged in, + // unless the `enableAnonymousReadAndWriteSharing` setting is enabled + const { + isValidReadAndWrite, + isValidReadOnly, + } = await TokenAccessHandler.promises.validateTokenForAnonymousAccess( + projectId, + token + ) + if (isValidReadOnly) { + // Grant anonymous user read-only access + return PrivilegeLevels.READ_ONLY + } + if (isValidReadAndWrite) { + // Grant anonymous user read-and-write access + return PrivilegeLevels.READ_AND_WRITE + } + // Deny anonymous access + return PrivilegeLevels.NONE +} + +async function canUserReadProject(userId, projectId, token) { + const privilegeLevel = await getPrivilegeLevelForProject( + userId, + projectId, + token + ) + return [ + PrivilegeLevels.OWNER, + PrivilegeLevels.READ_AND_WRITE, + PrivilegeLevels.READ_ONLY, + ].includes(privilegeLevel) +} + +async function canUserWriteProjectContent(userId, projectId, token) { + const privilegeLevel = await getPrivilegeLevelForProject( + userId, + projectId, + token + ) + return [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes( + privilegeLevel + ) +} + +async function canUserWriteProjectSettings(userId, projectId, token) { + const privilegeLevel = await getPrivilegeLevelForProject( + userId, + projectId, + token, + { ignorePublicAccess: true } + ) + return [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes( + privilegeLevel + ) +} + +async function canUserRenameProject(userId, projectId, token) { + const privilegeLevel = await getPrivilegeLevelForProject( + userId, + projectId, + token + ) + return privilegeLevel === PrivilegeLevels.OWNER +} + +async function canUserAdminProject(userId, projectId, token) { + const privilegeLevel = await getPrivilegeLevelForProject( + userId, + projectId, + token + ) + return privilegeLevel === PrivilegeLevels.OWNER +} + +async function isUserSiteAdmin(userId) { + if (!userId) { + return false + } + const user = await User.findOne({ _id: userId }, { isAdmin: 1 }).exec() + return user != null && user.isAdmin === true +} + +module.exports = { + canUserReadProject: callbackify(canUserReadProject), + canUserWriteProjectContent: callbackify(canUserWriteProjectContent), + canUserWriteProjectSettings: callbackify(canUserWriteProjectSettings), + canUserRenameProject: callbackify(canUserRenameProject), + canUserAdminProject: callbackify(canUserAdminProject), + getPrivilegeLevelForProject: callbackify(getPrivilegeLevelForProject), + isRestrictedUser, + isRestrictedUserForProject: callbackify(isRestrictedUserForProject), + isUserSiteAdmin: callbackify(isUserSiteAdmin), + promises: { + canUserReadProject, + canUserWriteProjectContent, + canUserWriteProjectSettings, + canUserRenameProject, + canUserAdminProject, + getPrivilegeLevelForProject, + isRestrictedUserForProject, + isUserSiteAdmin, + }, +} diff --git a/services/web/app/src/Features/Authorization/AuthorizationMiddleware.js b/services/web/app/src/Features/Authorization/AuthorizationMiddleware.js index ef9a234d40..8324b6aa72 100644 --- a/services/web/app/src/Features/Authorization/AuthorizationMiddleware.js +++ b/services/web/app/src/Features/Authorization/AuthorizationMiddleware.js @@ -1,6 +1,4 @@ -let AuthorizationMiddleware const AuthorizationManager = require('./AuthorizationManager') -const async = require('async') const logger = require('logger-sharelatex') const { ObjectId } = require('mongodb') const Errors = require('../Errors/Errors') @@ -8,265 +6,188 @@ const HttpErrorHandler = require('../Errors/HttpErrorHandler') const AuthenticationController = require('../Authentication/AuthenticationController') const SessionManager = require('../Authentication/SessionManager') const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') +const { expressify } = require('../../util/promises') -module.exports = AuthorizationMiddleware = { - ensureUserCanReadMultipleProjects(req, res, next) { - const projectIds = (req.query.project_ids || '').split(',') - AuthorizationMiddleware._getUserId(req, function (error, userId) { - if (error) { - return next(error) - } - // Remove the projects we have access to. Note rejectSeries doesn't use - // errors in callbacks - async.rejectSeries( - projectIds, - function (projectId, cb) { - const token = TokenAccessHandler.getRequestToken(req, projectId) - AuthorizationManager.canUserReadProject( - userId, - projectId, - token, - function (error, canRead) { - if (error) { - return next(error) - } - cb(canRead) - } - ) - }, - function (unauthorizedProjectIds) { - if (unauthorizedProjectIds.length > 0) { - return AuthorizationMiddleware.redirectToRestricted(req, res, next) - } - next() - } - ) - }) - }, - - blockRestrictedUserFromProject(req, res, next) { - AuthorizationMiddleware._getUserAndProjectId( - req, - function (error, userId, projectId) { - if (error) { - return next(error) - } - const token = TokenAccessHandler.getRequestToken(req, projectId) - AuthorizationManager.isRestrictedUserForProject( - userId, - projectId, - token, - (err, isRestrictedUser) => { - if (err) { - return next(err) - } - if (isRestrictedUser) { - return res.sendStatus(403) - } - next() - } - ) - } +async function ensureUserCanReadMultipleProjects(req, res, next) { + const projectIds = (req.query.project_ids || '').split(',') + const userId = _getUserId(req) + for (const projectId of projectIds) { + const token = TokenAccessHandler.getRequestToken(req, projectId) + const canRead = await AuthorizationManager.promises.canUserReadProject( + userId, + projectId, + token ) - }, - - ensureUserCanReadProject(req, res, next) { - AuthorizationMiddleware._getUserAndProjectId( - req, - function (error, userId, projectId) { - if (error) { - return next(error) - } - const token = TokenAccessHandler.getRequestToken(req, projectId) - AuthorizationManager.canUserReadProject( - userId, - projectId, - token, - function (error, canRead) { - if (error) { - return next(error) - } - if (canRead) { - logger.log( - { userId, projectId }, - 'allowing user read access to project' - ) - return next() - } - logger.log( - { userId, projectId }, - 'denying user read access to project' - ) - HttpErrorHandler.forbidden(req, res) - } - ) - } - ) - }, - - ensureUserCanWriteProjectSettings(req, res, next) { - AuthorizationMiddleware._getUserAndProjectId( - req, - function (error, userId, projectId) { - if (error) { - return next(error) - } - const token = TokenAccessHandler.getRequestToken(req, projectId) - AuthorizationManager.canUserWriteProjectSettings( - userId, - projectId, - token, - function (error, canWrite) { - if (error) { - return next(error) - } - if (canWrite) { - logger.log( - { userId, projectId }, - 'allowing user write access to project settings' - ) - return next() - } - logger.log( - { userId, projectId }, - 'denying user write access to project settings' - ) - HttpErrorHandler.forbidden(req, res) - } - ) - } - ) - }, - - ensureUserCanWriteProjectContent(req, res, next) { - AuthorizationMiddleware._getUserAndProjectId( - req, - function (error, userId, projectId) { - if (error) { - return next(error) - } - const token = TokenAccessHandler.getRequestToken(req, projectId) - AuthorizationManager.canUserWriteProjectContent( - userId, - projectId, - token, - function (error, canWrite) { - if (error) { - return next(error) - } - if (canWrite) { - logger.log( - { userId, projectId }, - 'allowing user write access to project content' - ) - return next() - } - logger.log( - { userId, projectId }, - 'denying user write access to project settings' - ) - HttpErrorHandler.forbidden(req, res) - } - ) - } - ) - }, - - ensureUserCanAdminProject(req, res, next) { - AuthorizationMiddleware._getUserAndProjectId( - req, - function (error, userId, projectId) { - if (error) { - return next(error) - } - const token = TokenAccessHandler.getRequestToken(req, projectId) - AuthorizationManager.canUserAdminProject( - userId, - projectId, - token, - function (error, canAdmin) { - if (error) { - return next(error) - } - if (canAdmin) { - logger.log( - { userId, projectId }, - 'allowing user admin access to project' - ) - return next() - } - logger.log( - { userId, projectId }, - 'denying user admin access to project' - ) - HttpErrorHandler.forbidden(req, res) - } - ) - } - ) - }, - - ensureUserIsSiteAdmin(req, res, next) { - AuthorizationMiddleware._getUserId(req, function (error, userId) { - if (error) { - return next(error) - } - AuthorizationManager.isUserSiteAdmin(userId, function (error, isAdmin) { - if (error) { - return next(error) - } - if (isAdmin) { - logger.log({ userId }, 'allowing user admin access to site') - return next() - } - logger.log({ userId }, 'denying user admin access to site') - AuthorizationMiddleware.redirectToRestricted(req, res, next) - }) - }) - }, - - _getUserAndProjectId(req, callback) { - const projectId = req.params.project_id || req.params.Project_id - if (!projectId) { - return callback(new Error('Expected project_id in request parameters')) + if (!canRead) { + return _redirectToRestricted(req, res, next) } - if (!ObjectId.isValid(projectId)) { - return callback( - new Errors.NotFoundError(`invalid projectId: ${projectId}`) - ) - } - AuthorizationMiddleware._getUserId(req, function (error, userId) { - if (error) { - return callback(error) - } - callback(null, userId, projectId) - }) - }, - - _getUserId(req, callback) { - const userId = - SessionManager.getLoggedInUserId(req.session) || - (req.oauth_user && req.oauth_user._id) || - null - callback(null, userId) - }, - - redirectToRestricted(req, res, next) { - // TODO: move this to throwing ForbiddenError - res.redirect( - `/restricted?from=${encodeURIComponent(res.locals.currentUrl)}` - ) - }, - - restricted(req, res, next) { - if (SessionManager.isUserLoggedIn(req.session)) { - return res.render('user/restricted', { title: 'restricted' }) - } - const { from } = req.query - logger.log({ from }, 'redirecting to login') - if (from) { - AuthenticationController.setRedirectInSession(req, from) - } - res.redirect('/login') - }, + } + next() +} + +async function blockRestrictedUserFromProject(req, res, next) { + const projectId = _getProjectId(req) + const userId = _getUserId(req) + const token = TokenAccessHandler.getRequestToken(req, projectId) + const isRestrictedUser = await AuthorizationManager.promises.isRestrictedUserForProject( + userId, + projectId, + token + ) + if (isRestrictedUser) { + return HttpErrorHandler.forbidden(req, res) + } + next() +} + +async function ensureUserCanReadProject(req, res, next) { + const projectId = _getProjectId(req) + const userId = _getUserId(req) + const token = TokenAccessHandler.getRequestToken(req, projectId) + const canRead = await AuthorizationManager.promises.canUserReadProject( + userId, + projectId, + token + ) + if (canRead) { + logger.log({ userId, projectId }, 'allowing user read access to project') + return next() + } + logger.log({ userId, projectId }, 'denying user read access to project') + HttpErrorHandler.forbidden(req, res) +} + +async function ensureUserCanWriteProjectSettings(req, res, next) { + const projectId = _getProjectId(req) + const userId = _getUserId(req) + const token = TokenAccessHandler.getRequestToken(req, projectId) + + if (req.body.name != null) { + const canRename = await AuthorizationManager.promises.canUserRenameProject( + userId, + projectId, + token + ) + if (!canRename) { + return HttpErrorHandler.forbidden(req, res) + } + } + + const otherParams = Object.keys(req.body).filter(x => x !== 'name') + if (otherParams.length > 0) { + const canWrite = await AuthorizationManager.promises.canUserWriteProjectSettings( + userId, + projectId, + token + ) + if (!canWrite) { + return HttpErrorHandler.forbidden(req, res) + } + } + + next() +} + +async function ensureUserCanWriteProjectContent(req, res, next) { + const projectId = _getProjectId(req) + const userId = _getUserId(req) + const token = TokenAccessHandler.getRequestToken(req, projectId) + const canWrite = await AuthorizationManager.promises.canUserWriteProjectContent( + userId, + projectId, + token + ) + if (canWrite) { + logger.log( + { userId, projectId }, + 'allowing user write access to project content' + ) + return next() + } + logger.log( + { userId, projectId }, + 'denying user write access to project settings' + ) + HttpErrorHandler.forbidden(req, res) +} + +async function ensureUserCanAdminProject(req, res, next) { + const projectId = _getProjectId(req) + const userId = _getUserId(req) + const token = TokenAccessHandler.getRequestToken(req, projectId) + const canAdmin = await AuthorizationManager.promises.canUserAdminProject( + userId, + projectId, + token + ) + if (canAdmin) { + logger.log({ userId, projectId }, 'allowing user admin access to project') + return next() + } + logger.log({ userId, projectId }, 'denying user admin access to project') + HttpErrorHandler.forbidden(req, res) +} + +async function ensureUserIsSiteAdmin(req, res, next) { + const userId = _getUserId(req) + const isAdmin = await AuthorizationManager.promises.isUserSiteAdmin(userId) + if (isAdmin) { + logger.log({ userId }, 'allowing user admin access to site') + return next() + } + logger.log({ userId }, 'denying user admin access to site') + _redirectToRestricted(req, res, next) +} + +function _getProjectId(req) { + const projectId = req.params.project_id || req.params.Project_id + if (!projectId) { + throw new Error('Expected project_id in request parameters') + } + if (!ObjectId.isValid(projectId)) { + throw new Errors.NotFoundError(`invalid projectId: ${projectId}`) + } + return projectId +} + +function _getUserId(req) { + return ( + SessionManager.getLoggedInUserId(req.session) || + (req.oauth_user && req.oauth_user._id) || + null + ) +} + +function _redirectToRestricted(req, res, next) { + // TODO: move this to throwing ForbiddenError + res.redirect(`/restricted?from=${encodeURIComponent(res.locals.currentUrl)}`) +} + +function restricted(req, res, next) { + if (SessionManager.isUserLoggedIn(req.session)) { + return res.render('user/restricted', { title: 'restricted' }) + } + const { from } = req.query + logger.log({ from }, 'redirecting to login') + if (from) { + AuthenticationController.setRedirectInSession(req, from) + } + res.redirect('/login') +} + +module.exports = { + ensureUserCanReadMultipleProjects: expressify( + ensureUserCanReadMultipleProjects + ), + blockRestrictedUserFromProject: expressify(blockRestrictedUserFromProject), + ensureUserCanReadProject: expressify(ensureUserCanReadProject), + ensureUserCanWriteProjectSettings: expressify( + ensureUserCanWriteProjectSettings + ), + ensureUserCanWriteProjectContent: expressify( + ensureUserCanWriteProjectContent + ), + ensureUserCanAdminProject: expressify(ensureUserCanAdminProject), + ensureUserIsSiteAdmin: expressify(ensureUserIsSiteAdmin), + restricted, } diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js b/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js index 8f9a63ab42..6f57657063 100644 --- a/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js +++ b/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js @@ -302,8 +302,10 @@ TokenAccessHandler.promises = promisifyAll(TokenAccessHandler, { 'grantSessionTokenAccess', 'getRequestToken', 'protectTokens', - 'validateTokenForAnonymousAccess', ], + multiResult: { + validateTokenForAnonymousAccess: ['isValidReadAndWrite', 'isValidReadOnly'], + }, }) module.exports = TokenAccessHandler diff --git a/services/web/app/src/router.js b/services/web/app/src/router.js index 925da72c0a..758cd39b8a 100644 --- a/services/web/app/src/router.js +++ b/services/web/app/src/router.js @@ -316,6 +316,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) { ) webRouter.post( '/project/:Project_id/settings', + validate({ body: Joi.object() }), AuthorizationMiddleware.ensureUserCanWriteProjectSettings, ProjectController.updateProjectSettings ) diff --git a/services/web/test/acceptance/src/AuthorizationTests.js b/services/web/test/acceptance/src/AuthorizationTests.js index 2d03f90f9a..ecacf65f72 100644 --- a/services/web/test/acceptance/src/AuthorizationTests.js +++ b/services/web/test/acceptance/src/AuthorizationTests.js @@ -33,6 +33,24 @@ function tryReadAccess(user, projectId, test, callback) { ) } +function tryRenameProjectAccess(user, projectId, test, callback) { + user.request.post( + { + uri: `/project/${projectId}/settings`, + json: { + name: 'new name', + }, + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + test(response, body) + callback() + } + ) +} + function trySettingsWriteAccess(user, projectId, test, callback) { async.series( [ @@ -166,6 +184,17 @@ function expectContentWriteAccess(user, projectId, callback) { ) } +function expectRenameProjectAccess(user, projectId, callback) { + tryRenameProjectAccess( + user, + projectId, + (response, body) => { + expect(response.statusCode).to.be.oneOf([200, 204]) + }, + callback + ) +} + function expectSettingsWriteAccess(user, projectId, callback) { trySettingsWriteAccess( user, @@ -184,7 +213,7 @@ function expectAdminAccess(user, projectId, callback) { ) } -function expectNoReadAccess(user, projectId, options, callback) { +function expectNoReadAccess(user, projectId, callback) { async.series( [ cb => @@ -214,7 +243,7 @@ function expectNoContentWriteAccess(user, projectId, callback) { ) } -function expectNoSettingsWriteAccess(user, projectId, options, callback) { +function expectNoSettingsWriteAccess(user, projectId, callback) { trySettingsWriteAccess( user, projectId, @@ -223,6 +252,15 @@ function expectNoSettingsWriteAccess(user, projectId, options, callback) { ) } +function expectNoRenameProjectAccess(user, projectId, callback) { + tryRenameProjectAccess( + user, + projectId, + expectErrorResponse.restricted.json, + callback + ) +} + function expectNoAdminAccess(user, projectId, callback) { tryAdminAccess( user, @@ -325,6 +363,10 @@ describe('Authorization', function () { expectSettingsWriteAccess(this.owner, this.projectId, done) }) + it('should allow the owner to rename the project', function (done) { + expectRenameProjectAccess(this.owner, this.projectId, done) + }) + it('should allow the owner admin access to it', function (done) { expectAdminAccess(this.owner, this.projectId, done) }) @@ -334,12 +376,7 @@ describe('Authorization', function () { }) it('should not allow another user read access to the project', function (done) { - expectNoReadAccess( - this.other1, - this.projectId, - { redirect_to: '/restricted' }, - done - ) + expectNoReadAccess(this.other1, this.projectId, done) }) it('should not allow another user write access to its content', function (done) { @@ -347,12 +384,11 @@ describe('Authorization', function () { }) it('should not allow another user write access to its settings', function (done) { - expectNoSettingsWriteAccess( - this.other1, - this.projectId, - { redirect_to: '/restricted' }, - done - ) + expectNoSettingsWriteAccess(this.other1, this.projectId, done) + }) + + it('should not allow another user to rename the project', function (done) { + expectNoRenameProjectAccess(this.other1, this.projectId, done) }) it('should not allow another user admin access to it', function (done) { @@ -364,12 +400,7 @@ describe('Authorization', function () { }) it('should not allow anonymous user read access to it', function (done) { - expectNoReadAccess( - this.anon, - this.projectId, - { redirect_to: '/restricted' }, - done - ) + expectNoReadAccess(this.anon, this.projectId, done) }) it('should not allow anonymous user write access to its content', function (done) { @@ -377,12 +408,11 @@ describe('Authorization', function () { }) it('should not allow anonymous user write access to its settings', function (done) { - expectNoSettingsWriteAccess( - this.anon, - this.projectId, - { redirect_to: '/restricted' }, - done - ) + expectNoSettingsWriteAccess(this.anon, this.projectId, done) + }) + + it('should not allow anonymous user to rename the project', function (done) { + expectNoRenameProjectAccess(this.anon, this.projectId, done) }) it('should not allow anonymous user admin access to it', function (done) { @@ -405,6 +435,10 @@ describe('Authorization', function () { expectSettingsWriteAccess(this.site_admin, this.projectId, done) }) + it('should allow site admin users to rename the project', function (done) { + expectRenameProjectAccess(this.site_admin, this.projectId, done) + }) + it('should allow site admin users admin access to it', function (done) { expectAdminAccess(this.site_admin, this.projectId, done) }) @@ -456,12 +490,11 @@ describe('Authorization', function () { }) it('should not allow the read-only user write access to its settings', function (done) { - expectNoSettingsWriteAccess( - this.ro_user, - this.projectId, - { redirect_to: '/restricted' }, - done - ) + expectNoSettingsWriteAccess(this.ro_user, this.projectId, done) + }) + + it('should not allow the read-only user to rename the project', function (done) { + expectNoRenameProjectAccess(this.ro_user, this.projectId, done) }) it('should not allow the read-only user admin access to it', function (done) { @@ -480,6 +513,10 @@ describe('Authorization', function () { expectSettingsWriteAccess(this.rw_user, this.projectId, done) }) + it('should not allow the read-write user to rename the project', function (done) { + expectNoRenameProjectAccess(this.rw_user, this.projectId, done) + }) + it('should not allow the read-write user admin access to it', function (done) { expectNoAdminAccess(this.rw_user, this.projectId, done) }) @@ -513,12 +550,11 @@ describe('Authorization', function () { }) it('should not allow a user write access to its settings', function (done) { - expectNoSettingsWriteAccess( - this.other1, - this.projectId, - { redirect_to: '/restricted' }, - done - ) + expectNoSettingsWriteAccess(this.other1, this.projectId, done) + }) + + it('should not allow a user to rename the project', function (done) { + expectNoRenameProjectAccess(this.other1, this.projectId, done) }) it('should not allow a user admin access to it', function (done) { @@ -538,12 +574,11 @@ describe('Authorization', function () { }) it('should not allow an anonymous user write access to its settings', function (done) { - expectNoSettingsWriteAccess( - this.anon, - this.projectId, - { redirect_to: '/restricted' }, - done - ) + expectNoSettingsWriteAccess(this.anon, this.projectId, done) + }) + + it('should not allow an anonymous user to rename the project', function (done) { + expectNoRenameProjectAccess(this.anon, this.projectId, done) }) it('should not allow an anonymous user admin access to it', function (done) { @@ -571,12 +606,11 @@ describe('Authorization', function () { }) it('should not allow a user write access to its settings', function (done) { - expectNoSettingsWriteAccess( - this.other1, - this.projectId, - { redirect_to: '/restricted' }, - done - ) + expectNoSettingsWriteAccess(this.other1, this.projectId, done) + }) + + it('should not allow a user to rename the project', function (done) { + expectNoRenameProjectAccess(this.other1, this.projectId, done) }) it('should not allow a user admin access to it', function (done) { @@ -597,12 +631,11 @@ describe('Authorization', function () { }) it('should not allow an anonymous user write access to its settings', function (done) { - expectNoSettingsWriteAccess( - this.anon, - this.projectId, - { redirect_to: '/restricted' }, - done - ) + expectNoSettingsWriteAccess(this.anon, this.projectId, done) + }) + + it('should not allow an anonymous user to rename the project', function (done) { + expectNoRenameProjectAccess(this.anon, this.projectId, done) }) it('should not allow an anonymous user admin access to it', function (done) { diff --git a/services/web/test/unit/src/Authorization/AuthorizationManagerTests.js b/services/web/test/unit/src/Authorization/AuthorizationManagerTests.js index 657dddd11f..06e65c6207 100644 --- a/services/web/test/unit/src/Authorization/AuthorizationManagerTests.js +++ b/services/web/test/unit/src/Authorization/AuthorizationManagerTests.js @@ -1,47 +1,62 @@ -/* eslint-disable - node/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 { expect } = require('chai') const modulePath = '../../../../app/src/Features/Authorization/AuthorizationManager.js' const SandboxedModule = require('sandboxed-module') -const Errors = require('../../../../app/src/Features/Errors/Errors.js') +const Errors = require('../../../../app/src/Features/Errors/Errors') +const PrivilegeLevels = require('../../../../app/src/Features/Authorization/PrivilegeLevels') +const PublicAccessLevels = require('../../../../app/src/Features/Authorization/PublicAccessLevels') const { ObjectId } = require('mongodb') describe('AuthorizationManager', function () { beforeEach(function () { + this.user = { _id: new ObjectId() } + this.project = { _id: new ObjectId() } + this.token = 'some-token' + + this.ProjectGetter = { + promises: { + getProject: sinon.stub().resolves(null), + }, + } + this.ProjectGetter.promises.getProject + .withArgs(this.project._id) + .resolves(this.project) + + this.CollaboratorsGetter = { + promises: { + getMemberIdPrivilegeLevel: sinon.stub().resolves(PrivilegeLevels.NONE), + }, + } + + this.CollaboratorsHandler = {} + + this.User = { + findOne: sinon.stub().returns({ exec: sinon.stub().resolves(null) }), + } + this.User.findOne + .withArgs({ _id: this.user._id }) + .returns({ exec: sinon.stub().resolves(this.user) }) + + this.TokenAccessHandler = { + promises: { + validateTokenForAnonymousAccess: sinon + .stub() + .resolves({ isValidReadAndWrite: false, isValidReadOnly: false }), + }, + } + this.AuthorizationManager = SandboxedModule.require(modulePath, { requires: { mongodb: { ObjectId }, - '../Collaborators/CollaboratorsGetter': (this.CollaboratorsGetter = {}), - '../Collaborators/CollaboratorsHandler': (this.CollaboratorsHandler = {}), - '../Project/ProjectGetter': (this.ProjectGetter = {}), - '../../models/User': { - User: (this.User = {}), - }, - '../TokenAccess/TokenAccessHandler': (this.TokenAccessHandler = { - validateTokenForAnonymousAccess: sinon - .stub() - .callsArgWith(2, null, false, false), - }), + '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter, + '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler, + '../Project/ProjectGetter': this.ProjectGetter, + '../../models/User': { User: this.User }, + '../TokenAccess/TokenAccessHandler': this.TokenAccessHandler, '@overleaf/settings': { passwordStrengthOptions: {} }, }, }) - this.user_id = 'user-id-1' - this.project_id = 'project-id-1' - this.token = 'some-token' - return (this.callback = sinon.stub()) }) describe('isRestrictedUser', function () { @@ -59,12 +74,12 @@ describe('AuthorizationManager', function () { ['id', false, true], ['id', false, false], ] - for (var notRestrictedArgs of notRestrictedScenarios) { + for (const notRestrictedArgs of notRestrictedScenarios) { expect( this.AuthorizationManager.isRestrictedUser(...notRestrictedArgs) ).to.equal(false) } - for (var restrictedArgs of restrictedScenarios) { + for (const restrictedArgs of restrictedScenarios) { expect( this.AuthorizationManager.isRestrictedUser(...restrictedArgs) ).to.equal(true) @@ -73,203 +88,144 @@ describe('AuthorizationManager', function () { }) describe('getPrivilegeLevelForProject', function () { - beforeEach(function () { - this.ProjectGetter.getProject = sinon.stub() - this.AuthorizationManager.isUserSiteAdmin = sinon.stub() - return (this.CollaboratorsGetter.getMemberIdPrivilegeLevel = sinon.stub()) - }) - describe('with a token-based project', function () { beforeEach(function () { - return this.ProjectGetter.getProject - .withArgs(this.project_id, { publicAccesLevel: 1 }) - .yields(null, { publicAccesLevel: 'tokenBased' }) + this.project.publicAccesLevel = 'tokenBased' }) - describe('with a user_id with a privilege level', function () { - beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin - .withArgs(this.user_id) - .yields(null, false) - this.CollaboratorsGetter.getMemberIdPrivilegeLevel - .withArgs(this.user_id, this.project_id) - .yields(null, 'readOnly') - return this.AuthorizationManager.getPrivilegeLevelForProject( - this.user_id, - this.project_id, - this.token, - this.callback + describe('with a user id with a privilege level', function () { + beforeEach(async function () { + this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel + .withArgs(this.user._id, this.project._id) + .resolves(PrivilegeLevels.READ_ONLY) + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + this.user._id, + this.project._id, + this.token ) }) it("should return the user's privilege level", function () { - return this.callback - .calledWith(null, 'readOnly', false, false) - .should.equal(true) + expect(this.result).to.equal('readOnly') }) }) - describe('with a user_id with no privilege level', function () { - beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin - .withArgs(this.user_id) - .yields(null, false) - this.CollaboratorsGetter.getMemberIdPrivilegeLevel - .withArgs(this.user_id, this.project_id) - .yields(null, false) - return this.AuthorizationManager.getPrivilegeLevelForProject( - this.user_id, - this.project_id, - this.token, - this.callback + describe('with a user id with no privilege level', function () { + beforeEach(async function () { + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + this.user._id, + this.project._id, + this.token ) }) it('should return false', function () { - return this.callback - .calledWith(null, false, false, false) - .should.equal(true) + expect(this.result).to.equal(false) }) }) - describe('with a user_id who is an admin', function () { - beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin - .withArgs(this.user_id) - .yields(null, true) - this.CollaboratorsGetter.getMemberIdPrivilegeLevel - .withArgs(this.user_id, this.project_id) - .yields(null, false) - return this.AuthorizationManager.getPrivilegeLevelForProject( - this.user_id, - this.project_id, - this.token, - this.callback + describe('with a user id who is an admin', function () { + beforeEach(async function () { + this.user.isAdmin = true + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + this.user._id, + this.project._id, + this.token ) }) it('should return the user as an owner', function () { - return this.callback - .calledWith(null, 'owner', false, true) - .should.equal(true) + expect(this.result).to.equal('owner') }) }) describe('with no user (anonymous)', function () { describe('when the token is not valid', function () { - beforeEach(function () { - this.TokenAccessHandler.validateTokenForAnonymousAccess = sinon - .stub() - .withArgs(this.project_id, this.token) - .yields(null, false, false) - return this.AuthorizationManager.getPrivilegeLevelForProject( + beforeEach(async function () { + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( null, - this.project_id, - this.token, - this.callback + this.project._id, + this.token ) }) it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function () { - return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal( - false - ) - }) - - it('should not call AuthorizationManager.isUserSiteAdmin', function () { - return this.AuthorizationManager.isUserSiteAdmin.called.should.equal( + this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.called.should.equal( false ) }) it('should check if the token is valid', function () { - return this.TokenAccessHandler.validateTokenForAnonymousAccess - .calledWith(this.project_id, this.token) - .should.equal(true) + this.TokenAccessHandler.promises.validateTokenForAnonymousAccess.should.have.been.calledWith( + this.project._id, + this.token + ) }) it('should return false', function () { - return this.callback - .calledWith(null, false, false, false) - .should.equal(true) + expect(this.result).to.equal(false) }) }) describe('when the token is valid for read-and-write', function () { - beforeEach(function () { - this.TokenAccessHandler.validateTokenForAnonymousAccess = sinon + beforeEach(async function () { + this.TokenAccessHandler.promises.validateTokenForAnonymousAccess = sinon .stub() - .withArgs(this.project_id, this.token) - .yields(null, true, false) - return this.AuthorizationManager.getPrivilegeLevelForProject( + .withArgs(this.project._id, this.token) + .resolves({ isValidReadAndWrite: true, isValidReadOnly: false }) + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( null, - this.project_id, - this.token, - this.callback + this.project._id, + this.token ) }) it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function () { - return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal( - false - ) - }) - - it('should not call AuthorizationManager.isUserSiteAdmin', function () { - return this.AuthorizationManager.isUserSiteAdmin.called.should.equal( + this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.called.should.equal( false ) }) it('should check if the token is valid', function () { - return this.TokenAccessHandler.validateTokenForAnonymousAccess - .calledWith(this.project_id, this.token) - .should.equal(true) + this.TokenAccessHandler.promises.validateTokenForAnonymousAccess.should.have.been.calledWith( + this.project._id, + this.token + ) }) it('should give read-write access', function () { - return this.callback - .calledWith(null, 'readAndWrite', false) - .should.equal(true) + expect(this.result).to.equal('readAndWrite') }) }) describe('when the token is valid for read-only', function () { - beforeEach(function () { - this.TokenAccessHandler.validateTokenForAnonymousAccess = sinon + beforeEach(async function () { + this.TokenAccessHandler.promises.validateTokenForAnonymousAccess = sinon .stub() - .withArgs(this.project_id, this.token) - .yields(null, false, true) - return this.AuthorizationManager.getPrivilegeLevelForProject( + .withArgs(this.project._id, this.token) + .resolves({ isValidReadAndWrite: false, isValidReadOnly: true }) + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( null, - this.project_id, - this.token, - this.callback + this.project._id, + this.token ) }) it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function () { - return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal( - false - ) - }) - - it('should not call AuthorizationManager.isUserSiteAdmin', function () { - return this.AuthorizationManager.isUserSiteAdmin.called.should.equal( + this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.called.should.equal( false ) }) it('should check if the token is valid', function () { - return this.TokenAccessHandler.validateTokenForAnonymousAccess - .calledWith(this.project_id, this.token) - .should.equal(true) + this.TokenAccessHandler.promises.validateTokenForAnonymousAccess.should.have.been.calledWith( + this.project._id, + this.token + ) }) it('should give read-only access', function () { - return this.callback - .calledWith(null, 'readOnly', false) - .should.equal(true) + expect(this.result).to.equal('readOnly') }) }) }) @@ -277,692 +233,372 @@ describe('AuthorizationManager', function () { describe('with a private project', function () { beforeEach(function () { - return this.ProjectGetter.getProject - .withArgs(this.project_id, { publicAccesLevel: 1 }) - .yields(null, { publicAccesLevel: 'private' }) + this.project.publicAccesLevel = 'private' }) - describe('with a user_id with a privilege level', function () { - beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin - .withArgs(this.user_id) - .yields(null, false) - this.CollaboratorsGetter.getMemberIdPrivilegeLevel - .withArgs(this.user_id, this.project_id) - .yields(null, 'readOnly') - return this.AuthorizationManager.getPrivilegeLevelForProject( - this.user_id, - this.project_id, - this.token, - this.callback + describe('with a user id with a privilege level', function () { + beforeEach(async function () { + this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel + .withArgs(this.user._id, this.project._id) + .resolves(PrivilegeLevels.READ_ONLY) + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + this.user._id, + this.project._id, + this.token ) }) it("should return the user's privilege level", function () { - return this.callback - .calledWith(null, 'readOnly', false, false) - .should.equal(true) + expect(this.result).to.equal('readOnly') }) }) - describe('with a user_id with no privilege level', function () { - beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin - .withArgs(this.user_id) - .yields(null, false) - this.CollaboratorsGetter.getMemberIdPrivilegeLevel - .withArgs(this.user_id, this.project_id) - .yields(null, false) - return this.AuthorizationManager.getPrivilegeLevelForProject( - this.user_id, - this.project_id, - this.token, - this.callback + describe('with a user id with no privilege level', function () { + beforeEach(async function () { + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + this.user._id, + this.project._id, + this.token ) }) it('should return false', function () { - return this.callback - .calledWith(null, false, false, false) - .should.equal(true) + expect(this.result).to.equal(false) }) }) - describe('with a user_id who is an admin', function () { - beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin - .withArgs(this.user_id) - .yields(null, true) - this.CollaboratorsGetter.getMemberIdPrivilegeLevel - .withArgs(this.user_id, this.project_id) - .yields(null, false) - return this.AuthorizationManager.getPrivilegeLevelForProject( - this.user_id, - this.project_id, - this.token, - this.callback + describe('with a user id who is an admin', function () { + beforeEach(async function () { + this.user.isAdmin = true + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + this.user._id, + this.project._id, + this.token ) }) it('should return the user as an owner', function () { - return this.callback - .calledWith(null, 'owner', false, true) - .should.equal(true) + expect(this.result).to.equal('owner') }) }) describe('with no user (anonymous)', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject( + beforeEach(async function () { + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( null, - this.project_id, - this.token, - this.callback + this.project._id, + this.token ) }) it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function () { - return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal( - false - ) - }) - - it('should not call AuthorizationManager.isUserSiteAdmin', function () { - return this.AuthorizationManager.isUserSiteAdmin.called.should.equal( + this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.called.should.equal( false ) }) it('should return false', function () { - return this.callback - .calledWith(null, false, false, false) - .should.equal(true) + expect(this.result).to.equal(false) }) }) }) describe('with a public project', function () { beforeEach(function () { - return this.ProjectGetter.getProject - .withArgs(this.project_id, { publicAccesLevel: 1 }) - .yields(null, { publicAccesLevel: 'readAndWrite' }) + this.project.publicAccesLevel = 'readAndWrite' }) - describe('with a user_id with a privilege level', function () { - beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin - .withArgs(this.user_id) - .yields(null, false) - this.CollaboratorsGetter.getMemberIdPrivilegeLevel - .withArgs(this.user_id, this.project_id) - .yields(null, 'readOnly') - return this.AuthorizationManager.getPrivilegeLevelForProject( - this.user_id, - this.project_id, - this.token, - this.callback + describe('with a user id with a privilege level', function () { + beforeEach(async function () { + this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel + .withArgs(this.user._id, this.project._id) + .resolves(PrivilegeLevels.READ_ONLY) + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + this.user._id, + this.project._id, + this.token ) }) it("should return the user's privilege level", function () { - return this.callback - .calledWith(null, 'readOnly', false) - .should.equal(true) + expect(this.result).to.equal('readOnly') }) }) - describe('with a user_id with no privilege level', function () { - beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin - .withArgs(this.user_id) - .yields(null, false) - this.CollaboratorsGetter.getMemberIdPrivilegeLevel - .withArgs(this.user_id, this.project_id) - .yields(null, false) - return this.AuthorizationManager.getPrivilegeLevelForProject( - this.user_id, - this.project_id, - this.token, - this.callback + describe('with a user id with no privilege level', function () { + beforeEach(async function () { + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + this.user._id, + this.project._id, + this.token ) }) it('should return the public privilege level', function () { - return this.callback - .calledWith(null, 'readAndWrite', true) - .should.equal(true) + expect(this.result).to.equal('readAndWrite') }) }) - describe('with a user_id who is an admin', function () { - beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin - .withArgs(this.user_id) - .yields(null, true) - this.CollaboratorsGetter.getMemberIdPrivilegeLevel - .withArgs(this.user_id, this.project_id) - .yields(null, false) - return this.AuthorizationManager.getPrivilegeLevelForProject( - this.user_id, - this.project_id, - this.token, - this.callback + describe('with a user id who is an admin', function () { + beforeEach(async function () { + this.user.isAdmin = true + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + this.user._id, + this.project._id, + this.token ) }) it('should return the user as an owner', function () { - return this.callback - .calledWith(null, 'owner', false) - .should.equal(true) + expect(this.result).to.equal('owner') }) }) describe('with no user (anonymous)', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject( + beforeEach(async function () { + this.result = await this.AuthorizationManager.promises.getPrivilegeLevelForProject( null, - this.project_id, - this.token, - this.callback + this.project._id, + this.token ) }) it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function () { - return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal( - false - ) - }) - - it('should not call AuthorizationManager.isUserSiteAdmin', function () { - return this.AuthorizationManager.isUserSiteAdmin.called.should.equal( + this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.called.should.equal( false ) }) it('should return the public privilege level', function () { - return this.callback - .calledWith(null, 'readAndWrite', true) - .should.equal(true) + expect(this.result).to.equal('readAndWrite') }) }) }) describe("when the project doesn't exist", function () { - beforeEach(function () { - return this.ProjectGetter.getProject - .withArgs(this.project_id, { publicAccesLevel: 1 }) - .yields(null, null) - }) - - it('should return a NotFoundError', function () { - return this.AuthorizationManager.getPrivilegeLevelForProject( - this.user_id, - this.project_id, - this.token, - error => error.should.be.instanceof(Errors.NotFoundError) - ) + it('should return a NotFoundError', async function () { + const someOtherId = new ObjectId() + await expect( + this.AuthorizationManager.promises.getPrivilegeLevelForProject( + this.user._id, + someOtherId, + this.token + ) + ).to.be.rejectedWith(Errors.NotFoundError) }) }) describe('when the project id is not valid', function () { beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin - .withArgs(this.user_id) - .yields(null, false) - return this.CollaboratorsGetter.getMemberIdPrivilegeLevel - .withArgs(this.user_id, this.project_id) - .yields(null, 'readOnly') + this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel + .withArgs(this.user._id, this.project._id) + .resolves(PrivilegeLevels.READ_ONLY) }) - it('should return a error', function (done) { - return this.AuthorizationManager.getPrivilegeLevelForProject( - undefined, - 'not project id', - this.token, - err => { - this.ProjectGetter.getProject.called.should.equal(false) - expect(err).to.exist - return done() - } - ) + it('should return a error', async function () { + await expect( + this.AuthorizationManager.promises.getPrivilegeLevelForProject( + undefined, + 'not project id', + this.token + ) + ).to.be.rejected }) }) }) - describe('canUserReadProject', function () { - beforeEach(function () { - return (this.AuthorizationManager.getPrivilegeLevelForProject = sinon.stub()) - }) - - describe('when user is owner', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'owner', false) - }) - - it('should return true', function (done) { - return this.AuthorizationManager.canUserReadProject( - this.user_id, - this.project_id, - this.token, - (error, canRead) => { - expect(canRead).to.equal(true) - return done() - } - ) - }) - }) - - describe('when user has read-write access', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'readAndWrite', false) - }) - - it('should return true', function (done) { - return this.AuthorizationManager.canUserReadProject( - this.user_id, - this.project_id, - this.token, - (error, canRead) => { - expect(canRead).to.equal(true) - return done() - } - ) - }) - }) - - describe('when user has read-only access', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'readOnly', false) - }) - - it('should return true', function (done) { - return this.AuthorizationManager.canUserReadProject( - this.user_id, - this.project_id, - this.token, - (error, canRead) => { - expect(canRead).to.equal(true) - return done() - } - ) - }) - }) - - describe('when user has no access', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, false, false) - }) - - it('should return false', function (done) { - return this.AuthorizationManager.canUserReadProject( - this.user_id, - this.project_id, - this.token, - (error, canRead) => { - expect(canRead).to.equal(false) - return done() - } - ) - }) - }) + testPermission('canUserReadProject', { + siteAdmin: true, + owner: true, + readAndWrite: true, + readOnly: true, + publicReadAndWrite: true, + publicReadOnly: true, + tokenReadAndWrite: true, + tokenReadOnly: true, }) - describe('canUserWriteProjectContent', function () { - beforeEach(function () { - return (this.AuthorizationManager.getPrivilegeLevelForProject = sinon.stub()) - }) - - describe('when user is owner', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'owner', false) - }) - - it('should return true', function (done) { - return this.AuthorizationManager.canUserWriteProjectContent( - this.user_id, - this.project_id, - this.token, - (error, canWrite) => { - expect(canWrite).to.equal(true) - return done() - } - ) - }) - }) - - describe('when user has read-write access', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'readAndWrite', false) - }) - - it('should return true', function (done) { - return this.AuthorizationManager.canUserWriteProjectContent( - this.user_id, - this.project_id, - this.token, - (error, canWrite) => { - expect(canWrite).to.equal(true) - return done() - } - ) - }) - }) - - describe('when user has read-only access', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'readOnly', false) - }) - - it('should return false', function (done) { - return this.AuthorizationManager.canUserWriteProjectContent( - this.user_id, - this.project_id, - this.token, - (error, canWrite) => { - expect(canWrite).to.equal(false) - return done() - } - ) - }) - }) - - describe('when user has no access', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, false, false) - }) - - it('should return false', function (done) { - return this.AuthorizationManager.canUserWriteProjectContent( - this.user_id, - this.project_id, - this.token, - (error, canWrite) => { - expect(canWrite).to.equal(false) - return done() - } - ) - }) - }) + testPermission('canUserWriteProjectContent', { + siteAdmin: true, + owner: true, + readAndWrite: true, + publicReadAndWrite: true, + tokenReadAndWrite: true, }) - describe('canUserWriteProjectSettings', function () { - beforeEach(function () { - return (this.AuthorizationManager.getPrivilegeLevelForProject = sinon.stub()) - }) - - describe('when user is owner', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'owner', false) - }) - - it('should return true', function (done) { - return this.AuthorizationManager.canUserWriteProjectSettings( - this.user_id, - this.project_id, - this.token, - (error, canWrite) => { - expect(canWrite).to.equal(true) - return done() - } - ) - }) - }) - - describe('when user has read-write access as a collaborator', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'readAndWrite', false) - }) - - it('should return true', function (done) { - return this.AuthorizationManager.canUserWriteProjectSettings( - this.user_id, - this.project_id, - this.token, - (error, canWrite) => { - expect(canWrite).to.equal(true) - return done() - } - ) - }) - }) - - describe('when user has read-write access as the public', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'readAndWrite', true) - }) - - it('should return false', function (done) { - return this.AuthorizationManager.canUserWriteProjectSettings( - this.user_id, - this.project_id, - this.token, - (error, canWrite) => { - expect(canWrite).to.equal(false) - return done() - } - ) - }) - }) - - describe('when user has read-only access', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'readOnly', false) - }) - - it('should return false', function (done) { - return this.AuthorizationManager.canUserWriteProjectSettings( - this.user_id, - this.project_id, - this.token, - (error, canWrite) => { - expect(canWrite).to.equal(false) - return done() - } - ) - }) - }) - - describe('when user has no access', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, false, false) - }) - - it('should return false', function (done) { - return this.AuthorizationManager.canUserWriteProjectSettings( - this.user_id, - this.project_id, - this.token, - (error, canWrite) => { - expect(canWrite).to.equal(false) - return done() - } - ) - }) - }) + testPermission('canUserWriteProjectSettings', { + siteAdmin: true, + owner: true, + readAndWrite: true, + tokenReadAndWrite: true, }) - describe('canUserAdminProject', function () { - beforeEach(function () { - return (this.AuthorizationManager.getPrivilegeLevelForProject = sinon.stub()) - }) - - describe('when user is owner', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'owner', false) - }) - - it('should return true', function (done) { - return this.AuthorizationManager.canUserAdminProject( - this.user_id, - this.project_id, - this.token, - (error, canAdmin) => { - expect(canAdmin).to.equal(true) - return done() - } - ) - }) - }) - - describe('when user has read-write access', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'readAndWrite', false) - }) - - it('should return false', function (done) { - return this.AuthorizationManager.canUserAdminProject( - this.user_id, - this.project_id, - this.token, - (error, canAdmin) => { - expect(canAdmin).to.equal(false) - return done() - } - ) - }) - }) - - describe('when user has read-only access', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, 'readOnly', false) - }) - - it('should return false', function (done) { - return this.AuthorizationManager.canUserAdminProject( - this.user_id, - this.project_id, - this.token, - (error, canAdmin) => { - expect(canAdmin).to.equal(false) - return done() - } - ) - }) - }) - - describe('when user has no access', function () { - beforeEach(function () { - return this.AuthorizationManager.getPrivilegeLevelForProject - .withArgs(this.user_id, this.project_id, this.token) - .yields(null, false, false) - }) - - it('should return false', function (done) { - return this.AuthorizationManager.canUserAdminProject( - this.user_id, - this.project_id, - this.token, - (error, canAdmin) => { - expect(canAdmin).to.equal(false) - return done() - } - ) - }) - }) + testPermission('canUserRenameProject', { + siteAdmin: true, + owner: true, }) + testPermission('canUserAdminProject', { siteAdmin: true, owner: true }) + describe('isUserSiteAdmin', function () { - beforeEach(function () { - return (this.User.findOne = sinon.stub()) - }) - describe('when user is admin', function () { beforeEach(function () { - return this.User.findOne - .withArgs({ _id: this.user_id }, { isAdmin: 1 }) - .yields(null, { isAdmin: true }) + this.user.isAdmin = true }) - it('should return true', function (done) { - return this.AuthorizationManager.isUserSiteAdmin( - this.user_id, - (error, isAdmin) => { - expect(isAdmin).to.equal(true) - return done() - } + it('should return true', async function () { + const isAdmin = await this.AuthorizationManager.promises.isUserSiteAdmin( + this.user._id ) + expect(isAdmin).to.equal(true) }) }) describe('when user is not admin', function () { - beforeEach(function () { - return this.User.findOne - .withArgs({ _id: this.user_id }, { isAdmin: 1 }) - .yields(null, { isAdmin: false }) - }) - - it('should return false', function (done) { - return this.AuthorizationManager.isUserSiteAdmin( - this.user_id, - (error, isAdmin) => { - expect(isAdmin).to.equal(false) - return done() - } + it('should return false', async function () { + const isAdmin = await this.AuthorizationManager.promises.isUserSiteAdmin( + this.user._id ) + expect(isAdmin).to.equal(false) }) }) describe('when user is not found', function () { - beforeEach(function () { - return this.User.findOne - .withArgs({ _id: this.user_id }, { isAdmin: 1 }) - .yields(null, null) - }) - - it('should return false', function (done) { - return this.AuthorizationManager.isUserSiteAdmin( - this.user_id, - (error, isAdmin) => { - expect(isAdmin).to.equal(false) - return done() - } + it('should return false', async function () { + const someOtherId = new ObjectId() + const isAdmin = await this.AuthorizationManager.promises.isUserSiteAdmin( + someOtherId ) + expect(isAdmin).to.equal(false) }) }) describe('when no user is passed', function () { - it('should return false', function (done) { - return this.AuthorizationManager.isUserSiteAdmin( - null, - (error, isAdmin) => { - this.User.findOne.called.should.equal(false) - expect(isAdmin).to.equal(false) - return done() - } + it('should return false', async function () { + const isAdmin = await this.AuthorizationManager.promises.isUserSiteAdmin( + null ) + expect(isAdmin).to.equal(false) }) }) }) }) + +function testPermission(permission, privilegeLevels) { + describe(permission, function () { + describe('when authenticated', function () { + describe('when user is site admin', function () { + beforeEach('set user as site admin', function () { + this.user.isAdmin = true + }) + expectPermission(permission, privilegeLevels.siteAdmin || false) + }) + + describe('when user is owner', function () { + setupUserPrivilegeLevel(PrivilegeLevels.OWNER) + expectPermission(permission, privilegeLevels.owner || false) + }) + + describe('when user has read-write access', function () { + setupUserPrivilegeLevel(PrivilegeLevels.READ_AND_WRITE) + expectPermission(permission, privilegeLevels.readAndWrite || false) + }) + + describe('when user has read-only access', function () { + setupUserPrivilegeLevel(PrivilegeLevels.READ_ONLY) + expectPermission(permission, privilegeLevels.readOnly || false) + }) + + describe('when user has read-write access as the public', function () { + setupPublicAccessLevel(PublicAccessLevels.READ_AND_WRITE) + expectPermission( + permission, + privilegeLevels.publicReadAndWrite || false + ) + }) + + describe('when user has read-only access as the public', function () { + setupPublicAccessLevel(PublicAccessLevels.READ_ONLY) + expectPermission(permission, privilegeLevels.publicReadOnly || false) + }) + + describe('when user is not found', function () { + it('should return false', async function () { + const otherUserId = new ObjectId() + const value = await this.AuthorizationManager.promises[permission]( + otherUserId, + this.project._id, + this.token + ) + expect(value).to.equal(false) + }) + }) + }) + + describe('when anonymous', function () { + beforeEach(function () { + this.user = null + }) + + describe('with read-write access through a token', function () { + setupTokenAccessLevel('readAndWrite') + expectPermission(permission, privilegeLevels.tokenReadAndWrite || false) + }) + + describe('with read-only access through a token', function () { + setupTokenAccessLevel('readOnly') + expectPermission(permission, privilegeLevels.tokenReadOnly || false) + }) + + describe('with public read-write access', function () { + setupPublicAccessLevel(PublicAccessLevels.READ_AND_WRITE) + expectPermission( + permission, + privilegeLevels.publicReadAndWrite || false + ) + }) + + describe('with public read-only access', function () { + setupPublicAccessLevel(PublicAccessLevels.READ_ONLY) + expectPermission(permission, privilegeLevels.publicReadOnly || false) + }) + }) + }) +} + +function setupUserPrivilegeLevel(privilegeLevel) { + beforeEach(`set user privilege level to ${privilegeLevel}`, function () { + this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel + .withArgs(this.user._id, this.project._id) + .resolves(privilegeLevel) + }) +} + +function setupPublicAccessLevel(level) { + beforeEach(`set public access level to ${level}`, function () { + this.project.publicAccesLevel = level + }) +} + +function setupTokenAccessLevel(level) { + beforeEach(`set token access level to ${level}`, function () { + this.project.publicAccesLevel = PublicAccessLevels.TOKEN_BASED + this.TokenAccessHandler.promises.validateTokenForAnonymousAccess + .withArgs(this.project._id, this.token) + .resolves({ + isValidReadAndWrite: level === 'readAndWrite', + isValidReadOnly: level === 'readOnly', + }) + }) +} + +function expectPermission(permission, expectedValue) { + it(`should return ${expectedValue}`, async function () { + const value = await this.AuthorizationManager.promises[permission]( + this.user && this.user._id, + this.project._id, + this.token + ) + expect(value).to.equal(expectedValue) + }) +} diff --git a/services/web/test/unit/src/Authorization/AuthorizationMiddlewareTests.js b/services/web/test/unit/src/Authorization/AuthorizationMiddlewareTests.js index f0d1b9631c..2bcfd51ac0 100644 --- a/services/web/test/unit/src/Authorization/AuthorizationMiddlewareTests.js +++ b/services/web/test/unit/src/Authorization/AuthorizationMiddlewareTests.js @@ -1,6 +1,7 @@ const sinon = require('sinon') const { expect } = require('chai') const SandboxedModule = require('sandboxed-module') +const { ObjectId } = require('mongodb') const Errors = require('../../../../app/src/Features/Errors/Errors.js') const MODULE_PATH = @@ -8,31 +9,34 @@ const MODULE_PATH = describe('AuthorizationMiddleware', function () { beforeEach(function () { - this.userId = 'user-id-123' - this.project_id = 'project-id-123' + this.userId = new ObjectId().toString() + this.project_id = new ObjectId().toString() this.token = 'some-token' this.AuthenticationController = {} this.SessionManager = { getLoggedInUserId: sinon.stub().returns(this.userId), isUserLoggedIn: sinon.stub().returns(true), } - this.AuthorizationManager = {} + this.AuthorizationManager = { + promises: { + canUserReadProject: sinon.stub(), + canUserWriteProjectSettings: sinon.stub(), + canUserWriteProjectContent: sinon.stub(), + canUserAdminProject: sinon.stub(), + canUserRenameProject: sinon.stub(), + isUserSiteAdmin: sinon.stub(), + isRestrictedUserForProject: sinon.stub(), + }, + } this.HttpErrorHandler = { forbidden: sinon.stub(), } this.TokenAccessHandler = { getRequestToken: sinon.stub().returns(this.token), } - this.ObjectId = { - isValid: sinon.stub().withArgs(this.project_id).returns(true), - } - this.AuthorizationManager = {} this.AuthorizationMiddleware = SandboxedModule.require(MODULE_PATH, { requires: { './AuthorizationManager': this.AuthorizationManager, - mongodb: { - ObjectId: this.ObjectId, - }, '../Errors/HttpErrorHandler': this.HttpErrorHandler, '../Authentication/AuthenticationController': this .AuthenticationController, @@ -40,542 +44,355 @@ describe('AuthorizationMiddleware', function () { '../TokenAccess/TokenAccessHandler': this.TokenAccessHandler, }, }) - this.req = {} - this.res = {} + this.req = { + params: { + project_id: this.project_id, + }, + body: {}, + } + this.res = { + redirect: sinon.stub(), + locals: { + currentUrl: '/current/url', + }, + } this.next = sinon.stub() }) - describe('_getUserId', function () { - beforeEach(function () { - this.req = {} + describe('ensureCanReadProject', function () { + testMiddleware('ensureUserCanReadProject', 'canUserReadProject') + }) + + describe('ensureUserCanWriteProjectContent', function () { + testMiddleware( + 'ensureUserCanWriteProjectContent', + 'canUserWriteProjectContent' + ) + }) + + describe('ensureUserCanWriteProjectSettings', function () { + describe('when renaming a project', function () { + beforeEach(function () { + this.req.body.name = 'new project name' + }) + + testMiddleware( + 'ensureUserCanWriteProjectSettings', + 'canUserRenameProject' + ) }) - it('should get the user from session', function (done) { - this.SessionManager.getLoggedInUserId = sinon.stub().returns('1234') - this.AuthorizationMiddleware._getUserId(this.req, (err, userId) => { - expect(err).to.not.exist - expect(userId).to.equal('1234') - done() + describe('when setting another parameter', function () { + beforeEach(function () { + this.req.body.compiler = 'texlive-2017' }) - }) - it('should get oauth_user from request', function (done) { - this.SessionManager.getLoggedInUserId = sinon.stub().returns(null) - this.req.oauth_user = { _id: '5678' } - this.AuthorizationMiddleware._getUserId(this.req, (err, userId) => { - expect(err).to.not.exist - expect(userId).to.equal('5678') - done() - }) - }) - - it('should fall back to null', function (done) { - this.SessionManager.getLoggedInUserId = sinon.stub().returns(null) - this.req.oauth_user = undefined - this.AuthorizationMiddleware._getUserId(this.req, (err, userId) => { - expect(err).to.not.exist - expect(userId).to.equal(null) - done() - }) + testMiddleware( + 'ensureUserCanWriteProjectSettings', + 'canUserWriteProjectSettings' + ) }) }) - const METHODS_TO_TEST = { - ensureUserCanReadProject: 'canUserReadProject', - ensureUserCanWriteProjectSettings: 'canUserWriteProjectSettings', - ensureUserCanWriteProjectContent: 'canUserWriteProjectContent', - } - Object.entries(METHODS_TO_TEST).forEach( - ([middlewareMethod, managerMethod]) => { - describe(middlewareMethod, function () { - beforeEach(function () { - this.req.params = { project_id: this.project_id } - this.AuthorizationManager[managerMethod] = sinon.stub() - this.AuthorizationMiddleware.redirectToRestricted = sinon.stub() - }) - - describe('with missing project_id', function () { - beforeEach(function () { - this.req.params = {} - }) - - it('should return an error to next', function () { - this.AuthorizationMiddleware[middlewareMethod]( - this.req, - this.res, - this.next - ) - this.next - .calledWith(sinon.match.instanceOf(Error)) - .should.equal(true) - }) - }) - - describe('with logged in user', function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(this.userId) - }) - - describe('when user has permission', function () { - beforeEach(function () { - this.AuthorizationManager[managerMethod] - .withArgs(this.userId, this.project_id, this.token) - .yields(null, true) - }) - - it('should return next', function () { - this.AuthorizationMiddleware[middlewareMethod]( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(true) - }) - }) - - describe("when user doesn't have permission", function () { - beforeEach(function () { - this.AuthorizationManager[managerMethod] - .withArgs(this.userId, this.project_id, this.token) - .yields(null, false) - }) - - it('should raise a 403', function () { - this.AuthorizationMiddleware[middlewareMethod]( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(false) - this.HttpErrorHandler.forbidden - .calledWith(this.req, this.res) - .should.equal(true) - }) - }) - }) - - describe('with anonymous user', function () { - describe('when user has permission', function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(null) - this.AuthorizationManager[managerMethod] - .withArgs(null, this.project_id, this.token) - .yields(null, true) - }) - - it('should return next', function () { - this.AuthorizationMiddleware[middlewareMethod]( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(true) - }) - }) - - describe("when user doesn't have permission", function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(null) - this.AuthorizationManager[managerMethod] - .withArgs(null, this.project_id, this.token) - .yields(null, false) - }) - - it('should redirect to redirectToRestricted', function () { - this.AuthorizationMiddleware[middlewareMethod]( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(false) - this.HttpErrorHandler.forbidden - .calledWith(this.req, this.res) - .should.equal(true) - }) - }) - }) - - describe('with malformed project id', function () { - beforeEach(function () { - this.req.params = { project_id: 'blah' } - this.ObjectId.isValid = sinon.stub().returns(false) - }) - - it('should return a not found error', function (done) { - this.AuthorizationMiddleware[middlewareMethod]( - this.req, - this.res, - error => { - error.should.be.instanceof(Errors.NotFoundError) - return done() - } - ) - }) - }) - }) - } - ) - describe('ensureUserCanAdminProject', function () { - beforeEach(function () { - this.req.params = { project_id: this.project_id } - this.AuthorizationManager.canUserAdminProject = sinon.stub() - this.AuthorizationMiddleware.redirectToRestricted = sinon.stub() - }) - - describe('with missing project_id', function () { - beforeEach(function () { - this.req.params = {} - }) - - it('should return an error to next', function () { - this.AuthorizationMiddleware.ensureUserCanAdminProject( - this.req, - this.res, - this.next - ) - this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) - }) - }) - - describe('with logged in user', function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(this.userId) - }) - - describe('when user has permission', function () { - beforeEach(function () { - this.AuthorizationManager.canUserAdminProject - .withArgs(this.userId, this.project_id, this.token) - .yields(null, true) - }) - - it('should return next', function () { - this.AuthorizationMiddleware.ensureUserCanAdminProject( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(true) - }) - }) - - describe("when user doesn't have permission", function () { - beforeEach(function () { - this.AuthorizationManager.canUserAdminProject - .withArgs(this.userId, this.project_id, this.token) - .yields(null, false) - }) - - it('should invoke HTTP forbidden error handler', function (done) { - this.HttpErrorHandler.forbidden = sinon.spy(() => done()) - this.AuthorizationMiddleware.ensureUserCanAdminProject( - this.req, - this.res - ) - }) - }) - }) - - describe('with anonymous user', function () { - describe('when user has permission', function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(null) - this.AuthorizationManager.canUserAdminProject - .withArgs(null, this.project_id, this.token) - .yields(null, true) - }) - - it('should return next', function () { - this.AuthorizationMiddleware.ensureUserCanAdminProject( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(true) - }) - }) - - describe("when user doesn't have permission", function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(null) - this.AuthorizationManager.canUserAdminProject - .withArgs(null, this.project_id, this.token) - .yields(null, false) - }) - - it('should invoke HTTP forbidden error handler', function (done) { - this.HttpErrorHandler.forbidden = sinon.spy(() => done()) - this.AuthorizationMiddleware.ensureUserCanAdminProject( - this.req, - this.res - ) - }) - }) - }) - - describe('with malformed project id', function () { - beforeEach(function () { - this.req.params = { project_id: 'blah' } - this.ObjectId.isValid = sinon.stub().returns(false) - }) - - it('should return a not found error', function (done) { - this.AuthorizationMiddleware.ensureUserCanAdminProject( - this.req, - this.res, - error => { - error.should.be.instanceof(Errors.NotFoundError) - return done() - } - ) - }) - }) + testMiddleware('ensureUserCanAdminProject', 'canUserAdminProject') }) describe('ensureUserIsSiteAdmin', function () { - beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin = sinon.stub() - this.AuthorizationMiddleware.redirectToRestricted = sinon.stub() - }) - describe('with logged in user', function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(this.userId) - }) - describe('when user has permission', function () { - beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin - .withArgs(this.userId) - .yields(null, true) - }) - - it('should return next', function () { - this.AuthorizationMiddleware.ensureUserIsSiteAdmin( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(true) - }) + setupSiteAdmin(true) + invokeMiddleware('ensureUserIsSiteAdmin') + expectNext() }) describe("when user doesn't have permission", function () { - beforeEach(function () { - this.AuthorizationManager.isUserSiteAdmin - .withArgs(this.userId) - .yields(null, false) - }) + setupSiteAdmin(false) + invokeMiddleware('ensureUserIsSiteAdmin') + expectRedirectToRestricted() + }) + }) - it('should redirect to redirectToRestricted', function () { - this.AuthorizationMiddleware.ensureUserIsSiteAdmin( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(false) - this.AuthorizationMiddleware.redirectToRestricted - .calledWith(this.req, this.res, this.next) - .should.equal(true) - }) + describe('with oauth user', function () { + setupOAuthUser() + + describe('when user has permission', function () { + setupSiteAdmin(true) + invokeMiddleware('ensureUserIsSiteAdmin') + expectNext() + }) + + describe("when user doesn't have permission", function () { + setupSiteAdmin(false) + invokeMiddleware('ensureUserIsSiteAdmin') + expectRedirectToRestricted() }) }) describe('with anonymous user', function () { - describe('when user has permission', function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(null) - this.AuthorizationManager.isUserSiteAdmin - .withArgs(null) - .yields(null, true) - }) - - it('should return next', function () { - this.AuthorizationMiddleware.ensureUserIsSiteAdmin( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(true) - }) - }) - - describe("when user doesn't have permission", function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(null) - this.AuthorizationManager.isUserSiteAdmin - .withArgs(null) - .yields(null, false) - }) - - it('should redirect to redirectToRestricted', function () { - this.AuthorizationMiddleware.ensureUserIsSiteAdmin( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(false) - this.AuthorizationMiddleware.redirectToRestricted - .calledWith(this.req, this.res, this.next) - .should.equal(true) - }) - }) + setupAnonymousUser() + invokeMiddleware('ensureUserIsSiteAdmin') + expectRedirectToRestricted() }) }) describe('blockRestrictedUserFromProject', function () { - beforeEach(function () { - this.AuthorizationMiddleware._getUserAndProjectId = sinon - .stub() - .callsArgWith(1, null, this.userId, this.project_id) + describe('for a restricted user', function () { + setupPermission('isRestrictedUserForProject', true) + invokeMiddleware('blockRestrictedUserFromProject') + expectForbidden() }) - it('should issue a 401 response for a restricted user', function (done) { - this.AuthorizationManager.isRestrictedUserForProject = sinon - .stub() - .callsArgWith(3, null, true) - this.req = {} - this.next = sinon.stub() - this.res.sendStatus = status => { - expect(status).to.equal(403) - expect( - this.AuthorizationManager.isRestrictedUserForProject.called - ).to.equal(true) - expect(this.next.called).to.equal(false) - done() - } - this.AuthorizationMiddleware.blockRestrictedUserFromProject( - this.req, - this.res, - this.next - ) - }) - - it('should pass through for a regular user', function (done) { - this.AuthorizationManager.isRestrictedUserForProject = sinon - .stub() - .callsArgWith(3, null, false) - this.req = {} - this.res.sendStatus = sinon.stub() - this.next = status => { - expect( - this.AuthorizationManager.isRestrictedUserForProject.called - ).to.equal(true) - expect(this.res.sendStatus.called).to.equal(false) - done() - } - this.AuthorizationMiddleware.blockRestrictedUserFromProject( - this.req, - this.res, - this.next - ) + describe('for a regular user', function (done) { + setupPermission('isRestrictedUserForProject', false) + invokeMiddleware('blockRestrictedUserFromProject') + expectNext() }) }) describe('ensureUserCanReadMultipleProjects', function () { beforeEach(function () { - this.AuthorizationManager.canUserReadProject = sinon.stub() - this.AuthorizationMiddleware.redirectToRestricted = sinon.stub() this.req.query = { project_ids: 'project1,project2' } }) describe('with logged in user', function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(this.userId) - }) - describe('when user has permission to access all projects', function () { beforeEach(function () { - this.AuthorizationManager.canUserReadProject + this.AuthorizationManager.promises.canUserReadProject .withArgs(this.userId, 'project1', this.token) - .yields(null, true) - this.AuthorizationManager.canUserReadProject + .resolves(true) + this.AuthorizationManager.promises.canUserReadProject .withArgs(this.userId, 'project2', this.token) - .yields(null, true) + .resolves(true) }) - it('should return next', function () { - this.AuthorizationMiddleware.ensureUserCanReadMultipleProjects( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(true) - }) + invokeMiddleware('ensureUserCanReadMultipleProjects') + expectNext() }) describe("when user doesn't have permission to access one of the projects", function () { beforeEach(function () { - this.AuthorizationManager.canUserReadProject + this.AuthorizationManager.promises.canUserReadProject .withArgs(this.userId, 'project1', this.token) - .yields(null, true) - this.AuthorizationManager.canUserReadProject + .resolves(true) + this.AuthorizationManager.promises.canUserReadProject .withArgs(this.userId, 'project2', this.token) - .yields(null, false) + .resolves(false) }) - it('should redirect to redirectToRestricted', function () { - this.AuthorizationMiddleware.ensureUserCanReadMultipleProjects( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(false) - this.AuthorizationMiddleware.redirectToRestricted - .calledWith(this.req, this.res, this.next) - .should.equal(true) - }) + invokeMiddleware('ensureUserCanReadMultipleProjects') + expectRedirectToRestricted() }) }) + describe('with oauth user', function () { + setupOAuthUser() + + beforeEach(function () { + this.AuthorizationManager.promises.canUserReadProject + .withArgs(this.userId, 'project1', this.token) + .resolves(true) + this.AuthorizationManager.promises.canUserReadProject + .withArgs(this.userId, 'project2', this.token) + .resolves(true) + }) + + invokeMiddleware('ensureUserCanReadMultipleProjects') + expectNext() + }) + describe('with anonymous user', function () { + setupAnonymousUser() + describe('when user has permission', function () { describe('when user has permission to access all projects', function () { beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(null) - this.AuthorizationManager.canUserReadProject + this.AuthorizationManager.promises.canUserReadProject .withArgs(null, 'project1', this.token) - .yields(null, true) - this.AuthorizationManager.canUserReadProject + .resolves(true) + this.AuthorizationManager.promises.canUserReadProject .withArgs(null, 'project2', this.token) - .yields(null, true) + .resolves(true) }) - it('should return next', function () { - this.AuthorizationMiddleware.ensureUserCanReadMultipleProjects( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(true) - }) + invokeMiddleware('ensureUserCanReadMultipleProjects') + expectNext() }) describe("when user doesn't have permission to access one of the projects", function () { beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(null) - this.AuthorizationManager.canUserReadProject + this.AuthorizationManager.promises.canUserReadProject .withArgs(null, 'project1', this.token) - .yields(null, true) - this.AuthorizationManager.canUserReadProject + .resolves(true) + this.AuthorizationManager.promises.canUserReadProject .withArgs(null, 'project2', this.token) - .yields(null, false) + .resolves(false) }) - it('should redirect to redirectToRestricted', function () { - this.AuthorizationMiddleware.ensureUserCanReadMultipleProjects( - this.req, - this.res, - this.next - ) - this.next.called.should.equal(false) - this.AuthorizationMiddleware.redirectToRestricted - .calledWith(this.req, this.res, this.next) - .should.equal(true) - }) + invokeMiddleware('ensureUserCanReadMultipleProjects') + expectRedirectToRestricted() }) }) }) }) }) + +function testMiddleware(middleware, permission) { + describe(middleware, function () { + describe('with missing project_id', function () { + setupMissingProjectId() + invokeMiddleware(middleware) + expectError() + }) + + describe('with logged in user', function () { + describe('when user has permission', function () { + setupPermission(permission, true) + invokeMiddleware(middleware) + expectNext() + }) + + describe("when user doesn't have permission", function () { + setupPermission(permission, false) + invokeMiddleware(middleware) + expectForbidden() + }) + }) + + describe('with oauth user', function () { + setupOAuthUser() + + describe('when user has permission', function () { + setupPermission(permission, true) + invokeMiddleware(middleware) + expectNext() + }) + + describe("when user doesn't have permission", function () { + setupPermission(permission, false) + invokeMiddleware(middleware) + expectForbidden() + }) + }) + + describe('with anonymous user', function () { + setupAnonymousUser() + + describe('when user has permission', function () { + setupAnonymousPermission(permission, true) + invokeMiddleware(middleware) + expectNext() + }) + + describe("when user doesn't have permission", function () { + setupAnonymousPermission(permission, false) + invokeMiddleware(middleware) + expectForbidden() + }) + }) + + describe('with malformed project id', function () { + setupMalformedProjectId() + invokeMiddleware(middleware) + expectNotFound() + }) + }) +} + +function setupAnonymousUser() { + beforeEach('set up anonymous user', function () { + this.SessionManager.getLoggedInUserId.returns(null) + this.SessionManager.isUserLoggedIn.returns(false) + }) +} + +function setupOAuthUser() { + beforeEach('set up oauth user', function () { + this.SessionManager.getLoggedInUserId.returns(null) + this.req.oauth_user = { _id: this.userId } + }) +} + +function setupPermission(permission, value) { + beforeEach(`set permission ${permission} to ${value}`, function () { + this.AuthorizationManager.promises[permission] + .withArgs(this.userId, this.project_id, this.token) + .resolves(value) + }) +} + +function setupAnonymousPermission(permission, value) { + beforeEach(`set anonymous permission ${permission} to ${value}`, function () { + this.AuthorizationManager.promises[permission] + .withArgs(null, this.project_id, this.token) + .resolves(value) + }) +} + +function setupSiteAdmin(value) { + beforeEach(`set site admin to ${value}`, function () { + this.AuthorizationManager.promises.isUserSiteAdmin + .withArgs(this.userId) + .resolves(value) + }) +} + +function setupMissingProjectId() { + beforeEach('set up missing project id', function () { + delete this.req.params.project_id + }) +} + +function setupMalformedProjectId() { + beforeEach('set up malformed project id', function () { + this.req.params = { project_id: 'bad-project-id' } + }) +} + +function invokeMiddleware(method) { + beforeEach(`invoke ${method}`, function (done) { + this.next.callsFake(() => done()) + this.HttpErrorHandler.forbidden.callsFake(() => done()) + this.res.redirect.callsFake(() => done()) + this.AuthorizationMiddleware[method](this.req, this.res, this.next) + }) +} + +function expectNext() { + it('calls the next middleware', function () { + expect(this.next).to.have.been.calledWithExactly() + }) +} + +function expectError() { + it('calls the error middleware', function () { + expect(this.next).to.have.been.calledWith(sinon.match.instanceOf(Error)) + }) +} + +function expectNotFound() { + it('raises a 404', function () { + expect(this.next).to.have.been.calledWith( + sinon.match.instanceOf(Errors.NotFoundError) + ) + }) +} + +function expectForbidden() { + it('raises a 403', function () { + expect(this.HttpErrorHandler.forbidden).to.have.been.calledWith( + this.req, + this.res + ) + expect(this.next).not.to.have.been.called + }) +} + +function expectRedirectToRestricted() { + it('redirects to restricted', function () { + expect(this.res.redirect).to.have.been.calledWith( + '/restricted?from=%2Fcurrent%2Furl' + ) + expect(this.next).not.to.have.been.called + }) +}