ARTEMIS-4267 original exception lost for NoCacheLoginException

When skipping the authentication cache details for the original
exception are not logged.

This commit ensures these details are logged and adopts the
ExceptionUtils class from Apache Commons Lang in lieu of the previous
custom implementation.
This commit is contained in:
Justin Bertram 2023-04-28 15:22:13 -05:00 committed by Bruscino Domenico Francesco
parent 5e32a1ab62
commit c2bada6a77
15 changed files with 218 additions and 127 deletions

View File

@ -1,33 +0,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.
*/
package org.apache.activemq.artemis.utils;
import java.util.ArrayList;
import java.util.List;
public class ExceptionUtil {
public static Throwable getRootCause(final Throwable throwable) {
List<Throwable> list = new ArrayList<>();
Throwable current = throwable;
while (current != null && list.contains(current) == false) {
list.add(current);
current = current.getCause();
}
return (list.size() < 2 ? throwable : list.get(list.size() - 1));
}
}

View File

@ -94,8 +94,8 @@ import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextConfig;
import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactoryProvider;
import org.apache.activemq.artemis.utils.ActiveMQThreadFactory;
import org.apache.activemq.artemis.utils.ConfigurationHelper;
import org.apache.activemq.artemis.utils.ExceptionUtil;
import org.apache.activemq.artemis.utils.collections.TypedProperties;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -479,7 +479,7 @@ public class NettyAcceptor extends AbstractAcceptor {
pipeline.addLast("ssl", getSslHandler(channel.alloc(), peerInfo.getA(), peerInfo.getB()));
pipeline.addLast("sslHandshakeExceptionHandler", new SslHandshakeExceptionHandler());
} catch (Exception e) {
Throwable rootCause = ExceptionUtil.getRootCause(e);
Throwable rootCause = ExceptionUtils.getRootCause(e);
ActiveMQServerLogger.LOGGER.gettingSslHandlerFailed(channel.remoteAddress().toString(), rootCause.getClass().getName() + ": " + rootCause.getMessage());
logger.debug("Getting SSL handler failed", e);
@ -1036,7 +1036,7 @@ public class NettyAcceptor extends AbstractAcceptor {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (cause.getMessage() != null && cause.getMessage().startsWith(SSLHandshakeException.class.getName())) {
Throwable rootCause = ExceptionUtil.getRootCause(cause);
Throwable rootCause = ExceptionUtils.getRootCause(cause);
String errorMessage = rootCause.getClass().getName() + ": " + rootCause.getMessage();
ActiveMQServerLogger.LOGGER.sslHandshakeFailed(ctx.channel().remoteAddress().toString(), errorMessage);

View File

@ -189,7 +189,7 @@ public class SecurityStoreImpl implements SecurityStore, HierarchicalRepositoryC
authenticationCache.put(createAuthenticationCacheKey(user, password, connection), new Pair<>(subject != null, subject));
validatedUser = getUserFromSubject(subject);
} catch (NoCacheLoginException e) {
logger.debug("Skipping authentication cache due to exception", e);
handleNoCacheLoginException(e);
}
} else if (securityManager instanceof ActiveMQSecurityManager4) {
validatedUser = ((ActiveMQSecurityManager4) securityManager).validateUser(user, password, connection, securityDomain);
@ -413,12 +413,17 @@ public class SecurityStoreImpl implements SecurityStore, HierarchicalRepositoryC
authenticationCache.put(createAuthenticationCacheKey(auth.getUsername(), auth.getPassword(), auth.getRemotingConnection()), new Pair<>(subject != null, subject));
return subject;
} catch (NoCacheLoginException e) {
handleNoCacheLoginException(e);
return null;
}
}
return cached.getB();
}
private void handleNoCacheLoginException(NoCacheLoginException e) {
logger.debug("Skipping authentication cache due to exception: {}", e.getMessage());
}
// public for testing purposes
public void invalidateAuthorizationCache() {
authorizationCache.invalidateAll();

View File

@ -19,6 +19,7 @@ package org.apache.activemq.artemis.spi.core.security;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.lang.invoke.MethodHandles;
import java.util.Set;
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration;
@ -28,11 +29,9 @@ import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.spi.core.security.jaas.JaasCallbackHandler;
import org.apache.activemq.artemis.spi.core.security.jaas.NoCacheLoginException;
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal;
import org.apache.activemq.artemis.utils.ExceptionUtil;
import org.apache.activemq.artemis.utils.SecurityManagerUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles;
import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getCertsFromConnection;
@ -90,11 +89,14 @@ public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager5 {
}
@Override
public Subject authenticate(final String user, final String password, RemotingConnection remotingConnection, final String securityDomain) {
public Subject authenticate(final String user, final String password, RemotingConnection remotingConnection, final String securityDomain) throws NoCacheLoginException {
try {
return getAuthenticatedSubject(user, password, remotingConnection, securityDomain);
} catch (LoginException e) {
logger.debug("Couldn't validate user", e);
if (e instanceof NoCacheLoginException) {
throw (NoCacheLoginException) e;
}
return null;
}
}
@ -138,16 +140,7 @@ public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager5 {
} else {
lc = new LoginContext(configurationName, null, new JaasCallbackHandler(user, password, remotingConnection), configuration);
}
try {
lc.login();
} catch (LoginException e) {
Throwable rootCause = ExceptionUtil.getRootCause(e);
if (rootCause instanceof NoCacheLoginException) {
throw (NoCacheLoginException) rootCause;
} else {
throw e;
}
}
lc.login();
return lc.getSubject();
} finally {
if (thisLoader != currentLoader) {

View File

@ -22,6 +22,7 @@ import java.util.Set;
import org.apache.activemq.artemis.core.security.CheckType;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.spi.core.security.jaas.NoCacheLoginException;
/**
* Used to validate whether a user is authorized to connect to the
@ -44,7 +45,7 @@ public interface ActiveMQSecurityManager5 extends ActiveMQSecurityManager {
* @param securityDomain the name of the JAAS security domain to use (can be null)
* @return the Subject of the authenticated user, else null
*/
Subject authenticate(String user, String password, RemotingConnection remotingConnection, String securityDomain);
Subject authenticate(String user, String password, RemotingConnection remotingConnection, String securityDomain) throws NoCacheLoginException;
/**
* Determine whether the given user has the correct role for the given check type.

View File

@ -41,6 +41,7 @@ import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
@ -59,11 +60,10 @@ import java.util.Queue;
import java.util.Set;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.apache.activemq.artemis.utils.ExceptionUtil;
import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles;
public class LDAPLoginModule implements AuditLoginModule {
@ -262,9 +262,9 @@ public class LDAPLoginModule implements AuditLoginModule {
}
private LoginException handleException(LoginException e) {
Throwable t = ExceptionUtil.getRootCause(e);
if (noCacheExceptions.contains(t.getClass().getName())) {
t.initCause(new NoCacheLoginException());
Throwable rootCause = ExceptionUtils.getRootCause(e);
if (noCacheExceptions.contains(rootCause.getClass().getName())) {
return (NoCacheLoginException) new NoCacheLoginException(rootCause.getClass().getName() + (rootCause.getMessage() == null ? "" : ": " + rootCause.getMessage())).initCause(e);
}
return e;
}

View File

@ -17,11 +17,13 @@
package org.apache.activemq.artemis.spi.core.security.jaas;
public class NoCacheLoginException extends RuntimeException {
import javax.security.auth.login.LoginException;
public class NoCacheLoginException extends LoginException {
public NoCacheLoginException() {
super();
}
public NoCacheLoginException(Exception e) {
super(e);
public NoCacheLoginException(String message) {
super(message);
}
}

View File

@ -0,0 +1,104 @@
/*
* 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.core.security.jaas;
import javax.security.auth.Subject;
import java.io.UnsupportedEncodingException;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.apache.activemq.artemis.core.security.CheckType;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.security.impl.SecurityStoreImpl;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(Parameterized.class)
public class JAASSecurityManagerClassLoadingTest {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Parameterized.Parameters(name = "newLoader=({0})")
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {{true}, {false}});
}
static {
String path = System.getProperty("java.security.auth.login.config");
if (path == null) {
URL resource = PropertiesLoginModuleTest.class.getClassLoader().getResource("login.config");
if (resource != null) {
try {
path = URLDecoder.decode(resource.getFile(), StandardCharsets.UTF_8.name());
System.setProperty("java.security.auth.login.config", path);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}
}
@Parameterized.Parameter
public boolean usingNewLoader;
@Rule
public TemporaryFolder tmpDir = new TemporaryFolder();
@Test
public void testLoginClassloading() throws Exception {
ClassLoader existingLoader = Thread.currentThread().getContextClassLoader();
logger.debug("loader: {}", existingLoader);
try {
if (usingNewLoader) {
URLClassLoader simulatedLoader = new URLClassLoader(new URL[]{tmpDir.getRoot().toURI().toURL()}, null);
Thread.currentThread().setContextClassLoader(simulatedLoader);
}
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager("PropertiesLogin");
Subject result = securityManager.authenticate("first", "secret", null, null);
assertNotNull(result);
assertEquals("first", SecurityStoreImpl.getUserFromSubject(result));
Role role = new Role("programmers", true, true, true, true, true, true, true, true, true, true);
Set<Role> roles = new HashSet<>();
roles.add(role);
boolean authorizationResult = securityManager.authorize(result, roles, CheckType.SEND, "someaddress");
assertTrue(authorizationResult);
} finally {
Thread.currentThread().setContextClassLoader(existingLoader);
}
}
}

View File

@ -16,43 +16,19 @@
*/
package org.apache.activemq.artemis.core.security.jaas;
import javax.security.auth.Subject;
import org.apache.activemq.artemis.core.security.CheckType;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.security.impl.SecurityStoreImpl;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.apache.activemq.artemis.spi.core.security.jaas.NoCacheLoginException;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@RunWith(Parameterized.class)
public class JAASSecurityManagerTest {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Parameterized.Parameters(name = "newLoader=({0})")
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {{true}, {false}});
}
static {
String path = System.getProperty("java.security.auth.login.config");
@ -69,38 +45,14 @@ public class JAASSecurityManagerTest {
}
}
@Parameterized.Parameter
public boolean usingNewLoader;
@Rule
public TemporaryFolder tmpDir = new TemporaryFolder();
@Test
public void testLoginClassloading() throws Exception {
ClassLoader existingLoader = Thread.currentThread().getContextClassLoader();
logger.debug("loader: {}", existingLoader);
public void testNoCacheLoginException() {
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager("testNoCacheLoginException");
try {
if (usingNewLoader) {
URLClassLoader simulatedLoader = new URLClassLoader(new URL[]{tmpDir.getRoot().toURI().toURL()}, null);
Thread.currentThread().setContextClassLoader(simulatedLoader);
}
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager("PropertiesLogin");
Subject result = securityManager.authenticate("first", "secret", null, null);
assertNotNull(result);
assertEquals("first", SecurityStoreImpl.getUserFromSubject(result));
Role role = new Role("programmers", true, true, true, true, true, true, true, true, true, true);
Set<Role> roles = new HashSet<>();
roles.add(role);
boolean authorizationResult = securityManager.authorize(result, roles, CheckType.SEND, "someaddress");
assertTrue(authorizationResult);
} finally {
Thread.currentThread().setContextClassLoader(existingLoader);
securityManager.authenticate(null, null, null, null);
fail();
} catch (NoCacheLoginException ncle) {
assertEquals(NoCacheLoginModule.MESSAGE, ncle.getMessage());
}
}
}

View File

@ -0,0 +1,56 @@
/**
* 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.core.security.jaas;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import java.util.Map;
import org.apache.activemq.artemis.spi.core.security.jaas.NoCacheLoginException;
import org.apache.activemq.artemis.utils.RandomUtil;
public class NoCacheLoginModule implements LoginModule {
public static final String MESSAGE = RandomUtil.randomString();
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> map, Map<String, ?> map1) {
}
@Override
public boolean login() throws LoginException {
throw (NoCacheLoginException) new NoCacheLoginException(MESSAGE).initCause(new FailedLoginException());
}
@Override
public boolean commit() throws LoginException {
return false;
}
@Override
public boolean abort() throws LoginException {
return false;
}
@Override
public boolean logout() throws LoginException {
return false;
}
}

View File

@ -237,4 +237,8 @@ HttpServerAuthenticator {
org.apache.activemq.artemis.spi.core.security.jaas.KubernetesLoginModule sufficient
debug=true
org.apache.activemq.jaas.kubernetes.role="cert-roles.properties";
};
};
testNoCacheLoginException {
org.apache.activemq.artemis.core.security.jaas.NoCacheLoginModule required;
};

View File

@ -835,13 +835,18 @@ system. It is implemented by
because it has no default value. Example value: `(member={0})`.
- `noCacheExceptions` - comma separated list of class names of exceptions which
may thrown during communication with the LDAP server; default is empty.
may be thrown during communication with the LDAP server; default is empty.
Typically any failure to authenticate will be stored in the authentication cache
so that the underlying security data store (e.g. LDAP) is spared any unnecessary
traffic. However, in cases where the failure is, for example, due to a temporary
network outage and the `security-invalidation-interval` is relatively high this
can be problematic. Users can enumerate any relevant exceptions which the cache
should ignore (e.g. `java.net.ConnectException`) to avoid any such problems.
traffic. For example, an application with the wrong password attempting to login
multiple times in short order might adversely impact the LDAP server. However, in
cases where the failure is, for example, due to a temporary network outage and
the `security-invalidation-interval` is relatively high then _not_ caching such
failures would be better. Users can enumerate any relevant exceptions which the
cache should ignore (e.g. `java.net.ConnectException`). The name of the exception
should be the **root cause** from the relevant stack-trace. Users can confirm
the configured exceptions are being skipped by enabling debug logging for
`org.apache.activemq.artemis.core.security.impl.SecurityStoreImpl`.
- `debug` - boolean flag; if `true`, enable debugging; this is used only for
testing or debugging; normally, it should be set to `false`, or omitted;

View File

@ -26,12 +26,13 @@ import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager5;
import org.apache.activemq.artemis.spi.core.security.jaas.NoCacheLoginException;
public class JAASSecurityManagerWrapper implements ActiveMQSecurityManager5 {
ActiveMQJAASSecurityManager activeMQJAASSecurityManager;
@Override
public Subject authenticate(String user, String password, RemotingConnection remotingConnection, String securityDomain) {
public Subject authenticate(String user, String password, RemotingConnection remotingConnection, String securityDomain) throws NoCacheLoginException {
System.out.println("authenticate(" + user + ", " + password + ", " + remotingConnection.getRemoteAddress() + ", " + securityDomain + ")");
return activeMQJAASSecurityManager.authenticate(user, password, remotingConnection, securityDomain);
}

View File

@ -142,7 +142,7 @@ public class SecurityTest extends ActiveMQTestBase {
public Subject authenticate(String user,
String password,
RemotingConnection remotingConnection,
String securityDomain) {
String securityDomain) throws NoCacheLoginException {
flipper = !flipper;
if (flipper) {
return new Subject();

View File

@ -29,6 +29,7 @@ import org.apache.activemq.artemis.core.server.ActiveMQServers;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.apache.activemq.artemis.spi.core.security.jaas.InVMLoginModule;
import org.apache.activemq.artemis.spi.core.security.jaas.NoCacheLoginException;
import org.apache.activemq.artemis.tests.integration.stomp.util.StompClientConnection;
import org.apache.activemq.artemis.tests.integration.stomp.util.StompClientConnectionFactory;
import org.junit.Test;
@ -52,7 +53,7 @@ public class StompWithClientIdValidationTest extends StompTestBase {
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager(InVMLoginModule.class.getName(), new SecurityConfiguration()) {
@Override
public Subject authenticate(String user, String password, RemotingConnection remotingConnection, String securityDomain) {
public Subject authenticate(String user, String password, RemotingConnection remotingConnection, String securityDomain) throws NoCacheLoginException {
Subject validatedUser = super.authenticate(user, password, remotingConnection, securityDomain);
if (validatedUser == null) {