Skip to content

Commit 006c7d3

Browse files
committed
feat: implement permission management for BaseNode with role-based access control
1 parent e91fe2b commit 006c7d3

File tree

13 files changed

+1021
-30
lines changed

13 files changed

+1021
-30
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { SetMetadata } from '@nestjs/common';
2+
import type { BaseNodeAction } from '@teable/core';
3+
4+
export const BASE_NODE_PERMISSIONS_KEY = 'baseNodePermissions';
5+
6+
// eslint-disable-next-line @typescript-eslint/naming-convention
7+
export const BaseNodePermissions = (...permissions: BaseNodeAction[]) =>
8+
SetMetadata(BASE_NODE_PERMISSIONS_KEY, permissions);
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import type { ExecutionContext } from '@nestjs/common';
2+
import { Injectable } from '@nestjs/common';
3+
import { Reflector } from '@nestjs/core';
4+
import { HttpErrorCode } from '@teable/core';
5+
import type { BaseNodeAction } from '@teable/core';
6+
import { PrismaService } from '@teable/db-main-prisma';
7+
import type { BaseNodeResourceType } from '@teable/openapi';
8+
import { ClsService } from 'nestjs-cls';
9+
import { CustomHttpException } from '../../../custom.exception';
10+
import type { IClsStore } from '../../../types/cls';
11+
import {
12+
checkBaseNodePermission,
13+
checkBaseNodePermissionCreate,
14+
} from '../../base-node/base-node.permission.helper';
15+
import { BASE_NODE_PERMISSIONS_KEY } from '../decorators/base-node-permissions.decorator';
16+
import { IS_DISABLED_PERMISSION } from '../decorators/disabled-permission.decorator';
17+
import { PermissionService } from '../permission.service';
18+
import { PermissionGuard } from './permission.guard';
19+
20+
@Injectable()
21+
export class BaseNodePermissionGuard extends PermissionGuard {
22+
constructor(
23+
private readonly reflectorInner: Reflector,
24+
private readonly clsInner: ClsService<IClsStore>,
25+
private readonly permissionServiceInner: PermissionService,
26+
private readonly prismaService: PrismaService
27+
) {
28+
super(reflectorInner, clsInner, permissionServiceInner);
29+
}
30+
31+
async canActivate(context: ExecutionContext) {
32+
const superResult = await super.canActivate(context);
33+
if (!superResult) {
34+
return false;
35+
}
36+
37+
// disabled check
38+
const isDisabledPermission = this.reflectorInner.getAllAndOverride<boolean>(
39+
IS_DISABLED_PERMISSION,
40+
[context.getHandler(), context.getClass()]
41+
);
42+
43+
if (isDisabledPermission) {
44+
return true;
45+
}
46+
47+
const baseId = this.getBaseId(context);
48+
if (!baseId) {
49+
throw new CustomHttpException('Base ID is required', HttpErrorCode.RESTRICTED_RESOURCE);
50+
}
51+
const permissionContext = await this.getPermissionContext();
52+
return this.checkActivate(context, baseId, permissionContext);
53+
}
54+
55+
async checkActivate(
56+
context: ExecutionContext,
57+
baseId: string,
58+
permissionContext: {
59+
permissionSet: Set<string>;
60+
tablePermissionMap?: Record<string, string[]>;
61+
}
62+
) {
63+
const baseNodePermissions = this.reflectorInner.getAllAndOverride<BaseNodeAction[] | undefined>(
64+
BASE_NODE_PERMISSIONS_KEY,
65+
[context.getHandler(), context.getClass()]
66+
);
67+
68+
if (!baseNodePermissions?.length) {
69+
return true;
70+
}
71+
const nodeId = this.getNodeId(context);
72+
const node = await this.getNode(baseId, nodeId);
73+
const checkCreate = checkBaseNodePermissionCreate(
74+
node ?? { resourceType: this.getNodeResourceType(context), resourceId: '' },
75+
baseNodePermissions,
76+
permissionContext
77+
);
78+
79+
if (!checkCreate) {
80+
return false;
81+
}
82+
83+
const baseNodePermissionsWithoutCreate = baseNodePermissions.filter(
84+
(permission: BaseNodeAction) => permission !== 'base_node|create'
85+
);
86+
if (!baseNodePermissionsWithoutCreate.length) {
87+
return true;
88+
}
89+
90+
if (!nodeId) {
91+
throw new CustomHttpException('Node ID is required', HttpErrorCode.RESTRICTED_RESOURCE);
92+
}
93+
94+
if (!node) {
95+
throw new CustomHttpException('Node not found', HttpErrorCode.NOT_FOUND);
96+
}
97+
98+
return baseNodePermissionsWithoutCreate.every((permission: BaseNodeAction) =>
99+
checkBaseNodePermission(node, permission, permissionContext)
100+
);
101+
}
102+
103+
getBaseId(context: ExecutionContext): string | undefined {
104+
const request = context.switchToHttp().getRequest();
105+
const defaultBaseId = request.params ?? {};
106+
return super.getResourceId(context) || defaultBaseId.baseId;
107+
}
108+
109+
getNodeId(context: ExecutionContext): string | undefined {
110+
const req = context.switchToHttp().getRequest();
111+
return req.params.nodeId;
112+
}
113+
114+
getNodeResourceType(context: ExecutionContext): BaseNodeResourceType {
115+
const req = context.switchToHttp().getRequest();
116+
return req.body.resourceType;
117+
}
118+
119+
async getNode(baseId: string, nodeId?: string) {
120+
if (!nodeId) {
121+
return;
122+
}
123+
const node = await this.prismaService.baseNode.findFirst({
124+
where: { baseId, id: nodeId },
125+
select: {
126+
id: true,
127+
resourceType: true,
128+
resourceId: true,
129+
},
130+
});
131+
132+
if (node) {
133+
return {
134+
resourceType: node.resourceType as BaseNodeResourceType,
135+
resourceId: node.resourceId,
136+
};
137+
}
138+
}
139+
140+
private async getPermissionContext() {
141+
const permissions = this.clsInner.get('permissions');
142+
const permissionSet = new Set(permissions);
143+
return { permissionSet };
144+
}
145+
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Global, Module } from '@nestjs/common';
2+
import { BaseNodePermissionGuard } from './guard/base-node-permission.guard';
23
import { PermissionGuard } from './guard/permission.guard';
34
import { PermissionService } from './permission.service';
45

56
@Global()
67
@Module({
7-
providers: [PermissionService, PermissionGuard],
8-
exports: [PermissionService, PermissionGuard],
8+
providers: [PermissionService, PermissionGuard, BaseNodePermissionGuard],
9+
exports: [PermissionService, PermissionGuard, BaseNodePermissionGuard],
910
})
1011
export class PermissionModule {}
Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable sonarjs/no-duplicate-string */
2-
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
2+
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
33
import type { IBaseNodeTreeVo, IBaseNodeVo } from '@teable/openapi';
44
import {
55
moveBaseNodeRoSchema,
@@ -11,31 +11,67 @@ import {
1111
updateBaseNodeRoSchema,
1212
IUpdateBaseNodeRo,
1313
} from '@teable/openapi';
14+
import { ClsService } from 'nestjs-cls';
15+
import type { IClsStore } from '../../types/cls';
1416
import { ZodValidationPipe } from '../../zod.validation.pipe';
17+
import { BaseNodePermissions } from '../auth/decorators/base-node-permissions.decorator';
1518
import { Permissions } from '../auth/decorators/permissions.decorator';
19+
import { BaseNodePermissionGuard } from '../auth/guard/base-node-permission.guard';
20+
import { checkBaseNodePermission } from './base-node.permission.helper';
1621
import { BaseNodeService } from './base-node.service';
1722

1823
@Controller('api/base/:baseId/node')
24+
@UseGuards(BaseNodePermissionGuard)
1925
export class BaseNodeController {
20-
constructor(private readonly baseNodeService: BaseNodeService) {}
26+
constructor(
27+
private readonly baseNodeService: BaseNodeService,
28+
private readonly cls: ClsService<IClsStore>
29+
) {}
30+
31+
@Get('list')
32+
@Permissions('base|read')
33+
async getList(@Param('baseId') baseId: string): Promise<IBaseNodeVo[]> {
34+
const permissionContext = await this.getPermissionContext(baseId);
35+
const nodeList = await this.baseNodeService.getList(baseId);
36+
return nodeList.filter((node) =>
37+
checkBaseNodePermission(
38+
{ resourceType: node.resourceType, resourceId: node.resourceId },
39+
'base_node|read',
40+
permissionContext
41+
)
42+
);
43+
}
2144

2245
@Get('tree')
2346
@Permissions('base|read')
2447
async getTree(@Param('baseId') baseId: string): Promise<IBaseNodeTreeVo> {
25-
return this.baseNodeService.getTree(baseId);
48+
const permissionContext = await this.getPermissionContext(baseId);
49+
const tree = await this.baseNodeService.getTree(baseId);
50+
return {
51+
...tree,
52+
nodes: tree.nodes.filter((node) =>
53+
checkBaseNodePermission(
54+
{ resourceType: node.resourceType, resourceId: node.resourceId },
55+
'base_node|read',
56+
permissionContext
57+
)
58+
),
59+
};
2660
}
2761

2862
@Get(':nodeId')
2963
@Permissions('base|read')
30-
async get(
64+
@BaseNodePermissions('base_node|read')
65+
async getNode(
3166
@Param('baseId') baseId: string,
3267
@Param('nodeId') nodeId: string
3368
): Promise<IBaseNodeVo> {
34-
return this.baseNodeService.getNode(baseId, nodeId);
69+
return this.baseNodeService.getNodeVo(baseId, nodeId);
3570
}
3671

3772
@Post()
38-
@Permissions('base|update')
73+
@Permissions('base|read')
74+
@BaseNodePermissions('base_node|create')
3975
async create(
4076
@Param('baseId') baseId: string,
4177
@Body(new ZodValidationPipe(createBaseNodeRoSchema)) ro: ICreateBaseNodeRo
@@ -44,7 +80,8 @@ export class BaseNodeController {
4480
}
4581

4682
@Post(':nodeId/duplicate')
47-
@Permissions('base|update')
83+
@Permissions('base|read')
84+
@BaseNodePermissions('base_node|read', 'base_node|create')
4885
async duplicate(
4986
@Param('baseId') baseId: string,
5087
@Param('nodeId') nodeId: string,
@@ -53,14 +90,9 @@ export class BaseNodeController {
5390
return this.baseNodeService.duplicate(baseId, nodeId, ro);
5491
}
5592

56-
@Delete(':nodeId')
57-
@Permissions('base|update')
58-
async delete(@Param('baseId') baseId: string, @Param('nodeId') nodeId: string): Promise<void> {
59-
return this.baseNodeService.delete(baseId, nodeId);
60-
}
61-
6293
@Put(':nodeId')
63-
@Permissions('base|update')
94+
@Permissions('base|read')
95+
@BaseNodePermissions('base_node|update')
6496
async update(
6597
@Param('baseId') baseId: string,
6698
@Param('nodeId') nodeId: string,
@@ -71,11 +103,25 @@ export class BaseNodeController {
71103

72104
@Put(':nodeId/move')
73105
@Permissions('base|update')
106+
@BaseNodePermissions('base_node|update')
74107
async move(
75108
@Param('baseId') baseId: string,
76109
@Param('nodeId') nodeId: string,
77110
@Body(new ZodValidationPipe(moveBaseNodeRoSchema)) ro: IMoveBaseNodeRo
78111
): Promise<IBaseNodeVo> {
79112
return this.baseNodeService.move(baseId, nodeId, ro);
80113
}
114+
115+
@Delete(':nodeId')
116+
@Permissions('base|read')
117+
@BaseNodePermissions('base_node|delete')
118+
async delete(@Param('baseId') baseId: string, @Param('nodeId') nodeId: string): Promise<void> {
119+
return this.baseNodeService.delete(baseId, nodeId);
120+
}
121+
122+
protected async getPermissionContext(_baseId: string) {
123+
const permissions = this.cls.get('permissions');
124+
const permissionSet = new Set(permissions);
125+
return { permissionSet };
126+
}
81127
}

0 commit comments

Comments
 (0)