From fe3e87361d94216c74a6ee4ba3078a10e8309eb2 Mon Sep 17 00:00:00 2001 From: euvre <93761161+euvre@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:09:22 +0800 Subject: [PATCH] fix(agent): restrict permission field edits to canvas owner only (#17185) --- internal/service/agent.go | 11 +++++ internal/service/agent_test.go | 83 ++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/internal/service/agent.go b/internal/service/agent.go index c0bbf8705c..188879b1f2 100644 --- a/internal/service/agent.go +++ b/internal/service/agent.go @@ -581,12 +581,23 @@ func (s *AgentService) GetAgent(ctx context.Context, userID, canvasID string) (* // UpdateAgent applies a draft patch to user_canvas. Settings updates may omit // dsl; in that case the existing draft DSL must be preserved. +// +// Permission is an owner-only setting: team members who have access to the +// canvas can still update title/avatar/description, but any permission value +// they send is ignored so they cannot make a team agent private (or vice +// versa). The owner can change permission together with title/avatar in one +// request. func (s *AgentService) UpdateAgent(ctx context.Context, userID, canvasID string, patch map[string]interface{}) error { canvasInstance, err := s.loadCanvasForUser(ctx, userID, canvasID) if err != nil { return err } + // Only the canvas owner may change the permission field. + if _, ok := patch["permission"]; ok && canvasInstance.UserID != userID { + delete(patch, "permission") + } + updates := map[string]interface{}{} for _, key := range []string{"title", "avatar", "description", "permission", "canvas_type", "canvas_category"} { if value, ok := patch[key]; ok && value != nil { diff --git a/internal/service/agent_test.go b/internal/service/agent_test.go index 34b2b2e512..f13a58bcda 100644 --- a/internal/service/agent_test.go +++ b/internal/service/agent_test.go @@ -1469,6 +1469,89 @@ func TestUpdateAgentSettingsPreservesDSL(t *testing.T) { } } +func TestUpdateAgentPermissionOwnerOnly(t *testing.T) { + setupAgentSessionServiceTest(t) + + status := "1" + if err := dao.DB.Create(&entity.UserTenant{ + ID: "ut-owner", + UserID: "user-1", + TenantID: "user-1", + Role: "owner", + InvitedBy: "user-1", + Status: &status, + }).Error; err != nil { + t.Fatalf("failed to seed owner tenant: %v", err) + } + if err := dao.DB.Create(&entity.UserTenant{ + ID: "ut-member", + UserID: "user-2", + TenantID: "user-1", + Role: "normal", + InvitedBy: "user-1", + Status: &status, + }).Error; err != nil { + t.Fatalf("failed to seed member tenant: %v", err) + } + if err := dao.DB.Create(&entity.UserCanvas{ + ID: "canvas-team", + UserID: "user-1", + Title: sptr("Team Agent"), + Avatar: sptr("owner-avatar"), + Permission: "team", + CanvasCategory: "agent_canvas", + DSL: entity.JSONMap{}, + }).Error; err != nil { + t.Fatalf("failed to seed canvas: %v", err) + } + + // Team member tries to make the agent private while updating title/avatar. + err := NewAgentService().UpdateAgent(context.Background(), "user-2", "canvas-team", map[string]interface{}{ + "title": "Renamed by member", + "avatar": "member-avatar", + "permission": "me", + }) + if err != nil { + t.Fatalf("team member update should succeed for non-permission fields: %v", err) + } + persisted, err := dao.NewUserCanvasDAO().GetByID("canvas-team") + if err != nil { + t.Fatalf("failed to reload canvas: %v", err) + } + if persisted.Permission != "team" { + t.Fatalf("team member changed permission to %q; want team", persisted.Permission) + } + if persisted.Title == nil || *persisted.Title != "Renamed by member" { + t.Fatalf("title = %v, want Renamed by member", persisted.Title) + } + if persisted.Avatar == nil || *persisted.Avatar != "member-avatar" { + t.Fatalf("avatar = %v, want member-avatar", persisted.Avatar) + } + + // Owner can change permission together with title/avatar. + err = NewAgentService().UpdateAgent(context.Background(), "user-1", "canvas-team", map[string]interface{}{ + "title": "Owner updated", + "avatar": "owner-avatar-2", + "permission": "me", + }) + if err != nil { + t.Fatalf("owner update failed: %v", err) + } + persisted, err = dao.NewUserCanvasDAO().GetByID("canvas-team") + if err != nil { + t.Fatalf("failed to reload canvas: %v", err) + } + if persisted.Permission != "me" { + t.Fatalf("owner permission = %q, want me", persisted.Permission) + } + if persisted.Title == nil || *persisted.Title != "Owner updated" { + t.Fatalf("title = %v, want Owner updated", persisted.Title) + } + if persisted.Avatar == nil || *persisted.Avatar != "owner-avatar-2" { + t.Fatalf("avatar = %v, want owner-avatar-2", persisted.Avatar) + } +} + func TestUpdateAgentPersistsDSLAsJSONMap(t *testing.T) { setupAgentSessionServiceTest(t)