From 3099493bf7d7159fbc0dc37f32936e26677bb5c3 Mon Sep 17 00:00:00 2001 From: xuans Date: Mon, 31 Aug 2026 02:06:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=AF=81=E4=B9=A6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E9=94=99=E8=AF=AF=E4=BF=A1=E6=81=AF=E8=84=B1=E6=95=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/httpapi/error_sanitization.go | 34 +++++++++++++++ backend/internal/httpapi/server.go | 2 +- backend/internal/httpapi/task_api.go | 10 ++--- backend/internal/httpapi/task_api_test.go | 42 +++++++++++++++++-- doc/Scert开发任务清单.md | 20 ++++++++- 5 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 backend/internal/httpapi/error_sanitization.go diff --git a/backend/internal/httpapi/error_sanitization.go b/backend/internal/httpapi/error_sanitization.go new file mode 100644 index 0000000..0b12f4a --- /dev/null +++ b/backend/internal/httpapi/error_sanitization.go @@ -0,0 +1,34 @@ +package httpapi + +import ( + "context" + "errors" + + "git.pcux.cn/xuans/Scert/backend/internal/deployment" +) + +// certificateTaskErrorMessage 返回可以展示给管理页面的稳定错误摘要。 +func certificateTaskErrorMessage(err error) string { + if errors.Is(err, context.Canceled) { + return "证书任务已取消" + } + if errors.Is(err, context.DeadlineExceeded) { + return "证书任务执行超时" + } + return "证书申请或部署失败" +} + +// sanitizeDeploymentReport 移除部署失败结果中的远程路径、命令输出和底层错误。 +func sanitizeDeploymentReport(report deployment.Report) deployment.Report { + if len(report.Results) == 0 { + return report + } + result := report + result.Results = append([]deployment.Result(nil), report.Results...) + for index := range result.Results { + if result.Results[index].Status == "failed" { + result.Results[index].Message = "证书部署失败" + } + } + return result +} diff --git a/backend/internal/httpapi/server.go b/backend/internal/httpapi/server.go index b700c7f..4307eee 100644 --- a/backend/internal/httpapi/server.go +++ b/backend/internal/httpapi/server.go @@ -441,7 +441,7 @@ func (s *Server) issueDomain(c *gin.Context) { s.writeError(c, http.StatusBadGateway, "证书申请或部署失败", err) return } - c.JSON(http.StatusOK, gin.H{"certificate": certificateResult, "deployment": deploymentReport}) + c.JSON(http.StatusOK, gin.H{"certificate": certificateResult, "deployment": sanitizeDeploymentReport(deploymentReport)}) } func (s *Server) listServers(c *gin.Context) { diff --git a/backend/internal/httpapi/task_api.go b/backend/internal/httpapi/task_api.go index f813d56..07130ab 100644 --- a/backend/internal/httpapi/task_api.go +++ b/backend/internal/httpapi/task_api.go @@ -95,19 +95,19 @@ func (s *Server) issueDomainAsTask(c *gin.Context) { s.unregisterTaskCancel(task.ID) }() certificateResult, deploymentReport, issueErr := s.issueCertificate(executionCtx, domainID) + deploymentReport = sanitizeDeploymentReport(deploymentReport) finished := time.Now().UTC() status := "success" message := "证书申请和部署完成" cancelled := false if issueErr != nil { status = "failed" - message = issueErr.Error() + message = certificateTaskErrorMessage(issueErr) + if s.logger != nil { + s.logger.Error("证书任务执行失败", "task_id", task.ID, "domain_id", domainID, "error", issueErr) + } if errors.Is(issueErr, context.Canceled) { status = "cancelled" - message = "证书任务已取消" - } - if errors.Is(issueErr, context.DeadlineExceeded) { - message = "证书任务执行超时" } } if persisted, err := s.tasks.GetTask(context.Background(), task.ID); err == nil && persisted.Status == "cancelled" { diff --git a/backend/internal/httpapi/task_api_test.go b/backend/internal/httpapi/task_api_test.go index 31e7528..979362a 100644 --- a/backend/internal/httpapi/task_api_test.go +++ b/backend/internal/httpapi/task_api_test.go @@ -102,15 +102,21 @@ func TestTaskAPIRecordsSuccessfulIssueAndAudit(t *testing.T) { func TestTaskAPIReturnsFailureAndKeepsTaskTrace(t *testing.T) { server, taskRepo, auditRepo, closeDB := newTaskAPITestServer(t, func(context.Context, string) (certificate.IssueResult, deployment.Report, error) { - return certificate.IssueResult{}, deployment.Report{Status: "failed"}, errors.New("模拟证书申请失败") + return certificate.IssueResult{}, deployment.Report{ + Status: "failed", + Results: []deployment.Result{{Status: "failed", Message: "上传远程证书失败:/etc/nginx/private/key.pem: permission denied"}}, + }, errors.New("模拟证书申请失败:/etc/scert/secret/config.json") }) defer closeDB() response := httptest.NewRecorder() serveAdminHTTP(server, response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil)) - if response.Code != http.StatusBadGateway || !strings.Contains(response.Body.String(), "模拟证书申请失败") { + if response.Code != http.StatusBadGateway || !strings.Contains(response.Body.String(), "证书申请或部署失败") { t.Fatalf("失败任务响应错误:%d %s", response.Code, response.Body.String()) } + if strings.Contains(response.Body.String(), "/etc/scert/secret/config.json") || strings.Contains(response.Body.String(), "/etc/nginx/private/key.pem") || strings.Contains(response.Body.String(), "permission denied") { + t.Fatalf("失败任务响应泄露底层错误:%s", response.Body.String()) + } var payload struct { Task struct { Status string `json:"status"` @@ -125,7 +131,7 @@ func TestTaskAPIReturnsFailureAndKeepsTaskTrace(t *testing.T) { if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil { t.Fatalf("失败任务响应不是有效 JSON:%v;响应:%s", err, response.Body.String()) } - if payload.Task.Status != "failed" || payload.Task.Stage != "certificate_issue" || payload.Task.Message != "模拟证书申请失败" { + if payload.Task.Status != "failed" || payload.Task.Stage != "certificate_issue" || payload.Task.Message != "证书申请或部署失败" { t.Fatalf("失败任务嵌套状态不一致:%+v", payload.Task) } if payload.TaskStatus != "failed" || payload.FinishedAt == "" || payload.Task.FinishedAt == "" || payload.Task.UpdatedAt == "" { @@ -240,3 +246,33 @@ func TestTaskAPIReturnsNotImplementedWhenTaskDependenciesAreMissing(t *testing.T t.Fatalf("任务依赖缺失时应返回 501:%d %s", response.Code, response.Body.String()) } } + +func TestIssueDomainSanitizesDeploymentReportWithoutTaskRepository(t *testing.T) { + cfg := readyTestConfig(t.TempDir()) + server := New(Dependencies{ + Config: cfg, + Repository: &stubDomainRepository{}, + IssueCertificate: func(context.Context, string) (certificate.IssueResult, deployment.Report, error) { + return certificate.IssueResult{DomainID: "domain-1"}, deployment.Report{ + Status: "failed", + Results: []deployment.Result{{ + ServerID: "server-1", + Server: "生产服务器", + Status: "failed", + Message: "替换远程证书失败:/etc/scert/private/key.pem: permission denied", + }}, + }, nil + }, + }) + response := httptest.NewRecorder() + serveAdminHTTP(server, response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil)) + if response.Code != http.StatusOK { + t.Fatalf("无任务仓储时证书申请响应错误:%d %s", response.Code, response.Body.String()) + } + if strings.Contains(response.Body.String(), "/etc/scert/private/key.pem") || strings.Contains(response.Body.String(), "permission denied") { + t.Fatalf("直接证书申请响应泄露部署错误:%s", response.Body.String()) + } + if !strings.Contains(response.Body.String(), "证书部署失败") { + t.Fatalf("直接证书申请响应未返回稳定部署错误摘要:%s", response.Body.String()) + } +} diff --git a/doc/Scert开发任务清单.md b/doc/Scert开发任务清单.md index 57efb7e..a1895cd 100644 --- a/doc/Scert开发任务清单.md +++ b/doc/Scert开发任务清单.md @@ -3059,4 +3059,22 @@ Linux Ubuntu 22.04 上的 `Scert-admin` 手动启停、无开机自启动、无 - `[✓]` 证书接口专项测试通过。 - `[✓]` 后端 `go test ./...`、`go vet ./...` 和 `go mod verify` 已通过。 - `[✓]` 前端生产构建已通过。 -- `[!]` 真实 ACME、DNS Provider、SSH 部署、Nginx 反向代理和生产 HTTPS 仍需在授权目标环境执行实机验收。 \ No newline at end of file +- `[!]` 真实 ACME、DNS Provider、SSH 部署、Nginx 反向代理和生产 HTTPS 仍需在授权目标环境执行实机验收。 + +## 七十九、证书任务与部署报告错误信息脱敏(2026-08-30) + +### 79.1 本轮需求清单 + +- `[✓]` 证书任务失败时不再把 ACME、SSH、远程路径或命令执行错误写入任务消息和 HTTP 响应。 +- `[✓]` 证书任务底层错误保留在服务日志中,便于管理员通过日志排查具体原因。 +- `[✓]` 任务接口返回的失败部署结果统一使用“证书部署失败”摘要。 +- `[✓]` 无任务仓储的直接证书申请接口同样对部署报告失败项进行脱敏。 +- `[✓]` 增加任务失败错误脱敏和直接申请部署报告脱敏回归测试。 + +### 79.2 代码与验证状态 + +- `[✓]` 新增 HTTP API 层错误摘要和部署报告脱敏辅助逻辑。 +- `[✓]` `go test ./internal/httpapi` 定向测试通过。 +- `[✓]` 后端 `go test ./...`、`go test -race ./...`、`go vet ./...`、`go mod verify` 和 `git diff --check` 已通过。 +- `[~]` 本批源码提交和远程推送待完成。 +- `[!]` 真实 ACME、DNS Provider、SSH 部署、Nginx 反向代理和生产 HTTPS 仍需在授权目标环境执行实机验收。