From 423a6749493b060faa59c3a4561abea6721ba9af Mon Sep 17 00:00:00 2001 From: xuans Date: Fri, 16 Oct 2026 00:22:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=B7=B2=E9=85=8D=E7=BD=AEDN?= =?UTF-8?q?S=E8=B4=A6=E6=88=B7=E7=BC=96=E8=BE=91=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充编辑凭据前的权限与版本校验,完善名称编辑提示,并增加凭据替换、验证状态和越权写入回归测试。 --- backend/internal/httpapi/dns_account_mutations.go | 7 +++++++ .../internal/httpapi/dns_account_mutations_test.go | 12 +++++++++++- frontend/src/views/DNSAccounts/DNSAccountEditor.js | 4 +++- frontend/src/views/DNSAccounts/DNSAccounts.test.js | 1 + 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/backend/internal/httpapi/dns_account_mutations.go b/backend/internal/httpapi/dns_account_mutations.go index c6464a1..dd3b53d 100644 --- a/backend/internal/httpapi/dns_account_mutations.go +++ b/backend/internal/httpapi/dns_account_mutations.go @@ -136,6 +136,13 @@ func (s *Server) renameManagedDNSAccount(c *gin.Context) { s.writeDNSAccountError(c, err) return } + if !s.authorizeProjectPermissions(c, item.ProjectID, domain.PermissionDNSManage) { + return + } + if item.Revision != request.Revision { + s.writeDNSAccountError(c, repository.ErrDNSAccountConflict) + return + } item.Name = strings.TrimSpace(request.Name) item.CredentialRef = "secret://dns/validation" if err := item.Validate(); err != nil { diff --git a/backend/internal/httpapi/dns_account_mutations_test.go b/backend/internal/httpapi/dns_account_mutations_test.go index 9c4067d..3dad182 100644 --- a/backend/internal/httpapi/dns_account_mutations_test.go +++ b/backend/internal/httpapi/dns_account_mutations_test.go @@ -74,6 +74,15 @@ func TestManagedDNSAccountLifecycle(t *testing.T) { oldReference := stored.CredentialRef managedDNSRequest(t, f, "manager", http.MethodPut, path, `{"revision":2,"name":"更名后的账户"}`, http.StatusOK) managedDNSRequest(t, f, "manager", http.MethodPut, path, `{"revision":2,"name":"旧版本"}`, http.StatusConflict) + stored, err = f.members.GetDNSAccountForVerification(t.Context(), "manager", item.ID) + if err != nil || stored.Revision != 3 || stored.Name != "更名后的账户" || stored.CredentialRef != oldReference || stored.VerificationState != domain.DNSAccountVerified { + t.Fatal("仅修改名称没有保留账户凭据和验证状态", err) + } + managedDNSRequest(t, f, "manager", http.MethodPut, path, `{"revision":3,"name":"更名并更新凭据","credentials":{"api_token":"edited-sensitive-token"}}`, http.StatusOK) + stored, err = f.members.GetDNSAccountForVerification(t.Context(), "manager", item.ID) + if err != nil || stored.Revision != 4 || stored.Name != "更名并更新凭据" || stored.CredentialRef == oldReference || stored.VerificationState != domain.DNSAccountUnverified || len(secrets.data) != 2 { + t.Fatal("编辑凭据没有原子替换并清除验证状态", err) + } managedDNSRequest(t, f, "manager", http.MethodPut, path+"/status", `{"revision":4,"enabled":false}`, http.StatusOK) managedDNSRequest(t, f, "manager", http.MethodPost, path+"/verify", `{"revision":5,"test_domain":"example.test"}`, http.StatusForbidden) if verifications != 1 { @@ -82,7 +91,7 @@ func TestManagedDNSAccountLifecycle(t *testing.T) { managedDNSRequest(t, f, "manager", http.MethodPut, path+"/credentials", `{"revision":5,"credentials":{"api_token":"rotated-sensitive-token"}}`, http.StatusOK) managedDNSRequest(t, f, "manager", http.MethodPut, path+"/status", `{"revision":6,"enabled":true}`, http.StatusOK) stored, err = f.members.GetDNSAccountForVerification(t.Context(), "manager", item.ID) - if err != nil || stored.CredentialRef == oldReference || stored.VerificationState != domain.DNSAccountUnverified || len(secrets.data) != 2 { + if err != nil || stored.CredentialRef == oldReference || stored.VerificationState != domain.DNSAccountUnverified || len(secrets.data) != 3 { t.Fatal("轮换没有生成新引用、保留旧引用或清除验证状态", err) } f.server.dnsAccountVerifier = accountVerifierFunc(func(_ context.Context, _, _ string, credentials map[string]string) (dnsverification.Report, error) { @@ -115,6 +124,7 @@ func TestManagedDNSAccountPermissionsAndListing(t *testing.T) { managedDNSRequest(t, f, actor, http.MethodPut, "/"+item.ID+"/credentials", `{"revision":1,"credentials":{"api_token":"rotated-sensitive-token"}}`, status) managedDNSRequest(t, f, actor, http.MethodPost, "/"+item.ID+"/verify", `{"revision":1,"test_domain":"example.test"}`, status) } + managedDNSRequest(t, f, "viewer", http.MethodPut, "/"+item.ID, `{"revision":1,"name":"forbidden-edit","credentials":{"api_token":"forbidden-sensitive-token"}}`, http.StatusForbidden) if len(secrets.data) != 1 || secrets.getCalls != 0 { t.Fatal("越权请求访问了秘密存储") } diff --git a/frontend/src/views/DNSAccounts/DNSAccountEditor.js b/frontend/src/views/DNSAccounts/DNSAccountEditor.js index 6858d5c..a213ea8 100644 --- a/frontend/src/views/DNSAccounts/DNSAccountEditor.js +++ b/frontend/src/views/DNSAccounts/DNSAccountEditor.js @@ -105,7 +105,9 @@ export function useDNSAccountEditor(projects, providers, loading, refresh) { body: JSON.stringify(body), }); if (!mounted) return; - notice.value = ["create", "edit", "rotate"].includes(form.action) + const credentialChanged = Boolean(body.credentials); + notice.value = ["create", "rotate"].includes(form.action) + || (form.action === "edit" && credentialChanged) ? "账户已保存,请使用有权管理的测试域名执行在线验证" : "账户操作已完成"; } catch (reason) { if (!mounted) return; diff --git a/frontend/src/views/DNSAccounts/DNSAccounts.test.js b/frontend/src/views/DNSAccounts/DNSAccounts.test.js index e3de4f0..19d181c 100644 --- a/frontend/src/views/DNSAccounts/DNSAccounts.test.js +++ b/frontend/src/views/DNSAccounts/DNSAccounts.test.js @@ -90,6 +90,7 @@ test("编辑账户时凭据留空表示保留原配置", async (t) => { await state.saveEditor(); const write = calls.find((item) => item.method); assert.deepEqual(JSON.parse(write.body), { revision: 7, name: "仅修改名称" }); + assert.equal(state.notice.value, "账户操作已完成"); }); test("只读项目和历史配置不能发起账户写操作", async (t) => {