Files
Agent-System/internal/server/document_revision_access.go
WeiCHEN 9c7a5be209 feat(arch): clear G1 U3/U4 sql_write and path exceptions
Move document_records duty-slot writes into docgov, project ledger
heads through docgov/freedoc ports, and workspace deliverable I/O
through DeliverableStore so arch-check baseline drops to sql_write:1
path:13.
2026-09-22 21:40:12 +08:00

168 lines
5.0 KiB
Go

package server
import (
"context"
"errors"
"net/http"
"strings"
"chat-agent-system/internal/auth"
"chat-agent-system/internal/authz"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/freedoc"
"chat-agent-system/internal/projectinstance"
)
type docRevisionRef struct {
Kind string // controlled|freeform|orphan
ProjectID int64
WorkDir string
LifecycleState string
Markdown string
Title string
AuthorID int64
}
func (s *Server) resolveDocRevisionRef(ctx context.Context, user *auth.UserAccount, docID string) (*docRevisionRef, error) {
if strings.TrimSpace(docID) == "" {
return nil, docrevision.ErrInvalidDocID
}
if s.gov != nil {
if doc, err := s.gov.GetDocument(ctx, docID); err == nil && doc != nil {
ref := &docRevisionRef{
Kind: "controlled", ProjectID: doc.ProjectID, WorkDir: doc.WorkDir,
LifecycleState: string(doc.CurrentState), AuthorID: doc.AuthorID,
}
if s.docRev() != nil {
if md, _, herr := s.docRev().HeadContent(ctx, docID); herr == nil && strings.TrimSpace(md) != "" {
ref.Markdown = md
return ref, nil
}
}
if content, cErr := s.gov.ReadDocumentContent(ctx, docID, s.dataRoot()); cErr == nil && content != nil {
ref.Markdown = content.Markdown
}
return ref, nil
}
}
if s.freeDoc != nil && user != nil {
if detail, err := s.freeDoc.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, write bool) error {
if user == nil || ref == nil {
return authz.ErrForbidden
}
if ref.Kind == "orphan" {
// Unknown doc IDs: never bootstrap a silent fake ledger (read or write).
return docrevision.ErrNotFound
}
if ref.ProjectID > 0 {
if s.authorizer == nil {
if s.failClosedAuthz() {
return authz.ErrForbidden
}
} else if err := s.authorizer.RequireProjectAccessOrAdmin(ctx, ref.ProjectID, user.ID, s.isSystemAdminUser(ctx, user)); err != nil {
return err
}
} else if ref.Kind == "controlled" {
return authz.ErrForbidden
}
if write && ref.LifecycleState != "" && ref.LifecycleState != "drafting" {
return docrevision.ErrLifecycleLocked
}
if write && ref.Kind == "controlled" && ref.AuthorID > 0 && user.ID != ref.AuthorID && !s.isSystemAdminUser(ctx, user) {
return docrevision.ErrForbidden
}
return nil
}
func (s *Server) requireDocRevisionAccess(w http.ResponseWriter, r *http.Request, write bool) (*auth.UserAccount, string, *docRevisionRef, bool) {
user, ok := s.requirePrincipal(w, r)
if !ok {
return nil, "", nil, false
}
docID := strings.TrimSpace(r.PathValue("doc_id"))
if docID == "" {
writeDocRevisionError(w, docrevision.ErrInvalidDocID)
return nil, "", nil, false
}
ref, err := s.resolveDocRevisionRef(r.Context(), user, docID)
if err != nil {
writeDocRevisionError(w, err)
return nil, "", nil, false
}
if err := s.authorizeDocRevision(r.Context(), user, ref, write); err != nil {
if errors.Is(err, authz.ErrForbidden) || errors.Is(err, authz.ErrUnauthenticated) {
writeAuthzError(w, err)
return nil, "", nil, false
}
writeDocRevisionError(w, err)
return nil, "", nil, false
}
if write && !s.enforceDocGovWorkflowAction(w, r.Context(), user.ID, docID, projectinstance.DocAccessActionEdit) {
return nil, "", nil, false
}
return user, docID, ref, true
}
func (s *Server) dataRoot() string {
if s != nil && s.cfg != nil && s.cfg.DataDir != "" {
return s.cfg.DataDir
}
return "."
}
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
}
if ref != nil && ref.Kind == "orphan" {
return nil, docrevision.ErrNotFound
}
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
}
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,
})
}