You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@activemq.apache.org by jb...@apache.org on 2026/10/16 11:47:20 UTC
(activemq) branch main updated: Tighten default Jolokia access policy and bind the web console to loopback (#2639)
This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/activemq.git
The following commit(s) were added to refs/heads/main by this push:
new 58c1f024e2 Tighten default Jolokia access policy and bind the web console to loopback (#2639)
58c1f024e2 is described below
commit 58c1f024e2ab1f6b884f07981f42b33f468e46ac
Author: JB Onofré <jb...@apache.org>
AuthorDate: Fri Oct 16 12:47:14 2026 +0100
Tighten default Jolokia access policy and bind the web console to loopback (#2639)
- jolokia-access.xml: list the allowed read-only operations explicitly
instead of a wildcard, list the allowed origins for strict checking, and
let denied operation names match a name sent with its signature
- jetty-spring.properties: bind the http/https connectors to 127.0.0.1
- JettyServerBean: -Djetty.host overrides the connector bind address
- add tests evaluating the shipped Jolokia policy and Jetty configuration
---
.../activemq/spring/jetty/JettyServerBeanTest.java | 20 ++
.../test/resources/conf/jetty-spring.properties | 2 +
.../activemq/spring/jetty/JettyServerBean.java | 11 +
assembly/src/release/conf/jetty-spring.properties | 6 +
assembly/src/release/conf/jolokia-access.xml | 255 +++++++++++++++------
.../activemq/config/JettyBindAddressTest.java | 65 ++++++
.../activemq/config/JolokiaAccessPolicyTest.java | 211 +++++++++++++++++
7 files changed, 502 insertions(+), 68 deletions(-)
diff --git a/activemq-web-console/src/test/java/org/apache/activemq/spring/jetty/JettyServerBeanTest.java b/activemq-web-console/src/test/java/org/apache/activemq/spring/jetty/JettyServerBeanTest.java
index 6efe6709ae..ad5855bff1 100644
--- a/activemq-web-console/src/test/java/org/apache/activemq/spring/jetty/JettyServerBeanTest.java
+++ b/activemq-web-console/src/test/java/org/apache/activemq/spring/jetty/JettyServerBeanTest.java
@@ -55,6 +55,26 @@ public class JettyServerBeanTest {
HttpResponse.BodyHandlers.ofString());
assertEquals("Web console must require authentication", 401, response.statusCode());
+ // jetty.http.host from jetty-spring.properties: without it Jetty binds every interface
+ assertEquals("127.0.0.1", ((ServerConnector) server.getConnectors()[0]).getHost());
+
context.stop();
}
+
+ @Test
+ public void testJettyHostSystemPropertyOverridesBindAddress() throws Exception {
+ System.setProperty(JettyServerBean.SYSTEM_PROPERTY_HOST, "localhost");
+ try {
+ var context = new ClassPathXmlApplicationContext("conf/jetty-spring.xml");
+ context.start();
+ try {
+ var jettyServer = (JettyServerBean) context.getBean("jettyServer");
+ assertEquals("localhost", ((ServerConnector) jettyServer.getServer().getConnectors()[0]).getHost());
+ } finally {
+ context.stop();
+ }
+ } finally {
+ System.clearProperty(JettyServerBean.SYSTEM_PROPERTY_HOST);
+ }
+ }
}
diff --git a/activemq-web-console/src/test/resources/conf/jetty-spring.properties b/activemq-web-console/src/test/resources/conf/jetty-spring.properties
index c1b0b15b0c..4c0d2f9ebc 100644
--- a/activemq-web-console/src/test/resources/conf/jetty-spring.properties
+++ b/activemq-web-console/src/test/resources/conf/jetty-spring.properties
@@ -22,6 +22,8 @@ jettyXmlFiles=jetty-bytebufferpool.xml,jetty-threadpool.xml,jetty-scheduler.xml,
jettyHttpXmlFiles=jetty-http.xml
httpEnabled=true
httpsEnabled=false
+# Loopback only, as in the shipped jetty-spring.properties.
+jetty.http.host=127.0.0.1
# Ephemeral port for tests to avoid collisions.
jetty.http.port=0
jetty.dos.leakingBucketTracker.maxRequestsPerSecond=100
diff --git a/activemq-web/src/main/java/org/apache/activemq/spring/jetty/JettyServerBean.java b/activemq-web/src/main/java/org/apache/activemq/spring/jetty/JettyServerBean.java
index 586aee538a..c0d689894a 100644
--- a/activemq-web/src/main/java/org/apache/activemq/spring/jetty/JettyServerBean.java
+++ b/activemq-web/src/main/java/org/apache/activemq/spring/jetty/JettyServerBean.java
@@ -50,6 +50,9 @@ public class JettyServerBean implements InitializingBean, DisposableBean {
public static final String PROPERTY_HTTP_ENABLED = "httpEnabled";
public static final String PROPERTY_HTTPS_ENABLED = "httpsEnabled";
public static final String PROPERTY_EXTRA_XML_FILES = "jettyExtraXmlFiles";
+ public static final String PROPERTY_HTTP_HOST = "jetty.http.host";
+ public static final String PROPERTY_HTTPS_HOST = "jetty.ssl.host";
+ public static final String SYSTEM_PROPERTY_HOST = "jetty.host";
public static final int PROPERTY_XML_FILES_LIMIT = 128;
public static final String PROPERTY_XML_FILES_SEPARATOR = ",";
@@ -161,6 +164,14 @@ public class JettyServerBean implements InitializingBean, DisposableBean {
jettyProperties.putIfAbsent("jetty.home", jettyProperties.get("activemq.home"));
jettyProperties.putIfAbsent("jetty.base", jettyProperties.get("activemq.base"));
+ // -Djetty.host overrides the bind address of the http and https connectors set in
+ // jetty-spring.properties (jetty.http.host / jetty.ssl.host), e.g. for the docker image.
+ var jettyHost = System.getProperty(SYSTEM_PROPERTY_HOST);
+ if (jettyHost != null && !jettyHost.isBlank()) {
+ jettyProperties.put(PROPERTY_HTTP_HOST, jettyHost);
+ jettyProperties.put(PROPERTY_HTTPS_HOST, jettyHost);
+ }
+
// Now lets tie it all together
idMap = configure(xmls, jettyProperties);
}
diff --git a/assembly/src/release/conf/jetty-spring.properties b/assembly/src/release/conf/jetty-spring.properties
index 23ab44e491..26962252b5 100644
--- a/assembly/src/release/conf/jetty-spring.properties
+++ b/assembly/src/release/conf/jetty-spring.properties
@@ -32,6 +32,12 @@ jettyHttpXmlFiles=jetty-http.xml
jettyHttpsXmlFiles=jetty-ssl.xml,jetty-ssl-context.xml,jetty-https.xml,jetty-secure-redirect.xml
httpEnabled=true
httpsEnabled=false
+# Bind address of the http / https connectors. Loopback only by default: without a
+# value Jetty binds every interface. To expose the console set an interface address
+# (or 0.0.0.0) here and widen the client allow-list in jetty/jetty-security.xml and
+# the <allow-origin> list in jolokia-access.xml. -Djetty.host=<address> overrides both.
+jetty.http.host=127.0.0.1
+jetty.ssl.host=127.0.0.1
jetty.http.port=8161
jetty.ssl.port=8443
jetty.httpConfig.securePort=8443
diff --git a/assembly/src/release/conf/jolokia-access.xml b/assembly/src/release/conf/jolokia-access.xml
index 6844c31460..03469fe40d 100644
--- a/assembly/src/release/conf/jolokia-access.xml
+++ b/assembly/src/release/conf/jolokia-access.xml
@@ -22,10 +22,22 @@
<method>post</method>
</http>
- <!-- Enforce that an Origin/Referer header is present to prevent CSRF.
- Add explicit <allow-origin> entries below if the console is served from
- a different host. -->
+ <!-- CSRF protection: with <strict-checking/> every request must carry an
+ Origin (or Referer) header matching one of the <allow-origin> entries.
+ The list must not be empty: without any entry Jolokia accepts every
+ origin and only rejects requests that have no such header at all.
+ The entries below are the console's own loopback addresses on the default
+ http / https ports. Add the exact scheme://host:port the console is
+ reached through when the bind address, port or host name is changed, and
+ avoid a leading wildcard. Non-browser clients must send one of these
+ values as their Origin header. -->
<cors>
+ <allow-origin>http://localhost:8161</allow-origin>
+ <allow-origin>http://127.0.0.1:8161</allow-origin>
+ <allow-origin>http://[::1]:8161</allow-origin>
+ <allow-origin>https://localhost:8443</allow-origin>
+ <allow-origin>https://127.0.0.1:8443</allow-origin>
+ <allow-origin>https://[::1]:8443</allow-origin>
<strict-checking/>
</cors>
@@ -37,13 +49,117 @@
<command>search</command>
</commands>
+ <!--
+ How Jolokia combines <commands>, <allow> and <deny>: the two lists are
+ never applied to the same request.
+ - A command type listed in <commands> (read) is permitted everywhere
+ except where <deny> matches. <allow> is not consulted.
+ - A command type NOT listed in <commands> (write, exec) is refused
+ everywhere except where <allow> matches. <deny> is not consulted.
+ <allow> is therefore the only thing limiting write and exec, so each
+ entry must name the exact operations it opens. A wildcard such as
+ <operation>*</operation> or <attribute>*</attribute> on the broker
+ MBeans re-enables every operation / attribute write, including the ones
+ listed in <deny> below.
+
+ Operation names are matched as plain strings against what the client
+ sent, which may carry a signature, e.g. "browse(java.lang.String)". Both
+ spellings are listed for each operation.
+
+ To open an additional management operation, add it to <allow>. Do not add
+ exec or write to <commands>: that permits everything <deny> does not name,
+ on every MBean in the JVM.
+ -->
<allow>
- <!-- Allow all operations for the broker itself -->
+ <!-- Broker: read-only queries -->
<mbean>
- <name>org.apache.activemq:*</name>
- <attribute>*</attribute>
- <operation>*</operation>
+ <name>org.apache.activemq:type=Broker,brokerName=*</name>
+ <operation>queryQueues</operation>
+ <operation>queryQueues(java.lang.String,int,int)</operation>
+ <operation>queryTopics</operation>
+ <operation>queryTopics(java.lang.String,int,int)</operation>
+ <operation>browseQueue</operation>
+ <operation>browseQueue(java.lang.String)</operation>
+ <operation>getTransportConnectorByType</operation>
+ <operation>getTransportConnectorByType(java.lang.String)</operation>
+ </mbean>
+
+ <!-- Destinations: browsing and cursor state -->
+ <mbean>
+ <name>org.apache.activemq:type=Broker,brokerName=*,destinationType=*,destinationName=*</name>
+ <operation>browse</operation>
+ <operation>browse()</operation>
+ <operation>browse(java.lang.String)</operation>
+ <operation>browseAsTable</operation>
+ <operation>browseAsTable()</operation>
+ <operation>browseAsTable(java.lang.String)</operation>
+ <operation>getMessage</operation>
+ <operation>getMessage(java.lang.String)</operation>
+ <operation>cursorSize</operation>
+ <operation>cursorSize()</operation>
+ <operation>doesCursorHaveMessagesBuffered</operation>
+ <operation>doesCursorHaveMessagesBuffered()</operation>
+ <operation>doesCursorHaveSpace</operation>
+ <operation>doesCursorHaveSpace()</operation>
+ </mbean>
+
+ <!-- Subscriptions: browsing, cursor state and selector matching -->
+ <mbean>
+ <name>org.apache.activemq:type=Broker,brokerName=*,destinationType=*,destinationName=*,endpoint=Consumer,clientId=*,consumerId=*</name>
+ <operation>browse</operation>
+ <operation>browse()</operation>
+ <operation>browseAsTable</operation>
+ <operation>browseAsTable()</operation>
+ <operation>cursorSize</operation>
+ <operation>cursorSize()</operation>
+ <operation>doesCursorHaveMessagesBuffered</operation>
+ <operation>doesCursorHaveMessagesBuffered()</operation>
+ <operation>doesCursorHaveSpace</operation>
+ <operation>doesCursorHaveSpace()</operation>
+ <operation>isMatchingQueue</operation>
+ <operation>isMatchingQueue(java.lang.String)</operation>
+ <operation>isMatchingTopic</operation>
+ <operation>isMatchingTopic(java.lang.String)</operation>
+ </mbean>
+
+ <!-- Health checks -->
+ <mbean>
+ <name>org.apache.activemq:type=Broker,brokerName=*,service=Health</name>
+ <operation>health</operation>
+ <operation>health()</operation>
+ <operation>healthList</operation>
+ <operation>healthList()</operation>
+ <operation>healthStatus</operation>
+ <operation>healthStatus()</operation>
+ </mbean>
+
+ <!-- JobScheduler: listing scheduled jobs -->
+ <mbean>
+ <name>org.apache.activemq:type=Broker,brokerName=*,service=JobScheduler,name=*</name>
+ <operation>getAllJobs</operation>
+ <operation>getAllJobs(boolean)</operation>
+ <operation>getAllJobs(java.lang.String,java.lang.String)</operation>
+ <operation>getAllJobs(java.lang.String,java.lang.String,boolean)</operation>
+ <operation>getNextScheduleJobs</operation>
+ <operation>getNextScheduleJobs(boolean)</operation>
+ <operation>getExecutionCount</operation>
+ <operation>getExecutionCount(java.lang.String)</operation>
+ </mbean>
+
+ <!-- Log4JConfiguration: reading a logger level -->
+ <mbean>
+ <name>org.apache.activemq:type=Broker,brokerName=*,service=Log4JConfiguration</name>
+ <operation>getLogLevel</operation>
+ <operation>getLogLevel(java.lang.String)</operation>
</mbean>
+
+ <!-- Transport connectors: connection count -->
+ <mbean>
+ <name>org.apache.activemq:type=Broker,brokerName=*,connector=clientConnectors,connectorName=*</name>
+ <operation>connectionCount</operation>
+ <operation>connectionCount()</operation>
+ </mbean>
+
<!-- Allow all operations for Jolokia Config -->
<mbean>
<name>jolokia:type=Config</name>
@@ -51,7 +167,10 @@
</mbean>
</allow>
- <!-- deny all operations or getting attributes from these mbeans -->
+ <!-- deny all operations or getting attributes from these mbeans.
+ Attribute entries apply to reads. Operation entries only take effect if
+ exec is added to <commands>; they end with a wildcard so that they also
+ match a name sent with its signature. -->
<deny>
<!-- ActiveMQ broker: deny destructive / privileged operations.
Safe read-only getters, browse*, and statistics queries remain allowed
@@ -59,74 +178,74 @@
<mbean>
<name>org.apache.activemq:type=Broker,brokerName=*</name>
<!-- JVM and broker lifecycle -->
- <operation>terminateJVM</operation>
- <operation>stop</operation>
- <operation>stopGracefully</operation>
- <operation>restart</operation>
- <operation>gc</operation>
+ <operation>terminateJVM*</operation>
+ <operation>stop*</operation>
+ <operation>stopGracefully*</operation>
+ <operation>restart*</operation>
+ <operation>gc*</operation>
<!-- Topology and destination management -->
- <operation>addConnector</operation>
- <operation>removeConnector</operation>
- <operation>addNetworkConnector</operation>
- <operation>removeNetworkConnector</operation>
- <operation>addQueue</operation>
- <operation>removeQueue</operation>
- <operation>addTopic</operation>
- <operation>removeTopic</operation>
- <operation>createDurableSubscriber</operation>
- <operation>destroyDurableSubscriber</operation>
+ <operation>addConnector*</operation>
+ <operation>removeConnector*</operation>
+ <operation>addNetworkConnector*</operation>
+ <operation>removeNetworkConnector*</operation>
+ <operation>addQueue*</operation>
+ <operation>removeQueue*</operation>
+ <operation>addTopic*</operation>
+ <operation>removeTopic*</operation>
+ <operation>createDurableSubscriber*</operation>
+ <operation>destroyDurableSubscriber*</operation>
<!-- Runtime configuration mutation -->
- <operation>reloadLog4jProperties</operation>
- <operation>setMemoryLimit</operation>
- <operation>setStoreLimit</operation>
- <operation>setTempLimit</operation>
- <operation>setJobSchedulerStoreLimit</operation>
- <operation>setMaxUncommittedCount</operation>
+ <operation>reloadLog4jProperties*</operation>
+ <operation>setMemoryLimit*</operation>
+ <operation>setStoreLimit*</operation>
+ <operation>setTempLimit*</operation>
+ <operation>setJobSchedulerStoreLimit*</operation>
+ <operation>setMaxUncommittedCount*</operation>
</mbean>
<!-- Destinations: deny message data manipulation and injection.
browse / browseAsTable / counters remain allowed. -->
<mbean>
<name>org.apache.activemq:type=Broker,brokerName=*,destinationType=*,destinationName=*</name>
- <operation>purge</operation>
- <operation>removeMessage</operation>
- <operation>removeMatchingMessages</operation>
- <operation>copyMessageTo</operation>
- <operation>copyMatchingMessagesTo</operation>
- <operation>moveMessageTo</operation>
- <operation>moveMatchingMessagesTo</operation>
- <operation>retryMessage</operation>
- <operation>retryMessages</operation>
- <operation>removeMessageGroup</operation>
- <operation>removeAllMessageGroups</operation>
- <operation>sendTextMessage</operation>
- <operation>sendTextMessageWithProperties</operation>
- <operation>pause</operation>
- <operation>resume</operation>
+ <operation>purge*</operation>
+ <operation>removeMessage*</operation>
+ <operation>removeMatchingMessages*</operation>
+ <operation>copyMessageTo*</operation>
+ <operation>copyMatchingMessagesTo*</operation>
+ <operation>moveMessageTo*</operation>
+ <operation>moveMatchingMessagesTo*</operation>
+ <operation>retryMessage*</operation>
+ <operation>retryMessages*</operation>
+ <operation>removeMessageGroup*</operation>
+ <operation>removeAllMessageGroups*</operation>
+ <operation>sendTextMessage*</operation>
+ <operation>sendTextMessageWithProperties*</operation>
+ <operation>pause*</operation>
+ <operation>resume*</operation>
</mbean>
<!-- Durable subscriptions: deny destroy and selector tampering -->
<mbean>
<name>org.apache.activemq:type=Broker,brokerName=*,destinationType=*,destinationName=*,endpoint=Consumer,clientId=*,consumerId=*</name>
- <operation>destroy</operation>
- <operation>removeMessage</operation>
- <operation>setSelector</operation>
+ <operation>destroy*</operation>
+ <operation>removeMessage*</operation>
+ <operation>setSelector*</operation>
</mbean>
<!-- JobScheduler: deny job removal -->
<mbean>
<name>org.apache.activemq:type=Broker,brokerName=*,service=JobScheduler,name=*</name>
- <operation>removeJob</operation>
- <operation>removeAllJobs</operation>
- <operation>removeAllJobsAtScheduledTime</operation>
+ <operation>removeJob*</operation>
+ <operation>removeAllJobs*</operation>
+ <operation>removeAllJobsAtScheduledTime*</operation>
</mbean>
<!-- Log4JConfiguration MBean: deny runtime log level / config changes -->
<mbean>
<name>org.apache.activemq:type=Broker,brokerName=*,service=Log4JConfiguration</name>
- <operation>setRootLogLevel</operation>
- <operation>setLogLevel</operation>
- <operation>reloadLog4jProperties</operation>
+ <operation>setRootLogLevel*</operation>
+ <operation>setLogLevel*</operation>
+ <operation>reloadLog4jProperties*</operation>
</mbean>
<!-- NetworkConnector: deny credential exposure and config mutation -->
@@ -134,21 +253,21 @@
<name>org.apache.activemq:type=Broker,brokerName=*,connector=networkConnectors,networkConnectorName=*</name>
<attribute>Password</attribute>
<attribute>RemotePassword</attribute>
- <operation>setUserName</operation>
- <operation>setPassword</operation>
- <operation>setRemoteUserName</operation>
- <operation>setRemotePassword</operation>
- <operation>setBridgeTempDestinations</operation>
- <operation>setConduitSubscriptions</operation>
- <operation>setDispatchAsync</operation>
- <operation>setDynamicOnly</operation>
- <operation>setMessageTTL</operation>
- <operation>setConsumerTTL</operation>
- <operation>setPrefetchSize</operation>
- <operation>setAdvisoryPrefetchSize</operation>
- <operation>setDecreaseNetworkConsumerPriority</operation>
- <operation>setSuppressDuplicateQueueSubscriptions</operation>
- <operation>setSuppressDuplicateTopicSubscriptions</operation>
+ <operation>setUserName*</operation>
+ <operation>setPassword*</operation>
+ <operation>setRemoteUserName*</operation>
+ <operation>setRemotePassword*</operation>
+ <operation>setBridgeTempDestinations*</operation>
+ <operation>setConduitSubscriptions*</operation>
+ <operation>setDispatchAsync*</operation>
+ <operation>setDynamicOnly*</operation>
+ <operation>setMessageTTL*</operation>
+ <operation>setConsumerTTL*</operation>
+ <operation>setPrefetchSize*</operation>
+ <operation>setAdvisoryPrefetchSize*</operation>
+ <operation>setDecreaseNetworkConsumerPriority*</operation>
+ <operation>setSuppressDuplicateQueueSubscriptions*</operation>
+ <operation>setSuppressDuplicateTopicSubscriptions*</operation>
</mbean>
<!-- MLet allows remote class loading: well-known JMX/Jolokia RCE vector -->
diff --git a/assembly/src/test/java/org/apache/activemq/config/JettyBindAddressTest.java b/assembly/src/test/java/org/apache/activemq/config/JettyBindAddressTest.java
new file mode 100644
index 0000000000..97619cc132
--- /dev/null
+++ b/assembly/src/test/java/org/apache/activemq/config/JettyBindAddressTest.java
@@ -0,0 +1,65 @@
+/**
+ * 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.activemq.config;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.activemq.spring.jetty.JettyServerBean;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.junit.Test;
+
+/**
+ * Applies the shipped Jetty XML files with the shipped jetty-spring.properties, without
+ * starting the server. A connector without a host binds every interface, so the web
+ * console relies on these properties to stay on loopback.
+ */
+public class JettyBindAddressTest {
+
+ private static final Path CONF = Path.of("src/release/conf");
+
+ @Test
+ public void consoleConnectorsBindToLoopbackByDefault() throws Exception {
+ Map<String, String> properties = new HashMap<>();
+ Properties shipped = new Properties();
+ try (InputStream in = new FileInputStream(CONF.resolve("jetty-spring.properties").toFile())) {
+ shipped.load(in);
+ }
+ shipped.forEach((key, value) -> properties.put(String.valueOf(key), String.valueOf(value)));
+
+ List<Resource> xmls = new ArrayList<>();
+ for (String xml : new String[] {"jetty-bytebufferpool.xml", "jetty-threadpool.xml", "jetty-scheduler.xml",
+ "jetty-http-config.xml", "jetty.xml", "jetty-http.xml", "jetty-ssl.xml"}) {
+ xmls.add(ResourceFactory.root().newResource(CONF.resolve("jetty").resolve(xml)));
+ }
+
+ Map<String, Object> ids = new JettyServerBean().configure(xmls, properties);
+
+ assertEquals("127.0.0.1", ((ServerConnector) ids.get("httpConnector")).getHost());
+ assertEquals("127.0.0.1", ((ServerConnector) ids.get("sslConnector")).getHost());
+ }
+}
diff --git a/assembly/src/test/java/org/apache/activemq/config/JolokiaAccessPolicyTest.java b/assembly/src/test/java/org/apache/activemq/config/JolokiaAccessPolicyTest.java
new file mode 100644
index 0000000000..2876e9fe4e
--- /dev/null
+++ b/assembly/src/test/java/org/apache/activemq/config/JolokiaAccessPolicyTest.java
@@ -0,0 +1,211 @@
+/**
+ * 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.activemq.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.management.ObjectName;
+
+import org.apache.activemq.broker.jmx.AbortSlowConsumerStrategyViewMBean;
+import org.apache.activemq.broker.jmx.BrokerViewMBean;
+import org.apache.activemq.broker.jmx.ConnectionViewMBean;
+import org.apache.activemq.broker.jmx.ConnectorViewMBean;
+import org.apache.activemq.broker.jmx.DurableSubscriptionViewMBean;
+import org.apache.activemq.broker.jmx.HealthViewMBean;
+import org.apache.activemq.broker.jmx.JobSchedulerViewMBean;
+import org.apache.activemq.broker.jmx.Log4JConfigViewMBean;
+import org.apache.activemq.broker.jmx.NetworkBridgeViewMBean;
+import org.apache.activemq.broker.jmx.NetworkConnectorViewMBean;
+import org.apache.activemq.broker.jmx.PersistenceAdapterViewMBean;
+import org.apache.activemq.broker.jmx.ProducerViewMBean;
+import org.apache.activemq.broker.jmx.QueueViewMBean;
+import org.apache.activemq.broker.jmx.RecoveredXATransactionViewMBean;
+import org.apache.activemq.broker.jmx.TopicSubscriptionViewMBean;
+import org.apache.activemq.broker.jmx.TopicViewMBean;
+import org.apache.activemq.broker.jmx.VirtualDestinationSelectorCacheViewMBean;
+import org.apache.activemq.plugin.jmx.RuntimeConfigurationViewMBean;
+import org.apache.activemq.security.SecurityAdminMBean;
+import org.apache.activemq.transport.TransportLoggerControlMBean;
+import org.apache.activemq.transport.TransportLoggerViewMBean;
+import org.jolokia.server.core.restrictor.policy.PolicyRestrictor;
+import org.jolokia.server.core.util.HttpMethod;
+import org.jolokia.server.core.util.RequestType;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Evaluates the shipped jolokia-access.xml with Jolokia's own policy restrictor.
+ * Jolokia consults <allow> only for command types missing from <commands> and
+ * <deny> only for the ones present, so reading the file is not enough to tell
+ * what it permits.
+ */
+public class JolokiaAccessPolicyTest {
+
+ private static final String BROKER = "org.apache.activemq:type=Broker,brokerName=localhost";
+ private static final String QUEUE = BROKER + ",destinationType=Queue,destinationName=TEST";
+ private static final String TOPIC = BROKER + ",destinationType=Topic,destinationName=TEST";
+ private static final String SUBSCRIPTION = QUEUE + ",endpoint=Consumer,clientId=client,consumerId=consumer";
+ private static final String CONNECTOR = BROKER + ",connector=clientConnectors,connectorName=openwire";
+ private static final String NETWORK_CONNECTOR = BROKER + ",connector=networkConnectors,networkConnectorName=nc";
+
+ // The only operations reachable through Jolokia by default: none of them changes broker state.
+ private static final Set<String> READ_ONLY_OPERATIONS = Set.of(
+ "queryQueues", "queryTopics", "browseQueue", "getTransportConnectorByType",
+ "browse", "browseAsTable", "getMessage",
+ "cursorSize", "doesCursorHaveMessagesBuffered", "doesCursorHaveSpace",
+ "isMatchingQueue", "isMatchingTopic",
+ "health", "healthList", "healthStatus",
+ "getAllJobs", "getNextScheduleJobs", "getExecutionCount",
+ "getLogLevel", "connectionCount");
+
+ private static final Map<String, Class<?>> MBEANS = new LinkedHashMap<>();
+ static {
+ MBEANS.put(BROKER, BrokerViewMBean.class);
+ MBEANS.put(QUEUE, QueueViewMBean.class);
+ MBEANS.put(TOPIC, TopicViewMBean.class);
+ MBEANS.put(SUBSCRIPTION, DurableSubscriptionViewMBean.class);
+ MBEANS.put(TOPIC + ",endpoint=Consumer,clientId=client,consumerId=consumer", TopicSubscriptionViewMBean.class);
+ MBEANS.put(QUEUE + ",endpoint=Producer,clientId=client,producerId=producer", ProducerViewMBean.class);
+ MBEANS.put(BROKER + ",service=Health", HealthViewMBean.class);
+ MBEANS.put(BROKER + ",service=JobScheduler,name=JMS", JobSchedulerViewMBean.class);
+ MBEANS.put(BROKER + ",service=Log4JConfiguration", Log4JConfigViewMBean.class);
+ MBEANS.put(BROKER + ",service=PersistenceAdapter,instanceName=KahaDB", PersistenceAdapterViewMBean.class);
+ MBEANS.put(BROKER + ",service=SlowConsumerStrategy,instanceName=strategy", AbortSlowConsumerStrategyViewMBean.class);
+ MBEANS.put(BROKER + ",service=RuntimeConfiguration,name=XBean", RuntimeConfigurationViewMBean.class);
+ MBEANS.put(BROKER + ",service=plugin,virtualDestinationSelectoCache=cache", VirtualDestinationSelectorCacheViewMBean.class);
+ MBEANS.put(BROKER + ",transactionType=RecoveredXaTransaction,xid=xid", RecoveredXATransactionViewMBean.class);
+ MBEANS.put(CONNECTOR, ConnectorViewMBean.class);
+ MBEANS.put(CONNECTOR + ",connectionViewType=clientId,connectionName=client", ConnectionViewMBean.class);
+ MBEANS.put(NETWORK_CONNECTOR, NetworkConnectorViewMBean.class);
+ MBEANS.put(NETWORK_CONNECTOR + ",networkBridge=bridge", NetworkBridgeViewMBean.class);
+ MBEANS.put("org.apache.activemq:type=Broker,brokerName=localhost,service=SecurityAdmin", SecurityAdminMBean.class);
+ MBEANS.put("org.apache.activemq:type=Broker,brokerName=localhost,service=TransportLoggerControl", TransportLoggerControlMBean.class);
+ MBEANS.put("org.apache.activemq:type=Broker,brokerName=localhost,service=TransportLogger", TransportLoggerViewMBean.class);
+ }
+
+ private PolicyRestrictor restrictor;
+
+ @Before
+ public void loadPolicy() throws Exception {
+ try (InputStream in = new FileInputStream("src/release/conf/jolokia-access.xml")) {
+ restrictor = new PolicyRestrictor(in);
+ }
+ }
+
+ @Test
+ public void onlyReadCommandsAreEnabled() {
+ assertTrue(restrictor.isTypeAllowed(RequestType.READ));
+ assertTrue(restrictor.isTypeAllowed(RequestType.LIST));
+ assertTrue(restrictor.isTypeAllowed(RequestType.SEARCH));
+ assertTrue(restrictor.isTypeAllowed(RequestType.VERSION));
+ assertFalse(restrictor.isTypeAllowed(RequestType.WRITE));
+ assertFalse(restrictor.isTypeAllowed(RequestType.EXEC));
+ assertTrue(restrictor.isHttpMethodAllowed(HttpMethod.POST));
+ assertFalse(restrictor.isHttpMethodAllowed(HttpMethod.GET));
+ }
+
+ @Test
+ public void onlyReadOnlyOperationsCanBeExecuted() throws Exception {
+ int allowed = 0;
+ for (Map.Entry<String, Class<?>> mbean : MBEANS.entrySet()) {
+ ObjectName name = new ObjectName(mbean.getKey());
+ for (Method method : mbean.getValue().getMethods()) {
+ if (isAttribute(method)) {
+ continue;
+ }
+ boolean expected = READ_ONLY_OPERATIONS.contains(method.getName());
+ // a client may send the bare name or the name with its signature
+ for (String operation : new String[] {method.getName(), signature(method)}) {
+ assertEquals(operation + " on " + name, expected, restrictor.isOperationAllowed(name, operation));
+ }
+ if (expected) {
+ allowed++;
+ }
+ }
+ }
+ assertTrue("no read-only operation was checked", allowed > 0);
+ }
+
+ @Test
+ public void operationsOutsideTheBrokerDomainCannotBeExecuted() throws Exception {
+ assertFalse(restrictor.isOperationAllowed(new ObjectName("java.lang:type=Memory"), "gc"));
+ assertFalse(restrictor.isOperationAllowed(new ObjectName("java.lang:type=Threading"), "dumpAllThreads"));
+ assertFalse(restrictor.isOperationAllowed(new ObjectName("com.sun.management:type=DiagnosticCommand"), "vmSystemProperties"));
+ assertFalse(restrictor.isOperationAllowed(new ObjectName("com.sun.management:type=HotSpotDiagnostic"), "dumpHeap"));
+ assertFalse(restrictor.isOperationAllowed(new ObjectName("javax.management.loading:type=MLet"), "getMBeansFromURL"));
+ }
+
+ @Test
+ public void attributesCannotBeWritten() throws Exception {
+ assertFalse(restrictor.isAttributeWriteAllowed(new ObjectName(BROKER), "MemoryLimit"));
+ assertFalse(restrictor.isAttributeWriteAllowed(new ObjectName(QUEUE), "MaxPageSize"));
+ assertFalse(restrictor.isAttributeWriteAllowed(new ObjectName(NETWORK_CONNECTOR), "Password"));
+ assertFalse(restrictor.isAttributeWriteAllowed(new ObjectName("java.lang:type=Memory"), "Verbose"));
+ }
+
+ @Test
+ public void attributesCanBeReadExceptSensitiveOnes() throws Exception {
+ assertTrue(restrictor.isAttributeReadAllowed(new ObjectName(BROKER), "BrokerVersion"));
+ assertTrue(restrictor.isAttributeReadAllowed(new ObjectName(QUEUE), "QueueSize"));
+ assertFalse(restrictor.isAttributeReadAllowed(new ObjectName(NETWORK_CONNECTOR), "Password"));
+ assertFalse(restrictor.isAttributeReadAllowed(new ObjectName(NETWORK_CONNECTOR), "RemotePassword"));
+ assertFalse(restrictor.isAttributeReadAllowed(new ObjectName("java.lang:type=Runtime"), "SystemProperties"));
+ }
+
+ @Test
+ public void onlyTheConsoleOriginsPassTheOriginCheck() {
+ for (String origin : new String[] {"http://localhost:8161", "http://127.0.0.1:8161", "http://[::1]:8161",
+ "https://localhost:8443", "https://127.0.0.1:8443", "https://[::1]:8443"}) {
+ assertTrue(origin, restrictor.isOriginAllowed(origin, true));
+ }
+ for (String origin : new String[] {"https://example.org", "http://localhost:9999",
+ "http://localhost.example.org:8161", "http://localhost:8161.example.org",
+ "https://example.org/http://localhost:8161"}) {
+ assertFalse(origin, restrictor.isOriginAllowed(origin, true));
+ assertFalse(origin, restrictor.isOriginAllowed(origin, false));
+ }
+ // neither an Origin nor a Referer header
+ assertFalse(restrictor.isOriginAllowed(null, true));
+ }
+
+ // JMX exposes getters and setters as attributes, everything else as operations
+ private static boolean isAttribute(Method method) {
+ String name = method.getName();
+ int parameters = method.getParameterCount();
+ Class<?> returnType = method.getReturnType();
+ return (name.startsWith("get") && parameters == 0 && returnType != void.class)
+ || (name.startsWith("is") && parameters == 0 && returnType == boolean.class)
+ || (name.startsWith("set") && parameters == 1 && returnType == void.class);
+ }
+
+ private static String signature(Method method) {
+ return method.getName() + Arrays.stream(method.getParameterTypes())
+ .map(Class::getName)
+ .collect(Collectors.joining(",", "(", ")"));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@activemq.apache.org
For additional commands, e-mail: commits-help@activemq.apache.org
For further information, visit: https://activemq.apache.org/contact