diff --git a/backend/api/handler/coze/workflow_service_test.go b/backend/api/handler/coze/workflow_service_test.go index 3f0016bdc..490bcee58 100644 --- a/backend/api/handler/coze/workflow_service_test.go +++ b/backend/api/handler/coze/workflow_service_test.go @@ -72,6 +72,8 @@ import ( "github.com/coze-dev/coze-studio/backend/crossdomain/knowledge/knowledgemock" knowledge "github.com/coze-dev/coze-studio/backend/crossdomain/knowledge/model" "github.com/coze-dev/coze-studio/backend/crossdomain/message/messagemock" + crosspermission "github.com/coze-dev/coze-studio/backend/crossdomain/permission" + "github.com/coze-dev/coze-studio/backend/crossdomain/permission/permissionmock" crossplugin "github.com/coze-dev/coze-studio/backend/crossdomain/plugin" pluginImpl "github.com/coze-dev/coze-studio/backend/crossdomain/plugin/impl" pluginmodel "github.com/coze-dev/coze-studio/backend/crossdomain/plugin/model" @@ -81,6 +83,7 @@ import ( conventity "github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/entity" entity4 "github.com/coze-dev/coze-studio/backend/domain/memory/database/entity" entity2 "github.com/coze-dev/coze-studio/backend/domain/openauth/openapiauth/entity" + permission "github.com/coze-dev/coze-studio/backend/domain/permission" "github.com/coze-dev/coze-studio/backend/domain/plugin/dto" entity3 "github.com/coze-dev/coze-studio/backend/domain/plugin/entity" entity5 "github.com/coze-dev/coze-studio/backend/domain/plugin/entity" @@ -181,6 +184,11 @@ func newWfTestRunner(t *testing.T) *wfTestRunner { ctxcache.Store(c, consts.SessionDataKeyInCtx, &userentity.Session{ UserID: 123, }) + // Add API auth info for OpenAPI endpoints + ctxcache.Store(c, consts.OpenapiAuthKeyInCtx, &entity2.ApiKey{ + UserID: 123, + ConnectorID: consts.APIConnectorID, + }) ctx.Next(c) }) h.POST("/api/workflow_api/node_template_list", NodeTemplateList) @@ -330,6 +338,11 @@ func newWfTestRunner(t *testing.T) *wfTestRunner { mockAgentRun := agentrunmock.NewMockAgentRun(ctrl) crossagentrun.SetDefaultSVC(mockAgentRun) + // Initialize permission service for tests + mockPermission := permissionmock.NewMockPermission(ctrl) + mockPermission.EXPECT().CheckAuthz(gomock.Any(), gomock.Any()).Return(&permission.CheckAuthzResult{Decision: permission.Allow}, nil).AnyTimes() + crosspermission.SetDefaultSVC(mockPermission) + mockey.Mock((*user.UserApplicationService).MGetUserBasicInfo).Return(&playground.MGetUserBasicInfoResponse{ UserBasicInfoMap: make(map[string]*playground.UserBasicInfo), }, nil).Build() @@ -1029,6 +1042,8 @@ func (r *wfTestRunner) openapiStream(id string, input any) *sse.Reader { hReq.SetMethod("POST") hReq.SetBody(m) hReq.SetHeader("Content-Type", "application/json") + // Add Authorization header for API authentication + hReq.SetHeader("Authorization", "Bearer test-api-key-123") err = c.Do(context.Background(), hReq, hResp) assert.NoError(r.t, err) @@ -1059,6 +1074,8 @@ func (r *wfTestRunner) openapiResume(id string, eventID string, resumeData strin hReq.SetMethod("POST") hReq.SetBody(m) hReq.SetHeader("Content-Type", "application/json") + // Add Authorization header for API authentication + hReq.SetHeader("Authorization", "Bearer test-api-key-123") err = c.Do(context.Background(), hReq, hResp) assert.NoError(r.t, err) diff --git a/backend/application/app/app.go b/backend/application/app/app.go index aad8a2fb0..fd159287b 100644 --- a/backend/application/app/app.go +++ b/backend/application/app/app.go @@ -45,6 +45,7 @@ import ( "github.com/coze-dev/coze-studio/backend/application/workflow" "github.com/coze-dev/coze-studio/backend/bizpkg/config" connectorModel "github.com/coze-dev/coze-studio/backend/crossdomain/connector/model" + knowledgeModel "github.com/coze-dev/coze-studio/backend/crossdomain/knowledge/model" pluginConsts "github.com/coze-dev/coze-studio/backend/crossdomain/plugin/consts" "github.com/coze-dev/coze-studio/backend/domain/app/entity" @@ -52,6 +53,7 @@ import ( "github.com/coze-dev/coze-studio/backend/domain/app/service" connector "github.com/coze-dev/coze-studio/backend/domain/connector/service" variables "github.com/coze-dev/coze-studio/backend/domain/memory/variables/service" + "github.com/coze-dev/coze-studio/backend/domain/permission" "github.com/coze-dev/coze-studio/backend/domain/plugin/dto" searchEntity "github.com/coze-dev/coze-studio/backend/domain/search/entity" search "github.com/coze-dev/coze-studio/backend/domain/search/service" @@ -389,7 +391,37 @@ func (a *APPApplicationService) getLatestPublishRecord(ctx context.Context, appI return latestRecord, nil } +func checkUserSpace(ctx context.Context, uid int64, spaceID int64) error { + // Use permission service to check workspace access + result, err := permission.DefaultSVC().CheckAuthz(ctx, &permission.CheckAuthzData{ + ResourceIdentifier: []*permission.ResourceIdentifier{ + { + Type: permission.ResourceTypeWorkspace, + ID: []int64{spaceID}, + Action: permission.ActionRead, + }, + }, + OperatorID: uid, + }) + if err != nil { + return fmt.Errorf("failed to check workspace permission: %w", err) + } + + if result.Decision != permission.Allow { + return fmt.Errorf("user %d does not have access to space %d", uid, spaceID) + } + + return nil +} + func (a *APPApplicationService) ReportUserBehavior(ctx context.Context, req *playground.ReportUserBehaviorRequest) (resp *playground.ReportUserBehaviorResponse, err error) { + uid := ctxutil.MustGetUIDFromCtx(ctx) + + err = checkUserSpace(ctx, uid, req.GetSpaceID()) + if err != nil { + return nil, err + } + err = a.projectEventBus.PublishProject(ctx, &searchEntity.ProjectDomainEvent{ OpType: searchEntity.Updated, Project: &searchEntity.ProjectDocument{ diff --git a/backend/application/application.go b/backend/application/application.go index 8e91f1a4c..ae91a7d0f 100644 --- a/backend/application/application.go +++ b/backend/application/application.go @@ -20,6 +20,8 @@ import ( "context" "fmt" + "github.com/coze-dev/coze-studio/backend/application/permission" + "github.com/coze-dev/coze-studio/backend/application/app" "github.com/coze-dev/coze-studio/backend/application/base/appinfra" "github.com/coze-dev/coze-studio/backend/application/connector" @@ -41,6 +43,8 @@ import ( singleagentImpl "github.com/coze-dev/coze-studio/backend/crossdomain/agent/impl" crossagentrun "github.com/coze-dev/coze-studio/backend/crossdomain/agentrun" agentrunImpl "github.com/coze-dev/coze-studio/backend/crossdomain/agentrun/impl" + crossapp "github.com/coze-dev/coze-studio/backend/crossdomain/app" + appImpl "github.com/coze-dev/coze-studio/backend/crossdomain/app/impl" crossconnector "github.com/coze-dev/coze-studio/backend/crossdomain/connector" connectorImpl "github.com/coze-dev/coze-studio/backend/crossdomain/connector/impl" crossconversation "github.com/coze-dev/coze-studio/backend/crossdomain/conversation" @@ -53,6 +57,8 @@ import ( knowledgeImpl "github.com/coze-dev/coze-studio/backend/crossdomain/knowledge/impl" crossmessage "github.com/coze-dev/coze-studio/backend/crossdomain/message" messageImpl "github.com/coze-dev/coze-studio/backend/crossdomain/message/impl" + crosspermission "github.com/coze-dev/coze-studio/backend/crossdomain/permission" + permissionImpl "github.com/coze-dev/coze-studio/backend/crossdomain/permission/impl" crossplugin "github.com/coze-dev/coze-studio/backend/crossdomain/plugin" pluginImpl "github.com/coze-dev/coze-studio/backend/crossdomain/plugin/impl" crosssearch "github.com/coze-dev/coze-studio/backend/crossdomain/search" @@ -90,6 +96,8 @@ type basicServices struct { templateSVC *template.ApplicationService openAuthSVC *openauth.OpenAuthApplicationService uploadSVC *upload.UploadService + + permissionSVC *permission.PermissionApplicationService } type primaryServices struct { @@ -101,6 +109,7 @@ type primaryServices struct { knowledgeSVC *knowledge.KnowledgeApplicationService workflowSVC *workflow.ApplicationService shortcutSVC *shortcutcmd.ShortcutCmdApplicationService + appSVC *app.APPApplicationService } type complexServices struct { @@ -138,6 +147,9 @@ func Init(ctx context.Context) (err error) { return fmt.Errorf("Init - initVitalServices failed, err: %v", err) } + // Initialize permission service first as it's required by other services + crosspermission.SetDefaultSVC(permissionImpl.InitDomainService(basicServices.permissionSVC.DomainSVC)) + crossconnector.SetDefaultSVC(connectorImpl.InitDomainService(basicServices.connectorSVC.DomainSVC)) crossdatabase.SetDefaultSVC(databaseImpl.InitDomainService(primaryServices.memorySVC.DatabaseDomainSVC)) crossknowledge.SetDefaultSVC(knowledgeImpl.InitDomainService(primaryServices.knowledgeSVC.DomainSVC)) @@ -153,6 +165,8 @@ func Init(ctx context.Context) (err error) { crosssearch.SetDefaultSVC(searchImpl.InitDomainService(complexServices.searchSVC.DomainSVC)) crossupload.SetDefaultSVC(uploadImpl.InitDomainService(basicServices.uploadSVC.UploadSVC)) + crossapp.SetDefaultSVC(appImpl.InitDomainService(complexServices.appSVC.DomainSVC)) + return nil } @@ -179,6 +193,8 @@ func initBasicServices(ctx context.Context, infra *appinfra.AppDependencies, e * Storage: infra.OSS, }) + permissionSVC := permission.InitService(&permission.ServiceComponents{}) + return &basicServices{ infra: infra, eventbus: e, @@ -189,6 +205,8 @@ func initBasicServices(ctx context.Context, infra *appinfra.AppDependencies, e * templateSVC: templateSVC, openAuthSVC: openAuthSVC, uploadSVC: uploadSVC, + + permissionSVC: permissionSVC, }, nil } diff --git a/backend/application/conversation/agent_run.go b/backend/application/conversation/agent_run.go index b8f39d0c9..d9cd5810b 100644 --- a/backend/application/conversation/agent_run.go +++ b/backend/application/conversation/agent_run.go @@ -91,6 +91,9 @@ func (c *ConversationApplicationService) Run(ctx context.Context, sseSender *sse if err != nil { return err } + if cmdMeta.ObjectID > 0 && cmdMeta.ObjectID != agentInfo.AgentID { + return errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "agent not match")) + } shortcutCmd = cmdMeta } diff --git a/backend/application/conversation/message.go b/backend/application/conversation/message.go index a1941847b..697a98963 100644 --- a/backend/application/conversation/message.go +++ b/backend/application/conversation/message.go @@ -322,8 +322,24 @@ func (c *ConversationApplicationService) DeleteMessage(ctx context.Context, mr * func (c *ConversationApplicationService) BreakMessage(ctx context.Context, mr *message.BreakMessageRequest) (*message.BreakMessageResponse, error) { resp := new(message.BreakMessageResponse) + messageInfo, err := c.MessageDomainSVC.GetByID(ctx, mr.GetAnswerMessageID()) + if err != nil { + return resp, err + } + if messageInfo == nil { + return resp, errorx.New(errno.ErrConversationMessageNotFound) + } - err := c.MessageDomainSVC.Broken(ctx, &entity.BrokenMeta{ + userID := ctxutil.GetUIDFromCtx(ctx) + if messageInfo.UserID != conv.Int64ToStr(*userID) { + return resp, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "permission denied")) + } + + if messageInfo.ConversationID != mr.ConversationID { + return resp, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "conversation not match")) + } + + err = c.MessageDomainSVC.Broken(ctx, &entity.BrokenMeta{ ID: *mr.AnswerMessageID, Position: mr.BrokenPos, }) diff --git a/backend/application/conversation/openapi_agent_run.go b/backend/application/conversation/openapi_agent_run.go index 91e4a2d81..92c85445f 100644 --- a/backend/application/conversation/openapi_agent_run.go +++ b/backend/application/conversation/openapi_agent_run.go @@ -63,12 +63,20 @@ func (a *OpenapiAgentRunApplication) OpenapiAgentRun(ctx context.Context, sseSen return caErr } + if creatorID != agentInfo.CreatorID { + return errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "agent not match")) + } + conversationData, ccErr := a.checkConversation(ctx, ar, creatorID, connectorID) if ccErr != nil { logs.CtxErrorf(ctx, "checkConversation err:%v", ccErr) return ccErr } + if conversationData.CreatorID != creatorID { + return errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "user not match")) + } + spaceID := agentInfo.SpaceID arr, err := a.buildAgentRunRequest(ctx, ar, connectorID, spaceID, conversationData) if err != nil { @@ -643,6 +651,10 @@ func (a *OpenapiAgentRunApplication) CancelRun(ctx context.Context, req *run.Can return nil, err } + if req.ConversationID != runRecord.ConversationID { + return nil, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "conversation not match")) + } + if userID != conversationData.CreatorID { return nil, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "user not match")) } diff --git a/backend/application/conversation/openapi_agent_run_test.go b/backend/application/conversation/openapi_agent_run_test.go index c56b4d08c..bbabc6211 100644 --- a/backend/application/conversation/openapi_agent_run_test.go +++ b/backend/application/conversation/openapi_agent_run_test.go @@ -159,8 +159,9 @@ func TestOpenapiAgentRun_Success(t *testing.T) { // Mock agent check mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -217,8 +218,9 @@ func TestOpenapiAgentRun_CheckConversationError(t *testing.T) { // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -240,8 +242,9 @@ func TestOpenapiAgentRun_ConversationPermissionError(t *testing.T) { // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -268,8 +271,9 @@ func TestOpenapiAgentRun_CreateNewConversation(t *testing.T) { // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -305,7 +309,8 @@ func TestOpenapiAgentRun_AgentRunError(t *testing.T) { mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ AgentID: 67890, - SpaceID: 54321, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -339,8 +344,9 @@ func TestOpenapiAgentRun_WithShortcutCommand(t *testing.T) { // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -379,8 +385,9 @@ func TestOpenapiAgentRun_WithMultipleMessages(t *testing.T) { // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -421,8 +428,9 @@ func TestOpenapiAgentRun_WithAssistantMessage(t *testing.T) { // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -490,8 +498,9 @@ func TestOpenapiAgentRun_WithMixedContentTypes(t *testing.T) { // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -554,8 +563,9 @@ func TestOpenapiAgentRun_ParseAdditionalMessages_InvalidRole(t *testing.T) { // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -597,8 +607,9 @@ func TestOpenapiAgentRun_ParseAdditionalMessages_InvalidType(t *testing.T) { // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -640,8 +651,9 @@ func TestOpenapiAgentRun_ParseAdditionalMessages_AnswerWithNonTextContent(t *tes // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -681,8 +693,9 @@ func TestOpenapiAgentRun_ParseAdditionalMessages_MixApiWithFileURL(t *testing.T) // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -725,8 +738,9 @@ func TestOpenapiAgentRun_ParseAdditionalMessages_MixApiWithFileID(t *testing.T) // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -780,8 +794,9 @@ func TestOpenapiAgentRun_ParseAdditionalMessages_FileIDError(t *testing.T) { // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -829,8 +844,9 @@ func TestOpenapiAgentRun_ParseAdditionalMessages_EmptyContent(t *testing.T) { // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) @@ -881,8 +897,9 @@ func TestOpenapiAgentRun_ParseAdditionalMessages_NilMessage(t *testing.T) { // Mock agent check success mockAgent := &saEntity.SingleAgent{ SingleAgent: &singleagent.SingleAgent{ - AgentID: 67890, - SpaceID: 54321, + AgentID: 67890, + SpaceID: 54321, + CreatorID: 12345, }, } mockSingleAgent.EXPECT().ObtainAgentByIdentity(ctx, gomock.Any()).Return(mockAgent, nil) diff --git a/backend/application/knowledge/knowledge.go b/backend/application/knowledge/knowledge.go index a0e3f3731..2ad9e75c4 100644 --- a/backend/application/knowledge/knowledge.go +++ b/backend/application/knowledge/knowledge.go @@ -32,9 +32,11 @@ import ( resource "github.com/coze-dev/coze-studio/backend/api/model/resource/common" "github.com/coze-dev/coze-studio/backend/application/base/ctxutil" "github.com/coze-dev/coze-studio/backend/application/search" + model "github.com/coze-dev/coze-studio/backend/crossdomain/knowledge/model" "github.com/coze-dev/coze-studio/backend/domain/knowledge/entity" "github.com/coze-dev/coze-studio/backend/domain/knowledge/service" + "github.com/coze-dev/coze-studio/backend/domain/permission" resourceEntity "github.com/coze-dev/coze-studio/backend/domain/search/entity" cd "github.com/coze-dev/coze-studio/backend/infra/document" "github.com/coze-dev/coze-studio/backend/infra/document/parser" @@ -65,6 +67,12 @@ func (k *KnowledgeApplicationService) CreateKnowledge(ctx context.Context, req * if uid == nil { return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) } + + err := k.checkPermission(ctx, uid, ptr.Of(req.SpaceID), nil, nil, nil) + if err != nil { + return dataset.NewCreateDatasetResponse(), err + } + createReq := service.CreateKnowledgeRequest{ Name: req.Name, Description: req.Description, @@ -115,6 +123,16 @@ func (k *KnowledgeApplicationService) DatasetDetail(ctx context.Context, req *da var err error var datasetIDs []int64 + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + + err = k.checkPermission(ctx, uid, ptr.Of(req.SpaceID), nil, nil, nil) + if err != nil { + return dataset.NewDatasetDetailResponse(), err + } + datasetIDs, err = slices.TransformWithErrorCheck(req.GetDatasetIDs(), func(s string) (int64, error) { id, err := strconv.ParseInt(s, 10, 64) return id, err @@ -146,6 +164,12 @@ func (k *KnowledgeApplicationService) DatasetDetail(ctx context.Context, req *da } func (k *KnowledgeApplicationService) ListKnowledge(ctx context.Context, req *dataset.ListDatasetRequest) (*dataset.ListDatasetResponse, error) { + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + var err error var projectID int64 request := service.ListKnowledgeRequest{} @@ -178,6 +202,12 @@ func (k *KnowledgeApplicationService) ListKnowledge(ctx context.Context, req *da } if req.GetSpaceID() != 0 { request.SpaceID = &req.SpaceID + + err = k.checkPermission(ctx, uid, ptr.Of(req.SpaceID), nil, nil, nil) + if err != nil { + return dataset.NewListDatasetResponse(), err + } + } request.OrderType = &orderType @@ -219,7 +249,18 @@ func (k *KnowledgeApplicationService) ListKnowledge(ctx context.Context, req *da } func (k *KnowledgeApplicationService) DeleteKnowledge(ctx context.Context, req *dataset.DeleteDatasetRequest) (*dataset.DeleteDatasetResponse, error) { - err := k.DomainSVC.DeleteKnowledge(ctx, &service.DeleteKnowledgeRequest{ + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + + err := k.checkPermission(ctx, uid, nil, nil, ptr.Of(req.GetDatasetID()), nil) + if err != nil { + return nil, err + } + + err = k.DomainSVC.DeleteKnowledge(ctx, &service.DeleteKnowledgeRequest{ KnowledgeID: req.GetDatasetID(), }) if err != nil { @@ -241,6 +282,17 @@ func (k *KnowledgeApplicationService) DeleteKnowledge(ctx context.Context, req * } func (k *KnowledgeApplicationService) UpdateKnowledge(ctx context.Context, req *dataset.UpdateDatasetRequest) (*dataset.UpdateDatasetResponse, error) { + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + + err := k.checkPermission(ctx, uid, nil, nil, ptr.Of(req.GetDatasetID()), nil) + if err != nil { + return nil, err + } + now := time.Now().UnixMilli() updateReq := service.UpdateKnowledgeRequest{ KnowledgeID: req.GetDatasetID(), @@ -253,7 +305,7 @@ func (k *KnowledgeApplicationService) UpdateKnowledge(ctx context.Context, req * if req.Status != nil { updateReq.Status = ptr.Of(convertDatasetStatus2Entity(req.GetStatus())) } - err := k.DomainSVC.UpdateKnowledge(ctx, &updateReq) + err = k.DomainSVC.UpdateKnowledge(ctx, &updateReq) if err != nil { logs.CtxErrorf(ctx, "update knowledge failed, err: %v", err) return dataset.NewUpdateDatasetResponse(), err @@ -288,6 +340,11 @@ func (k *KnowledgeApplicationService) CreateDocument(ctx context.Context, req *d return dataset.NewCreateDocumentResponse(), errors.New("knowledge not found") } knowledgeInfo := listResp.KnowledgeList[0] + + if knowledgeInfo.CreatorID != *uid { + return dataset.NewCreateDocumentResponse(), errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "permission denied")) + } + documents := []*entity.Document{} if len(req.GetDocumentBases()) == 0 { return dataset.NewCreateDocumentResponse(), errors.New("document base is empty") @@ -345,6 +402,12 @@ func (k *KnowledgeApplicationService) CreateDocument(ctx context.Context, req *d } func (k *KnowledgeApplicationService) ListDocument(ctx context.Context, req *dataset.ListDocumentRequest) (*dataset.ListDocumentResponse, error) { + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + var limit int = int(req.GetSize()) var offset int = int(req.GetPage() * req.GetSize()) var err error @@ -374,6 +437,9 @@ func (k *KnowledgeApplicationService) ListDocument(ctx context.Context, req *dat resp.Total = int32(listResp.Total) resp.DocumentInfos = make([]*dataset.DocumentInfo, 0) for i := range documents { + if documents[i].CreatorID != *uid { + return dataset.NewListDocumentResponse(), errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "permission denied")) + } resp.DocumentInfos = append(resp.DocumentInfos, convertDocument2Model(documents[i])) } return resp, nil @@ -383,6 +449,26 @@ func (k *KnowledgeApplicationService) DeleteDocument(ctx context.Context, req *d if len(req.GetDocumentIds()) == 0 { return dataset.NewDeleteDocumentResponse(), errors.New("document ids is empty") } + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + + transformedIDs, err := slices.TransformWithErrorCheck(req.GetDocumentIds(), func(s string) (int64, error) { + id, err := strconv.ParseInt(s, 10, 64) + return id, err + }) + if err != nil { + logs.CtxErrorf(ctx, "convert string ids failed, err: %v", err) + return dataset.NewDeleteDocumentResponse(), err + } + + err = k.checkPermission(ctx, uid, nil, transformedIDs, nil, nil) + if err != nil { + return dataset.NewDeleteDocumentResponse(), err + } + for i := range req.GetDocumentIds() { docID, err := strconv.ParseInt(req.GetDocumentIds()[i], 10, 64) if err != nil { @@ -401,7 +487,17 @@ func (k *KnowledgeApplicationService) DeleteDocument(ctx context.Context, req *d } func (k *KnowledgeApplicationService) UpdateDocument(ctx context.Context, req *dataset.UpdateDocumentRequest) (*dataset.UpdateDocumentResponse, error) { - err := k.DomainSVC.UpdateDocument(ctx, &service.UpdateDocumentRequest{ + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + err := k.checkPermission(ctx, uid, nil, []int64{req.GetDocumentID()}, nil, nil) + if err != nil { + return dataset.NewUpdateDocumentResponse(), err + } + + err = k.DomainSVC.UpdateDocument(ctx, &service.UpdateDocumentRequest{ DocumentID: req.GetDocumentID(), DocumentName: req.DocumentName, TableInfo: &entity.TableInfo{ @@ -415,7 +511,66 @@ func (k *KnowledgeApplicationService) UpdateDocument(ctx context.Context, req *d return &dataset.UpdateDocumentResponse{}, nil } +func (k *KnowledgeApplicationService) checkPermission(ctx context.Context, uid *int64, spaceID *int64, documentIDs []int64, knowledgeID *int64, sliceIDs []int64) error { + + rd := []*permission.ResourceIdentifier{} + + if spaceID != nil { + rd = append(rd, &permission.ResourceIdentifier{ + Type: permission.ResourceTypeWorkspace, + ID: []int64{*spaceID}, + Action: permission.ActionRead, + }) + } + + if documentIDs != nil { + rd = append(rd, &permission.ResourceIdentifier{ + Type: permission.ResourceTypeKnowledgeDocument, + ID: documentIDs, + Action: permission.ActionRead, + }) + } + if sliceIDs != nil { + rd = append(rd, &permission.ResourceIdentifier{ + Type: permission.ResourceTypeKnowledgeSlice, + ID: sliceIDs, + Action: permission.ActionRead, + }) + } + + if knowledgeID != nil { + rd = append(rd, &permission.ResourceIdentifier{ + Type: permission.ResourceTypeKnowledge, + ID: []int64{*knowledgeID}, + Action: permission.ActionRead, + }) + } + + if len(rd) == 0 { + return nil + } + + checkResult, err := permission.DefaultSVC().CheckAuthz(ctx, &permission.CheckAuthzData{ + ResourceIdentifier: rd, + OperatorID: *uid, + }) + if err != nil { + logs.CtxErrorf(ctx, "check authz failed, err: %v", err) + return err + } + if checkResult.Decision != permission.Allow { + return errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "permission denied")) + } + + return nil +} + func (k *KnowledgeApplicationService) GetDocumentProgress(ctx context.Context, req *dataset.GetDocumentProgressRequest) (*dataset.GetDocumentProgressResponse, error) { + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } docIDs, err := slices.TransformWithErrorCheck(req.GetDocumentIds(), func(s string) (int64, error) { id, err := strconv.ParseInt(s, 10, 64) return id, err @@ -424,6 +579,11 @@ func (k *KnowledgeApplicationService) GetDocumentProgress(ctx context.Context, r logs.CtxErrorf(ctx, "convert string ids failed, err: %v", err) return dataset.NewGetDocumentProgressResponse(), err } + err = k.checkPermission(ctx, uid, nil, docIDs, nil, nil) + if err != nil { + return dataset.NewGetDocumentProgressResponse(), err + } + domainResp, err := k.DomainSVC.MGetDocumentProgress(ctx, &service.MGetDocumentProgressRequest{ DocumentIDs: docIDs, }) @@ -450,6 +610,10 @@ func (k *KnowledgeApplicationService) GetDocumentProgress(ctx context.Context, r } func (k *KnowledgeApplicationService) Resegment(ctx context.Context, req *dataset.ResegmentRequest) (*dataset.ResegmentResponse, error) { + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } resp := dataset.NewResegmentResponse() resp.DocumentInfos = make([]*dataset.DocumentInfo, 0) for i := range req.GetDocumentIds() { @@ -471,6 +635,10 @@ func (k *KnowledgeApplicationService) Resegment(ctx context.Context, req *datase logs.CtxErrorf(ctx, "resegment document failed, err: %v", err) return dataset.NewResegmentResponse(), err } + if resegmentResp.Document.Info.CreatorID != *uid { + return dataset.NewResegmentResponse(), errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "permission denied")) + } + resp.DocumentInfos = append(resp.DocumentInfos, &dataset.DocumentInfo{ Name: resegmentResp.Document.Name, DocumentID: resegmentResp.Document.ID, @@ -494,6 +662,9 @@ func (k *KnowledgeApplicationService) CreateSlice(ctx context.Context, req *data if len(listResp.Documents) != 1 { return dataset.NewCreateSliceResponse(), errors.New("document not found") } + if listResp.Documents[0].CreatorID != *uid { + return dataset.NewCreateSliceResponse(), errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "permission denied")) + } sliceEntity := &model.Slice{ Info: model.Info{ CreatorID: *uid, @@ -531,12 +702,28 @@ func (k *KnowledgeApplicationService) CreateSlice(ctx context.Context, req *data } func (k *KnowledgeApplicationService) DeleteSlice(ctx context.Context, req *dataset.DeleteSliceRequest) (*dataset.DeleteSliceResponse, error) { + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + + sliceIDs := make([]int64, 0, len(req.GetSliceIds())) for i := range req.GetSliceIds() { sliceID, err := strconv.ParseInt(req.GetSliceIds()[i], 10, 64) if err != nil { logs.CtxErrorf(ctx, "parse int failed, err: %v", err) return dataset.NewDeleteSliceResponse(), err } + sliceIDs = append(sliceIDs, sliceID) + } + + err := k.checkPermission(ctx, uid, nil, nil, nil, sliceIDs) + if err != nil { + logs.CtxErrorf(ctx, "check permission failed, err: %v", err) + return dataset.NewDeleteSliceResponse(), err + } + for i := range sliceIDs { + sliceID := sliceIDs[i] err = k.DomainSVC.DeleteSlice(ctx, &service.DeleteSliceRequest{ SliceID: sliceID, }) @@ -559,6 +746,10 @@ func (k *KnowledgeApplicationService) UpdateSlice(ctx context.Context, req *data if err != nil { return nil, errorx.New(errno.ErrKnowledgeInvalidParamCode, errorx.KV("msg", "slice not found")) } + + if getSliceResp.Slice != nil && getSliceResp.Slice.Info.CreatorID != *uid { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "permission denied")) + } docID := getSliceResp.Slice.DocumentID listResp, err := k.DomainSVC.ListDocument(ctx, &service.ListDocumentRequest{ @@ -646,6 +837,19 @@ func packTableSliceColumnData(ctx context.Context, slice *model.Slice, text stri } func (k *KnowledgeApplicationService) ListSlice(ctx context.Context, req *dataset.ListSliceRequest) (*dataset.ListSliceResponse, error) { + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + + if req.DatasetID != nil { + err := k.checkPermission(ctx, uid, nil, nil, ptr.Of(*req.DatasetID), nil) + if err != nil { + return nil, err + } + } + listResp, err := k.DomainSVC.ListSlice(ctx, &service.ListSliceRequest{ KnowledgeID: req.DatasetID, DocumentID: req.DocumentID, @@ -684,6 +888,17 @@ func (k *KnowledgeApplicationService) GetTableSchema(ctx context.Context, req *d domainResp *service.TableSchemaResponse err error ) + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + + if req.GetDocumentID() > 0 { + err = k.checkPermission(ctx, uid, nil, []int64{req.GetDocumentID()}, nil, nil) + if err != nil { + return nil, err + } + } if req.SourceFile == nil { // alter table domainResp, err = k.DomainSVC.GetAlterTableSchema(ctx, &service.AlterTableSchemaRequest{ @@ -751,6 +966,18 @@ func (k *KnowledgeApplicationService) ValidateTableSchema(ctx context.Context, r if srcInfo == nil { return nil, fmt.Errorf("source info not provided") } + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + if req.GetDocumentID() > 0 { + err = k.checkPermission(ctx, uid, nil, []int64{req.GetDocumentID()}, nil, nil) + if err != nil { + return nil, err + } + } + var tableSheet *entity.TableSheet if req.TableSheet != nil { tableSheet = &entity.TableSheet{ @@ -773,6 +1000,18 @@ func (k *KnowledgeApplicationService) ValidateTableSchema(ctx context.Context, r } func (k *KnowledgeApplicationService) GetDocumentTableInfo(ctx context.Context, req *document.GetDocumentTableInfoRequest) (*document.GetDocumentTableInfoResponse, error) { + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + if req.GetDocumentID() > 0 { + err := k.checkPermission(ctx, uid, nil, []int64{req.GetDocumentID()}, nil, nil) + if err != nil { + return nil, err + } + } + domainResp, err := k.DomainSVC.GetDocumentTableInfo(ctx, &service.GetDocumentTableInfoRequest{ DocumentID: req.DocumentID, SourceInfo: &service.TableSourceInfo{ @@ -804,6 +1043,13 @@ func (k *KnowledgeApplicationService) CreateDocumentReview(ctx context.Context, if uid == nil { return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) } + if req.GetDatasetID() > 0 { + err := k.checkPermission(ctx, uid, nil, nil, ptr.Of(req.GetDatasetID()), nil) + if err != nil { + return nil, err + } + } + createResp, err := k.DomainSVC.CreateDocumentReview(ctx, convertCreateDocReviewReq(req)) if err != nil { logs.CtxErrorf(ctx, "create document review failed, err: %v", err) @@ -838,6 +1084,12 @@ func (k *KnowledgeApplicationService) MGetDocumentReview(ctx context.Context, re logs.CtxErrorf(ctx, "parse int failed, err: %v", err) return dataset.NewMGetDocumentReviewResponse(), err } + + err = k.checkPermission(ctx, uid, nil, nil, ptr.Of(req.GetDatasetID()), nil) + if err != nil { + return nil, err + } + mGetResp, err := k.DomainSVC.MGetDocumentReview(ctx, &service.MGetDocumentReviewRequest{ KnowledgeID: req.GetDatasetID(), ReviewIDs: reviewIDs, @@ -867,7 +1119,12 @@ func (k *KnowledgeApplicationService) SaveDocumentReview(ctx context.Context, re if uid == nil { return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) } - err := k.DomainSVC.SaveDocumentReview(ctx, &service.SaveDocumentReviewRequest{ + err := k.checkPermission(ctx, uid, nil, nil, ptr.Of(req.GetDatasetID()), nil) + if err != nil { + return nil, err + } + + err = k.DomainSVC.SaveDocumentReview(ctx, &service.SaveDocumentReviewRequest{ KnowledgeID: req.GetDatasetID(), DocTreeJson: req.GetDocTreeJSON(), ReviewID: req.GetReviewID(), @@ -953,6 +1210,12 @@ func (k *KnowledgeApplicationService) UpdatePhotoCaption(ctx context.Context, re if uid == nil { return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) } + + err := k.checkPermission(ctx, uid, nil, []int64{req.DocumentID}, nil, nil) + if err != nil { + return nil, err + } + resp := dataset.NewUpdatePhotoCaptionResponse() listResp, err := k.DomainSVC.ListSlice(ctx, &service.ListSliceRequest{DocumentID: ptr.Of(req.DocumentID)}) if err != nil { @@ -999,8 +1262,17 @@ func (k *KnowledgeApplicationService) MoveKnowledgeToLibrary(ctx context.Context return nil } func (k *KnowledgeApplicationService) ListPhoto(ctx context.Context, req *dataset.ListPhotoRequest) (*dataset.ListPhotoResponse, error) { + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + + err := k.checkPermission(ctx, uid, nil, nil, ptr.Of(req.GetDatasetID()), nil) + if err != nil { + return nil, err + } + resp := dataset.NewListPhotoResponse() - var err error var offset int if req.GetPage() >= 1 { offset = int(req.GetSize() * (req.GetPage() - 1)) @@ -1071,6 +1343,17 @@ func (k *KnowledgeApplicationService) PhotoDetail(ctx context.Context, req *data resp.Msg = "document ids is empty" return resp, nil } + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + + err := k.checkPermission(ctx, uid, nil, nil, ptr.Of(req.GetDatasetID()), nil) + if err != nil { + return nil, err + } + docIDs, err := slices.TransformWithErrorCheck(req.GetDocumentIds(), func(s string) (int64, error) { id, err := strconv.ParseInt(s, 10, 64) return id, err @@ -1084,15 +1367,12 @@ func (k *KnowledgeApplicationService) PhotoDetail(ctx context.Context, req *data logs.CtxErrorf(ctx, "list photo slice failed, err: %v", err) return resp, err } - listDocResp, err := k.DomainSVC.ListDocument(ctx, &service.ListDocumentRequest{DocumentIDs: docIDs, SelectAll: true}) - if err != nil { - logs.CtxErrorf(ctx, "get documents by slice ids failed, err: %v", err) - return resp, err - } + listDocResp, err := k.DomainSVC.ListDocument(ctx, &service.ListDocumentRequest{DocumentIDs: docIDs, SelectAll: true, KnowledgeID: req.GetDatasetID()}) if err != nil { logs.CtxErrorf(ctx, "get documents by slice ids failed, err: %v", err) return resp, err } + photos := k.packPhotoInfo(listResp.Slices, listDocResp.Documents) sort.SliceStable(photos, func(i, j int) bool { return photos[i].UpdateTime > photos[j].UpdateTime @@ -1110,6 +1390,16 @@ func (k *KnowledgeApplicationService) ExtractPhotoCaption(ctx context.Context, r resp.Msg = "document id is empty" return resp, nil } + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required")) + } + + err := k.checkPermission(ctx, uid, nil, []int64{req.GetDocumentID()}, nil, nil) + if err != nil { + return nil, err + } + extractResp, err := k.DomainSVC.ExtractPhotoCaption(ctx, &service.ExtractPhotoCaptionRequest{DocumentID: req.GetDocumentID()}) if err != nil { return resp, err diff --git a/backend/application/memory/database.go b/backend/application/memory/database.go index 4b5e3a918..c85aa8b5b 100644 --- a/backend/application/memory/database.go +++ b/backend/application/memory/database.go @@ -103,6 +103,11 @@ func (d *DatabaseApplicationService) ListDatabase(ctx context.Context, req *tabl } func (d *DatabaseApplicationService) GetDatabaseByID(ctx context.Context, req *table.SingleDatabaseRequest) (*table.SingleDatabaseResponse, error) { + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required")) + } + basics := make([]*model.DatabaseBasic, 1) b := &model.DatabaseBasic{ ID: req.ID, @@ -127,6 +132,10 @@ func (d *DatabaseApplicationService) GetDatabaseByID(ctx context.Context, req *t return nil, fmt.Errorf("database %d not found", req.GetID()) } + if res.Databases[0].CreatorID != *uid { + return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "creator id is invalid")) + } + return ConvertDatabaseRes(res.Databases[0]), nil } @@ -353,6 +362,11 @@ func (d *DatabaseApplicationService) UpdateDatabaseRecords(ctx context.Context, } func (d *DatabaseApplicationService) GetOnlineDatabaseId(ctx context.Context, req *table.GetOnlineDatabaseIdRequest) (*table.GetOnlineDatabaseIdResponse, error) { + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required")) + } + basics := make([]*model.DatabaseBasic, 1) basics[0] = &model.DatabaseBasic{ ID: req.ID, @@ -370,6 +384,10 @@ func (d *DatabaseApplicationService) GetOnlineDatabaseId(ctx context.Context, re return nil, fmt.Errorf("database %d not found", req.ID) } + if res.Databases[0].CreatorID != *uid { + return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "creator id is invalid")) + } + return &table.GetOnlineDatabaseIdResponse{ ID: res.Databases[0].OnlineID, @@ -544,6 +562,12 @@ func (d *DatabaseApplicationService) GetConnectorName(ctx context.Context, req * } func (d *DatabaseApplicationService) GetBotDatabase(ctx context.Context, req *table.GetBotTableRequest) (*table.GetBotTableResponse, error) { + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required")) + } + relationResp, err := d.DomainSVC.MGetRelationsByAgentID(ctx, &database.MGetRelationsByAgentIDRequest{ AgentID: req.GetBotID(), TableType: req.GetTableType(), @@ -564,6 +588,11 @@ func (d *DatabaseApplicationService) GetBotDatabase(ctx context.Context, req *ta if err != nil { return nil, err } + for _, db := range resp.Databases { + if db.CreatorID != *uid { + return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "creator id is invalid")) + } + } return &table.GetBotTableResponse{ BotTableList: convertToBotTableList(resp.Databases, req.GetBotID(), relationMap), @@ -650,6 +679,11 @@ func (d *DatabaseApplicationService) GetDatabaseTableSchema(ctx context.Context, tableType = req.GetTableDataType() } + err := d.ValidateAccess(ctx, req.GetDatabaseID(), table.TableType_OnlineTable) + if err != nil { + return nil, err + } + schema, err := d.DomainSVC.GetDatabaseTableSchema(ctx, &database.GetDatabaseTableSchemaRequest{ DatabaseID: req.GetDatabaseID(), UserID: *uid, diff --git a/backend/application/memory/variables.go b/backend/application/memory/variables.go index a972210f0..e1300c31a 100644 --- a/backend/application/memory/variables.go +++ b/backend/application/memory/variables.go @@ -26,9 +26,11 @@ import ( "github.com/coze-dev/coze-studio/backend/api/model/data/variable/kvmemory" "github.com/coze-dev/coze-studio/backend/api/model/data/variable/project_memory" "github.com/coze-dev/coze-studio/backend/application/base/ctxutil" + crosspermission "github.com/coze-dev/coze-studio/backend/crossdomain/permission" model "github.com/coze-dev/coze-studio/backend/crossdomain/variables/model" "github.com/coze-dev/coze-studio/backend/domain/memory/variables/entity" variables "github.com/coze-dev/coze-studio/backend/domain/memory/variables/service" + "github.com/coze-dev/coze-studio/backend/domain/permission" "github.com/coze-dev/coze-studio/backend/pkg/errorx" "github.com/coze-dev/coze-studio/backend/pkg/i18n" "github.com/coze-dev/coze-studio/backend/pkg/lang/ternary" @@ -213,7 +215,28 @@ func (v *VariableApplicationService) UpdateProjectVariable(ctx context.Context, req.UserID = *uid } - // TODO: project owner check + projectID, err := strconv.ParseInt(req.ProjectID, 10, 64) + if err != nil { + return nil, err + } + + checkResult, err := crosspermission.DefaultSVC().CheckAuthz(ctx, &permission.CheckAuthzData{ + OperatorID: *uid, + ResourceIdentifier: []*permission.ResourceIdentifier{ + { + Type: permission.ResourceTypeApp, + ID: []int64{projectID}, + Action: permission.ActionRead, + }, + }, + }) + if err != nil { + return nil, err + } + + if checkResult.Decision != permission.Allow { + return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "no permission")) + } sysVars := v.DomainSVC.GetSysVariableConf(ctx).ToVariables() @@ -259,7 +282,7 @@ func (v *VariableApplicationService) UpdateProjectVariable(ctx context.Context, } } - _, err := v.DomainSVC.UpsertProjectMeta(ctx, req.ProjectID, "", req.UserID, entity.NewVariables(list)) + _, err = v.DomainSVC.UpsertProjectMeta(ctx, req.ProjectID, "", req.UserID, entity.NewVariables(list)) if err != nil { return nil, err } @@ -327,6 +350,15 @@ func (v *VariableApplicationService) GetPlayGroundMemory(ctx context.Context, re connectId := ternary.IFElse(req.ConnectorID == nil, consts.CozeConnectorID, req.GetConnectorID()) connectorUID := ternary.IFElse(req.UserID == 0, *uid, req.UserID) + resourceID, err := strconv.ParseInt(bizID, 10, 64) + if err != nil { + return nil, err + } + err = v.checkPermission(ctx, uid, isProjectKV, resourceID, permission.ActionRead) + if err != nil { + return nil, err + } + e := entity.NewUserVariableMeta(&model.UserVariableMeta{ BizType: bizType, BizID: bizID, @@ -345,6 +377,32 @@ func (v *VariableApplicationService) GetPlayGroundMemory(ctx context.Context, re }, nil } +func (v *VariableApplicationService) checkPermission(ctx context.Context, uid *int64, isProjectKV bool, resourceID int64, action permission.Action) error { + checkResult, err := crosspermission.DefaultSVC().CheckAuthz(ctx, &permission.CheckAuthzData{ + OperatorID: *uid, + ResourceIdentifier: []*permission.ResourceIdentifier{ + { + Type: func() permission.ResourceType { + if isProjectKV { + return permission.ResourceTypeApp + } + return permission.ResourceTypeAgent + }(), + ID: []int64{resourceID}, + Action: action, + }, + }, + }) + if err != nil { + return err + } + + if checkResult.Decision != permission.Allow { + return errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "no permission")) + } + return nil +} + func (v *VariableApplicationService) SetVariableInstance(ctx context.Context, req *kvmemory.SetKvMemoryReq) (*kvmemory.SetKvMemoryResp, error) { uid := ctxutil.GetUIDFromCtx(ctx) if uid == nil { @@ -363,6 +421,15 @@ func (v *VariableApplicationService) SetVariableInstance(ctx context.Context, re connectId := ternary.IFElse(req.ConnectorID == nil, consts.CozeConnectorID, req.GetConnectorID()) connectorUID := ternary.IFElse(req.GetUserID() == 0, *uid, req.GetUserID()) + resourceID, err := strconv.ParseInt(bizID, 10, 64) + if err != nil { + return nil, err + } + err = v.checkPermission(ctx, uid, isProjectKV, resourceID, permission.ActionRead) + if err != nil { + return nil, err + } + e := entity.NewUserVariableMeta(&model.UserVariableMeta{ BizType: bizType, BizID: bizID, diff --git a/backend/application/permission/init.go b/backend/application/permission/init.go new file mode 100644 index 000000000..c50e0ace5 --- /dev/null +++ b/backend/application/permission/init.go @@ -0,0 +1,36 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package permission + +import ( + "github.com/coze-dev/coze-studio/backend/domain/permission" +) + +type ServiceComponents struct { +} + +type PermissionApplicationService struct { + DomainSVC permission.Permission +} + +func InitService(components *ServiceComponents) *PermissionApplicationService { + domainSVC := permission.NewService() + + return &PermissionApplicationService{ + DomainSVC: domainSVC, + } +} diff --git a/backend/application/plugin/lifecycle.go b/backend/application/plugin/lifecycle.go index 50873d970..a16aa63a8 100644 --- a/backend/application/plugin/lifecycle.go +++ b/backend/application/plugin/lifecycle.go @@ -23,6 +23,7 @@ import ( pluginAPI "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop" common "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop/common" resCommon "github.com/coze-dev/coze-studio/backend/api/model/resource/common" + "github.com/coze-dev/coze-studio/backend/application/base/ctxutil" "github.com/coze-dev/coze-studio/backend/crossdomain/plugin/model" "github.com/coze-dev/coze-studio/backend/domain/plugin/dto" "github.com/coze-dev/coze-studio/backend/domain/plugin/entity" @@ -30,6 +31,7 @@ import ( "github.com/coze-dev/coze-studio/backend/pkg/errorx" "github.com/coze-dev/coze-studio/backend/pkg/lang/ptr" "github.com/coze-dev/coze-studio/backend/pkg/logs" + "github.com/coze-dev/coze-studio/backend/types/errno" ) func (p *PluginApplicationService) PublishPlugin(ctx context.Context, req *pluginAPI.PublishPluginRequest) (resp *pluginAPI.PublishPluginResponse, err error) { @@ -110,6 +112,12 @@ func (p *PluginApplicationService) GetPluginNextVersion(ctx context.Context, req } func (p *PluginApplicationService) GetDevPluginList(ctx context.Context, req *pluginAPI.GetDevPluginListRequest) (resp *pluginAPI.GetDevPluginListResponse, err error) { + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "session is required")) + } + pageInfo := dto.PageInfo{ Name: req.Name, Page: int(req.GetPage()), @@ -133,6 +141,10 @@ func (p *PluginApplicationService) GetDevPluginList(ctx context.Context, req *pl pluginList := make([]*common.PluginInfoForPlayground, 0, len(res.Plugins)) for _, pl := range res.Plugins { + + if pl.DeveloperID > 0 && pl.DeveloperID != *uid { + return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "plugin developer is not current user")) + } tools, err := p.toolRepo.GetPluginAllDraftTools(ctx, pl.ID) if err != nil { return nil, errorx.Wrapf(err, "GetPluginAllDraftTools failed, pluginID=%d", pl.ID) diff --git a/backend/application/prompt/prompt.go b/backend/application/prompt/prompt.go index b6aff2b01..538b7b81f 100644 --- a/backend/application/prompt/prompt.go +++ b/backend/application/prompt/prompt.go @@ -97,10 +97,19 @@ func (p *PromptApplicationService) UpsertPromptResource(ctx context.Context, req func (p *PromptApplicationService) GetPromptResourceInfo(ctx context.Context, req *playground.GetPromptResourceInfoRequest) ( resp *playground.GetPromptResourceInfoResponse, err error, ) { + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrPromptPermissionCode, errorx.KV("msg", "no session data provided")) + } + promptInfo, err := p.DomainSVC.GetPromptResource(ctx, req.GetPromptResourceID()) if err != nil { return nil, err } + if promptInfo.CreatorID != *uid { + return nil, errorx.New(errno.ErrPromptPermissionCode, errorx.KV("msg", "no permission")) + } return &playground.GetPromptResourceInfoResponse{ Data: promptInfoDo2To(promptInfo), diff --git a/backend/application/search/resource_search.go b/backend/application/search/resource_search.go index 4c0e98fac..87bdf29e5 100644 --- a/backend/application/search/resource_search.go +++ b/backend/application/search/resource_search.go @@ -124,6 +124,9 @@ func (s *SearchApplicationService) LibraryResourceList(ctx context.Context, req if res == nil { continue } + if res.CreatorID != nil && *res.CreatorID != *userID { + return nil, errorx.New(errno.ErrSearchPermissionCode, errorx.KV("msg", "user can't search resources created by themselves")) + } filterResource = append(filterResource, res) } @@ -276,6 +279,12 @@ func (s *SearchApplicationService) getAPPAllResources(ctx context.Context, appID } func (s *SearchApplicationService) packAPPResources(ctx context.Context, resources []*entity.ResourceDocument) ([]*common.ProjectResourceGroup, error) { + + uid := ctxutil.GetUIDFromCtx(ctx) + if uid == nil { + return nil, errorx.New(errno.ErrSearchPermissionCode, errorx.KV(errno.PluginMsgKey, "session is required")) + } + workflowGroup := &common.ProjectResourceGroup{ GroupType: common.ProjectResourceGroupType_Workflow, ResourceList: []*common.ProjectResourceInfo{}, @@ -294,6 +303,10 @@ func (s *SearchApplicationService) packAPPResources(ctx context.Context, resourc for idx := range resources { v := resources[idx] + if v.OwnerID != nil && *v.OwnerID != *uid { + return nil, errorx.New(errno.ErrSearchPermissionCode, errorx.KV("msg", "user can't search resources created by others")) + } + tasks.Go(func() error { ri, err := s.packProjectResource(ctx, v) if err != nil { diff --git a/backend/application/shortcutcmd/shortcut_cmd.go b/backend/application/shortcutcmd/shortcut_cmd.go index b67798797..592ec2b1d 100644 --- a/backend/application/shortcutcmd/shortcut_cmd.go +++ b/backend/application/shortcutcmd/shortcut_cmd.go @@ -18,10 +18,13 @@ package shortcutcmd import ( "context" + "fmt" "strconv" "github.com/coze-dev/coze-studio/backend/api/model/playground" "github.com/coze-dev/coze-studio/backend/application/base/ctxutil" + + "github.com/coze-dev/coze-studio/backend/domain/permission" "github.com/coze-dev/coze-studio/backend/domain/shortcutcmd/entity" "github.com/coze-dev/coze-studio/backend/domain/shortcutcmd/service" "github.com/coze-dev/coze-studio/backend/pkg/lang/conv" @@ -31,13 +34,54 @@ type ShortcutCmdApplicationService struct { ShortCutDomainSVC service.ShortcutCmd } +func checkPermission(ctx context.Context, uid int64, spaceID int64, workflowID int64) error { + // Use permission service to check workspace access + + rd := []*permission.ResourceIdentifier{ + { + Type: permission.ResourceTypeWorkspace, + ID: []int64{spaceID}, + Action: permission.ActionRead, + }, + } + if workflowID > 0 { + rd = append(rd, &permission.ResourceIdentifier{ + Type: permission.ResourceTypeWorkflow, + ID: []int64{workflowID}, + Action: permission.ActionRead, + }) + } + + result, err := permission.DefaultSVC().CheckAuthz(ctx, &permission.CheckAuthzData{ + ResourceIdentifier: rd, + OperatorID: uid, + }) + if err != nil { + return fmt.Errorf("failed to check workspace permission: %w", err) + } + + if result.Decision != permission.Allow { + return fmt.Errorf("user %d does not have access to space %d", uid, spaceID) + } + + return nil +} + func (s *ShortcutCmdApplicationService) Handler(ctx context.Context, req *playground.CreateUpdateShortcutCommandRequest) (*playground.ShortcutCommand, error) { + var err error + uid := ctxutil.MustGetUIDFromCtx(ctx) + cr, buildErr := s.buildReq(ctx, req) if buildErr != nil { return nil, buildErr } - var err error + + err = checkPermission(ctx, uid, req.GetSpaceID(), cr.WorkFlowID) + if err != nil { + return nil, err + } + var cmdDO *entity.ShortcutCmd if cr.CommandID > 0 { cmdDO, err = s.ShortCutDomainSVC.UpdateCMD(ctx, cr) diff --git a/backend/application/singleagent/get.go b/backend/application/singleagent/get.go index 74505103a..4986f3d35 100644 --- a/backend/application/singleagent/get.go +++ b/backend/application/singleagent/get.go @@ -26,6 +26,7 @@ import ( "github.com/coze-dev/coze-studio/backend/api/model/playground" "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop/common" "github.com/coze-dev/coze-studio/backend/api/model/workflow" + "github.com/coze-dev/coze-studio/backend/application/base/ctxutil" "github.com/coze-dev/coze-studio/backend/bizpkg/config" "github.com/coze-dev/coze-studio/backend/bizpkg/config/modelmgr" knowledgeModel "github.com/coze-dev/coze-studio/backend/crossdomain/knowledge/model" @@ -47,6 +48,9 @@ import ( ) func (s *SingleAgentApplicationService) GetAgentBotInfo(ctx context.Context, req *playground.GetDraftBotInfoAgwRequest) (*playground.GetDraftBotInfoAgwResponse, error) { + + uid := ctxutil.MustGetUIDFromCtx(ctx) + agentInfo, err := s.DomainSVC.GetSingleAgent(ctx, req.GetBotID(), req.GetVersion()) if err != nil { return nil, err @@ -56,6 +60,14 @@ func (s *SingleAgentApplicationService) GetAgentBotInfo(ctx context.Context, req return nil, errorx.New(errno.ErrAgentInvalidParamCode, errorx.KVf("msg", "agent %d not found", req.GetBotID())) } + + + + + if agentInfo.CreatorID != uid { + return nil, errorx.New(errno.ErrAgentInvalidParamCode, errorx.KVf("msg", "agent %d not found", req.GetBotID())) + } + vo, err := s.singleAgentDraftDo2Vo(ctx, agentInfo) if err != nil { return nil, err diff --git a/backend/application/singleagent/single_agent.go b/backend/application/singleagent/single_agent.go index 938c8c344..b9ee956c7 100644 --- a/backend/application/singleagent/single_agent.go +++ b/backend/application/singleagent/single_agent.go @@ -36,6 +36,7 @@ import ( crossdatabase "github.com/coze-dev/coze-studio/backend/crossdomain/database" database "github.com/coze-dev/coze-studio/backend/crossdomain/database/model" pluginConsts "github.com/coze-dev/coze-studio/backend/crossdomain/plugin/consts" + crossuser "github.com/coze-dev/coze-studio/backend/crossdomain/user" "github.com/coze-dev/coze-studio/backend/domain/agent/singleagent/entity" singleagent "github.com/coze-dev/coze-studio/backend/domain/agent/singleagent/service" variableEntity "github.com/coze-dev/coze-studio/backend/domain/memory/variables/entity" @@ -624,7 +625,36 @@ func (s *SingleAgentApplicationService) ListAgentPublishHistory(ctx context.Cont return resp, nil } +func checkUserSpace(ctx context.Context, uid int64, spaceID int64) error { + spaces, err := crossuser.DefaultSVC().GetUserSpaceList(ctx, uid) + if err != nil { + return err + } + + var match bool + for _, s := range spaces { + if s.ID == spaceID { + match = true + break + } + } + + if !match { + return fmt.Errorf("user %d does not have access to space %d", uid, spaceID) + } + + return nil +} + func (s *SingleAgentApplicationService) ReportUserBehavior(ctx context.Context, req *playground.ReportUserBehaviorRequest) (resp *playground.ReportUserBehaviorResponse, err error) { + + uid := ctxutil.MustGetUIDFromCtx(ctx) + + err = checkUserSpace(ctx, uid, req.GetSpaceID()) + if err != nil { + return nil, err + } + err = s.appContext.EventBus.PublishProject(ctx, &searchEntity.ProjectDomainEvent{ OpType: searchEntity.Updated, Project: &searchEntity.ProjectDocument{ diff --git a/backend/application/workflow/chatflow.go b/backend/application/workflow/chatflow.go index f80505cfb..5b90c210c 100644 --- a/backend/application/workflow/chatflow.go +++ b/backend/application/workflow/chatflow.go @@ -33,12 +33,14 @@ import ( "github.com/coze-dev/coze-studio/backend/application/base/ctxutil" "github.com/coze-dev/coze-studio/backend/bizpkg/debugutil" crossagentrun "github.com/coze-dev/coze-studio/backend/crossdomain/agentrun" + crossconversation "github.com/coze-dev/coze-studio/backend/crossdomain/conversation" crossmessage "github.com/coze-dev/coze-studio/backend/crossdomain/message" message "github.com/coze-dev/coze-studio/backend/crossdomain/message/model" crossupload "github.com/coze-dev/coze-studio/backend/crossdomain/upload" workflowModel "github.com/coze-dev/coze-studio/backend/crossdomain/workflow/model" agententity "github.com/coze-dev/coze-studio/backend/domain/conversation/agentrun/entity" + "github.com/coze-dev/coze-studio/backend/domain/permission" "github.com/coze-dev/coze-studio/backend/domain/upload/service" "github.com/coze-dev/coze-studio/backend/domain/workflow/entity" "github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo" @@ -469,6 +471,41 @@ func (w *ApplicationService) ListApplicationConversationDef(ctx context.Context, return resp, nil } +func checkPermission(ctx context.Context, userID int64, workflowID int64, appID *int64, agentID *int64) error { + rd := []*permission.ResourceIdentifier{ + { + Type: permission.ResourceTypeWorkflow, + ID: []int64{workflowID}, + Action: permission.ActionRead, + }, + } + if appID != nil { + rd = append(rd, &permission.ResourceIdentifier{ + Type: permission.ResourceTypeApp, + ID: []int64{*appID}, + Action: permission.ActionRead, + }) + } + if agentID != nil { + rd = append(rd, &permission.ResourceIdentifier{ + Type: permission.ResourceTypeAgent, + ID: []int64{*agentID}, + Action: permission.ActionRead, + }) + } + + checkResult, err := permission.DefaultSVC().CheckAuthz(ctx, &permission.CheckAuthzData{ + OperatorID: userID, + ResourceIdentifier: rd, + }) + if err != nil { + return err + } + if checkResult.Decision != permission.Allow { + return errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "no permission")) + } + return nil +} func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workflow.ChatFlowRunRequest) ( _ *schema.StreamReader[[]*workflow.ChatFlowRunResponse], err error) { @@ -520,6 +557,11 @@ func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workfl connectorID = mustParseInt64(req.GetConnectorID()) } + err = checkPermission(ctx, userID, workflowID, appID, agentID) + if err != nil { + return nil, err + } + if req.IsSetAppID() { appID = ptr.Of(mustParseInt64(req.GetAppID())) bizID = mustParseInt64(req.GetAppID()) @@ -1222,7 +1264,22 @@ func (w *ApplicationService) OpenAPICreateConversation(ctx context.Context, req //_ = spaceID ) - // todo check permission + checkResult, err := permission.DefaultSVC().CheckAuthz(ctx, &permission.CheckAuthzData{ + OperatorID: userID, + ResourceIdentifier: []*permission.ResourceIdentifier{ + { + Type: permission.ResourceTypeWorkflow, + ID: []int64{mustParseInt64(req.GetWorkflowID())}, + Action: permission.ActionRead, + }, + }, + }) + if err != nil { + return nil, err + } + if checkResult.Decision != permission.Allow { + return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "no permission")) + } if !req.GetGetOrCreate() { cID, err = GetWorkflowDomainSVC().UpdateConversation(ctx, env, appID, req.GetConnectorId(), userID, req.GetConversationMame()) diff --git a/backend/application/workflow/workflow.go b/backend/application/workflow/workflow.go index 9b737d2ec..ad79a6ac0 100644 --- a/backend/application/workflow/workflow.go +++ b/backend/application/workflow/workflow.go @@ -45,9 +45,11 @@ import ( "github.com/coze-dev/coze-studio/backend/bizpkg/debugutil" crossknowledge "github.com/coze-dev/coze-studio/backend/crossdomain/knowledge" model "github.com/coze-dev/coze-studio/backend/crossdomain/knowledge/model" + crosspermission "github.com/coze-dev/coze-studio/backend/crossdomain/permission" pluginConsts "github.com/coze-dev/coze-studio/backend/crossdomain/plugin/consts" crossuser "github.com/coze-dev/coze-studio/backend/crossdomain/user" workflowModel "github.com/coze-dev/coze-studio/backend/crossdomain/workflow/model" + "github.com/coze-dev/coze-studio/backend/domain/permission" "github.com/coze-dev/coze-studio/backend/domain/plugin/dto" search "github.com/coze-dev/coze-studio/backend/domain/search/entity" domainWorkflow "github.com/coze-dev/coze-studio/backend/domain/workflow" @@ -1623,6 +1625,25 @@ func (w *ApplicationService) OpenAPIStreamResume(ctx context.Context, req *workf apiKeyInfo := ctxutil.GetApiAuthFromCtx(ctx) userID := apiKeyInfo.UserID + checkResult, err := crosspermission.DefaultSVC().CheckAuthz(ctx, &permission.CheckAuthzData{ + OperatorID: userID, + ResourceIdentifier: []*permission.ResourceIdentifier{ + { + Type: permission.ResourceTypeWorkflow, + ID: []int64{workflowID}, + Action: permission.ActionRead, + }, + }, + }) + + if err != nil { + return nil, err + } + + if checkResult.Decision != permission.Allow { + return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "no permission")) + } + var connectorID int64 if req.IsSetConnectorID() { connectorID = mustParseInt64(req.GetConnectorID()) diff --git a/backend/crossdomain/agent/contract.go b/backend/crossdomain/agent/contract.go index a77d5b8c4..e2aebeb45 100644 --- a/backend/crossdomain/agent/contract.go +++ b/backend/crossdomain/agent/contract.go @@ -33,6 +33,7 @@ type SingleAgent interface { StreamExecute(ctx context.Context, agentRuntime *AgentRuntime) (*schema.StreamReader[*model.AgentEvent], error) ObtainAgentByIdentity(ctx context.Context, identity *model.AgentIdentity) (*model.SingleAgent, error) + GetSingleAgentDraft(ctx context.Context, agentID int64) (agentInfo *model.SingleAgent, err error) } type AgentRuntime struct { diff --git a/backend/crossdomain/agent/impl/single_agent.go b/backend/crossdomain/agent/impl/single_agent.go index 80db61de7..4140f1731 100644 --- a/backend/crossdomain/agent/impl/single_agent.go +++ b/backend/crossdomain/agent/impl/single_agent.go @@ -106,3 +106,14 @@ func (c *impl) ObtainAgentByIdentity(ctx context.Context, identity *model.AgentI } return agentInfo.SingleAgent, nil } + +func (c *impl) GetSingleAgentDraft(ctx context.Context, agentID int64) (*model.SingleAgent, error) { + agentInfo, err := c.DomainSVC.GetSingleAgentDraft(ctx, agentID) + if err != nil { + return nil, err + } + if agentInfo == nil { + return nil, nil + } + return agentInfo.SingleAgent, nil +} diff --git a/backend/crossdomain/app/contract.go b/backend/crossdomain/app/contract.go new file mode 100644 index 000000000..961bf3173 --- /dev/null +++ b/backend/crossdomain/app/contract.go @@ -0,0 +1,37 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "context" + + "github.com/coze-dev/coze-studio/backend/domain/app/entity" +) + +var defaultSVC AppService + +func DefaultSVC() AppService { + return defaultSVC +} + +func SetDefaultSVC(c AppService) { + defaultSVC = c +} + +type AppService interface { + GetDraftAPP(ctx context.Context, appID int64) (app *entity.APP, err error) +} diff --git a/backend/crossdomain/app/impl/app.go b/backend/crossdomain/app/impl/app.go new file mode 100644 index 000000000..5309e62fa --- /dev/null +++ b/backend/crossdomain/app/impl/app.go @@ -0,0 +1,39 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package crossapp + +import ( + "context" + + crossapp "github.com/coze-dev/coze-studio/backend/crossdomain/app" + "github.com/coze-dev/coze-studio/backend/domain/app/entity" + "github.com/coze-dev/coze-studio/backend/domain/app/service" +) + +type appServiceImpl struct { + DomainSVC service.AppService +} + +func InitDomainService(domainSVC service.AppService) crossapp.AppService { + return &appServiceImpl{ + DomainSVC: domainSVC, + } +} + +func (a *appServiceImpl) GetDraftAPP(ctx context.Context, appID int64) (app *entity.APP, err error) { + return a.DomainSVC.GetDraftAPP(ctx, appID) +} diff --git a/backend/crossdomain/app/model/app.go b/backend/crossdomain/app/model/app.go new file mode 100644 index 000000000..d8fc2595e --- /dev/null +++ b/backend/crossdomain/app/model/app.go @@ -0,0 +1,129 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import ( + "github.com/coze-dev/coze-studio/backend/api/model/app/intelligence/common" + publishAPI "github.com/coze-dev/coze-studio/backend/api/model/app/intelligence/publish" + resourceCommon "github.com/coze-dev/coze-studio/backend/api/model/resource/common" + "github.com/coze-dev/coze-studio/backend/pkg/lang/ptr" +) + +type APP struct { + ID int64 + SpaceID int64 + IconURI *string + Name *string + Desc *string + OwnerID int64 + + ConnectorIDs []int64 + Version *string + VersionDesc *string + PublishRecordID *int64 + PublishStatus *PublishStatus + PublishExtraInfo *PublishRecordExtraInfo + + CreatedAtMS int64 + UpdatedAtMS int64 + PublishedAtMS *int64 +} + +func (a APP) Published() bool { + return a.PublishStatus != nil && *a.PublishStatus == PublishStatusOfPublishDone +} + +func (a APP) GetPublishedAtMS() int64 { + return ptr.FromOrDefault(a.PublishedAtMS, 0) +} + +func (a APP) GetVersion() string { + return ptr.FromOrDefault(a.Version, "") +} + +func (a APP) GetName() string { + return ptr.FromOrDefault(a.Name, "") +} + +func (a APP) GetDesc() string { + return ptr.FromOrDefault(a.Desc, "") +} + +func (a APP) GetVersionDesc() string { + return ptr.FromOrDefault(a.VersionDesc, "") +} + +func (a APP) GetIconURI() string { + return ptr.FromOrDefault(a.IconURI, "") +} + +func (a APP) GetPublishStatus() PublishStatus { + return ptr.FromOrDefault(a.PublishStatus, 0) +} + +func (a APP) GetPublishRecordID() int64 { + return ptr.FromOrDefault(a.PublishRecordID, 0) +} + +type PublishRecord struct { + APP *APP + ConnectorPublishRecords []*ConnectorPublishRecord +} + +type PublishRecordExtraInfo struct { + PackFailedInfo []*PackResourceFailedInfo `json:"pack_failed_info,omitempty"` +} + +func (p *PublishRecordExtraInfo) ToVO() *publishAPI.PublishRecordStatusDetail { + if p == nil || len(p.PackFailedInfo) == 0 { + return &publishAPI.PublishRecordStatusDetail{} + } + + packFailedDetail := make([]*publishAPI.PackFailedDetail, 0, len(p.PackFailedInfo)) + for _, info := range p.PackFailedInfo { + packFailedDetail = append(packFailedDetail, &publishAPI.PackFailedDetail{ + EntityID: info.ResID, + EntityType: common.ResourceType(info.ResType), + EntityName: info.ResName, + }) + } + + return &publishAPI.PublishRecordStatusDetail{ + PackFailedDetail: packFailedDetail, + } +} + +type PackResourceFailedInfo struct { + ResID int64 `json:"res_id"` + ResType resourceCommon.ResType `json:"res_type"` + ResName string `json:"res_name"` +} + +type ResourceCopyResult struct { + ResID int64 `json:"res_id"` + ResType ResourceType `json:"res_type"` + ResName string `json:"res_name"` + CopyStatus ResourceCopyStatus `json:"copy_status"` + CopyScene resourceCommon.ResourceCopyScene `json:"copy_scene"` + FailedReason string `json:"reason"` +} + +type Resource struct { + ResID int64 + ResType ResourceType + ResName string +} diff --git a/backend/crossdomain/app/model/connector.go b/backend/crossdomain/app/model/connector.go new file mode 100644 index 000000000..55def240f --- /dev/null +++ b/backend/crossdomain/app/model/connector.go @@ -0,0 +1,61 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import ( + publishAPI "github.com/coze-dev/coze-studio/backend/api/model/app/intelligence/publish" + "github.com/coze-dev/coze-studio/backend/types/consts" +) + +var ConnectorIDWhiteList = []int64{ + consts.WebSDKConnectorID, + consts.APIConnectorID, +} + +type ConnectorPublishRecord struct { + ConnectorID int64 `json:"connector_id"` + PublishStatus ConnectorPublishStatus `json:"publish_status"` + PublishConfig PublishConfig `json:"publish_config"` +} + +type PublishConfig struct { + SelectedWorkflows []*SelectedWorkflow `json:"selected_workflows,omitempty"` +} + +func (p *PublishConfig) ToVO() *publishAPI.ConnectorPublishConfig { + config := &publishAPI.ConnectorPublishConfig{ + SelectedWorkflows: make([]*publishAPI.SelectedWorkflow, 0, len(p.SelectedWorkflows)), + } + + if p == nil { + return config + } + + for _, w := range p.SelectedWorkflows { + config.SelectedWorkflows = append(config.SelectedWorkflows, &publishAPI.SelectedWorkflow{ + WorkflowID: w.WorkflowID, + WorkflowName: w.WorkflowName, + }) + } + + return config +} + +type SelectedWorkflow struct { + WorkflowID int64 `json:"workflow_id"` + WorkflowName string `json:"workflow_name"` +} diff --git a/backend/crossdomain/app/model/consts.go b/backend/crossdomain/app/model/consts.go new file mode 100644 index 000000000..bad29f6b0 --- /dev/null +++ b/backend/crossdomain/app/model/consts.go @@ -0,0 +1,55 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +type PublishStatus int + +const ( + PublishStatusOfPacking PublishStatus = 0 + PublishStatusOfPackFailed PublishStatus = 1 + PublishStatusOfAuditing PublishStatus = 2 + PublishStatusOfAuditNotPass PublishStatus = 3 + PublishStatusOfConnectorPublishing PublishStatus = 4 + PublishStatusOfPublishDone PublishStatus = 5 +) + +type ConnectorPublishStatus int + +const ( + ConnectorPublishStatusOfDefault ConnectorPublishStatus = 0 + ConnectorPublishStatusOfAuditing ConnectorPublishStatus = 1 + ConnectorPublishStatusOfSuccess ConnectorPublishStatus = 2 + ConnectorPublishStatusOfFailed ConnectorPublishStatus = 3 + ConnectorPublishStatusOfDisable ConnectorPublishStatus = 4 +) + +type ResourceType string + +const ( + ResourceTypeOfPlugin ResourceType = "plugin" + ResourceTypeOfWorkflow ResourceType = "workflow" + ResourceTypeOfKnowledge ResourceType = "knowledge" + ResourceTypeOfDatabase ResourceType = "database" +) + +type ResourceCopyStatus int + +const ( + ResourceCopyStatusOfSuccess ResourceCopyStatus = 1 + ResourceCopyStatusOfProcessing ResourceCopyStatus = 2 + ResourceCopyStatusOfFailed ResourceCopyStatus = 3 +) diff --git a/backend/crossdomain/knowledge/contract.go b/backend/crossdomain/knowledge/contract.go index f1691d595..8e5ad425e 100644 --- a/backend/crossdomain/knowledge/contract.go +++ b/backend/crossdomain/knowledge/contract.go @@ -32,6 +32,8 @@ type Knowledge interface { Store(ctx context.Context, document *model.CreateDocumentRequest) (*model.CreateDocumentResponse, error) Delete(ctx context.Context, r *model.DeleteDocumentRequest) (*model.DeleteDocumentResponse, error) ListKnowledgeDetail(ctx context.Context, req *model.ListKnowledgeDetailRequest) (*model.ListKnowledgeDetailResponse, error) + MGetSlice(ctx context.Context, request *model.MGetSliceRequest) (response *model.MGetSliceResponse, err error) + MGetDocument(ctx context.Context, request *model.MGetDocumentRequest) (response *model.MGetDocumentResponse, err error) } var defaultSVC Knowledge diff --git a/backend/crossdomain/knowledge/impl/knowledge.go b/backend/crossdomain/knowledge/impl/knowledge.go index bb05ffb19..f8fe74a9e 100644 --- a/backend/crossdomain/knowledge/impl/knowledge.go +++ b/backend/crossdomain/knowledge/impl/knowledge.go @@ -181,6 +181,49 @@ func (i *impl) ListKnowledgeDetail(ctx context.Context, req *model.ListKnowledge return resp, nil } +func (i *impl) MGetSlice(ctx context.Context, request *model.MGetSliceRequest) (response *model.MGetSliceResponse, err error) { + resp, err := i.DomainSVC.MGetSlice(ctx, &service.MGetSliceRequest{ + SliceIDs: request.SliceIDs, + }) + if err != nil { + return nil, err + } + + return &model.MGetSliceResponse{ + Slices: slices.Transform(resp.Slices, func(a *entity.Slice) *model.Slice { + return &model.Slice{ + Info: model.Info{ + ID: a.ID, + CreatorID: a.CreatorID, + SpaceID: a.SpaceID, + }, + KnowledgeID: a.KnowledgeID, + DocumentID: a.DocumentID, + } + }), + }, nil +} + +func (i *impl) MGetDocument(ctx context.Context, request *model.MGetDocumentRequest) (response *model.MGetDocumentResponse, err error) { + resp, err := i.DomainSVC.MGetDocument(ctx, &service.MGetDocumentRequest{ + DocumentIDs: request.DocumentIDs, + }) + if err != nil { + return nil, err + } + + return &model.MGetDocumentResponse{ + Documents: slices.Transform(resp.Documents, func(a *entity.Document) *model.Document { + return &model.Document{ + ID: a.ID, + Name: a.Name, + CreatorID: a.CreatorID, + SpaceID: a.SpaceID, + } + }), + }, nil +} + func toChunkType(typ model.ChunkType) (parser.ChunkType, error) { switch typ { case model.ChunkTypeDefault: diff --git a/backend/crossdomain/knowledge/knowledgemock/knowledge_mock.go b/backend/crossdomain/knowledge/knowledgemock/knowledge_mock.go index 5644c0050..00961b0a9 100644 --- a/backend/crossdomain/knowledge/knowledgemock/knowledge_mock.go +++ b/backend/crossdomain/knowledge/knowledgemock/knowledge_mock.go @@ -115,6 +115,21 @@ func (mr *MockKnowledgeMockRecorder) ListKnowledgeDetail(ctx, req any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListKnowledgeDetail", reflect.TypeOf((*MockKnowledge)(nil).ListKnowledgeDetail), ctx, req) } +// MGetDocument mocks base method. +func (m *MockKnowledge) MGetDocument(ctx context.Context, request *knowledge.MGetDocumentRequest) (*knowledge.MGetDocumentResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MGetDocument", ctx, request) + ret0, _ := ret[0].(*knowledge.MGetDocumentResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MGetDocument indicates an expected call of MGetDocument. +func (mr *MockKnowledgeMockRecorder) MGetDocument(ctx, request any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MGetDocument", reflect.TypeOf((*MockKnowledge)(nil).MGetDocument), ctx, request) +} + // MGetKnowledgeByID mocks base method. func (m *MockKnowledge) MGetKnowledgeByID(ctx context.Context, request *knowledge.MGetKnowledgeByIDRequest) (*knowledge.MGetKnowledgeByIDResponse, error) { m.ctrl.T.Helper() @@ -130,6 +145,21 @@ func (mr *MockKnowledgeMockRecorder) MGetKnowledgeByID(ctx, request any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MGetKnowledgeByID", reflect.TypeOf((*MockKnowledge)(nil).MGetKnowledgeByID), ctx, request) } +// MGetSlice mocks base method. +func (m *MockKnowledge) MGetSlice(ctx context.Context, request *knowledge.MGetSliceRequest) (*knowledge.MGetSliceResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MGetSlice", ctx, request) + ret0, _ := ret[0].(*knowledge.MGetSliceResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MGetSlice indicates an expected call of MGetSlice. +func (mr *MockKnowledgeMockRecorder) MGetSlice(ctx, request any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MGetSlice", reflect.TypeOf((*MockKnowledge)(nil).MGetSlice), ctx, request) +} + // Retrieve mocks base method. func (m *MockKnowledge) Retrieve(ctx context.Context, req *knowledge.RetrieveRequest) (*knowledge.RetrieveResponse, error) { m.ctrl.T.Helper() diff --git a/backend/crossdomain/knowledge/model/knowledge.go b/backend/crossdomain/knowledge/model/knowledge.go index 6cd777561..dcf3bdc23 100644 --- a/backend/crossdomain/knowledge/model/knowledge.go +++ b/backend/crossdomain/knowledge/model/knowledge.go @@ -347,3 +347,26 @@ type ListKnowledgeDetailRequest struct { type ListKnowledgeDetailResponse struct { KnowledgeDetails []*KnowledgeDetail } + +type MGetSliceRequest struct { + SliceIDs []int64 +} + +type MGetSliceResponse struct { + Slices []*Slice +} + +type MGetDocumentRequest struct { + DocumentIDs []int64 +} + +type Document struct { + ID int64 + Name string + CreatorID int64 + SpaceID int64 +} + +type MGetDocumentResponse struct { + Documents []*Document +} diff --git a/backend/crossdomain/permission/contract.go b/backend/crossdomain/permission/contract.go new file mode 100644 index 000000000..d232336e0 --- /dev/null +++ b/backend/crossdomain/permission/contract.go @@ -0,0 +1,37 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package permission + +import ( + "context" + + "github.com/coze-dev/coze-studio/backend/crossdomain/permission/model" +) + +var defaultSVC Permission + +func DefaultSVC() Permission { + return defaultSVC +} + +func SetDefaultSVC(c Permission) { + defaultSVC = c +} + +type Permission interface { + CheckAuthz(ctx context.Context, req *model.CheckAuthzData) (*model.CheckAuthzResult, error) +} diff --git a/backend/crossdomain/permission/impl/permission.go b/backend/crossdomain/permission/impl/permission.go new file mode 100644 index 000000000..ff56ee1f6 --- /dev/null +++ b/backend/crossdomain/permission/impl/permission.go @@ -0,0 +1,39 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package crosspermission + +import ( + "context" + + crosspermission "github.com/coze-dev/coze-studio/backend/crossdomain/permission" + "github.com/coze-dev/coze-studio/backend/crossdomain/permission/model" + "github.com/coze-dev/coze-studio/backend/domain/permission" +) + +type impl struct { + DomainSVC permission.Permission +} + +func InitDomainService(domainSVC permission.Permission) crosspermission.Permission { + return &impl{ + DomainSVC: domainSVC, + } +} + +func (i *impl) CheckAuthz(ctx context.Context, req *model.CheckAuthzData) (*model.CheckAuthzResult, error) { + return i.DomainSVC.CheckAuthz(ctx, req) +} diff --git a/backend/crossdomain/permission/model/permission.go b/backend/crossdomain/permission/model/permission.go new file mode 100644 index 000000000..303319ed0 --- /dev/null +++ b/backend/crossdomain/permission/model/permission.go @@ -0,0 +1,22 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import "github.com/coze-dev/coze-studio/backend/domain/permission" + +type CheckAuthzData = permission.CheckAuthzData +type CheckAuthzResult = permission.CheckAuthzResult diff --git a/backend/crossdomain/permission/permissionmock/permission_mock.go b/backend/crossdomain/permission/permissionmock/permission_mock.go new file mode 100644 index 000000000..175e14e4b --- /dev/null +++ b/backend/crossdomain/permission/permissionmock/permission_mock.go @@ -0,0 +1,57 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: contract.go +// +// Generated by this command: +// +// mockgen -destination permissionmock/permission_mock.go --package permissionmock -source contract.go +// + +// Package permissionmock is a generated GoMock package. +package permissionmock + +import ( + context "context" + reflect "reflect" + + model "github.com/coze-dev/coze-studio/backend/crossdomain/permission/model" + gomock "go.uber.org/mock/gomock" +) + +// MockPermission is a mock of Permission interface. +type MockPermission struct { + ctrl *gomock.Controller + recorder *MockPermissionMockRecorder + isgomock struct{} +} + +// MockPermissionMockRecorder is the mock recorder for MockPermission. +type MockPermissionMockRecorder struct { + mock *MockPermission +} + +// NewMockPermission creates a new mock instance. +func NewMockPermission(ctrl *gomock.Controller) *MockPermission { + mock := &MockPermission{ctrl: ctrl} + mock.recorder = &MockPermissionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPermission) EXPECT() *MockPermissionMockRecorder { + return m.recorder +} + +// CheckAuthz mocks base method. +func (m *MockPermission) CheckAuthz(ctx context.Context, req *model.CheckAuthzData) (*model.CheckAuthzResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckAuthz", ctx, req) + ret0, _ := ret[0].(*model.CheckAuthzResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CheckAuthz indicates an expected call of CheckAuthz. +func (mr *MockPermissionMockRecorder) CheckAuthz(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckAuthz", reflect.TypeOf((*MockPermission)(nil).CheckAuthz), ctx, req) +} diff --git a/backend/crossdomain/user/contract.go b/backend/crossdomain/user/contract.go index 40e9ca334..acf057b77 100644 --- a/backend/crossdomain/user/contract.go +++ b/backend/crossdomain/user/contract.go @@ -24,9 +24,10 @@ import ( type EntitySpace = entity.Space -//go:generate mockgen -destination ../../../internal/mock/crossdomain/crossuser/crossuser.go --package mockCrossUser -source crossuser.go +//go:generate mockgen -destination ../../internal/mock/crossdomain/crossuser/crossuser.go --package mockCrossUser -source contract.go type User interface { GetUserSpaceList(ctx context.Context, userID int64) (spaces []*EntitySpace, err error) + GetUserSpaceBySpaceID(ctx context.Context, spaceID []int64) (space []*EntitySpace, err error) } var defaultSVC User diff --git a/backend/crossdomain/user/impl/user.go b/backend/crossdomain/user/impl/user.go index a18be4dfc..0908cd15b 100644 --- a/backend/crossdomain/user/impl/user.go +++ b/backend/crossdomain/user/impl/user.go @@ -40,3 +40,7 @@ func InitDomainService(u service.User) crossuser.User { func (u *impl) GetUserSpaceList(ctx context.Context, userID int64) (spaces []*entity.Space, err error) { return u.DomainSVC.GetUserSpaceList(ctx, userID) } + +func (u *impl) GetUserSpaceBySpaceID(ctx context.Context, spaceID []int64) (space []*entity.Space, err error) { + return u.DomainSVC.GetUserSpaceBySpaceID(ctx, spaceID) +} diff --git a/backend/crossdomain/workflow/contract.go b/backend/crossdomain/workflow/contract.go index ee5b49853..d947197c0 100644 --- a/backend/crossdomain/workflow/contract.go +++ b/backend/crossdomain/workflow/contract.go @@ -45,6 +45,7 @@ type Workflow interface { WithMessagePipe() (compose.Option, *schema.StreamReader[*entity.Message], func()) StreamResume(ctx context.Context, req *entity.ResumeRequest, config workflowModel.ExecuteConfig) (*schema.StreamReader[*entity.Message], error) InitApplicationDefaultConversationTemplate(ctx context.Context, spaceID int64, appID int64, userID int64) error + MGet(ctx context.Context, policy *vo.MGetPolicy) ([]*entity.Workflow, int64, error) } type ExecuteConfig = workflowModel.ExecuteConfig diff --git a/backend/crossdomain/workflow/impl/workflow.go b/backend/crossdomain/workflow/impl/workflow.go index 8121fea30..4e6ab984f 100644 --- a/backend/crossdomain/workflow/impl/workflow.go +++ b/backend/crossdomain/workflow/impl/workflow.go @@ -95,3 +95,7 @@ func (i *impl) GetWorkflowIDsByAppID(ctx context.Context, appID int64) ([]int64, return a.ID }), err } + +func (i *impl) MGet(ctx context.Context, policy *vo.MGetPolicy) ([]*entity.Workflow, int64, error) { + return i.DomainSVC.MGet(ctx, policy) +} diff --git a/backend/domain/app/entity/app.go b/backend/domain/app/entity/app.go index 5dfd12a02..e9dc6b8d9 100644 --- a/backend/domain/app/entity/app.go +++ b/backend/domain/app/entity/app.go @@ -17,113 +17,17 @@ package entity import ( - "github.com/coze-dev/coze-studio/backend/api/model/app/intelligence/common" - publishAPI "github.com/coze-dev/coze-studio/backend/api/model/app/intelligence/publish" - resourceCommon "github.com/coze-dev/coze-studio/backend/api/model/resource/common" - "github.com/coze-dev/coze-studio/backend/pkg/lang/ptr" + "github.com/coze-dev/coze-studio/backend/crossdomain/app/model" ) -type APP struct { - ID int64 - SpaceID int64 - IconURI *string - Name *string - Desc *string - OwnerID int64 +type APP = model.APP - ConnectorIDs []int64 - Version *string - VersionDesc *string - PublishRecordID *int64 - PublishStatus *PublishStatus - PublishExtraInfo *PublishRecordExtraInfo +type PublishRecord = model.PublishRecord - CreatedAtMS int64 - UpdatedAtMS int64 - PublishedAtMS *int64 -} +type PublishRecordExtraInfo = model.PublishRecordExtraInfo -func (a APP) Published() bool { - return a.PublishStatus != nil && *a.PublishStatus == PublishStatusOfPublishDone -} +type PackResourceFailedInfo = model.PackResourceFailedInfo -func (a APP) GetPublishedAtMS() int64 { - return ptr.FromOrDefault(a.PublishedAtMS, 0) -} +type ResourceCopyResult = model.ResourceCopyResult -func (a APP) GetVersion() string { - return ptr.FromOrDefault(a.Version, "") -} - -func (a APP) GetName() string { - return ptr.FromOrDefault(a.Name, "") -} - -func (a APP) GetDesc() string { - return ptr.FromOrDefault(a.Desc, "") -} - -func (a APP) GetVersionDesc() string { - return ptr.FromOrDefault(a.VersionDesc, "") -} - -func (a APP) GetIconURI() string { - return ptr.FromOrDefault(a.IconURI, "") -} - -func (a APP) GetPublishStatus() PublishStatus { - return ptr.FromOrDefault(a.PublishStatus, 0) -} - -func (a APP) GetPublishRecordID() int64 { - return ptr.FromOrDefault(a.PublishRecordID, 0) -} - -type PublishRecord struct { - APP *APP - ConnectorPublishRecords []*ConnectorPublishRecord -} - -type PublishRecordExtraInfo struct { - PackFailedInfo []*PackResourceFailedInfo `json:"pack_failed_info,omitempty"` -} - -func (p *PublishRecordExtraInfo) ToVO() *publishAPI.PublishRecordStatusDetail { - if p == nil || len(p.PackFailedInfo) == 0 { - return &publishAPI.PublishRecordStatusDetail{} - } - - packFailedDetail := make([]*publishAPI.PackFailedDetail, 0, len(p.PackFailedInfo)) - for _, info := range p.PackFailedInfo { - packFailedDetail = append(packFailedDetail, &publishAPI.PackFailedDetail{ - EntityID: info.ResID, - EntityType: common.ResourceType(info.ResType), - EntityName: info.ResName, - }) - } - - return &publishAPI.PublishRecordStatusDetail{ - PackFailedDetail: packFailedDetail, - } -} - -type PackResourceFailedInfo struct { - ResID int64 `json:"res_id"` - ResType resourceCommon.ResType `json:"res_type"` - ResName string `json:"res_name"` -} - -type ResourceCopyResult struct { - ResID int64 `json:"res_id"` - ResType ResourceType `json:"res_type"` - ResName string `json:"res_name"` - CopyStatus ResourceCopyStatus `json:"copy_status"` - CopyScene resourceCommon.ResourceCopyScene `json:"copy_scene"` - FailedReason string `json:"reason"` -} - -type Resource struct { - ResID int64 - ResType ResourceType - ResName string -} +type Resource = model.Resource diff --git a/backend/domain/app/entity/connector.go b/backend/domain/app/entity/connector.go index 21956430e..2b01ef61a 100644 --- a/backend/domain/app/entity/connector.go +++ b/backend/domain/app/entity/connector.go @@ -17,7 +17,7 @@ package entity import ( - publishAPI "github.com/coze-dev/coze-studio/backend/api/model/app/intelligence/publish" + "github.com/coze-dev/coze-studio/backend/crossdomain/app/model" "github.com/coze-dev/coze-studio/backend/types/consts" ) @@ -26,36 +26,6 @@ var ConnectorIDWhiteList = []int64{ consts.APIConnectorID, } -type ConnectorPublishRecord struct { - ConnectorID int64 `json:"connector_id"` - PublishStatus ConnectorPublishStatus `json:"publish_status"` - PublishConfig PublishConfig `json:"publish_config"` -} - -type PublishConfig struct { - SelectedWorkflows []*SelectedWorkflow `json:"selected_workflows,omitempty"` -} - -func (p *PublishConfig) ToVO() *publishAPI.ConnectorPublishConfig { - config := &publishAPI.ConnectorPublishConfig{ - SelectedWorkflows: make([]*publishAPI.SelectedWorkflow, 0, len(p.SelectedWorkflows)), - } - - if p == nil { - return config - } - - for _, w := range p.SelectedWorkflows { - config.SelectedWorkflows = append(config.SelectedWorkflows, &publishAPI.SelectedWorkflow{ - WorkflowID: w.WorkflowID, - WorkflowName: w.WorkflowName, - }) - } - - return config -} - -type SelectedWorkflow struct { - WorkflowID int64 `json:"workflow_id"` - WorkflowName string `json:"workflow_name"` -} +type ConnectorPublishRecord = model.ConnectorPublishRecord +type PublishConfig = model.PublishConfig +type SelectedWorkflow = model.SelectedWorkflow diff --git a/backend/domain/app/entity/consts.go b/backend/domain/app/entity/consts.go index b27bdb902..e9f98d259 100644 --- a/backend/domain/app/entity/consts.go +++ b/backend/domain/app/entity/consts.go @@ -16,7 +16,9 @@ package entity -type PublishStatus int +import "github.com/coze-dev/coze-studio/backend/crossdomain/app/model" + +type PublishStatus = model.PublishStatus const ( PublishStatusOfPacking PublishStatus = 0 @@ -27,7 +29,7 @@ const ( PublishStatusOfPublishDone PublishStatus = 5 ) -type ConnectorPublishStatus int +type ConnectorPublishStatus = model.ConnectorPublishStatus const ( ConnectorPublishStatusOfDefault ConnectorPublishStatus = 0 @@ -37,7 +39,7 @@ const ( ConnectorPublishStatusOfDisable ConnectorPublishStatus = 4 ) -type ResourceType string +type ResourceType = model.ResourceType const ( ResourceTypeOfPlugin ResourceType = "plugin" @@ -46,7 +48,7 @@ const ( ResourceTypeOfDatabase ResourceType = "database" ) -type ResourceCopyStatus int +type ResourceCopyStatus = model.ResourceCopyStatus const ( ResourceCopyStatusOfSuccess ResourceCopyStatus = 1 diff --git a/backend/domain/knowledge/service/interface.go b/backend/domain/knowledge/service/interface.go index b8923993f..efc3226ea 100644 --- a/backend/domain/knowledge/service/interface.go +++ b/backend/domain/knowledge/service/interface.go @@ -57,6 +57,8 @@ type Knowledge interface { ListSlice(ctx context.Context, request *ListSliceRequest) (response *ListSliceResponse, err error) ListPhotoSlice(ctx context.Context, request *ListPhotoSliceRequest) (response *ListPhotoSliceResponse, err error) GetSlice(ctx context.Context, request *GetSliceRequest) (response *GetSliceResponse, err error) + MGetSlice(ctx context.Context, request *MGetSliceRequest) (response *MGetSliceResponse, err error) + MGetDocument(ctx context.Context, request *MGetDocumentRequest) (response *MGetDocumentResponse, err error) Retrieve(ctx context.Context, request *RetrieveRequest) (response *RetrieveResponse, err error) CreateDocumentReview(ctx context.Context, request *CreateDocumentReviewRequest) (response *CreateDocumentReviewResponse, err error) MGetDocumentReview(ctx context.Context, request *MGetDocumentReviewRequest) (response *MGetDocumentReviewResponse, err error) @@ -354,5 +356,21 @@ type ExtractPhotoCaptionRequest struct { type ExtractPhotoCaptionResponse struct { Caption string } + +type MGetSliceRequest struct { + SliceIDs []int64 +} + +type MGetSliceResponse struct { + Slices []*entity.Slice +} + +type MGetDocumentRequest struct { + DocumentIDs []int64 +} + +type MGetDocumentResponse struct { + Documents []*entity.Document +} type MGetKnowledgeByIDRequest = knowledge.MGetKnowledgeByIDRequest type MGetKnowledgeByIDResponse = knowledge.MGetKnowledgeByIDResponse diff --git a/backend/domain/knowledge/service/knowledge.go b/backend/domain/knowledge/service/knowledge.go index 1dffd39fc..35dfef790 100644 --- a/backend/domain/knowledge/service/knowledge.go +++ b/backend/domain/knowledge/service/knowledge.go @@ -1503,3 +1503,45 @@ func (k *knowledgeSVC) genMultiIDs(ctx context.Context, counts int) ([]int64, er } return allIDs, nil } + +func (k *knowledgeSVC) MGetSlice(ctx context.Context, request *MGetSliceRequest) (response *MGetSliceResponse, err error) { + slices, err := k.sliceRepo.MGetSlices(ctx, request.SliceIDs) + if err != nil { + return nil, err + } + + var result []*entity.Slice + for _, slice := range slices { + if slice != nil { + result = append(result, k.fromModelSlice(ctx, slice)) + } + } + + return &MGetSliceResponse{ + Slices: result, + }, nil +} + +func (k *knowledgeSVC) MGetDocument(ctx context.Context, request *MGetDocumentRequest) (response *MGetDocumentResponse, err error) { + documents, err := k.documentRepo.MGetByID(ctx, request.DocumentIDs) + if err != nil { + return nil, err + } + + var result []*entity.Document + for _, doc := range documents { + if doc != nil { + docEntity, err := k.fromModelDocument(ctx, doc) + if err != nil { + return nil, err + } + if docEntity != nil { + result = append(result, docEntity) + } + } + } + + return &MGetDocumentResponse{ + Documents: result, + }, nil +} diff --git a/backend/domain/permission/authz_checker.go b/backend/domain/permission/authz_checker.go new file mode 100644 index 000000000..5ff283dee --- /dev/null +++ b/backend/domain/permission/authz_checker.go @@ -0,0 +1,124 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package permission + +import ( + "context" + "fmt" +) + +type ResourcePermissionRequest struct { + ResourceType ResourceType + ResourceIDs []int64 + Action Action + OperatorID int64 + IsDraft *bool +} + +type ResourceInfo struct { + ID int64 + CreatorID int64 + SpaceID *int64 +} + +type ResourceQueryer interface { + QueryResourceInfo(ctx context.Context, resourceIDs []int64, isDraft *bool) ([]*ResourceInfo, error) + + GetResourceType() ResourceType +} + +type AuthzChecker struct { + resourceQueryers map[ResourceType]ResourceQueryer +} + +func NewAuthzChecker() *AuthzChecker { + checker := &AuthzChecker{ + resourceQueryers: make(map[ResourceType]ResourceQueryer), + } + + checker.registerResourceQueryers() + + return checker +} + +func (c *AuthzChecker) registerResourceQueryers() { + + c.resourceQueryers[ResourceTypeWorkspace] = NewWorkspaceResourceQueryer() + + c.resourceQueryers[ResourceTypeAgent] = NewAgentResourceQueryer() + + c.resourceQueryers[ResourceTypePlugin] = NewPluginResourceQueryer() + + c.resourceQueryers[ResourceTypeWorkflow] = NewWorkflowResourceQueryer() + + c.resourceQueryers[ResourceTypeKnowledge] = NewKnowledgeResourceQueryer() + c.resourceQueryers[ResourceTypeKnowledgeSlice] = NewKnowledgeSliceResourceQueryer() + c.resourceQueryers[ResourceTypeKnowledgeDocument] = NewKnowledgeDocumentResourceQueryer() + + c.resourceQueryers[ResourceTypeDatabase] = NewDatabaseResourceQueryer() + + c.resourceQueryers[ResourceTypeApp] = NewAppResourceQueryer() + +} + +func (c *AuthzChecker) CheckResourcePermission(ctx context.Context, req *ResourcePermissionRequest) (bool, error) { + + queryeer, exists := c.resourceQueryers[req.ResourceType] + if !exists { + return false, fmt.Errorf("unsupported resource type: %d", req.ResourceType) + } + + resourceInfos, err := queryeer.QueryResourceInfo(ctx, req.ResourceIDs, req.IsDraft) + if err != nil { + return false, fmt.Errorf("failed to query resource info: %w", err) + } + + for _, resourceInfo := range resourceInfos { + allowed := c.checkSingleResourcePermission(req.OperatorID, resourceInfo, req.Action) + if !allowed { + return false, nil + } + } + + return true, nil +} + +func (c *AuthzChecker) checkSingleResourcePermission(operatorID int64, resourceInfo *ResourceInfo, action Action) bool { + + if operatorID == resourceInfo.CreatorID { + return true + } + + switch action { + case ActionRead: + return c.checkReadPermission(operatorID, resourceInfo) + case ActionWrite: + return c.checkWritePermission(operatorID, resourceInfo) + default: + return false + } +} + +func (c *AuthzChecker) checkReadPermission(operatorID int64, resourceInfo *ResourceInfo) bool { + + return operatorID == resourceInfo.CreatorID +} + +func (c *AuthzChecker) checkWritePermission(operatorID int64, resourceInfo *ResourceInfo) bool { + + return operatorID == resourceInfo.CreatorID +} diff --git a/backend/domain/permission/consts.go b/backend/domain/permission/consts.go index 43e3b925c..d6b3e3877 100644 --- a/backend/domain/permission/consts.go +++ b/backend/domain/permission/consts.go @@ -16,11 +16,17 @@ package permission +type ( + ResourceType int + Decision int + Action string +) + const ( ResourceTypeAccount ResourceType = 1 ResourceTypeWorkspace = 2 ResourceTypeApp = 3 - ResourceTypeBot = 4 + ResourceTypeAgent = 4 ResourceTypePlugin = 5 ResourceTypeWorkflow = 6 ResourceTypeKnowledge = 7 @@ -42,6 +48,8 @@ const ( ResourceTypeDatabase = 23 ResourceTypeOceanProject = 24 ResourceTypeFinetuneTask = 25 + ResourceTypeKnowledgeDocument = 26 + ResourceTypeKnowledgeSlice = 27 ) const ( @@ -50,3 +58,8 @@ const ( // Deny represents permission denied Deny Decision = 2 ) + +const ( + ActionRead Action = "read" + ActionWrite Action = "write" +) diff --git a/backend/domain/permission/permission.go b/backend/domain/permission/permission.go index ee98d64e1..91a17c3a2 100644 --- a/backend/domain/permission/permission.go +++ b/backend/domain/permission/permission.go @@ -20,33 +20,26 @@ import ( "context" ) -type ( - ResourceType int - Decision int -) - type ResourceIdentifier struct { - Type ResourceType - ID string + Type ResourceType + ID []int64 + Action Action } type ActionAndResource struct { - Action string + Action Action ResourceIdentifier ResourceIdentifier } -type CheckPermissionRequest struct { - IdentityTicket string - ActionAndResources []ActionAndResource +type CheckAuthzData struct { + ResourceIdentifier []*ResourceIdentifier + OperatorID int64 + IsDraft *bool } - -type CheckPermissionResponse struct { +type CheckAuthzResult struct { Decision Decision } type Permission interface { - CheckPermission(ctx context.Context, req *CheckPermissionRequest) (*CheckPermissionResponse, error) - CheckSingleAgentOperatePermission(ctx context.Context, botID, spaceID int64) (bool, error) - CheckSpaceOperatePermission(ctx context.Context, spaceID int64, path, ticket string) (bool, error) - UserSpaceCheck(ctx context.Context, spaceId, userId int64) (bool, error) + CheckAuthz(ctx context.Context, req *CheckAuthzData) (*CheckAuthzResult, error) } diff --git a/backend/domain/permission/permission_impl.go b/backend/domain/permission/permission_impl.go index 6c4cfefed..201f2dbca 100644 --- a/backend/domain/permission/permission_impl.go +++ b/backend/domain/permission/permission_impl.go @@ -26,18 +26,30 @@ func NewService() Permission { return &permissionImpl{} } -func (p *permissionImpl) CheckPermission(ctx context.Context, req *CheckPermissionRequest) (*CheckPermissionResponse, error) { - return &CheckPermissionResponse{Decision: 0}, nil +func DefaultSVC() Permission { + return NewService() } -func (p *permissionImpl) CheckSingleAgentOperatePermission(ctx context.Context, botID, spaceID int64) (bool, error) { - return true, nil -} +func (p *permissionImpl) CheckAuthz(ctx context.Context, req *CheckAuthzData) (*CheckAuthzResult, error) { -func (p *permissionImpl) CheckSpaceOperatePermission(ctx context.Context, spaceID int64, path, ticket string) (bool, error) { - return true, nil -} + authzChecker := NewAuthzChecker() -func (p *permissionImpl) UserSpaceCheck(ctx context.Context, spaceId, userId int64) (bool, error) { - return true, nil + for _, resourceIdentifier := range req.ResourceIdentifier { + allowed, err := authzChecker.CheckResourcePermission(ctx, &ResourcePermissionRequest{ + ResourceType: resourceIdentifier.Type, + ResourceIDs: resourceIdentifier.ID, + Action: resourceIdentifier.Action, + OperatorID: req.OperatorID, + IsDraft: req.IsDraft, + }) + if err != nil { + return nil, err + } + + if !allowed { + return &CheckAuthzResult{Decision: Deny}, nil + } + } + + return &CheckAuthzResult{Decision: Allow}, nil } diff --git a/backend/domain/permission/resource_queryiers.go b/backend/domain/permission/resource_queryiers.go new file mode 100644 index 000000000..c392ae69b --- /dev/null +++ b/backend/domain/permission/resource_queryiers.go @@ -0,0 +1,369 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package permission + +import ( + "context" + "fmt" + + "github.com/coze-dev/coze-studio/backend/api/model/data/database/table" + "github.com/coze-dev/coze-studio/backend/crossdomain/agent" + "github.com/coze-dev/coze-studio/backend/crossdomain/app" + "github.com/coze-dev/coze-studio/backend/crossdomain/database" + "github.com/coze-dev/coze-studio/backend/crossdomain/knowledge" + "github.com/coze-dev/coze-studio/backend/crossdomain/plugin" + "github.com/coze-dev/coze-studio/backend/crossdomain/user" + "github.com/coze-dev/coze-studio/backend/crossdomain/workflow" + "github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo" + + databaseModel "github.com/coze-dev/coze-studio/backend/crossdomain/database/model" + knowledgeModel "github.com/coze-dev/coze-studio/backend/crossdomain/knowledge/model" +) + +type AgentResourceQueryer struct { + agentService agent.SingleAgent +} + +func NewAgentResourceQueryer() *AgentResourceQueryer { + return &AgentResourceQueryer{ + agentService: agent.DefaultSVC(), + } +} + +func (q *AgentResourceQueryer) QueryResourceInfo(ctx context.Context, resourceIDs []int64, isDraft *bool) ([]*ResourceInfo, error) { + var result []*ResourceInfo + + for _, id := range resourceIDs { + agentInfo, err := q.agentService.GetSingleAgentDraft(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to query bot %d: %w", id, err) + } + + if agentInfo != nil { + result = append(result, &ResourceInfo{ + ID: id, + CreatorID: agentInfo.CreatorID, + SpaceID: &agentInfo.SpaceID, + }) + } + } + + return result, nil +} + +func (q *AgentResourceQueryer) GetResourceType() ResourceType { + return ResourceTypeAgent +} + +type PluginResourceQueryer struct { + pluginService plugin.PluginService +} + +func NewPluginResourceQueryer() *PluginResourceQueryer { + return &PluginResourceQueryer{ + pluginService: plugin.DefaultSVC(), + } +} + +func (q *PluginResourceQueryer) QueryResourceInfo(ctx context.Context, resourceIDs []int64, isDraft *bool) ([]*ResourceInfo, error) { + + plugins, err := q.pluginService.MGetDraftPlugins(ctx, resourceIDs) + if err != nil { + return nil, fmt.Errorf("failed to query draft plugins: %w", err) + } + var result []*ResourceInfo + for _, plugin := range plugins { + result = append(result, &ResourceInfo{ + ID: plugin.ID, + CreatorID: plugin.DeveloperID, + SpaceID: &plugin.SpaceID, + }) + } + + return result, nil +} + +func (q *PluginResourceQueryer) GetResourceType() ResourceType { + return ResourceTypePlugin +} + +type WorkflowResourceQueryer struct { + workflowService crossworkflow.Workflow +} + +func NewWorkflowResourceQueryer() *WorkflowResourceQueryer { + return &WorkflowResourceQueryer{ + workflowService: crossworkflow.DefaultSVC(), + } +} + +func (q *WorkflowResourceQueryer) QueryResourceInfo(ctx context.Context, resourceIDs []int64, isDraft *bool) ([]*ResourceInfo, error) { + + workflows, _, err := q.workflowService.MGet(ctx, &vo.MGetPolicy{ + + QType: crossworkflow.FromDraft, + MetaOnly: true, + MetaQuery: vo.MetaQuery{ + IDs: resourceIDs, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to query workflows: %w", err) + } + var result []*ResourceInfo + for _, workflow := range workflows { + result = append(result, &ResourceInfo{ + ID: workflow.ID, + CreatorID: workflow.CreatorID, + SpaceID: &workflow.SpaceID, + }) + } + + return result, nil +} + +func (q *WorkflowResourceQueryer) GetResourceType() ResourceType { + return ResourceTypeWorkflow +} + +type KnowledgeResourceQueryer struct { + knowledgeService knowledge.Knowledge +} + +func NewKnowledgeResourceQueryer() *KnowledgeResourceQueryer { + return &KnowledgeResourceQueryer{ + knowledgeService: knowledge.DefaultSVC(), + } +} + +func (q *KnowledgeResourceQueryer) QueryResourceInfo(ctx context.Context, resourceIDs []int64, isDraft *bool) ([]*ResourceInfo, error) { + + resp, err := q.knowledgeService.MGetKnowledgeByID(ctx, &knowledgeModel.MGetKnowledgeByIDRequest{ + KnowledgeIDs: resourceIDs, + }) + if err != nil { + return nil, fmt.Errorf("failed to query knowledge: %w", err) + } + + var result []*ResourceInfo + for _, knowledgeInfo := range resp.Knowledge { + if knowledgeInfo != nil { + result = append(result, &ResourceInfo{ + ID: knowledgeInfo.ID, + CreatorID: knowledgeInfo.CreatorID, + SpaceID: &knowledgeInfo.SpaceID, + }) + } + } + + return result, nil +} + +func (q *KnowledgeResourceQueryer) GetResourceType() ResourceType { + return ResourceTypeKnowledge +} + +type DatabaseResourceQueryer struct { + databaseService database.Database +} + +func NewDatabaseResourceQueryer() *DatabaseResourceQueryer { + return &DatabaseResourceQueryer{ + databaseService: database.DefaultSVC(), + } +} + +func (q *DatabaseResourceQueryer) QueryResourceInfo(ctx context.Context, resourceIDs []int64, isDraft *bool) ([]*ResourceInfo, error) { + var basics []*databaseModel.DatabaseBasic + for _, id := range resourceIDs { + basic := &databaseModel.DatabaseBasic{ + ID: id, + TableType: table.TableType_DraftTable, + } + if isDraft != nil && !*isDraft { + basic.TableType = table.TableType_OnlineTable + } + basics = append(basics, basic) + } + + resp, err := q.databaseService.MGetDatabase(ctx, &databaseModel.MGetDatabaseRequest{ + Basics: basics, + }) + if err != nil { + return nil, fmt.Errorf("failed to query database: %w", err) + } + + var result []*ResourceInfo + for _, dbInfo := range resp.Databases { + if dbInfo != nil { + result = append(result, &ResourceInfo{ + ID: dbInfo.ID, + CreatorID: dbInfo.CreatorID, + SpaceID: &dbInfo.SpaceID, + }) + } + } + + return result, nil +} + +func (q *DatabaseResourceQueryer) GetResourceType() ResourceType { + return ResourceTypeDatabase +} + +type AppResourceQueryer struct { + appService app.AppService +} + +func NewAppResourceQueryer() *AppResourceQueryer { + return &AppResourceQueryer{ + appService: app.DefaultSVC(), + } +} + +func (q *AppResourceQueryer) QueryResourceInfo(ctx context.Context, resourceIDs []int64, isDraft *bool) ([]*ResourceInfo, error) { + var result []*ResourceInfo + + for _, id := range resourceIDs { + appInfo, err := q.appService.GetDraftAPP(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to query app %d: %w", id, err) + } + + if appInfo != nil { + result = append(result, &ResourceInfo{ + ID: id, + CreatorID: appInfo.OwnerID, + SpaceID: &appInfo.SpaceID, + }) + } + } + + return result, nil +} + +func (q *AppResourceQueryer) GetResourceType() ResourceType { + return ResourceTypeApp +} + +type KnowledgeSliceResourceQueryer struct { + knowledgeService knowledge.Knowledge +} + +func NewKnowledgeSliceResourceQueryer() *KnowledgeSliceResourceQueryer { + return &KnowledgeSliceResourceQueryer{ + knowledgeService: knowledge.DefaultSVC(), + } +} + +func (q *KnowledgeSliceResourceQueryer) QueryResourceInfo(ctx context.Context, resourceIDs []int64, isDraft *bool) ([]*ResourceInfo, error) { + resp, err := q.knowledgeService.MGetSlice(ctx, &knowledgeModel.MGetSliceRequest{ + SliceIDs: resourceIDs, + }) + if err != nil { + return nil, fmt.Errorf("failed to query knowledge slice: %w", err) + } + + var result []*ResourceInfo + for _, slice := range resp.Slices { + if slice != nil { + result = append(result, &ResourceInfo{ + ID: slice.ID, + CreatorID: slice.CreatorID, + SpaceID: &slice.SpaceID, + }) + } + } + + return result, nil +} + +func (q *KnowledgeSliceResourceQueryer) GetResourceType() ResourceType { + return ResourceTypeKnowledgeSlice +} + +type KnowledgeDocumentResourceQueryer struct { + knowledgeService knowledge.Knowledge +} + +func NewKnowledgeDocumentResourceQueryer() *KnowledgeDocumentResourceQueryer { + return &KnowledgeDocumentResourceQueryer{ + knowledgeService: knowledge.DefaultSVC(), + } +} + +func (q *KnowledgeDocumentResourceQueryer) QueryResourceInfo(ctx context.Context, resourceIDs []int64, isDraft *bool) ([]*ResourceInfo, error) { + resp, err := q.knowledgeService.MGetDocument(ctx, &knowledgeModel.MGetDocumentRequest{ + DocumentIDs: resourceIDs, + }) + if err != nil { + return nil, fmt.Errorf("failed to query knowledge document: %w", err) + } + + var result []*ResourceInfo + for _, document := range resp.Documents { + if document != nil { + result = append(result, &ResourceInfo{ + ID: document.ID, + CreatorID: document.CreatorID, + SpaceID: &document.SpaceID, + }) + } + } + + return result, nil +} + +func (q *KnowledgeDocumentResourceQueryer) GetResourceType() ResourceType { + return ResourceTypeKnowledgeDocument +} + +type WorkspaceResourceQueryer struct { + userService crossuser.User +} + +func NewWorkspaceResourceQueryer() *WorkspaceResourceQueryer { + return &WorkspaceResourceQueryer{ + userService: crossuser.DefaultSVC(), + } +} + +func (q *WorkspaceResourceQueryer) QueryResourceInfo(ctx context.Context, resourceIDs []int64, isDraft *bool) ([]*ResourceInfo, error) { + // For workspace resources, we need to get space information for each user + var result []*ResourceInfo + + spaces, err := q.userService.GetUserSpaceBySpaceID(ctx, resourceIDs) + if err != nil { + return nil, fmt.Errorf("failed to get user space list for space %v: %w", resourceIDs, err) + } + + for _, space := range spaces { + if space != nil { + result = append(result, &ResourceInfo{ + ID: space.ID, + CreatorID: space.CreatorID, + SpaceID: &space.ID, + }) + } + } + + return result, nil +} + +func (q *WorkspaceResourceQueryer) GetResourceType() ResourceType { + return ResourceTypeWorkspace +} diff --git a/backend/domain/user/service/user.go b/backend/domain/user/service/user.go index 250afcf89..b21916153 100644 --- a/backend/domain/user/service/user.go +++ b/backend/domain/user/service/user.go @@ -78,6 +78,7 @@ type User interface { MGetUserProfiles(ctx context.Context, userIDs []int64) (users []*entity.User, err error) ValidateSession(ctx context.Context, sessionKey string) (session *entity.Session, exist bool, err error) GetUserSpaceList(ctx context.Context, userID int64) (spaces []*entity.Space, err error) + GetUserSpaceBySpaceID(ctx context.Context, spaceID []int64) (space []*entity.Space, err error) } type SaasUserProvider interface { diff --git a/backend/domain/user/service/user_impl.go b/backend/domain/user/service/user_impl.go index d662a9bfd..613a2dc4b 100644 --- a/backend/domain/user/service/user_impl.go +++ b/backend/domain/user/service/user_impl.go @@ -465,6 +465,40 @@ func (u *userImpl) GetUserSpaceList(ctx context.Context, userID int64) (spaces [ }), nil } +func (u *userImpl) GetUserSpaceBySpaceID(ctx context.Context, spaceID []int64) (spaces []*userEntity.Space, err error) { + if len(spaceID) == 0 { + return nil, errorx.New(errno.ErrUserInvalidParamCode, errorx.KV("msg", "spaceID cannot be empty")) + } + + spaceModels, err := u.SpaceRepo.GetSpaceByIDs(ctx, spaceID) + if err != nil { + return nil, err + } + + if len(spaceModels) == 0 { + return []*userEntity.Space{}, nil + } + + uris := slices.ToMap(spaceModels, func(sm *model.Space) (string, bool) { + return sm.IconURI, false + }) + + urls := make(map[string]string, len(uris)) + for uri := range uris { + if uri != "" { + url, err := u.IconOSS.GetObjectUrl(ctx, uri) + if err != nil { + return nil, err + } + urls[uri] = url + } + } + + return slices.Transform(spaceModels, func(sm *model.Space) *userEntity.Space { + return spacePo2Do(sm, urls[sm.IconURI]) + }), nil +} + func spacePo2Do(space *model.Space, iconUrl string) *userEntity.Space { return &userEntity.Space{ ID: space.ID, diff --git a/backend/internal/mock/crossdomain/crossuser/crossuser.go b/backend/internal/mock/crossdomain/crossuser/crossuser.go index 8417a20fa..f9fcd2c2c 100644 --- a/backend/internal/mock/crossdomain/crossuser/crossuser.go +++ b/backend/internal/mock/crossdomain/crossuser/crossuser.go @@ -1,9 +1,25 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + // Code generated by MockGen. DO NOT EDIT. -// Source: crossuser.go +// Source: contract.go // // Generated by this command: // -// mockgen -destination ../../../internal/mock/crossdomain/crossuser/crossuser.go --package mockCrossUser -source crossuser.go +// mockgen -destination ../../internal/mock/crossdomain/crossuser/crossuser.go --package mockCrossUser -source contract.go // // Package mockCrossUser is a generated GoMock package. @@ -13,9 +29,8 @@ import ( context "context" reflect "reflect" - gomock "go.uber.org/mock/gomock" - crossuser "github.com/coze-dev/coze-studio/backend/crossdomain/user" + gomock "go.uber.org/mock/gomock" ) // MockUser is a mock of User interface. @@ -42,6 +57,21 @@ func (m *MockUser) EXPECT() *MockUserMockRecorder { return m.recorder } +// GetUserSpaceBySpaceID mocks base method. +func (m *MockUser) GetUserSpaceBySpaceID(ctx context.Context, spaceID []int64) ([]*crossuser.EntitySpace, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserSpaceBySpaceID", ctx, spaceID) + ret0, _ := ret[0].([]*crossuser.EntitySpace) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserSpaceBySpaceID indicates an expected call of GetUserSpaceBySpaceID. +func (mr *MockUserMockRecorder) GetUserSpaceBySpaceID(ctx, spaceID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserSpaceBySpaceID", reflect.TypeOf((*MockUser)(nil).GetUserSpaceBySpaceID), ctx, spaceID) +} + // GetUserSpaceList mocks base method. func (m *MockUser) GetUserSpaceList(ctx context.Context, userID int64) ([]*crossuser.EntitySpace, error) { m.ctrl.T.Helper()