From f86e08e91c1cbb45ce81a9d97d897c495445e876 Mon Sep 17 00:00:00 2001
From: GuoQing Zhang
Date: Thu, 27 Aug 2026 11:43:17 +0800
Subject: [PATCH 1/4] ci: change version
---
docker/docker-compose.yml | 6 +++---
src/backend/bisheng/__init__.py | 2 +-
src/backend/pyproject.toml | 2 +-
update.sh | 4 ++--
4 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 5609122eb..b0727a401 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -86,7 +86,7 @@ services:
backend:
container_name: bisheng-backend
- image: dataelement/bisheng-backend:v2.6.0-fix2
+ image: dataelement/bisheng-backend:v3.0.0-beta1
ports:
- "7860:7860"
environment:
@@ -128,7 +128,7 @@ services:
backend_worker:
container_name: bisheng-backend-worker
- image: dataelement/bisheng-backend:v2.6.0-fix2
+ image: dataelement/bisheng-backend:v3.0.0-beta1
environment:
TZ: Asia/Shanghai
BS_SSO_SYNC__GATEWAY_HMAC_SECRET: "bisheng-local-hmac-20260422"
@@ -161,7 +161,7 @@ services:
frontend:
container_name: bisheng-frontend
- image: dataelement/bisheng-frontend:v2.6.0-fix2
+ image: dataelement/bisheng-frontend:v3.0.0-beta1
ports:
- "3001:3001"
environment:
diff --git a/src/backend/bisheng/__init__.py b/src/backend/bisheng/__init__.py
index 976b7d173..e29927b03 100644
--- a/src/backend/bisheng/__init__.py
+++ b/src/backend/bisheng/__init__.py
@@ -4,7 +4,7 @@ from importlib import metadata
try:
# SetujuciGo to automatic modification
- __version__ = '2.6.0-fix2'
+ __version__ = '3.0.0-beta1'
except metadata.PackageNotFoundError:
# Case where package metadata is not available.
__version__ = ''
diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml
index 1814db0fb..b9bbdec8c 100644
--- a/src/backend/pyproject.toml
+++ b/src/backend/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "backend"
-version = "2.6.0"
+version = "3.0.0-beta1"
description = "BISHENG backend service"
readme = "README.md"
license = "Apache 2.0"
diff --git a/update.sh b/update.sh
index 2f6e7bf2c..ef3737df6 100644
--- a/update.sh
+++ b/update.sh
@@ -1,7 +1,7 @@
#! /bin/bash
-old_version="2.6.0-fix"
-new_version="2.6.0-fix2"
+old_version="2.6.0-fix2"
+new_version="3.0.0-beta1"
sed -i.bak "s/$old_version/$new_version/g" ./docker/docker-compose.yml
sed -i.bak "s/$old_version/$new_version/g" ./src/backend/pyproject.toml
sed -i.bak "s/$old_version/$new_version/g" ./src/backend/bisheng/__init__.py
From c7d59ab734af5ad80fd5a7661df1015bf3483c93 Mon Sep 17 00:00:00 2001
From: GuoQing Zhang
Date: Thu, 27 Aug 2026 17:48:53 +0800
Subject: [PATCH 2/4] fix(permission): expose group admin grants to workers
---
.../permission/application/resource_api.py | 1 +
.../bisheng/permission/domain/schemas/f048.py | 1 +
.../bisheng/tool/domain/services/executor.py | 7 +-
.../test/permission/test_f048_resource_api.py | 24 +++--
.../tool/test_tool_executor_permissions.py | 14 +++
src/frontend/client/src/api/permission.ts | 1 +
.../permission/PermissionGrantTab.test.tsx | 89 ++++++++++++++++++-
.../permission/PermissionGrantTab.tsx | 7 +-
.../src/components/permission/SourceBadge.tsx | 5 ++
.../client/src/locales/en/translation.json | 1 +
.../client/src/locales/ja/translation.json | 1 +
.../src/locales/zh-Hans/translation.json | 1 +
.../public/locales/en-US/permission.json | 1 +
.../public/locales/ja/permission.json | 1 +
.../public/locales/zh-Hans/permission.json | 1 +
.../bs-comp/permission/SourceBadge.tsx | 3 +
.../src/controllers/API/permission.ts | 1 +
.../src/test/f048PermissionGrantTab.test.tsx | 83 ++++++++++++++++-
18 files changed, 229 insertions(+), 13 deletions(-)
diff --git a/src/backend/bisheng/permission/application/resource_api.py b/src/backend/bisheng/permission/application/resource_api.py
index ec2c6e57a..11dd46ced 100644
--- a/src/backend/bisheng/permission/application/resource_api.py
+++ b/src/backend/bisheng/permission/application/resource_api.py
@@ -222,6 +222,7 @@ class F048ResourcePermissionApi:
"source": {
"type": row.source_type,
"include_children": row.include_children,
+ "userset_relation": row.userset_relation,
},
"scope": row.scope,
"inherited_from": row.inherited_from,
diff --git a/src/backend/bisheng/permission/domain/schemas/f048.py b/src/backend/bisheng/permission/domain/schemas/f048.py
index 9e442a6bf..ce6d96bc3 100644
--- a/src/backend/bisheng/permission/domain/schemas/f048.py
+++ b/src/backend/bisheng/permission/domain/schemas/f048.py
@@ -313,6 +313,7 @@ class GrantModelDTO(BaseModel):
class GrantSourceDTO(BaseModel):
type: str
include_children: bool = False
+ userset_relation: str | None = None
class GrantAssigneeDTO(BaseModel):
diff --git a/src/backend/bisheng/tool/domain/services/executor.py b/src/backend/bisheng/tool/domain/services/executor.py
index 485d1ee1e..3e0b30a36 100644
--- a/src/backend/bisheng/tool/domain/services/executor.py
+++ b/src/backend/bisheng/tool/domain/services/executor.py
@@ -93,22 +93,25 @@ class ToolExecutor(BaseTool):
tool_instance: BaseTool = Field(..., description="Langchain Tool Instance")
@staticmethod
- def _build_permission_user(
+ async def _build_permission_user(
user_id: int,
tenant_id: int,
) -> UserPayload:
+ from bisheng.utils.http_middleware import _check_is_global_super
+
return UserPayload(
user_id=user_id,
user_name="",
user_role=[],
tenant_id=tenant_id,
+ is_global_super=await _check_is_global_super(user_id),
)
@classmethod
async def _ensure_use_permission_async(cls, tool_type: GptsToolsType, user_id: int) -> None:
if not tool_type.id or not tool_type.tenant_id:
raise PermissionError("Permission denied: use")
- login_user = cls._build_permission_user(
+ login_user = await cls._build_permission_user(
user_id,
int(tool_type.tenant_id),
)
diff --git a/src/backend/test/permission/test_f048_resource_api.py b/src/backend/test/permission/test_f048_resource_api.py
index 23d18157a..25d8e5c72 100644
--- a/src/backend/test/permission/test_f048_resource_api.py
+++ b/src/backend/test/permission/test_f048_resource_api.py
@@ -124,11 +124,11 @@ class _Runtime:
PermissionSourceExplanation(
source_id=91,
source_version=2,
- subject_type="user",
- subject_id="8",
- userset_relation=None,
+ subject_type="user_group",
+ subject_id="2",
+ userset_relation="admin",
include_children=False,
- source_type="DIRECT",
+ source_type="USER_GROUP",
model_key="viewer",
model_level=1,
scope="LOCAL",
@@ -156,7 +156,10 @@ class _Subjects:
)
async def display_names(self, subjects):
- return {("user", "8"): "Member 8"}
+ return {
+ ("user", "8"): "Member 8",
+ ("user_group", "2"): "Reviewers",
+ }
async def actor_projected_subjects(self, actor):
return frozenset({f"user:{actor.user_id}"})
@@ -500,7 +503,16 @@ async def test_roster_uses_bounded_sql_page_instead_of_full_explanation() -> Non
page_size=25,
)
- assert result["data"][0]["subject"]["name"] == "Member 8"
+ assert result["data"][0]["subject"] == {
+ "type": "user_group",
+ "id": "2",
+ "name": "Reviewers",
+ }
+ assert result["data"][0]["source"] == {
+ "type": "USER_GROUP",
+ "include_children": False,
+ "userset_relation": "admin",
+ }
assert result["has_more"] is True
assert result["next_cursor"]
assert runtime.page_calls == [
diff --git a/src/backend/test/tool/test_tool_executor_permissions.py b/src/backend/test/tool/test_tool_executor_permissions.py
index f09b9268d..7cd649d1d 100644
--- a/src/backend/test/tool/test_tool_executor_permissions.py
+++ b/src/backend/test/tool/test_tool_executor_permissions.py
@@ -12,10 +12,15 @@ from bisheng.tool.domain.services.executor import ToolExecutor, ToolInitializati
@pytest.mark.asyncio
async def test_preset_tool_execution_still_checks_exact_use_action(monkeypatch):
check_action = AsyncMock(return_value=True)
+ check_global_super = AsyncMock(return_value=True)
monkeypatch.setattr(
"bisheng.tool.domain.services.executor.check_business_action",
check_action,
)
+ monkeypatch.setattr(
+ "bisheng.utils.http_middleware._check_is_global_super",
+ check_global_super,
+ )
tool_type = SimpleNamespace(
id=10,
tenant_id=5,
@@ -27,6 +32,8 @@ async def test_preset_tool_execution_still_checks_exact_use_action(monkeypatch):
login_user = check_action.await_args.args[0]
assert login_user.user_id == 7
assert login_user.tenant_id == 5
+ assert login_user.is_global_super is True
+ check_global_super.assert_awaited_once_with(7)
assert check_action.await_args.kwargs == {
"resource_type": "tool",
"resource_id": 10,
@@ -39,6 +46,10 @@ async def test_preset_tool_execution_never_falls_back_when_fga_fails(
monkeypatch,
):
check_action = AsyncMock(side_effect=PermissionFGAUnavailableError())
+ monkeypatch.setattr(
+ "bisheng.utils.http_middleware._check_is_global_super",
+ AsyncMock(return_value=False),
+ )
monkeypatch.setattr(
"bisheng.tool.domain.services.executor.check_business_action",
check_action,
@@ -52,6 +63,9 @@ async def test_preset_tool_execution_never_falls_back_when_fga_fails(
with pytest.raises(PermissionFGAUnavailableError):
await ToolExecutor._ensure_use_permission_async(tool_type, user_id=7)
+ login_user = check_action.await_args.args[0]
+ assert login_user.is_global_super is False
+
@pytest.mark.asyncio
async def test_init_by_tool_ids_can_skip_unauthorized_tools(monkeypatch):
diff --git a/src/frontend/client/src/api/permission.ts b/src/frontend/client/src/api/permission.ts
index 41e42b088..b1ca9ea6f 100644
--- a/src/frontend/client/src/api/permission.ts
+++ b/src/frontend/client/src/api/permission.ts
@@ -74,6 +74,7 @@ export interface PermissionGrantSubject {
export interface PermissionGrantSource {
type: string;
include_children: boolean;
+ userset_relation?: string | null;
}
export interface PermissionGrantAssignee {
diff --git a/src/frontend/client/src/components/permission/PermissionGrantTab.test.tsx b/src/frontend/client/src/components/permission/PermissionGrantTab.test.tsx
index 4bd668d2d..c741194fe 100644
--- a/src/frontend/client/src/components/permission/PermissionGrantTab.test.tsx
+++ b/src/frontend/client/src/components/permission/PermissionGrantTab.test.tsx
@@ -65,7 +65,22 @@ jest.mock("./SubjectSearchDepartment", () => ({
}));
jest.mock("./SubjectSearchUserGroup", () => ({
- SubjectSearchUserGroup: () => group picker
,
+ SubjectSearchUserGroup: ({
+ onChange,
+ }: {
+ onChange: (
+ subjects: Array<{ type: "user_group"; id: number; name: string }>,
+ ) => void;
+ }) => (
+
+ ),
}));
const mockedGetModels = getGrantablePermissionModels as jest.MockedFunction<
@@ -256,6 +271,37 @@ describe("F048 Client PermissionGrantTab", () => {
).toBeDisabled();
});
+ it("shows an existing user-group administrator grant", async () => {
+ render(
+ ,
+ );
+
+ expect(
+ await screen.findByText(
+ /f048_permission\.source\.user_group_admin/,
+ ),
+ ).toBeInTheDocument();
+ });
+
it("keeps an inactive existing row read-only when it is not grantable", async () => {
render(
{
);
});
+ it("does not expose or submit a user-group administrator grant", async () => {
+ render(
+ ,
+ );
+
+ await screen.findByLabelText("f048_permission.grant.add_model");
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: "f048_permission.subject.user_group",
+ }),
+ );
+ fireEvent.click(screen.getByRole("button", { name: "select group" }));
+ fireEvent.click(
+ screen.getByRole("button", { name: "f048_permission.grant.add" }),
+ );
+ fireEvent.click(
+ screen.getByRole("button", { name: "f048_permission.grant.submit" }),
+ );
+
+ await waitFor(() => expect(mockedMutate).toHaveBeenCalledTimes(1));
+ expect(mockedMutate.mock.calls[0][2]).toEqual(
+ expect.objectContaining({
+ changes: [
+ {
+ op: "ADD",
+ model_key: "viewer",
+ subject: {
+ type: "user_group",
+ id: "77",
+ },
+ },
+ ],
+ }),
+ );
+ });
+
it("fails closed on a version conflict", async () => {
mockedMutate.mockRejectedValueOnce(new Error("version conflict"));
diff --git a/src/frontend/client/src/components/permission/PermissionGrantTab.tsx b/src/frontend/client/src/components/permission/PermissionGrantTab.tsx
index 5768cadd3..181177f4c 100644
--- a/src/frontend/client/src/components/permission/PermissionGrantTab.tsx
+++ b/src/frontend/client/src/components/permission/PermissionGrantTab.tsx
@@ -17,6 +17,7 @@ import type {
import { Button, Checkbox } from "~/components/ui";
import { useLocalize } from "~/hooks";
import { canMutatePermissionAssignee } from "./assigneePolicy";
+import { SourceBadge } from "./SourceBadge";
import { SubjectSearchDepartment } from "./SubjectSearchDepartment";
import { SubjectSearchUser } from "./SubjectSearchUser";
import { SubjectSearchUserGroup } from "./SubjectSearchUserGroup";
@@ -398,15 +399,15 @@ export function PermissionGrantTab({
{assignee.subject.name ||
`${assignee.subject.type}:${assignee.subject.id}`}
-
- {assignee.source.type}
+
+
{assignee.protected && (
{localize("f048_permission.roster.protected")}
)}
-
+