From 56167b5e136fb5554807d02253a8e5f1ebe31c09 Mon Sep 17 00:00:00 2001 From: Justin Bertram Date: Fri, 23 Dec 2022 00:51:29 -0600 Subject: [PATCH] ARTEMIS-4122 support timed refresh for LegacyLDAPSecuritySettingPlugin Some LDAP servers (e.g. OpenLDAP) do not support the "persistent search" feature and therefore the existing "listener" feature does not actually fetch updates. This commit implements a "pull" feature controlled by a configurable interval equivalent to what is implemented in the cached LDAP authorization module from ActiveMQ "Classic." --- .../core/server/ActiveMQServerLogger.java | 2 + .../impl/LegacyLDAPSecuritySettingPlugin.java | 31 +++++++++ docs/user-manual/en/security.md | 13 ++++ ...LDAPSecuritySettingPluginListenerTest.java | 68 ++++++++++++------- ...yLDAPSecuritySettingPluginRefreshTest.java | 32 +++++++++ 5 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LegacyLDAPSecuritySettingPluginRefreshTest.java 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; + } +}