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 && ( )} -

+