补充服务器主机指纹变更审计

为服务器创建和主机指纹更新补充审计记录,并增加 HTTP 回归测试。
This commit is contained in:
xuans 2026-09-29 23:40:49 +08:00
parent f5c557e2d9
commit 8c80a7261f
2 changed files with 80 additions and 0 deletions

View File

@ -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)
}
}

View File

@ -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)
}
}