补充DNS过期凭据清理重试

修复DNS验证会话过期后临时凭据清理失败的可恢复性,保留原会话以便重试且不恢复过期授权,补充回归测试并更新四份整改清单。
This commit is contained in:
xuans 2026-09-30 12:09:29 +08:00
parent 4ec81eecab
commit f9dcb37763
6 changed files with 119 additions and 9 deletions

View File

@ -79,6 +79,11 @@ type dnsVerificationSession struct {
ExpiresAt time.Time
}
type expiredDNSVerificationSession struct {
Token string
Session dnsVerificationSession
}
// DNSVerificationSession 仅供服务端完整初始化事务读取验证结果。
// 所有字段均禁止 JSON 序列化,避免凭据引用被误写入 HTTP 响应。
type DNSVerificationSession struct {
@ -89,20 +94,21 @@ type DNSVerificationSession struct {
}
type DNSVerificationManager struct {
mu sync.Mutex
tester DNSCredentialTester
store SecretWriter
deleter SecretDeleter
now func() time.Time
sessions map[string]dnsVerificationSession
inFlight int
mu sync.Mutex
tester DNSCredentialTester
store SecretWriter
deleter SecretDeleter
now func() time.Time
sessions map[string]dnsVerificationSession
expiredSession map[string]expiredDNSVerificationSession
inFlight int
}
func NewDNSVerificationManager(tester DNSCredentialTester, store SecretWriter) *DNSVerificationManager {
deleter, _ := store.(SecretDeleter)
return &DNSVerificationManager{
tester: tester, store: store, deleter: deleter, now: time.Now,
sessions: make(map[string]dnsVerificationSession),
sessions: make(map[string]dnsVerificationSession), expiredSession: make(map[string]expiredDNSVerificationSession),
}
}
@ -396,7 +402,10 @@ func (m *DNSVerificationManager) clearExpiredExceptLocked(now time.Time, exclude
}
if !session.ExpiresAt.After(now) {
delete(m.sessions, token)
references = append(references, session.CredentialRef)
if reference := strings.TrimSpace(session.CredentialRef); reference != "" {
m.expiredSession[reference] = expiredDNSVerificationSession{Token: token, Session: session}
references = append(references, reference)
}
}
}
return references
@ -416,11 +425,39 @@ func (m *DNSVerificationManager) restoreSession(token string, session dnsVerific
func (m *DNSVerificationManager) deleteCredentials(references []string) {
for _, reference := range references {
if err := m.deleteCredential(reference); err != nil {
m.restoreExpiredSession(reference)
m.logCleanupFailure()
continue
}
m.forgetExpiredSession(reference)
}
}
func (m *DNSVerificationManager) restoreExpiredSession(reference string) {
if m == nil || strings.TrimSpace(reference) == "" {
return
}
m.mu.Lock()
defer m.mu.Unlock()
expired, ok := m.expiredSession[reference]
if !ok {
return
}
if _, exists := m.sessions[expired.Token]; !exists {
m.sessions[expired.Token] = expired.Session
}
delete(m.expiredSession, reference)
}
func (m *DNSVerificationManager) forgetExpiredSession(reference string) {
if m == nil || strings.TrimSpace(reference) == "" {
return
}
m.mu.Lock()
delete(m.expiredSession, reference)
m.mu.Unlock()
}
func (m *DNSVerificationManager) logCleanupFailure() {
log.Print("DNS 临时凭据回收失败")
}

View File

@ -546,3 +546,33 @@ func TestDNSVerificationResolveIsServerOnlyAndDoesNotConsumeToken(t *testing.T)
t.Fatal("已消费的 DNS 验证令牌仍可被解析")
}
}
func TestDNSVerificationManagerRetriesExpiredCredentialCleanup(t *testing.T) {
store := &retryableSecretWriter{failDelete: true}
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
now := time.Date(2026, 9, 30, 12, 0, 0, 0, time.UTC)
manager.now = func() time.Time { return now }
result, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "cloudflare", TestDomain: "example.com",
Credentials: map[string]string{"api_token": "secret"},
})
if err != nil {
t.Fatal(err)
}
now = now.Add(dnsVerificationLifetime + time.Second)
if err := manager.Check(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err == nil {
t.Fatal("expired DNS verification token should be rejected")
}
if len(manager.sessions) != 1 || len(store.values) != 1 {
t.Fatalf("failed expired cleanup should remain retryable: sessions=%d values=%d", len(manager.sessions), len(store.values))
}
store.failDelete = false
if err := manager.Check(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err == nil {
t.Fatal("expired DNS verification token should remain rejected after cleanup retry")
}
if len(manager.sessions) != 0 || len(store.values) != 0 {
t.Fatalf("expired cleanup retry left temporary state: sessions=%d values=%d", len(manager.sessions), len(store.values))
}
}

View File

@ -3769,3 +3769,14 @@ Linux Ubuntu 22.04 上的 `Scert-admin` 手动启停、无开机自启动、无
- [~] 进行中:DNS 过期清理与初始化回滚矩阵、其他 P0-10 任务链、跨数据库实机验证和统一复审。
- [ ] 未开始:P0-12、真实数据库清理初始化、密码 SSH、远端多服务器部署及最终生产门禁。
- [!] 不得标记 `[✓]`,总体验收仍为 **P0-03:完成,待复审**。
## 本轮开发进度(2026-09-30,P0-10 DNS 过期凭据清理重试)
总体验收继续保持 **P0-03:完成,待复审**,本轮继续先开发、后统一测试。
- [x] 已完成,等待复审:过期 DNS 验证会话清理失败时恢复原会话,后续操作可重试删除临时凭据。
- [x] 已完成,等待复审:清理重试成功后仍拒绝过期令牌,避免凭据回收修复引入授权回退。
- [x] 已完成:新增过期清理失败回归测试;后端全量测试、静态检查和差异检查均通过。
- [~] 进行中:DNS 初始化回滚矩阵、令牌生命周期、其他 P0-10 任务链、跨数据库实机验证和统一复审。
- [ ] 未开始:P0-12、真实数据库清理初始化、密码 SSH、远端多服务器部署及最终生产门禁。
- [!] 不得标记 `[✓]`,总体验收仍为 **P0-03:完成,待复审**。

View File

@ -1074,3 +1074,14 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全
- [~] 继续执行:DNS 过期清理、初始化回滚、令牌生命周期、任务链、三数据库实机矩阵和统一复审。
- [ ] 尚未执行:P0-12、真实生产数据库清理、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁。
- [!] 不新增 `[✓]`;总体验收继续为 **P0-03:完成,待复审**。
## 本轮执行记录(2026-09-30,P0-10 DNS 过期凭据清理重试)
本轮按“先开发、后统一测试”执行,总体验收仍保持 **P0-03:完成,待复审**。
- [x] 过期 DNS 会话删除临时凭据失败时恢复原会话,避免清理失败造成不可重试残留。
- [x] 清理重试成功后令牌仍不可用,验证会话不会因补偿清理重新获得授权。
- [x] `go test ./...`、`go vet ./...`、`git diff --check` 已通过;本轮仅完成本地开发与自动化回归。
- [~] 继续执行:DNS 初始化回滚、令牌消费、任务链、三数据库实机矩阵和统一复审。
- [ ] 尚未执行:P0-12、真实生产数据库清理、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁。
- [!] 不新增 `[✓]`;总体验收继续为 **P0-03:完成,待复审**。

View File

@ -1722,3 +1722,14 @@ P0-11 生产验收门禁
- [~] 进行中:DNS 过期清理、初始化回滚、令牌消费、其他 P0-10 任务链、跨数据库实机矩阵和统一复审。
- [ ] 未开始:P0-12 备份恢复、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁。
- [!] 总体验收不得升级:仍为 **P0-03:完成,待复审**,本轮没有新增 `[✓]` 复审通过项。
## 本轮开发进度(2026-09-30,P0-10 DNS 过期凭据清理重试)
总体验收位置继续保持 **P0-03:完成,待复审**;本轮仅补齐 DNS 验证令牌过期后的临时凭据清理重试,不代表 P0-10 统一复审通过或生产部署验收通过。
- [x] 已完成,等待复审:过期验证会话对应的临时凭据删除失败时恢复原会话,保留后续清理入口,不再静默丢失。
- [x] 已完成,等待复审:清理重试成功后过期会话和临时凭据同时移除,令牌仍保持过期不可用。
- [x] 已完成:新增过期清理失败与重试回归测试;后端 `go test ./...`、`go vet ./...` 和 `git diff --check` 通过。
- [~] 进行中:DNS 初始化回滚、令牌消费、其他 P0-10 任务链、跨数据库实机矩阵和统一复审。
- [ ] 未开始:P0-12 备份恢复、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁。
- [!] 总体验收不得升级:仍为 **P0-03:完成,待复审**,本轮没有新增 `[✓]` 复审通过项。

View File

@ -647,3 +647,13 @@ P0-06 与 P0-07 均维持 `[~]`,六阶段中的隔离、生产和最终验收
- [x] 已新增对应回归测试,并通过后端全量测试、静态检查和差异检查。
- [~] DNS 过期清理、初始化回滚、令牌消费及 P0-10 其他任务链继续开发和统一复审。
- [!] 本地自动化结果不得替代真实 DNS/ACME、密码 SSH、远端多服务器部署和最终生产门禁;不得新增 `[✓]`。
## 本轮规格执行记录(2026-09-30,P0-10 DNS 过期凭据清理重试)
本轮仅补齐 DNS 验证令牌过期后的临时凭据清理重试要求,总体验收继续保持 **P0-03:完成,待复审**。
- [x] 过期会话临时凭据清理失败时必须恢复原会话,后续操作必须能够再次触发清理。
- [x] 补偿清理成功后必须保持令牌过期,不得因恢复清理状态而重新授权验证结果。
- [x] 已新增对应回归测试,并通过后端全量测试、静态检查和差异检查。
- [~] DNS 初始化回滚、令牌消费及 P0-10 其他任务链继续开发和统一复审。
- [!] 本地自动化结果不得替代真实 DNS/ACME、密码 SSH、远端多服务器部署和最终生产门禁;不得新增 `[✓]`。