mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
* [web] add infrastructure level zod change * [web] set req validation mode for tests to 'enforce' GitOrigin-RevId: 6dbc8bff7ed9f89045c398df93d84bca589de4e9
357 lines
10 KiB
JavaScript
357 lines
10 KiB
JavaScript
import AuthorizationManager from './AuthorizationManager.mjs'
|
|
import logger from '@overleaf/logger'
|
|
|
|
import HttpErrorHandler from '../Errors/HttpErrorHandler.mjs'
|
|
import AuthenticationController from '../Authentication/AuthenticationController.mjs'
|
|
import SessionManager from '../Authentication/SessionManager.mjs'
|
|
import TokenAccessHandler from '../TokenAccess/TokenAccessHandler.mjs'
|
|
import { expressify } from '@overleaf/promise-utils'
|
|
import AdminAuthorizationHelper from '../Helpers/AdminAuthorizationHelper.mjs'
|
|
import UrlHelper from '../Helpers/UrlHelper.mjs'
|
|
import ChatApiHandler from '../Chat/ChatApiHandler.mjs'
|
|
import {
|
|
getRawReqInput,
|
|
parseReq,
|
|
z,
|
|
zz,
|
|
} from '../../infrastructure/Validation.mjs'
|
|
|
|
// middleware schemas are non-strict: they validate only the fields this
|
|
// middleware consumes; the route schema stays responsible for strictness
|
|
const projectIdsQuerySchema = z.object({
|
|
query: z.object({ project_ids: z.string().optional() }),
|
|
})
|
|
|
|
const projectIdParamsSchema = z.object({
|
|
params: z.object({
|
|
project_id: zz.objectId().optional(),
|
|
Project_id: zz.objectId().optional(),
|
|
}),
|
|
})
|
|
|
|
const threadIdParamsSchema = z.object({
|
|
params: z.object({ thread_id: zz.objectId().optional() }),
|
|
})
|
|
|
|
const messageIdParamsSchema = z.object({
|
|
params: z.object({ message_id: zz.objectId().optional() }),
|
|
})
|
|
|
|
const settingsBodySchema = z.object({
|
|
body: z.object({ name: z.string().optional() }),
|
|
})
|
|
|
|
const restrictedQuerySchema = z.object({
|
|
query: z.object({ from: z.string().optional() }),
|
|
})
|
|
|
|
function _handleAdminDomainRedirect(req, res) {
|
|
if (
|
|
AdminAuthorizationHelper.canRedirectToAdminDomain(
|
|
SessionManager.getSessionUser(req.session)
|
|
)
|
|
) {
|
|
logger.warn({ req }, 'redirecting admin user to admin domain')
|
|
res.redirect(UrlHelper.getSafeAdminDomainRedirect(req.originalUrl))
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
async function ensureUserCanReadMultipleProjects(req, res, next) {
|
|
const { query } = parseReq(req, projectIdsQuerySchema, { logOnly: true })
|
|
const projectIds = (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()
|
|
}
|
|
|
|
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.debug({ userId, projectId }, 'allowing user read access to project')
|
|
return next()
|
|
}
|
|
logger.debug({ userId, projectId }, 'denying user read access to project')
|
|
HttpErrorHandler.forbidden(req, res)
|
|
}
|
|
|
|
async function ensureUserCanWriteProjectSettings(req, res, next) {
|
|
const projectId = _getProjectId(req)
|
|
const userId = _getUserId(req)
|
|
const token = TokenAccessHandler.getRequestToken(req, projectId)
|
|
const { body } = parseReq(req, settingsBodySchema, { logOnly: true })
|
|
|
|
if (body.name != null) {
|
|
const canRename = await AuthorizationManager.promises.canUserRenameProject(
|
|
userId,
|
|
projectId,
|
|
token
|
|
)
|
|
if (!canRename) {
|
|
return HttpErrorHandler.forbidden(req, res)
|
|
}
|
|
}
|
|
|
|
// raw access justified: inspects the key set (not the values) to decide
|
|
// which authorization check applies; the route schema validates the values
|
|
const otherParams = Object.keys(getRawReqInput(req).body).filter(
|
|
x => x !== 'name'
|
|
)
|
|
if (otherParams.length > 0) {
|
|
const canWrite =
|
|
await AuthorizationManager.promises.canUserWriteProjectSettings(
|
|
userId,
|
|
projectId,
|
|
token
|
|
)
|
|
if (!canWrite) {
|
|
return HttpErrorHandler.forbidden(req, res)
|
|
}
|
|
}
|
|
|
|
next()
|
|
}
|
|
|
|
async function ensureUserCanDeleteOrResolveThread(req, res, next) {
|
|
const projectId = _getProjectId(req)
|
|
const threadId = _getThreadId(req)
|
|
const userId = _getUserId(req)
|
|
const token = TokenAccessHandler.getRequestToken(req, projectId)
|
|
const canDeleteThread =
|
|
await AuthorizationManager.promises.canUserDeleteOrResolveThread(
|
|
userId,
|
|
projectId,
|
|
threadId,
|
|
token
|
|
)
|
|
if (canDeleteThread) {
|
|
logger.debug(
|
|
{ userId, projectId },
|
|
'allowing user to delete or resolve a comment thread'
|
|
)
|
|
return next()
|
|
}
|
|
|
|
logger.debug(
|
|
{ userId, projectId, threadId },
|
|
'denying user to delete or resolve a comment thread'
|
|
)
|
|
return HttpErrorHandler.forbidden(req, res)
|
|
}
|
|
|
|
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.debug(
|
|
{ userId, projectId },
|
|
'allowing user write access to project content'
|
|
)
|
|
return next()
|
|
}
|
|
logger.debug(
|
|
{ userId, projectId },
|
|
'denying user write access to project settings'
|
|
)
|
|
HttpErrorHandler.forbidden(req, res)
|
|
}
|
|
|
|
async function ensureUserCanWriteOrReviewProjectContent(req, res, next) {
|
|
const projectId = _getProjectId(req)
|
|
const userId = _getUserId(req)
|
|
const token = TokenAccessHandler.getRequestToken(req, projectId)
|
|
|
|
const canWriteOrReviewProjectContent =
|
|
await AuthorizationManager.promises.canUserWriteOrReviewProjectContent(
|
|
userId,
|
|
projectId,
|
|
token
|
|
)
|
|
if (canWriteOrReviewProjectContent) {
|
|
logger.debug(
|
|
{ userId, projectId },
|
|
'allowing user write or review access to project content'
|
|
)
|
|
return next()
|
|
}
|
|
|
|
logger.debug(
|
|
{ userId, projectId },
|
|
'denying user write or review access to project content'
|
|
)
|
|
return HttpErrorHandler.forbidden(req, res)
|
|
}
|
|
|
|
async function ensureUserCanAdminProject(req, res, next) {
|
|
const projectId = _getProjectId(req)
|
|
const userId = _getUserId(req)
|
|
const token = TokenAccessHandler.getRequestToken(req, projectId)
|
|
const canAdmin = await AuthorizationManager.promises.canUserAdminProject(
|
|
userId,
|
|
projectId,
|
|
token
|
|
)
|
|
if (canAdmin) {
|
|
logger.debug({ userId, projectId }, 'allowing user admin access to project')
|
|
return next()
|
|
}
|
|
logger.debug({ userId, projectId }, 'denying user admin access to project')
|
|
HttpErrorHandler.forbidden(req, res)
|
|
}
|
|
|
|
async function ensureUserIsMessageAuthor(req, res, next) {
|
|
const projectId = _getProjectId(req)
|
|
const messageId = _getMessageId(req)
|
|
const userId = _getUserId(req)
|
|
|
|
if (!userId) {
|
|
logger.debug({ projectId, messageId }, 'denying access: no logged in user')
|
|
return HttpErrorHandler.forbidden(req, res)
|
|
}
|
|
|
|
const message = await ChatApiHandler.promises.getGlobalMessage(
|
|
projectId,
|
|
messageId
|
|
)
|
|
if (message.user_id === userId) {
|
|
logger.debug(
|
|
{ userId, projectId, messageId },
|
|
'allowing user to modify their own message'
|
|
)
|
|
return next()
|
|
}
|
|
logger.debug(
|
|
{ userId, projectId, messageId, messageAuthor: message.user_id },
|
|
'denying user access to modify message: not the author'
|
|
)
|
|
return HttpErrorHandler.forbidden(req, res)
|
|
}
|
|
|
|
async function ensureUserIsSiteAdmin(req, res, next) {
|
|
const userId = _getUserId(req)
|
|
if (await AuthorizationManager.promises.isUserSiteAdmin(userId)) {
|
|
logger.debug({ userId }, 'allowing user admin access to site')
|
|
return next()
|
|
}
|
|
if (_handleAdminDomainRedirect(req, res)) return
|
|
logger.debug({ userId }, 'denying user admin access to site')
|
|
_redirectToRestricted(req, res, next)
|
|
}
|
|
|
|
function _getProjectId(req) {
|
|
const { params } = parseReq(req, projectIdParamsSchema)
|
|
const projectId = params.project_id || params.Project_id
|
|
if (!projectId) {
|
|
throw new Error('Expected project_id in request parameters')
|
|
}
|
|
return projectId
|
|
}
|
|
|
|
function _getThreadId(req) {
|
|
const threadId = parseReq(req, threadIdParamsSchema).params.thread_id
|
|
if (!threadId) {
|
|
throw new Error('Expected thread_id in request parameters')
|
|
}
|
|
return threadId
|
|
}
|
|
|
|
function _getMessageId(req) {
|
|
const messageId = parseReq(req, messageIdParamsSchema).params.message_id
|
|
if (!messageId) {
|
|
throw new Error('Expected message_id in request parameters')
|
|
}
|
|
return messageId
|
|
}
|
|
|
|
function _getUserId(req) {
|
|
return (
|
|
SessionManager.getLoggedInUserId(req.session) ||
|
|
(req.oauth_user && req.oauth_user._id?.toString()) ||
|
|
null
|
|
)
|
|
}
|
|
|
|
function _redirectToRestricted(req, res, next) {
|
|
// TODO: move this to throwing ForbiddenError
|
|
res.redirect(`/restricted?from=${encodeURIComponent(res.locals.currentUrl)}`)
|
|
}
|
|
|
|
function restricted(req, res, next) {
|
|
if (SessionManager.isUserLoggedIn(req.session)) {
|
|
return res.render('user/restricted', { title: 'restricted' })
|
|
}
|
|
const { from } = parseReq(req, restrictedQuerySchema, {
|
|
logOnly: true,
|
|
}).query
|
|
logger.debug({ from }, 'redirecting to login')
|
|
if (from) {
|
|
AuthenticationController.setRedirectInSession(req, from)
|
|
}
|
|
res.redirect('/login')
|
|
}
|
|
|
|
export default {
|
|
ensureUserCanReadMultipleProjects: expressify(
|
|
ensureUserCanReadMultipleProjects
|
|
),
|
|
blockRestrictedUserFromProject: expressify(blockRestrictedUserFromProject),
|
|
ensureUserCanReadProject: expressify(ensureUserCanReadProject),
|
|
ensureUserCanWriteProjectSettings: expressify(
|
|
ensureUserCanWriteProjectSettings
|
|
),
|
|
ensureUserCanDeleteOrResolveThread: expressify(
|
|
ensureUserCanDeleteOrResolveThread
|
|
),
|
|
ensureUserCanWriteProjectContent: expressify(
|
|
ensureUserCanWriteProjectContent
|
|
),
|
|
ensureUserCanWriteOrReviewProjectContent: expressify(
|
|
ensureUserCanWriteOrReviewProjectContent
|
|
),
|
|
ensureUserCanAdminProject: expressify(ensureUserCanAdminProject),
|
|
ensureUserIsSiteAdmin: expressify(ensureUserIsSiteAdmin),
|
|
ensureUserIsMessageAuthor: expressify(ensureUserIsMessageAuthor),
|
|
restricted,
|
|
}
|