fix(agent): 画布权限 - 使用真实画布归属查询

This commit is contained in:
tanzui
2026-10-09 10:11:49 +08:00
parent ac68378246
commit d312a33cf3
2 changed files with 124 additions and 42 deletions

View File

@@ -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])
}
}
}

View File

@@ -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,