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.
168 lines
5.0 KiB
Go
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,
|
|
})
|
|
}
|