From d312a33cf3271c2a79c5a053143856dc15d43373 Mon Sep 17 00:00:00 2001 From: tanzui <305375675@.com> Date: Fri, 9 Oct 2026 10:11:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(agent):=20=E7=94=BB=E5=B8=83=E6=9D=83?= =?UTF-8?q?=E9=99=90=20-=20=E4=BD=BF=E7=94=A8=E7=9C=9F=E5=AE=9E=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E5=BD=92=E5=B1=9E=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/cloud_agent_permissions_test.go | 111 ++++++++++++++++++ .../internal/app/cloud_agent_pi_request.go | 55 ++------- 2 files changed, 124 insertions(+), 42 deletions(-) create mode 100644 backend/internal/app/cloud_agent_permissions_test.go diff --git a/backend/internal/app/cloud_agent_permissions_test.go b/backend/internal/app/cloud_agent_permissions_test.go new file mode 100644 index 000000000..9dd12b750 --- /dev/null +++ b/backend/internal/app/cloud_agent_permissions_test.go @@ -0,0 +1,111 @@ +package app + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "yingce/backend/internal/model" + "yingce/backend/internal/repository" +) + +// agentPermissionsDB 为权限测试创建数据库,只迁移真实画布表。 +// CI 使用临时 SQLite;Windows 可提供 PostgreSQL DSN,数据只写入独立临时 schema。 +func agentPermissionsDB(t *testing.T) *gorm.DB { + t.Helper() + options := &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)} + var db *gorm.DB + var err error + if dsn := os.Getenv("CANVAS_TEST_POSTGRES_DSN"); dsn != "" { + admin, openErr := gorm.Open(postgres.Open(dsn), options) + if openErr != nil { + t.Fatal(openErr) + } + adminSQL, sqlErr := admin.DB() + if sqlErr != nil { + t.Fatal(sqlErr) + } + t.Cleanup(func() { _ = adminSQL.Close() }) + schema := fmt.Sprintf("agent_permissions_%d", time.Now().UnixNano()) + if err := admin.Exec("CREATE SCHEMA " + schema).Error; err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := admin.Exec("DROP SCHEMA " + schema + " CASCADE").Error; err != nil { + t.Error(err) + } + }) + parsed, parseErr := url.Parse(dsn) + if parseErr != nil { + t.Fatal(parseErr) + } + query := parsed.Query() + query.Set("search_path", schema) + parsed.RawQuery = query.Encode() + db, err = gorm.Open(postgres.Open(parsed.String()), options) + } else { + db, err = gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "permissions.db")), options) + } + if err != nil { + t.Fatal(err) + } + sqlDB, err := db.DB() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = sqlDB.Close() }) + if err := db.AutoMigrate(&model.CanvasProject{}); err != nil { + t.Fatal(err) + } + return db +} + +func TestAgentPermissionsUseCanvasProjects(t *testing.T) { + db := agentPermissionsDB(t) + s := &Service{repo: repository.New(db)} + for _, canvas := range []model.CanvasProject{ + {ID: "independent", UserID: "owner", PayloadJSON: `{}`}, + {ID: "associated", UserID: "owner", ProjectID: "domain-project", PayloadJSON: `{}`}, + } { + if err := db.Create(&canvas).Error; err != nil { + t.Fatal(err) + } + } + if db.Migrator().HasTable("canvas") { + t.Fatal("权限测试不能创建旧 canvas 表") + } + for _, tc := range []struct { + user, canvas string + allowed bool + }{ + {"owner", "independent", true}, {"owner", "associated", true}, + {"other", "independent", false}, {"other", "associated", false}, + {"owner", "missing", false}, {"owner", "domain-project", false}, + } { + t.Run(tc.user+"/"+tc.canvas, func(t *testing.T) { + permissions := s.buildPermissionsConfig(tc.user, tc.canvas) + for _, key := range []string{"canReadCanvas", "canWriteCanvas", "canDeleteNodes", "canCreateNodes", "canMoveNodes", "canDuplicateNodes", "canManageRelations", "canInviteUsers", "canExportCanvas"} { + if permissions[key] != tc.allowed { + t.Errorf("%s=%v, want %t", key, permissions[key], tc.allowed) + } + } + }) + } + // 缺表模拟数据库读取故障,不能把读取失败当成只读或导出许可。 + if err := db.Migrator().DropTable(&model.CanvasProject{}); err != nil { + t.Fatal(err) + } + permissions := s.buildPermissionsConfig("owner", "independent") + for _, key := range []string{"canReadCanvas", "canWriteCanvas", "canExportCanvas"} { + if permissions[key] != false { + t.Errorf("数据库故障时 %s=%v", key, permissions[key]) + } + } +} diff --git a/backend/internal/app/cloud_agent_pi_request.go b/backend/internal/app/cloud_agent_pi_request.go index 2bc4f37db..ed6e5a022 100644 --- a/backend/internal/app/cloud_agent_pi_request.go +++ b/backend/internal/app/cloud_agent_pi_request.go @@ -170,15 +170,16 @@ func (s *Service) buildFeaturesConfig(state *cloudAgentRuntime) map[string]any { } } -// buildPermissionsConfig 构建权限配置 +// buildPermissionsConfig 根据当前账号的画布归属生成运行时权限。 +// userID 是登录账号,canvasID 是画布 ID(不是所属项目 ID);查询失败时拒绝访问。 func (s *Service) buildPermissionsConfig(userID, canvasID string) map[string]any { - // 从数据库读取实际权限 - canvas, err := s.repo.GetCanvas(userID, canvasID) + // 与画布读写工具共用账号归属查询,不访问旧的 Canvas 扩展模型。 + _, err := s.repo.CanvasProjectForUser(userID, canvasID) if err != nil { log.Printf("[Agent] failed to get canvas for permissions: %v", err) // 返回最小权限集 return map[string]any{ - "canReadCanvas": true, + "canReadCanvas": false, "canWriteCanvas": false, "canDeleteNodes": false, "canCreateNodes": false, @@ -186,51 +187,21 @@ func (s *Service) buildPermissionsConfig(userID, canvasID string) map[string]any "canDuplicateNodes": false, "canManageRelations": false, "canInviteUsers": false, - "canExportCanvas": true, + "canExportCanvas": false, "maxTokenBudget": 200000, "maxSteps": 50, } } - // 检查用户是否是画布所有者 - isOwner := canvas.UserID == userID - - // 检查协作权限 - canWrite := isOwner - canDelete := isOwner - canInvite := isOwner - - if canvas.Metadata != nil { - if collaborators, ok := canvas.Metadata["collaborators"].([]any); ok { - for _, collab := range collaborators { - if collabMap, ok := collab.(map[string]any); ok { - if collabUserID, _ := collabMap["userId"].(string); collabUserID == userID { - role, _ := collabMap["role"].(string) - switch role { - case "admin": - canWrite = true - canDelete = true - canInvite = true - case "editor": - canWrite = true - case "viewer": - // 只读权限 - } - } - } - } - } - } - return map[string]any{ "canReadCanvas": true, - "canWriteCanvas": canWrite, - "canDeleteNodes": canDelete, - "canCreateNodes": canWrite, - "canMoveNodes": canWrite, - "canDuplicateNodes": canWrite, - "canManageRelations": canWrite, - "canInviteUsers": canInvite, + "canWriteCanvas": true, + "canDeleteNodes": true, + "canCreateNodes": true, + "canMoveNodes": true, + "canDuplicateNodes": true, + "canManageRelations": true, + "canInviteUsers": true, "canExportCanvas": true, "maxTokenBudget": 200000, "maxSteps": 50,