Files
Agent-System-Backend/internal/server/resource_authz_consumption_test.go
T
weichen adcd8fefd2 Fence U3 account writes and stabilize directory cursors
Why: Account disable could be followed by late Backend business writes, API key creation could implicitly create users, account bootstrap locks could invert, and directory cursors were process-local or leaked out-of-scope changes.

Evidence: Local go test ./... and isolated PostgreSQL package suites passed on the staged candidate; exact committed-head SELF replay and stage 6 human review remain pending.

Impact: Adds final account transactions and external-effect checks across HTTP, Host, Bus and run manager; tightens API key issuance and persists versioned cursor signing. Delegated RDM rename still needs the separately proposed real-operator contract, so A1 and stage 6 are not complete.

Refs: Documents U3 r1, approved A1-A4 amendment fb3a821, pending delegated-projection amendment 5783940, Dev feature/iam-u3-account-directory-recovery.
2026-10-14 07:33:54 +00:00

130 lines
4.6 KiB
Go

package server
// EXT-8b 读面消费红测(方案 r1 U9 第二切片):五处共享读路径在 V2 注册后按
// 资源级查询咨询(Actor/Action/ResourceType/ResourceID/Owner/ProjectID 由
// 受信上下文构造);V2 拒绝压过 V1 放行(资源级权威);V2 未注册时五处全部
// 回落 V1(兼容现行,RDM 现网行为不变)。红:当前 server 尚无 V2 槽位与
// AuthorizeResource 消费——注册接口不存在,编译红先行。
import (
"net/http"
"net/http/httptest"
"testing"
"agent-system-backend/internal/store"
"agent-system-backend/pkg/extension"
core "agent-system-core"
)
func newV2CaptureServer(t *testing.T) (*Server, *store.Store, string, string, string) {
t.Helper()
srv, st := newAuthTestServer(t)
alice, err := createLegacyAPIKeyFixture(t, st, "alice", "e2e")
if err != nil {
t.Fatal(err)
}
bob, err := createLegacyAPIKeyFixture(t, st, "bob", "e2e")
if err != nil {
t.Fatal(err)
}
pj, err := st.CreateProject("alice", "资源级项目")
if err != nil {
t.Fatal(err)
}
return srv, st, alice, bob, pj.ID
}
func TestSharedReadPathsConsultV2WithResourceContext(t *testing.T) {
srv, st, alice, bob, pid := newV2CaptureServer(t)
_ = alice
// 项目内造一个工件(供 download 路径取元数据)。
art := &core.Artifact{ProjectID: core.ID(pid), Owner: "alice", Name: "res.txt", Kind: "file", MIME: "text/plain"}
if err := st.CreateArtifact(art, []byte("body")); err != nil {
t.Fatal(err)
}
var got []extension.ResourceAuthzQuery
srv.SetResourceAuthorizerV2(func(q extension.ResourceAuthzQuery) (bool, error) {
got = append(got, q)
return true, nil
})
h := srv.Handler()
call := func(method, path string) int {
req := httptest.NewRequest(method, path, nil)
req.Header.Set("Authorization", "Bearer "+bob)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec.Code
}
// ① 工件下载(owner 自有未命中 → 资源级咨询)。
code := call(http.MethodGet, "/api/artifacts/"+string(art.ID)+"/content?download=1")
if code != http.StatusOK {
t.Fatalf("download: %d", code)
}
// ② 项目工件清单。
call(http.MethodGet, "/api/projects/"+pid+"/artifacts")
// ③ 共享项目建会话。
call(http.MethodPost, "/api/projects/"+pid+"/conversations")
byAction := map[string]extension.ResourceAuthzQuery{}
for _, q := range got {
byAction[q.Action] = q
}
dl := byAction["download"]
if dl.Actor != "bob" || dl.ResourceType != "artifact" || dl.ResourceID != art.ID || dl.ProjectID != pid || dl.Owner != "alice" {
t.Fatalf("download 查询六要素不符: %+v", dl)
}
ls := byAction["list"]
if ls.Actor != "bob" || ls.ResourceType != "artifact" || ls.ResourceID != art.ID || ls.ProjectID != pid || ls.Owner != "alice" {
t.Fatalf("list 查询不符: %+v", ls)
}
rd := byAction["read"]
if rd.Actor != "bob" || rd.ResourceType != "project" || rd.ResourceID != pid {
t.Fatalf("read(conversation) 查询不符: %+v", rd)
}
}
func TestV2DenyOverridesV1Allow(t *testing.T) {
srv, st, alice, bob, pid := newV2CaptureServer(t)
_ = alice
art := &core.Artifact{ProjectID: core.ID(pid), Owner: "alice", Name: "d.txt", Kind: "file"}
if err := st.CreateArtifact(art, []byte("body")); err != nil {
t.Fatal(err)
}
// V1 全放行、V2 全拒绝——资源级权威必须压过 V1。
srv.SetProjectAuthorizer(func(owner string, projectID core.ID) (bool, error) {
return true, nil
})
srv.SetResourceAuthorizerV2(func(q extension.ResourceAuthzQuery) (bool, error) {
return false, nil
})
req := httptest.NewRequest(http.MethodGet, "/api/artifacts/"+string(art.ID)+"/content?download=1", nil)
req.Header.Set("Authorization", "Bearer "+bob)
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("V2 拒绝须压过 V1 放行并保持 404(不泄露存在性), got %d", rec.Code)
}
}
func TestV2AbsentFallsBackToV1(t *testing.T) {
srv, st, alice, bob, pid := newV2CaptureServer(t)
_ = alice
art := &core.Artifact{ProjectID: core.ID(pid), Owner: "alice", Name: "f.txt", Kind: "file"}
if err := st.CreateArtifact(art, []byte("body")); err != nil {
t.Fatal(err)
}
srv.SetProjectAuthorizer(func(owner string, projectID core.ID) (bool, error) {
return owner == "bob" && string(projectID) == pid, nil
})
req := httptest.NewRequest(http.MethodGet, "/api/artifacts/"+string(art.ID)+"/content?download=1", nil)
req.Header.Set("Authorization", "Bearer "+bob)
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("V2 未注册应回落 V1(现行行为), got %d", rec.Code)
}
}