From 1f0bcd01a37a522393fc1b288395e3a72a79ccad Mon Sep 17 00:00:00 2001 From: akshitha-07 <154409368+akshitha-07@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:38:24 +0530 Subject: [PATCH] fix: enforce Doc.Read permission on workspace histories field (#15192) The histories() resolver was returning document edit history (including editor names, emails, and timestamps) without checking Doc.Read permission first. This let any workspace member view history for private docs they weren't given access to, by passing an arbitrary document guid. Added the same permission check already used by WorkspaceDocResolver.doc() and recoverDoc() in this file. Fixes #15179 ## Summary by CodeRabbit * **Bug Fixes** * Added permission checks when viewing document history so only authorized users can access snapshot histories. * Prevented workspace collaborators without read access from querying histories for private documents. * **Tests** * Added an end-to-end test to verify history access is denied when the required permission is missing. --- .../src/__tests__/e2e/doc/resolver.spec.ts | 47 +++++++++++++++++++ .../src/core/workspaces/resolvers/history.ts | 3 ++ 2 files changed, 50 insertions(+) diff --git a/packages/backend/server/src/__tests__/e2e/doc/resolver.spec.ts b/packages/backend/server/src/__tests__/e2e/doc/resolver.spec.ts index 93ddb70685..192980f938 100644 --- a/packages/backend/server/src/__tests__/e2e/doc/resolver.spec.ts +++ b/packages/backend/server/src/__tests__/e2e/doc/resolver.spec.ts @@ -306,3 +306,50 @@ e2e('should require Doc.Read to query workspace page meta', async t => { }) ); }); + +e2e('should require Doc.Read to query doc histories', async t => { + const owner = await app.signup(); + const member = await app.createUser(); + await app.login(member); + + await app.switchUser(owner); + const workspace = await app.create(Mockers.Workspace, { + owner: { id: owner.id }, + }); + await app.create(Mockers.WorkspaceUser, { + workspaceId: workspace.id, + userId: member.id, + type: WorkspaceRole.Collaborator, + }); + + const docSnapshot = await app.create(Mockers.DocSnapshot, { + workspaceId: workspace.id, + user: owner, + }); + const doc = await app.create(Mockers.DocMeta, { + workspaceId: workspace.id, + docId: docSnapshot.id, + title: 'private-doc', + defaultRole: DocRole.None, + }); + + await app.switchUser(member); + await t.throwsAsync( + app.gql({ + query: { + id: 'workspaceDocHistoriesPermissionCheck', + op: 'workspaceDocHistoriesPermissionCheck', + query: ` + query { + workspace(id: "${workspace.id}") { + histories(guid: "${doc.docId}") { + timestamp + } + } + } + `, + } satisfies GraphQLQuery, + variables: undefined, + }) + ); +}); diff --git a/packages/backend/server/src/core/workspaces/resolvers/history.ts b/packages/backend/server/src/core/workspaces/resolvers/history.ts index b87dee5686..88b6962096 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/history.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/history.ts @@ -42,6 +42,7 @@ export class DocHistoryResolver { @ResolveField(() => [DocHistoryType]) async histories( + @CurrentUser() user: CurrentUser, @Parent() workspace: WorkspaceType, @Args('guid') guid: string, @Args({ name: 'before', type: () => GraphQLISODateTime, nullable: true }) @@ -51,6 +52,8 @@ export class DocHistoryResolver { ): Promise { const docId = new DocID(guid, workspace.id); + await this.ac.user(user.id).doc(docId).assert('Doc.Read'); + const histories = await this.workspace.listDocHistories( workspace.id, docId.guid,