Răsfoiți Sursa

Merge pull request #4947 from overleaf/em-project-rename-for-owners-only

Prevent collaborators from renaming a project

GitOrigin-RevId: 94d12e25592fea55b84427aeae78f7bb2a544a58
Eric Mc Sween 5 ani în urmă
părinte
comite
a10c042e20

+ 220 - 271
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)
-    )
-  },
+function 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
-              )
-            )
-          }
-        )
-      }
-    )
-  },
+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
+}
 
-  getPublicAccessLevel(projectId, callback) {
-    if (!ObjectId.isValid(projectId)) {
-      return callback(new Error('invalid project id'))
+/**
+ * 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
     }
-    // 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
-      )
+  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
+    }
+  }
 
-  // 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)
-            }
-          )
-        })
-      }
-    )
-  },
+  return PrivilegeLevels.NONE
+}
 
-  // 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)
-      }
-    )
-  },
+// 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)
+  }
 
-  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)
-      }
-    )
-  },
+  // Deny anonymous user access
+  return PrivilegeLevels.NONE
+}
 
-  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)
-        )
-      }
-    )
-  },
+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
+}
 
-  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
-          )
-        )
-      }
-    )
-  },
+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)
+}
 
-  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)
-      }
-    )
-  },
+async function canUserWriteProjectContent(userId, projectId, token) {
+  const privilegeLevel = await getPrivilegeLevelForProject(
+    userId,
+    projectId,
+    token
+  )
+  return [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
+    privilegeLevel
+  )
+}
 
-  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
-        )
-      }
-    )
-  },
+async function canUserWriteProjectSettings(userId, projectId, token) {
+  const privilegeLevel = await getPrivilegeLevelForProject(
+    userId,
+    projectId,
+    token,
+    { ignorePublicAccess: true }
+  )
+  return [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
+    privilegeLevel
+  )
+}
 
-  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)
-    })
-  },
+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
 }
 
-module.exports = AuthorizationManager
-module.exports.promises = promisifyAll(AuthorizationManager, {
-  without: 'isRestrictedUser',
-})
+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,
+  },
+}

+ 170 - 249
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
     )
-  },
+    if (!canRead) {
+      return _redirectToRestricted(req, res, next)
+    }
+  }
+  next()
+}
 
-  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)
-          }
-        )
-      }
-    )
-  },
+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)
+}
 
-  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)
-          }
-        )
-      }
+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)
+    }
+  }
 
-  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)
-          }
-        )
-      }
+  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)
+    }
+  }
 
-  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)
-          }
-        )
-      }
+  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)
+}
 
-  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)
-      })
-    })
-  },
+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)
+}
 
-  _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 (!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)
-    })
-  },
+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)
+}
 
-  _getUserId(req, callback) {
-    const userId =
-      SessionManager.getLoggedInUserId(req.session) ||
-      (req.oauth_user && req.oauth_user._id) ||
-      null
-    callback(null, userId)
-  },
+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
+}
 
-  redirectToRestricted(req, res, next) {
-    // TODO: move this to throwing ForbiddenError
-    res.redirect(
-      `/restricted?from=${encodeURIComponent(res.locals.currentUrl)}`
-    )
-  },
+function _getUserId(req) {
+  return (
+    SessionManager.getLoggedInUserId(req.session) ||
+    (req.oauth_user && req.oauth_user._id) ||
+    null
+  )
+}
 
-  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')
-  },
+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,
 }

+ 3 - 1
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

+ 1 - 0
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
   )

+ 89 - 56
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) {

+ 340 - 704
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' })
-      })
-
-      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
+        this.project.publicAccesLevel = 'tokenBased'
+      })
+
+      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' })
-      })
-
-      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
+        this.project.publicAccesLevel = 'private'
+      })
+
+      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' })
-      })
-
-      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
+        this.project.publicAccesLevel = 'readAndWrite'
+      })
+
+      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')
-      })
-
-      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()
-          }
-        )
+        this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel
+          .withArgs(this.user._id, this.project._id)
+          .resolves(PrivilegeLevels.READ_ONLY)
       })
-    })
-  })
 
-  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()
-          }
-        )
+      it('should return a error', async function () {
+        await expect(
+          this.AuthorizationManager.promises.getPrivilegeLevelForProject(
+            undefined,
+            'not project id',
+            this.token
+          )
+        ).to.be.rejected
       })
     })
+  })
 
-    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()
-          }
-        )
-      })
-    })
+  testPermission('canUserReadProject', {
+    siteAdmin: true,
+    owner: true,
+    readAndWrite: true,
+    readOnly: true,
+    publicReadAndWrite: true,
+    publicReadOnly: true,
+    tokenReadAndWrite: true,
+    tokenReadOnly: true,
+  })
 
-    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()
-          }
-        )
-      })
-    })
+  testPermission('canUserWriteProjectContent', {
+    siteAdmin: true,
+    owner: true,
+    readAndWrite: true,
+    publicReadAndWrite: true,
+    tokenReadAndWrite: true,
+  })
 
-    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('canUserWriteProjectSettings', {
+    siteAdmin: true,
+    owner: true,
+    readAndWrite: true,
+    tokenReadAndWrite: true,
   })
 
-  describe('canUserWriteProjectContent', function () {
-    beforeEach(function () {
-      return (this.AuthorizationManager.getPrivilegeLevelForProject = sinon.stub())
-    })
+  testPermission('canUserRenameProject', {
+    siteAdmin: true,
+    owner: true,
+  })
 
-    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()
-          }
-        )
-      })
-    })
+  testPermission('canUserAdminProject', { siteAdmin: true, owner: true })
 
-    describe('when user has read-write access', function () {
+  describe('isUserSiteAdmin', function () {
+    describe('when user is admin', 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()
-          }
-        )
+        this.user.isAdmin = true
       })
-    })
 
-    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()
-          }
+      it('should return true', async function () {
+        const isAdmin = await this.AuthorizationManager.promises.isUserSiteAdmin(
+          this.user._id
         )
+        expect(isAdmin).to.equal(true)
       })
     })
 
-    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()
-          }
+    describe('when user is not admin', function () {
+      it('should return false', async function () {
+        const isAdmin = await this.AuthorizationManager.promises.isUserSiteAdmin(
+          this.user._id
         )
+        expect(isAdmin).to.equal(false)
       })
     })
-  })
 
-  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 is not found', function () {
+      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 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 no user is passed', function () {
+      it('should return false', async function () {
+        const isAdmin = await this.AuthorizationManager.promises.isUserSiteAdmin(
+          null
         )
+        expect(isAdmin).to.equal(false)
       })
     })
+  })
+})
 
-    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()
-          }
-        )
+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 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 is owner', function () {
+        setupUserPrivilegeLevel(PrivilegeLevels.OWNER)
+        expectPermission(permission, privilegeLevels.owner || false)
       })
-    })
 
-    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()
-          }
-        )
+      describe('when user has read-write access', function () {
+        setupUserPrivilegeLevel(PrivilegeLevels.READ_AND_WRITE)
+        expectPermission(permission, privilegeLevels.readAndWrite || false)
       })
-    })
-  })
 
-  describe('canUserAdminProject', function () {
-    beforeEach(function () {
-      return (this.AuthorizationManager.getPrivilegeLevelForProject = sinon.stub())
-    })
+      describe('when user has read-only access', function () {
+        setupUserPrivilegeLevel(PrivilegeLevels.READ_ONLY)
+        expectPermission(permission, privilegeLevels.readOnly || false)
+      })
 
-    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 as the public', function () {
+        setupPublicAccessLevel(PublicAccessLevels.READ_AND_WRITE)
+        expectPermission(
+          permission,
+          privilegeLevels.publicReadAndWrite || false
         )
       })
-    })
 
-    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 as the public', function () {
+        setupPublicAccessLevel(PublicAccessLevels.READ_ONLY)
+        expectPermission(permission, privilegeLevels.publicReadOnly || false)
       })
-    })
 
-    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 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 user has no access', function () {
+    describe('when anonymous', 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()
-          }
-        )
+        this.user = null
       })
-    })
-  })
 
-  describe('isUserSiteAdmin', function () {
-    beforeEach(function () {
-      return (this.User.findOne = sinon.stub())
-    })
+      describe('with read-write access through a token', function () {
+        setupTokenAccessLevel('readAndWrite')
+        expectPermission(permission, privilegeLevels.tokenReadAndWrite || false)
+      })
 
-    describe('when user is admin', function () {
-      beforeEach(function () {
-        return this.User.findOne
-          .withArgs({ _id: this.user_id }, { isAdmin: 1 })
-          .yields(null, { isAdmin: true })
-      })
-
-      it('should return true', function (done) {
-        return this.AuthorizationManager.isUserSiteAdmin(
-          this.user_id,
-          (error, isAdmin) => {
-            expect(isAdmin).to.equal(true)
-            return done()
-          }
-        )
+      describe('with read-only access through a token', function () {
+        setupTokenAccessLevel('readOnly')
+        expectPermission(permission, privilegeLevels.tokenReadOnly || false)
       })
-    })
 
-    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()
-          }
+      describe('with public read-write access', function () {
+        setupPublicAccessLevel(PublicAccessLevels.READ_AND_WRITE)
+        expectPermission(
+          permission,
+          privilegeLevels.publicReadAndWrite || 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()
-          }
-        )
+      describe('with public read-only access', function () {
+        setupPublicAccessLevel(PublicAccessLevels.READ_ONLY)
+        expectPermission(permission, privilegeLevels.publicReadOnly || 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()
-          }
-        )
+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)
+  })
+}

+ 281 - 464
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 = {}
-    })
-
-    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()
-      })
-    })
-
-    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()
-      })
-    })
+  describe('ensureCanReadProject', function () {
+    testMiddleware('ensureUserCanReadProject', 'canUserReadProject')
   })
 
-  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('ensureUserCanWriteProjectContent', function () {
+    testMiddleware(
+      'ensureUserCanWriteProjectContent',
+      'canUserWriteProjectContent'
+    )
+  })
 
-    describe('with missing project_id', function () {
+  describe('ensureUserCanWriteProjectSettings', function () {
+    describe('when renaming a project', function () {
       beforeEach(function () {
-        this.req.params = {}
+        this.req.body.name = 'new project name'
       })
 
-      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)
-      })
+      testMiddleware(
+        'ensureUserCanWriteProjectSettings',
+        'canUserRenameProject'
+      )
     })
 
-    describe('with logged in user', function () {
+    describe('when setting another parameter', function () {
       beforeEach(function () {
-        this.SessionManager.getLoggedInUserId.returns(this.userId)
+        this.req.body.compiler = 'texlive-2017'
       })
 
-      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
-          )
-        })
-      })
+      testMiddleware(
+        'ensureUserCanWriteProjectSettings',
+        'canUserWriteProjectSettings'
+      )
     })
+  })
 
-    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()
-          }
-        )
-      })
-    })
+  describe('ensureUserCanAdminProject', function () {
+    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)
-        })
-
-        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)
-        })
+        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)
-        })
+    describe('with oauth user', function () {
+      setupOAuthUser()
 
-        it('should return next', function () {
-          this.AuthorizationMiddleware.ensureUserIsSiteAdmin(
-            this.req,
-            this.res,
-            this.next
-          )
-          this.next.called.should.equal(true)
-        })
+      describe('when user has permission', function () {
+        setupSiteAdmin(true)
+        invokeMiddleware('ensureUserIsSiteAdmin')
+        expectNext()
       })
 
       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)
-        })
+        setupSiteAdmin(false)
+        invokeMiddleware('ensureUserIsSiteAdmin')
+        expectRedirectToRestricted()
       })
     })
-  })
 
-  describe('blockRestrictedUserFromProject', function () {
-    beforeEach(function () {
-      this.AuthorizationMiddleware._getUserAndProjectId = sinon
-        .stub()
-        .callsArgWith(1, null, this.userId, this.project_id)
+    describe('with anonymous user', function () {
+      setupAnonymousUser()
+      invokeMiddleware('ensureUserIsSiteAdmin')
+      expectRedirectToRestricted()
     })
+  })
 
-    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
-      )
+  describe('blockRestrictedUserFromProject', function () {
+    describe('for a restricted user', function () {
+      setupPermission('isRestrictedUserForProject', true)
+      invokeMiddleware('blockRestrictedUserFromProject')
+      expectForbidden()
     })
 
-    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
+  })
+}