diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java index 6bcc08a45c..2b9699df52 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java @@ -1557,4 +1557,6 @@ public interface ActiveMQServerLogger { @LogMessage(id = 224118, value = "The SQL Database is returning a current time too far from this system current time. Adjust clock on the SQL Database server. DatabaseTime={}, CurrentTime={}, allowed variance={}", level = LogMessage.Level.WARN) void dbReturnedTimeOffClock(long dbTime, long systemTime, long variance); + @LogMessage(id = 224119, value = "Unable to refresh security settings: {}", level = LogMessage.Level.WARN) + void unableToRefreshSecuritySettings(String exceptionMessage); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/LegacyLDAPSecuritySettingPlugin.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/LegacyLDAPSecuritySettingPlugin.java index dd758bae5c..d31a242970 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/LegacyLDAPSecuritySettingPlugin.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/LegacyLDAPSecuritySettingPlugin.java @@ -47,6 +47,10 @@ import org.apache.activemq.artemis.core.settings.HierarchicalRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; public class LegacyLDAPSecuritySettingPlugin implements SecuritySettingPlugin { @@ -67,6 +71,7 @@ public class LegacyLDAPSecuritySettingPlugin implements SecuritySettingPlugin { public static final String READ_PERMISSION_VALUE = "readPermissionValue"; public static final String WRITE_PERMISSION_VALUE = "writePermissionValue"; public static final String ENABLE_LISTENER = "enableListener"; + public static final String REFRESH_INTERVAL = "refreshInterval"; public static final String MAP_ADMIN_TO_MANAGE = "mapAdminToManage"; public static final String ALLOW_QUEUE_ADMIN_ON_READ = "allowQueueAdminOnRead"; @@ -83,6 +88,7 @@ public class LegacyLDAPSecuritySettingPlugin implements SecuritySettingPlugin { private String readPermissionValue = "read"; private String writePermissionValue = "write"; private boolean enableListener = true; + private int refreshInterval = 0; private boolean mapAdminToManage = false; private boolean allowQueueAdminOnRead = false; @@ -90,6 +96,8 @@ public class LegacyLDAPSecuritySettingPlugin implements SecuritySettingPlugin { private EventDirContext eventContext; private Map> securityRoles; private HierarchicalRepository> securityRepository; + private ScheduledExecutorService scheduler; + private ScheduledFuture scheduledFuture; @Override public LegacyLDAPSecuritySettingPlugin init(Map options) { @@ -107,10 +115,26 @@ public class LegacyLDAPSecuritySettingPlugin implements SecuritySettingPlugin { readPermissionValue = getOption(options, READ_PERMISSION_VALUE, readPermissionValue); writePermissionValue = getOption(options, WRITE_PERMISSION_VALUE, writePermissionValue); enableListener = getOption(options, ENABLE_LISTENER, Boolean.TRUE.toString()).equalsIgnoreCase(Boolean.TRUE.toString()); + refreshInterval = Integer.parseInt(getOption(options, REFRESH_INTERVAL, Integer.valueOf(refreshInterval).toString())); mapAdminToManage = getOption(options, MAP_ADMIN_TO_MANAGE, Boolean.FALSE.toString()).equalsIgnoreCase(Boolean.TRUE.toString()); allowQueueAdminOnRead = getOption(options, ALLOW_QUEUE_ADMIN_ON_READ, Boolean.FALSE.toString()).equalsIgnoreCase(Boolean.TRUE.toString()); } + if (refreshInterval > 0) { + scheduler = Executors.newScheduledThreadPool(1); + logger.debug("Scheduling refresh every {} seconds.", refreshInterval); + scheduledFuture = scheduler.scheduleAtFixedRate(() -> { + logger.debug("Refreshing after {} seconds...", refreshInterval); + try { + securityRoles = null; + securityRepository.swap(getSecurityRoles().entrySet()); + } catch (Exception e) { + ActiveMQServerLogger.LOGGER.unableToRefreshSecuritySettings(e.getMessage()); + logger.debug("security refresh failure", e); + } + }, refreshInterval, refreshInterval, TimeUnit.SECONDS); + } + return this; } @@ -458,6 +482,13 @@ public class LegacyLDAPSecuritySettingPlugin implements SecuritySettingPlugin { @Override public SecuritySettingPlugin stop() { + if (scheduledFuture != null) { + scheduledFuture.cancel(true); + } + if (scheduler != null) { + scheduler.shutdown(); + } + try { eventContext.close(); } catch (NamingException e) { diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md index 1e7d405c81..004b6deafc 100644 --- a/docs/user-manual/en/security.md +++ b/docs/user-manual/en/security.md @@ -298,6 +298,19 @@ and Security Layer (SASL) authentication is currently not supported. receive updates made in the LDAP server and update the broker's authorization configuration in real-time. The default value is `true`. + Some LDAP servers (e.g. OpenLDAP) don't support the "persistent search" + feature which allows the "listener" functionality to work. For these servers + set the `refreshInterval` to a value greater than `0`. + +- `refreshInterval`. How long to wait (in seconds) before refreshing the + security settings from the LDAP server. This can be used for LDAP servers + which don't support the "persistent search" feature needed for use with + `enableListener` (e.g. OpenLDAP). Default is `0` (i.e. no refresh). + + Keep in mind that this can be a potentially expensive operation based on how + often the refresh is configured and how large the data set is so take care + in how `refreshInterval` is configured. + - `mapAdminToManage`. Whether or not to map the legacy `admin` permission to the `manage` permission. See details of the mapping semantics below. The default value is `false`. diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LegacyLDAPSecuritySettingPluginListenerTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LegacyLDAPSecuritySettingPluginListenerTest.java index 3db6ce45f3..116608231c 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LegacyLDAPSecuritySettingPluginListenerTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LegacyLDAPSecuritySettingPluginListenerTest.java @@ -107,6 +107,15 @@ public class LegacyLDAPSecuritySettingPluginListenerTest extends AbstractLdapTes testDir = temporaryFolder.getRoot().getAbsolutePath(); LegacyLDAPSecuritySettingPlugin legacyLDAPSecuritySettingPlugin = new LegacyLDAPSecuritySettingPlugin(); + legacyLDAPSecuritySettingPlugin.init(getSecuritSettingPluginConfigMap()); + + ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager("LDAPLogin"); + Configuration configuration = new ConfigurationImpl().setSecurityEnabled(true).addAcceptorConfiguration(new TransportConfiguration(InVMAcceptorFactory.class.getCanonicalName())).setJournalDirectory(ActiveMQTestBase.getJournalDir(testDir, 0, false)).setBindingsDirectory(ActiveMQTestBase.getBindingsDir(testDir, 0, false)).setPagingDirectory(ActiveMQTestBase.getPageDir(testDir, 0, false)).setLargeMessagesDirectory(ActiveMQTestBase.getLargeMessagesDir(testDir, 0, false)).setPersistenceEnabled(false).addSecuritySettingPlugin(legacyLDAPSecuritySettingPlugin); + + server = ActiveMQServers.newActiveMQServer(configuration, ManagementFactory.getPlatformMBeanServer(), securityManager, false); + } + + protected Map getSecuritSettingPluginConfigMap() { Map map = new HashMap<>(); map.put(LegacyLDAPSecuritySettingPlugin.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); map.put(LegacyLDAPSecuritySettingPlugin.CONNECTION_URL, "ldap://localhost:1024"); @@ -115,12 +124,7 @@ public class LegacyLDAPSecuritySettingPluginListenerTest extends AbstractLdapTes map.put(LegacyLDAPSecuritySettingPlugin.CONNECTION_PROTOCOL, "s"); map.put(LegacyLDAPSecuritySettingPlugin.AUTHENTICATION, "simple"); map.put(LegacyLDAPSecuritySettingPlugin.ENABLE_LISTENER, "true"); - legacyLDAPSecuritySettingPlugin.init(map); - - ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager("LDAPLogin"); - Configuration configuration = new ConfigurationImpl().setSecurityEnabled(true).addAcceptorConfiguration(new TransportConfiguration(InVMAcceptorFactory.class.getCanonicalName())).setJournalDirectory(ActiveMQTestBase.getJournalDir(testDir, 0, false)).setBindingsDirectory(ActiveMQTestBase.getBindingsDir(testDir, 0, false)).setPagingDirectory(ActiveMQTestBase.getPageDir(testDir, 0, false)).setLargeMessagesDirectory(ActiveMQTestBase.getLargeMessagesDir(testDir, 0, false)).setPersistenceEnabled(false).addSecuritySettingPlugin(legacyLDAPSecuritySettingPlugin); - - server = ActiveMQServers.newActiveMQServer(configuration, ManagementFactory.getPlatformMBeanServer(), securityManager, false); + return map; } @After @@ -289,12 +293,14 @@ public class LegacyLDAPSecuritySettingPluginListenerTest extends AbstractLdapTes ctx.unbind("cn=read,cn=" + queue + ",ou=queues,ou=destinations,o=ActiveMQ,ou=system"); ctx.close(); - try { - session.createConsumer(queue); - Assert.fail("Consuming here should fail due to the modified security data."); - } catch (ActiveMQException e) { - // ok - } + Wait.assertTrue("Consuming here should fail due to the modified security data.", () -> { + try { + session.createConsumer(queue); + return false; + } catch (Exception e) { + return true; + } + }, 2000, 100); cf.close(); } @@ -338,12 +344,14 @@ public class LegacyLDAPSecuritySettingPluginListenerTest extends AbstractLdapTes ctx.unbind("cn=write,cn=" + queue + ",ou=queues,ou=destinations,o=ActiveMQ,ou=system"); ctx.close(); - try { - producer.send(session.createMessage(true)); - Assert.fail("Producing here should fail due to the modified security data."); - } catch (ActiveMQException e) { - // ok - } + Wait.assertTrue("Producing here should fail due to the modified security data.", () -> { + try { + producer.send(session.createMessage(true)); + return false; + } catch (Exception e) { + return true; + } + }, 2000, 100); cf.close(); } @@ -409,7 +417,15 @@ public class LegacyLDAPSecuritySettingPluginListenerTest extends AbstractLdapTes ClientSession session = cf.createSession(USERNAME, "secret", false, true, true, false, 0); ClientProducer producer = session.createProducer(queue); - producer.send(session.createMessage(true)); + + Wait.assertTrue("Producing here should succeed due to the modified security data.", () -> { + try { + producer.send(session.createMessage(true)); + return true; + } catch (Exception e) { + return false; + } + }, 2000, 100); cf.close(); } @@ -491,17 +507,23 @@ public class LegacyLDAPSecuritySettingPluginListenerTest extends AbstractLdapTes server.createQueue(new QueueConfiguration(goodQueue).setRoutingType(RoutingType.ANYCAST).setDurable(false)); ClientSession session = cf.createSession(USERNAME, "secret", false, true, true, false, 0); - ClientProducer producer = session.createProducer(goodQueue); - producer.send(session.createMessage(true)); + + ClientSession finalSession = session; + Wait.assertTrue("Producing here should succeed due to the modified security data.", () -> { + try (ClientProducer producer = finalSession.createProducer(goodQueue)) { + return true; + } catch (Exception e) { + return false; + } + }, 2000, 100); session.close(); - producer.close(); server.createQueue(new QueueConfiguration(badQueue).setRoutingType(RoutingType.ANYCAST).setDurable(false)); // authorization for sending should fail for the new queue try { session = cf.createSession(USERNAME, "secret", false, true, true, false, 0); - producer = session.createProducer(badQueue); + ClientProducer producer = session.createProducer(badQueue); producer.send(session.createMessage(true)); Assert.fail("Producing here should fail."); } catch (ActiveMQException e) { diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LegacyLDAPSecuritySettingPluginRefreshTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LegacyLDAPSecuritySettingPluginRefreshTest.java new file mode 100644 index 0000000000..bf99aa32c0 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LegacyLDAPSecuritySettingPluginRefreshTest.java @@ -0,0 +1,32 @@ +/* + * 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.artemis.tests.integration.security; + +import java.util.Map; + +import org.apache.activemq.artemis.core.server.impl.LegacyLDAPSecuritySettingPlugin; + +public class LegacyLDAPSecuritySettingPluginRefreshTest extends LegacyLDAPSecuritySettingPluginListenerTest { + + @Override + protected Map getSecuritSettingPluginConfigMap() { + Map map = super.getSecuritSettingPluginConfigMap(); + map.put(LegacyLDAPSecuritySettingPlugin.ENABLE_LISTENER, "false"); + map.put(LegacyLDAPSecuritySettingPlugin.REFRESH_INTERVAL, "1"); + return map; + } +}