This is an automated email from the ASF dual-hosted git repository.

tomsun28 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/master by this push:
     new c9e3338c9f [improve] mask secret fields of notice receiver in rest api 
response (#4220)
c9e3338c9f is described below

commit c9e3338c9f287753aced7ee498969ad0b8420f3d
Author: Duansg <[email protected]>
AuthorDate: Tue Jul 21 23:11:30 2026 +0800

    [improve] mask secret fields of notice receiver in rest api response (#4220)
---
 .../alert/controller/NoticeConfigController.java   |   9 +-
 .../service/impl/NoticeConfigServiceImpl.java      |  16 +++
 .../alert/util/NoticeReceiverMaskUtil.java         | 125 +++++++++++++++++++
 .../controller/NoticeConfigControllerTest.java     |   5 +
 .../alert/service/NoticeConfigServiceTest.java     |  35 ++++++
 .../alert/util/NoticeReceiverMaskUtilTest.java     | 134 +++++++++++++++++++++
 .../common/entity/alerter/NoticeReceiver.java      |   2 +-
 7 files changed, 322 insertions(+), 4 deletions(-)

diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java
index 7a349284ff..151fc0b12f 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java
@@ -30,6 +30,7 @@ import 
org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
 import org.apache.hertzbeat.common.entity.alerter.NoticeRule;
 import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
 import org.apache.hertzbeat.alert.service.NoticeConfigService;
+import org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
 import org.springframework.http.ResponseEntity;
@@ -87,14 +88,16 @@ public class NoticeConfigController {
             @Parameter(description = "en: Recipient name,support fuzzy query", 
example = "tom") @RequestParam(required = false) final String name,
             @Parameter(description = "en: List current page", example = "0") 
@RequestParam(defaultValue = "0") final int pageIndex,
             @Parameter(description = "en: Number of list pages", example = 
"8") @RequestParam(defaultValue = "8") final int pageSize) {
-        return 
ResponseEntity.ok(Message.success(noticeConfigService.getNoticeReceivers(name, 
pageIndex, pageSize)));
+        return 
ResponseEntity.ok(Message.success(noticeConfigService.getNoticeReceivers(name, 
pageIndex, pageSize)
+                .map(NoticeReceiverMaskUtil::mask)));
     }
 
     @GetMapping(path = "/receivers/all")
     @Operation(summary = "Get a list of all message notification recipients",
             description = "Get a list of all message notification recipients")
     public ResponseEntity<Message<List<NoticeReceiver>>> getAllReceivers() {
-        return 
ResponseEntity.ok(Message.success(noticeConfigService.getAllNoticeReceivers()));
+        return 
ResponseEntity.ok(Message.success(noticeConfigService.getAllNoticeReceivers().stream()
+                .map(NoticeReceiverMaskUtil::mask).toList()));
     }
 
     @GetMapping(path = "/receiver/{id}")
@@ -106,7 +109,7 @@ public class NoticeConfigController {
         if (noticeReceiver == null) {
             return ResponseEntity.ok(Message.fail(FAIL_CODE, "The relevant 
information of the recipient could not be found, please check whether the 
parameters are correct or refresh the page"));
         }
-        return ResponseEntity.ok(Message.success(noticeReceiver));
+        return 
ResponseEntity.ok(Message.success(NoticeReceiverMaskUtil.mask(noticeReceiver)));
     }
 
     @PostMapping(path = "/rule")
diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java
index 24a719b5bf..2a16f39ab3 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java
@@ -31,6 +31,7 @@ import org.apache.hertzbeat.alert.dao.NoticeReceiverDao;
 import org.apache.hertzbeat.alert.dao.NoticeRuleDao;
 import org.apache.hertzbeat.alert.dao.NoticeTemplateDao;
 import org.apache.hertzbeat.alert.service.NoticeConfigService;
+import org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.CommandLineRunner;
@@ -176,9 +177,23 @@ public class NoticeConfigServiceImpl implements 
NoticeConfigService, CommandLine
 
     @Override
     public void editReceiver(NoticeReceiver noticeReceiver) {
+        resolveMaskedSecrets(noticeReceiver);
         noticeReceiverDao.save(noticeReceiver);
     }
 
+    /**
+     * The rest api returns receivers with masked secret fields, so a receiver 
submitted
+     * from the ui may carry the mask placeholder instead of the real secret.
+     * Restore such fields from the stored entity before using the receiver.
+     */
+    private void resolveMaskedSecrets(NoticeReceiver noticeReceiver) {
+        if (noticeReceiver == null || noticeReceiver.getId() == null) {
+            return;
+        }
+        noticeReceiverDao.findById(noticeReceiver.getId())
+                .ifPresent(existing -> 
NoticeReceiverMaskUtil.resolveMask(noticeReceiver, existing));
+    }
+
     @Override
     public void deleteReceiver(Long receiverId) {
         noticeReceiverDao.deleteById(receiverId);
@@ -319,6 +334,7 @@ public class NoticeConfigServiceImpl implements 
NoticeConfigService, CommandLine
 
     @Override
     public boolean sendTestMsg(NoticeReceiver noticeReceiver) {
+        resolveMaskedSecrets(noticeReceiver);
         Map<String, String> labels = new HashMap<>(8);
         labels.put(CommonConstants.LABEL_INSTANCE, "127.0.0.1");
         labels.put(CommonConstants.LABEL_ALERT_NAME, "CPU Usage Alert");
diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/NoticeReceiverMaskUtil.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/NoticeReceiverMaskUtil.java
new file mode 100644
index 0000000000..db5fe30a43
--- /dev/null
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/NoticeReceiverMaskUtil.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+
+package org.apache.hertzbeat.alert.util;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
+
+/**
+ * Masks the secret fields of {@link NoticeReceiver} before it is exposed 
through the rest api,
+ * and resolves masked values back to the stored secrets when an edited 
receiver is submitted.
+ * A secret long enough keeps its last characters visible so different tokens 
stay
+ * distinguishable in the ui, while an unchanged secret round-trips through 
the ui
+ * without ever leaving the server.
+ */
+public final class NoticeReceiverMaskUtil {
+
+    /**
+     * Fixed-length placeholder replacing the hidden part of a secret. Its 
length is
+     * constant on purpose so the mask never reveals how long the real secret 
is.
+     */
+    public static final String SECRET_MASK = "******";
+
+    /**
+     * A secret shorter than this is masked entirely: revealing a suffix of a 
short
+     * secret would give away too large a fraction of it.
+     */
+    private static final int MIN_LENGTH_TO_SHOW_SUFFIX = 12;
+
+    /**
+     * Number of trailing characters kept visible for a long secret,
+     * enough to tell configured tokens apart.
+     */
+    private static final int VISIBLE_SUFFIX_LENGTH = 4;
+
+    private record SecretField(Function<NoticeReceiver, String> getter, 
BiConsumer<NoticeReceiver, String> setter) {
+    }
+
+    private static final List<SecretField> SECRET_FIELDS = List.of(
+            new SecretField(NoticeReceiver::getHookAuthToken, 
NoticeReceiver::setHookAuthToken),
+            new SecretField(NoticeReceiver::getAccessToken, 
NoticeReceiver::setAccessToken),
+            new SecretField(NoticeReceiver::getTgBotToken, 
NoticeReceiver::setTgBotToken),
+            new SecretField(NoticeReceiver::getSlackWebHookUrl, 
NoticeReceiver::setSlackWebHookUrl),
+            new SecretField(NoticeReceiver::getAppSecret, 
NoticeReceiver::setAppSecret),
+            new SecretField(NoticeReceiver::getDiscordBotToken, 
NoticeReceiver::setDiscordBotToken),
+            new SecretField(NoticeReceiver::getSmnAk, 
NoticeReceiver::setSmnAk),
+            new SecretField(NoticeReceiver::getSmnSk, 
NoticeReceiver::setSmnSk),
+            new SecretField(NoticeReceiver::getServerChanToken, 
NoticeReceiver::setServerChanToken),
+            new SecretField(NoticeReceiver::getGotifyToken, 
NoticeReceiver::setGotifyToken),
+            new SecretField(NoticeReceiver::getNtfyToken, 
NoticeReceiver::setNtfyToken));
+
+    private NoticeReceiverMaskUtil() {
+    }
+
+    /**
+     * Return a copy of the receiver with all secret fields masked.
+     * The given entity is not modified as it may still be attached to the 
persistence context.
+     * @param receiver receiver to mask, may be null
+     * @return masked copy, or null if receiver is null
+     */
+    public static NoticeReceiver mask(NoticeReceiver receiver) {
+        if (receiver == null) {
+            return null;
+        }
+        NoticeReceiver masked = receiver.toBuilder().build();
+        for (SecretField field : SECRET_FIELDS) {
+            field.setter().accept(masked, 
maskValue(field.getter().apply(masked)));
+        }
+        return masked;
+    }
+
+    /**
+     * Replace every secret field of the incoming receiver that still holds 
the masked form
+     * of the stored secret with the stored value, so an edit that did not 
touch a secret keeps it.
+     * A re-entered secret or a cleared field is left untouched.
+     * @param incoming receiver submitted by the ui, modified in place
+     * @param existing receiver currently stored in the database
+     */
+    public static void resolveMask(NoticeReceiver incoming, NoticeReceiver 
existing) {
+        if (incoming == null || existing == null) {
+            return;
+        }
+        for (SecretField field : SECRET_FIELDS) {
+            String submitted = field.getter().apply(incoming);
+            String stored = field.getter().apply(existing);
+            if (isMaskOf(submitted, stored)) {
+                field.setter().accept(incoming, stored);
+            }
+        }
+    }
+
+    private static boolean isMaskOf(String submitted, String stored) {
+        if (submitted == null || StringUtils.isBlank(stored)) {
+            return false;
+        }
+        return submitted.equals(SECRET_MASK) || 
submitted.equals(maskValue(stored));
+    }
+
+    private static String maskValue(String value) {
+        if (StringUtils.isBlank(value)) {
+            return value;
+        }
+        if (value.length() < MIN_LENGTH_TO_SHOW_SUFFIX) {
+            return SECRET_MASK;
+        }
+        return SECRET_MASK + value.substring(value.length() - 
VISIBLE_SUFFIX_LENGTH);
+    }
+}
diff --git 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java
 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java
index 84ca807bcb..f82556c822 100644
--- 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java
+++ 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java
@@ -31,6 +31,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
 import org.apache.hertzbeat.alert.service.impl.NoticeConfigServiceImpl;
+import org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil;
 import org.apache.hertzbeat.common.constants.CommonConstants;
 import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
 import org.apache.hertzbeat.common.entity.alerter.NoticeRule;
@@ -174,6 +175,7 @@ class NoticeConfigControllerTest {
         NoticeReceiver receiver1 = new NoticeReceiver();
         receiver1.setId(1L);
         receiver1.setName("Receiver1");
+        receiver1.setTgBotToken("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM");
 
         NoticeReceiver receiver2 = new NoticeReceiver();
         receiver2.setId(2L);
@@ -197,6 +199,7 @@ class NoticeConfigControllerTest {
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.data.content[0].id").value(1))
                 
.andExpect(jsonPath("$.data.content[0].name").value("Receiver1"))
+                
.andExpect(jsonPath("$.data.content[0].tgBotToken").value(NoticeReceiverMaskUtil.SECRET_MASK
 + "voJM"))
                 .andExpect(jsonPath("$.data.content[1].id").value(2))
                 
.andExpect(jsonPath("$.data.content[1].name").value("Receiver2"))
                 .andExpect(jsonPath("$.data.totalElements").value(2))
@@ -222,6 +225,8 @@ class NoticeConfigControllerTest {
         
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/receiver/{id}", 
7565463543L))
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.code").value((int) 
CommonConstants.SUCCESS_CODE))
+                
.andExpect(jsonPath("$.data.accessToken").value(NoticeReceiverMaskUtil.SECRET_MASK
 + "739d"))
+                .andExpect(jsonPath("$.data.email").value("[email protected]"))
                 .andReturn();
     }
 
diff --git 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java
 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java
index 4206140963..9b2780ba1e 100644
--- 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java
+++ 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java
@@ -22,6 +22,7 @@ import org.apache.hertzbeat.alert.dao.NoticeRuleDao;
 import org.apache.hertzbeat.alert.dao.NoticeTemplateDao;
 import org.apache.hertzbeat.alert.notice.AlertNoticeDispatch;
 import org.apache.hertzbeat.alert.service.impl.NoticeConfigServiceImpl;
+import org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil;
 import org.apache.hertzbeat.common.cache.CacheFactory;
 import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
 import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
@@ -48,6 +49,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -216,6 +218,39 @@ class NoticeConfigServiceTest {
         verify(noticeReceiverDao, times(1)).save(noticeReceiver);
     }
 
+    @Test
+    void editReceiverKeepsStoredSecretWhenMasked() {
+        final NoticeReceiver stored = new NoticeReceiver();
+        stored.setId(5L);
+        stored.setTgBotToken("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM");
+        when(noticeReceiverDao.findById(5L)).thenReturn(Optional.of(stored));
+
+        final NoticeReceiver incoming = new NoticeReceiver();
+        incoming.setId(5L);
+        incoming.setTgBotToken(NoticeReceiverMaskUtil.SECRET_MASK + "voJM");
+
+        noticeConfigService.editReceiver(incoming);
+
+        assertEquals("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM", 
incoming.getTgBotToken());
+        verify(noticeReceiverDao, times(1)).save(incoming);
+    }
+
+    @Test
+    void sendTestMsgResolvesMaskedSecret() {
+        final NoticeReceiver stored = new NoticeReceiver();
+        stored.setId(5L);
+        stored.setTgBotToken("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM");
+        when(noticeReceiverDao.findById(5L)).thenReturn(Optional.of(stored));
+
+        final NoticeReceiver incoming = new NoticeReceiver();
+        incoming.setId(5L);
+        incoming.setTgBotToken(NoticeReceiverMaskUtil.SECRET_MASK + "voJM");
+
+        noticeConfigService.sendTestMsg(incoming);
+
+        assertEquals("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM", 
incoming.getTgBotToken());
+    }
+
     @Test
     void deleteReceiver() {
         final Long receiverId = 23342525L;
diff --git 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/NoticeReceiverMaskUtilTest.java
 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/NoticeReceiverMaskUtilTest.java
new file mode 100644
index 0000000000..7d42ed6e87
--- /dev/null
+++ 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/NoticeReceiverMaskUtilTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+package org.apache.hertzbeat.alert.util;
+
+import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
+import org.junit.jupiter.api.Test;
+
+import static 
org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil.SECRET_MASK;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Test case for {@link NoticeReceiverMaskUtil}
+ */
+class NoticeReceiverMaskUtilTest {
+
+    private NoticeReceiver buildReceiverWithSecrets() {
+        NoticeReceiver receiver = new NoticeReceiver();
+        receiver.setId(1L);
+        receiver.setName("tom");
+        receiver.setEmail("[email protected]");
+        receiver.setHookUrl("https://example.com/hook";);
+        receiver.setHookAuthToken("hook-auth-token-abcd");
+        
receiver.setAccessToken("c03a568a306f8fd84dab51ff03cf6af6ba676a3be940c904e1df2de34853739d");
+        receiver.setTgBotToken("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM");
+        
receiver.setSlackWebHookUrl("https://hooks.slack.com/services/X/Y/Zt0k3n";);
+        receiver.setAppSecret("oUydwn92ey0lnuY02MixNa57eNK-20dJn5NEOG-u2uE");
+        receiver.setDiscordBotToken("MTA2NTMwMzU0ODY4Mzg4MjUzNw.discord.t0kn");
+        receiver.setSmnAk("NCVBODJOEYHSW3VNSMAK");
+        receiver.setSmnSk("nmSNhUJN9MlpPl8lfCsgdA0KvHCL9JSMSK");
+        receiver.setServerChanToken("SCT193569TSNm6xIabdjqeZPtOGOWcvU1e");
+        receiver.setGotifyToken("A845h__ZMqDxZlO");
+        receiver.setNtfyToken("tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2");
+        return receiver;
+    }
+
+    @Test
+    void maskKeepsSuffixOfLongSecrets() {
+        NoticeReceiver receiver = buildReceiverWithSecrets();
+        NoticeReceiver masked = NoticeReceiverMaskUtil.mask(receiver);
+
+        assertEquals(SECRET_MASK + "abcd", masked.getHookAuthToken());
+        assertEquals(SECRET_MASK + "739d", masked.getAccessToken());
+        assertEquals(SECRET_MASK + "voJM", masked.getTgBotToken());
+        assertEquals(SECRET_MASK + "0k3n", masked.getSlackWebHookUrl());
+        assertEquals(SECRET_MASK + "u2uE", masked.getAppSecret());
+        assertEquals(SECRET_MASK + "t0kn", masked.getDiscordBotToken());
+        assertEquals(SECRET_MASK + "SMAK", masked.getSmnAk());
+        assertEquals(SECRET_MASK + "SMSK", masked.getSmnSk());
+        assertEquals(SECRET_MASK + "vU1e", masked.getServerChanToken());
+        assertEquals(SECRET_MASK + "xZlO", masked.getGotifyToken());
+        assertEquals(SECRET_MASK + "NIz2", masked.getNtfyToken());
+
+        assertEquals(receiver.getId(), masked.getId());
+        assertEquals(receiver.getName(), masked.getName());
+        assertEquals(receiver.getEmail(), masked.getEmail());
+        assertEquals(receiver.getHookUrl(), masked.getHookUrl());
+    }
+
+    @Test
+    void maskHidesShortSecretsEntirely() {
+        NoticeReceiver receiver = new NoticeReceiver();
+        receiver.setAccessToken("short-token");
+        receiver.setGotifyToken("tiny");
+        NoticeReceiver masked = NoticeReceiverMaskUtil.mask(receiver);
+
+        assertEquals(SECRET_MASK, masked.getAccessToken());
+        assertEquals(SECRET_MASK, masked.getGotifyToken());
+    }
+
+    @Test
+    void maskDoesNotModifyOriginalAndKeepsEmptySecrets() {
+        NoticeReceiver receiver = buildReceiverWithSecrets();
+        NoticeReceiverMaskUtil.mask(receiver);
+
+        assertEquals("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM", 
receiver.getTgBotToken());
+        assertEquals("nmSNhUJN9MlpPl8lfCsgdA0KvHCL9JSMSK", 
receiver.getSmnSk());
+
+        NoticeReceiver empty = new NoticeReceiver();
+        NoticeReceiver maskedEmpty = NoticeReceiverMaskUtil.mask(empty);
+        assertNull(maskedEmpty.getAccessToken());
+        assertNull(maskedEmpty.getNtfyToken());
+        assertNull(NoticeReceiverMaskUtil.mask(null));
+    }
+
+    @Test
+    void resolveMaskRestoresOnlyMaskedFields() {
+        NoticeReceiver existing = buildReceiverWithSecrets();
+        NoticeReceiver incoming = NoticeReceiverMaskUtil.mask(existing);
+        incoming.setAccessToken("new-access-token-1234");
+        incoming.setGotifyToken(null);
+        incoming.setNtfyToken(SECRET_MASK);
+
+        NoticeReceiverMaskUtil.resolveMask(incoming, existing);
+
+        assertEquals("new-access-token-1234", incoming.getAccessToken());
+        assertNull(incoming.getGotifyToken());
+        assertEquals("tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", 
incoming.getNtfyToken());
+        assertEquals("hook-auth-token-abcd", incoming.getHookAuthToken());
+        assertEquals("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM", 
incoming.getTgBotToken());
+        assertEquals("https://hooks.slack.com/services/X/Y/Zt0k3n";, 
incoming.getSlackWebHookUrl());
+        assertEquals("oUydwn92ey0lnuY02MixNa57eNK-20dJn5NEOG-u2uE", 
incoming.getAppSecret());
+        assertEquals("MTA2NTMwMzU0ODY4Mzg4MjUzNw.discord.t0kn", 
incoming.getDiscordBotToken());
+        assertEquals("NCVBODJOEYHSW3VNSMAK", incoming.getSmnAk());
+        assertEquals("nmSNhUJN9MlpPl8lfCsgdA0KvHCL9JSMSK", 
incoming.getSmnSk());
+        assertEquals("SCT193569TSNm6xIabdjqeZPtOGOWcvU1e", 
incoming.getServerChanToken());
+    }
+
+    @Test
+    void resolveMaskIgnoresMaskWhenNothingIsStored() {
+        NoticeReceiver existing = new NoticeReceiver();
+        NoticeReceiver incoming = new NoticeReceiver();
+        incoming.setAccessToken(SECRET_MASK);
+
+        NoticeReceiverMaskUtil.resolveMask(incoming, existing);
+
+        assertEquals(SECRET_MASK, incoming.getAccessToken());
+    }
+}
diff --git 
a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java
 
b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java
index cc7dcd6d6b..e31a1ac29c 100644
--- 
a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java
+++ 
b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java
@@ -48,7 +48,7 @@ import 
org.springframework.data.jpa.domain.support.AuditingEntityListener;
 @Entity
 @Table(name = "hzb_notice_receiver")
 @Data
-@Builder
+@Builder(toBuilder = true)
 @AllArgsConstructor
 @NoArgsConstructor
 @Schema(description = "Message notification recipient entity")


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to