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 cf737db2ee [fix] stop serving the openapi document to anonymous
callers (#4276)
cf737db2ee is described below
commit cf737db2eebb3028f4445bd757280d74a716f376
Author: Duansg <[email protected]>
AuthorDate: Fri Aug 14 10:44:48 2026 +0800
[fix] stop serving the openapi document to anonymous callers (#4276)
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.../src/test/resources/sureness.yml | 15 +-
.../config/AuthorizedSwaggerIndexTransformer.java | 126 +++++++++++++
.../hertzbeat/manager/config/SwaggerConfig.java | 28 +++
.../AuthorizedSwaggerIndexTransformerTest.java | 105 +++++++++++
hertzbeat-manager/src/test/resources/sureness.yml | 15 +-
.../src/main/resources/application.yml | 10 +
hertzbeat-startup/src/main/resources/sureness.yml | 15 +-
.../OpenApiDocumentDisabledByDefaultTest.java | 142 ++++++++++++++
.../security/SurenessOpenApiDocRuleTest.java | 207 +++++++++++++++++++++
home/docs/start/account-modify.md | 37 +++-
.../current/start/account-modify.md | 37 +++-
script/application.yml | 10 +
.../hertzbeat-mysql-iotdb/conf/application.yml | 10 +
.../hertzbeat-mysql-iotdb/conf/sureness.yml | 15 +-
.../hertzbeat-mysql-tdengine/conf/application.yml | 10 +
.../hertzbeat-mysql-tdengine/conf/sureness.yml | 15 +-
.../conf/application.yml | 10 +
.../conf/sureness.yml | 15 +-
.../conf/application.yml | 10 +
.../conf/sureness.yml | 15 +-
.../conf/application.yml | 10 +
.../conf/sureness.yml | 15 +-
script/sureness.yml | 15 +-
23 files changed, 825 insertions(+), 62 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 1763868f1e..dff4e65966 100644
--- a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
+++ b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
@@ -16,7 +16,7 @@
## -- sureness.yml account source -- ##
# config the resource restful api that need auth protection, base rbac
-# rule: api===method===role
+# rule: api===method===role
# eg: /api/v1/source1===get===[admin] means /api/v2/host===post support
role[admin] access.
# eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by
any role.
resourceRole:
@@ -86,9 +86,16 @@ resourceRole:
- /api/ingestion/otlp/**===get===[admin,user,guest]
- /api/logs/**===get===[admin,user,guest]
- /api/traces/**===get===[admin,user,guest]
+ # The OpenAPI document is a map of every route, parameter and model, so it is
+ # scoped like any other administrative resource instead of being anonymous
+ - /v3/api-docs/**===get===[admin]
+ - /v3/api-docs.yaml===get===[admin]
+ - /v3/api-docs.yaml/**===get===[admin]
+ - /v2/api-docs/**===get===[admin]
+ - /swagger-resources/**===get===[admin]
# config the resource restful api that need bypass auth protection
-# rule: api===method
+# rule: api===method
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by
anyone, no need auth.
excludedResource:
- /api/alert/sse/**===*
@@ -121,10 +128,6 @@ excludedResource:
- /**/*.json===get
- /**/*.woff===get
- /**/*.eot===get
- # swagger ui resource
- - /swagger-resources/**===get
- - /v2/api-docs===get
- - /v3/api-docs===get
# h2 database
- /h2-console/**===*
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/AuthorizedSwaggerIndexTransformer.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/AuthorizedSwaggerIndexTransformer.java
new file mode 100644
index 0000000000..9b71128609
--- /dev/null
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/AuthorizedSwaggerIndexTransformer.java
@@ -0,0 +1,126 @@
+/*
+ * 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.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springdoc.core.properties.SwaggerUiConfigProperties;
+import org.springdoc.core.properties.SwaggerUiOAuthProperties;
+import org.springdoc.core.providers.ObjectMapperProvider;
+import org.springdoc.webmvc.ui.SwaggerIndexPageTransformer;
+import org.springdoc.webmvc.ui.SwaggerWelcomeCommon;
+import org.springframework.core.io.Resource;
+import org.springframework.web.servlet.resource.ResourceTransformerChain;
+import org.springframework.web.servlet.resource.TransformedResource;
+
+/**
+ * Adds the HertzBeat access token to same-origin requests made by Swagger UI.
+ */
+final class AuthorizedSwaggerIndexTransformer extends
SwaggerIndexPageTransformer {
+
+ private static final String SWAGGER_INITIALIZER = "swagger-initializer.js";
+
+ private static final String PRESETS_MARKER = "presets: [";
+
+ private static final String INTERCEPTOR_MARKER = "requestInterceptor:
(request) => {";
+
+ private static final String INTERCEPTOR_RETURN = "return request;";
+
+ private static final String AUTHORIZATION_LOGIC = """
+ const currentUrl = new URL(document.URL);
+ const requestUrl = new URL(request.url,
document.location.origin);
+ const sameOrigin = currentUrl.protocol === requestUrl.protocol
&& currentUrl.host === requestUrl.host;
+ const token = window.localStorage.getItem('Authorization');
+ if (sameOrigin && token) {
+ request.headers['Authorization'] = `Bearer ${token}`;
+ }
+ """;
+
+ private static final String AUTHORIZATION_INTERCEPTOR = """
+ requestInterceptor: (request) => {
+ %s
+ return request;
+ },
+ """.formatted(AUTHORIZATION_LOGIC.stripTrailing());
+
+ AuthorizedSwaggerIndexTransformer(SwaggerUiConfigProperties
swaggerUiConfig,
+ SwaggerUiOAuthProperties
swaggerUiOauthProperties,
+ SwaggerWelcomeCommon
swaggerWelcomeCommon,
+ ObjectMapperProvider
objectMapperProvider) {
+ super(swaggerUiConfig, swaggerUiOauthProperties, swaggerWelcomeCommon,
objectMapperProvider);
+ }
+
+ @Override
+ public Resource transform(HttpServletRequest request, Resource resource,
+ ResourceTransformerChain transformerChain)
throws IOException {
+ final Resource transformed = super.transform(request, resource,
transformerChain);
+ if (!SWAGGER_INITIALIZER.equals(resource.getFilename())) {
+ return transformed;
+ }
+ final String initializer;
+ try (final var input = transformed.getInputStream()) {
+ initializer = new String(input.readAllBytes(),
StandardCharsets.UTF_8);
+ }
+ return new TransformedResource(transformed,
+
addAuthorizationInterceptor(initializer).getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Springdoc writes an interceptor of its own when csrf support is turned
on. Two keys
+ * of the same name would silently drop one of them, so the token is
appended to the
+ * body already there. It goes in at the end because the last write to a
header wins,
+ * and springdoc can be configured to write to {@code Authorization} as
well. The
+ * declarations above are named apart from the ones springdoc emits on
purpose: the
+ * two bodies share a scope, so a collision would be a syntax error.
+ *
+ * @param initializer the swagger initializer script
+ * @return the script with the token attached to its outgoing requests
+ */
+ static String addAuthorizationInterceptor(String initializer) {
+ final int interceptor = initializer.indexOf(INTERCEPTOR_MARKER);
+ if (interceptor >= 0) {
+ final int returnStatement = initializer.indexOf(INTERCEPTOR_RETURN,
+ interceptor + INTERCEPTOR_MARKER.length());
+ if (returnStatement < 0) {
+ throw new IllegalStateException("the swagger initializer
interceptor no longer returns the request");
+ }
+ return insertLineBefore(initializer, returnStatement,
AUTHORIZATION_LOGIC);
+ }
+ final int presets = initializer.indexOf(PRESETS_MARKER);
+ if (presets < 0) {
+ throw new IllegalStateException("the swagger initializer no longer
contains the presets marker");
+ }
+ return insertLineBefore(initializer, presets,
AUTHORIZATION_INTERCEPTOR);
+ }
+
+ /**
+ * @param initializer the swagger initializer script
+ * @param index an index into the line to insert in front of
+ * @param insertion the lines to insert, newline terminated
+ * @return the script with the insertion on its own lines, leaving the
indentation of
+ * the line at {@code index} alone
+ */
+ private static String insertLineBefore(String initializer, int index,
String insertion) {
+ final int lineStart = initializer.lastIndexOf('\n', index) + 1;
+ return initializer.substring(0, lineStart) + insertion +
initializer.substring(lineStart);
+ }
+}
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SwaggerConfig.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SwaggerConfig.java
index 295cbc91de..2ea060bf59 100644
---
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SwaggerConfig.java
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SwaggerConfig.java
@@ -25,6 +25,13 @@ import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
+import org.springdoc.core.properties.SwaggerUiConfigProperties;
+import org.springdoc.core.properties.SwaggerUiOAuthProperties;
+import org.springdoc.core.providers.ObjectMapperProvider;
+import org.springdoc.webmvc.ui.SwaggerIndexTransformer;
+import org.springdoc.webmvc.ui.SwaggerWelcomeCommon;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import
org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -37,6 +44,27 @@ public class SwaggerConfig {
private static final String SECURITY_SCHEME_NAME = "BearerAuth";
+ /**
+ * The springdoc beans this one is built from only exist while both
switches are on:
+ * its own ui configuration is conditional on {@code
SpringDocConfiguration}, which
+ * {@code springdoc.api-docs.enabled} gates in turn. Matching both
switches keeps a
+ * deployment that turns the document off from failing to start.
+ *
+ * @return the swagger ui index transformer, replacing the springdoc
default
+ */
+ @Bean
+ @ConditionalOnWebApplication(type =
ConditionalOnWebApplication.Type.SERVLET)
+ @ConditionalOnProperty(name = {"springdoc.api-docs.enabled",
"springdoc.swagger-ui.enabled"},
+ havingValue = "true", matchIfMissing = true)
+ public SwaggerIndexTransformer authorizedSwaggerIndexTransformer(
+ SwaggerUiConfigProperties swaggerUiConfig,
+ SwaggerUiOAuthProperties swaggerUiOauthProperties,
+ SwaggerWelcomeCommon swaggerWelcomeCommon,
+ ObjectMapperProvider objectMapperProvider) {
+ return new AuthorizedSwaggerIndexTransformer(swaggerUiConfig,
swaggerUiOauthProperties,
+ swaggerWelcomeCommon, objectMapperProvider);
+ }
+
@Bean
public OpenAPI springOpenApi() {
return new OpenAPI()
diff --git
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/AuthorizedSwaggerIndexTransformerTest.java
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/AuthorizedSwaggerIndexTransformerTest.java
new file mode 100644
index 0000000000..985e740788
--- /dev/null
+++
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/AuthorizedSwaggerIndexTransformerTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test case for {@link AuthorizedSwaggerIndexTransformer}.
+ */
+class AuthorizedSwaggerIndexTransformerTest {
+
+ @Test
+ void shouldAttachTheHertzBeatTokenToSameOriginRequests() {
+ final String initializer = """
+ window.ui = SwaggerUIBundle({
+ configUrl: "/v3/api-docs/swagger-config",
+ presets: [SwaggerUIBundle.presets.apis]
+ });
+ """;
+
+ final String transformed =
AuthorizedSwaggerIndexTransformer.addAuthorizationInterceptor(initializer);
+
+
assertTrue(transformed.contains("window.localStorage.getItem('Authorization')"));
+ assertTrue(transformed.contains("request.headers['Authorization'] =
`Bearer ${token}`"));
+ assertTrue(transformed.contains("sameOrigin && token"));
+ assertTrue(transformed.contains("presets:
[SwaggerUIBundle.presets.apis]"));
+ }
+
+ @Test
+ void shouldFailClosedWhenTheSwaggerInitializerShapeChanges() {
+ assertThrows(IllegalStateException.class,
+ () ->
AuthorizedSwaggerIndexTransformer.addAuthorizationInterceptor("window.ui =
{};"));
+ }
+
+ /**
+ * Springdoc writes an interceptor of its own when csrf support is turned
on, and it
+ * can be configured to write to the {@code Authorization} header too. The
last write
+ * wins, so ours has to come after the one already there.
+ */
+ @Test
+ void shouldComposeWithAnExistingRequestInterceptor() {
+ final String initializer = """
+ window.ui = SwaggerUIBundle({
+ requestInterceptor: (request) => {
+ request.headers['Authorization'] = 'csrf';
+ return request;
+ },
+ presets: [SwaggerUIBundle.presets.apis]
+ });
+ """;
+
+ final String transformed =
AuthorizedSwaggerIndexTransformer.addAuthorizationInterceptor(initializer);
+
+ assertTrue(transformed.contains("request.headers['Authorization'] =
'csrf'"));
+ assertTrue(transformed.indexOf("request.headers['Authorization'] =
`Bearer ${token}`")
+ >
transformed.indexOf("request.headers['Authorization'] = 'csrf'"),
+ "the token has to be written after the interceptor that was
already there");
+ assertEquals(1, countOf(transformed, "requestInterceptor:"),
+ "a second key of the same name would drop one of the two
interceptors");
+ }
+
+ @Test
+ void shouldFailClosedWhenTheExistingInterceptorDoesNotReturnTheRequest() {
+ final String initializer = """
+ window.ui = SwaggerUIBundle({
+ requestInterceptor: (request) => {
+ presets: [SwaggerUIBundle.presets.apis]
+ """;
+
+ assertThrows(IllegalStateException.class,
+ () ->
AuthorizedSwaggerIndexTransformer.addAuthorizationInterceptor(initializer));
+ }
+
+ /**
+ * @param initializer the transformed script
+ * @param token the substring to count
+ * @return how many times the substring occurs
+ */
+ private static int countOf(String initializer, String token) {
+ int count = 0;
+ for (int index = initializer.indexOf(token); index >= 0; index =
initializer.indexOf(token, index + 1)) {
+ count++;
+ }
+ return count;
+ }
+}
diff --git a/hertzbeat-manager/src/test/resources/sureness.yml
b/hertzbeat-manager/src/test/resources/sureness.yml
index 1763868f1e..dff4e65966 100644
--- a/hertzbeat-manager/src/test/resources/sureness.yml
+++ b/hertzbeat-manager/src/test/resources/sureness.yml
@@ -16,7 +16,7 @@
## -- sureness.yml account source -- ##
# config the resource restful api that need auth protection, base rbac
-# rule: api===method===role
+# rule: api===method===role
# eg: /api/v1/source1===get===[admin] means /api/v2/host===post support
role[admin] access.
# eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by
any role.
resourceRole:
@@ -86,9 +86,16 @@ resourceRole:
- /api/ingestion/otlp/**===get===[admin,user,guest]
- /api/logs/**===get===[admin,user,guest]
- /api/traces/**===get===[admin,user,guest]
+ # The OpenAPI document is a map of every route, parameter and model, so it is
+ # scoped like any other administrative resource instead of being anonymous
+ - /v3/api-docs/**===get===[admin]
+ - /v3/api-docs.yaml===get===[admin]
+ - /v3/api-docs.yaml/**===get===[admin]
+ - /v2/api-docs/**===get===[admin]
+ - /swagger-resources/**===get===[admin]
# config the resource restful api that need bypass auth protection
-# rule: api===method
+# rule: api===method
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by
anyone, no need auth.
excludedResource:
- /api/alert/sse/**===*
@@ -121,10 +128,6 @@ excludedResource:
- /**/*.json===get
- /**/*.woff===get
- /**/*.eot===get
- # swagger ui resource
- - /swagger-resources/**===get
- - /v2/api-docs===get
- - /v3/api-docs===get
# h2 database
- /h2-console/**===*
diff --git a/hertzbeat-startup/src/main/resources/application.yml
b/hertzbeat-startup/src/main/resources/application.yml
index fa6cda6e10..425dd4e1f6 100644
--- a/hertzbeat-startup/src/main/resources/application.yml
+++ b/hertzbeat-startup/src/main/resources/application.yml
@@ -71,6 +71,16 @@ management:
export:
enabled: true
+# The generated OpenAPI document is a map of every route, HTTP method,
parameter
+# and model, so it is not served by default. A deployment that wants the
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+ api-docs:
+ enabled: false
+ swagger-ui:
+ enabled: false
+
sureness:
container: jakarta_servlet
auths:
diff --git a/hertzbeat-startup/src/main/resources/sureness.yml
b/hertzbeat-startup/src/main/resources/sureness.yml
index f63bee0e8f..8cdaeec049 100644
--- a/hertzbeat-startup/src/main/resources/sureness.yml
+++ b/hertzbeat-startup/src/main/resources/sureness.yml
@@ -16,7 +16,7 @@
## -- sureness.yml account source -- ##
# config the resource restful api that need auth protection, base rbac
-# rule: api===method===role
+# rule: api===method===role
# eg: /api/v1/source1===get===[admin] means /api/v2/host===post support
role[admin] access.
# eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by
any role.
resourceRole:
@@ -116,11 +116,18 @@ resourceRole:
- /api/account/token===get===[admin]
- /api/account/token/**===post===[admin]
- /api/account/token/**===delete===[admin]
+ # The OpenAPI document is a map of every route, parameter and model, so it is
+ # scoped like any other administrative resource instead of being anonymous
+ - /v3/api-docs/**===get===[admin]
+ - /v3/api-docs.yaml===get===[admin]
+ - /v3/api-docs.yaml/**===get===[admin]
+ - /v2/api-docs/**===get===[admin]
+ - /swagger-resources/**===get===[admin]
# spring boot actuator exposes jvm, http and datasource internals for
scraping
- /actuator/**===get===[admin]
# config the resource restful api that need bypass auth protection
-# rule: api===method
+# rule: api===method
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by
anyone, no need auth.
excludedResource:
- /api/alert/sse/**===*
@@ -157,10 +164,6 @@ excludedResource:
- /**/*.json===get
- /**/*.woff===get
- /**/*.eot===get
- # swagger ui resource
- - /swagger-resources/**===get
- - /v2/api-docs===get
- - /v3/api-docs===get
# h2 database
- /h2-console/**===*
diff --git
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/OpenApiDocumentDisabledByDefaultTest.java
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/OpenApiDocumentDisabledByDefaultTest.java
new file mode 100644
index 0000000000..33af10aeff
--- /dev/null
+++
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/OpenApiDocumentDisabledByDefaultTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.yaml.snakeyaml.Yaml;
+
+/**
+ * Guards the springdoc switches that keep the openapi document off by default.
+ *
+ * <p>Scoping the document to the admin role is only half the story. The
document is a map
+ * of every route, http method, parameter and model, so a deployment that has
no use for it
+ * should not serve it at all: both switches are off unless a deployment opts
in, and the
+ * rbac rules stay as the second line of defence for deployments that do. The
swagger ui
+ * page itself is reachable anonymously through the {@code /**}{@code
/*.html===get}
+ * exclusion, but it renders nothing until the caller proves it holds the
admin role.
+ */
+class OpenApiDocumentDisabledByDefaultTest {
+
+ private static final Path SCRIPT_DIR = Path.of("..", "script");
+
+ private static final List<List<String>> SWITCHES = List.of(
+ List.of("springdoc", "api-docs", "enabled"),
+ List.of("springdoc", "swagger-ui", "enabled"));
+
+ @Test
+ void shouldDisableTheOpenApiDocumentInThePackagedConfig() throws
IOException {
+ try (InputStream in =
OpenApiDocumentDisabledByDefaultTest.class.getResourceAsStream("/application.yml"))
{
+ assertNotNull(in, "application.yml must be on the classpath");
+ assertDisabled(documentsOf(in), "application.yml");
+ }
+ }
+
+ /**
+ * The deployment scripts ship their own copies of {@code application.yml}
and mount
+ * them over the packaged one, so a switch flipped only in the packaged
file would
+ * still leave every container deployment serving the document.
+ */
+ @Test
+ void shouldDisableTheOpenApiDocumentInDeploymentCopies() throws
IOException {
+ for (Path copy : deploymentCopies()) {
+ try (InputStream in = Files.newInputStream(copy)) {
+ assertDisabled(documentsOf(in), copy.toString());
+ }
+ }
+ }
+
+ /**
+ * Asserts that every declaration of the springdoc switches across a multi
document
+ * yaml turns the endpoint off, and that at least one declaration exists -
a file that
+ * simply omits them falls back to the springdoc default, which is enabled.
+ *
+ * @param documents the yaml documents, in the order spring applies them
+ * @param source the file the documents came from, for the failure
message
+ */
+ private static void assertDisabled(List<Map<String, Object>> documents,
String source) {
+ for (List<String> path : SWITCHES) {
+ List<Object> declarations = documents.stream()
+ .map(document -> valueAt(document, path))
+ .filter(Objects::nonNull)
+ .toList();
+ assertFalse(declarations.isEmpty(),
+ String.join(".", path) + " is unset in " + source + ", so
it falls back to enabled");
+ declarations.forEach(declared -> assertEquals(Boolean.FALSE,
declared,
+ String.join(".", path) + " is enabled in " + source));
+ }
+ }
+
+ /**
+ * @param document the parsed yaml document
+ * @param path the key path to walk, outermost first
+ * @return the value at that path, or null when any segment is missing
+ */
+ @Nullable
+ private static Object valueAt(Map<String, Object> document, List<String>
path) {
+ Object current = document;
+ for (String key : path) {
+ if (!(current instanceof Map<?, ?> map)) {
+ return null;
+ }
+ current = map.get(key);
+ }
+ return current;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<Map<String, Object>> documentsOf(InputStream in) {
+ List<Map<String, Object>> documents = new ArrayList<>();
+ for (Object document : new Yaml().loadAll(in)) {
+ if (document instanceof Map<?, ?> map) {
+ documents.add((Map<String, Object>) map);
+ }
+ }
+ return documents;
+ }
+
+ /**
+ * @return the {@code application.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("application.yml"))
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ assertFalse(copies.isEmpty(), "expected the deployment scripts to
ship application.yml copies");
+ return copies;
+ }
+ }
+}
diff --git
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessOpenApiDocRuleTest.java
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessOpenApiDocRuleTest.java
new file mode 100644
index 0000000000..76ff76b2e3
--- /dev/null
+++
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessOpenApiDocRuleTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import com.usthe.sureness.matcher.util.TirePathTree;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.yaml.snakeyaml.Yaml;
+
+/**
+ * Guards the rbac rules covering the generated openapi document.
+ *
+ * <p>These paths used to sit in {@code excludedResource}. Sureness checks the
exclusion
+ * tree before any credential check, so an anonymous request returned the full
document:
+ * every route, http method, parameter name and type, and every request and
response
+ * model. That is a ready made map of the attack surface, so it is scoped like
any other
+ * administrative resource.
+ */
+class SurenessOpenApiDocRuleTest {
+
+ private static final String SEPARATOR = "===";
+
+ /**
+ * {@code excludedResource} entries are written as {@code api===method},
but
+ * {@code TirePathTree} only accepts the three segment {@code
api===method===roles}
+ * shape and silently drops anything else. {@code DefaultPathRoleMatcher}
therefore
+ * appends this marker to every excluded rule before it builds the
exclusion tree, and
+ * a test that skips the same step builds an empty tree that matches
nothing - every
+ * {@code assertNull} against it then passes no matter what the yaml says.
+ */
+ private static final String EXCLUDE_ROLE = SEPARATOR + "[exclude]";
+
+ private static final Path SCRIPT_DIR = Path.of("..", "script");
+
+ private static TirePathTree roleTree;
+
+ private static TirePathTree excludeTree;
+
+ @BeforeAll
+ @SuppressWarnings("unchecked")
+ static void loadSurenessConfig() throws IOException {
+ List<String> resourceRole;
+ List<String> excludedResource;
+ try (InputStream in =
SurenessOpenApiDocRuleTest.class.getResourceAsStream("/sureness.yml")) {
+ assertNotNull(in, "sureness.yml must be on the classpath");
+ Map<String, Object> document = new Yaml().load(in);
+ resourceRole = (List<String>) document.get("resourceRole");
+ excludedResource = (List<String>) document.get("excludedResource");
+ }
+ assertNotNull(resourceRole, "resourceRole must be present");
+ assertNotNull(excludedResource, "excludedResource must be present");
+ roleTree = new TirePathTree();
+ roleTree.buildTree(new LinkedHashSet<>(resourceRole));
+ excludeTree = excludeTreeOf(excludedResource);
+ }
+
+ @Test
+ void shouldRestrictTheOpenApiDocumentToAdmin() {
+ assertEquals("[admin]", rolesFor(roleTree, "/v3/api-docs"));
+ assertEquals("[admin]", rolesFor(roleTree, "/v2/api-docs"));
+ assertEquals("[admin]", rolesFor(roleTree,
"/swagger-resources/configuration/ui"));
+ }
+
+ /**
+ * Springdoc also serves the grouped documents and its own config under
the same
+ * prefix; a rule bound to the bare path would leave those anonymous.
+ */
+ @Test
+ void shouldCoverTheGroupedDocumentsToo() {
+ assertEquals("[admin]", rolesFor(roleTree,
"/v3/api-docs/swagger-config"));
+ assertEquals("[admin]", rolesFor(roleTree, "/v3/api-docs/default"));
+ assertEquals("[admin]", rolesFor(roleTree,
"/v3/api-docs.yaml/default"));
+ }
+
+ /**
+ * {@code OpenApiWebMvcResource} maps the document twice, at the
configured path and at
+ * that path with a {@code .yaml} suffix. The suffixed form is a sibling
path segment
+ * rather than a child, so {@code /v3/api-docs/**} does not reach it and
it needs its
+ * own rule. Without one the yaml document carries no role requirement at
all, which
+ * sureness treats as no restriction for any authenticated caller
including guest.
+ */
+ @Test
+ void shouldRestrictTheYamlDocumentToAdmin() {
+ assertEquals("[admin]", rolesFor(roleTree, "/v3/api-docs.yaml"));
+ }
+
+ @Test
+ void shouldStopTreatingTheOpenApiDocumentAsAnonymous() {
+ assertNull(rolesFor(excludeTree, "/v3/api-docs"));
+ assertNull(rolesFor(excludeTree, "/v3/api-docs.yaml"));
+ assertNull(rolesFor(excludeTree, "/v3/api-docs.yaml/default"));
+ assertNull(rolesFor(excludeTree, "/v3/api-docs/swagger-config"));
+ assertNull(rolesFor(excludeTree, "/v2/api-docs"));
+ assertNull(rolesFor(excludeTree,
"/swagger-resources/configuration/ui"));
+ }
+
+ /**
+ * The assertions above are all {@code assertNull}, so they only mean
something while
+ * the exclusion tree is capable of matching at all. This pins a path that
is meant to
+ * stay anonymous and fails if the tree was built in a shape sureness
would have
+ * rejected.
+ */
+ @Test
+ void shouldKeepMatchingIntentionallyExcludedResources() {
+ assertEquals("[exclude]", rolesFor(excludeTree, "/api/i18n/lang"));
+ }
+
+ /**
+ * 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
serving the
+ * document to anonymous callers.
+ */
+ @Test
+ void deploymentCopiesRestrictTheOpenApiDocument() throws IOException {
+ for (Path copy : deploymentCopies()) {
+ TirePathTree copyRoleTree = new TirePathTree();
+ copyRoleTree.buildTree(new LinkedHashSet<>(sectionOf(copy,
"resourceRole")));
+ assertEquals("[admin]", rolesFor(copyRoleTree, "/v3/api-docs"),
+ "the openapi document is unruled or over-granted in " +
copy);
+ assertEquals("[admin]", rolesFor(copyRoleTree,
"/v3/api-docs.yaml"),
+ "the yaml openapi document is unruled or over-granted in "
+ copy);
+ assertEquals("[admin]", rolesFor(copyRoleTree,
"/v3/api-docs.yaml/default"),
+ "the grouped yaml openapi document is unruled or
over-granted in " + copy);
+ assertEquals("[admin]", rolesFor(copyRoleTree,
"/v3/api-docs/swagger-config"),
+ "the springdoc ui config is unruled or over-granted in " +
copy);
+
+ TirePathTree copyExcludeTree = excludeTreeOf(sectionOf(copy,
"excludedResource"));
+ assertNull(rolesFor(copyExcludeTree, "/v3/api-docs"),
+ "the openapi document is still anonymous in " + copy);
+ assertEquals("[exclude]", rolesFor(copyExcludeTree,
"/api/i18n/lang"),
+ "the exclusion tree matches nothing in " + copy + ", the
assertion above proves nothing");
+ }
+ }
+
+ @Nullable
+ private static String rolesFor(TirePathTree tree, String path) {
+ return tree.searchPathFilterRoles(path + SEPARATOR + "get");
+ }
+
+ /**
+ * @param excludedResource the {@code excludedResource} rules, as written
in the yaml
+ * @return the exclusion tree, built the way {@code
DefaultPathRoleMatcher} builds it
+ */
+ private static TirePathTree excludeTreeOf(List<String> excludedResource) {
+ TirePathTree tree = new TirePathTree();
+ tree.buildTree(excludedResource.stream()
+ .map(rule -> rule + EXCLUDE_ROLE)
+ .collect(Collectors.toCollection(LinkedHashSet::new)));
+ return tree;
+ }
+
+ /**
+ * @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/home/docs/start/account-modify.md
b/home/docs/start/account-modify.md
index ea72ca06a1..8fdba1e21d 100644
--- a/home/docs/start/account-modify.md
+++ b/home/docs/start/account-modify.md
@@ -51,6 +51,13 @@ resourceRole:
- /api/status/page/**===post===[admin,user]
- /api/status/page/**===put===[admin,user]
- /api/status/page/**===delete===[admin]
+ # The OpenAPI document is a map of every route, parameter and model, so it is
+ # scoped like any other administrative resource instead of being anonymous
+ - /v3/api-docs/**===get===[admin]
+ - /v3/api-docs.yaml===get===[admin]
+ - /v3/api-docs.yaml/**===get===[admin]
+ - /v2/api-docs/**===get===[admin]
+ - /swagger-resources/**===get===[admin]
# config the resource restful api that need bypass auth protection
# rule: api===method
@@ -82,10 +89,6 @@ excludedResource:
- /**/*.json===get
- /**/*.woff===get
- /**/*.eot===get
- # swagger ui resource
- - /swagger-resources/**===get
- - /v2/api-docs===get
- - /v3/api-docs===get
# h2 database
- /h2-console/**===*
@@ -140,6 +143,32 @@ account:
role: [user]
```
+## OpenAPI Document And Swagger UI
+
+The generated OpenAPI document lists every route, http method, parameter name
and type, and every request and response model. It is a ready made map of the
attack surface, so HertzBeat does not serve it by default:
`springdoc.api-docs.enabled` and `springdoc.swagger-ui.enabled` are both
`false` in the shipped `application.yml`, which makes `/v3/api-docs` and
`/swagger-ui/index.html` return 404.
+
+If you need the document, opt in by updating the `application.yml` file in the
`config` directory:
+
+```yaml
+springdoc:
+ api-docs:
+ enabled: true
+ swagger-ui:
+ enabled: true
+```
+
+Once enabled, the document endpoints are still scoped to the `admin` role by
the `resourceRole` rules above. Sign in to the HertzBeat web application as an
administrator before opening `/swagger-ui/index.html`; the Swagger UI attaches
the stored HertzBeat token to its same-origin document and try-it-out requests,
and the page loads without asking for anything.
+
+Without that session the document is not exposed, but the page does not fail
silently either. `/swagger-ui/index.html` is a static file and still loads; its
request for `/v3/api-docs/swagger-config` is answered with `401` and a
`WWW-Authenticate: Digest` challenge, so the browser asks for a username and
password. Administrator credentials entered there let the document through, and
an account without the `admin` role is answered with `403`.
+
+The document can also be fetched directly with an administrator token:
+
+```shell
+curl -H "Authorization: Bearer $YOUR_ADMIN_TOKEN"
http://localhost:1157/v3/api-docs
+```
+
+> ⚠️ Do not move the OpenAPI paths into `excludedResource`; doing so makes the
complete document anonymous again.
+
## Update Security Secret
> This secret is the key for account security encryption management and needs
> to be updated to your custom key string of the same length.
diff --git
a/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/account-modify.md
b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/account-modify.md
index 9818ff8620..24f82b02e7 100644
---
a/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/account-modify.md
+++
b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/account-modify.md
@@ -52,6 +52,13 @@ resourceRole:
- /api/status/page/**===post===[admin,user]
- /api/status/page/**===put===[admin,user]
- /api/status/page/**===delete===[admin]
+ # OpenAPI 文档包含全部路由、参数与数据模型,等同于一份接口地图,
+ # 因此按普通管理类资源收敛到 admin,不再匿名开放
+ - /v3/api-docs/**===get===[admin]
+ - /v3/api-docs.yaml===get===[admin]
+ - /v3/api-docs.yaml/**===get===[admin]
+ - /v2/api-docs/**===get===[admin]
+ - /swagger-resources/**===get===[admin]
# 需要被过滤保护的资源,不认证鉴权直接访问
# /api/v1/source3===get 表示 /api/v1/source3===get 可以被任何人访问 无需登录认证鉴权
@@ -82,10 +89,6 @@ excludedResource:
- /**/*.json===get
- /**/*.woff===get
- /**/*.eot===get
- # swagger ui resource
- - /swagger-resources/**===get
- - /v2/api-docs===get
- - /v3/api-docs===get
# h2 database
- /h2-console/**===*
@@ -141,6 +144,32 @@ account:
role: [user]
```
+## OpenAPI 文档与 Swagger UI
+
+生成的 OpenAPI 文档会列出全部路由、HTTP 方法、参数名与类型,以及所有请求和响应模型,等同于一份现成的攻击面地图,因此 HertzBeat
默认不对外提供:随包发布的 `application.yml` 中 `springdoc.api-docs.enabled` 与
`springdoc.swagger-ui.enabled` 均为 `false`,此时 `/v3/api-docs` 和
`/swagger-ui/index.html` 返回 404。
+
+如果确实需要该文档,更新 `config` 目录下的 `application.yml` 文件显式开启:
+
+```yaml
+springdoc:
+ api-docs:
+ enabled: true
+ swagger-ui:
+ enabled: true
+```
+
+开启之后,文档接口仍然被上面的 `resourceRole` 规则收敛在 `admin` 角色。请先以管理员身份登录 HertzBeat Web
应用,再打开 `/swagger-ui/index.html`;Swagger UI 会在同源的文档请求与 try-it-out 请求中附带
HertzBeat 保存的令牌,页面不会再要求任何输入。
+
+没有该会话时文档同样不会泄露,但页面并非静默失败:`/swagger-ui/index.html` 是静态文件,仍然可以打开,而它请求
`/v3/api-docs/swagger-config` 会得到 `401` 与 `WWW-Authenticate: Digest`
挑战,浏览器因此弹出用户名密码框。在该弹框中输入管理员账号可以正常加载文档;没有 `admin` 角色的账号则会收到 `403`。
+
+也可以直接携带管理员令牌获取文档:
+
+```shell
+curl -H "Authorization: Bearer $YOUR_ADMIN_TOKEN"
http://localhost:1157/v3/api-docs
+```
+
+> ⚠️ 不要把 OpenAPI 路径放回 `excludedResource`,否则完整接口文档会再次匿名开放。
+
## 更新安全密钥
> 此密钥为账户安全加密管理的密钥,需要更新为相同长度的你自定义密钥串。
diff --git a/script/application.yml b/script/application.yml
index fa6cda6e10..425dd4e1f6 100644
--- a/script/application.yml
+++ b/script/application.yml
@@ -71,6 +71,16 @@ management:
export:
enabled: true
+# The generated OpenAPI document is a map of every route, HTTP method,
parameter
+# and model, so it is not served by default. A deployment that wants the
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+ api-docs:
+ enabled: false
+ swagger-ui:
+ enabled: false
+
sureness:
container: jakarta_servlet
auths:
diff --git a/script/docker-compose/hertzbeat-mysql-iotdb/conf/application.yml
b/script/docker-compose/hertzbeat-mysql-iotdb/conf/application.yml
index 7695271d03..f141461fb4 100644
--- a/script/docker-compose/hertzbeat-mysql-iotdb/conf/application.yml
+++ b/script/docker-compose/hertzbeat-mysql-iotdb/conf/application.yml
@@ -74,6 +74,16 @@ management:
export:
enabled: true
+# The generated OpenAPI document is a map of every route, HTTP method,
parameter
+# and model, so it is not served by default. A deployment that wants the
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+ api-docs:
+ enabled: false
+ swagger-ui:
+ enabled: false
+
sureness:
container: jakarta_servlet
auths:
diff --git a/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
b/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
index 069fba3069..1815529be7 100644
--- a/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
@@ -16,7 +16,7 @@
## -- sureness.yml account source -- ##
# config the resource restful api that need auth protection, base rbac
-# rule: api===method===role
+# rule: api===method===role
# eg: /api/v1/source1===get===[admin] means /api/v2/host===post support
role[admin] access.
# eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by
any role.
resourceRole:
@@ -109,11 +109,18 @@ resourceRole:
- /api/ai/**===delete===[admin]
- /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
+ # The OpenAPI document is a map of every route, parameter and model, so it is
+ # scoped like any other administrative resource instead of being anonymous
+ - /v3/api-docs/**===get===[admin]
+ - /v3/api-docs.yaml===get===[admin]
+ - /v3/api-docs.yaml/**===get===[admin]
+ - /v2/api-docs/**===get===[admin]
+ - /swagger-resources/**===get===[admin]
# spring boot actuator exposes jvm, http and datasource internals for
scraping
- /actuator/**===get===[admin]
# config the resource restful api that need bypass auth protection
-# rule: api===method
+# rule: api===method
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by
anyone, no need auth.
excludedResource:
- /api/alert/sse/**===*
@@ -147,10 +154,6 @@ excludedResource:
- /**/*.json===get
- /**/*.woff===get
- /**/*.eot===get
- # swagger ui resource
- - /swagger-resources/**===get
- - /v2/api-docs===get
- - /v3/api-docs===get
# h2 database
- /h2-console/**===*
diff --git
a/script/docker-compose/hertzbeat-mysql-tdengine/conf/application.yml
b/script/docker-compose/hertzbeat-mysql-tdengine/conf/application.yml
index 90220d0669..f6aa53215a 100644
--- a/script/docker-compose/hertzbeat-mysql-tdengine/conf/application.yml
+++ b/script/docker-compose/hertzbeat-mysql-tdengine/conf/application.yml
@@ -74,6 +74,16 @@ management:
export:
enabled: true
+# The generated OpenAPI document is a map of every route, HTTP method,
parameter
+# and model, so it is not served by default. A deployment that wants the
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+ api-docs:
+ enabled: false
+ swagger-ui:
+ enabled: false
+
sureness:
container: jakarta_servlet
auths:
diff --git a/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
b/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
index 069fba3069..1815529be7 100644
--- a/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
@@ -16,7 +16,7 @@
## -- sureness.yml account source -- ##
# config the resource restful api that need auth protection, base rbac
-# rule: api===method===role
+# rule: api===method===role
# eg: /api/v1/source1===get===[admin] means /api/v2/host===post support
role[admin] access.
# eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by
any role.
resourceRole:
@@ -109,11 +109,18 @@ resourceRole:
- /api/ai/**===delete===[admin]
- /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
+ # The OpenAPI document is a map of every route, parameter and model, so it is
+ # scoped like any other administrative resource instead of being anonymous
+ - /v3/api-docs/**===get===[admin]
+ - /v3/api-docs.yaml===get===[admin]
+ - /v3/api-docs.yaml/**===get===[admin]
+ - /v2/api-docs/**===get===[admin]
+ - /swagger-resources/**===get===[admin]
# spring boot actuator exposes jvm, http and datasource internals for
scraping
- /actuator/**===get===[admin]
# config the resource restful api that need bypass auth protection
-# rule: api===method
+# rule: api===method
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by
anyone, no need auth.
excludedResource:
- /api/alert/sse/**===*
@@ -147,10 +154,6 @@ excludedResource:
- /**/*.json===get
- /**/*.woff===get
- /**/*.eot===get
- # swagger ui resource
- - /swagger-resources/**===get
- - /v2/api-docs===get
- - /v3/api-docs===get
# h2 database
- /h2-console/**===*
diff --git
a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/application.yml
b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/application.yml
index 0074a9dd3f..413bdb3718 100644
---
a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/application.yml
+++
b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/application.yml
@@ -74,6 +74,16 @@ management:
export:
enabled: true
+# The generated OpenAPI document is a map of every route, HTTP method,
parameter
+# and model, so it is not served by default. A deployment that wants the
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+ api-docs:
+ enabled: false
+ swagger-ui:
+ enabled: false
+
sureness:
container: jakarta_servlet
auths:
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 069fba3069..1815529be7 100644
--- a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
@@ -16,7 +16,7 @@
## -- sureness.yml account source -- ##
# config the resource restful api that need auth protection, base rbac
-# rule: api===method===role
+# rule: api===method===role
# eg: /api/v1/source1===get===[admin] means /api/v2/host===post support
role[admin] access.
# eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by
any role.
resourceRole:
@@ -109,11 +109,18 @@ resourceRole:
- /api/ai/**===delete===[admin]
- /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
+ # The OpenAPI document is a map of every route, parameter and model, so it is
+ # scoped like any other administrative resource instead of being anonymous
+ - /v3/api-docs/**===get===[admin]
+ - /v3/api-docs.yaml===get===[admin]
+ - /v3/api-docs.yaml/**===get===[admin]
+ - /v2/api-docs/**===get===[admin]
+ - /swagger-resources/**===get===[admin]
# spring boot actuator exposes jvm, http and datasource internals for
scraping
- /actuator/**===get===[admin]
# config the resource restful api that need bypass auth protection
-# rule: api===method
+# rule: api===method
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by
anyone, no need auth.
excludedResource:
- /api/alert/sse/**===*
@@ -147,10 +154,6 @@ excludedResource:
- /**/*.json===get
- /**/*.woff===get
- /**/*.eot===get
- # swagger ui resource
- - /swagger-resources/**===get
- - /v2/api-docs===get
- - /v3/api-docs===get
# h2 database
- /h2-console/**===*
diff --git
a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/application.yml
b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/application.yml
index c25b533a48..2f59761b65 100644
--- a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/application.yml
+++ b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/application.yml
@@ -74,6 +74,16 @@ management:
export:
enabled: true
+# The generated OpenAPI document is a map of every route, HTTP method,
parameter
+# and model, so it is not served by default. A deployment that wants the
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+ api-docs:
+ enabled: false
+ swagger-ui:
+ enabled: false
+
sureness:
container: jakarta_servlet
auths:
diff --git
a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
index d15a6ac5d2..6281d0927d 100644
--- a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
@@ -16,7 +16,7 @@
## -- sureness.yml account source -- ##
# config the resource restful api that need auth protection, base rbac
-# rule: api===method===role
+# rule: api===method===role
# eg: /api/v1/source1===get===[admin] means /api/v2/host===post support
role[admin] access.
# eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by
any role.
resourceRole:
@@ -113,11 +113,18 @@ resourceRole:
- /api/ingestion/otlp/**===get===[admin,user,guest]
- /api/logs/**===get===[admin,user,guest]
- /api/traces/**===get===[admin,user,guest]
+ # The OpenAPI document is a map of every route, parameter and model, so it is
+ # scoped like any other administrative resource instead of being anonymous
+ - /v3/api-docs/**===get===[admin]
+ - /v3/api-docs.yaml===get===[admin]
+ - /v3/api-docs.yaml/**===get===[admin]
+ - /v2/api-docs/**===get===[admin]
+ - /swagger-resources/**===get===[admin]
# spring boot actuator exposes jvm, http and datasource internals for
scraping
- /actuator/**===get===[admin]
# config the resource restful api that need bypass auth protection
-# rule: api===method
+# rule: api===method
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by
anyone, no need auth.
excludedResource:
- /api/alert/sse/**===*
@@ -154,10 +161,6 @@ excludedResource:
- /**/*.json===get
- /**/*.woff===get
- /**/*.eot===get
- # swagger ui resource
- - /swagger-resources/**===get
- - /v2/api-docs===get
- - /v3/api-docs===get
# h2 database
- /h2-console/**===*
diff --git
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/application.yml
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/application.yml
index 71d7fb2f58..9189445de0 100644
---
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/application.yml
+++
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/application.yml
@@ -74,6 +74,16 @@ management:
export:
enabled: true
+# The generated OpenAPI document is a map of every route, HTTP method,
parameter
+# and model, so it is not served by default. A deployment that wants the
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+ api-docs:
+ enabled: false
+ swagger-ui:
+ enabled: false
+
sureness:
container: jakarta_servlet
auths:
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 069fba3069..1815529be7 100644
---
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
+++
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
@@ -16,7 +16,7 @@
## -- sureness.yml account source -- ##
# config the resource restful api that need auth protection, base rbac
-# rule: api===method===role
+# rule: api===method===role
# eg: /api/v1/source1===get===[admin] means /api/v2/host===post support
role[admin] access.
# eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by
any role.
resourceRole:
@@ -109,11 +109,18 @@ resourceRole:
- /api/ai/**===delete===[admin]
- /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
+ # The OpenAPI document is a map of every route, parameter and model, so it is
+ # scoped like any other administrative resource instead of being anonymous
+ - /v3/api-docs/**===get===[admin]
+ - /v3/api-docs.yaml===get===[admin]
+ - /v3/api-docs.yaml/**===get===[admin]
+ - /v2/api-docs/**===get===[admin]
+ - /swagger-resources/**===get===[admin]
# spring boot actuator exposes jvm, http and datasource internals for
scraping
- /actuator/**===get===[admin]
# config the resource restful api that need bypass auth protection
-# rule: api===method
+# rule: api===method
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by
anyone, no need auth.
excludedResource:
- /api/alert/sse/**===*
@@ -147,10 +154,6 @@ excludedResource:
- /**/*.json===get
- /**/*.woff===get
- /**/*.eot===get
- # swagger ui resource
- - /swagger-resources/**===get
- - /v2/api-docs===get
- - /v3/api-docs===get
# h2 database
- /h2-console/**===*
diff --git a/script/sureness.yml b/script/sureness.yml
index d15a6ac5d2..6281d0927d 100644
--- a/script/sureness.yml
+++ b/script/sureness.yml
@@ -16,7 +16,7 @@
## -- sureness.yml account source -- ##
# config the resource restful api that need auth protection, base rbac
-# rule: api===method===role
+# rule: api===method===role
# eg: /api/v1/source1===get===[admin] means /api/v2/host===post support
role[admin] access.
# eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by
any role.
resourceRole:
@@ -113,11 +113,18 @@ resourceRole:
- /api/ingestion/otlp/**===get===[admin,user,guest]
- /api/logs/**===get===[admin,user,guest]
- /api/traces/**===get===[admin,user,guest]
+ # The OpenAPI document is a map of every route, parameter and model, so it is
+ # scoped like any other administrative resource instead of being anonymous
+ - /v3/api-docs/**===get===[admin]
+ - /v3/api-docs.yaml===get===[admin]
+ - /v3/api-docs.yaml/**===get===[admin]
+ - /v2/api-docs/**===get===[admin]
+ - /swagger-resources/**===get===[admin]
# spring boot actuator exposes jvm, http and datasource internals for
scraping
- /actuator/**===get===[admin]
# config the resource restful api that need bypass auth protection
-# rule: api===method
+# rule: api===method
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by
anyone, no need auth.
excludedResource:
- /api/alert/sse/**===*
@@ -154,10 +161,6 @@ excludedResource:
- /**/*.json===get
- /**/*.woff===get
- /**/*.eot===get
- # swagger ui resource
- - /swagger-resources/**===get
- - /v2/api-docs===get
- - /v3/api-docs===get
# h2 database
- /h2-console/**===*
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]