From da7d43837724ff8efd95b2865c1213e6f103c0fd Mon Sep 17 00:00:00 2001 From: FailSafe <190101117+failsafesecurity@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:45:33 -0700 Subject: [PATCH] fix: enforce quota for comment attachments (#15149) ## Summary This change includes comment attachments in workspace storage usage and checks workspace storage quota before accepting a new comment attachment upload. ## Impact Comment attachments already had a per-file size limit, but they were not counted in the same workspace storage usage path as other uploaded blobs. A user with comment permission could keep adding attachments without those bytes participating in workspace storage quota calculations. ## Fix - Count comment attachment bytes in workspace storage usage reconciliation. - Check the workspace quota before storing a new comment attachment. - Return the existing comment attachment quota error when the upload would exceed limits. ## Validation - `git diff --check` - Full test/lint suite was not run locally because dependencies are not installed in this checkout. ## Summary by CodeRabbit * **New Features** * Workspace attachment uploads now respect storage and file quota limits more accurately. * Workspace storage tracking now includes comment attachments, improving quota enforcement. * **Bug Fixes** * Attachment uploads now fail with a clear quota error when a workspace is out of space or blob capacity. * Storage usage calculations now better reflect actual workspace content, including non-deleted files. Signed-off-by: failsafesecurity <190101117+failsafesecurity@users.noreply.github.com> --- .../backend/server/src/core/comment/index.ts | 2 ++ .../server/src/core/comment/resolver.ts | 10 +++++- .../backend/server/src/core/quota/state.ts | 33 +++++++++++++------ 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/packages/backend/server/src/core/comment/index.ts b/packages/backend/server/src/core/comment/index.ts index 75957eb060..51bcd8ebc2 100644 --- a/packages/backend/server/src/core/comment/index.ts +++ b/packages/backend/server/src/core/comment/index.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { ServerConfigModule } from '../config'; import { PermissionModule } from '../permission'; +import { QuotaServiceModule } from '../quota'; import { StorageModule } from '../storage'; import { CommentRealtimeModule } from './realtime.module'; import { CommentResolver } from './resolver'; @@ -9,6 +10,7 @@ import { CommentResolver } from './resolver'; @Module({ imports: [ PermissionModule, + QuotaServiceModule, StorageModule, ServerConfigModule, CommentRealtimeModule, diff --git a/packages/backend/server/src/core/comment/resolver.ts b/packages/backend/server/src/core/comment/resolver.ts index 7f33c0a8cd..5dcf950b69 100644 --- a/packages/backend/server/src/core/comment/resolver.ts +++ b/packages/backend/server/src/core/comment/resolver.ts @@ -26,6 +26,7 @@ import { Comment, DocMode, Models, Reply } from '../../models'; import { CurrentUser } from '../auth/session'; import { ServerFeature, ServerService } from '../config'; import { DocAction, PermissionAccess } from '../permission'; +import { QuotaService } from '../quota'; import { RealtimePublisher } from '../realtime'; import { CommentAttachmentStorage } from '../storage'; import { UserType } from '../user'; @@ -56,6 +57,7 @@ export class CommentResolver { private readonly service: CommentService, private readonly ac: PermissionAccess, private readonly commentAttachmentStorage: CommentAttachmentStorage, + private readonly quota: QuotaService, private readonly queue: JobQueue, private readonly models: Models, private readonly server: ServerService, @@ -354,13 +356,19 @@ export class CommentResolver { 'Doc.Comments.Create' ); - // TODO(@fengmk2): should check total attachment quota in the future version const buffer = await readableToBuffer(attachment.createReadStream()); // max attachment size is 10MB if (buffer.length > 10 * 1024 * 1024) { throw new CommentAttachmentQuotaExceeded(); } + const checkExceeded = + await this.quota.getWorkspaceQuotaCalculator(workspaceId); + const result = checkExceeded(buffer.length); + if (result?.blobQuotaExceeded || result?.storageQuotaExceeded) { + throw new CommentAttachmentQuotaExceeded(); + } + const key = randomUUID(); await this.commentAttachmentStorage.put( workspaceId, diff --git a/packages/backend/server/src/core/quota/state.ts b/packages/backend/server/src/core/quota/state.ts index 71ff12284a..e02e550cd9 100644 --- a/packages/backend/server/src/core/quota/state.ts +++ b/packages/backend/server/src/core/quota/state.ts @@ -287,17 +287,30 @@ export class QuotaStateService { } private async getWorkspaceStorageUsage(workspaceId: string) { - const sum = await this.db.blob.aggregate({ - where: { - workspaceId, - deletedAt: null, - }, - _sum: { - size: true, - }, - }); + const [blobSum, commentAttachmentSum] = await Promise.all([ + this.db.blob.aggregate({ + where: { + workspaceId, + deletedAt: null, + }, + _sum: { + size: true, + }, + }), + this.db.commentAttachment.aggregate({ + where: { + workspaceId, + }, + _sum: { + size: true, + }, + }), + ]); - return BigInt(sum._sum.size ?? 0); + return ( + BigInt(blobSum._sum.size ?? 0) + + BigInt(commentAttachmentSum._sum.size ?? 0) + ); } private hasStandaloneWorkspaceQuota(plan: string) {