This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 2787d225 fix: harden K8s certificate management (#644)
2787d225 is described below
commit 2787d22588d527cbe5d63da0c439dc8a5e1c87ac
Author: aias00 <[email protected]>
AuthorDate: Tue Jul 28 07:32:25 2026 -0700
fix: harden K8s certificate management (#644)
---
.../rocketmq/studio/cluster/k8s/CreateCertDTO.java | 8 ++
.../rocketmq/studio/cluster/k8s/DeleteCertDTO.java | 2 +
.../studio/cluster/k8s/K8sCertController.java | 9 +-
.../rocketmq/studio/cluster/k8s/RenewCertDTO.java | 2 +
.../rocketmq/studio/cluster/k8s/UpdateCertDTO.java | 4 +
.../studio/cluster/k8s/K8sCertControllerTest.java | 84 +++++++++++++++++++
.../pages/cluster/__tests__/K8sCertsPage.test.tsx | 98 ++++++++++++++++++++++
web/src/pages/cluster/certs.tsx | 7 +-
8 files changed, 207 insertions(+), 7 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/CreateCertDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/CreateCertDTO.java
index 939c7035..0f3a7010 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/CreateCertDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/CreateCertDTO.java
@@ -16,6 +16,8 @@
*/
package org.apache.rocketmq.studio.cluster.k8s;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -28,10 +30,16 @@ import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
public class CreateCertDTO {
+ @NotBlank(message = "name is required")
private String name;
+ @NotBlank(message = "namespace is required")
private String namespace;
+ @NotBlank(message = "cluster is required")
private String cluster;
+ @NotBlank(message = "type is required")
+ @Pattern(regexp = "TLS|mTLS|ServiceAccount", message = "type must be one
of TLS, mTLS, ServiceAccount")
private String type;
+ @NotBlank(message = "issuer is required")
private String issuer;
private List<String> san;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/DeleteCertDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/DeleteCertDTO.java
index c81712ac..2b0cbd89 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/DeleteCertDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/DeleteCertDTO.java
@@ -16,6 +16,7 @@
*/
package org.apache.rocketmq.studio.cluster.k8s;
+import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -26,5 +27,6 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
public class DeleteCertDTO {
+ @NotBlank(message = "id is required")
private String id;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertController.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertController.java
index e4d7765b..10ce3c58 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertController.java
@@ -17,6 +17,7 @@
package org.apache.rocketmq.studio.cluster.k8s;
import org.apache.rocketmq.studio.common.domain.Result;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -39,22 +40,22 @@ public class K8sCertController {
}
@PostMapping("/create")
- public Result<K8sCertVO> createCert(@RequestBody CreateCertDTO command) {
+ public Result<K8sCertVO> createCert(@Valid @RequestBody CreateCertDTO
command) {
return Result.ok(k8sCertService.createCert(command));
}
@PostMapping("/update")
- public Result<K8sCertVO> updateCert(@RequestBody UpdateCertDTO command) {
+ public Result<K8sCertVO> updateCert(@Valid @RequestBody UpdateCertDTO
command) {
return Result.ok(k8sCertService.updateCert(command));
}
@PostMapping("/renew")
- public Result<K8sCertVO> renewCert(@RequestBody RenewCertDTO command) {
+ public Result<K8sCertVO> renewCert(@Valid @RequestBody RenewCertDTO
command) {
return Result.ok(k8sCertService.renewCert(command));
}
@PostMapping("/delete")
- public Result<Void> deleteCert(@RequestBody DeleteCertDTO command) {
+ public Result<Void> deleteCert(@Valid @RequestBody DeleteCertDTO command) {
k8sCertService.deleteCert(command);
return Result.ok();
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/RenewCertDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/RenewCertDTO.java
index 3cf3d609..5a14920d 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/RenewCertDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/RenewCertDTO.java
@@ -16,6 +16,7 @@
*/
package org.apache.rocketmq.studio.cluster.k8s;
+import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -26,5 +27,6 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
public class RenewCertDTO {
+ @NotBlank(message = "id is required")
private String id;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/UpdateCertDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/UpdateCertDTO.java
index 8e1a5ff9..e525b0d9 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/UpdateCertDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/UpdateCertDTO.java
@@ -16,6 +16,8 @@
*/
package org.apache.rocketmq.studio.cluster.k8s;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -28,10 +30,12 @@ import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
public class UpdateCertDTO {
+ @NotBlank(message = "id is required")
private String id;
private String name;
private String namespace;
private String cluster;
+ @Pattern(regexp = "TLS|mTLS|ServiceAccount", message = "type must be one
of TLS, mTLS, ServiceAccount")
private String type;
private String issuer;
private List<String> san;
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java
index 9756c65b..10abd3ae 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java
@@ -33,6 +33,7 @@ import java.util.Collections;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -138,6 +139,89 @@ class K8sCertControllerTest {
.andExpect(jsonPath("$.data.id").value("cert-min"));
}
+ @Test
+ void createCertShouldRejectMissingName() throws Exception {
+ mockMvc.perform(post("/api/k8s-certs/create")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("""
+ {
+ "namespace": "default",
+ "cluster": "test-cluster",
+ "type": "TLS",
+ "issuer": "vault"
+ }
+ """))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("name is required"));
+
+ verifyNoInteractions(k8sCertService);
+ }
+
+ @Test
+ void createCertShouldRejectUnsupportedType() throws Exception {
+ mockMvc.perform(post("/api/k8s-certs/create")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("""
+ {
+ "name": "new-cert",
+ "namespace": "default",
+ "cluster": "test-cluster",
+ "type": "PEM",
+ "issuer": "vault"
+ }
+ """))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("type must be one of
TLS, mTLS, ServiceAccount"));
+
+ verifyNoInteractions(k8sCertService);
+ }
+
+ @Test
+ void updateCertShouldRejectMissingId() throws Exception {
+ mockMvc.perform(post("/api/k8s-certs/update")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("""
+ {
+ "name": "updated-cert"
+ }
+ """))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("id is required"));
+
+ verifyNoInteractions(k8sCertService);
+ }
+
+ @Test
+ void renewCertShouldRejectBlankId() throws Exception {
+ mockMvc.perform(post("/api/k8s-certs/renew")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("""
+ {
+ "id": " "
+ }
+ """))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("id is required"));
+
+ verifyNoInteractions(k8sCertService);
+ }
+
+ @Test
+ void deleteCertShouldRejectMissingId() throws Exception {
+ mockMvc.perform(post("/api/k8s-certs/delete")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("id is required"));
+
+ verifyNoInteractions(k8sCertService);
+ }
+
private K8sCertVO buildCert(String id, String name, CertType type,
CertStatus status) {
K8sCertVO cert = K8sCertVO.builder()
.name(name)
diff --git a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
new file mode 100644
index 00000000..0e7381d6
--- /dev/null
+++ b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { App } from 'antd';
+import type { K8sCertInfo } from '../../../api/cluster';
+import { listK8sCerts } from '../../../services/clusterService';
+import K8sCertsPage from '../certs';
+
+vi.mock('../../../services/clusterService', () => ({
+ createK8sCert: vi.fn(),
+ deleteK8sCert: vi.fn(),
+ listK8sCerts: vi.fn(),
+ renewK8sCert: vi.fn(),
+ updateK8sCert: vi.fn(),
+}));
+
+const certs: K8sCertInfo[] = [
+ {
+ id: 'cert-prod',
+ name: 'rocketmq-prod-tls',
+ namespace: 'rocketmq',
+ cluster: 'prod-cluster',
+ type: 'TLS',
+ issuer: 'kubernetes-ca',
+ notBefore: '2026-01-01T00:00:00Z',
+ notAfter: '2027-01-01T00:00:00Z',
+ status: 'valid',
+ daysRemaining: 365,
+ san: ['broker.prod.example.com'],
+ },
+ {
+ id: 'cert-staging',
+ name: 'rocketmq-staging-tls',
+ namespace: 'rocketmq',
+ cluster: 'staging-cluster',
+ type: 'TLS',
+ issuer: 'kubernetes-ca',
+ notBefore: '2026-01-01T00:00:00Z',
+ notAfter: '2027-01-01T00:00:00Z',
+ status: 'valid',
+ daysRemaining: 365,
+ san: ['broker.staging.example.com'],
+ },
+];
+
+beforeAll(() => {
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+});
+
+describe('K8sCertsPage', () => {
+ beforeEach(() => {
+ vi.mocked(listK8sCerts).mockResolvedValue(certs);
+ });
+
+ it('trims certificate search text before filtering', async () => {
+ const user = userEvent.setup();
+ render(
+ <App>
+ <K8sCertsPage />
+ </App>,
+ );
+
+ await screen.findByText('rocketmq-prod-tls');
+ await user.type(screen.getByPlaceholderText('搜索证书名称或集群'), ' prod-cluster
{enter}');
+
+ expect(screen.getByText('rocketmq-prod-tls')).toBeInTheDocument();
+ expect(screen.queryByText('rocketmq-staging-tls')).not.toBeInTheDocument();
+ });
+});
diff --git a/web/src/pages/cluster/certs.tsx b/web/src/pages/cluster/certs.tsx
index dfa0da2c..a9408b13 100644
--- a/web/src/pages/cluster/certs.tsx
+++ b/web/src/pages/cluster/certs.tsx
@@ -129,11 +129,12 @@ const K8sCertsPage = () => {
}
};
+ const normalizedCertSearch = certSearch.trim().toLowerCase();
const filteredCerts = certs.filter((cert) => {
const matchSearch =
- !certSearch ||
- cert.name.toLowerCase().includes(certSearch.toLowerCase()) ||
- cert.cluster.toLowerCase().includes(certSearch.toLowerCase());
+ !normalizedCertSearch ||
+ cert.name.toLowerCase().includes(normalizedCertSearch) ||
+ cert.cluster.toLowerCase().includes(normalizedCertSearch);
const matchType = !certTypeFilter || cert.type === certTypeFilter;
return matchSearch && matchType;
});