From aa529810791d5e89b346d661ce4dc98f4d00c764 Mon Sep 17 00:00:00 2001 From: xuans Date: Wed, 16 Sep 2026 18:06:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E5=87=AD=E6=8D=AE=E7=B2=BE=E7=A1=AE=E5=9B=9E?= =?UTF-8?q?=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/bootstrap/database.go | 27 ++++++++ backend/internal/bootstrap/database_test.go | 41 ++++++++++++ backend/internal/setup/dns_verification.go | 45 +++++++++++++ .../internal/setup/dns_verification_test.go | 66 +++++++++++++++++++ 4 files changed, 179 insertions(+) diff --git a/backend/internal/bootstrap/database.go b/backend/internal/bootstrap/database.go index f118d98..ec106d2 100644 --- a/backend/internal/bootstrap/database.go +++ b/backend/internal/bootstrap/database.go @@ -150,6 +150,33 @@ func (m *DatabaseVerificationManager) Consume(token, credentialRef string) (conf return m.session(token, credentialRef, true) } +// Discard 按验证令牌精确回收尚未消费的数据库凭据,不影响其他初始化会话。 +func (m *DatabaseVerificationManager) Discard(token string) error { + if m == nil { + return errors.New("数据库验证服务尚未初始化") + } + token = strings.TrimSpace(token) + if token == "" { + return errors.New("数据库验证令牌不能为空") + } + now := m.now().UTC() + m.mu.Lock() + expired := m.clearExpiredLocked(now) + session, ok := m.sessions[token] + if ok { + delete(m.sessions, token) + } + m.mu.Unlock() + m.deleteCredentials(expired) + if !ok { + return errors.New("数据库验证令牌无效或已经过期") + } + if err := m.deleteCredential(session.CredentialRef); err != nil { + return errors.New("数据库临时凭据回收失败") + } + return nil +} + func (m *DatabaseVerificationManager) session(token, credentialRef string, consume bool) (config.DatabaseConfig, error) { if m == nil { return config.DatabaseConfig{}, errors.New("数据库验证服务尚未初始化") diff --git a/backend/internal/bootstrap/database_test.go b/backend/internal/bootstrap/database_test.go index c1a59ac..d3d03f2 100644 --- a/backend/internal/bootstrap/database_test.go +++ b/backend/internal/bootstrap/database_test.go @@ -157,3 +157,44 @@ func TestSQLiteRejectsUserControlledPathAndExternalFields(t *testing.T) { t.Fatal("SQLite 用户自定义路径未被拒绝") } } + +func TestDatabaseVerificationDiscardOnlyRemovesSelectedToken(t *testing.T) { + base := config.Default(t.TempDir()) + store := &memoryDatabaseStore{} + manager := NewDatabaseVerificationManager(base, store) + manager.verify = func(context.Context, config.DatabaseConfig) error { return nil } + + first, err := manager.Verify(t.Context(), DatabaseInput{ + Driver: "mysql", Host: "db.example.test", Database: "scert", + Username: "first_user", Password: "first-password", TLSMode: "true", + }) + if err != nil { + t.Fatal(err) + } + second, err := manager.Verify(t.Context(), DatabaseInput{ + Driver: "postgres", Host: "db.example.test", Database: "scert", + Username: "second_user", Password: "second-password", TLSMode: "verify-full", + }) + if err != nil { + t.Fatal(err) + } + if err := manager.Discard(first.VerificationToken); err != nil { + t.Fatalf("精确回收数据库验证令牌失败:%v", err) + } + if _, err := manager.Check(first.VerificationToken, first.CredentialRef); err == nil { + t.Fatal("已回收的数据库验证令牌仍然有效") + } + if _, err := manager.Check(second.VerificationToken, second.CredentialRef); err != nil { + t.Fatalf("精确回收影响了其他数据库验证令牌:%v", err) + } + store.mu.Lock() + _, firstExists := store.values[first.CredentialRef] + _, secondExists := store.values[second.CredentialRef] + store.mu.Unlock() + if firstExists || !secondExists { + t.Fatalf("数据库凭据回收范围错误:first=%v second=%v", firstExists, secondExists) + } + if err := manager.Discard(first.VerificationToken); err == nil { + t.Fatal("数据库验证令牌被重复回收") + } +} diff --git a/backend/internal/setup/dns_verification.go b/backend/internal/setup/dns_verification.go index 922ca4c..c4b2d5d 100644 --- a/backend/internal/setup/dns_verification.go +++ b/backend/internal/setup/dns_verification.go @@ -172,6 +172,51 @@ func (m *DNSVerificationManager) Consume(token, providerID, credentialRef, testD return m.validateSession(token, providerID, credentialRef, testDomain, true) } +// Discard 按验证令牌精确回收尚未消费的 DNS 临时凭据,不影响其他初始化会话。 +func (m *DNSVerificationManager) Discard(token string) error { + if m == nil { + return errors.New("DNS 在线验证服务未初始化") + } + token = strings.TrimSpace(token) + if token == "" { + return errors.New("DNS 验证令牌不能为空") + } + now := m.now().UTC() + m.mu.Lock() + expiredRefs := m.clearExpiredLocked(now) + session, ok := m.sessions[token] + if ok { + delete(m.sessions, token) + } + m.mu.Unlock() + m.deleteCredentials(expiredRefs) + if !ok { + return errors.New("DNS 验证令牌无效或已经过期") + } + if err := m.deleteCredential(session.CredentialRef); err != nil { + m.logCleanupFailure() + return errors.New("DNS 临时凭据回收失败") + } + return nil +} + +// DiscardAll 回收所有尚未消费的 DNS 验证令牌及其临时凭据。 +func (m *DNSVerificationManager) DiscardAll() { + if m == nil { + return + } + m.mu.Lock() + references := make([]string, 0, len(m.sessions)) + for token, session := range m.sessions { + delete(m.sessions, token) + if strings.TrimSpace(session.CredentialRef) != "" { + references = append(references, session.CredentialRef) + } + } + m.mu.Unlock() + m.deleteCredentials(references) +} + func (m *DNSVerificationManager) validateSession(token, providerID, credentialRef, testDomain string, consume bool) error { if m == nil { return errors.New("DNS 在线验证服务未初始化") diff --git a/backend/internal/setup/dns_verification_test.go b/backend/internal/setup/dns_verification_test.go index 5cc28b3..70aba2e 100644 --- a/backend/internal/setup/dns_verification_test.go +++ b/backend/internal/setup/dns_verification_test.go @@ -123,6 +123,72 @@ func TestDNSVerificationManagerRejectsMismatchedConfiguration(t *testing.T) { } } +func TestDNSVerificationDiscardOnlyRemovesSelectedToken(t *testing.T) { + store := &memorySecretWriter{} + manager := NewDNSVerificationManager(fakeDNSTester{}, store) + first, err := manager.Verify(t.Context(), DNSVerificationInput{ + ProviderID: "aliyun", TestDomain: "first.example.com", + Credentials: map[string]string{"access_key_id": "first-id", "access_key_secret": "first-secret"}, + }) + if err != nil { + t.Fatal(err) + } + second, err := manager.Verify(t.Context(), DNSVerificationInput{ + ProviderID: "aliyun", TestDomain: "second.example.com", + Credentials: map[string]string{"access_key_id": "second-id", "access_key_secret": "second-secret"}, + }) + if err != nil { + t.Fatal(err) + } + if err := manager.Discard(first.VerificationToken); err != nil { + t.Fatalf("精确回收 DNS 验证令牌失败:%v", err) + } + if err := manager.Check(first.VerificationToken, "aliyun", first.CredentialRef, "first.example.com"); err == nil { + t.Fatal("已回收的 DNS 验证令牌仍然有效") + } + if err := manager.Check(second.VerificationToken, "aliyun", second.CredentialRef, "second.example.com"); err != nil { + t.Fatalf("精确回收影响了其他 DNS 验证令牌:%v", err) + } + store.mu.Lock() + _, firstExists := store.values[first.CredentialRef] + _, secondExists := store.values[second.CredentialRef] + store.mu.Unlock() + if firstExists || !secondExists { + t.Fatalf("DNS 凭据回收范围错误:first=%v second=%v", firstExists, secondExists) + } + if err := manager.Discard(first.VerificationToken); err == nil { + t.Fatal("DNS 验证令牌被重复回收") + } +} + +func TestDNSVerificationDiscardAllRemovesPendingCredentials(t *testing.T) { + store := &memorySecretWriter{} + manager := NewDNSVerificationManager(fakeDNSTester{}, store) + results := make([]DNSVerificationResult, 0, 2) + for _, testDomain := range []string{"first.example.com", "second.example.com"} { + result, err := manager.Verify(t.Context(), DNSVerificationInput{ + ProviderID: "aliyun", TestDomain: testDomain, + Credentials: map[string]string{"access_key_id": "test-id", "access_key_secret": "test-secret"}, + }) + if err != nil { + t.Fatal(err) + } + results = append(results, result) + } + manager.DiscardAll() + for _, result := range results { + if err := manager.Check(result.VerificationToken, "aliyun", result.CredentialRef, "first.example.com"); err == nil { + t.Fatal("DiscardAll 后 DNS 验证令牌仍然有效") + } + } + store.mu.Lock() + remaining := len(store.values) + store.mu.Unlock() + if remaining != 0 { + t.Fatalf("DiscardAll 后仍有 DNS 临时凭据:%d", remaining) + } +} + func TestDNSVerificationManagerRejectsExpiredToken(t *testing.T) { store := &memorySecretWriter{} manager := NewDNSVerificationManager(fakeDNSTester{}, store)