diff --git a/hertzbeat-startup/src/main/resources/sureness.yml b/hertzbeat-startup/src/main/resources/sureness.yml index ee294024421..f63bee0e8f5 100644 --- a/hertzbeat-startup/src/main/resources/sureness.yml +++ b/hertzbeat-startup/src/main/resources/sureness.yml @@ -70,6 +70,23 @@ resourceRole: - /api/config/**===post===[admin] - /api/config/**===put===[admin] - /api/config/**===delete===[admin] + # queue depth of the hertzbeat process itself, operational data + - /api/metrics===get===[admin] + # per account metric favourites rendered on the monitor pages + - /api/metrics/**===get===[admin,user,guest] + - /api/metrics/**===post===[admin,user,guest] + - /api/metrics/**===delete===[admin,user,guest] + - /api/label/**===get===[admin,user,guest] + - /api/label/**===post===[admin,user] + - /api/label/**===put===[admin,user] + - /api/label/**===delete===[admin] + # the storage availability probe is read by every monitor page, while the query + # route forwards a raw promql expression straight to the time series database + - /api/warehouse/**===get===[admin,user,guest] + - /api/warehouse/query===post===[admin] + - /api/logs/otlp/**===post===[admin,user] + - /api/logs===delete===[admin] + - /api/v2/alerts===post===[admin,user] - /api/status/page/**===get===[admin,user,guest] - /api/status/page/**===post===[admin,user] - /api/status/page/**===put===[admin,user] diff --git a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessUnruledEndpointTest.java b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessUnruledEndpointTest.java new file mode 100644 index 00000000000..49ef4a867ed --- /dev/null +++ b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessUnruledEndpointTest.java @@ -0,0 +1,130 @@ +/* + * 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.startup.security; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import com.usthe.sureness.matcher.util.TirePathTree; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; + +/** + * Guards the routes that carried no rbac rule at all. + * + *

A route absent from {@code sureness.yml} leaves `supportRoles` null, and + * `BaseProcessor.authorized` returns early when no role is required, so every one of + * these was reachable by any authenticated account including {@code guest}: a raw promql + * passthrough to the time series database, log deletion, log and alert injection, label + * management and the internal queue metrics of the hertzbeat process. + */ +class SurenessUnruledEndpointTest { + + private static final String SEPARATOR = "==="; + + private static TirePathTree roleTree; + + @BeforeAll + @SuppressWarnings("unchecked") + static void loadSurenessConfig() throws IOException { + List resourceRole; + try (InputStream in = SurenessUnruledEndpointTest.class.getResourceAsStream("/sureness.yml")) { + assertNotNull(in, "sureness.yml must be on the classpath"); + Map document = new Yaml().load(in); + resourceRole = (List) document.get("resourceRole"); + } + assertNotNull(resourceRole, "resourceRole must be present"); + roleTree = new TirePathTree(); + roleTree.buildTree(new LinkedHashSet<>(resourceRole)); + } + + private static String rolesFor(String path, String method) { + return roleTree.searchPathFilterRoles(path + SEPARATOR + method); + } + + /** + * `PromqlQueryExecutor` forwards the submitted expression verbatim, so this route reads + * the whole metric store regardless of which monitors the caller may see. + */ + @Test + void queryingTheWarehouseDirectlyIsRestrictedToAdmin() { + assertEquals("[admin]", rolesFor("/api/warehouse/query", "post")); + } + + @Test + void probingStorageAvailabilityStaysOpenToEveryRole() { + assertEquals("[admin,user,guest]", rolesFor("/api/warehouse/storage/status", "get")); + } + + @Test + void deletingLogsIsRestrictedToAdmin() { + assertEquals("[admin]", rolesFor("/api/logs", "delete")); + } + + @Test + void readingLogsStaysOpenToEveryRole() { + assertEquals("[admin,user,guest]", rolesFor("/api/logs/list", "get")); + } + + /** + * Matches how the sibling ingestion routes `/api/otlp/**` and `/api/logs/ingest/**` + * are already scoped, so a low privileged account can no longer forge log records. + */ + @Test + void ingestingOtlpLogsRequiresAtLeastUser() { + assertEquals("[admin,user]", rolesFor("/api/logs/otlp/v1/logs", "post")); + } + + /** + * The prometheus alertmanager webhook injects alerts, which drive notifications. + * Scoped like the sibling `/api/alerts/report` route. + */ + @Test + void injectingPrometheusAlertsRequiresAtLeastUser() { + assertEquals("[admin,user]", rolesFor("/api/v2/alerts", "post")); + } + + @Test + void labelWritesFollowTheUsualScoping() { + assertEquals("[admin,user,guest]", rolesFor("/api/label", "get")); + assertEquals("[admin,user]", rolesFor("/api/label", "post")); + assertEquals("[admin,user]", rolesFor("/api/label", "put")); + assertEquals("[admin]", rolesFor("/api/label", "delete")); + } + + @Test + void processQueueMetricsAreRestrictedToAdmin() { + assertEquals("[admin]", rolesFor("/api/metrics", "get")); + } + + /** + * Favourites are stored per account and rendered on the monitor pages, so they stay + * reachable by every role even though they sit under the same path prefix. + */ + @Test + void metricFavouritesStayOpenToEveryRole() { + assertEquals("[admin,user,guest]", rolesFor("/api/metrics/favorite/1", "get")); + assertEquals("[admin,user,guest]", rolesFor("/api/metrics/favorite/1/cpu", "post")); + assertEquals("[admin,user,guest]", rolesFor("/api/metrics/favorite/1/cpu", "delete")); + } +} diff --git a/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml b/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml index fd283a60c68..069fba30690 100644 --- a/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml +++ b/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml @@ -70,6 +70,23 @@ resourceRole: - /api/config/**===post===[admin] - /api/config/**===put===[admin] - /api/config/**===delete===[admin] + # queue depth of the hertzbeat process itself, operational data + - /api/metrics===get===[admin] + # per account metric favourites rendered on the monitor pages + - /api/metrics/**===get===[admin,user,guest] + - /api/metrics/**===post===[admin,user,guest] + - /api/metrics/**===delete===[admin,user,guest] + - /api/label/**===get===[admin,user,guest] + - /api/label/**===post===[admin,user] + - /api/label/**===put===[admin,user] + - /api/label/**===delete===[admin] + # the storage availability probe is read by every monitor page, while the query + # route forwards a raw promql expression straight to the time series database + - /api/warehouse/**===get===[admin,user,guest] + - /api/warehouse/query===post===[admin] + - /api/logs/otlp/**===post===[admin,user] + - /api/logs===delete===[admin] + - /api/v2/alerts===post===[admin,user] - /api/status/page/**===get===[admin,user,guest] - /api/status/page/**===post===[admin,user] - /api/status/page/**===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 fd283a60c68..069fba30690 100644 --- a/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml +++ b/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml @@ -70,6 +70,23 @@ resourceRole: - /api/config/**===post===[admin] - /api/config/**===put===[admin] - /api/config/**===delete===[admin] + # queue depth of the hertzbeat process itself, operational data + - /api/metrics===get===[admin] + # per account metric favourites rendered on the monitor pages + - /api/metrics/**===get===[admin,user,guest] + - /api/metrics/**===post===[admin,user,guest] + - /api/metrics/**===delete===[admin,user,guest] + - /api/label/**===get===[admin,user,guest] + - /api/label/**===post===[admin,user] + - /api/label/**===put===[admin,user] + - /api/label/**===delete===[admin] + # the storage availability probe is read by every monitor page, while the query + # route forwards a raw promql expression straight to the time series database + - /api/warehouse/**===get===[admin,user,guest] + - /api/warehouse/query===post===[admin] + - /api/logs/otlp/**===post===[admin,user] + - /api/logs===delete===[admin] + - /api/v2/alerts===post===[admin,user] - /api/status/page/**===get===[admin,user,guest] - /api/status/page/**===post===[admin,user] - /api/status/page/**===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 fd283a60c68..069fba30690 100644 --- a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml +++ b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml @@ -70,6 +70,23 @@ resourceRole: - /api/config/**===post===[admin] - /api/config/**===put===[admin] - /api/config/**===delete===[admin] + # queue depth of the hertzbeat process itself, operational data + - /api/metrics===get===[admin] + # per account metric favourites rendered on the monitor pages + - /api/metrics/**===get===[admin,user,guest] + - /api/metrics/**===post===[admin,user,guest] + - /api/metrics/**===delete===[admin,user,guest] + - /api/label/**===get===[admin,user,guest] + - /api/label/**===post===[admin,user] + - /api/label/**===put===[admin,user] + - /api/label/**===delete===[admin] + # the storage availability probe is read by every monitor page, while the query + # route forwards a raw promql expression straight to the time series database + - /api/warehouse/**===get===[admin,user,guest] + - /api/warehouse/query===post===[admin] + - /api/logs/otlp/**===post===[admin,user] + - /api/logs===delete===[admin] + - /api/v2/alerts===post===[admin,user] - /api/status/page/**===get===[admin,user,guest] - /api/status/page/**===post===[admin,user] - /api/status/page/**===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 2c056a8448d..d15a6ac5d26 100644 --- a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml +++ b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml @@ -70,6 +70,23 @@ resourceRole: - /api/config/**===post===[admin] - /api/config/**===put===[admin] - /api/config/**===delete===[admin] + # queue depth of the hertzbeat process itself, operational data + - /api/metrics===get===[admin] + # per account metric favourites rendered on the monitor pages + - /api/metrics/**===get===[admin,user,guest] + - /api/metrics/**===post===[admin,user,guest] + - /api/metrics/**===delete===[admin,user,guest] + - /api/label/**===get===[admin,user,guest] + - /api/label/**===post===[admin,user] + - /api/label/**===put===[admin,user] + - /api/label/**===delete===[admin] + # the storage availability probe is read by every monitor page, while the query + # route forwards a raw promql expression straight to the time series database + - /api/warehouse/**===get===[admin,user,guest] + - /api/warehouse/query===post===[admin] + - /api/logs/otlp/**===post===[admin,user] + - /api/logs===delete===[admin] + - /api/v2/alerts===post===[admin,user] - /api/status/page/**===get===[admin,user,guest] - /api/status/page/**===post===[admin,user] - /api/status/page/**===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 fd283a60c68..069fba30690 100644 --- a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml +++ b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml @@ -70,6 +70,23 @@ resourceRole: - /api/config/**===post===[admin] - /api/config/**===put===[admin] - /api/config/**===delete===[admin] + # queue depth of the hertzbeat process itself, operational data + - /api/metrics===get===[admin] + # per account metric favourites rendered on the monitor pages + - /api/metrics/**===get===[admin,user,guest] + - /api/metrics/**===post===[admin,user,guest] + - /api/metrics/**===delete===[admin,user,guest] + - /api/label/**===get===[admin,user,guest] + - /api/label/**===post===[admin,user] + - /api/label/**===put===[admin,user] + - /api/label/**===delete===[admin] + # the storage availability probe is read by every monitor page, while the query + # route forwards a raw promql expression straight to the time series database + - /api/warehouse/**===get===[admin,user,guest] + - /api/warehouse/query===post===[admin] + - /api/logs/otlp/**===post===[admin,user] + - /api/logs===delete===[admin] + - /api/v2/alerts===post===[admin,user] - /api/status/page/**===get===[admin,user,guest] - /api/status/page/**===post===[admin,user] - /api/status/page/**===put===[admin,user] diff --git a/script/sureness.yml b/script/sureness.yml index 2c056a8448d..d15a6ac5d26 100644 --- a/script/sureness.yml +++ b/script/sureness.yml @@ -70,6 +70,23 @@ resourceRole: - /api/config/**===post===[admin] - /api/config/**===put===[admin] - /api/config/**===delete===[admin] + # queue depth of the hertzbeat process itself, operational data + - /api/metrics===get===[admin] + # per account metric favourites rendered on the monitor pages + - /api/metrics/**===get===[admin,user,guest] + - /api/metrics/**===post===[admin,user,guest] + - /api/metrics/**===delete===[admin,user,guest] + - /api/label/**===get===[admin,user,guest] + - /api/label/**===post===[admin,user] + - /api/label/**===put===[admin,user] + - /api/label/**===delete===[admin] + # the storage availability probe is read by every monitor page, while the query + # route forwards a raw promql expression straight to the time series database + - /api/warehouse/**===get===[admin,user,guest] + - /api/warehouse/query===post===[admin] + - /api/logs/otlp/**===post===[admin,user] + - /api/logs===delete===[admin] + - /api/v2/alerts===post===[admin,user] - /api/status/page/**===get===[admin,user,guest] - /api/status/page/**===post===[admin,user] - /api/status/page/**===put===[admin,user]