vdi-cloud/platform/internal/server/audits_test.go

134 lines
5.3 KiB
Go

package server
import (
"encoding/csv"
"fmt"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
func TestAuditHistoryFiltersAndDatabasePagination(t *testing.T) {
f := newFixtureWorker(t, false)
token := f.login("viewer")
_, err := f.db.Exec(`INSERT INTO audit_logs SELECT 'history_'||lpad(i::text,5,'0'),'worker','save_alert_rule','node_'||i,'success','request_'||i,'2025-01-01T00:00:00Z'::timestamptz FROM generate_series(1,1050) i`)
if err != nil {
t.Fatal(err)
}
if _, err = f.db.Exec(`INSERT INTO audit_logs VALUES('needle','Root_100%','save_alert_rule','研发,"一组"','denied','req_trace','2025-01-02T00:00:00Z')`); err != nil {
t.Fatal(err)
}
code, out := f.request("GET", "/audit-logs?action=save_alert_rule&page=11&pageSize=100", token, nil, "")
expect(t, 200, code, out)
items := out["items"].([]any)
if out["total"] != float64(1051) || len(items) != 51 || items[50].(map[string]any)["id"] != "history_00001" {
t.Fatal(out)
}
q := url.Values{"keyword": {"req_trace"}, "actor": {"_100%"}, "action": {"save_alert_rule"}, "result": {"denied"}, "from": {"2025-01-01T08:00:00+08:00"}, "to": {"2025-01-02T08:00:00+08:00"}}
code, out = f.request("GET", "/audit-logs?"+q.Encode(), token, nil, "")
expect(t, 200, code, out)
if out["total"] != float64(1) || out["items"].([]any)[0].(map[string]any)["id"] != "needle" {
t.Fatal(out)
}
for _, term := range []string{"_100%", "研发,", "' OR true --"} {
code, out = f.request("GET", "/audit-logs?keyword="+url.QueryEscape(term), token, nil, "")
expect(t, 200, code, out)
want := float64(0)
if term == "_100%" || term == "研发," {
want = 1
}
if out["total"] != want {
t.Fatal(term, out)
}
}
code, out = f.request("GET", "/audit-logs?page=99999999&pageSize=999", token, nil, "")
expect(t, 200, code, out)
if out["page"] != float64(1000000) || out["pageSize"] != float64(100) || len(out["items"].([]any)) != 0 {
t.Fatal(out)
}
for _, query := range []string{"from=broken", "to=broken", "from=2025-01-02T00:00:00Z&to=2025-01-01T00:00:00Z", "keyword=" + strings.Repeat("x", 201), "actor=%00"} {
for _, path := range []string{"/audit-logs?", "/audit-logs/export?"} {
code, out = f.request("GET", path+query, token, nil, "")
expect(t, 400, code, out)
}
}
}
func auditDownload(f *fixture, token, query string) *httptest.ResponseRecorder {
r := httptest.NewRequest("GET", "/api/v1/audit-logs/export"+query, nil)
if token != "" {
r.Header.Set("Authorization", "Bearer "+token)
}
w := httptest.NewRecorder()
f.server.Router.ServeHTTP(w, r)
return w
}
func TestAuditCSVExportPermissionsCompletenessAndSafety(t *testing.T) {
f := newFixtureWorker(t, false)
if _, err := f.db.Exec(`INSERT INTO audit_logs VALUES('csv-special','=malicious','custom_action',$1,'success','req-csv','2025-01-01T00:00:00Z')`, "研发,\"一组\"\r\n第二行"); err != nil {
t.Fatal(err)
}
w := auditDownload(f, "", "")
if w.Code != 401 {
t.Fatal(w.Code)
}
for _, role := range []string{"admin", "operator", "viewer"} {
token := f.login(role)
w = auditDownload(f, token, "?action=custom_action&page=999&pageSize=1")
if w.Code != 200 || w.Header().Get("X-Export-Count") != "1" || !strings.HasPrefix(w.Header().Get("Content-Type"), "text/csv") || !strings.Contains(w.Header().Get("Content-Disposition"), ".csv") {
t.Fatal(w.Code, w.Body.String())
}
body := w.Body.String()
if !strings.HasPrefix(body, "\ufeff") {
t.Fatal("missing Excel UTF-8 BOM")
}
rows, err := csv.NewReader(strings.NewReader(strings.TrimPrefix(body, "\ufeff"))).ReadAll()
if err != nil || len(rows) != 2 || len(rows[0]) != 7 || rows[1][1] != "'=malicious" || rows[1][3] != "研发,\"一组\"\n第二行" || rows[1][6] != "csv-special" {
t.Fatal(rows, err)
}
var logged int
if err := f.db.Get(&logged, "SELECT count(*) FROM audit_logs WHERE action='export_audit_logs' AND actor=$1 AND target='rows=1'", role); err != nil || logged != 1 {
t.Fatal(logged, err)
}
}
for _, cell := range []string{"=1+2", "+cmd", "-1", "@SUM(A1)", "\t=HYPERLINK(\"x\")", "\r\n @cmd", "\ufeff=cmd"} {
if safeCSVCell(cell) != "'"+cell {
t.Fatalf("unsafe: %q", cell)
}
}
for _, cell := range []string{"研发", "", "request-id", "success", "2025-01-01"} {
if safeCSVCell(cell) != cell {
t.Fatal(cell)
}
}
}
func TestAuditExportLimitAndEmptyResults(t *testing.T) {
f := newFixtureWorker(t, false)
token := f.login("admin")
if _, err := f.db.Exec(`INSERT INTO audit_logs SELECT 'bulk_'||i,'worker','bulk','target','success','request','2025-01-01T00:00:00Z'::timestamptz FROM generate_series(1,10001) i`); err != nil {
t.Fatal(err)
}
w := auditDownload(f, token, "?action=bulk")
if w.Code != 413 || strings.Contains(w.Header().Get("Content-Type"), "csv") {
t.Fatal(w.Code, w.Body.String())
}
if _, err := f.db.Exec("DELETE FROM audit_logs WHERE id='bulk_10001'"); err != nil {
t.Fatal(err)
}
w = auditDownload(f, token, "?action=bulk&pageSize=1")
if w.Code != 200 || w.Header().Get("X-Export-Count") != fmt.Sprint(auditExportLimit) {
t.Fatal(w.Code)
}
rows, err := csv.NewReader(strings.NewReader(strings.TrimPrefix(w.Body.String(), "\ufeff"))).ReadAll()
if err != nil || len(rows) != auditExportLimit+1 {
t.Fatal(len(rows), err)
}
w = auditDownload(f, token, "?action=nonexistent")
if w.Code != 200 || w.Header().Get("X-Export-Count") != "0" {
t.Fatal(w.Code)
}
}