This is an automated email from the ASF dual-hosted git repository.
Duansg 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 1f257a8870 maintenance: classify service credentials as passwords
(#4281)
1f257a8870 is described below
commit 1f257a8870da11997deaa5561dd8d119c45f532e
Author: Logic <[email protected]>
AuthorDate: Thu Aug 27 09:50:46 2026 +0800
maintenance: classify service credentials as passwords (#4281)
Co-authored-by: Duansg <[email protected]>
---
.../timer/WheelTimerTaskCredentialTest.java | 69 ++++++
.../validator/impl/PasswordParamValidator.java | 5 +-
.../manager/config/ServiceCredentialMigration.java | 122 +++++++++
.../hertzbeat/manager/pojo/dto/MonitorDto.java | 6 +-
.../hertzbeat/manager/pojo/dto/MonitorParam.java | 20 +-
.../hertzbeat/manager/service/MonitorService.java | 10 +
.../service/impl/AbstractImExportServiceImpl.java | 8 +-
.../manager/service/impl/MonitorServiceImpl.java | 89 ++++++-
.../src/main/resources/define/app-http_sd.yml | 2 +-
.../src/main/resources/define/app-ollama.yml | 2 +-
.../validator/impl/PasswordParamValidatorTest.java | 20 ++
.../config/ServiceCredentialMigrationTest.java | 145 +++++++++++
.../manager/controller/MonitorControllerTest.java | 7 +-
.../hertzbeat/manager/service/AppServiceTest.java | 14 ++
.../manager/service/JsonImExportServiceTest.java | 37 +++
.../manager/service/MonitorServiceTest.java | 272 +++++++++++++++++++++
home/docs/help/http_sd.md | 12 +
home/docs/help/ollama.md | 11 +
18 files changed, 838 insertions(+), 13 deletions(-)
diff --git
a/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/WheelTimerTaskCredentialTest.java
b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/WheelTimerTaskCredentialTest.java
new file mode 100644
index 0000000000..ec62516258
--- /dev/null
+++
b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/WheelTimerTaskCredentialTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.collector.timer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.entity.job.Configmap;
+import org.apache.hertzbeat.common.entity.job.Job;
+import org.apache.hertzbeat.common.entity.job.Metrics;
+import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol;
+import org.apache.hertzbeat.common.util.AesUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+class WheelTimerTaskCredentialTest {
+
+ private static final String TEST_SECRET = "0123456789abcdef";
+
+ @AfterEach
+ void tearDown() {
+ AesUtil.setDefaultSecretKey(AesUtil.DEFAULT_ENCODE_RULES);
+ }
+
+ @Test
+ void decryptsTheMigratedCredentialBeforeProtocolReplacement() {
+ AesUtil.setDefaultSecretKey(TEST_SECRET);
+ String ciphertext = AesUtil.aesEncode("runtime-ollama-key");
+ HttpProtocol.Authorization authorization = new
HttpProtocol.Authorization();
+ authorization.setType("Bearer Token");
+ authorization.setBearerTokenToken("^_^apiKey^_^");
+ Metrics metrics = Metrics.builder()
+ .name("version")
+ .interval(60)
+
.http(HttpProtocol.builder().authorization(authorization).build())
+ .build();
+ Job job = Job.builder()
+ .app("ollama")
+ .defaultInterval(60)
+ .configmap(List.of(new Configmap(
+ "apiKey",
+ ciphertext,
+ CommonConstants.PARAM_TYPE_PASSWORD)))
+ .metrics(List.of(metrics))
+ .build();
+
+ new WheelTimerTask(job, timeout -> {
+ });
+
+ assertEquals("runtime-ollama-key",
+
job.getMetrics().get(0).getHttp().getAuthorization().getBearerTokenToken());
+ }
+}
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidator.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidator.java
index 7183d479f5..7916510631 100644
---
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidator.java
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidator.java
@@ -37,7 +37,10 @@ public class PasswordParamValidator implements
ParamValidator {
@Override
public void validate(ParamDefineInfo paramDefine, MonitorParam param) {
String passwordValue = param.getParamValue();
- if (!AesUtil.isCiphertext(passwordValue)) {
+ boolean currentCiphertext = AesUtil.isCiphertext(passwordValue);
+ boolean legacyCiphertext =
!AesUtil.DEFAULT_ENCODE_RULES.equals(AesUtil.getDefaultSecretKey())
+ && AesUtil.isCiphertext(passwordValue,
AesUtil.DEFAULT_ENCODE_RULES);
+ if (!currentCiphertext && !legacyCiphertext) {
passwordValue = AesUtil.aesEncode(passwordValue);
param.setParamValue(passwordValue);
}
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ServiceCredentialMigration.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ServiceCredentialMigration.java
new file mode 100644
index 0000000000..a2aa94ad78
--- /dev/null
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ServiceCredentialMigration.java
@@ -0,0 +1,122 @@
+/*
+ * 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.manager.config;
+
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.util.AesUtil;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+/**
+ * Encrypts credentials that were stored before their template parameter type
+ * was classified as a password.
+ */
+@Component
+@Order(Ordered.HIGHEST_PRECEDENCE)
+@Slf4j
+public class ServiceCredentialMigration implements CommandLineRunner {
+
+ private static final String MIGRATION_MARKER =
"migration.service-credentials.v1";
+
+ private static final String FIND_CREDENTIALS_SQL = """
+ SELECT p.id, p.param_value, p.type
+ FROM hzb_param p
+ INNER JOIN hzb_monitor m ON m.id = p.monitor_id
+ WHERE (m.app = 'ollama' AND p.field = 'apiKey')
+ OR (m.scrape = 'http_sd' AND p.field = '__sd_token__')
+ """;
+
+ private static final String UPDATE_CREDENTIAL_SQL =
+ "UPDATE hzb_param SET param_value = ?, type = ? WHERE id = ?";
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public ServiceCredentialMigration(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void run(String... args) {
+ if (isMigrationComplete()) {
+ return;
+ }
+ int migrated = migrateStoredCredentials();
+ jdbcTemplate.update("INSERT INTO hzb_config(type, content) VALUES (?,
?)",
+ MIGRATION_MARKER, "complete");
+ if (migrated > 0) {
+ log.info("Migrated {} stored service credential parameters",
migrated);
+ }
+ }
+
+ private boolean isMigrationComplete() {
+ Integer markerCount = jdbcTemplate.queryForObject(
+ "SELECT COUNT(*) FROM hzb_config WHERE type = ?",
+ Integer.class,
+ MIGRATION_MARKER);
+ return markerCount != null && markerCount > 0;
+ }
+
+ int migrateStoredCredentials() {
+ List<StoredCredential> credentials =
jdbcTemplate.query(FIND_CREDENTIALS_SQL,
+ (resultSet, rowNumber) -> new StoredCredential(
+ resultSet.getLong("id"),
+ resultSet.getString("param_value"),
+ resultSet.getByte("type")));
+ int migrated = 0;
+ for (StoredCredential credential : credentials) {
+ String value = credential.value();
+ boolean passwordType = credential.type() ==
CommonConstants.PARAM_TYPE_PASSWORD;
+ boolean encrypted = !StringUtils.hasText(value) ||
isSupportedCiphertext(value);
+ if (passwordType && encrypted) {
+ continue;
+ }
+ String valueToStore = value;
+ if (StringUtils.hasText(value) && !encrypted) {
+ valueToStore = AesUtil.aesEncode(value);
+ if (!AesUtil.isCiphertext(valueToStore)) {
+ throw new IllegalStateException("Could not encrypt a
stored service credential");
+ }
+ }
+ jdbcTemplate.update(UPDATE_CREDENTIAL_SQL,
+ valueToStore,
+ CommonConstants.PARAM_TYPE_PASSWORD,
+ credential.id());
+ migrated++;
+ }
+ return migrated;
+ }
+
+ private boolean isSupportedCiphertext(String value) {
+ if (AesUtil.isCiphertext(value)) {
+ return true;
+ }
+ return
!AesUtil.DEFAULT_ENCODE_RULES.equals(AesUtil.getDefaultSecretKey())
+ && AesUtil.isCiphertext(value, AesUtil.DEFAULT_ENCODE_RULES);
+ }
+
+ private record StoredCredential(long id, String value, byte type) {
+ }
+}
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDto.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDto.java
index 33f77bdef3..682107da15 100644
---
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDto.java
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDto.java
@@ -91,9 +91,13 @@ public class MonitorDto {
}
public void setParams(List<Param> params) {
+ setParams(params, true);
+ }
+
+ public void setParams(List<Param> params, boolean maskCredentials) {
this.paramInfos = params == null ? null : params.stream()
.filter(Objects::nonNull)
- .map(MonitorParam::fromEntity)
+ .map(param -> MonitorParam.fromEntity(param, maskCredentials))
.toList();
}
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorParam.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorParam.java
index 4fdde3f3d8..932b287fbe 100644
---
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorParam.java
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorParam.java
@@ -24,6 +24,7 @@ import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
+import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.manager.Param;
/**
@@ -34,6 +35,8 @@ import org.apache.hertzbeat.common.entity.manager.Param;
@NoArgsConstructor
public class MonitorParam {
+ public static final String SECRET_MASK = "******";
+
private Long id;
private Long monitorId;
@@ -53,6 +56,10 @@ public class MonitorParam {
private LocalDateTime gmtUpdate;
public static MonitorParam fromEntity(Param param) {
+ return fromEntity(param, true);
+ }
+
+ public static MonitorParam fromEntity(Param param, boolean maskCredential)
{
if (param == null) {
return null;
}
@@ -60,13 +67,24 @@ public class MonitorParam {
monitorParam.setId(param.getId());
monitorParam.setMonitorId(param.getMonitorId());
monitorParam.setField(param.getField());
- monitorParam.setParamValue(param.getParamValue());
+ String value = param.getParamValue();
+ if (maskCredential
+ && param.getType() == CommonConstants.PARAM_TYPE_PASSWORD
+ && value != null
+ && !value.isEmpty()) {
+ value = SECRET_MASK;
+ }
+ monitorParam.setParamValue(value);
monitorParam.setType(param.getType());
monitorParam.setGmtCreate(param.getGmtCreate());
monitorParam.setGmtUpdate(param.getGmtUpdate());
return monitorParam;
}
+ public static boolean isSecretMask(String value) {
+ return SECRET_MASK.equals(value);
+ }
+
public Param toEntity() {
Param param = new Param();
param.setId(id);
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MonitorService.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MonitorService.java
index 083d6e12d5..39730ff5e8 100644
---
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MonitorService.java
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MonitorService.java
@@ -102,6 +102,16 @@ public interface MonitorService {
*/
MonitorDto getMonitorDto(long id) throws RuntimeException;
+ /**
+ * Get monitoring information for a portable configuration export. Password
+ * values remain encrypted so the exported configuration can be imported by
+ * an installation configured with the same common secret.
+ *
+ * @param id Monitor ID
+ * @return monitor information with encrypted parameter values
+ */
+ MonitorDto getMonitorDtoForExport(long id) throws RuntimeException;
+
/**
* Dynamic conditional query
*
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
index 901c5132a0..0cc0fb7eba 100644
---
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
@@ -98,7 +98,11 @@ public abstract class AbstractImExportServiceImpl implements
ImExportService {
@Override
public void exportConfig(OutputStream os, List<Long> configList) {
- var monitorList = configList.stream().map(it ->
monitorService.getMonitorDto(it)).filter(Objects::nonNull).map(this::convert).toList();
+ var monitorList = configList.stream()
+ .map(monitorService::getMonitorDtoForExport)
+ .filter(Objects::nonNull)
+ .map(this::convert)
+ .toList();
writeOs(monitorList, os);
}
@@ -168,7 +172,7 @@ public abstract class AbstractImExportServiceImpl
implements ImExportService {
param.setType(it.type);
param.setParamValue(it.value);
return param;
- }).toList());
+ }).toList(), false);
} else {
monitorDto.setParams(Collections.emptyList());
}
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
index 9b061731e4..c3628fcb0d 100644
---
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
@@ -43,6 +43,7 @@ import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.support.event.MonitorDeletedEvent;
+import org.apache.hertzbeat.common.util.AesUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.hertzbeat.common.util.JexlCheckerUtil;
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
@@ -103,6 +104,10 @@ public class MonitorServiceImpl implements MonitorService {
public static final String PATTERN_HTTPS = "(?i)https://";
private static final Long MONITOR_ID_TMP = 1000000000L;
private static final byte ALL_MONITOR_STATUS = 9;
+ private static final Map<String, Set<String>>
CREDENTIAL_DESTINATION_FIELDS = Map.of(
+ "apiKey", Set.of("host", "port", "ssl"),
+ "__sd_token__", Set.of("__sd_url__", "__sd_authType__"),
+ "__sd_password__", Set.of("__sd_url__", "__sd_authType__"));
public static final String PARAM_FIELD_PORT = "port";
@Autowired
@@ -323,10 +328,33 @@ public class MonitorServiceImpl implements MonitorService
{
monitorDto.setCollector(null);
}
// Parameter definition structure verification
- List<ParamDefineInfo> paramDefines =
appService.getAppParamDefines(monitor.getApp());
+ boolean isStatic =
CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape())
+ || !StringUtils.hasText(monitor.getScrape());
+ List<ParamDefineInfo> paramDefines = new ArrayList<>();
+ List<ParamDefineInfo> applicationParamDefines =
appService.getAppParamDefines(monitor.getApp());
+ if (!CollectionUtils.isEmpty(applicationParamDefines)) {
+ paramDefines.addAll(applicationParamDefines);
+ }
+ if (!isStatic && !Objects.equals(monitor.getApp(),
monitor.getScrape())) {
+ List<ParamDefineInfo> scrapeParamDefines =
appService.getAppParamDefines(monitor.getScrape());
+ if (!CollectionUtils.isEmpty(scrapeParamDefines)) {
+ paramDefines.addAll(scrapeParamDefines);
+ }
+ }
+ boolean restoresMaskedCredential = (Boolean.TRUE.equals(isModify)
+ || (isModify == null && monitor.getId() != null))
+ && !CollectionUtils.isEmpty(paramDefines)
+ && paramDefines.stream()
+ .filter(paramDefine ->
"password".equals(paramDefine.getType()))
+ .map(ParamDefineInfo::getField)
+ .map(paramMap::get)
+ .filter(Objects::nonNull)
+ .anyMatch(param ->
MonitorParam.isSecretMask(param.getParamValue()));
+ Map<String, Param> storedParams = restoresMaskedCredential
+ ? paramDao.findParamsByMonitorId(monitor.getId()).stream()
+ .collect(Collectors.toMap(Param::getField, param -> param))
+ : Map.of();
if (!CollectionUtils.isEmpty(paramDefines)) {
- boolean isStatic =
CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape())
- || !StringUtils.hasText(monitor.getScrape());
for (ParamDefineInfo paramDefine : paramDefines) {
String field = paramDefine.getField();
MonitorParam param = paramMap.get(field);
@@ -334,6 +362,27 @@ public class MonitorServiceImpl implements MonitorService {
if (!isStatic && "host".equals(field)) {
continue;
}
+ if ("password".equals(paramDefine.getType())
+ && param != null
+ && MonitorParam.isSecretMask(param.getParamValue())) {
+ if (!restoresMaskedCredential) {
+ throw new IllegalArgumentException("The credential
mask cannot be used as a new value.");
+ }
+ Param storedParam = storedParams.get(field);
+ if (storedParam == null ||
!StringUtils.hasText(storedParam.getParamValue())) {
+ throw new IllegalArgumentException("The masked
credential has no stored value.");
+ }
+ String storedValue = storedParam.getParamValue();
+ boolean currentCiphertext =
AesUtil.isCiphertext(storedValue);
+ boolean legacyCiphertext =
!AesUtil.DEFAULT_ENCODE_RULES.equals(AesUtil.getDefaultSecretKey())
+ && AesUtil.isCiphertext(storedValue,
AesUtil.DEFAULT_ENCODE_RULES);
+ if (!currentCiphertext && !legacyCiphertext) {
+ throw new IllegalStateException("The stored credential
migration is incomplete.");
+ }
+ validateCredentialDestination(field, paramMap,
storedParams);
+ param.setParamValue(storedValue);
+ param.setType(CommonConstants.PARAM_TYPE_PASSWORD);
+ }
if (paramDefine.isRequired() && (param == null ||
param.getParamValue() == null)) {
throw new IllegalArgumentException("Params field " + field
+ " is required.");
}
@@ -342,7 +391,27 @@ public class MonitorServiceImpl implements MonitorService {
}
}
}
- checkJobFields(monitorDto.getMonitor().getApp());
+ checkJobFields(monitor.getApp());
+ if (!isStatic && !Objects.equals(monitor.getApp(),
monitor.getScrape())) {
+ checkJobFields(monitor.getScrape());
+ }
+ }
+
+ private void validateCredentialDestination(
+ String credentialField,
+ Map<String, MonitorParam> submittedParams,
+ Map<String, Param> storedParams) {
+ for (String destinationField :
CREDENTIAL_DESTINATION_FIELDS.getOrDefault(
+ credentialField, Set.of())) {
+ MonitorParam submitted = submittedParams.get(destinationField);
+ Param stored = storedParams.get(destinationField);
+ String submittedValue = submitted == null ? null :
submitted.getParamValue();
+ String storedValue = stored == null ? null :
stored.getParamValue();
+ if (!Objects.equals(submittedValue, storedValue)) {
+ throw new IllegalArgumentException(
+ "A credential must be re-entered when its destination
changes.");
+ }
+ }
}
private void checkJobFields(String app) {
@@ -509,6 +578,16 @@ public class MonitorServiceImpl implements MonitorService {
@Override
@Transactional(readOnly = true)
public MonitorDto getMonitorDto(long id) throws RuntimeException {
+ return getMonitorDto(id, true);
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public MonitorDto getMonitorDtoForExport(long id) throws RuntimeException {
+ return getMonitorDto(id, false);
+ }
+
+ private MonitorDto getMonitorDto(long id, boolean maskCredentials) throws
RuntimeException {
Optional<Monitor> monitorOptional = monitorDao.findById(id);
if (monitorOptional.isPresent()) {
// Get current user ID for favorite status
@@ -524,7 +603,7 @@ public class MonitorServiceImpl implements MonitorService {
Monitor monitor = monitorOptional.get();
MonitorDto monitorDto = new MonitorDto();
List<Param> params = paramDao.findParamsByMonitorId(id);
- monitorDto.setParams(params);
+ monitorDto.setParams(params, maskCredentials);
List<MetricsInfo> metricsInfos;
if
(DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(monitor.getApp())
|| monitor.getType() ==
CommonConstants.MONITOR_TYPE_PUSH_AUTO_CREATE) {
diff --git a/hertzbeat-manager/src/main/resources/define/app-http_sd.yml
b/hertzbeat-manager/src/main/resources/define/app-http_sd.yml
index dc0527d667..2ebd0d3f4a 100644
--- a/hertzbeat-manager/src/main/resources/define/app-http_sd.yml
+++ b/hertzbeat-manager/src/main/resources/define/app-http_sd.yml
@@ -62,7 +62,7 @@ params:
en-US: Access Token
ja-JP: アクセストークン
# type-param field type(most mapping the html input type)
- type: text
+ type: password
# dependent parameter values list
depend:
__sd_authType__:
diff --git a/hertzbeat-manager/src/main/resources/define/app-ollama.yml
b/hertzbeat-manager/src/main/resources/define/app-ollama.yml
index dd818a27ed..941fac9f37 100644
--- a/hertzbeat-manager/src/main/resources/define/app-ollama.yml
+++ b/hertzbeat-manager/src/main/resources/define/app-ollama.yml
@@ -65,7 +65,7 @@ params:
zh-CN: API Key
en-US: API Key
ja-JP: API キー
- type: text
+ type: password
required: false
# collect metrics config list
metrics:
diff --git
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidatorTest.java
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidatorTest.java
index 72e3d21cdb..49bd962d51 100644
---
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidatorTest.java
+++
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidatorTest.java
@@ -72,4 +72,24 @@ class PasswordParamValidatorTest {
assertEquals(ciphertext, param.getParamValue());
assertEquals(CommonConstants.PARAM_TYPE_PASSWORD, param.getType());
}
+
+ @Test
+ void validate_IgnoresLegacyCiphertextUntilMigrationReencryptsIt() {
+ String originalSecret = AesUtil.getDefaultSecretKey();
+ try {
+ AesUtil.setDefaultSecretKey("nextSecretKey123");
+ String legacyCiphertext = AesUtil.aesEncode("password123",
AesUtil.DEFAULT_ENCODE_RULES);
+ ParamDefineInfo paramDefine = new ParamDefineInfo();
+ paramDefine.setType("password");
+ MonitorParam param = new MonitorParam();
+ param.setParamValue(legacyCiphertext);
+
+ validator.validate(paramDefine, param);
+
+ assertEquals(legacyCiphertext, param.getParamValue());
+ assertEquals(CommonConstants.PARAM_TYPE_PASSWORD, param.getType());
+ } finally {
+ AesUtil.setDefaultSecretKey(originalSecret);
+ }
+ }
}
diff --git
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ServiceCredentialMigrationTest.java
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ServiceCredentialMigrationTest.java
new file mode 100644
index 0000000000..092d831d63
--- /dev/null
+++
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ServiceCredentialMigrationTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.manager.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Map;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.util.AesUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+
+class ServiceCredentialMigrationTest {
+
+ private static final String TEST_SECRET = "0123456789abcdef";
+
+ private JdbcTemplate jdbcTemplate;
+
+ @BeforeEach
+ void setUp() {
+ AesUtil.setDefaultSecretKey(TEST_SECRET);
+ DriverManagerDataSource dataSource = new DriverManagerDataSource(
+
"jdbc:h2:mem:credential-migration;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
+ "sa",
+ "");
+ jdbcTemplate = new JdbcTemplate(dataSource);
+ jdbcTemplate.execute("DROP ALL OBJECTS");
+ jdbcTemplate.execute("""
+ CREATE TABLE hzb_monitor (
+ id BIGINT PRIMARY KEY,
+ app VARCHAR(100),
+ scrape VARCHAR(100)
+ )
+ """);
+ jdbcTemplate.execute("""
+ CREATE TABLE hzb_param (
+ id BIGINT PRIMARY KEY,
+ monitor_id BIGINT,
+ field VARCHAR(100),
+ param_value VARCHAR(8126),
+ type TINYINT
+ )
+ """);
+ jdbcTemplate.execute("""
+ CREATE TABLE hzb_config (
+ type VARCHAR(100) PRIMARY KEY,
+ content VARCHAR(8192)
+ )
+ """);
+ }
+
+ @AfterEach
+ void tearDown() {
+ AesUtil.setDefaultSecretKey(AesUtil.DEFAULT_ENCODE_RULES);
+ }
+
+ @Test
+ void migratesLegacyCredentialsInTheDatabaseIdempotently() {
+ jdbcTemplate.update("INSERT INTO hzb_monitor(id, app, scrape) VALUES
(?, ?, ?)", 1L, "ollama", "static");
+ jdbcTemplate.update("INSERT INTO hzb_monitor(id, app, scrape) VALUES
(?, ?, ?)", 2L, "linux", "http_sd");
+ jdbcTemplate.update("INSERT INTO hzb_monitor(id, app, scrape) VALUES
(?, ?, ?)", 3L, "website", "static");
+ jdbcTemplate.update("INSERT INTO hzb_monitor(id, app, scrape) VALUES
(?, ?, ?)", 4L, "ollama", "static");
+ String alreadyEncrypted = AesUtil.aesEncode("existing-ciphertext");
+ jdbcTemplate.update("INSERT INTO hzb_param VALUES (?, ?, ?, ?, ?)",
+ 11L, 1L, "apiKey", "legacy-ollama-key",
CommonConstants.PARAM_TYPE_STRING);
+ jdbcTemplate.update("INSERT INTO hzb_param VALUES (?, ?, ?, ?, ?)",
+ 12L, 2L, "__sd_token__", "legacy-http-sd-token",
CommonConstants.PARAM_TYPE_STRING);
+ jdbcTemplate.update("INSERT INTO hzb_param VALUES (?, ?, ?, ?, ?)",
+ 13L, 3L, "apiKey", "ordinary-text",
CommonConstants.PARAM_TYPE_STRING);
+ jdbcTemplate.update("INSERT INTO hzb_param VALUES (?, ?, ?, ?, ?)",
+ 14L, 4L, "apiKey", alreadyEncrypted,
CommonConstants.PARAM_TYPE_PASSWORD);
+
+ ServiceCredentialMigration migration = new
ServiceCredentialMigration(jdbcTemplate);
+ assertEquals(2, migration.migrateStoredCredentials());
+
+ Map<String, Object> ollama = jdbcTemplate.queryForMap(
+ "SELECT param_value, type FROM hzb_param WHERE id = 11");
+ Map<String, Object> httpSd = jdbcTemplate.queryForMap(
+ "SELECT param_value, type FROM hzb_param WHERE id = 12");
+ Map<String, Object> ordinary = jdbcTemplate.queryForMap(
+ "SELECT param_value, type FROM hzb_param WHERE id = 13");
+ Map<String, Object> encrypted = jdbcTemplate.queryForMap(
+ "SELECT param_value, type FROM hzb_param WHERE id = 14");
+
+ String ollamaCiphertext = String.valueOf(ollama.get("PARAM_VALUE"));
+ String httpSdCiphertext = String.valueOf(httpSd.get("PARAM_VALUE"));
+ assertNotEquals("legacy-ollama-key", ollamaCiphertext);
+ assertNotEquals("legacy-http-sd-token", httpSdCiphertext);
+ assertTrue(AesUtil.isCiphertext(ollamaCiphertext));
+ assertTrue(AesUtil.isCiphertext(httpSdCiphertext));
+ assertEquals("legacy-ollama-key", AesUtil.aesDecode(ollamaCiphertext));
+ assertEquals("legacy-http-sd-token",
AesUtil.aesDecode(httpSdCiphertext));
+ assertEquals(CommonConstants.PARAM_TYPE_PASSWORD, ((Number)
ollama.get("TYPE")).byteValue());
+ assertEquals(CommonConstants.PARAM_TYPE_PASSWORD, ((Number)
httpSd.get("TYPE")).byteValue());
+ assertEquals("ordinary-text", ordinary.get("PARAM_VALUE"));
+ assertEquals(CommonConstants.PARAM_TYPE_STRING, ((Number)
ordinary.get("TYPE")).byteValue());
+ assertEquals(alreadyEncrypted, encrypted.get("PARAM_VALUE"));
+
+ assertEquals(0, migration.migrateStoredCredentials());
+ assertEquals(ollamaCiphertext, jdbcTemplate.queryForObject(
+ "SELECT param_value FROM hzb_param WHERE id = 11",
String.class));
+ }
+
+ @Test
+ void startupMigrationRunsOnlyOnceAndDoesNotRewriteLaterCredentials() {
+ jdbcTemplate.update("INSERT INTO hzb_monitor(id, app, scrape) VALUES
(?, ?, ?)",
+ 1L, "ollama", "static");
+ jdbcTemplate.update("INSERT INTO hzb_param VALUES (?, ?, ?, ?, ?)",
+ 11L, 1L, "apiKey", "legacy-ollama-key",
CommonConstants.PARAM_TYPE_STRING);
+ ServiceCredentialMigration migration = new
ServiceCredentialMigration(jdbcTemplate);
+
+ migration.run();
+ jdbcTemplate.update("INSERT INTO hzb_param VALUES (?, ?, ?, ?, ?)",
+ 12L, 1L, "apiKey", "later-operator-value",
CommonConstants.PARAM_TYPE_STRING);
+ migration.run();
+
+ assertTrue(AesUtil.isCiphertext(jdbcTemplate.queryForObject(
+ "SELECT param_value FROM hzb_param WHERE id = 11",
String.class)));
+ assertEquals("later-operator-value", jdbcTemplate.queryForObject(
+ "SELECT param_value FROM hzb_param WHERE id = 12",
String.class));
+ assertEquals(1, jdbcTemplate.queryForObject(
+ "SELECT COUNT(*) FROM hzb_config WHERE type =
'migration.service-credentials.v1'",
+ Integer.class));
+ }
+}
diff --git
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java
index b30c392c12..61a2b6a8c5 100644
---
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java
+++
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java
@@ -123,13 +123,18 @@ class MonitorControllerTest {
MonitorDto monitorDto = new MonitorDto();
monitorDto.setMonitor(monitor);
-
+ Param secret = new Param();
+ secret.setField("apiKey");
+ secret.setParamValue("ciphertext-must-not-leave-the-api");
+ secret.setType(CommonConstants.PARAM_TYPE_PASSWORD);
+ monitorDto.setParams(List.of(secret));
Mockito.when(monitorService.getMonitorDto(6565463543L))
.thenReturn(monitorDto);
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/monitor/{id}",
6565463543L))
.andExpect(jsonPath("$.code").value((int)
CommonConstants.SUCCESS_CODE))
+
.andExpect(jsonPath("$.data.params[0].paramValue").value("******"))
.andExpect(status().isOk())
.andReturn();
}
diff --git
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AppServiceTest.java
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AppServiceTest.java
index 56f9c014a6..cce91d02d1 100644
---
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AppServiceTest.java
+++
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AppServiceTest.java
@@ -46,6 +46,7 @@ import java.util.Map;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -85,6 +86,12 @@ class AppServiceTest {
assertDoesNotThrow(() -> appService.getAppParamDefines("jvm"));
}
+ @Test
+ void credentialParametersUsePasswordType() {
+ assertEquals("password", findParam("ollama", "apiKey").getType());
+ assertEquals("password", findParam("http_sd",
"__sd_token__").getType());
+ }
+
@Test
void getAppParamDefinesShouldNotContainDuplicateFields() {
List<ParamDefineInfo> paramDefines =
appService.getAppParamDefines("nvidia");
@@ -203,4 +210,11 @@ class AppServiceTest {
assertNotNull(appParamDefines);
assertTrue(appParamDefines.stream().anyMatch(t ->
t.getField().equals("host_test")));
}
+
+ private ParamDefineInfo findParam(String app, String field) {
+ return appService.getAppParamDefines(app).stream()
+ .filter(param -> field.equals(param.getField()))
+ .findFirst()
+ .orElseThrow();
+ }
}
diff --git
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/JsonImExportServiceTest.java
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/JsonImExportServiceTest.java
index 198c89d0d3..e5deff1460 100644
---
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/JsonImExportServiceTest.java
+++
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/JsonImExportServiceTest.java
@@ -32,6 +32,8 @@ import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.manager.config.ManagerSseManager;
import org.apache.hertzbeat.manager.service.impl.AbstractImExportServiceImpl;
import org.apache.hertzbeat.manager.service.impl.JsonImExportServiceImpl;
+import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
+import org.apache.hertzbeat.manager.pojo.dto.MonitorParam;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -110,6 +112,41 @@ class JsonImExportServiceTest {
assertEquals("JSON", jsonImExportService.type());
}
+ @Test
+ void testExportConfigPreservesEncryptedCredentialForImportRoundTrip() {
+ String ciphertext = "HBA2-export-ciphertext";
+ MonitorDto monitorDto = new MonitorDto();
+
monitorDto.setMonitor(Monitor.builder().id(1L).name("ollama").app("ollama").build());
+ MonitorParam secret = new MonitorParam();
+ secret.setField("apiKey");
+
secret.setType(org.apache.hertzbeat.common.constants.CommonConstants.PARAM_TYPE_PASSWORD);
+ secret.setParamValue(ciphertext);
+ monitorDto.setParamInfos(List.of(secret));
+
org.mockito.Mockito.when(monitorService.getMonitorDtoForExport(1L)).thenReturn(monitorDto);
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+ jsonImExportService.exportConfig(output, List.of(1L));
+
+
assertTrue(output.toString(StandardCharsets.UTF_8).contains(ciphertext));
+ }
+
+ @Test
+ void testImportConfigPreservesEncryptedCredentialForImportRoundTrip() {
+ String ciphertext = "HBA2-import-ciphertext";
+ String json =
"[{\"monitor\":{\"name\":\"ollama-import\",\"app\":\"ollama\","
+ +
"\"intervals\":6000,\"status\":1},\"params\":[{\"field\":\"apiKey\","
+ + "\"type\":2,\"value\":\"" + ciphertext + "\"}]}]";
+ ArgumentCaptor<List<Param>> paramsCaptor =
ArgumentCaptor.forClass(List.class);
+ doNothing().when(monitorService).addMonitor(
+ org.mockito.Mockito.any(), paramsCaptor.capture(),
+ org.mockito.Mockito.any(), org.mockito.Mockito.any());
+
+ jsonImExportService.importConfig(
+ "ollama.json", new
ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)));
+
+ assertEquals(ciphertext,
paramsCaptor.getValue().get(0).getParamValue());
+ }
+
@Test
void testImportConfig_shouldSetInstanceFromHostAndPortParams() {
String json =
"[{\"monitor\":{\"name\":\"test\",\"app\":\"windows\",\"intervals\":6000,\"status\":1},"
diff --git
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/MonitorServiceTest.java
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/MonitorServiceTest.java
index 6b3b5c502d..17e387cf4d 100644
---
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/MonitorServiceTest.java
+++
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/MonitorServiceTest.java
@@ -21,6 +21,8 @@ import static
org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
@@ -42,6 +44,7 @@ import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.message.CollectRep;
+import org.apache.hertzbeat.common.util.AesUtil;
import org.apache.hertzbeat.manager.dao.CollectorDao;
import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao;
import org.apache.hertzbeat.manager.dao.MonitorBindDao;
@@ -49,6 +52,7 @@ import org.apache.hertzbeat.manager.dao.MonitorDao;
import org.apache.hertzbeat.manager.dao.ParamDao;
import org.apache.hertzbeat.manager.pojo.dto.AppCount;
import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
+import org.apache.hertzbeat.manager.pojo.dto.MonitorParam;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
import org.apache.hertzbeat.manager.scheduler.CollectJobScheduling;
import org.apache.hertzbeat.manager.component.validator.ParamValidatorManager;
@@ -161,6 +165,242 @@ class MonitorServiceTest {
return paramDefine;
}
+ @Test
+ void validateModifyRestoresTheStoredCredentialBehindTheResponseMask() {
+ long monitorId = 101L;
+ Monitor monitor = Monitor.builder()
+ .id(monitorId)
+ .name("ollama-local")
+ .app("ollama")
+ .scrape("static")
+ .instance("127.0.0.1")
+ .intervals(60)
+ .build();
+ Param submitted = Param.builder()
+ .monitorId(monitorId)
+ .field("apiKey")
+ .paramValue(MonitorParam.SECRET_MASK)
+ .type(CommonConstants.PARAM_TYPE_PASSWORD)
+ .build();
+ String storedCiphertext = AesUtil.aesEncode("stored-ollama-key");
+ Param stored = Param.builder()
+ .id(11L)
+ .monitorId(monitorId)
+ .field("apiKey")
+ .paramValue(storedCiphertext)
+ .type(CommonConstants.PARAM_TYPE_PASSWORD)
+ .build();
+ MonitorDto dto = new MonitorDto();
+ dto.setMonitor(monitor);
+ dto.setParams(List.of(submitted));
+ when(appService.getAppParamDefines("ollama"))
+ .thenReturn(List.of(newParamDefine("apiKey", "password",
false)));
+
when(appService.getAppDefineOption("ollama-local")).thenReturn(Optional.empty());
+
when(monitorDao.findMonitorByNameEquals("ollama-local")).thenReturn(Optional.of(monitor));
+
when(paramDao.findParamsByMonitorId(monitorId)).thenReturn(List.of(stored));
+ Job job = new Job();
+ job.setMetrics(Collections.emptyList());
+ when(appService.getAppDefine("ollama")).thenReturn(job);
+
+ monitorService.validate(dto, true);
+
+ ArgumentCaptor<MonitorParam> paramCaptor =
ArgumentCaptor.forClass(MonitorParam.class);
+ verify(paramValidatorManager).validate(any(ParamDefineInfo.class),
paramCaptor.capture());
+ assertEquals(storedCiphertext, paramCaptor.getValue().getParamValue());
+ assertEquals(storedCiphertext, dto.getParams().get(0).getParamValue());
+ }
+
+ @Test
+ void validateDetectRestoresStoredCredentialForAnExistingMonitor() {
+ long monitorId = 103L;
+ Monitor monitor = Monitor.builder()
+ .id(monitorId)
+ .name("ollama-detect")
+ .app("ollama")
+ .scrape("static")
+ .instance("127.0.0.1")
+ .intervals(60)
+ .build();
+ Param submitted = Param.builder()
+ .monitorId(monitorId)
+ .field("apiKey")
+ .paramValue(MonitorParam.SECRET_MASK)
+ .type(CommonConstants.PARAM_TYPE_PASSWORD)
+ .build();
+ String storedCiphertext = AesUtil.aesEncode("stored-detect-key");
+ Param stored = Param.builder()
+ .id(31L)
+ .monitorId(monitorId)
+ .field("apiKey")
+ .paramValue(storedCiphertext)
+ .type(CommonConstants.PARAM_TYPE_PASSWORD)
+ .build();
+ MonitorDto dto = new MonitorDto();
+ dto.setMonitor(monitor);
+ dto.setParams(List.of(submitted));
+ when(appService.getAppParamDefines("ollama"))
+ .thenReturn(List.of(newParamDefine("apiKey", "password",
false)));
+
when(paramDao.findParamsByMonitorId(monitorId)).thenReturn(List.of(stored));
+ Job job = new Job();
+ job.setMetrics(Collections.emptyList());
+ when(appService.getAppDefine("ollama")).thenReturn(job);
+
+ monitorService.validate(dto, null);
+
+ assertEquals(storedCiphertext, dto.getParams().get(0).getParamValue());
+ verify(paramValidatorManager).validate(any(ParamDefineInfo.class),
any(MonitorParam.class));
+ }
+
+ @Test
+ void validateModifyRejectsMaskedCredentialWhenDestinationChanges() {
+ long monitorId = 102L;
+ Monitor monitor = Monitor.builder()
+ .id(monitorId)
+ .name("ollama-remote")
+ .app("ollama")
+ .scrape("static")
+ .instance("attacker.example")
+ .intervals(60)
+ .build();
+ Param submittedHost = Param.builder()
+ .monitorId(monitorId)
+ .field("host")
+ .paramValue("attacker.example")
+ .type(CommonConstants.PARAM_TYPE_STRING)
+ .build();
+ Param submittedSecret = Param.builder()
+ .monitorId(monitorId)
+ .field("apiKey")
+ .paramValue(MonitorParam.SECRET_MASK)
+ .type(CommonConstants.PARAM_TYPE_PASSWORD)
+ .build();
+ Param storedHost = Param.builder()
+ .id(21L)
+ .monitorId(monitorId)
+ .field("host")
+ .paramValue("trusted.example")
+ .type(CommonConstants.PARAM_TYPE_STRING)
+ .build();
+ Param storedSecret = Param.builder()
+ .id(22L)
+ .monitorId(monitorId)
+ .field("apiKey")
+ .paramValue(AesUtil.aesEncode("stored-ollama-key"))
+ .type(CommonConstants.PARAM_TYPE_PASSWORD)
+ .build();
+ MonitorDto dto = new MonitorDto();
+ dto.setMonitor(monitor);
+ dto.setParams(List.of(submittedHost, submittedSecret));
+ when(appService.getAppParamDefines("ollama")).thenReturn(List.of(
+ newParamDefine("host", "host", true),
+ newParamDefine("apiKey", "password", false)));
+
when(appService.getAppDefineOption("ollama-remote")).thenReturn(Optional.empty());
+
when(monitorDao.findMonitorByNameEquals("ollama-remote")).thenReturn(Optional.of(monitor));
+
when(paramDao.findParamsByMonitorId(monitorId)).thenReturn(List.of(storedHost,
storedSecret));
+
+ assertThrows(IllegalArgumentException.class, () ->
monitorService.validate(dto, true));
+ }
+
+ @Test
+ void
validateModifyRestoresHttpServiceDiscoveryCredentialFromScrapeDefinition() {
+ long monitorId = 103L;
+ Monitor monitor = Monitor.builder()
+ .id(monitorId)
+ .name("discovered-prometheus")
+ .app("prometheus")
+ .scrape("http_sd")
+ .instance("https://discovery.example")
+ .intervals(60)
+ .build();
+ Param submittedUrl = Param.builder()
+ .monitorId(monitorId)
+ .field("__sd_url__")
+ .paramValue("https://discovery.example")
+ .type(CommonConstants.PARAM_TYPE_STRING)
+ .build();
+ Param submittedToken = Param.builder()
+ .monitorId(monitorId)
+ .field("__sd_token__")
+ .paramValue(MonitorParam.SECRET_MASK)
+ .type(CommonConstants.PARAM_TYPE_PASSWORD)
+ .build();
+ String storedCiphertext = AesUtil.aesEncode("stored-discovery-token");
+ Param storedUrl = submittedUrl.clone();
+ Param storedToken = Param.builder()
+ .id(32L)
+ .monitorId(monitorId)
+ .field("__sd_token__")
+ .paramValue(storedCiphertext)
+ .type(CommonConstants.PARAM_TYPE_PASSWORD)
+ .build();
+ MonitorDto dto = new MonitorDto();
+ dto.setMonitor(monitor);
+ dto.setParams(List.of(submittedUrl, submittedToken));
+
when(appService.getAppParamDefines("prometheus")).thenReturn(Collections.emptyList());
+ when(appService.getAppParamDefines("http_sd")).thenReturn(List.of(
+ newParamDefine("__sd_url__", "text", true),
+ newParamDefine("__sd_token__", "password", false)));
+
when(appService.getAppDefineOption("discovered-prometheus")).thenReturn(Optional.empty());
+
when(monitorDao.findMonitorByNameEquals("discovered-prometheus")).thenReturn(Optional.of(monitor));
+
when(paramDao.findParamsByMonitorId(monitorId)).thenReturn(List.of(storedUrl,
storedToken));
+ Job applicationJob = new Job();
+ applicationJob.setMetrics(Collections.emptyList());
+ when(appService.getAppDefine("prometheus")).thenReturn(applicationJob);
+ Job job = new Job();
+ job.setMetrics(Collections.emptyList());
+ when(appService.getAppDefine("http_sd")).thenReturn(job);
+
+ monitorService.validate(dto, true);
+
+ assertEquals(storedCiphertext, dto.getParams().stream()
+ .filter(param -> "__sd_token__".equals(param.getField()))
+ .findFirst()
+ .orElseThrow()
+ .getParamValue());
+ }
+
+ @Test
+ void
validateServiceDiscoveryMonitorChecksApplicationAndScrapeCredentials() {
+ Monitor monitor = Monitor.builder()
+ .id(104L)
+ .name("discovered-mysql")
+ .app("mysql")
+ .scrape("http_sd")
+ .instance("https://discovery.example")
+ .intervals(60)
+ .build();
+ Param submittedPassword = Param.builder()
+ .monitorId(monitor.getId())
+ .field("password")
+ .paramValue("database-secret")
+ .build();
+ Param submittedDiscoveryToken = Param.builder()
+ .monitorId(monitor.getId())
+ .field("__sd_token__")
+ .paramValue("discovery-secret")
+ .build();
+ MonitorDto dto = new MonitorDto();
+ dto.setMonitor(monitor);
+ dto.setParams(List.of(submittedPassword, submittedDiscoveryToken));
+ ParamDefineInfo applicationPassword = newParamDefine("password",
"password", true);
+ ParamDefineInfo discoveryToken = newParamDefine("__sd_token__",
"password", true);
+
when(appService.getAppParamDefines("mysql")).thenReturn(List.of(applicationPassword));
+
when(appService.getAppParamDefines("http_sd")).thenReturn(List.of(discoveryToken));
+ Job applicationJob = new Job();
+ applicationJob.setMetrics(Collections.emptyList());
+ Job scrapeJob = new Job();
+ scrapeJob.setMetrics(Collections.emptyList());
+ when(appService.getAppDefine("mysql")).thenReturn(applicationJob);
+ when(appService.getAppDefine("http_sd")).thenReturn(scrapeJob);
+
+ monitorService.validate(dto, null);
+
+ verify(paramValidatorManager).validate(
+ eq(applicationPassword), argThat(param ->
"password".equals(param.getField())));
+ verify(paramValidatorManager).validate(
+ eq(discoveryToken), argThat(param ->
"__sd_token__".equals(param.getField())));
+ }
+
@Test
void detectMonitorEmpty() {
Monitor monitor = Monitor.builder()
@@ -775,6 +1015,38 @@ class MonitorServiceTest {
assertNotNull(monitorDto);
}
+ @Test
+ void getMonitorDtoMasksCredentialsButExportKeepsCiphertext() {
+ long id = 2L;
+ Monitor monitor = Monitor.builder()
+ .jobId(id)
+ .intervals(1)
+ .app("ollama")
+ .name("ollama-export")
+ .instance("localhost")
+ .id(id)
+ .build();
+ String ciphertext = AesUtil.aesEncode("portable-secret");
+ Param secret = Param.builder()
+ .monitorId(id)
+ .field("apiKey")
+ .paramValue(ciphertext)
+ .type(CommonConstants.PARAM_TYPE_PASSWORD)
+ .build();
+ when(monitorDao.findById(id)).thenReturn(Optional.of(monitor));
+ when(paramDao.findParamsByMonitorId(id)).thenReturn(List.of(secret));
+ Job job = new Job();
+ job.setMetrics(new ArrayList<>());
+ when(appService.getAppDefine(monitor.getApp())).thenReturn(job);
+
when(collectorMonitorBindDao.findCollectorMonitorBindByMonitorId(id)).thenReturn(Optional.empty());
+
+ MonitorDto response = monitorService.getMonitorDto(id);
+ MonitorDto export = monitorService.getMonitorDtoForExport(id);
+
+ assertEquals(MonitorParam.SECRET_MASK,
response.getParamInfos().getFirst().getParamValue());
+ assertEquals(ciphertext,
export.getParamInfos().getFirst().getParamValue());
+ }
+
@Test
void getMonitors() {
when(monitorDao.findAll(any(Specification.class),
any(PageRequest.class))).thenAnswer((invocation) -> {
diff --git a/home/docs/help/http_sd.md b/home/docs/help/http_sd.md
index a1eb5ffffd..9e45cbee2e 100644
--- a/home/docs/help/http_sd.md
+++ b/home/docs/help/http_sd.md
@@ -50,6 +50,18 @@ You need to provide or develop an HTTP API that meets the
following requirements
| Collection interval | Interval time of monitor periodic data collection,
unit: second, and the minimum interval that can be set is 30 seconds. |
| Description remarks | For more information about identifying and describing
this monitoring, users can note information here. |
+### Credential upgrade behavior
+
+The access token and password parameters are encrypted before they are stored
+and are returned by the monitor API as `******`. Submitting that mask while
+editing an existing monitor keeps the stored credential unchanged. If the
+service-discovery URL or authentication type changes, the credential must be
+re-entered so that a stored value cannot be replayed to a new endpoint.
+
+On the first startup after upgrading, HertzBeat encrypts access tokens created
+by older versions before scheduling service-discovery jobs. The migration is
+idempotent and leaves already encrypted values unchanged.
+
### Usage Steps
1. **Prepare HTTP API**
diff --git a/home/docs/help/ollama.md b/home/docs/help/ollama.md
index 007864d0ed..16abc2a2a5 100644
--- a/home/docs/help/ollama.md
+++ b/home/docs/help/ollama.md
@@ -28,6 +28,17 @@ allow external access.
| Bound Tags | Tags for categorizing and managing monitoring
resources. |
| Description/Remarks | Additional remarks to identify and describe this
monitoring. Users can add notes here. |
+### Credential upgrade behavior
+
+The API key is encrypted before it is stored and is returned by the monitor API
+as `******`. Submitting that mask while editing an existing monitor keeps the
+stored key unchanged. If the host, port, or SSL setting changes, the key must
+be re-entered so that a stored credential cannot be replayed to a new endpoint.
+
+On the first startup after upgrading, HertzBeat encrypts API keys created by
+older versions before scheduling collection jobs. The migration is idempotent
+and does not change an already encrypted value.
+
### Collection Metrics
#### Metric Set: Version Info
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]