Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions .agents/design/support/default-team-basic-permissions-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# 新建团队默认基础权限技术设计

## 1. 目标

新增环境变量:

```env
DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED=true
```

开启后,系统创建团队时为默认全员组授予以下权限:

- 创建应用;
- 创建 Skill;
- 创建知识库。

该能力作用于所有新建团队。环境变量默认值为 `false`,未配置时保持现有行为。

## 2. 总体方案

团队存在两条创建链路,因此主仓库和 Pro 都需要接入:

| 场景 | 创建入口 | 仓库 |
| --- | --- | --- |
| 应用初始化 `root` 团队 | `createDefaultTeam()` | FastGPT |
| 注册、手动创建等普通团队 | `createTeam()` | fastgpt-pro |

公共逻辑放在 FastGPT 主仓库的 `packages/service` 中:

1. 在 `serviceEnv` 定义环境变量;
2. 提供 `createTeamDefaultGroup()`,创建默认全员组并按配置初始化权限;
3. `createDefaultTeam()` 和 Pro `createTeam()` 统一调用该函数。

## 3. 权限设计

默认全员组名称为 `DEFAULT_GROUP`。鉴权时,系统会自动将该组加入团队成员的群组列表,因此只需为默认组创建一条团队资源权限记录,不需要逐个成员授权。

权限使用现有角色常量按位组合:

```ts
const defaultBasicRole =
TeamAppCreateRoleVal |
TeamSkillCreateRoleVal |
TeamDatasetCreateRoleVal;
```

创建的权限记录如下:

```ts
{
teamId,
resourceType: PerResourceTypeEnum.team,
groupId: defaultGroup._id,
permission: defaultBasicRole
}
```

该组合不包含团队管理和 API Key 创建权限。实现中应使用角色常量,不直接写权限数值。

## 4. 实现设计

### 4.1 环境变量

在主仓库 `packages/service/env.ts` 中增加:

```ts
DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED: BoolSchema.default(false).meta({
description: '新建团队时是否为默认全员组授予应用、Skill 和知识库创建权限'
})
```

FastGPT App 和 Pro 均使用 `packages/service`,因此共享同一份环境变量定义。部署时两个服务必须配置相同值。

### 4.2 默认全员组初始化

在主仓库新增独立模块:

```text
packages/service/support/permission/memberGroup/teamDefaultGroup.ts
```

该模块只依赖环境变量、权限常量、默认组 Model 和权限 service,不依赖团队 controller,避免与团队创建逻辑形成循环依赖。

新增函数:

```ts
export async function createTeamDefaultGroup({
teamId,
avatar,
session
}: {
teamId: string;
avatar?: string;
session?: ClientSession;
})
```

函数负责:

1. 创建团队默认全员组;
2. 环境变量开启时,创建默认组的团队资源权限;
3. 返回创建的默认组;
4. 调用方传入 session 时,默认组和权限写入复用该 session;未传入时由函数开启事务。

核心逻辑:

```ts
const [group] = await MongoMemberGroupModel.create(
[
{
teamId,
name: DefaultGroupName,
avatar
}
],
{ session, ordered: true }
);

if (serviceEnv.DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED) {
await updateTeamCollaborator({
teamId,
collaborator: { groupId: String(group._id) },
permission:
TeamAppCreateRoleVal |
TeamSkillCreateRoleVal |
TeamDatasetCreateRoleVal,
session
});
}

return group;
```

权限记录通过现有 `updateTeamCollaborator()` 和 permission repository 写入,不直接操作 `MongoResourcePermission`。

现有 `getTeamDefaultGroup()` 继续只负责读取时补建缺失的默认组,不自动授予基础权限,避免环境变量开启后改变存量团队权限。

### 4.3 接入团队创建链路

主仓库修改 `packages/service/support/user/team/controller.ts`:

```text
createDefaultTeam()
-> 创建 Team
-> 创建 Owner TeamMember
-> createTeamDefaultGroup()
-> 创建根组织
```

Pro 修改 `admin/src/service/support/user/team/controller.ts`:

```text
createTeam()
-> 创建 Team
-> 创建 Owner TeamMember
-> createTeamDefaultGroup()
-> 执行原有后续初始化
```

两个入口都用 `createTeamDefaultGroup()` 替换原来直接创建 `MongoMemberGroupModel` 的代码。

### 4.4 事务

主仓库 `createDefaultTeam()`、手动创建团队和注册流程原本已使用事务,本次新增的默认权限写入继续复用同一个 session。

Pro `createTeam()` 的 session 调整为必传,并为企业微信授权创建团队增加 `mongoSessionRun()`。团队、Owner、默认组和默认权限任一步写入失败时整体回滚。公共 `createTeamDefaultGroup()` 按代码规范保留可选 session,但两个团队创建入口必须传入已有 session。

## 5. 修改文件

FastGPT 主仓库:

```text
packages/service/env.ts
packages/service/support/permission/memberGroup/teamDefaultGroup.ts
packages/service/support/user/team/controller.ts
projects/app/.env.template
```

fastgpt-pro:

```text
admin/src/service/support/user/team/controller.ts
admin/src/service/support/wecom/handler/auth.ts
admin/.env.template
```

## 6. 测试要点

1. 环境变量未配置或为 `false` 时,只创建默认组,不创建权限记录;
2. 环境变量为 `true` 时,默认组获得应用、Skill、知识库创建权限;
3. 默认权限不包含团队管理和 API Key 创建权限;
4. `root` 团队和 Pro 创建的普通团队行为一致;
5. 普通成员加入团队后可以创建应用、Skill 和知识库;
6. 权限写入失败时,团队创建事务能够回滚;
7. 企业微信授权创建团队时,所有初始化数据在同一事务中写入。

## 7. 验收标准

1. 默认关闭时,团队创建行为与当前版本一致;
2. 开启后,所有新建团队的全员组均获得三项基础权限;
3. 普通成员不能管理团队或创建 API Key;
4. 所有团队创建入口均在同一事务内完成团队和默认权限初始化。
3 changes: 3 additions & 0 deletions packages/service/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@ export const serviceEnv = createEnv({
DISABLE_CACHE: BoolSchema.default(false).meta({
description: '是否禁用系统内存缓存'
}),
DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED: BoolSchema.default(false).meta({
description: '新建团队时是否为默认全员组授予应用、Skill 和知识库创建权限'
}),

// ==================== 并发控制与限制 ====================
WECHAT_CHANNEL_CONCURRENCY: IntSchema.min(10).default(1000).meta({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
TeamAppCreateRoleVal,
TeamDatasetCreateRoleVal,
TeamSkillCreateRoleVal
} from '@fastgpt/global/support/permission/user/constant';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { serviceEnv } from '../../../env';
import type { ClientSession } from '../../../common/mongo';
import { mongoSessionRun } from '../../../common/mongo/sessionRun';
import { updateTeamCollaborator } from '../resourcePermissionService';
import { MongoMemberGroupModel } from './memberGroupSchema';

const defaultTeamBasicRole =
TeamAppCreateRoleVal | TeamSkillCreateRoleVal | TeamDatasetCreateRoleVal;

/**
* 创建团队默认全员组,并按环境配置初始化团队级基础权限。
* 调用方传入 session 时复用现有事务,否则为组和权限写入开启独立事务。
*/
export async function createTeamDefaultGroup({
teamId,
avatar,
session
}: {
teamId: string;
avatar?: string;
session?: ClientSession;
}) {
const create = async (activeSession: ClientSession) => {
const [group] = await MongoMemberGroupModel.create(
[
{
teamId,
name: DefaultGroupName,
avatar
}
],
{ session: activeSession, ordered: true }
);

if (serviceEnv.DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED) {
await updateTeamCollaborator({
teamId,
collaborator: { groupId: String(group._id) },
permission: defaultTeamBasicRole,
session: activeSession
});
}

return group;
};

return session ? create(session) : mongoSessionRun(create);
}
13 changes: 2 additions & 11 deletions packages/service/support/user/team/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
formatTeamAccountCancellationSummary,
getActiveAccountCancellationsByTeamIds
} from '../account/cancellation';
import { createTeamDefaultGroup } from '../../permission/memberGroup/teamDefaultGroup';

const logger = getLogger(LogCategories.MODULE.USER.TEAM);

Expand Down Expand Up @@ -163,17 +164,7 @@ export async function createDefaultTeam({
],
{ session }
);
// create default group
await MongoMemberGroupModel.create(
[
{
teamId: tmb.teamId,
name: DefaultGroupName,
avatar
}
],
{ session }
);
await createTeamDefaultGroup({ teamId: tmb.teamId, avatar, session });
await createRootOrg({ teamId: tmb.teamId, session });
logger.info('Default team created', { userId, teamId: tmb.teamId, tmbId: tmb._id });
return tmb;
Expand Down
21 changes: 21 additions & 0 deletions packages/service/test/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const originalEnv = {
FILE_DOWNLOAD_PUBLIC_URL_PREFIX: process.env.FILE_DOWNLOAD_PUBLIC_URL_PREFIX,
STORAGE_DOWNLOAD_URL_MODE: process.env.STORAGE_DOWNLOAD_URL_MODE,
SYNC_INDEX: process.env.SYNC_INDEX,
DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED: process.env.DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED,
AES256_SECRET_KEY: process.env.AES256_SECRET_KEY,
INVOKE_TOKEN_SECRET: process.env.INVOKE_TOKEN_SECRET,
SOMARK_API_KEY: process.env.SOMARK_API_KEY,
Expand Down Expand Up @@ -69,6 +70,10 @@ describe('serviceEnv', () => {
vi.stubEnv('FILE_DOWNLOAD_PUBLIC_URL_PREFIX', originalEnv.FILE_DOWNLOAD_PUBLIC_URL_PREFIX);
vi.stubEnv('STORAGE_DOWNLOAD_URL_MODE', originalEnv.STORAGE_DOWNLOAD_URL_MODE);
vi.stubEnv('SYNC_INDEX', originalEnv.SYNC_INDEX);
vi.stubEnv(
'DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED',
originalEnv.DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED
);
vi.stubEnv('AES256_SECRET_KEY', originalEnv.AES256_SECRET_KEY);
vi.stubEnv('INVOKE_TOKEN_SECRET', originalEnv.INVOKE_TOKEN_SECRET);
vi.stubEnv('SOMARK_API_KEY', originalEnv.SOMARK_API_KEY);
Expand Down Expand Up @@ -169,6 +174,22 @@ describe('serviceEnv', () => {
await expect(importServiceEnv()).rejects.toThrow('Invalid environment variables');
});

it('disables default team basic permissions by default and supports enabling them', async () => {
vi.stubEnv('FILE_TOKEN_KEY', 'filetokenkey');
vi.stubEnv('AES256_SECRET_KEY', 'fastgptsecret');
vi.stubEnv('INVOKE_TOKEN_SECRET', validInvokeTokenSecret);

vi.stubEnv('DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED', undefined);
await expect(importServiceEnv()).resolves.toMatchObject({
serviceEnv: { DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED: false }
});

vi.stubEnv('DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED', 'true');
await expect(importServiceEnv()).resolves.toMatchObject({
serviceEnv: { DEFAULT_TEAM_BASIC_PERMISSIONS_ENABLED: true }
});
});

it('reads the optional SoMark API key', async () => {
vi.stubEnv('FILE_TOKEN_KEY', 'filetokenkey');
vi.stubEnv('AES256_SECRET_KEY', 'fastgptsecret');
Expand Down
Loading
Loading