package server import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/http/httptest" "testing" core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git" ) // EXT-8 资源授权适配器(红测:基础设施已就位、读取入口尚未咨询)—— // 默认拒绝保持现状;注册适配器后同项目成员可读共享资源、跨项目仍拒。 func TestProjectResourceAuthorizerSharesReads(t *testing.T) { srv, st := newAuthTestServer(t) h := srv.Handler() alice, err := st.CreateAPIKey("alice", "e2e") if err != nil { t.Fatal(err) } bob, err := st.CreateAPIKey("bob", "e2e") if err != nil { t.Fatal(err) } carol, err := st.CreateAPIKey("carol", "e2e") if err != nil { t.Fatal(err) } // alice 建项目并上传一份工件(owner=alice)。 prj := struct { Name string `json:"name"` }{Name: "EXT8 共享项目"} req := httptest.NewRequest(http.MethodPost, "/api/projects", bytes.NewReader(marshal(t, prj))) req.Header.Set("Authorization", "Bearer "+alice) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("create project: %d %s", rec.Code, rec.Body.String()) } pid := decodeField(t, rec.Body.Bytes(), "id") var buf bytes.Buffer mw := multipart.NewWriter(&buf) fw, _ := mw.CreateFormFile("file", "共享说明.txt") fw.Write([]byte("SHARED-CONTENT")) mw.Close() req = httptest.NewRequest(http.MethodPost, "/api/projects/"+pid+"/uploads", &buf) req.Header.Set("Authorization", "Bearer "+alice) req.Header.Set("Content-Type", mw.FormDataContentType()) rec = httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("upload: %d %s", rec.Code, rec.Body.String()) } aid := decodeField(t, rec.Body.Bytes(), "artifactId") get := func(key, path string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodGet, path, nil) req.Header.Set("Authorization", "Bearer "+key) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec } // 默认拒绝(未注册适配器):bob 任何读取都是 NotFound,不泄露存在性。 for _, path := range []string{"/api/projects/" + pid, "/api/projects/" + pid + "/artifacts", "/api/artifacts/" + aid, "/api/artifacts/" + aid + "/content"} { if rec := get(bob, path); rec.Code != http.StatusNotFound { t.Fatalf("default-deny: %s = %d, want 404", path, rec.Code) } } // 注册适配器:bob 对该项目有共享授权;carol 无。 srv.SetProjectAuthorizer(func(owner string, projectID core.ID) (bool, error) { return owner == "bob" && string(projectID) == pid, nil }) if rec := get(bob, "/api/artifacts/"+aid); rec.Code != http.StatusOK { t.Fatalf("shared artifact meta: %d %s", rec.Code, rec.Body.String()) } rec = get(bob, "/api/artifacts/"+aid+"/content") if rec.Code != http.StatusOK || rec.Body.String() != "SHARED-CONTENT" { t.Fatalf("shared artifact content: %d %q", rec.Code, rec.Body.String()) } rec = get(bob, "/api/projects/"+pid+"/artifacts") if rec.Code != http.StatusOK || !bytes.Contains(rec.Body.Bytes(), []byte(aid)) { t.Fatalf("shared artifact list: %d %s", rec.Code, rec.Body.String()) } // 跨项目/未授权用户与错误路径仍拒(NotFound;错误回调 fail-closed)。 for _, path := range []string{"/api/artifacts/" + aid, "/api/artifacts/" + aid + "/content"} { if rec := get(carol, path); rec.Code != http.StatusNotFound { t.Fatalf("carol must stay denied: %s = %d", path, rec.Code) } } srv.SetProjectAuthorizer(func(string, core.ID) (bool, error) { return false, io.ErrClosedPipe }) if rec := get(bob, "/api/artifacts/"+aid); rec.Code != http.StatusNotFound { t.Fatalf("authorizer error must fail closed: %d", rec.Code) } } func marshal(t *testing.T, v any) []byte { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatal(err) } return b } func decodeField(t *testing.T, body []byte, field string) string { t.Helper() var m map[string]any if err := json.Unmarshal(body, &m); err != nil { t.Fatalf("decode %s: %v (%s)", field, err, body) } return fmt.Sprint(m[field]) }