Files
overleaf-cep/services/web/app/src/Features/Authorization/AuthorizationManager.js
T
Jakob Ackermann 591f631852 Merge pull request #13083 from overleaf/ds-jpa-project-ownership-change
[web] let invited admins transfer ownership of projects

GitOrigin-RevId: 49da7d42bec089f2278bde8942a63e5538fb5401
2023-05-16 08:05:22 +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 = {}
) {
if (!opts.ignoreSiteAdmin) {
if (await isUserSiteAdmin(userId)) {
return PrivilegeLevels.OWNER
}
}
const privilegeLevel =
await CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
userId,
projectId
)
if (privilegeLevel && privilegeLevel !== PrivilegeLevels.NONE) {
// The user has direct access
return privilegeLevel
}
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,
},
}