test(server): reject non-member comment and audit reads on postgres
The in-memory authorizer treats any positive user id as a member, so the HTTP negative has to run against a real project row. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/auth"
|
||||
"chat-agent-system/internal/authz"
|
||||
"chat-agent-system/internal/bizdev"
|
||||
)
|
||||
|
||||
// 批注与审计的项目成员检查必须走 Postgres 授权器。
|
||||
// 内存授权器在无库时把任意正用户 id 视为成员,不能用它证明 HTTP 负例。
|
||||
func TestDocGovCommentsAndAudit_NonMemberIs403_PG(t *testing.T) {
|
||||
cfg, pool := pgConfigFromTestDSN(t)
|
||||
defer pool.Close()
|
||||
cfg.AuthRegistrationEnabled = true
|
||||
cfg.AuthRequireApproval = false
|
||||
|
||||
svc := bizdev.MustNewService(t, cfg)
|
||||
srv := NewServer(cfg, svc)
|
||||
srv.authorizer = authz.NewAuthorizer(pool)
|
||||
|
||||
ctx := context.Background()
|
||||
var projectID int64
|
||||
if err := pool.QueryRow(ctx, `SELECT id FROM projects ORDER BY id ASC LIMIT 1`).Scan(&projectID); err != nil || projectID <= 0 {
|
||||
t.Skip("no projects for comment/audit negative test")
|
||||
}
|
||||
|
||||
docID := fmt.Sprintf("doc_h2_nonmember_%d", time.Now().UnixNano())
|
||||
if _, err := pool.Exec(ctx, `
|
||||
INSERT INTO document_records (doc_id, project_id, task_key, title)
|
||||
VALUES ($1, $2, 'intake', 'H2 非成员负例')
|
||||
`, docID, projectID); err != nil {
|
||||
t.Fatalf("insert document: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(context.Background(), `DELETE FROM document_records WHERE doc_id = $1`, docID)
|
||||
})
|
||||
|
||||
uname := fmt.Sprintf("outsider_h2_%d", time.Now().UnixNano())
|
||||
pass := "OutsiderPass123!"
|
||||
regBody, _ := json.Marshal(map[string]string{
|
||||
"username": uname,
|
||||
"password": pass,
|
||||
"name": "局外人",
|
||||
})
|
||||
regReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(regBody))
|
||||
regW := httptest.NewRecorder()
|
||||
srv.Routes().ServeHTTP(regW, regReq)
|
||||
if regW.Code != http.StatusOK && regW.Code != http.StatusCreated {
|
||||
t.Fatalf("register outsider: %d %s", regW.Code, regW.Body.String())
|
||||
}
|
||||
|
||||
loginBody, _ := json.Marshal(map[string]string{"username": uname, "password": pass})
|
||||
loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody))
|
||||
loginW := httptest.NewRecorder()
|
||||
srv.Routes().ServeHTTP(loginW, loginReq)
|
||||
if loginW.Code != http.StatusOK {
|
||||
t.Fatalf("outsider login: %d %s", loginW.Code, loginW.Body.String())
|
||||
}
|
||||
var session auth.SessionInfo
|
||||
if err := json.NewDecoder(loginW.Body).Decode(&session); err != nil || session.Token == "" || session.User == nil {
|
||||
t.Fatalf("decode outsider session: %v body=%s", err, loginW.Body.String())
|
||||
}
|
||||
if err := srv.authorizer.RequireProjectAccess(ctx, projectID, session.User.ID); err != authz.ErrForbidden {
|
||||
t.Fatalf("outsider must not be a member of project %d: %v", projectID, err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
method string
|
||||
target string
|
||||
body []byte
|
||||
}{
|
||||
{name: "list comments", method: http.MethodGet, target: "/api/v1/docgov/comments?doc_id=" + url.QueryEscape(docID)},
|
||||
{name: "list audit", method: http.MethodGet, target: "/api/v1/docgov/audit-logs?doc_id=" + url.QueryEscape(docID)},
|
||||
{
|
||||
name: "add comment",
|
||||
method: http.MethodPost,
|
||||
target: "/api/v1/docgov/comments",
|
||||
body: []byte(`{"doc_id":"` + docID + `","content":"should-fail"}`),
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tc.method, tc.target, bytes.NewReader(tc.body))
|
||||
withBearerAuth(req, session.Token)
|
||||
w := httptest.NewRecorder()
|
||||
srv.Routes().ServeHTTP(w, req)
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("%s want 403 got %d %s", tc.name, w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user