From 8c80a7261f255dce11545becedf1a574e9a61f90 Mon Sep 17 00:00:00 2001 From: xuans Date: Tue, 29 Sep 2026 23:40:49 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E4=B8=BB=E6=9C=BA=E6=8C=87=E7=BA=B9=E5=8F=98=E6=9B=B4=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为服务器创建和主机指纹更新补充审计记录,并增加 HTTP 回归测试。 --- backend/internal/httpapi/server.go | 17 +++++++ backend/internal/httpapi/server_test.go | 63 +++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/backend/internal/httpapi/server.go b/backend/internal/httpapi/server.go index 31661ec..6dde601 100644 --- a/backend/internal/httpapi/server.go +++ b/backend/internal/httpapi/server.go @@ -819,6 +819,7 @@ func (s *Server) createServer(c *gin.Context) { s.writeError(c, serverErrorStatus(err), "保存服务器失败", err) return } + s.recordServerHostKeyAudit(c, "server_host_key_fingerprint_register", item.ID) item, provisionErr := s.provisionServerSSHKey(c.Request.Context(), item) if provisionErr != nil { if updateErr := s.servers.UpdateServer(c.Request.Context(), item); updateErr != nil && s.logger != nil { @@ -883,6 +884,9 @@ func (s *Server) updateServer(c *gin.Context) { s.writeError(c, serverErrorStatus(err), "更新服务器失败", err) return } + if existing.HostKeyFingerprint != item.HostKeyFingerprint { + s.recordServerHostKeyAudit(c, "server_host_key_fingerprint_update", item.ID) + } item, provisionErr := s.provisionServerSSHKey(c.Request.Context(), item) if provisionErr != nil { if updateErr := s.servers.UpdateServer(c.Request.Context(), item); updateErr != nil && s.logger != nil { @@ -1129,3 +1133,16 @@ func (s *Server) writeError(c *gin.Context, status int, message string, err erro } c.JSON(status, gin.H{"error": message}) } + +func (s *Server) recordServerHostKeyAudit(c *gin.Context, action, serverID string) { + if s.audit == nil { + return + } + if err := s.audit.CreateAuditLog(c.Request.Context(), repository.AuditLog{ + OperatorID: s.currentOperatorID(c), + Action: action, ResourceType: "server", ResourceID: serverID, Status: "success", + Message: "已登记服务器主机指纹变更", + }); err != nil && s.logger != nil { + s.logger.Warn("保存服务器主机指纹审计日志失败", "server_id", serverID, "error", err) + } +} diff --git a/backend/internal/httpapi/server_test.go b/backend/internal/httpapi/server_test.go index b8505aa..c617df1 100644 --- a/backend/internal/httpapi/server_test.go +++ b/backend/internal/httpapi/server_test.go @@ -836,3 +836,66 @@ func TestServerMutationRequiresProjectPermission(t *testing.T) { t.Fatalf("权限拒绝后服务器不应被删除:%v", err) } } + +func TestServerHostKeyChangeCreatesAuditRecord(t *testing.T) { + directory := t.TempDir() + cfg := readyTestConfig(directory) + cfg.Database.DSN = filepath.Join(directory, "data", "host-key-audit.db") + db, err := database.Open(context.Background(), cfg.Database) + if err != nil { + t.Fatalf("打开数据库失败:%v", err) + } + defer db.Close() + if err := database.Migrate(context.Background(), db.DB, "sqlite"); err != nil { + t.Fatalf("数据库迁移失败:%v", err) + } + if _, err := db.DB.ExecContext(t.Context(), `INSERT INTO users (id, username, password_hash, role, status) VALUES ('test-admin', 'test-admin', 'test-only-no-login', 'admin', 'active')`); err != nil { + t.Fatalf("创建测试账户失败:%v", err) + } + if _, err := db.DB.ExecContext(t.Context(), `INSERT INTO project_members (project_id, user_id, role) VALUES ('default', 'test-admin', 'project_admin')`); err != nil { + t.Fatalf("创建项目成员失败:%v", err) + } + store, err := secretstore.Open(filepath.Join(directory, "config")) + if err != nil { + t.Fatalf("打开敏感信息存储失败:%v", err) + } + sshKeys := sshkey.NewService(store) + key, err := sshKeys.Create(t.Context(), "审计测试私钥", testSSHPrivateKey(t), "") + if err != nil { + t.Fatalf("创建测试私钥失败:%v", err) + } + serverRepo := repository.NewSQLServerRepository(db.DB) + resourceRepo := repository.NewSQLProjectMemberRepository(db.DB) + server := New(Dependencies{ + Config: cfg, Repository: repository.NewSQLDomainRepository(db.DB), Servers: serverRepo, + SSHKeys: sshKeys, Audit: repository.NewSQLAuditRepository(db.DB), + ProjectResources: resourceRepo, ProjectBindings: resourceRepo, ProjectDomains: resourceRepo, + }) + createBody := `{"name":"审计测试服务器","address":"192.0.2.80","port":22,"username":"deploy","private_key_ref":"` + key.Reference + `","host_key_fingerprint":"SHA256:first","web_server":"nginx"}` + createRequest := httptest.NewRequest(http.MethodPost, "/api/v1/servers", strings.NewReader(createBody)) + createRequest.Header.Set("Content-Type", "application/json") + createResponse := httptest.NewRecorder() + serveAdminHTTP(server, createResponse, createRequest) + if createResponse.Code != http.StatusCreated { + t.Fatalf("创建测试服务器失败:%d %s", createResponse.Code, createResponse.Body.String()) + } + var created deployment.Server + if err := json.Unmarshal(createResponse.Body.Bytes(), &created); err != nil || created.ID == "" { + t.Fatalf("解析测试服务器响应失败:%v %s", err, createResponse.Body.String()) + } + updateBody := `{"name":"审计测试服务器","address":"192.0.2.80","port":22,"username":"deploy","private_key_ref":"` + key.Reference + `","host_key_fingerprint":"SHA256:second","web_server":"nginx"}` + updateRequest := httptest.NewRequest(http.MethodPut, "/api/v1/servers/"+created.ID, strings.NewReader(updateBody)) + updateRequest.Header.Set("Content-Type", "application/json") + updateResponse := httptest.NewRecorder() + serveAdminHTTP(server, updateResponse, updateRequest) + if updateResponse.Code != http.StatusOK { + t.Fatalf("更新服务器指纹失败:%d %s", updateResponse.Code, updateResponse.Body.String()) + } + var action, status, message string + if err := db.DB.QueryRowContext(t.Context(), `SELECT action, status, message FROM audit_logs WHERE resource_type = 'server' AND resource_id = ? AND action = 'server_host_key_fingerprint_update' ORDER BY id DESC LIMIT 1`, created.ID).Scan(&action, &status, &message); err != nil { + t.Fatalf("读取服务器指纹审计记录失败:%v", err) + } + if action != "server_host_key_fingerprint_update" || status != "success" || message == "" { + t.Fatalf("服务器指纹审计记录不完整:%s %s %s", action, status, message) + } +}