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."
This commit is contained in:
Justin Bertram 2022-12-23 00:51:29 -06:00
parent c354c8e642
commit 56167b5e13
5 changed files with 123 additions and 23 deletions

View File

@ -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) @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); 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);
} }

View File

@ -47,6 +47,10 @@ import org.apache.activemq.artemis.core.settings.HierarchicalRepository;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles; 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 { 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 READ_PERMISSION_VALUE = "readPermissionValue";
public static final String WRITE_PERMISSION_VALUE = "writePermissionValue"; public static final String WRITE_PERMISSION_VALUE = "writePermissionValue";
public static final String ENABLE_LISTENER = "enableListener"; 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 MAP_ADMIN_TO_MANAGE = "mapAdminToManage";
public static final String ALLOW_QUEUE_ADMIN_ON_READ = "allowQueueAdminOnRead"; 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 readPermissionValue = "read";
private String writePermissionValue = "write"; private String writePermissionValue = "write";
private boolean enableListener = true; private boolean enableListener = true;
private int refreshInterval = 0;
private boolean mapAdminToManage = false; private boolean mapAdminToManage = false;
private boolean allowQueueAdminOnRead = false; private boolean allowQueueAdminOnRead = false;
@ -90,6 +96,8 @@ public class LegacyLDAPSecuritySettingPlugin implements SecuritySettingPlugin {
private EventDirContext eventContext; private EventDirContext eventContext;
private Map<String, Set<Role>> securityRoles; private Map<String, Set<Role>> securityRoles;
private HierarchicalRepository<Set<Role>> securityRepository; private HierarchicalRepository<Set<Role>> securityRepository;
private ScheduledExecutorService scheduler;
private ScheduledFuture<?> scheduledFuture;
@Override @Override
public LegacyLDAPSecuritySettingPlugin init(Map<String, String> options) { public LegacyLDAPSecuritySettingPlugin init(Map<String, String> options) {
@ -107,10 +115,26 @@ public class LegacyLDAPSecuritySettingPlugin implements SecuritySettingPlugin {
readPermissionValue = getOption(options, READ_PERMISSION_VALUE, readPermissionValue); readPermissionValue = getOption(options, READ_PERMISSION_VALUE, readPermissionValue);
writePermissionValue = getOption(options, WRITE_PERMISSION_VALUE, writePermissionValue); writePermissionValue = getOption(options, WRITE_PERMISSION_VALUE, writePermissionValue);
enableListener = getOption(options, ENABLE_LISTENER, Boolean.TRUE.toString()).equalsIgnoreCase(Boolean.TRUE.toString()); 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()); 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()); 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; return this;
} }
@ -458,6 +482,13 @@ public class LegacyLDAPSecuritySettingPlugin implements SecuritySettingPlugin {
@Override @Override
public SecuritySettingPlugin stop() { public SecuritySettingPlugin stop() {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
if (scheduler != null) {
scheduler.shutdown();
}
try { try {
eventContext.close(); eventContext.close();
} catch (NamingException e) { } catch (NamingException e) {

View File

@ -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 receive updates made in the LDAP server and update the broker's authorization
configuration in real-time. The default value is `true`. 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 - `mapAdminToManage`. Whether or not to map the legacy `admin` permission to the
`manage` permission. See details of the mapping semantics below. The default `manage` permission. See details of the mapping semantics below. The default
value is `false`. value is `false`.

View File

@ -107,6 +107,15 @@ public class LegacyLDAPSecuritySettingPluginListenerTest extends AbstractLdapTes
testDir = temporaryFolder.getRoot().getAbsolutePath(); testDir = temporaryFolder.getRoot().getAbsolutePath();
LegacyLDAPSecuritySettingPlugin legacyLDAPSecuritySettingPlugin = new LegacyLDAPSecuritySettingPlugin(); 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<String, String> getSecuritSettingPluginConfigMap() {
Map<String, String> map = new HashMap<>(); Map<String, String> map = new HashMap<>();
map.put(LegacyLDAPSecuritySettingPlugin.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); map.put(LegacyLDAPSecuritySettingPlugin.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
map.put(LegacyLDAPSecuritySettingPlugin.CONNECTION_URL, "ldap://localhost:1024"); 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.CONNECTION_PROTOCOL, "s");
map.put(LegacyLDAPSecuritySettingPlugin.AUTHENTICATION, "simple"); map.put(LegacyLDAPSecuritySettingPlugin.AUTHENTICATION, "simple");
map.put(LegacyLDAPSecuritySettingPlugin.ENABLE_LISTENER, "true"); map.put(LegacyLDAPSecuritySettingPlugin.ENABLE_LISTENER, "true");
legacyLDAPSecuritySettingPlugin.init(map); return 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);
} }
@After @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.unbind("cn=read,cn=" + queue + ",ou=queues,ou=destinations,o=ActiveMQ,ou=system");
ctx.close(); ctx.close();
try { Wait.assertTrue("Consuming here should fail due to the modified security data.", () -> {
session.createConsumer(queue); try {
Assert.fail("Consuming here should fail due to the modified security data."); session.createConsumer(queue);
} catch (ActiveMQException e) { return false;
// ok } catch (Exception e) {
} return true;
}
}, 2000, 100);
cf.close(); 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.unbind("cn=write,cn=" + queue + ",ou=queues,ou=destinations,o=ActiveMQ,ou=system");
ctx.close(); ctx.close();
try { Wait.assertTrue("Producing here should fail due to the modified security data.", () -> {
producer.send(session.createMessage(true)); try {
Assert.fail("Producing here should fail due to the modified security data."); producer.send(session.createMessage(true));
} catch (ActiveMQException e) { return false;
// ok } catch (Exception e) {
} return true;
}
}, 2000, 100);
cf.close(); cf.close();
} }
@ -409,7 +417,15 @@ public class LegacyLDAPSecuritySettingPluginListenerTest extends AbstractLdapTes
ClientSession session = cf.createSession(USERNAME, "secret", false, true, true, false, 0); ClientSession session = cf.createSession(USERNAME, "secret", false, true, true, false, 0);
ClientProducer producer = session.createProducer(queue); 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(); cf.close();
} }
@ -491,17 +507,23 @@ public class LegacyLDAPSecuritySettingPluginListenerTest extends AbstractLdapTes
server.createQueue(new QueueConfiguration(goodQueue).setRoutingType(RoutingType.ANYCAST).setDurable(false)); server.createQueue(new QueueConfiguration(goodQueue).setRoutingType(RoutingType.ANYCAST).setDurable(false));
ClientSession session = cf.createSession(USERNAME, "secret", false, true, true, false, 0); 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(); session.close();
producer.close();
server.createQueue(new QueueConfiguration(badQueue).setRoutingType(RoutingType.ANYCAST).setDurable(false)); server.createQueue(new QueueConfiguration(badQueue).setRoutingType(RoutingType.ANYCAST).setDurable(false));
// authorization for sending should fail for the new queue // authorization for sending should fail for the new queue
try { try {
session = cf.createSession(USERNAME, "secret", false, true, true, false, 0); session = cf.createSession(USERNAME, "secret", false, true, true, false, 0);
producer = session.createProducer(badQueue); ClientProducer producer = session.createProducer(badQueue);
producer.send(session.createMessage(true)); producer.send(session.createMessage(true));
Assert.fail("Producing here should fail."); Assert.fail("Producing here should fail.");
} catch (ActiveMQException e) { } catch (ActiveMQException e) {

View File

@ -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<String, String> getSecuritSettingPluginConfigMap() {
Map<String, String> map = super.getSecuritSettingPluginConfigMap();
map.put(LegacyLDAPSecuritySettingPlugin.ENABLE_LISTENER, "false");
map.put(LegacyLDAPSecuritySettingPlugin.REFRESH_INTERVAL, "1");
return map;
}
}