补齐证书恢复任务权限与专项测试
This commit is contained in:
parent
aeb2d2f2cc
commit
3aa1d50251
156
backend/internal/httpapi/certificate_restore_async_test.go
Normal file
156
backend/internal/httpapi/certificate_restore_async_test.go
Normal file
@ -0,0 +1,156 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
|
||||
)
|
||||
|
||||
func restoreRequest(key string) *http.Request {
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/certificate/versions/history-1/restore", nil)
|
||||
request.Header.Set("Idempotency-Key", key)
|
||||
return request
|
||||
}
|
||||
|
||||
func TestAsyncRestoreSubmitsPersistedTaskWithoutNestedWorkflow(t *testing.T) {
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
var calls atomic.Int32
|
||||
var gotDomain, gotVersion string
|
||||
server, tasks, _, cleanup := newTaskAPITestServer(t, nil)
|
||||
defer cleanup()
|
||||
server.restoreVersion = func(context.Context, string, string) (certificate.IssueResult, error) {
|
||||
t.Fatal("异步恢复不得调用兼容同步工作流")
|
||||
return certificate.IssueResult{}, nil
|
||||
}
|
||||
server.restoreVersionTask = func(ctx context.Context, domainID, version string) (certificate.IssueResult, error) {
|
||||
calls.Add(1)
|
||||
gotDomain, gotVersion = domainID, version
|
||||
close(started)
|
||||
select {
|
||||
case <-release:
|
||||
return certificate.IssueResult{DomainID: domainID}, nil
|
||||
case <-ctx.Done():
|
||||
return certificate.IssueResult{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
serveAdminHTTP(server, response, restoreRequest("restore-async-1"))
|
||||
task := submittedTask(t, response)
|
||||
if task.TaskType != "certificate_restore" {
|
||||
t.Fatalf("恢复任务类型错误:%+v", task)
|
||||
}
|
||||
if response.Header().Get("Location") != "/api/v1/tasks/"+task.ID {
|
||||
t.Fatalf("恢复任务查询地址错误:%s", response.Header().Get("Location"))
|
||||
}
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("恢复后台任务未开始执行")
|
||||
}
|
||||
close(release)
|
||||
waitTaskWorkersForTest(t, server)
|
||||
|
||||
stored, err := tasks.GetTask(t.Context(), task.ID)
|
||||
if err != nil || stored.Status != "success" {
|
||||
t.Fatalf("恢复任务未成功收敛:%+v %v", stored, err)
|
||||
}
|
||||
if gotDomain != "domain-1" || gotVersion != "history-1" || calls.Load() != 1 {
|
||||
t.Fatalf("恢复参数或执行次数错误:domain=%q version=%q calls=%d", gotDomain, gotVersion, calls.Load())
|
||||
}
|
||||
if !strings.Contains(stored.Message, "本地证书历史版本恢复完成") || !strings.Contains(stored.Message, "远端服务器部署完成") {
|
||||
t.Fatalf("恢复任务状态文案未说明本地与远端边界:%s", stored.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsyncRestoreIdempotencyAndActiveTaskConflict(t *testing.T) {
|
||||
release := make(chan struct{})
|
||||
var calls atomic.Int32
|
||||
server, tasks, _, cleanup := newTaskAPITestServer(t, nil)
|
||||
defer cleanup()
|
||||
server.restoreVersionTask = func(ctx context.Context, domainID, _ string) (certificate.IssueResult, error) {
|
||||
calls.Add(1)
|
||||
select {
|
||||
case <-release:
|
||||
return certificate.IssueResult{DomainID: domainID}, nil
|
||||
case <-ctx.Done():
|
||||
return certificate.IssueResult{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
first := httptest.NewRecorder()
|
||||
serveAdminHTTP(server, first, restoreRequest("restore-duplicate"))
|
||||
task := submittedTask(t, first)
|
||||
replay := httptest.NewRecorder()
|
||||
serveAdminHTTP(server, replay, restoreRequest("restore-duplicate"))
|
||||
if replay.Code != http.StatusConflict || !strings.Contains(replay.Body.String(), task.ID) {
|
||||
t.Fatalf("活动恢复任务幂等重放未被拒绝:%d %s", replay.Code, replay.Body.String())
|
||||
}
|
||||
conflict := httptest.NewRecorder()
|
||||
serveAdminHTTP(server, conflict, restoreRequest("restore-other"))
|
||||
if conflict.Code != http.StatusConflict {
|
||||
t.Fatalf("同域名活动恢复任务未被拒绝:%d %s", conflict.Code, conflict.Body.String())
|
||||
}
|
||||
close(release)
|
||||
waitTaskWorkersForTest(t, server)
|
||||
stored, err := tasks.GetTask(t.Context(), task.ID)
|
||||
if err != nil || stored.Status != "success" || calls.Load() != 1 {
|
||||
t.Fatalf("幂等或冲突检查影响恢复任务:%+v %v calls=%d", stored, err, calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsyncRestoreFailureDoesNotExposePrivatePath(t *testing.T) {
|
||||
server, tasks, _, cleanup := newTaskAPITestServer(t, nil)
|
||||
defer cleanup()
|
||||
server.restoreVersionTask = func(context.Context, string, string) (certificate.IssueResult, error) {
|
||||
return certificate.IssueResult{}, errors.New("读取私钥失败:/etc/scert/private/live/key.pem")
|
||||
}
|
||||
response := httptest.NewRecorder()
|
||||
serveAdminHTTP(server, response, restoreRequest("restore-failure"))
|
||||
task := submittedTask(t, response)
|
||||
waitTaskWorkersForTest(t, server)
|
||||
stored, err := tasks.GetTask(t.Context(), task.ID)
|
||||
if err != nil || stored.Status != "failed" {
|
||||
t.Fatalf("恢复失败任务未收敛:%+v %v", stored, err)
|
||||
}
|
||||
if strings.Contains(stored.Message, "/etc/scert/private") || strings.Contains(response.Body.String(), "/etc/scert/private") {
|
||||
t.Fatal("恢复失败结果泄露私钥路径")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsyncRestoreCancellationStopsWorkflow(t *testing.T) {
|
||||
started := make(chan struct{})
|
||||
server, tasks, _, cleanup := newTaskAPITestServer(t, nil)
|
||||
defer cleanup()
|
||||
server.restoreVersionTask = func(ctx context.Context, domainID, _ string) (certificate.IssueResult, error) {
|
||||
close(started)
|
||||
<-ctx.Done()
|
||||
return certificate.IssueResult{DomainID: domainID}, ctx.Err()
|
||||
}
|
||||
response := httptest.NewRecorder()
|
||||
serveAdminHTTP(server, response, restoreRequest("restore-cancel"))
|
||||
task := submittedTask(t, response)
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("恢复任务未开始执行")
|
||||
}
|
||||
cancel := httptest.NewRecorder()
|
||||
serveAdminHTTP(server, cancel, httptest.NewRequest(http.MethodDelete, "/api/v1/tasks/"+task.ID, nil))
|
||||
if cancel.Code != http.StatusAccepted {
|
||||
t.Fatalf("取消恢复任务失败:%d %s", cancel.Code, cancel.Body.String())
|
||||
}
|
||||
waitTaskWorkersForTest(t, server)
|
||||
stored, err := tasks.GetTask(t.Context(), task.ID)
|
||||
if err != nil || stored.Status != "cancelled" {
|
||||
t.Fatalf("恢复任务取消后状态错误:%+v %v", stored, err)
|
||||
}
|
||||
}
|
||||
@ -222,7 +222,7 @@ type certificateTaskResult struct {
|
||||
func (s *Server) runCertificateOperation(ctx context.Context, task repository.Task, attempt repository.TaskAttempt, auditAction string, operation func(context.Context, string) (certificateTaskResult, error), stage, runningMessage, successMessage string) {
|
||||
if task.TaskType == "certificate_restore" {
|
||||
runningMessage = "\u6b63\u5728\u6062\u590d\u672c\u5730\u8bc1\u4e66\u5386\u53f2\u7248\u672c"
|
||||
successMessage = "\u672c\u5730\u8bc1\u4e66\u5386\u53f2\u7248\u672c\u6062\u590d\u5b8c\u6210\uff0c\u5c1a\u672a\u4ee3\u8868\u8fdc\u7aef\u670d\u52a1\u5668\u90e8\u7f6e\u5b8c\u6210"
|
||||
successMessage = "\u672c\u5730\u8bc1\u4e66\u5386\u53f2\u7248\u672c\u6062\u590d\u5b8c\u6210\uff0c\u5c1a\u672a\u4ee3\u8868\u8fdc\u7aef\u670d\u52a1\u5668\u90e8\u7f72\u5b8c\u6210"
|
||||
}
|
||||
executionCtx := deployment.WithTransactionContext(ctx, deployment.TransactionContext{
|
||||
TaskID: task.ID, AttemptID: attempt.ID, AttemptNumber: attempt.AttemptNumber,
|
||||
|
||||
@ -26,6 +26,8 @@ func (s *Server) authorizeCertificateTaskExecution(ctx context.Context, task rep
|
||||
case "certificate_issue":
|
||||
permissions = append(permissions, domain.PermissionCertificateManage, domain.PermissionDNSCredentialUse)
|
||||
case "certificate_deploy":
|
||||
case "certificate_restore":
|
||||
permissions = append(permissions, domain.PermissionPrivateKeyRead)
|
||||
case "certificate_import":
|
||||
permissions = append(permissions, domain.PermissionCertificateManage)
|
||||
case "certificate_validate":
|
||||
|
||||
Loading…
Reference in New Issue
Block a user