Guard process event replay for disabled local accounts
ci / vet-and-test (pull_request) Successful in 1m4s
ci / vet-and-test (pull_request) Successful in 1m4s
Why: Merging run-process visibility introduced a read endpoint that bypassed U3 effective-account checks when authentication was off. Evidence: A new regression first returned HTTP 200 for disabled local; with entry and final response checks it passed, and the event pagination test passed ten repeated runs after anchoring to its appended event sequence. Impact: Explicitly disabled local accounts cannot read stored process events; missing implicit local remains compatible. Refs: IAM U3 Stage 8 integration review; Backend PR #198; main PR #199
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"agent-system-backend/internal/store"
|
||||
core "agent-system-core"
|
||||
)
|
||||
|
||||
@@ -17,10 +18,6 @@ func TestRunProcessEventsAreOrderedAndBoundToConversation(t *testing.T) {
|
||||
_, otherConvID := setupConversation(t, h)
|
||||
runID := postMessage(t, h, convID, "整理材料")
|
||||
waitRunTerminal(t, h, runID, 5*time.Second)
|
||||
last, err := st.LastEventSeq(convID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
events := []core.StreamEvent{
|
||||
{RunID: runID, Type: core.EvThinkingDelta, Payload: json.RawMessage(`{"index":0,"text":"先核对日期"}`)},
|
||||
{RunID: runID, Type: core.EvToolOutput, Payload: json.RawMessage(`{"callId":"call-1","stream":"stdout","text":"完整工具输出"}`)},
|
||||
@@ -28,7 +25,9 @@ func TestRunProcessEventsAreOrderedAndBoundToConversation(t *testing.T) {
|
||||
if _, err := st.AppendEvents(convID, events); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := "/api/conversations/" + convID + "/runs/" + runID + "/events?after=" + strconv.FormatInt(last, 10) + "&limit=1"
|
||||
// The runner may persist its terminal event after its status becomes terminal.
|
||||
// Anchor paging to the events this test appended, not an earlier snapshot.
|
||||
path := "/api/conversations/" + convID + "/runs/" + runID + "/events?after=" + strconv.FormatInt(events[0].Seq-1, 10) + "&limit=1"
|
||||
code, first := doJSON(t, h, http.MethodGet, path, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("first page: %d %v", code, first)
|
||||
@@ -79,3 +78,28 @@ func TestRunProcessEventsAreOrderedAndBoundToConversation(t *testing.T) {
|
||||
t.Fatalf("mismatched conversation exposed run: %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessEventsRejectsDisabledImplicitLocal(t *testing.T) {
|
||||
srv, st := newTestServer(t)
|
||||
if err := st.CreateAccount("admin", "", "", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := srv.Handler()
|
||||
_, convID := setupConversation(t, h)
|
||||
runID := postMessage(t, h, convID, "整理材料")
|
||||
waitRunTerminal(t, h, runID, 5*time.Second)
|
||||
if err := st.EnsureUser(store.OwnerDefault, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
account, err := st.GetAccountByOwner(store.OwnerDefault)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := st.SetAccountEnabled("admin", account.ID, account.StateVersion, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
code, _ := doJSON(t, h, http.MethodGet, "/api/conversations/"+convID+"/runs/"+runID+"/events", nil)
|
||||
if code != http.StatusForbidden {
|
||||
t.Fatalf("disabled local account read process events: %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -917,6 +917,10 @@ func (s *Server) conversationDetail(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) runProcessEvents(w http.ResponseWriter, r *http.Request) {
|
||||
id, runID := r.PathValue("id"), r.PathValue("runId")
|
||||
owner := s.owner(r)
|
||||
if err := s.checkActorBeforeExternalEffect(owner); err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
if _, err := s.st.GetConversation(owner, id); err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
@@ -951,6 +955,12 @@ func (s *Server) runProcessEvents(w http.ResponseWriter, r *http.Request) {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
// The authenticated account can be disabled while stored events are read.
|
||||
// Recheck immediately before returning private process output.
|
||||
if err := s.checkActorBeforeExternalEffect(owner); err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"events": events})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user