| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- const { callbackify } = require('util')
- const { ObjectId } = require('mongodb-legacy')
- const Features = require('../../infrastructure/Features')
- 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,
- getAdminCapabilities,
- } = require('../Helpers/AdminAuthorizationHelper')
- const Settings = require('@overleaf/settings')
- const ChatApiHandler = require('../Chat/ChatApiHandler')
- 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 {string} token
- * @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 await getPrivilegeLevelForProjectWithUser(
- userId,
- projectId,
- null,
- opts
- )
- } else {
- return await getPrivilegeLevelForProjectWithoutUser(projectId, token, opts)
- }
- }
- /**
- * 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 {string} token
- * @param {ProjectAccess} projectAccess
- * @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 getPrivilegeLevelForProjectWithProjectAccess(
- userId,
- projectId,
- token,
- projectAccess,
- opts = {}
- ) {
- if (userId) {
- return await getPrivilegeLevelForProjectWithUser(
- userId,
- projectId,
- projectAccess,
- opts
- )
- } else {
- return await _getPrivilegeLevelForProjectWithoutUserWithPublicAccessLevel(
- projectId,
- token,
- projectAccess.publicAccessLevel(),
- opts
- )
- }
- }
- // User is present, get their privilege level from database
- async function getPrivilegeLevelForProjectWithUser(
- userId,
- projectId,
- projectAccess,
- opts = {}
- ) {
- let adminReadOnly = false
- if (!opts.ignoreSiteAdmin && (await isUserSiteAdmin(userId))) {
- if (!Settings.adminRolesEnabled) {
- return PrivilegeLevels.OWNER
- }
- const { adminCapabilities } = await getAdminCapabilities({ _id: userId })
- if (adminCapabilities.includes('modify-project-content')) {
- return PrivilegeLevels.OWNER
- }
- if (adminCapabilities.includes('view-project-content')) {
- adminReadOnly = true
- }
- }
- projectAccess =
- projectAccess ||
- (await CollaboratorsGetter.promises.getProjectAccess(projectId))
- const privilegeLevel = projectAccess.privilegeLevelForUser(userId)
- 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 = projectAccess.publicAccessLevel()
- if (publicAccessLevel === PublicAccessLevels.READ_ONLY) {
- return PrivilegeLevels.READ_ONLY
- }
- if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) {
- return PrivilegeLevels.READ_AND_WRITE
- }
- }
- if (adminReadOnly) {
- return PrivilegeLevels.READ_ONLY
- }
- return PrivilegeLevels.NONE
- }
- // User is Anonymous, Try Token-based access
- async function getPrivilegeLevelForProjectWithoutUser(
- projectId,
- token,
- opts = {}
- ) {
- return await _getPrivilegeLevelForProjectWithoutUserWithPublicAccessLevel(
- projectId,
- token,
- await getPublicAccessLevel(projectId),
- opts
- )
- }
- // User is Anonymous, Try Token-based access
- async function _getPrivilegeLevelForProjectWithoutUserWithPublicAccessLevel(
- projectId,
- token,
- publicAccessLevel,
- opts = {}
- ) {
- if (!Features.hasFeature('link-sharing')) {
- // Link sharing disabled globally.
- return PrivilegeLevels.NONE
- }
- 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 await 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,
- { ignoreSiteAdmin: true }
- )
- return (
- [
- PrivilegeLevels.OWNER,
- PrivilegeLevels.READ_AND_WRITE,
- PrivilegeLevels.READ_ONLY,
- PrivilegeLevels.REVIEW,
- ].includes(privilegeLevel) ||
- (await hasAdminProjectCapability(userId, 'view-project-content'))
- )
- }
- async function canUserWriteProjectContent(userId, projectId, token) {
- const privilegeLevel = await getPrivilegeLevelForProject(
- userId,
- projectId,
- token,
- { ignoreSiteAdmin: true }
- )
- return (
- [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
- privilegeLevel
- ) || (await hasAdminProjectCapability(userId, 'modify-project-content'))
- )
- }
- async function canUserWriteOrReviewProjectContent(userId, projectId, token) {
- const privilegeLevel = await getPrivilegeLevelForProject(
- userId,
- projectId,
- token,
- { ignoreSiteAdmin: true }
- )
- return (
- privilegeLevel === PrivilegeLevels.OWNER ||
- privilegeLevel === PrivilegeLevels.READ_AND_WRITE ||
- privilegeLevel === PrivilegeLevels.REVIEW ||
- (await hasAdminProjectCapability(userId, 'modify-project-content'))
- )
- }
- async function canUserWriteProjectSettings(userId, projectId, token) {
- const privilegeLevel = await getPrivilegeLevelForProject(
- userId,
- projectId,
- token,
- { ignorePublicAccess: true, ignoreSiteAdmin: true }
- )
- return (
- [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
- privilegeLevel
- ) || (await hasAdminProjectCapability(userId, 'modify-project-setting'))
- )
- }
- async function canUserRenameProject(userId, projectId, token) {
- const privilegeLevel = await getPrivilegeLevelForProject(
- userId,
- projectId,
- token,
- { ignoreSiteAdmin: true }
- )
- return (
- privilegeLevel === PrivilegeLevels.OWNER ||
- (await hasAdminProjectCapability(userId, 'modify-project-setting'))
- )
- }
- async function canUserAdminProject(userId, projectId, token) {
- const privilegeLevel = await getPrivilegeLevelForProject(
- userId,
- projectId,
- token,
- { ignoreSiteAdmin: true }
- )
- return (
- privilegeLevel === PrivilegeLevels.OWNER ||
- (await hasAdminProjectCapability(userId, 'modify-project-setting'))
- )
- }
- 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)
- }
- /**
- * @param {string} userId
- * @param {'view-project-setting'|'view-project-content'|'modify-project-setting'|'modify-project-content'} adminCapability
- */
- async function hasAdminProjectCapability(userId, adminCapability) {
- if (!Settings.adminPrivilegeAvailable || !(await isUserSiteAdmin(userId))) {
- return false
- }
- if (!Settings.adminRolesEnabled) {
- return true
- }
- const { adminCapabilities } = await getAdminCapabilities({ _id: userId })
- return adminCapabilities.includes(adminCapability)
- }
- async function canUserDeleteOrResolveThread(
- userId,
- projectId,
- threadId,
- token
- ) {
- const privilegeLevel = await getPrivilegeLevelForProject(
- userId,
- projectId,
- token,
- { ignorePublicAccess: true }
- )
- if (
- privilegeLevel === PrivilegeLevels.OWNER ||
- privilegeLevel === PrivilegeLevels.READ_AND_WRITE
- ) {
- return true
- }
- if (privilegeLevel !== PrivilegeLevels.REVIEW) {
- return false
- }
- try {
- const thread = await ChatApiHandler.promises.getThread(projectId, threadId)
- // Check if the user created the thread (first message)
- return thread.messages.length > 0 && thread.messages[0].user_id === userId
- } catch (error) {
- // If thread doesn't exist or other error, deny access
- return false
- }
- }
- module.exports = {
- canUserReadProject: callbackify(canUserReadProject),
- canUserWriteProjectContent: callbackify(canUserWriteProjectContent),
- canUserWriteOrReviewProjectContent: callbackify(
- canUserWriteOrReviewProjectContent
- ),
- canUserDeleteOrResolveThread: callbackify(canUserDeleteOrResolveThread),
- canUserWriteProjectSettings: callbackify(canUserWriteProjectSettings),
- canUserRenameProject: callbackify(canUserRenameProject),
- canUserAdminProject: callbackify(canUserAdminProject),
- getPrivilegeLevelForProject: callbackify(getPrivilegeLevelForProject),
- isRestrictedUser,
- isRestrictedUserForProject: callbackify(isRestrictedUserForProject),
- isUserSiteAdmin: callbackify(isUserSiteAdmin),
- promises: {
- canUserReadProject,
- canUserWriteProjectContent,
- canUserWriteOrReviewProjectContent,
- canUserDeleteOrResolveThread,
- canUserWriteProjectSettings,
- canUserRenameProject,
- canUserAdminProject,
- getPrivilegeLevelForProject,
- getPrivilegeLevelForProjectWithProjectAccess,
- isRestrictedUserForProject,
- isUserSiteAdmin,
- },
- }
|