修复已配置DNS账户延迟导入和重复展示

将已配置的旧 DNS 账户按项目权限幂等导入为可编辑账户,避免重复展示只读兼容视图;补充回归测试和整改任务清单。
This commit is contained in:
xuans 2026-10-16 01:42:34 +08:00
parent 527bf29908
commit b188c779ab
3 changed files with 140 additions and 1 deletions

View File

@ -36,6 +36,12 @@ func (s *Server) listDNSAccounts(c *gin.Context) {
cfg := s.config
s.configMu.RUnlock()
items := make([]dnsAccountView, 0, 1)
hasLegacyAccount := false
var legacyAccount domain.DNSAccount
configuredLegacyProjectID := strings.TrimSpace(cfg.DNS.ProjectID)
if configuredLegacyProjectID == "" {
configuredLegacyProjectID = domain.DefaultProjectID
}
if repo, ok := s.projectMembers.(repository.DNSAccountRepository); ok {
accounts, err := repo.ListVisibleDNSAccounts(c.Request.Context(), c.GetString(authenticatedActorKey))
if err != nil {
@ -43,6 +49,14 @@ func (s *Server) listDNSAccounts(c *gin.Context) {
return
}
for _, account := range accounts {
if account.ID == "legacy-system-dns" {
if account.ProjectID != configuredLegacyProjectID {
continue
}
hasLegacyAccount = true
legacyAccount = account
continue
}
item, err := s.managedDNSAccountView(c, account)
if err != nil {
s.writeError(c, http.StatusServiceUnavailable, "读取 DNS 账户归属失败", nil)
@ -78,6 +92,54 @@ func (s *Server) listDNSAccounts(c *gin.Context) {
s.writeError(c, http.StatusServiceUnavailable, "读取项目 DNS 权限失败,请稍后重试", nil)
return
}
if hasLegacyAccount {
item, viewErr := s.configuredLegacyDNSAccountView(c, legacyAccount, cfg.DNS.Provider, cfg.DNS.CredentialRef, cfg.DNS.OwnerUserID, cfg.Admin.Username)
if viewErr != nil {
s.writeError(c, http.StatusServiceUnavailable, "读取 DNS 账户归属失败", nil)
return
}
items = append(items, item)
c.JSON(http.StatusOK, gin.H{"items": items})
return
}
if importer, ok := s.projectMembers.(repository.DNSLegacyAccountImporter); ok {
if managedRepo, repoOK := s.projectMembers.(repository.DNSAccountRepository); repoOK && strings.TrimSpace(cfg.DNS.Provider) != "" {
const legacyID = "legacy-system-dns"
legacyAccount, err := managedRepo.GetVisibleDNSAccount(c.Request.Context(), c.GetString(authenticatedActorKey), legacyID)
if errors.Is(err, repository.ErrProjectAccessDenied) {
legacyProjectID := strings.TrimSpace(cfg.DNS.ProjectID)
if legacyProjectID == "" {
legacyProjectID = domain.DefaultProjectID
}
legacyOwnerID := strings.TrimSpace(cfg.DNS.OwnerUserID)
if legacyOwnerID == "" {
legacyOwnerID = "primary-super-admin"
}
if err := importer.ImportLegacyDNSAccount(c.Request.Context(), domain.DNSAccount{
ID: legacyID, Name: "旧系统 DNS 账户", ProjectID: legacyProjectID,
OwnerUserID: legacyOwnerID, Provider: cfg.DNS.Provider, CredentialRef: cfg.DNS.CredentialRef,
}); err != nil {
s.writeError(c, http.StatusServiceUnavailable, "登记已配置的 DNS 账户失败,请稍后重试", nil)
return
}
legacyAccount, err = managedRepo.GetVisibleDNSAccount(c.Request.Context(), c.GetString(authenticatedActorKey), legacyID)
}
if err == nil {
item, viewErr := s.configuredLegacyDNSAccountView(c, legacyAccount, cfg.DNS.Provider, cfg.DNS.CredentialRef, cfg.DNS.OwnerUserID, cfg.Admin.Username)
if viewErr != nil {
s.writeError(c, http.StatusServiceUnavailable, "读取 DNS 账户归属失败", nil)
return
}
items = append(items, item)
c.JSON(http.StatusOK, gin.H{"items": items})
return
}
if !errors.Is(err, repository.ErrProjectAccessDenied) {
s.writeError(c, http.StatusServiceUnavailable, "读取已配置的 DNS 账户失败,请稍后重试", nil)
return
}
}
}
item := dnsAccountView{
ID: "system-dns", Name: "系统 DNS 账户", Provider: cfg.DNS.Provider,
ProjectID: projectID,
@ -129,6 +191,53 @@ func (s *Server) listDNSAccounts(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"items": items})
}
func (s *Server) configuredLegacyDNSAccountView(c *gin.Context, account domain.DNSAccount, provider, credentialRef, ownerUserID, adminUsername string) (dnsAccountView, error) {
ownerID := strings.TrimSpace(ownerUserID)
if ownerID == "" {
ownerID = account.OwnerUserID
}
if ownerID == "" {
ownerID = "primary-super-admin"
}
item := dnsAccountView{
ID: account.ID, Name: account.Name, Provider: account.Provider, ProjectID: account.ProjectID,
OwnerUserID: ownerID, OwnerState: "unassigned", OwnerUsername: "未分配",
VerificationState: account.VerificationState, CredentialState: "available",
Status: account.Status, Revision: account.Revision, Managed: true,
}
if descriptor, err := dnsprovider.GetDescriptor(provider); err == nil {
item.ProviderName = descriptor.Name
} else {
item.ProviderName = "未知服务商"
}
if ownerID == "primary-super-admin" {
item.OwnerState, item.OwnerUsername = "assigned", adminUsername
} else {
if s.users == nil {
return dnsAccountView{}, errors.New("用户归属查询暂不可用")
}
owner, err := s.users.GetUserByID(c.Request.Context(), ownerID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return dnsAccountView{}, err
}
if errors.Is(err, sql.ErrNoRows) {
item.OwnerState, item.OwnerUsername = "missing", "原用户已不存在"
} else {
item.OwnerState, item.OwnerUsername = "assigned", owner.Username
}
}
credentials, err := s.readDNSCredentials(c.Request.Context(), credentialRef)
if err != nil {
item.CredentialState, item.AccountIdentifier = "unavailable", "暂不可读取"
} else {
item.AccountIdentifier = dnsAccountIdentifier(provider, credentials)
if _, err := dnsprovider.ValidateCredentials(provider, credentials); err != nil {
item.CredentialState = "invalid"
}
}
return item, nil
}
func (s *Server) managedDNSAccountView(c *gin.Context, account domain.DNSAccount) (dnsAccountView, error) {
item := dnsAccountView{
ID: account.ID, Name: account.Name, Provider: account.Provider, ProviderName: "未知服务商",

View File

@ -103,6 +103,34 @@ func TestAdminDNSAccountsExposeProjectScope(t *testing.T) {
}
}
func TestDNSAccountsLazilyImportConfiguredAccountForEditing(t *testing.T) {
f := newProjectMemberAPIFixture(t)
f.server.config.DNS.ProjectID = "project-a"
f.server.config.DNS.Provider = "cloudflare"
f.server.config.DNS.CredentialRef = "secret://dns/cloudflare/project-a"
f.server.secrets = &fakeSecretReaderStore{data: map[string]map[string]string{
f.server.config.DNS.CredentialRef: {"api_token": "test-token"},
}}
response := serveUserRequest(f.server, f.cookies["manager"], http.MethodGet, "/api/v1/dns/accounts", "")
if response.Code != http.StatusOK {
t.Fatalf("读取已配置 DNS 账户失败:%d %s", response.Code, response.Body.String())
}
if !strings.Contains(response.Body.String(), `"id":"legacy-system-dns"`) ||
!strings.Contains(response.Body.String(), `"managed":true`) {
t.Fatalf("已配置 DNS 账户未迁移为可编辑账户:%s", response.Body.String())
}
if strings.Contains(response.Body.String(), `"id":"system-dns"`) {
t.Fatalf("已迁移账户仍重复显示旧系统只读视图:%s", response.Body.String())
}
updated := serveUserRequest(f.server, f.cookies["manager"], http.MethodPut,
"/api/v1/dns/accounts/legacy-system-dns", `{"revision":1,"name":"修改后的账户"}`)
if updated.Code != http.StatusOK {
t.Fatalf("已配置 DNS 账户不能修改:%d %s", updated.Code, updated.Body.String())
}
}
func TestDNSCredentialUseRejectsUnauthorizedProject(t *testing.T) {
server := New(Dependencies{
ProjectDomains: projectDomainRepositoryStub{authorizeErr: repository.ErrProjectAccessDenied},

View File

@ -177,7 +177,9 @@
- [✓] 编辑表单不回显 AccessKey、Secret、Token 等旧敏感值;留空表示保留当前凭据。
- [✓] 填写任意新凭据时使用白名单字段替换秘密引用,并把账户状态置为待重新验证。
- [✓] 编辑请求在写入秘密前核对实时项目 DNS 管理权限和账户版本;越权或过期请求不会写入秘密。
- [x] 后端生命周期、权限隔离和前端 26 项专项测试已通过,等待全量回归及发布复审。
- [✓] 已配置账户首次读取时按当前项目权限幂等导入为可编辑账户,不再重复展示 `system-dns` 只读兼容视图。
- [✓] 延迟导入不会绕过项目 DNS 权限;无权用户不会触发导入或读取秘密仓库。
- [✓] 后端全量测试、前端 349 项测试、`go vet ./...` 和前端生产构建均已通过,等待发布复审。
- [ ] 真实 DNS 服务商在线验证、三数据库实机回归和第一台候选包上线仍未完成。
### 验收边界