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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## 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.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
akshitha-07
2026-07-03 15:38:24 +05:30
committed by GitHub
parent 5b7f83a6e3
commit 1f0bcd01a3
2 changed files with 50 additions and 0 deletions
@@ -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,
})
);
});
@@ -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<DocHistoryType[]> {
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,