feat(api): duty-check canonical docs before they are persisted

Template doc_proj ids were skipping CheckDocTaskAccess and returning 400 not-found.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-14 08:39:44 +00:00
co-authored by Cursor
parent 68fee06da4
commit aec166ea41
3 changed files with 58 additions and 6 deletions
+31 -5
View File
@@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"net/http"
"strconv"
"strings"
"chat-agent-system/internal/auth"
"chat-agent-system/internal/chat"
@@ -63,14 +65,38 @@ func (s *Server) enforceWorkflowDocTaskAction(w http.ResponseWriter, ctx context
}
func (s *Server) enforceDocGovWorkflowAction(w http.ResponseWriter, ctx context.Context, userID int64, docID, action string) bool {
if s.svc == nil || s.svc.DocGov() == nil || docID == "" {
if docID == "" {
return true
}
doc, err := s.svc.DocGov().GetDocument(ctx, docID)
if err != nil || doc == nil || doc.ProjectID <= 0 || doc.TaskKey == "" {
return true
if s.svc != nil && s.svc.DocGov() != nil {
doc, err := s.svc.DocGov().GetDocument(ctx, docID)
if err == nil && doc != nil && doc.ProjectID > 0 && doc.TaskKey != "" {
return s.enforceWorkflowDocTaskAction(w, ctx, doc.ProjectID, userID, doc.TaskKey, action)
}
}
return s.enforceWorkflowDocTaskAction(w, ctx, doc.ProjectID, userID, doc.TaskKey, action)
if projectID, taskKey, ok := parseCanonicalWorkflowDocID(docID); ok {
return s.enforceWorkflowDocTaskAction(w, ctx, projectID, userID, taskKey, action)
}
return true
}
// parseCanonicalWorkflowDocID 解析 doc_proj{projectID}_{taskKey}。
// 模板尚未落 document_records 时,送审/审核/批准仍按任务职责鉴权。
func parseCanonicalWorkflowDocID(docID string) (int64, string, bool) {
const prefix = "doc_proj"
if !strings.HasPrefix(docID, prefix) {
return 0, "", false
}
rest := strings.TrimPrefix(docID, prefix)
idx := strings.IndexByte(rest, '_')
if idx <= 0 || idx >= len(rest)-1 {
return 0, "", false
}
projectID, err := strconv.ParseInt(rest[:idx], 10, 64)
if err != nil || projectID <= 0 {
return 0, "", false
}
return projectID, rest[idx+1:], true
}
func (s *Server) writeDocAccessDecision(w http.ResponseWriter, decision projectinstance.DocAccessDecision) {
@@ -0,0 +1,26 @@
package server
import "testing"
func TestParseCanonicalWorkflowDocID(t *testing.T) {
t.Parallel()
cases := []struct {
in string
wantID int64
wantKey string
ok bool
}{
{"doc_proj6201_intake", 6201, "intake", true},
{"doc_proj12_feasibility_reg", 12, "feasibility_reg", true},
{"doc-p6201-intake", 0, "", false},
{"doc_proj_intake", 0, "", false},
{"doc_proj0_intake", 0, "", false},
{"", 0, "", false},
}
for _, tc := range cases {
gotID, gotKey, ok := parseCanonicalWorkflowDocID(tc.in)
if ok != tc.ok || gotID != tc.wantID || gotKey != tc.wantKey {
t.Fatalf("%q -> (%d,%q,%v) want (%d,%q,%v)", tc.in, gotID, gotKey, ok, tc.wantID, tc.wantKey, tc.ok)
}
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ type RunRecord struct {
WorkflowInstanceID int64
StageInstanceID int64
WorkflowStageKey string
Status string // pending | running | completed | failed | cancelled | pending_author_confirmation | …
Status string // pending | running | completed | failed | cancelled | pending_author_confirmation | blocked_pending_governance | …
WorkspaceID string
AssistantMsgID int64
WorkerID string