ARTEMIS-601 load runtime security cfg file changes

This commit is contained in:
jbertram 2016-08-03 17:27:26 -05:00
parent 85857ab8eb
commit 1ef9e74f14
17 changed files with 289 additions and 5 deletions

View File

@ -16,14 +16,31 @@
*/
package org.apache.activemq.cli.test;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
import org.apache.activemq.artemis.api.core.client.ClientProducer;
import org.apache.activemq.artemis.api.core.client.ClientSession;
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory;
import org.apache.activemq.artemis.api.core.client.ServerLocator;
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration;
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl;
import org.apache.activemq.artemis.dto.ServerDTO;
import org.apache.activemq.artemis.integration.FileBroker;
import org.apache.activemq.artemis.jms.server.impl.JMSServerManagerImpl;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.apache.activemq.artemis.spi.core.security.jaas.InVMLoginModule;
import org.junit.Assert;
import org.junit.Test;
import static org.junit.Assert.fail;
public class FileBrokerTest {
@Test
@ -71,4 +88,63 @@ public class FileBrokerTest {
broker.stop();
}
}
@Test
public void testConfigFileReload() throws Exception {
ServerDTO serverDTO = new ServerDTO();
serverDTO.configuration = "broker-reload.xml";
FileBroker broker = null;
String path = null;
try {
SecurityConfiguration securityConfiguration = new SecurityConfiguration();
securityConfiguration.addUser("myUser", "myPass");
securityConfiguration.addRole("myUser", "guest");
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager(InVMLoginModule.class.getName(), securityConfiguration);
broker = new FileBroker(serverDTO, securityManager);
broker.start();
ActiveMQServerImpl activeMQServer = (ActiveMQServerImpl) broker.getComponents().get("core");
Assert.assertNotNull(activeMQServer);
Assert.assertTrue(activeMQServer.isStarted());
Assert.assertTrue(broker.isStarted());
path = activeMQServer.getConfiguration().getConfigurationUrl().getPath();
Assert.assertNotNull(activeMQServer.getConfiguration().getConfigurationUrl());
Thread.sleep(activeMQServer.getConfiguration().getConfigurationFileRefreshPeriod() * 2);
ServerLocator locator = ActiveMQClient.createServerLocator("tcp://localhost:61616");
ClientSessionFactory sf = locator.createSessionFactory();
ClientSession session = sf.createSession("myUser", "myPass", false, true, false, false, 0);
ClientProducer producer = session.createProducer("jms.queue.DLQ");
producer.send(session.createMessage(true));
replacePatternInFile(path, "guest", "X");
Thread.sleep(activeMQServer.getConfiguration().getConfigurationFileRefreshPeriod() * 2);
try {
producer.send(session.createMessage(true));
fail("Should throw a security exception");
}
catch (Exception e) {
}
locator.close();
}
finally {
assert broker != null;
broker.stop();
if (path != null) {
replacePatternInFile(path, "X", "guest");
}
}
}
private void replacePatternInFile(String file, String regex, String replacement) throws IOException {
Path path = Paths.get(file);
Charset charset = StandardCharsets.UTF_8;
String content = new String(Files.readAllBytes(path), charset);
String replaced = content.replaceAll(regex, replacement);
Files.write(path, replaced.getBytes(charset));
Files.setLastModifiedTime(path, FileTime.fromMillis(System.currentTimeMillis()));
}
}

View File

@ -0,0 +1,76 @@
<?xml version='1.0'?>
<!--
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.
-->
<configuration xmlns="urn:activemq">
<jms xmlns="urn:activemq:jms">
<queue name="DLQ"/>
<queue name="ExpiryQueue"/>
</jms>
<core xmlns="urn:activemq:core">
<paging-directory>./target/paging</paging-directory>
<bindings-directory>./target/bindings</bindings-directory>
<journal-directory>./target/journal</journal-directory>
<journal-min-files>2</journal-min-files>
<large-messages-directory>./target/large-messages</large-messages-directory>
<configuration-file-refresh-period>500</configuration-file-refresh-period>
<connectors>
<!-- Default Connector. Returned to clients during broadcast and distributed around cluster. See broadcast and discovery-groups -->
<connector name="activemq">tcp://${activemq.remoting.default.host:localhost}:${activemq.remoting.default.port:61616}</connector>
</connectors>
<acceptors>
<!-- Default ActiveMQ Artemis Acceptor. Multi-protocol adapter. Currently supports Core, OpenWire, Stomp and AMQP. -->
<acceptor name="activemq">tcp://${activemq.remoting.default.host:localhost}:${activemq.remoting.default.port:61616}</acceptor>
<!-- AMQP Acceptor. Listens on default AMQP port for AMQP traffic.-->
<acceptor name="amqp">tcp://${activemq.remoting.amqp.host:localhost}:${activemq.remoting.amqp.port:5672}?protocols=AMQP</acceptor>
<!-- STOMP Acceptor. -->
<acceptor name="stomp">tcp://${activemq.remoting.stomp.host:localhost}:${activemq.remoting.stomp.port:61613}?protocols=STOMP</acceptor>
<!-- HornetQ Compatibility Acceptor. Enables ActiveMQ Artemis Core and STOMP for legacy HornetQ clients. -->
<acceptor name="hornetq">tcp://${activemq.remoting.hornetq.host:localhost}:${activemq.remoting.hornetq.port:5445}?protocols=CORE,STOMP</acceptor>
</acceptors>
<security-settings>
<security-setting match="#">
<permission type="send" roles="guest"/>
</security-setting>
</security-settings>
<address-settings>
<!--default for catch all-->
<address-setting match="#">
<dead-letter-address>jms.queue.DLQ</dead-letter-address>
<expiry-address>jms.queue.ExpiryQueue</expiry-address>
<redelivery-delay>0</redelivery-delay>
<max-size-bytes>10485760</max-size-bytes>
<message-counter-history-day-limit>10</message-counter-history-day-limit>
<address-full-policy>BLOCK</address-full-policy>
</address-setting>
</address-settings>
</core>
</configuration>

View File

@ -426,6 +426,9 @@ public final class ActiveMQDefaultConfiguration {
// Default period to wait between connection TTL checks
public static final long DEFAULT_CONNECTION_TTL_CHECK_INTERVAL = 2000;
// Default period to wait between configuration file checks
public static final long DEFAULT_CONFIGURATION_FILE_REFRESH_PERIOD = 5000;
/**
* If true then the ActiveMQ Artemis Server will make use of any Protocol Managers that are in available on the classpath. If false then only the core protocol will be available, unless in Embedded mode where users can inject their own Protocol Managers.
*/
@ -1137,4 +1140,8 @@ public final class ActiveMQDefaultConfiguration {
public static long getDefaultConnectionTtlCheckInterval() {
return DEFAULT_CONNECTION_TTL_CHECK_INTERVAL;
}
public static long getDefaultConfigurationFileRefreshPeriod() {
return DEFAULT_CONFIGURATION_FILE_REFRESH_PERIOD;
}
}

View File

@ -33,6 +33,7 @@ import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.management.MBeanServer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Map;
@ -57,7 +58,7 @@ public class FileJMSConfiguration extends JMSConfigurationImpl implements Deploy
private boolean parsed = false;
@Override
public void parse(Element config) throws Exception {
public void parse(Element config, URL url) throws Exception {
parseConfiguration(config);
parsed = true;
}

View File

@ -17,6 +17,7 @@
package org.apache.activemq.artemis.core.config;
import java.io.File;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -959,4 +960,12 @@ public interface Configuration {
long getConnectionTtlCheckInterval();
URL getConfigurationUrl();
Configuration setConfigurationUrl(URL configurationUrl);
long getConfigurationFileRefreshPeriod();
Configuration setConfigurationFileRefreshPeriod(long configurationFileRefreshPeriod);
}

View File

@ -83,7 +83,7 @@ public class FileDeploymentManager {
if (root != null && children.getLength() > 0) {
Node item = children.item(0);
XMLUtil.validate(item, deployable.getSchema());
deployable.parse((Element) item);
deployable.parse((Element) item, url);
}
}
}

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.net.URI;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
@ -241,6 +242,10 @@ public class ConfigurationImpl implements Configuration, Serializable {
private long connectionTtlCheckInterval = ActiveMQDefaultConfiguration.getDefaultConnectionTtlCheckInterval();
private URL configurationUrl;
private long configurationFileRefreshPeriod = ActiveMQDefaultConfiguration.getDefaultConfigurationFileRefreshPeriod();
/**
* Parent folder for all data folders.
*/
@ -1759,6 +1764,28 @@ public class ConfigurationImpl implements Configuration, Serializable {
return this;
}
@Override
public URL getConfigurationUrl() {
return configurationUrl;
}
@Override
public ConfigurationImpl setConfigurationUrl(URL configurationUrl) {
this.configurationUrl = configurationUrl;
return this;
}
@Override
public long getConfigurationFileRefreshPeriod() {
return configurationFileRefreshPeriod;
}
@Override
public ConfigurationImpl setConfigurationFileRefreshPeriod(long configurationFileRefreshPeriod) {
this.configurationFileRefreshPeriod = configurationFileRefreshPeriod;
return this;
}
/**
* It will find the right location of a subFolder, related to artemisInstance
*/

View File

@ -16,6 +16,7 @@
*/
package org.apache.activemq.artemis.core.config.impl;
import java.net.URL;
import java.util.Map;
import org.apache.activemq.artemis.core.deployers.Deployable;
@ -44,7 +45,7 @@ public final class FileConfiguration extends ConfigurationImpl implements Deploy
private boolean parsed = false;
@Override
public void parse(Element config) throws Exception {
public void parse(Element config, URL url) throws Exception {
FileConfigurationParser parser = new FileConfigurationParser();
// https://jira.jboss.org/browse/HORNETQ-478 - We only want to validate AIO when
@ -54,6 +55,8 @@ public final class FileConfiguration extends ConfigurationImpl implements Deploy
parser.parseMainConfig(config, this);
setConfigurationUrl(url);
parsed = true;
}

View File

@ -21,6 +21,7 @@ import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager;
import org.w3c.dom.Element;
import javax.management.MBeanServer;
import java.net.URL;
import java.util.Map;
/**
@ -32,7 +33,7 @@ public interface Deployable {
/*
* parse the element from the xml configuration
*/
void parse(Element config) throws Exception;
void parse(Element config, URL url) throws Exception;
/*
* has this Deployable been parsed

View File

@ -280,6 +280,8 @@ public final class FileConfigurationParser extends XMLConfigurationUtil {
config.setConnectionTtlCheckInterval(getLong(e, "connection-ttl-check-interval", config.getConnectionTtlCheckInterval(), Validators.GT_ZERO));
config.setConfigurationFileRefreshPeriod(getLong(e, "configuration-file-refresh-period", config.getConfigurationFileRefreshPeriod(), Validators.GT_ZERO));
// parsing cluster password
String passwordText = getString(e, "cluster-password", null, Validators.NO_CHECK);

View File

@ -1498,4 +1498,8 @@ public interface ActiveMQServerLogger extends BasicLogger {
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 224068, value = "Unable to stop component: {0}", format = Message.Format.MESSAGE_FORMAT)
void errorStoppingComponent(@Cause Throwable t, String componentClassName);
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 224069, value = "Change detected in broker configuration file, but reload failed", format = Message.Format.MESSAGE_FORMAT)
void configurationReloadFailed(@Cause Throwable t);
}

View File

@ -23,6 +23,7 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
@ -55,6 +56,7 @@ import org.apache.activemq.artemis.core.config.CoreQueueConfiguration;
import org.apache.activemq.artemis.core.config.DivertConfiguration;
import org.apache.activemq.artemis.core.config.StoreConfiguration;
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl;
import org.apache.activemq.artemis.core.deployers.impl.FileConfigurationParser;
import org.apache.activemq.artemis.core.filter.Filter;
import org.apache.activemq.artemis.core.filter.impl.FilterImpl;
import org.apache.activemq.artemis.core.io.IOCriticalErrorListener;
@ -450,6 +452,10 @@ public class ActiveMQServerImpl implements ActiveMQServer {
// start connector service
connectorsService = new ConnectorsService(configuration, storageManager, scheduledPool, postOffice, serviceRegistry);
connectorsService.start();
if (configuration.getConfigurationUrl() != null && getScheduledPool() != null) {
getScheduledPool().scheduleWithFixedDelay(new ConfigurationFileReloader(this), configuration.getConfigurationFileRefreshPeriod(), configuration.getConfigurationFileRefreshPeriod(), TimeUnit.MILLISECONDS);
}
}
finally {
// this avoids embedded applications using dirty contexts from startup
@ -2351,4 +2357,38 @@ public class ActiveMQServerImpl implements ActiveMQServer {
}
}
private final class ConfigurationFileReloader extends Thread {
long lastModified;
boolean first = true;
ActiveMQServer server;
ConfigurationFileReloader(ActiveMQServer server) {
this.server = server;
}
public void run() {
try {
URL url = server.getConfiguration().getConfigurationUrl();
long currentLastModified = new File(url.toURI()).lastModified();
if (first) {
first = false;
lastModified = currentLastModified;
return;
}
if (currentLastModified > lastModified) {
if (ActiveMQServerLogger.LOGGER.isDebugEnabled()) {
ActiveMQServerLogger.LOGGER.debug("Configuration file change detected. Reloading...");
}
Configuration config = new FileConfigurationParser().parseMainConfig(url.openStream());
securityRepository.swap(config.getSecurityRoles().entrySet());
lastModified = currentLastModified;
}
}
catch (Exception e) {
ActiveMQServerLogger.LOGGER.configurationReloadFailed(e);
}
}
}
}

View File

@ -17,6 +17,8 @@
package org.apache.activemq.artemis.core.settings;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* allows objects to be mapped against a regex pattern and held in order in a list
@ -85,6 +87,8 @@ public interface HierarchicalRepository<T> {
*/
void clear();
void swap(Set<Map.Entry<String, T>> entries);
/**
* Removes all listeners.
*/

View File

@ -147,6 +147,10 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
*/
@Override
public void addMatch(final String match, final T value, final boolean immutableMatch) {
addMatch(match, value, immutableMatch, true);
}
private void addMatch(final String match, final T value, final boolean immutableMatch, boolean notifyListeners) {
lock.writeLock().lock();
try {
clearCache();
@ -164,7 +168,9 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
}
// Calling the onChange outside of the wrieLock as some listeners may be doing reads on the matches
onChange();
if (notifyListeners) {
onChange();
}
}
@Override
@ -317,6 +323,24 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
}
}
@Override
public void swap(Set<Map.Entry<String, T>> entries) {
lock.writeLock().lock();
try {
clearCache();
immutables.clear();
matches.clear();
for (Map.Entry<String, T> entry : entries) {
addMatch(entry.getKey(), entry.getValue(), true, false);
}
}
finally {
lock.writeLock().unlock();
}
onChange();
}
@Override
public void clearListeners() {
listeners.clear();

View File

@ -246,6 +246,14 @@
</xsd:annotation>
</xsd:element>
<xsd:element name="configuration-file-refresh-period" type="xsd:long" default="5000" maxOccurs="1" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
how often (in ms) to check the configuration file for modifications
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="async-connection-execution-enabled" type="xsd:boolean" default="true" maxOccurs="1"
minOccurs="0">
<xsd:annotation>

View File

@ -104,6 +104,7 @@ public class FileConfigurationTest extends ConfigurationImplTest {
Assert.assertEquals(12345, conf.getGracefulShutdownTimeout());
Assert.assertEquals(true, conf.isPopulateValidatedUser());
Assert.assertEquals(98765, conf.getConnectionTtlCheckInterval());
Assert.assertEquals(1234567, conf.getConfigurationFileRefreshPeriod());
Assert.assertEquals("largemessagesdir", conf.getLargeMessagesDirectory());
Assert.assertEquals(95, conf.getMemoryWarningThreshold());

View File

@ -52,6 +52,7 @@
<persist-id-cache>true</persist-id-cache>
<populate-validated-user>true</populate-validated-user>
<connection-ttl-check-interval>98765</connection-ttl-check-interval>
<configuration-file-refresh-period>1234567</configuration-file-refresh-period>
<remoting-incoming-interceptors>
<class-name>org.apache.activemq.artemis.tests.unit.core.config.impl.TestInterceptor1</class-name>
<class-name>org.apache.activemq.artemis.tests.unit.core.config.impl.TestInterceptor2</class-name>