完善已配置DNS账户编辑校验

补充编辑凭据前的权限与版本校验,完善名称编辑提示,并增加凭据替换、验证状态和越权写入回归测试。
This commit is contained in:
xuans 2026-10-16 00:22:26 +08:00
parent 4aca34c16d
commit 423a674949
4 changed files with 22 additions and 2 deletions

View File

@ -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 {

View File

@ -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("越权请求访问了秘密存储")
}

View File

@ -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;

View File

@ -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) => {