mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-20 19:42:04 +08:00
1e7774929c
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 -->
141 lines
3.6 KiB
TypeScript
141 lines
3.6 KiB
TypeScript
import { Args, Parent, ResolveField, Resolver } from '@nestjs/graphql';
|
|
|
|
import { CurrentUser } from '../../core/auth';
|
|
import { AccessController } from '../../core/permission';
|
|
import { UserType } from '../../core/user';
|
|
import { WorkspaceType } from '../../core/workspaces';
|
|
import { Models } from '../../models';
|
|
import { AggregateBucket } from './providers';
|
|
import { IndexerService, SearchNodeWithMeta } from './service';
|
|
import {
|
|
AggregateInput,
|
|
AggregateResultObjectType,
|
|
SearchInput,
|
|
SearchQueryOccur,
|
|
SearchQueryType,
|
|
SearchResultObjectType,
|
|
} from './types';
|
|
|
|
@Resolver(() => WorkspaceType)
|
|
export class IndexerResolver {
|
|
constructor(
|
|
private readonly indexer: IndexerService,
|
|
private readonly ac: AccessController,
|
|
private readonly models: Models
|
|
) {}
|
|
|
|
@ResolveField(() => SearchResultObjectType, {
|
|
description: 'Search a specific table',
|
|
})
|
|
async search(
|
|
@CurrentUser() me: UserType,
|
|
@Parent() workspace: WorkspaceType,
|
|
@Args('input') input: SearchInput
|
|
): Promise<SearchResultObjectType> {
|
|
// currentUser can read the workspace
|
|
await this.ac.user(me.id).workspace(workspace.id).assert('Workspace.Read');
|
|
this.#addWorkspaceFilter(workspace, input);
|
|
|
|
const result = await this.indexer.search(input);
|
|
const nodes = await this.#filterUserReadableDocs(
|
|
workspace,
|
|
me,
|
|
result.nodes
|
|
);
|
|
return {
|
|
nodes,
|
|
pagination: {
|
|
count: result.total,
|
|
hasMore: nodes.length > 0,
|
|
nextCursor: result.nextCursor,
|
|
},
|
|
};
|
|
}
|
|
|
|
@ResolveField(() => AggregateResultObjectType, {
|
|
description: 'Search a specific table with aggregate',
|
|
})
|
|
async aggregate(
|
|
@CurrentUser() me: UserType,
|
|
@Parent() workspace: WorkspaceType,
|
|
@Args('input') input: AggregateInput
|
|
): Promise<AggregateResultObjectType> {
|
|
// currentUser can read the workspace
|
|
await this.ac.user(me.id).workspace(workspace.id).assert('Workspace.Read');
|
|
this.#addWorkspaceFilter(workspace, input);
|
|
|
|
const result = await this.indexer.aggregate(input);
|
|
const needs: AggregateBucket[] = [];
|
|
for (const bucket of result.buckets) {
|
|
bucket.hits.nodes = await this.#filterUserReadableDocs(
|
|
workspace,
|
|
me,
|
|
bucket.hits.nodes as SearchNodeWithMeta[]
|
|
);
|
|
if (bucket.hits.nodes.length > 0) {
|
|
needs.push(bucket);
|
|
}
|
|
}
|
|
return {
|
|
buckets: needs,
|
|
pagination: {
|
|
count: result.total,
|
|
hasMore: needs.length > 0,
|
|
nextCursor: result.nextCursor,
|
|
},
|
|
};
|
|
}
|
|
|
|
#addWorkspaceFilter(
|
|
workspace: WorkspaceType,
|
|
input: SearchInput | AggregateInput
|
|
) {
|
|
// filter by workspace id
|
|
input.query = {
|
|
type: SearchQueryType.boolean,
|
|
occur: SearchQueryOccur.must,
|
|
queries: [
|
|
{
|
|
type: SearchQueryType.match,
|
|
field: 'workspaceId',
|
|
match: workspace.id,
|
|
},
|
|
input.query,
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* filter user readable docs on team workspace
|
|
*/
|
|
async #filterUserReadableDocs(
|
|
workspace: WorkspaceType,
|
|
user: UserType,
|
|
nodes: SearchNodeWithMeta[]
|
|
) {
|
|
if (nodes.length === 0) {
|
|
return nodes;
|
|
}
|
|
|
|
const isTeamWorkspace = await this.models.workspaceFeature.has(
|
|
workspace.id,
|
|
'team_plan_v1'
|
|
);
|
|
if (!isTeamWorkspace) {
|
|
return nodes;
|
|
}
|
|
|
|
const needs = await this.ac
|
|
.user(user.id)
|
|
.workspace(workspace.id)
|
|
.docs(
|
|
nodes.map(node => ({
|
|
node,
|
|
docId: node._source.docId,
|
|
})),
|
|
'Doc.Read'
|
|
);
|
|
return needs.map(node => node.node);
|
|
}
|
|
}
|