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;
   });

Reply via email to