ARTEMIS-2886 optimize security auth

Both authentication and authorization will hit the underlying security
repository (e.g. files, LDAP, etc.). For example, creating a JMS
connection and a consumer will result in 2 hits with the *same*
authentication request. This can cause unwanted (and unnecessary)
resource utilization, especially in the case of networked configuration
like LDAP.

There is already a rudimentary cache for authorization, but it is
cleared *totally* every 10 seconds by default (controlled via the
security-invalidation-interval setting), and it must be populated
initially which still results in duplicate auth requests.

This commit optimizes authentication and authorization via the following
changes:

 - Replace our home-grown cache with Google Guava's cache. This provides
simple caching with both time-based and size-based LRU eviction. See more
at https://github.com/google/guava/wiki/CachesExplained. I also thought
about using Caffeine, but we already have a dependency on Guava and the
cache implementions look to be negligibly different for this use-case.
 - Add caching for authentication. Both successful and unsuccessful
authentication attempts will be cached to spare the underlying security
repository as much as possible. Authenticated Subjects will be cached
and re-used whenever possible.
 - Authorization will used Subjects cached during authentication. If the
required Subject is not in the cache it will be fetched from the
underlying security repo.
 - Caching can be disabled by setting the security-invalidation-interval
to 0.
 - Cache sizes are configurable.
 - Management operations exist to inspect cache sizes at runtime.
This commit is contained in:
Justin Bertram 2020-08-24 16:24:25 +02:00
parent b85156cc27
commit 90853409a0
25 changed files with 441 additions and 125 deletions

View File

@ -69,6 +69,7 @@ import org.apache.activemq.artemis.core.client.impl.ServerLocatorImpl;
import org.apache.activemq.artemis.core.config.FileDeploymentManager;
import org.apache.activemq.artemis.core.config.impl.FileConfiguration;
import org.apache.activemq.artemis.core.security.CheckType;
import org.apache.activemq.artemis.core.security.impl.SecurityStoreImpl;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.JournalType;
import org.apache.activemq.artemis.core.server.management.ManagementContext;
@ -621,6 +622,7 @@ public class ArtemisTest extends CliTestBase {
activeMQServerControl.addSecuritySettings("myAddress", "myRole", "myRole", "myRole", "myRole", "myRole", "myRole", "myRole", "myRole", "myRole", "myRole");
// change properties files which should cause another "reload" event
activeMQServerControl.addUser("foo", "bar", "myRole", true);
((SecurityStoreImpl)activeMQServer.getSecurityStore()).invalidateAuthenticationCache();
ClientSession session = sessionFactory.createSession("foo", "bar", false, false, false, false, 0);
session.createQueue("myAddress", RoutingType.ANYCAST, "myQueue", true);
ClientProducer producer = session.createProducer("myAddress");

View File

@ -2737,4 +2737,20 @@ public interface AuditLogger extends BasicLogger {
@LogMessage(level = Logger.Level.INFO)
@Message(id = 601735, value = "User {0} is getting group rebalance pause dispatch property on target resource: {1} {2}", format = Message.Format.MESSAGE_FORMAT)
void isGroupRebalancePauseDispatch(String user, Object source, Object... args);
static void getAuthenticationCacheSize(Object source) {
LOGGER.getAuthenticationCacheSize(getCaller(), source);
}
@LogMessage(level = Logger.Level.INFO)
@Message(id = 601736, value = "User {0} is getting authentication cache size on target resource: {1} {2}", format = Message.Format.MESSAGE_FORMAT)
void getAuthenticationCacheSize(String user, Object source, Object... args);
static void getAuthorizationCacheSize(Object source) {
LOGGER.getAuthorizationCacheSize(getCaller(), source);
}
@LogMessage(level = Logger.Level.INFO)
@Message(id = 601737, value = "User {0} is getting authorization cache size on target resource: {1} {2}", format = Message.Format.MESSAGE_FORMAT)
void getAuthorizationCacheSize(String user, Object source, Object... args);
}

View File

@ -160,6 +160,12 @@ public final class ActiveMQDefaultConfiguration {
// how long (in ms) to wait before invalidating the security cache
private static long DEFAULT_SECURITY_INVALIDATION_INTERVAL = 10000;
// how large to make the authentication cache
private static long DEFAULT_AUTHENTICATION_CACHE_SIZE = 1000;
// how large to make the authorization cache
private static long DEFAULT_AUTHORIZATION_CACHE_SIZE = 1000;
// how long (in ms) to wait to acquire a file lock on the journal
private static long DEFAULT_JOURNAL_LOCK_ACQUISITION_TIMEOUT = -1;
@ -680,6 +686,20 @@ public final class ActiveMQDefaultConfiguration {
return DEFAULT_SECURITY_INVALIDATION_INTERVAL;
}
/**
* how large to make the authentication cache
*/
public static long getDefaultAuthenticationCacheSize() {
return DEFAULT_AUTHENTICATION_CACHE_SIZE;
}
/**
* how large to make the authorization cache
*/
public static long getDefaultAuthorizationCacheSize() {
return DEFAULT_AUTHORIZATION_CACHE_SIZE;
}
/**
* how long (in ms) to wait to acquire a file lock on the journal
*/

View File

@ -460,6 +460,18 @@ public interface ActiveMQServerControl {
@Attribute(desc = ADDRESS_MEMORY_USAGE_PERCENTAGE_DESCRIPTION)
int getAddressMemoryUsagePercentage();
/**
* Returns the runtime size of the authentication cache
*/
@Attribute(desc = "The runtime size of the authentication cache")
long getAuthenticationCacheSize();
/**
* Returns the runtime size of the authorization cache
*/
@Attribute(desc = "The runtime size of the authorization cache")
long getAuthorizationCacheSize();
// Operations ----------------------------------------------------
@Operation(desc = "Isolate the broker", impact = MBeanOperationInfo.ACTION)
boolean freezeReplication();

View File

@ -71,6 +71,7 @@
<bundle dependency="true">mvn:org.apache.commons/commons-text/${commons.text.version}</bundle>
<bundle dependency="true">mvn:org.apache.commons/commons-lang3/${commons.lang.version}</bundle>
<bundle dependency="true">mvn:org.jctools/jctools-core/${jctools.version}</bundle>
<bundle dependency="true">mvn:com.google.guava/guava/${guava.version}</bundle>
<!-- Micrometer can't be included until it supports OSGi. It is currently an "optional" Maven dependency. -->
<!--bundle dependency="true">mvn:io.micrometer/micrometer-core/${version.micrometer}</bundle-->

View File

@ -48,6 +48,10 @@
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>

View File

@ -211,6 +211,28 @@ public interface Configuration {
*/
Configuration setSecurityInvalidationInterval(long interval);
/**
* Sets the size of the authentication cache.
*/
Configuration setAuthenticationCacheSize(long size);
/**
* Returns the configured size of the authentication cache. <br>
* Default value is {@link org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration#DEFAULT_AUTHENTICATION_CACHE_SIZE}.
*/
long getAuthenticationCacheSize();
/**
* Sets the size of the authorization cache.
*/
Configuration setAuthorizationCacheSize(long size);
/**
* Returns the configured size of the authorization cache. <br>
* Default value is {@link org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration#DEFAULT_AUTHORIZATION_CACHE_SIZE}.
*/
long getAuthorizationCacheSize();
/**
* Returns whether security is enabled for this server. <br>
* Default value is {@link org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration#DEFAULT_SECURITY_ENABLED}.

View File

@ -122,6 +122,10 @@ public class ConfigurationImpl implements Configuration, Serializable {
private long securityInvalidationInterval = ActiveMQDefaultConfiguration.getDefaultSecurityInvalidationInterval();
private long authenticationCacheSize = ActiveMQDefaultConfiguration.getDefaultAuthenticationCacheSize();
private long authorizationCacheSize = ActiveMQDefaultConfiguration.getDefaultAuthorizationCacheSize();
private boolean securityEnabled = ActiveMQDefaultConfiguration.isDefaultSecurityEnabled();
private boolean gracefulShutdownEnabled = ActiveMQDefaultConfiguration.isDefaultGracefulShutdownEnabled();
@ -506,6 +510,28 @@ public class ConfigurationImpl implements Configuration, Serializable {
return this;
}
@Override
public long getAuthenticationCacheSize() {
return authenticationCacheSize;
}
@Override
public ConfigurationImpl setAuthenticationCacheSize(final long size) {
authenticationCacheSize = size;
return this;
}
@Override
public long getAuthorizationCacheSize() {
return authorizationCacheSize;
}
@Override
public ConfigurationImpl setAuthorizationCacheSize(final long size) {
authorizationCacheSize = size;
return this;
}
@Override
public long getConnectionTTLOverride() {
return connectionTTLOverride;

View File

@ -370,7 +370,11 @@ public final class FileConfigurationParser extends XMLConfigurationUtil {
config.setJMXUseBrokerName(getBoolean(e, "jmx-use-broker-name", config.isJMXUseBrokerName()));
config.setSecurityInvalidationInterval(getLong(e, "security-invalidation-interval", config.getSecurityInvalidationInterval(), Validators.GT_ZERO));
config.setSecurityInvalidationInterval(getLong(e, "security-invalidation-interval", config.getSecurityInvalidationInterval(), Validators.GE_ZERO));
config.setAuthenticationCacheSize(getLong(e, "authentication-cache-size", config.getAuthenticationCacheSize(), Validators.GE_ZERO));
config.setAuthorizationCacheSize(getLong(e, "authorization-cache-size", config.getAuthorizationCacheSize(), Validators.GE_ZERO));
config.setConnectionTTLOverride(getLong(e, "connection-ttl-override", config.getConnectionTTLOverride(), Validators.MINUS_ONE_OR_GT_ZERO));

View File

@ -91,6 +91,7 @@ import org.apache.activemq.artemis.core.postoffice.impl.LocalQueueBinding;
import org.apache.activemq.artemis.core.remoting.server.RemotingService;
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.core.server.ActiveMQMessageBundle;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
@ -757,6 +758,22 @@ public class ActiveMQServerControlImpl extends AbstractControl implements Active
return (int) result;
}
@Override
public long getAuthenticationCacheSize() {
if (AuditLogger.isEnabled()) {
AuditLogger.getAuthenticationCacheSize(this.server);
}
return ((SecurityStoreImpl)server.getSecurityStore()).getAuthenticationCacheSize();
}
@Override
public long getAuthorizationCacheSize() {
if (AuditLogger.isEnabled()) {
AuditLogger.getAuthorizationCacheSize(this.server);
}
return ((SecurityStoreImpl)server.getSecurityStore()).getAuthorizationCacheSize();
}
@Override
public boolean freezeReplication() {
if (AuditLogger.isEnabled()) {

View File

@ -16,11 +16,14 @@
*/
package org.apache.activemq.artemis.core.security.impl;
import javax.security.auth.Subject;
import javax.security.cert.X509Certificate;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.apache.activemq.artemis.api.core.Pair;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.management.CoreNotificationType;
import org.apache.activemq.artemis.api.core.management.ManagementHelper;
@ -41,6 +44,8 @@ import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager2;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager3;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager4;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager5;
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
import org.apache.activemq.artemis.utils.CompositeAddress;
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet;
import org.apache.activemq.artemis.utils.collections.TypedProperties;
@ -57,11 +62,9 @@ public class SecurityStoreImpl implements SecurityStore, HierarchicalRepositoryC
private final ActiveMQSecurityManager securityManager;
private final ConcurrentMap<String, ConcurrentHashSet<SimpleString>> cache = new ConcurrentHashMap<>();
private final Cache<String, ConcurrentHashSet<SimpleString>> authorizationCache;
private final long invalidationInterval;
private volatile long lastCheck;
private final Cache<String, Pair<Boolean, Subject>> authenticationCache;
private boolean securityEnabled;
@ -82,14 +85,23 @@ public class SecurityStoreImpl implements SecurityStore, HierarchicalRepositoryC
final boolean securityEnabled,
final String managementClusterUser,
final String managementClusterPassword,
final NotificationService notificationService) {
final NotificationService notificationService,
final long authenticationCacheSize,
final long authorizationCacheSize) {
this.securityRepository = securityRepository;
this.securityManager = securityManager;
this.invalidationInterval = invalidationInterval;
this.securityEnabled = securityEnabled;
this.managementClusterUser = managementClusterUser;
this.managementClusterPassword = managementClusterPassword;
this.notificationService = notificationService;
authenticationCache = CacheBuilder.newBuilder()
.maximumSize(authenticationCacheSize)
.expireAfterWrite(invalidationInterval, TimeUnit.MILLISECONDS)
.build();
authorizationCache = CacheBuilder.newBuilder()
.maximumSize(authorizationCacheSize)
.expireAfterWrite(invalidationInterval, TimeUnit.MILLISECONDS)
.build();
this.securityRepository.registerListener(this);
}
@ -142,8 +154,27 @@ public class SecurityStoreImpl implements SecurityStore, HierarchicalRepositoryC
String validatedUser = null;
boolean userIsValid = false;
boolean check = true;
if (securityManager instanceof ActiveMQSecurityManager4) {
Pair<Boolean, Subject> cacheEntry = authenticationCache.getIfPresent(createAuthenticationCacheKey(user, password, connection));
if (cacheEntry != null) {
if (!cacheEntry.getA()) {
// cached authentication failed previously so don't check again
check = false;
} else {
// cached authentication succeeded previously so don't check again
check = false;
userIsValid = true;
validatedUser = getUserFromSubject(cacheEntry.getB());
}
}
if (check) {
if (securityManager instanceof ActiveMQSecurityManager5) {
Subject subject = ((ActiveMQSecurityManager5) securityManager).authenticate(user, password, connection, securityDomain);
authenticationCache.put(createAuthenticationCacheKey(user, password, connection), new Pair<>(subject != null, subject));
validatedUser = getUserFromSubject(subject);
} else if (securityManager instanceof ActiveMQSecurityManager4) {
validatedUser = ((ActiveMQSecurityManager4) securityManager).validateUser(user, password, connection, securityDomain);
} else if (securityManager instanceof ActiveMQSecurityManager3) {
validatedUser = ((ActiveMQSecurityManager3) securityManager).validateUser(user, password, connection);
@ -152,14 +183,12 @@ public class SecurityStoreImpl implements SecurityStore, HierarchicalRepositoryC
} else {
userIsValid = securityManager.validateUser(user, password);
}
if (!userIsValid && validatedUser == null) {
String certSubjectDN = "unavailable";
X509Certificate[] certs = CertificateUtil.getCertsFromConnection(connection);
if (certs != null && certs.length > 0 && certs[0] != null) {
certSubjectDN = certs[0].getSubjectDN().getName();
}
// authentication failed, send a notification & throw an exception
if (!userIsValid && validatedUser == null) {
String certSubjectDN = getCertSubjectDN(connection);
if (notificationService != null) {
TypedProperties props = new TypedProperties();
props.putSimpleStringProperty(ManagementHelper.HDR_USER, SimpleString.toSimpleString(user));
@ -184,6 +213,15 @@ public class SecurityStoreImpl implements SecurityStore, HierarchicalRepositoryC
return null;
}
public String getCertSubjectDN(RemotingConnection connection) {
String certSubjectDN = "unavailable";
X509Certificate[] certs = CertificateUtil.getCertsFromConnection(connection);
if (certs != null && certs.length > 0 && certs[0] != null) {
certSubjectDN = certs[0].getSubjectDN().getName();
}
return certSubjectDN;
}
@Override
public void check(final SimpleString address,
final CheckType checkType,
@ -201,8 +239,8 @@ public class SecurityStoreImpl implements SecurityStore, HierarchicalRepositoryC
logger.trace("checking access permissions to " + address);
}
String user = session.getUsername();
// bypass permission checks for management cluster user
String user = session.getUsername();
if (managementClusterUser.equals(user) && session.getPassword().equals(managementClusterPassword)) {
return;
}
@ -223,20 +261,20 @@ public class SecurityStoreImpl implements SecurityStore, HierarchicalRepositoryC
}
}
if (checkCached(isFullyQualified ? fqqn : address, user, checkType)) {
if (checkAuthorizationCache(isFullyQualified ? fqqn : address, user, checkType)) {
return;
}
final boolean validated;
if (securityManager instanceof ActiveMQSecurityManager4) {
final ActiveMQSecurityManager4 securityManager4 = (ActiveMQSecurityManager4) securityManager;
validated = securityManager4.validateUserAndRole(user, session.getPassword(), roles, checkType, saddress, session.getRemotingConnection(), session.getSecurityDomain()) != null;
final Boolean validated;
if (securityManager instanceof ActiveMQSecurityManager5) {
Subject subject = getSubjectForAuthorization(session, ((ActiveMQSecurityManager5) securityManager));
validated = ((ActiveMQSecurityManager5) securityManager).authorize(subject, roles, checkType);
} else if (securityManager instanceof ActiveMQSecurityManager4) {
validated = ((ActiveMQSecurityManager4) securityManager).validateUserAndRole(user, session.getPassword(), roles, checkType, saddress, session.getRemotingConnection(), session.getSecurityDomain()) != null;
} else if (securityManager instanceof ActiveMQSecurityManager3) {
final ActiveMQSecurityManager3 securityManager3 = (ActiveMQSecurityManager3) securityManager;
validated = securityManager3.validateUserAndRole(user, session.getPassword(), roles, checkType, saddress, session.getRemotingConnection()) != null;
validated = ((ActiveMQSecurityManager3) securityManager).validateUserAndRole(user, session.getPassword(), roles, checkType, saddress, session.getRemotingConnection()) != null;
} else if (securityManager instanceof ActiveMQSecurityManager2) {
final ActiveMQSecurityManager2 securityManager2 = (ActiveMQSecurityManager2) securityManager;
validated = securityManager2.validateUserAndRole(user, session.getPassword(), roles, checkType, saddress, session.getRemotingConnection());
validated = ((ActiveMQSecurityManager2) securityManager).validateUserAndRole(user, session.getPassword(), roles, checkType, saddress, session.getRemotingConnection());
} else {
validated = securityManager.validateUserAndRole(user, session.getPassword(), roles, checkType);
}
@ -263,11 +301,16 @@ public class SecurityStoreImpl implements SecurityStore, HierarchicalRepositoryC
AuditLogger.securityFailure(ex);
throw ex;
}
// if we get here we're granted, add to the cache
ConcurrentHashSet<SimpleString> set = new ConcurrentHashSet<>();
ConcurrentHashSet<SimpleString> act = cache.putIfAbsent(user + "." + checkType.name(), set);
ConcurrentHashSet<SimpleString> set;
String key = createAuthorizationCacheKey(user, checkType);
ConcurrentHashSet<SimpleString> act = authorizationCache.getIfPresent(key);
if (act != null) {
set = act;
} else {
set = new ConcurrentHashSet<>();
authorizationCache.put(key, set);
}
set.add(isFullyQualified ? fqqn : address);
}
@ -275,39 +318,78 @@ public class SecurityStoreImpl implements SecurityStore, HierarchicalRepositoryC
@Override
public void onChange() {
invalidateCache();
invalidateAuthorizationCache();
// we don't invalidate the authentication cache here because it's not necessary
}
// Public --------------------------------------------------------
// Protected -----------------------------------------------------
// Package Private -----------------------------------------------
// Private -------------------------------------------------------
private void invalidateCache() {
cache.clear();
public static String getUserFromSubject(Subject subject) {
if (subject == null) {
return null;
}
private boolean checkCached(final SimpleString dest, final String user, final CheckType checkType) {
long now = System.currentTimeMillis();
String validatedUser = "";
Set<UserPrincipal> users = subject.getPrincipals(UserPrincipal.class);
// should only ever be 1 UserPrincipal
for (UserPrincipal userPrincipal : users) {
validatedUser = userPrincipal.getName();
}
return validatedUser;
}
/**
* Get the cached Subject. If the Subject is not in the cache then authenticate again to retrieve it.
*
* @param auth contains the authentication data
* @param securityManager used to authenticate the user if the Subject is not in the cache
* @return the authenticated Subject with all associated role principals
*/
private Subject getSubjectForAuthorization(SecurityAuth auth, ActiveMQSecurityManager5 securityManager) {
Pair<Boolean, Subject> cached = authenticationCache.getIfPresent(createAuthenticationCacheKey(auth.getUsername(), auth.getPassword(), auth.getRemotingConnection()));
/*
* We don't need to worry about the cached boolean being false as users always have to
* successfully authenticate before requesting authorization for anything.
*/
if (cached == null) {
return securityManager.authenticate(auth.getUsername(), auth.getPassword(), auth.getRemotingConnection(), auth.getSecurityDomain());
}
return cached.getB();
}
// public for testing purposes
public void invalidateAuthorizationCache() {
authorizationCache.invalidateAll();
}
// public for testing purposes
public void invalidateAuthenticationCache() {
authenticationCache.invalidateAll();
}
public long getAuthenticationCacheSize() {
return authenticationCache.size();
}
public long getAuthorizationCacheSize() {
return authorizationCache.size();
}
private boolean checkAuthorizationCache(final SimpleString dest, final String user, final CheckType checkType) {
boolean granted = false;
if (now - lastCheck > invalidationInterval) {
invalidateCache();
lastCheck = now;
} else {
ConcurrentHashSet<SimpleString> act = cache.get(user + "." + checkType.name());
ConcurrentHashSet<SimpleString> act = authorizationCache.getIfPresent(createAuthorizationCacheKey(user, checkType));
if (act != null) {
granted = act.contains(dest);
}
}
return granted;
}
// Inner class ---------------------------------------------------
private String createAuthenticationCacheKey(String username, String password, RemotingConnection connection) {
return username + password + getCertSubjectDN(connection);
}
private String createAuthorizationCacheKey(String user, CheckType checkType) {
return user + "." + checkType.name();
}
}

View File

@ -2884,7 +2884,7 @@ public class ActiveMQServerImpl implements ActiveMQServer {
ActiveMQServerLogger.LOGGER.clusterSecurityRisk();
}
securityStore = new SecurityStoreImpl(securityRepository, securityManager, configuration.getSecurityInvalidationInterval(), configuration.isSecurityEnabled(), configuration.getClusterUser(), configuration.getClusterPassword(), managementService);
securityStore = new SecurityStoreImpl(securityRepository, securityManager, configuration.getSecurityInvalidationInterval(), configuration.isSecurityEnabled(), configuration.getClusterUser(), configuration.getClusterPassword(), managementService, configuration.getAuthenticationCacheSize(), configuration.getAuthorizationCacheSize());
queueFactory = new QueueFactoryImpl(executorFactory, scheduledPool, addressSettingsRepository, storageManager, this);

View File

@ -34,7 +34,6 @@ import org.apache.activemq.artemis.logs.AuditLogger;
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.RolePrincipal;
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
import org.jboss.logging.Logger;
import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getCertsFromConnection;
@ -45,7 +44,7 @@ import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getCerts
* The {@link Subject} returned by the login context is expecting to have a set of {@link RolePrincipal} for each
* role of the user.
*/
public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager4 {
public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager5 {
private static final Logger logger = Logger.getLogger(ActiveMQJAASSecurityManager.class);
@ -95,9 +94,9 @@ public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager4 {
}
@Override
public String validateUser(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) {
try {
return getUserFromSubject(getAuthenticatedSubject(user, password, remotingConnection, securityDomain));
return getAuthenticatedSubject(user, password, remotingConnection, securityDomain);
} catch (LoginException e) {
if (logger.isDebugEnabled()) {
logger.debug("Couldn't validate user", e);
@ -106,49 +105,24 @@ public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager4 {
}
}
public String getUserFromSubject(Subject subject) {
String validatedUser = "";
Set<UserPrincipal> users = subject.getPrincipals(UserPrincipal.class);
// should only ever be 1 UserPrincipal
for (UserPrincipal userPrincipal : users) {
validatedUser = userPrincipal.getName();
}
return validatedUser;
}
@Override
public boolean validateUserAndRole(String user, String password, Set<Role> roles, CheckType checkType) {
throw new UnsupportedOperationException("Invoke validateUserAndRole(String, String, Set<Role>, CheckType, String, RemotingConnection, String) instead");
}
@Override
public String validateUserAndRole(final String user,
final String password,
public boolean authorize(final Subject subject,
final Set<Role> roles,
final CheckType checkType,
final String address,
final RemotingConnection remotingConnection,
final String securityDomain) {
Subject localSubject;
try {
localSubject = getAuthenticatedSubject(user, password, remotingConnection, securityDomain);
} catch (LoginException e) {
if (logger.isDebugEnabled()) {
logger.debug("Couldn't validate user", e);
}
return null;
}
final CheckType checkType) {
boolean authorized = false;
if (localSubject != null) {
if (subject != null) {
Set<RolePrincipal> rolesWithPermission = getPrincipalsInRole(checkType, roles);
// Check the caller's roles
Set<Principal> rolesForSubject = new HashSet<>();
try {
rolesForSubject.addAll(localSubject.getPrincipals(Class.forName(rolePrincipalClass).asSubclass(Principal.class)));
rolesForSubject.addAll(subject.getPrincipals(Class.forName(rolePrincipalClass).asSubclass(Principal.class)));
} catch (Exception e) {
ActiveMQServerLogger.LOGGER.failedToFindRolesForTheSubject(e);
}
@ -169,11 +143,7 @@ public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager4 {
}
}
if (authorized) {
return getUserFromSubject(localSubject);
} else {
return null;
}
return authorized;
}
private Subject getAuthenticatedSubject(final String user,

View File

@ -0,0 +1,61 @@
/*
* 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.spi.core.security;
import javax.security.auth.Subject;
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;
/**
* Used to validate whether a user is authorized to connect to the
* server and perform certain functions on certain addresses
*
* This is an evolution of {@link ActiveMQSecurityManager4}
* that integrates with the new Subject caching functionality.
*/
public interface ActiveMQSecurityManager5 extends ActiveMQSecurityManager {
/**
* is this a valid user.
*
* This method is called instead of
* {@link ActiveMQSecurityManager#validateUser(String, String)}.
*
* @param user the user
* @param password the user's password
* @param remotingConnection the user's connection which contains any corresponding SSL certs
* @param securityDomain the name of the JAAS security domain to use (can be null)
* @return the Subject of the authenticated user or null if the user isn't authenticated
*/
Subject authenticate(String user, String password, RemotingConnection remotingConnection, String securityDomain);
/**
* Determine whether the given user has the correct role for the given check type.
*
* This method is called instead of
* {@link ActiveMQSecurityManager#validateUserAndRole(String, String, Set, CheckType)}.
*
* @param subject the Subject to authorize
* @param roles the roles configured in the security-settings
* @param checkType which permission to validate
* @return true if the user is authorized, else false
*/
boolean authorize(Subject subject, Set<Role> roles, CheckType checkType);
}

View File

@ -134,7 +134,23 @@
<xsd:element name="security-invalidation-interval" type="xsd:long" default="10000" maxOccurs="1" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
how long (in ms) to wait before invalidating the security cache
how long (in ms) to wait before invalidating an entry in the authentication or authorization cache
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="authentication-cache-size" type="xsd:long" default="1000" maxOccurs="1" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
how large to make the authentication cache
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="authorization-cache-size" type="xsd:long" default="1000" maxOccurs="1" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
how large to make the authorization cache
</xsd:documentation>
</xsd:annotation>
</xsd:element>

View File

@ -102,6 +102,8 @@ public class FileConfigurationTest extends ConfigurationImplTest {
Assert.assertEquals(54321, conf.getThreadPoolMaxSize());
Assert.assertEquals(false, conf.isSecurityEnabled());
Assert.assertEquals(5423, conf.getSecurityInvalidationInterval());
Assert.assertEquals(333, conf.getAuthenticationCacheSize());
Assert.assertEquals(444, conf.getAuthorizationCacheSize());
Assert.assertEquals(true, conf.isWildcardRoutingEnabled());
Assert.assertEquals(new SimpleString("Giraffe"), conf.getManagementAddress());
Assert.assertEquals(new SimpleString("Whatever"), conf.getManagementNotificationAddress());

View File

@ -16,8 +16,11 @@
*/
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.jboss.logging.Logger;
import org.junit.Rule;
@ -38,6 +41,7 @@ import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(Parameterized.class)
public class JAASSecurityManagerTest {
@ -80,18 +84,17 @@ public class JAASSecurityManagerTest {
}
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager("PropertiesLogin");
String result = securityManager.validateUser("first", "secret", null, null);
Subject result = securityManager.authenticate("first", "secret", null, null);
assertNotNull(result);
assertEquals("first", 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);
result = securityManager.validateUserAndRole("first", "secret", roles, CheckType.SEND, "someaddress", null, null);
boolean authorizationResult = securityManager.authorize(result, roles, CheckType.SEND);
assertNotNull(result);
assertEquals("first", result);
assertTrue(authorizationResult);
} finally {
Thread.currentThread().setContextClassLoader(existingLoader);

View File

@ -28,6 +28,8 @@
<graceful-shutdown-enabled>true</graceful-shutdown-enabled>
<graceful-shutdown-timeout>12345</graceful-shutdown-timeout>
<security-invalidation-interval>5423</security-invalidation-interval>
<authentication-cache-size>333</authentication-cache-size>
<authorization-cache-size>444</authorization-cache-size>
<journal-lock-acquisition-timeout>123</journal-lock-acquisition-timeout>
<wild-card-routing-enabled>true</wild-card-routing-enabled>
<management-address>Giraffe</management-address>

View File

@ -29,6 +29,8 @@
<graceful-shutdown-enabled>true</graceful-shutdown-enabled>
<graceful-shutdown-timeout>12345</graceful-shutdown-timeout>
<security-invalidation-interval>5423</security-invalidation-interval>
<authentication-cache-size>333</authentication-cache-size>
<authorization-cache-size>444</authorization-cache-size>
<journal-lock-acquisition-timeout>123</journal-lock-acquisition-timeout>
<wild-card-routing-enabled>true</wild-card-routing-enabled>
<management-address>Giraffe</management-address>

View File

@ -6,9 +6,17 @@ you can configure it.
To disable security completely simply set the `security-enabled` property to
`false` in the `broker.xml` file.
For performance reasons security is cached and invalidated every so long. To
change this period set the property `security-invalidation-interval`, which is
in milliseconds. The default is `10000` ms.
For performance reasons both **authentication and authorization is cached**
independently. Entries are removed from the caches (i.e. invalidated) either
when the cache reaches its maximum size in which case the least-recently used
entry is removed or when an entry has been in the cache "too long".
The size of the caches are controlled by the `authentication-cache-size` and
`authorization-cache-size` configuration parameters. Both deafult to `1000`.
How long cache entries are valid is controlled by
`security-invalidation-interval`, which is in milliseconds. Using `0` will
disable caching. The default is `10000` ms.
## Tracking the Validated User

View File

@ -17,6 +17,7 @@
package org.apache.activemq.artemis.jms.example;
import javax.security.auth.Subject;
import java.util.Map;
import java.util.Set;
@ -25,28 +26,23 @@ 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.ActiveMQJAASSecurityManager;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager4;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager5;
public class JAASSecurityManagerWrapper implements ActiveMQSecurityManager4 {
public class JAASSecurityManagerWrapper implements ActiveMQSecurityManager5 {
ActiveMQJAASSecurityManager activeMQJAASSecurityManager;
@Override
public String validateUser(String user, String password, RemotingConnection remotingConnection, String securityDomain) {
System.out.println("validateUser(" + user + ", " + password + ", " + remotingConnection.getRemoteAddress() + ")");
return activeMQJAASSecurityManager.validateUser(user, password, remotingConnection, securityDomain);
public Subject authenticate(String user, String password, RemotingConnection remotingConnection, String securityDomain) {
System.out.println("authenticate(" + user + ", " + password + ", " + remotingConnection.getRemoteAddress() + ")");
return activeMQJAASSecurityManager.authenticate(user, password, remotingConnection, securityDomain);
}
@Override
public String validateUserAndRole(String user,
String password,
public boolean authorize(Subject subject,
Set<Role> roles,
CheckType checkType,
String address,
RemotingConnection remotingConnection,
String securityDomain) {
System.out.println("validateUserAndRole(" + user + ", " + password + ", " + roles + ", " + checkType + ", " + address + ", " + remotingConnection.getRemoteAddress() + ")");
return activeMQJAASSecurityManager.validateUserAndRole(user, password, roles, checkType, address, remotingConnection, securityDomain);
CheckType checkType) {
System.out.println("authorize(" + subject + ", " + roles + ", " + checkType + ")");
return activeMQJAASSecurityManager.authorize(subject, roles, checkType);
}
@Override

View File

@ -197,6 +197,32 @@ public class ActiveMQServerControlTest extends ManagementTestBase {
Assert.assertTrue(serverControl.isActive());
}
@Test
public void testSecurityCacheSizes() throws Exception {
ActiveMQServerControl serverControl = createManagementControl();
Assert.assertEquals(usingCore() ? 1 : 0, serverControl.getAuthenticationCacheSize());
Assert.assertEquals(usingCore() ? 7 : 0, serverControl.getAuthorizationCacheSize());
ServerLocator loc = createInVMNonHALocator();
ClientSessionFactory csf = createSessionFactory(loc);
ClientSession session = csf.createSession("myUser", "myPass", false, true, false, false, 0);
session.start();
final String address = "ADDRESS";
serverControl.createAddress(address, "MULTICAST");
ClientProducer producer = session.createProducer(address);
ClientMessage m = session.createMessage(true);
m.putStringProperty("hello", "world");
producer.send(m);
Assert.assertEquals(usingCore() ? 2 : 1, serverControl.getAuthenticationCacheSize());
Assert.assertEquals(usingCore() ? 8 : 1, serverControl.getAuthorizationCacheSize());
}
@Test
public void testGetConnectors() throws Exception {
ActiveMQServerControl serverControl = createManagementControl();

View File

@ -768,6 +768,16 @@ public class ActiveMQServerControlUsingCoreTest extends ActiveMQServerControlTes
return 0;
}
@Override
public long getAuthenticationCacheSize() {
return (Long) proxy.retrieveAttributeValue("AuthenticationCacheSize", Long.class);
}
@Override
public long getAuthorizationCacheSize() {
return (Long) proxy.retrieveAttributeValue("AuthorizationCacheSize", Long.class);
}
@Override
public double getDiskStoreUsage() {
try {

View File

@ -51,6 +51,7 @@ import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnection;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
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.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.ActiveMQServers;
import org.apache.activemq.artemis.core.server.Queue;
@ -66,6 +67,7 @@ import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager4;
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
import org.apache.activemq.artemis.tests.util.CreateMessage;
import org.apache.activemq.artemis.utils.CompositeAddress;
import org.apache.activemq.artemis.utils.Wait;
import org.apache.activemq.command.ActiveMQQueue;
import org.junit.Assert;
import org.junit.Before;
@ -1412,6 +1414,9 @@ public class SecurityTest extends ActiveMQTestBase {
securityManager.getConfiguration().addRole("auser", "receiver");
// invalidate the authentication cache so the new role will be picked up
((SecurityStoreImpl)server.getSecurityStore()).invalidateAuthenticationCache();
session.createConsumer(SecurityTest.queueA);
// Removing the Role... the check should be cached, so the next createConsumer shouldn't fail
@ -1483,7 +1488,7 @@ public class SecurityTest extends ActiveMQTestBase {
@Test
public void testSendMessageUpdateSender() throws Exception {
Configuration configuration = createDefaultInVMConfig().setSecurityEnabled(true).setSecurityInvalidationInterval(-1);
Configuration configuration = createDefaultInVMConfig().setSecurityEnabled(true).setSecurityInvalidationInterval(1000);
ActiveMQServer server = createServer(false, configuration);
server.start();
HierarchicalRepository<Set<Role>> securityRepository = server.getSecurityRepository();
@ -1518,7 +1523,14 @@ public class SecurityTest extends ActiveMQTestBase {
securityManager.getConfiguration().addRole("auser", "receiver");
Wait.assertTrue(() -> {
try {
session.createConsumer(SecurityTest.queueA);
return true;
} catch (Exception e) {
return false;
}
}, 2000, 100);
// Removing the Role... the check should be cached... but we used
// setSecurityInvalidationInterval(0), so the

View File

@ -17,6 +17,7 @@
package org.apache.activemq.artemis.tests.integration.stomp;
import javax.security.auth.Subject;
import java.lang.management.ManagementFactory;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
@ -46,13 +47,14 @@ public class StompWithClientIdValidationTest extends StompTestBase {
.setSecurityEnabled(isSecurityEnabled())
.setPersistenceEnabled(isPersistenceEnabled())
.addAcceptorConfiguration("stomp", "tcp://localhost:61613?enabledProtocols=STOMP")
.addAcceptorConfiguration(new TransportConfiguration(InVMAcceptorFactory.class.getName()));
.addAcceptorConfiguration(new TransportConfiguration(InVMAcceptorFactory.class.getName()))
.setSecurityInvalidationInterval(0); // disable caching
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager(InVMLoginModule.class.getName(), new SecurityConfiguration()) {
@Override
public String validateUser(String user, String password, RemotingConnection remotingConnection, String securityDomain) {
public Subject authenticate(String user, String password, RemotingConnection remotingConnection, String securityDomain) {
String validatedUser = super.validateUser(user, password, remotingConnection, securityDomain);
Subject validatedUser = super.authenticate(user, password, remotingConnection, securityDomain);
if (validatedUser == null) {
return null;
}