mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Template gallery: add template management authorization middleware
- Middleware to enforce template access rules - Introduce template manager role - Fix minor issues
This commit is contained in:
@@ -731,7 +731,7 @@ const _ProjectController = {
|
||||
}
|
||||
|
||||
const isAdminOrTemplateOwner =
|
||||
hasAdminAccess(user) || Settings.templates?.nonAdminCanManage
|
||||
hasAdminAccess(user) || Settings.templates?.nonAdminCanManage || Settings.templates?.user_id === userId
|
||||
const showTemplatesServerPro =
|
||||
Features.hasFeature('templates-server-pro') && isAdminOrTemplateOwner
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ export const db = {
|
||||
systemmessages: internalDb.collection('systemmessages'),
|
||||
tags: internalDb.collection('tags'),
|
||||
teamInvites: internalDb.collection('teamInvites'),
|
||||
templates: internalDb.collection('templates'),
|
||||
tokens: internalDb.collection('tokens'),
|
||||
userAuditLogEntries: internalDb.collection('userAuditLogEntries'),
|
||||
users: internalDb.collection('users'),
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import Settings from '@overleaf/settings'
|
||||
import AdminAuthorizationHelper from '../../../../app/src/Features/Helpers/AdminAuthorizationHelper.mjs'
|
||||
const { hasAdminAccess } = AdminAuthorizationHelper
|
||||
import HttpErrorHandler from '../../../../app/src/Features/Errors/HttpErrorHandler.mjs'
|
||||
import SessionManager from '../../../../app/src/Features/Authentication/SessionManager.mjs'
|
||||
import { Template } from './models/Template.mjs'
|
||||
|
||||
async function ensureTemplateManagementAccess(req, res, next) {
|
||||
const user = SessionManager.getSessionUser(req.session)
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
|
||||
const isPrivileged =
|
||||
hasAdminAccess(user) ||
|
||||
Settings.templates?.user_id === userId
|
||||
|
||||
if (isPrivileged) return next()
|
||||
|
||||
const templateId = req.params?.template_id
|
||||
|
||||
if (!templateId) {
|
||||
if (Settings.templates?.nonAdminCanManage) return next()
|
||||
return HttpErrorHandler.forbidden(req, res)
|
||||
}
|
||||
|
||||
// unprivileged owner is allowed to edit/delete own template
|
||||
// even non-admin is not allowed to manage templates
|
||||
const template = await Template.findById(templateId)
|
||||
.select('owner')
|
||||
.lean()
|
||||
|
||||
if (template?.owner?.toString() === userId) return next()
|
||||
|
||||
return HttpErrorHandler.forbidden(req, res)
|
||||
}
|
||||
|
||||
export default {
|
||||
ensureTemplateManagementAccess,
|
||||
}
|
||||
@@ -123,12 +123,14 @@ async function templatesCategoryPage(req, res, next) {
|
||||
|
||||
async function templateDetailsPage(req, res, next) {
|
||||
const t = req.i18n.translate
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
try {
|
||||
const template = await TemplateGalleryManager.getTemplate('_id', req.params.template_id)
|
||||
res.render(Path.resolve(__dirname, '../views/template_gallery/template'), {
|
||||
title: `${t('template')}: ${template.name}`,
|
||||
template: JSON.stringify(template),
|
||||
languages: Settings.languages,
|
||||
userIsTemplatesManager: Boolean(Settings.templates?.user_id && Settings.templates.user_id === userId)
|
||||
})
|
||||
} catch (error) {
|
||||
return ErrorController.notFound(req, res, next)
|
||||
|
||||
@@ -71,13 +71,13 @@ export async function canUserOverrideTemplate(template, userId) {
|
||||
userIsOwner = false
|
||||
try {
|
||||
userIsAdmin = (await UserGetter.promises.getUser(userId, { isAdmin: 1 })).isAdmin
|
||||
} catch {
|
||||
} catch (error) {
|
||||
logger.error({ error, userId }, 'Logged in user does not exist, strange...')
|
||||
userIsAdmin = false
|
||||
}
|
||||
templateOwnerName = await getUserName(templateOwnerId) || 'unknown'
|
||||
}
|
||||
const canOverride = userIsOwner || userIsAdmin
|
||||
const canOverride = userIsOwner || userIsAdmin || (settings.templates?.user_id === userId)
|
||||
return { canOverride, templateOwnerName }
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ export async function deleteTemplateAssets(templateId, version, deleteFromDb) {
|
||||
try {
|
||||
await Template.deleteOne({ _id: templateId }).exec()
|
||||
} catch (error) {
|
||||
logger.error({ err, templateId }, 'Failed to delete template from MongoDB')
|
||||
logger.error({ error, templateId }, 'Failed to delete template from MongoDB')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import AuthenticationController from '../../../../app/src/Features/Authenticatio
|
||||
import RateLimiterMiddleware from '../../../../app/src/Features/Security/RateLimiterMiddleware.mjs'
|
||||
import { RateLimiter } from '../../../../app/src/infrastructure/RateLimiter.mjs'
|
||||
import TemplateGalleryController from './TemplateGalleryController.mjs'
|
||||
import TemplateAuthorizationMiddleware from './TemplateAuthorizationMiddleware.mjs'
|
||||
|
||||
const rateLimiterNewTemplate = new RateLimiter('create-template-from-project', {
|
||||
points: 20,
|
||||
@@ -18,7 +19,6 @@ const rateLimiterThumbnails = new RateLimiter('template-gallery-thumbnails', {
|
||||
duration: 60,
|
||||
})
|
||||
|
||||
|
||||
export default {
|
||||
rateLimiter,
|
||||
apply(webRouter) {
|
||||
@@ -28,6 +28,7 @@ export default {
|
||||
'/template/new/:Project_id',
|
||||
AuthenticationController.requireLogin(),
|
||||
RateLimiterMiddleware.rateLimit(rateLimiterNewTemplate),
|
||||
TemplateAuthorizationMiddleware.ensureTemplateManagementAccess,
|
||||
TemplateGalleryController.createTemplateFromProject
|
||||
)
|
||||
|
||||
@@ -41,6 +42,7 @@ export default {
|
||||
'/template/:template_id/edit',
|
||||
AuthenticationController.requireLogin(),
|
||||
RateLimiterMiddleware.rateLimit(rateLimiter),
|
||||
TemplateAuthorizationMiddleware.ensureTemplateManagementAccess,
|
||||
TemplateGalleryController.editTemplate
|
||||
)
|
||||
|
||||
@@ -48,6 +50,7 @@ export default {
|
||||
'/template/:template_id/delete',
|
||||
AuthenticationController.requireLogin(),
|
||||
RateLimiterMiddleware.rateLimit(rateLimiter),
|
||||
TemplateAuthorizationMiddleware.ensureTemplateManagementAccess,
|
||||
TemplateGalleryController.deleteTemplate
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ block append meta
|
||||
meta(name="ol-template" data-type="json" content=template)
|
||||
meta(name="ol-languages" data-type="json" content=languages)
|
||||
meta(name="ol-userIsAdmin" data-type="boolean" content=hasAdminAccess())
|
||||
meta(name="ol-userIsTemplatesManager" data-type="boolean" content=userIsTemplatesManager)
|
||||
|
||||
block content
|
||||
#template-root
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function Pagination({ currentPage, totalPages, onPageChange }) {
|
||||
const { t } = useTranslation()
|
||||
if (totalPages <= 1) return null
|
||||
|
||||
const pageNumbers = []
|
||||
let startPage = Math.max(1, currentPage - 4)
|
||||
let endPage = Math.min(totalPages, currentPage + 4)
|
||||
|
||||
if (startPage > 1) {
|
||||
pageNumbers.push(1)
|
||||
if (startPage > 2) {
|
||||
pageNumbers.push("...")
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pageNumbers.push(i)
|
||||
}
|
||||
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
pageNumbers.push("...")
|
||||
}
|
||||
pageNumbers.push(totalPages)
|
||||
}
|
||||
|
||||
return (
|
||||
<nav role="navigation" aria-label={t('pagination_navigation')}>
|
||||
<ul className="pagination">
|
||||
{/*
|
||||
{currentPage > 1 && (
|
||||
<li>
|
||||
<button aria-label={t('go_to_first_page')} onClick={() => onPageChange(1)}>
|
||||
<< {t('first')}
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
*/}
|
||||
{currentPage > 1 && (
|
||||
<li>
|
||||
<button aria-label={t('go_prev_page')} onClick={() => onPageChange(currentPage - 1)}>
|
||||
< {t('prev')}
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
{pageNumbers.map((page, index) => (
|
||||
<li key={index} className={page === currentPage ? "active" : ""}>
|
||||
{page === "..." ? (
|
||||
<span aria-hidden="true">{page}</span>
|
||||
) : page === currentPage ? (
|
||||
<span aria-label={t('page_current', { page })} aria-current="true">{page}</span>
|
||||
) : (
|
||||
<button aria-label={t('go_page', { page })} onClick={() => onPageChange(page)}>
|
||||
{page}
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
{currentPage < totalPages && (
|
||||
<li>
|
||||
<button aria-label={t('go_next_page')} onClick={() => onPageChange(currentPage + 1)}>
|
||||
{t('next')} >
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
{/*
|
||||
{currentPage < totalPages && (
|
||||
<li>
|
||||
<button aria-label={t('go_to_last_page')} onClick={() => onPageChange(totalPages)}>
|
||||
{t('last')} >>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
*/}
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import OLRow from '@/shared/components/ol/ol-row'
|
||||
import { useTemplateGalleryContext } from '../context/template-gallery-context'
|
||||
import TemplateGalleryEntry from './template-gallery-entry'
|
||||
import Pagination from './pagination'
|
||||
import Pagination from '@/shared/components/pagination-cep'
|
||||
|
||||
export default function TemplateGallery() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -16,7 +16,8 @@ function TemplateDetails() {
|
||||
const lastUpdatedDate = fromNowDate(template.lastUpdated)
|
||||
const tooltipText = formatDate(template.lastUpdated)
|
||||
const loggedInUserId = getMeta('ol-user_id')
|
||||
const loggedInUserIsAdmin = getMeta('ol-userIsAdmin')
|
||||
const loggedInUserCanManageTemplates = getMeta('ol-userIsAdmin')
|
||||
|| getMeta('ol-userIsTemplatesManager')
|
||||
|
||||
const openAsTemplateParams = new URLSearchParams({
|
||||
version: template.version,
|
||||
@@ -90,7 +91,7 @@ function TemplateDetails() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{loggedInUserId && (loggedInUserId === template.owner || loggedInUserIsAdmin) && (
|
||||
{loggedInUserId && (loggedInUserId === template.owner || loggedInUserCanManageTemplates) && (
|
||||
<OLRow className="cta-links-container">
|
||||
<OLCol md={12} className="text-end">
|
||||
<EditTemplateButton />
|
||||
|
||||
@@ -11,17 +11,16 @@ export function deleteTemplate(template: Template) {
|
||||
|
||||
type UpdateTemplateOptions = {
|
||||
template: Template
|
||||
initialTemplate: Template
|
||||
descriptionEdited: boolean
|
||||
editedTemplate: Template
|
||||
}
|
||||
|
||||
export function updateTemplate({
|
||||
editedTemplate,
|
||||
template
|
||||
template,
|
||||
editedTemplate
|
||||
}: UpdateTemplateOptions): Promise<Template | null> {
|
||||
const updatedFields: Partial<Template> = {
|
||||
name: editedTemplate.name.trim(),
|
||||
license: editedTemplate.license.trim(),
|
||||
license: editedTemplate.license,
|
||||
category: editedTemplate.category,
|
||||
language: editedTemplate.language,
|
||||
authorMD: editedTemplate.authorMD.trim(),
|
||||
@@ -36,12 +35,10 @@ export function updateTemplate({
|
||||
}, {} as Partial<Template>)
|
||||
|
||||
if (Object.keys(changedFields).length === 0) {
|
||||
return null
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
const updated = postJSON(`/template/${editedTemplate.id}/edit`, {
|
||||
return postJSON(`/template/${editedTemplate.id}/edit`, {
|
||||
body: changedFields
|
||||
})
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ if (process.env.OVERLEAF_TEMPLATE_GALLERY === 'true') {
|
||||
}
|
||||
|
||||
Settings.templates = {
|
||||
nonAdminCanManage: boolFromEnv(process.env.OVERLEAF_NON_ADMIN_CAN_PUBLISH_TEMPLATES)
|
||||
nonAdminCanManage: boolFromEnv(process.env.OVERLEAF_NON_ADMIN_CAN_PUBLISH_TEMPLATES),
|
||||
user_id: process.env.OVERLEAF_TEMPLATES_USER_ID
|
||||
}
|
||||
|
||||
const templateKeys = process.env.OVERLEAF_TEMPLATE_CATEGORIES
|
||||
|
||||
Reference in New Issue
Block a user