完善证书任务错误信息脱敏
This commit is contained in:
parent
d531200d40
commit
3099493bf7
34
backend/internal/httpapi/error_sanitization.go
Normal file
34
backend/internal/httpapi/error_sanitization.go
Normal file
@ -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
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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" {
|
||||
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@ -3059,4 +3059,22 @@ Linux Ubuntu 22.04 上的 `Scert-admin` 手动启停、无开机自启动、无
|
||||
- `[✓]` 证书接口专项测试通过。
|
||||
- `[✓]` 后端 `go test ./...`、`go vet ./...` 和 `go mod verify` 已通过。
|
||||
- `[✓]` 前端生产构建已通过。
|
||||
- `[!]` 真实 ACME、DNS Provider、SSH 部署、Nginx 反向代理和生产 HTTPS 仍需在授权目标环境执行实机验收。
|
||||
- `[!]` 真实 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 仍需在授权目标环境执行实机验收。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user