Files
Agent-System/internal/server/document_revision_access.go
T
weichenandCursor 46a1fcc945 feat(api): authorize revision routes and project fact-source saves
Resolve controlled/freeform/orphan docs, bootstrap on GET, write output.md or freeform content on PATCH, and wire lineage impacts for agent candidates.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 08:48:32 +00:00

136 lines
3.8 KiB
Go

package server
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"chat-agent-system/internal/auth"
"chat-agent-system/internal/authz"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/freedoc"
)
type docRevisionRef struct {
Kind string // controlled|freeform|orphan
ProjectID int64
WorkDir string
LifecycleState string
Markdown string
Title string
}
func (s *Server) resolveDocRevisionRef(ctx context.Context, user *auth.UserAccount, docID string) (*docRevisionRef, error) {
if s.svc != nil && s.svc.DocGov() != nil {
if doc, err := s.svc.DocGov().GetDocument(ctx, docID); err == nil && doc != nil {
ref := &docRevisionRef{
Kind: "controlled", ProjectID: doc.ProjectID, WorkDir: doc.WorkDir,
LifecycleState: string(doc.CurrentState),
}
if content, cErr := s.svc.DocGov().ReadDocumentContent(ctx, docID, s.dataRoot()); cErr == nil && content != nil {
ref.Markdown = content.Markdown
} else if doc.WorkDir != "" {
md, _ := readOutputMarkdown(s.dataRoot(), doc.WorkDir)
ref.Markdown = md
}
return ref, nil
}
}
if s.svc != nil && s.svc.FreeformDoc() != nil && user != nil {
if detail, err := s.svc.FreeformDoc().GetDocument(ctx, int(user.ID), docID); err == nil && detail != nil {
return &docRevisionRef{
Kind: "freeform", ProjectID: int64(detail.Document.ProjectID),
LifecycleState: "drafting", Markdown: detail.Content, Title: detail.Document.Title,
}, nil
} else if errors.Is(err, freedoc.ErrForbidden) {
return nil, docrevision.ErrForbidden
}
}
return &docRevisionRef{Kind: "orphan", LifecycleState: "drafting"}, nil
}
func (s *Server) authorizeDocRevision(ctx context.Context, user *auth.UserAccount, ref *docRevisionRef) error {
if user == nil || ref == nil {
return authz.ErrForbidden
}
if ref.Kind == "orphan" {
// Orphan upload: allow authenticated read/bootstrap only when client supplies content on PATCH.
return nil
}
if ref.ProjectID <= 0 {
return nil
}
if s.authorizer == nil {
if s.failClosedAuthz() {
return authz.ErrForbidden
}
return nil
}
return s.authorizer.RequireProjectAccessOrAdmin(ctx, ref.ProjectID, user.ID, s.isSystemAdminUser(ctx, user))
}
func (s *Server) dataRoot() string {
if s != nil && s.cfg != nil && s.cfg.DataDir != "" {
return s.cfg.DataDir
}
return "."
}
func readOutputMarkdown(dataRoot, workDir string) (string, error) {
path := workDir
if !filepath.IsAbs(path) {
path = filepath.Join(dataRoot, workDir)
}
b, err := os.ReadFile(filepath.Join(path, "output.md"))
if err != nil {
return "", err
}
return string(b), nil
}
func (s *Server) bootstrapRevisionIfNeeded(ctx context.Context, user *auth.UserAccount, docID string, ref *docRevisionRef) (*docrevision.RevisionState, error) {
rev := s.docRev()
if rev == nil {
return nil, errors.New("docrevision unavailable")
}
st, err := rev.GetRevisionState(ctx, docID)
if err == nil {
return st, nil
}
if !errors.Is(err, docrevision.ErrNotFound) {
return nil, err
}
md := ""
if ref != nil {
md = ref.Markdown
}
if strings.TrimSpace(md) == "" {
return nil, docrevision.ErrNeedsContent
}
kind := "controlled"
life := "drafting"
var projectID int64
workDir := ""
if ref != nil {
if ref.Kind != "" {
kind = ref.Kind
}
if ref.Kind == "orphan" {
kind = "freeform"
}
life = ref.LifecycleState
projectID = ref.ProjectID
workDir = ref.WorkDir
}
actor := docrevision.ActorSnapshot{DisplayName: "bootstrap", Role: "system"}
if user != nil {
actor = docrevision.ActorSnapshot{DisplayName: user.Name, Role: "author", Duty: "author"}
}
return rev.EnsureBootstrap(ctx, docrevision.BootstrapRequest{
DocID: docID, Kind: kind, Markdown: md, ProjectID: projectID,
WorkDir: workDir, LifecycleState: life, Actor: actor,
})
}