Files
Agent-System/internal/projectinstance/document_access.go
T
weichenandCursor 8d22fa3827 fix(projectinstance): resolve legacy task keys against namespaced snapshots
New workflow instances store bd_* task keys, but chat still sends the old keys. Scope and document access now treat those aliases as the same task so a live instance no longer fails closed on an exact string miss.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 05:03:08 +00:00

131 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package projectinstance
import (
"context"
"chat-agent-system/internal/workflowdef"
)
const (
DocAccessActionView = "view"
DocAccessActionEdit = "edit"
DocAccessActionSubmit = "submit"
DocAccessActionReview = "review"
DocAccessActionApprove = "approve"
DocAccessActionRun = "run"
AccessCodeDocTaskDisabled = "DOC_TASK_DISABLED"
AccessCodeNotCurrentStage = "DOC_TASK_NOT_IN_ACTIVE_STAGE"
AccessCodeForbiddenDuty = "DOC_DUTY_FORBIDDEN"
AccessCodeDutyNotManaged = "DOC_DUTY_NOT_MANAGED"
AccessCodeInstanceClosed = "WORKFLOW_INSTANCE_CLOSED"
)
// DocAccessDecision 统一文档任务访问判定结果。
type DocAccessDecision struct {
Allowed bool `json:"allowed"`
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
// CheckDocTaskAccess 对治理实例下的文档任务做判定。
//
// GetGoverningInstanceForAccess:从未激活 → view 放行、run 带 WORKFLOW_NOT_ACTIVATED(HTTP 转 202-blocked)、其余拒绝;
// completed/terminated → InstanceClosed;存储错误 fail-closed。full 文档按动作对应单一职责,创建者不隐式越权。
func (s *Service) CheckDocTaskAccess(
ctx context.Context,
projectID int64,
userID int64,
taskKey string,
action string,
isCreator bool,
) DocAccessDecision {
allow := DocAccessDecision{Allowed: true}
if s == nil || s.store == nil || projectID <= 0 || userID <= 0 || taskKey == "" {
return allow
}
gov, err := s.store.GetGoverningInstanceForAccess(ctx, projectID)
if err != nil {
return DocAccessDecision{Allowed: false, Code: AccessCodeInstanceClosed, Message: "工作流实例不可用"}
}
if gov == nil {
return classifyAccess(DocTaskPlan{Enabled: true}, action, userID, isCreator, false)
}
if IsClosedInstanceStatus(gov.Status) {
return DocAccessDecision{
Allowed: false,
Code: AccessCodeInstanceClosed,
Message: "工作流实例已结束",
}
}
detail, err := s.store.GetInstanceByID(ctx, gov.ID)
if err != nil || detail == nil {
return DocAccessDecision{Allowed: false, Code: AccessCodeInstanceClosed, Message: "工作流实例不可用"}
}
var docInst *DocTaskInstance
var stageKey string
for _, st := range detail.Stages {
for i := range st.DocTasks {
if workflowdef.SameTaskKey(st.DocTasks[i].TaskKey, taskKey) {
copyDoc := st.DocTasks[i]
docInst = &copyDoc
stageKey = st.StageKey
break
}
}
if docInst != nil {
break
}
}
if docInst == nil {
return allow
}
if docInst.Status == "skipped_disabled" {
return DocAccessDecision{
Allowed: false,
Code: AccessCodeDocTaskDisabled,
Message: "该文档已被创建者禁用",
}
}
if stageKey != "" && stageKey != detail.ActiveStageKey {
if action != DocAccessActionView {
return DocAccessDecision{
Allowed: false,
Code: AccessCodeNotCurrentStage,
Message: "文档不在当前活动阶段",
}
}
}
if action == DocAccessActionView {
return allow
}
planTask := findDocTaskPlan(gov.ConfigSnapshot, taskKey)
if planTask == nil || !planTask.Enabled {
return DocAccessDecision{
Allowed: false,
Code: AccessCodeDocTaskDisabled,
Message: "该文档已被创建者禁用",
}
}
return classifyAccess(*planTask, action, userID, isCreator, true)
}
func findDocTaskPlan(plan WorkflowPlan, taskKey string) *DocTaskPlan {
for _, st := range plan.Stages {
for i := range st.DocTasks {
if workflowdef.SameTaskKey(st.DocTasks[i].TaskKey, taskKey) {
return &st.DocTasks[i]
}
}
}
return nil
}