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 1a7b45ec26 [fix] restrict monitoring template writes to admin (#4278)
1a7b45ec26 is described below

commit 1a7b45ec263a8593b3ea27ebfb8da7d9d4e3140d
Author: Duansg <[email protected]>
AuthorDate: Sun Aug 9 20:46:58 2026 +0800

    [fix] restrict monitoring template writes to admin (#4278)
---
 .../src/test/resources/sureness.yml                |   6 ++
 hertzbeat-manager/src/test/resources/sureness.yml  |   6 ++
 hertzbeat-startup/src/main/resources/sureness.yml  |   6 ++
 .../startup/security/SurenessResourceRuleTest.java | 104 ++++++++++++++++-----
 .../hertzbeat-mysql-iotdb/conf/sureness.yml        |   6 ++
 .../hertzbeat-mysql-tdengine/conf/sureness.yml     |   6 ++
 .../conf/sureness.yml                              |   6 ++
 .../conf/sureness.yml                              |   6 ++
 .../conf/sureness.yml                              |   6 ++
 script/sureness.yml                                |   6 ++
 10 files changed, 137 insertions(+), 21 deletions(-)

diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml 
b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
index 6f2a7dbb36..1763868f1e 100644
--- a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
+++ b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
@@ -22,6 +22,12 @@
 resourceRole:
   - /api/account/auth/refresh===post===[admin,user,guest]
   - /api/apps/**===get===[admin,user,guest]
+  # the define yml routes persist the global collection templates
+  # post can only add a new type, put overwrites an existing one and 
immediately
+  # redispatches its collect job to every monitor already using it - hence 
admin only
+  - /api/apps/**===post===[admin,user]
+  - /api/apps/**===put===[admin]
+  - /api/apps/**===delete===[admin]
   - /api/monitor/**===get===[admin,user,guest]
   - /api/monitor/**===post===[admin,user]
   - /api/monitor/**===put===[admin,user]
diff --git a/hertzbeat-manager/src/test/resources/sureness.yml 
b/hertzbeat-manager/src/test/resources/sureness.yml
index 6f2a7dbb36..1763868f1e 100644
--- a/hertzbeat-manager/src/test/resources/sureness.yml
+++ b/hertzbeat-manager/src/test/resources/sureness.yml
@@ -22,6 +22,12 @@
 resourceRole:
   - /api/account/auth/refresh===post===[admin,user,guest]
   - /api/apps/**===get===[admin,user,guest]
+  # the define yml routes persist the global collection templates
+  # post can only add a new type, put overwrites an existing one and 
immediately
+  # redispatches its collect job to every monitor already using it - hence 
admin only
+  - /api/apps/**===post===[admin,user]
+  - /api/apps/**===put===[admin]
+  - /api/apps/**===delete===[admin]
   - /api/monitor/**===get===[admin,user,guest]
   - /api/monitor/**===post===[admin,user]
   - /api/monitor/**===put===[admin,user]
diff --git a/hertzbeat-startup/src/main/resources/sureness.yml 
b/hertzbeat-startup/src/main/resources/sureness.yml
index 04c9b3b739..da66fe4cea 100644
--- a/hertzbeat-startup/src/main/resources/sureness.yml
+++ b/hertzbeat-startup/src/main/resources/sureness.yml
@@ -22,6 +22,12 @@
 resourceRole:
   - /api/account/auth/refresh===post===[admin,user,guest]
   - /api/apps/**===get===[admin,user,guest]
+  # the define yml routes persist the global collection templates
+  # post can only add a new type, put overwrites an existing one and 
immediately
+  # redispatches its collect job to every monitor already using it - hence 
admin only
+  - /api/apps/**===post===[admin,user]
+  - /api/apps/**===put===[admin]
+  - /api/apps/**===delete===[admin]
   - /api/monitor/**===get===[admin,user,guest]
   - /api/monitor/**===post===[admin,user]
   - /api/monitor/**===put===[admin,user]
diff --git 
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessResourceRuleTest.java
 
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessResourceRuleTest.java
index 40fba97784..17301b30b7 100644
--- 
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessResourceRuleTest.java
+++ 
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessResourceRuleTest.java
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.startup.security;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
 import com.usthe.sureness.matcher.util.TirePathTree;
 import java.io.IOException;
 import java.io.InputStream;
@@ -53,6 +54,8 @@ class SurenessResourceRuleTest {
 
     private static final int EXCLUDED_RESOURCE_SEGMENTS = 2;
 
+    private static final Path SCRIPT_DIR = Path.of("..", "script");
+
     private static List<String> resourceRole;
 
     private static List<String> excludedResource;
@@ -114,6 +117,38 @@ class SurenessResourceRuleTest {
         assertEquals("[admin,user,guest]", 
roleTree.searchPathFilterRoles("/api/monitors" + SEPARATOR + "get"));
     }
 
+    /**
+     * Only the {@code get} verb under {@code /api/apps} used to be listed, so 
the monitoring
+     * template writes behind {@code AppController} reached any authenticated 
caller, down to
+     * a {@code guest}.
+     *
+     * <p>The write verbs are not equally dangerous, and the rules 
deliberately differ.
+     * {@code post} refuses to overwrite an existing template, so it only adds 
a type that
+     * nobody is obliged to use, which puts it on the same footing as creating 
a monitor.
+     * {@code put} overwrites any template including the built-in ones and 
then hands the new
+     * definition to {@code updateAppCollectJob}, retroactively changing what 
every existing
+     * monitor of that type collects for every account, so it stays admin only.
+     */
+    @Test
+    void addingMonitorTemplatesIsOpenToUsers() {
+        assertEquals("[admin,user]", 
roleTree.searchPathFilterRoles("/api/apps/define/yml" + SEPARATOR + "post"));
+    }
+
+    @Test
+    void overwritingMonitorTemplatesIsRestrictedToAdmin() {
+        assertEquals("[admin]", 
roleTree.searchPathFilterRoles("/api/apps/define/yml" + SEPARATOR + "put"));
+    }
+
+    @Test
+    void deletingMonitorTemplatesIsRestrictedToAdmin() {
+        assertEquals("[admin]", 
roleTree.searchPathFilterRoles("/api/apps/linux/define/yml" + SEPARATOR + 
"delete"));
+    }
+
+    @Test
+    void readingMonitorTemplatesStaysOpenToEveryRole() {
+        assertEquals("[admin,user,guest]", 
roleTree.searchPathFilterRoles("/api/apps/linux/define/yml" + SEPARATOR + 
"get"));
+    }
+
     /**
      * The deployment scripts ship their own copies of {@code sureness.yml}; a 
rule fixed only
      * in the packaged file would still leave every container deployment 
exposed.
@@ -127,28 +162,10 @@ class SurenessResourceRuleTest {
      * wave through.
      */
     @Test
-    @SuppressWarnings("unchecked")
     void deploymentCopiesCarryWellFormedRules() throws IOException {
-        Path scriptDir = Path.of("..", "script");
-        if (!Files.isDirectory(scriptDir)) {
-            // running outside the source tree, the packaged file asserted 
above is all we can see
-            return;
-        }
-        Set<Path> copies;
-        try (Stream<Path> paths = Files.walk(scriptDir)) {
-            copies = paths.filter(path -> 
path.getFileName().toString().equals("sureness.yml"))
-                    .collect(Collectors.toCollection(LinkedHashSet::new));
-        }
-        assertFalse(copies.isEmpty(), "expected the deployment scripts to ship 
sureness.yml copies");
-        for (Path copy : copies) {
-            Map<String, Object> document;
-            try (InputStream in = Files.newInputStream(copy)) {
-                document = new Yaml().load(in);
-            }
-            List<String> copyResourceRole = (List<String>) 
document.get("resourceRole");
-            List<String> copyExcludedResource = (List<String>) 
document.get("excludedResource");
-            assertNotNull(copyResourceRole, "resourceRole must be present in " 
+ copy);
-            assertNotNull(copyExcludedResource, "excludedResource must be 
present in " + copy);
+        for (Path copy : deploymentCopies()) {
+            List<String> copyResourceRole = sectionOf(copy, "resourceRole");
+            List<String> copyExcludedResource = sectionOf(copy, 
"excludedResource");
 
             for (String rule : copyResourceRole) {
                 assertEquals(RESOURCE_ROLE_SEGMENTS, rule.split(SEPARATOR, 
-1).length,
@@ -162,4 +179,49 @@ class SurenessResourceRuleTest {
             }
         }
     }
+
+    /**
+     * Well formed is not the same as correct: a copy that simply never listed 
the write verbs
+     * passes every shape check above while leaving the monitoring template 
writes unruled. The
+     * copies are edited by hand, one per deployment flavour, so the roles 
themselves are pinned
+     * here too and a missed copy fails instead of shipping.
+     */
+    @Test
+    void deploymentCopiesRestrictMonitorTemplateWrites() throws IOException {
+        for (Path copy : deploymentCopies()) {
+            TirePathTree copyTree = new TirePathTree();
+            copyTree.buildTree(new LinkedHashSet<>(sectionOf(copy, 
"resourceRole")));
+            assertEquals("[admin,user]", 
copyTree.searchPathFilterRoles("/api/apps/define/yml" + SEPARATOR + "post"),
+                    "adding a monitoring template is unruled or over-granted 
in " + copy);
+            assertEquals("[admin]", 
copyTree.searchPathFilterRoles("/api/apps/define/yml" + SEPARATOR + "put"),
+                    "overwriting a monitoring template is unruled or 
over-granted in " + copy);
+            assertEquals("[admin]", 
copyTree.searchPathFilterRoles("/api/apps/linux/define/yml" + SEPARATOR + 
"delete"),
+                    "deleting a monitoring template is unruled or over-granted 
in " + copy);
+        }
+    }
+
+    /**
+     * @return the {@code sureness.yml} copies shipped by the deployment 
scripts
+     */
+    private static Set<Path> deploymentCopies() throws IOException {
+        assumeTrue(Files.isDirectory(SCRIPT_DIR),
+                "running outside the source tree, the packaged file asserted 
above is all we can see");
+        try (Stream<Path> paths = Files.walk(SCRIPT_DIR)) {
+            Set<Path> copies = paths.filter(path -> 
path.getFileName().toString().equals("sureness.yml"))
+                    .collect(Collectors.toCollection(LinkedHashSet::new));
+            assertFalse(copies.isEmpty(), "expected the deployment scripts to 
ship sureness.yml copies");
+            return copies;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static List<String> sectionOf(Path copy, String section) throws 
IOException {
+        Map<String, Object> document;
+        try (InputStream in = Files.newInputStream(copy)) {
+            document = new Yaml().load(in);
+        }
+        List<String> rules = (List<String>) document.get(section);
+        assertNotNull(rules, section + " must be present in " + copy);
+        return rules;
+    }
 }
diff --git a/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml 
b/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
index fe6f8938f9..f19414fb5b 100644
--- a/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
@@ -22,6 +22,12 @@
 resourceRole:
   - /api/account/auth/refresh===post===[admin,user,guest]
   - /api/apps/**===get===[admin,user,guest]
+  # the define yml routes persist the global collection templates
+  # post can only add a new type, put overwrites an existing one and 
immediately
+  # redispatches its collect job to every monitor already using it - hence 
admin only
+  - /api/apps/**===post===[admin,user]
+  - /api/apps/**===put===[admin]
+  - /api/apps/**===delete===[admin]
   - /api/monitor/**===get===[admin,user,guest]
   - /api/monitor/**===post===[admin,user]
   - /api/monitor/**===put===[admin,user]
diff --git a/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml 
b/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
index fe6f8938f9..f19414fb5b 100644
--- a/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
@@ -22,6 +22,12 @@
 resourceRole:
   - /api/account/auth/refresh===post===[admin,user,guest]
   - /api/apps/**===get===[admin,user,guest]
+  # the define yml routes persist the global collection templates
+  # post can only add a new type, put overwrites an existing one and 
immediately
+  # redispatches its collect job to every monitor already using it - hence 
admin only
+  - /api/apps/**===post===[admin,user]
+  - /api/apps/**===put===[admin]
+  - /api/apps/**===delete===[admin]
   - /api/monitor/**===get===[admin,user,guest]
   - /api/monitor/**===post===[admin,user]
   - /api/monitor/**===put===[admin,user]
diff --git 
a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml 
b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
index fe6f8938f9..f19414fb5b 100644
--- a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
@@ -22,6 +22,12 @@
 resourceRole:
   - /api/account/auth/refresh===post===[admin,user,guest]
   - /api/apps/**===get===[admin,user,guest]
+  # the define yml routes persist the global collection templates
+  # post can only add a new type, put overwrites an existing one and 
immediately
+  # redispatches its collect job to every monitor already using it - hence 
admin only
+  - /api/apps/**===post===[admin,user]
+  - /api/apps/**===put===[admin]
+  - /api/apps/**===delete===[admin]
   - /api/monitor/**===get===[admin,user,guest]
   - /api/monitor/**===post===[admin,user]
   - /api/monitor/**===put===[admin,user]
diff --git 
a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml 
b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
index 2d6b4441ee..e9c2f9f4a1 100644
--- a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
@@ -22,6 +22,12 @@
 resourceRole:
   - /api/account/auth/refresh===post===[admin,user,guest]
   - /api/apps/**===get===[admin,user,guest]
+  # the define yml routes persist the global collection templates
+  # post can only add a new type, put overwrites an existing one and 
immediately
+  # redispatches its collect job to every monitor already using it - hence 
admin only
+  - /api/apps/**===post===[admin,user]
+  - /api/apps/**===put===[admin]
+  - /api/apps/**===delete===[admin]
   - /api/monitor/**===get===[admin,user,guest]
   - /api/monitor/**===post===[admin,user]
   - /api/monitor/**===put===[admin,user]
diff --git 
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml 
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
index fe6f8938f9..f19414fb5b 100644
--- 
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
+++ 
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
@@ -22,6 +22,12 @@
 resourceRole:
   - /api/account/auth/refresh===post===[admin,user,guest]
   - /api/apps/**===get===[admin,user,guest]
+  # the define yml routes persist the global collection templates
+  # post can only add a new type, put overwrites an existing one and 
immediately
+  # redispatches its collect job to every monitor already using it - hence 
admin only
+  - /api/apps/**===post===[admin,user]
+  - /api/apps/**===put===[admin]
+  - /api/apps/**===delete===[admin]
   - /api/monitor/**===get===[admin,user,guest]
   - /api/monitor/**===post===[admin,user]
   - /api/monitor/**===put===[admin,user]
diff --git a/script/sureness.yml b/script/sureness.yml
index 2d6b4441ee..e9c2f9f4a1 100644
--- a/script/sureness.yml
+++ b/script/sureness.yml
@@ -22,6 +22,12 @@
 resourceRole:
   - /api/account/auth/refresh===post===[admin,user,guest]
   - /api/apps/**===get===[admin,user,guest]
+  # the define yml routes persist the global collection templates
+  # post can only add a new type, put overwrites an existing one and 
immediately
+  # redispatches its collect job to every monitor already using it - hence 
admin only
+  - /api/apps/**===post===[admin,user]
+  - /api/apps/**===put===[admin]
+  - /api/apps/**===delete===[admin]
   - /api/monitor/**===get===[admin,user,guest]
   - /api/monitor/**===post===[admin,user]
   - /api/monitor/**===put===[admin,user]


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

Reply via email to