feat(server): filter docs by access role (#12311)

close CLOUD-208

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

- **New Features**
  - Enhanced document access control with batch permission checks, enabling efficient filtering of documents based on user roles and permissions.
  - Added detailed document-level role and permission management for workspace users.
- **Bug Fixes**
  - Improved accuracy in filtering search results to only display documents users have permission to read.
- **Tests**
  - Added comprehensive tests for document-level permission filtering and search result accuracy.
  - Introduced new mock utilities to support permission-related test scenarios.
- **Refactor**
  - Simplified and optimized permission logic for determining user roles and document access.
- **Documentation**
  - Updated type definitions for improved clarity in permission handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fengmk2
2025-05-19 03:28:22 +00:00
parent 85bb728ca8
commit 1e7774929c
16 changed files with 703 additions and 87 deletions
@@ -34,3 +34,107 @@ Generated by [AVA](https://avajs.dev).
highlights: null,
},
]
## should filter no read permission docs on team workspace
> Snapshot 1
[
{
fields: {
blockId: [
'block-0',
],
docId: [
'doc-0',
],
},
highlights: null,
},
{
fields: {
blockId: [
'block-2',
],
docId: [
'doc-2',
],
ref: [
'{"foo": "bar1"}',
'{"foo": "bar3"}',
],
refDocId: [
'doc-0',
'doc-2',
],
},
highlights: null,
},
{
fields: {
blockId: [
'block-1',
],
docId: [
'doc-1',
],
ref: [
'{"foo": "bar1"}',
],
refDocId: [
'doc-0',
],
},
highlights: null,
},
]
> Snapshot 2
[
{
fields: {
blockId: [
'block-0',
],
docId: [
'doc-0',
],
},
highlights: null,
},
{
fields: {
blockId: [
'block-1',
],
docId: [
'doc-1',
],
ref: [
'{"foo": "bar1"}',
],
refDocId: [
'doc-0',
],
},
highlights: null,
},
]
## should return empty results when search not match any docs
> Snapshot 1
{
workspace: {
search: {
nodes: [],
pagination: {
count: 0,
hasMore: false,
nextCursor: null,
},
},
},
}
@@ -5,6 +5,7 @@ import {
SearchTable,
} from '@affine/graphql';
import { DocRole } from '../../../models';
import { IndexerService } from '../../../plugins/indexer/service';
import { Mockers } from '../../mocks';
import { app, e2e } from '../test';
@@ -106,3 +107,172 @@ e2e('should search with query', async t => {
t.is(result.workspace.search.nodes.length, 2);
t.snapshot(result.workspace.search.nodes);
});
e2e('should filter no read permission docs on team workspace', async t => {
const owner = await app.signup();
const workspace = await app.create(Mockers.Workspace, {
owner,
});
await app.create(Mockers.TeamWorkspace, {
id: workspace.id,
});
const indexerService = app.get(IndexerService);
await indexerService.write(
SearchTable.block,
[
{
docId: 'doc-0',
workspaceId: workspace.id,
content: 'test1',
flavour: 'markdown',
blockId: 'block-0',
createdByUserId: owner.id,
updatedByUserId: owner.id,
createdAt: new Date('2025-04-22T00:00:00.000Z'),
updatedAt: new Date('2025-04-22T00:00:00.000Z'),
},
{
docId: 'doc-1',
workspaceId: workspace.id,
content: 'test2',
flavour: 'markdown',
blockId: 'block-1',
refDocId: ['doc-0'],
ref: ['{"foo": "bar1"}'],
createdByUserId: owner.id,
updatedByUserId: owner.id,
createdAt: new Date('2021-04-22T00:00:00.000Z'),
updatedAt: new Date('2021-04-22T00:00:00.000Z'),
},
{
docId: 'doc-2',
workspaceId: workspace.id,
content: 'test3',
flavour: 'markdown',
blockId: 'block-2',
refDocId: ['doc-0', 'doc-2'],
ref: ['{"foo": "bar1"}', '{"foo": "bar3"}'],
createdByUserId: owner.id,
updatedByUserId: owner.id,
createdAt: new Date('2025-03-22T00:00:00.000Z'),
updatedAt: new Date('2025-03-22T00:00:00.000Z'),
},
],
{
refresh: true,
}
);
// set all docs to no access
await app.create(Mockers.DocMeta, {
workspaceId: workspace.id,
docId: 'doc-0',
defaultRole: DocRole.None,
});
await app.create(Mockers.DocMeta, {
workspaceId: workspace.id,
docId: 'doc-1',
defaultRole: DocRole.None,
});
await app.create(Mockers.DocMeta, {
workspaceId: workspace.id,
docId: 'doc-2',
defaultRole: DocRole.None,
});
// owner can read all docs
const result = await app.gql({
query: indexerSearchQuery,
variables: {
id: workspace.id,
input: {
table: SearchTable.block,
query: {
type: SearchQueryType.match,
field: 'workspaceId',
match: workspace.id,
},
options: {
fields: ['docId', 'blockId', 'refDocId', 'ref'],
pagination: {
limit: 100,
},
},
},
},
});
t.snapshot(result.workspace.search.nodes);
// other user can only read docs that they have read permission
const other = await app.signup();
await app.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: other.id,
});
await app.create(Mockers.DocUser, {
workspaceId: workspace.id,
docId: 'doc-0',
userId: other.id,
type: DocRole.Reader,
});
await app.create(Mockers.DocUser, {
workspaceId: workspace.id,
docId: 'doc-1',
userId: other.id,
type: DocRole.Manager,
});
const otherResult = await app.gql({
query: indexerSearchQuery,
variables: {
id: workspace.id,
input: {
table: SearchTable.block,
query: {
type: SearchQueryType.match,
field: 'workspaceId',
match: workspace.id,
},
options: {
fields: ['docId', 'blockId', 'refDocId', 'ref'],
pagination: {
limit: 100,
},
},
},
},
});
t.snapshot(otherResult.workspace.search.nodes);
});
e2e('should return empty results when search not match any docs', async t => {
const owner = await app.signup();
const workspace = await app.create(Mockers.Workspace, {
owner,
});
const result = await app.gql({
query: indexerSearchQuery,
variables: {
id: workspace.id,
input: {
table: SearchTable.block,
query: {
type: SearchQueryType.match,
field: 'workspaceId',
match: workspace.id,
},
options: {
fields: ['docId', 'blockId', 'refDocId', 'ref'],
pagination: {
limit: 100,
},
},
},
},
});
t.snapshot(result);
});