Files
overleaf-cep/services/web/app/src/Features/Authorization/AuthorizationManager.js
T
Mathias Jakobsen b5e2604041 [web] Upgrade restricted user access if they are invited members (#9401)
* [web] Upgrade restricted user access if they are invited members

Previously, if a user joined a project via a read-only link and later on
joined the project via an invite, we would still treat them as
restricted users, disabling chat and commenting. This patch changes
that, so that we do *not* consider an invited user restricted.

GitOrigin-RevId: e2acdfd29cc0687cb7276310a9c96d697087b21a
2022-09-28 08:06:44 +00:00

262 lines
7.4 KiB
JavaScript

const { callbackify } = require('util')
const { ObjectId } = require('mongodb')
const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
const ProjectGetter = require('../Project/ProjectGetter')
const { User } = require('../../models/User')
const PrivilegeLevels = require('./PrivilegeLevels')
const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
const PublicAccessLevels = require('./PublicAccessLevels')
const Errors = require('../Errors/Errors')
const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
const Settings = require('@overleaf/settings')
function isRestrictedUser(
userId,
privilegeLevel,
isTokenMember,
isInvitedMember
) {
if (privilegeLevel === PrivilegeLevels.NONE) {
return true
}
return (
privilegeLevel === PrivilegeLevels.READ_ONLY &&
(isTokenMember || !userId) &&
!isInvitedMember
)
}
async function isRestrictedUserForProject(userId, projectId, token) {
const privilegeLevel = await getPrivilegeLevelForProject(
userId,
projectId,
token
)
const isTokenMember = await CollaboratorsHandler.promises.userIsTokenMember(
userId,
projectId
)
const isInvitedMember =
await CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
userId,
projectId
)
return isRestrictedUser(
userId,
privilegeLevel,
isTokenMember,
isInvitedMember
)
}
async function getPublicAccessLevel(projectId) {
if (!ObjectId.isValid(projectId)) {
throw new Error('invalid project id')
}
// Note, the Project property in the DB is `publicAccesLevel`, without the second `s`
const project = await ProjectGetter.promises.getProject(projectId, {
publicAccesLevel: 1,
})
if (!project) {
throw new Errors.NotFoundError(`no project found with id ${projectId}`)
}
return project.publicAccesLevel
}
/**
* Get the privilege level that the user has for the project.
*
* @param userId - The id of the user that wants to access the project.
* @param projectId - The id of the project to be accessed.
* @param {Object} opts
* @param {boolean} opts.ignoreSiteAdmin - Do not consider whether the user is
* a site admin.
* @param {boolean} opts.ignorePublicAccess - Do not consider the project is
* publicly accessible.
*
* @returns {string|boolean} The privilege level. One of "owner",
* "readAndWrite", "readOnly" or false.
*/
async function getPrivilegeLevelForProject(
userId,
projectId,
token,
opts = {}
) {
if (userId) {
return getPrivilegeLevelForProjectWithUser(userId, projectId, opts)
} else {
return getPrivilegeLevelForProjectWithoutUser(projectId, token, opts)
}
}
// User is present, get their privilege level from database
async function getPrivilegeLevelForProjectWithUser(
userId,
projectId,
opts = {}
) {
const privilegeLevel =
await CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
userId,
projectId
)
if (privilegeLevel && privilegeLevel !== PrivilegeLevels.NONE) {
// The user has direct access
return privilegeLevel
}
if (!opts.ignoreSiteAdmin) {
if (await isUserSiteAdmin(userId)) {
return PrivilegeLevels.OWNER
}
}
if (!opts.ignorePublicAccess) {
// Legacy public-access system
// User is present (not anonymous), but does not have direct access
const publicAccessLevel = await getPublicAccessLevel(projectId)
if (publicAccessLevel === PublicAccessLevels.READ_ONLY) {
return PrivilegeLevels.READ_ONLY
}
if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) {
return PrivilegeLevels.READ_AND_WRITE
}
}
return PrivilegeLevels.NONE
}
// User is Anonymous, Try Token-based access
async function getPrivilegeLevelForProjectWithoutUser(
projectId,
token,
opts = {}
) {
const publicAccessLevel = await getPublicAccessLevel(projectId)
if (!opts.ignorePublicAccess) {
if (publicAccessLevel === PublicAccessLevels.READ_ONLY) {
// Legacy public read-only access for anonymous user
return PrivilegeLevels.READ_ONLY
}
if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) {
// Legacy public read-write access for anonymous user
return PrivilegeLevels.READ_AND_WRITE
}
}
if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) {
return getPrivilegeLevelForProjectWithToken(projectId, token)
}
// Deny anonymous user access
return PrivilegeLevels.NONE
}
async function getPrivilegeLevelForProjectWithToken(projectId, token) {
// Anonymous users can have read-only access to token-based projects,
// while read-write access must be logged in,
// unless the `enableAnonymousReadAndWriteSharing` setting is enabled
const { isValidReadAndWrite, isValidReadOnly } =
await TokenAccessHandler.promises.validateTokenForAnonymousAccess(
projectId,
token
)
if (isValidReadOnly) {
// Grant anonymous user read-only access
return PrivilegeLevels.READ_ONLY
}
if (isValidReadAndWrite) {
// Grant anonymous user read-and-write access
return PrivilegeLevels.READ_AND_WRITE
}
// Deny anonymous access
return PrivilegeLevels.NONE
}
async function canUserReadProject(userId, projectId, token) {
const privilegeLevel = await getPrivilegeLevelForProject(
userId,
projectId,
token
)
return [
PrivilegeLevels.OWNER,
PrivilegeLevels.READ_AND_WRITE,
PrivilegeLevels.READ_ONLY,
].includes(privilegeLevel)
}
async function canUserWriteProjectContent(userId, projectId, token) {
const privilegeLevel = await getPrivilegeLevelForProject(
userId,
projectId,
token
)
return [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
privilegeLevel
)
}
async function canUserWriteProjectSettings(userId, projectId, token) {
const privilegeLevel = await getPrivilegeLevelForProject(
userId,
projectId,
token,
{ ignorePublicAccess: true }
)
return [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
privilegeLevel
)
}
async function canUserRenameProject(userId, projectId, token) {
const privilegeLevel = await getPrivilegeLevelForProject(
userId,
projectId,
token
)
return privilegeLevel === PrivilegeLevels.OWNER
}
async function canUserAdminProject(userId, projectId, token) {
const privilegeLevel = await getPrivilegeLevelForProject(
userId,
projectId,
token
)
return privilegeLevel === PrivilegeLevels.OWNER
}
async function isUserSiteAdmin(userId) {
if (!userId) {
return false
}
if (!Settings.adminPrivilegeAvailable) return false
const user = await User.findOne({ _id: userId }, { isAdmin: 1 }).exec()
return hasAdminAccess(user)
}
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,
},
}