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 ecd7d62ad fix(alert): reject native rules without a supported operator 
(#6642)
ecd7d62ad is described below

commit ecd7d62adee6b7e3722b718b7f5370ed347cd8e4
Author: Xiao Yang <[email protected]>
AuthorDate: Mon Oct 19 20:08:20 2026 +0800

    fix(alert): reject native rules without a supported operator (#6642)
    
    Verified against `rocketmq-studio` at 056af345c before merging: the defect 
is still present on the
    current trunk and the change merges cleanly.
    
    Maintainer follow-ups on top of the contributor commit:
    - fix(alert): judge the trimmed native operator and pin the whitelist to 
the evaluator
    
    Each follow-up commit message records the review finding it addresses.
    
    Locally verified: targeted suites green on the integration branch; a full 
backend and frontend
    build plus test pass runs on the trunk once this batch lands.
---
 .../studio/ops/alert/NativeAlertRulePolicy.java    | 11 +++-
 .../ops/alert/AlertRuleTransferServiceTest.java    | 28 ++++++++
 .../studio/ops/alert/AlertServiceTest.java         | 23 +++++++
 .../ops/alert/NativeAlertRulePolicyTest.java       | 74 +++++++++++++++++++++-
 4 files changed, 133 insertions(+), 3 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRulePolicy.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRulePolicy.java
index 36654aff2..b8a1085be 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRulePolicy.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRulePolicy.java
@@ -45,6 +45,7 @@ final class NativeAlertRulePolicy {
     private static final Set<String> AVAILABILITY_METRICS = Set.of(
             "nameserver.availability", "broker.availability", 
"proxy.availability", "cloud.instance.availability");
     private static final Set<String> NOTIFICATION_CHANNELS = 
Set.of("dingtalk", "sms", "email");
+    static final Set<String> NATIVE_OPERATORS = Set.of(">", ">=", "<", "<=", 
"==", "!=", "UNAVAILABLE");
 
     private NativeAlertRulePolicy() {
     }
@@ -69,7 +70,15 @@ final class NativeAlertRulePolicy {
         if (!StringUtils.hasText(rule.getInstanceId())) {
             throw new BusinessException(400, "instanceId is required for 
native alert rules");
         }
-        if ("UNAVAILABLE".equals(rule.getOperator()) && 
!AVAILABILITY_METRICS.contains(rule.getMetric())) {
+        // AlertRuleEvaluator trims the operator before comparing and 
NativeAlertRuleTestService stores the
+        // trimmed value, so the whitelist has to judge the trimmed operator 
as well. Otherwise " > " is
+        // rejected here while every consumer downstream would have accepted 
it.
+        String operator = StringUtils.hasText(rule.getOperator()) ? 
rule.getOperator().trim() : null;
+        if (operator == null || !NATIVE_OPERATORS.contains(operator)) {
+            throw new BusinessException(400,
+                    "operator must be one of >, >=, <, <=, ==, != or 
UNAVAILABLE for native alert rules");
+        }
+        if ("UNAVAILABLE".equals(operator) && 
!AVAILABILITY_METRICS.contains(rule.getMetric())) {
             throw new BusinessException(400, "UNAVAILABLE is only supported 
for native availability metrics");
         }
         if (StringUtils.hasText(rule.getConsumerGroup()) && 
!GROUP_SCOPED_METRICS.contains(rule.getMetric())) {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleTransferServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleTransferServiceTest.java
index a3aaaf982..b9849bb84 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleTransferServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleTransferServiceTest.java
@@ -7,6 +7,9 @@
 package org.apache.rocketmq.studio.ops.alert;
 
 import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.audit.OperationAuditService;
+import org.apache.rocketmq.studio.cluster.metrics.MetricProfileService;
+import org.apache.rocketmq.studio.cluster.metrics.PrometheusProperties;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
@@ -23,6 +26,8 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
@@ -97,6 +102,29 @@ class AlertRuleTransferServiceTest {
         verify(alertService, never()).createRule(eq(AlertDomain.CLUSTER), 
any(AlertRuleVO.class));
     }
 
+    @Test
+    void rejectsImportedNativeRuleWithoutOperatorBeforePersistenceTest() {
+        AlertRepository repository = mock(AlertRepository.class);
+        OperationAuditService auditService = mock(OperationAuditService.class);
+        AlertService realService = new AlertService(repository, 
mock(AlertStateRepository.class),
+                new AlertRuleAssetService(), auditService, new 
MetricProfileService(new PrometheusProperties()));
+        AlertRuleTransferService realTransfer = new 
AlertRuleTransferService(realService, metricCatalogService);
+        lenient().when(repository.insertRule(any(AlertRuleVO.class)))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+        AlertRuleRequestDTO invalid = request("Missing operator");
+        invalid.setMetric("broker.availability");
+        invalid.setInstanceId("local");
+        invalid.setOperator(null);
+
+        BusinessException error = assertThrows(BusinessException.class,
+                () -> realTransfer.importRules(AlertDomain.CLUSTER,
+                        transfer(AlertDomain.CLUSTER, invalid)));
+
+        assertEquals(400, error.getCode());
+        verify(repository, never()).insertRule(any(AlertRuleVO.class));
+        verifyNoInteractions(auditService);
+    }
+
     @Test
     void importsRulesAsNewRulesInTheRequestedDomainTest() {
         AlertRuleRequestDTO request = request("Broker unavailable");
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
index 82e4d1a89..cd726daa2 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
@@ -49,6 +49,8 @@ import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -71,6 +73,27 @@ class AlertServiceTest {
                 operationAuditService, new MetricProfileService(new 
PrometheusProperties()));
     }
 
+    @Test
+    void createNativeRuleShouldRejectMissingOperatorBeforePersistenceTest() {
+        AlertRuleRequestDTO request = new AlertRuleRequestDTO();
+        request.setName("Orders lag");
+        request.setMetric("consumer.lag.total");
+        request.setInstanceId("local");
+        request.setConsumerGroup("orders");
+        request.setThreshold(100);
+        request.setEnabled(true);
+        lenient().when(alertRepository.insertRule(any(AlertRuleVO.class)))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+
+        assertThatThrownBy(() -> alertService.createRule(AlertDomain.BUSINESS, 
request.toAlertRuleVO()))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("operator")
+                .satisfies(error -> assertThat(((BusinessException) 
error).getCode()).isEqualTo(400));
+
+        verify(alertRepository, never()).insertRule(any(AlertRuleVO.class));
+        verifyNoInteractions(operationAuditService);
+    }
+
     @Test
     void listRulesShouldReturnAllRulesTest() {
         AlertRuleVO rule1 = AlertRuleVO.builder().id(1L).name("High 
CPU").metric("cpu_usage")
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRulePolicyTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRulePolicyTest.java
index a2de0f1f2..5b304c9da 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRulePolicyTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRulePolicyTest.java
@@ -16,12 +16,20 @@
  */
 package org.apache.rocketmq.studio.ops.alert;
 
+import org.apache.rocketmq.studio.cluster.metrics.MetricAvailability;
+import org.apache.rocketmq.studio.cluster.metrics.MetricSample;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
 
+import java.time.Instant;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
@@ -84,7 +92,50 @@ class NativeAlertRulePolicyTest {
     @Test
     void leavesLegacyPrometheusRulesCompatibleTest() {
         assertThatCode(() -> 
NativeAlertRulePolicy.validate(rule(AlertDomain.BUSINESS,
-                
"rocketmq_consumer_lag_messages").build())).doesNotThrowAnyException();
+                
"rocketmq_consumer_lag_messages").operator(null).build())).doesNotThrowAnyException();
+    }
+
+    @ParameterizedTest
+    @NullAndEmptySource
+    @ValueSource(strings = {" ", "INVALID", "GT", "unavailable"})
+    void rejectsNativeRulesWithoutSupportedOperatorTest(String operator) {
+        assertThatThrownBy(() -> 
NativeAlertRulePolicy.validate(rule(AlertDomain.CLUSTER, "broker.availability")
+                .instanceId("local").operator(operator).build()))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("operator")
+                .satisfies(error -> assertThat(((BusinessException) 
error).getCode()).isEqualTo(400));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {">", ">=", "<", "<=", "==", "!=", "UNAVAILABLE"})
+    void acceptsSupportedNativeOperatorsTest(String operator) {
+        assertThatCode(() -> 
NativeAlertRulePolicy.validate(rule(AlertDomain.CLUSTER, "broker.availability")
+                
.instanceId("local").operator(operator).build())).doesNotThrowAnyException();
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {" > ", " UNAVAILABLE "})
+    void acceptsPaddedOperatorsLikeTheEvaluatorDoesTest(String operator) {
+        // AlertRuleEvaluator.compare() trims the operator, so validation has 
to accept the padded form
+        // instead of rejecting a rule the evaluator would happily run.
+        assertThatCode(() -> 
NativeAlertRulePolicy.validate(rule(AlertDomain.CLUSTER, "broker.availability")
+                
.instanceId("local").operator(operator).build())).doesNotThrowAnyException();
+    }
+
+    @Test
+    void keepsNativeOperatorWhitelistInSyncWithTheEvaluatorTest() {
+        // The whitelist exists three times: AlertRuleRequestDTO's @Pattern, 
AlertRuleEvaluator.compare()
+        // and NativeAlertRulePolicy.NATIVE_OPERATORS. Pinning the evaluator 
here means an operator added
+        // to the policy without a matching switch case fails the build 
instead of silently evaluating to
+        // false at runtime.
+        assertThat(NativeAlertRulePolicy.NATIVE_OPERATORS)
+                .containsExactlyInAnyOrder(">", ">=", "<", "<=", "==", "!=", 
"UNAVAILABLE");
+        AlertRuleEvaluator evaluator = new AlertRuleEvaluator();
+        for (String operator : NativeAlertRulePolicy.NATIVE_OPERATORS) {
+            assertThat(evaluatorSupports(evaluator, operator))
+                    .as("AlertRuleEvaluator must implement operator %s", 
operator)
+                    .isTrue();
+        }
     }
 
     @Test
@@ -118,6 +169,25 @@ class NativeAlertRulePolicyTest {
     }
 
     private static AlertRuleVO.AlertRuleVOBuilder rule(AlertDomain domain, 
String metric) {
-        return AlertRuleVO.builder().domain(domain).name("Test 
rule").metric(metric).consecutiveSamples(1);
+        return AlertRuleVO.builder().domain(domain).name("Test 
rule").metric(metric).operator(">")
+                .consecutiveSamples(1);
+    }
+
+    /**
+     * Probes one operator against the sample values that make every supported 
comparison fire at least
+     * once, so an operator missing from AlertRuleEvaluator.compare() reports 
false here.
+     */
+    private static boolean evaluatorSupports(AlertRuleEvaluator evaluator, 
String operator) {
+        List<MetricSample> samples = List.of(sample(4D, 
MetricAvailability.AVAILABLE),
+                sample(5D, MetricAvailability.AVAILABLE), sample(6D, 
MetricAvailability.AVAILABLE),
+                sample(null, MetricAvailability.UNAVAILABLE));
+        AlertRuleVO rule = rule(AlertDomain.CLUSTER, 
"broker.availability").instanceId("local")
+                .operator(operator).threshold(5D).build();
+        return samples.stream().anyMatch(sample -> evaluator.evaluate(rule, 
sample).conditionMet());
+    }
+
+    private static MetricSample sample(Double value, MetricAvailability 
availability) {
+        return new MetricSample("broker.availability", AlertDomain.CLUSTER, 
"local", "local", Map.of(), value,
+                availability, Instant.EPOCH);
     }
 }

Reply via email to