diff --git a/docs/en/security/authentication/user-cache.asciidoc b/docs/en/security/authentication/user-cache.asciidoc index cc02e214c55..ba2b363a843 100644 --- a/docs/en/security/authentication/user-cache.asciidoc +++ b/docs/en/security/authentication/user-cache.asciidoc @@ -6,7 +6,8 @@ remote authentication service or hitting the disk for every incoming request. You can configure characteristics of the user cache with the `cache.ttl`, `cache.max_users`, and `cache.hash_algo` realm settings. -NOTE: PKI realms do not use the user cache. +NOTE: PKI realms do not cache user credentials but do cache the resolved user +object to avoid unnecessarily needing to perform role mapping on each request. The cached user credentials are hashed in memory. By default, {security} uses a salted `sha-256` hash algorithm. You can use a different hashing algorithm by diff --git a/docs/en/settings/security-settings.asciidoc b/docs/en/settings/security-settings.asciidoc index 781e5415c10..eb2299e693d 100644 --- a/docs/en/settings/security-settings.asciidoc +++ b/docs/en/settings/security-settings.asciidoc @@ -640,6 +640,15 @@ Specifies the {xpack-ref}/security-files.html[location] of the {xpack-ref}/mapping-roles.html[YAML role mapping configuration file]. Defaults to `CONFIG_DIR/x-pack/role_mapping.yml`. +`cache.ttl`:: +Specifies the time-to-live for cached user entries. Use the +standard Elasticsearch {ref}/common-options.html#time-units[time units]). +Defaults to `20m`. + +`cache.max_users`:: +Specifies the maximum number of user entries that the cache can contain. +Defaults to `100000`. + [[ref-saml-settings]] [float] ===== SAML Realm Settings diff --git a/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java b/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java index c7a07c3c262..a3539b30d3e 100644 --- a/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java +++ b/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.security.authc.pki; import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.xpack.core.security.authc.support.mapper.CompositeRoleMapperSettings; import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; @@ -18,6 +19,11 @@ public final class PkiRealmSettings { public static final String DEFAULT_USERNAME_PATTERN = "CN=(.*?)(?:,|$)"; public static final Setting USERNAME_PATTERN_SETTING = new Setting<>("username_pattern", DEFAULT_USERNAME_PATTERN, s -> Pattern.compile(s, Pattern.CASE_INSENSITIVE), Setting.Property.NodeScope); + private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20); + public static final Setting CACHE_TTL_SETTING = Setting.timeSetting("cache.ttl", DEFAULT_TTL, Setting.Property.NodeScope); + private static final int DEFAULT_MAX_USERS = 100_000; //100k users + public static final Setting CACHE_MAX_USERS_SETTING = Setting.intSetting("cache.max_users", DEFAULT_MAX_USERS, + Setting.Property.NodeScope); public static final SSLConfigurationSettings SSL_SETTINGS = SSLConfigurationSettings.withoutPrefix(); private PkiRealmSettings() {} @@ -28,6 +34,8 @@ public final class PkiRealmSettings { public static Set> getSettings() { Set> settings = new HashSet<>(); settings.add(USERNAME_PATTERN_SETTING); + settings.add(CACHE_TTL_SETTING); + settings.add(CACHE_MAX_USERS_SETTING); settings.add(SSL_SETTINGS.truststorePath); settings.add(SSL_SETTINGS.truststorePassword); diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/BytesKey.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/BytesKey.java new file mode 100644 index 00000000000..1534b78899f --- /dev/null +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/BytesKey.java @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc; + +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.StringHelper; + +import java.util.Arrays; + +/** + * Simple wrapper around bytes so that it can be used as a cache key. The hashCode is computed + * once upon creation and cached. + */ +public class BytesKey { + + final byte[] bytes; + private final int hashCode; + + public BytesKey(byte[] bytes) { + this.bytes = bytes; + this.hashCode = StringHelper.murmurhash3_x86_32(bytes, 0, bytes.length, StringHelper.GOOD_FAST_HASH_SEED); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + if (other instanceof BytesKey == false) { + return false; + } + + BytesKey otherBytes = (BytesKey) other; + return Arrays.equals(otherBytes.bytes, bytes); + } + + @Override + public String toString() { + return new BytesRef(bytes).toString(); + } +} diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index ef4f1a2f94c..305c6caeba6 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -1165,44 +1165,6 @@ public final class TokenService extends AbstractComponent { } } - /** - * Simple wrapper around bytes so that it can be used as a cache key. The hashCode is computed - * once upon creation and cached. - */ - static class BytesKey { - - final byte[] bytes; - private final int hashCode; - - BytesKey(byte[] bytes) { - this.bytes = bytes; - this.hashCode = StringHelper.murmurhash3_x86_32(bytes, 0, bytes.length, StringHelper.GOOD_FAST_HASH_SEED); - } - - @Override - public int hashCode() { - return hashCode; - } - - @Override - public boolean equals(Object other) { - if (other == null) { - return false; - } - if (other instanceof BytesKey == false) { - return false; - } - - BytesKey otherBytes = (BytesKey) other; - return Arrays.equals(otherBytes.bytes, bytes); - } - - @Override - public String toString() { - return new BytesRef(bytes).toString(); - } - } - /** * Creates a new key unless present that is newer than the current active key and returns the corresponding metadata. Note: * this method doesn't modify the metadata used in this token service. See {@link #refreshMetaData(TokenMetaData)} diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java index 9815e6fae7b..a956351f86e 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java @@ -11,8 +11,12 @@ import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.cache.Cache; +import org.elasticsearch.common.cache.CacheBuilder; +import org.elasticsearch.common.hash.MessageDigests; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ReleasableLock; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.watcher.ResourceWatcherService; @@ -25,32 +29,52 @@ import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.CertUtils; import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; +import org.elasticsearch.xpack.security.authc.BytesKey; +import org.elasticsearch.xpack.security.authc.support.CachingRealm; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.elasticsearch.xpack.security.authc.support.mapper.CompositeRoleMapper; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; import javax.net.ssl.X509TrustManager; +import java.security.MessageDigest; import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class PkiRealm extends Realm { +public class PkiRealm extends Realm implements CachingRealm { public static final String PKI_CERT_HEADER_NAME = "__SECURITY_CLIENT_CERTIFICATE"; // For client based cert validation, the auth type must be specified but UNKNOWN is an acceptable value private static final String AUTH_TYPE = "UNKNOWN"; + // the lock is used in an odd manner; when iterating over the cache we cannot have modifiers other than deletes using + // the iterator but when not iterating we can modify the cache without external locking. When making normal modifications to the cache + // the read lock is obtained so that we can allow concurrent modifications; however when we need to iterate over the keys or values of + // the cache the write lock must obtained to prevent any modifications + private final ReleasableLock readLock; + private final ReleasableLock writeLock; + + { + final ReadWriteLock iterationLock = new ReentrantReadWriteLock(); + readLock = new ReleasableLock(iterationLock.readLock()); + writeLock = new ReleasableLock(iterationLock.writeLock()); + } + private final X509TrustManager trustManager; private final Pattern principalPattern; private final UserRoleMapper roleMapper; - + private final Cache cache; public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, NativeRoleMappingStore nativeRoleMappingStore) { this(config, new CompositeRoleMapper(PkiRealmSettings.TYPE, config, watcherService, nativeRoleMappingStore)); @@ -62,6 +86,10 @@ public class PkiRealm extends Realm { this.trustManager = trustManagers(config); this.principalPattern = PkiRealmSettings.USERNAME_PATTERN_SETTING.get(config.settings()); this.roleMapper = roleMapper; + this.cache = CacheBuilder.builder() + .setExpireAfterWrite(PkiRealmSettings.CACHE_TTL_SETTING.get(config.settings())) + .setMaximumWeight(PkiRealmSettings.CACHE_MAX_USERS_SETTING.get(config.settings())) + .build(); } @Override @@ -77,18 +105,28 @@ public class PkiRealm extends Realm { @Override public void authenticate(AuthenticationToken authToken, ActionListener listener) { X509AuthenticationToken token = (X509AuthenticationToken)authToken; - if (isCertificateChainTrusted(trustManager, token, logger) == false) { - listener.onResponse(AuthenticationResult.unsuccessful("Certificate for " + token.dn() + " is not trusted", null)); - } else { - final Map metadata = Collections.singletonMap("pki_dn", token.dn()); - final UserRoleMapper.UserData user = new UserRoleMapper.UserData(token.principal(), - token.dn(), Collections.emptySet(), metadata, this.config); - roleMapper.resolveRoles(user, ActionListener.wrap( - roles -> listener.onResponse(AuthenticationResult.success( - new User(token.principal(), roles.toArray(new String[roles.size()]), null, null, metadata, true) - )), - listener::onFailure - )); + try { + final BytesKey fingerprint = computeFingerprint(token.credentials()[0]); + User user = cache.get(fingerprint); + if (user != null) { + listener.onResponse(AuthenticationResult.success(user)); + } else if (isCertificateChainTrusted(trustManager, token, logger) == false) { + listener.onResponse(AuthenticationResult.unsuccessful("Certificate for " + token.dn() + " is not trusted", null)); + } else { + final Map metadata = Collections.singletonMap("pki_dn", token.dn()); + final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(token.principal(), + token.dn(), Collections.emptySet(), metadata, this.config); + roleMapper.resolveRoles(userData, ActionListener.wrap(roles -> { + final User computedUser = + new User(token.principal(), roles.toArray(new String[roles.size()]), null, null, metadata, true); + try (ReleasableLock ignored = readLock.acquire()) { + cache.put(fingerprint, computedUser); + } + listener.onResponse(AuthenticationResult.success(computedUser)); + }, listener::onFailure)); + } + } catch (CertificateEncodingException e) { + listener.onResponse(AuthenticationResult.unsuccessful("Certificate for " + token.dn() + " has encoding issues", e)); } } @@ -196,4 +234,29 @@ public class PkiRealm extends Realm { } } + @Override + public void expire(String username) { + try (ReleasableLock ignored = writeLock.acquire()) { + Iterator userIterator = cache.values().iterator(); + while (userIterator.hasNext()) { + if (userIterator.next().principal().equals(username)) { + userIterator.remove(); + // do not break since there is no guarantee username is unique in this realm + } + } + } + } + + @Override + public void expireAll() { + try (ReleasableLock ignored = readLock.acquire()) { + cache.invalidateAll(); + } + } + + private static BytesKey computeFingerprint(X509Certificate certificate) throws CertificateEncodingException { + MessageDigest digest = MessageDigests.sha256(); + digest.update(certificate.getEncoded()); + return new BytesKey(digest.digest()); + } } diff --git a/plugin/security/src/test/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java b/plugin/security/src/test/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java index de0bf7ea678..1ee654c0baf 100644 --- a/plugin/security/src/test/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java +++ b/plugin/security/src/test/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; import org.elasticsearch.client.Client; import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.SecureString; @@ -167,6 +168,12 @@ public abstract class SecuritySingleNodeTestCase extends ESSingleNodeTestCase { return builder.build(); } + protected Settings transportClientSettings() { + return Settings.builder() + .put(customSecuritySettingsSource.transportClientSettings()) + .build(); + } + @Override protected Collection> getPlugins() { return customSecuritySettingsSource.nodePlugins(); @@ -271,14 +278,19 @@ public abstract class SecuritySingleNodeTestCase extends ESSingleNodeTestCase { return getRestClient(client()); } + protected RestClient createRestClient(RestClientBuilder.HttpClientConfigCallback httpClientConfigCallback, String protocol) { + return createRestClient(client(), httpClientConfigCallback, protocol); + } + private static synchronized RestClient getRestClient(Client client) { if (restClient == null) { - restClient = createRestClient(client); + restClient = createRestClient(client, null, "http"); } return restClient; } - private static RestClient createRestClient(Client client) { + private static RestClient createRestClient(Client client, RestClientBuilder.HttpClientConfigCallback httpClientConfigCallback, + String protocol) { NodesInfoResponse nodesInfoResponse = client.admin().cluster().prepareNodesInfo().get(); assertFalse(nodesInfoResponse.hasFailures()); assertEquals(nodesInfoResponse.getNodes().size(), 1); @@ -286,7 +298,11 @@ public abstract class SecuritySingleNodeTestCase extends ESSingleNodeTestCase { assertNotNull(node.getHttp()); TransportAddress publishAddress = node.getHttp().address().publishAddress(); InetSocketAddress address = publishAddress.address(); - final HttpHost host = new HttpHost(NetworkAddress.format(address.getAddress()), address.getPort(), "http"); - return RestClient.builder(host).build(); + final HttpHost host = new HttpHost(NetworkAddress.format(address.getAddress()), address.getPort(), protocol); + RestClientBuilder builder = RestClient.builder(host); + if (httpClientConfigCallback != null) { + builder.setHttpClientConfigCallback(httpClientConfigCallback); + } + return builder.build(); } } diff --git a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index 898491bdcd9..9b401873941 100644 --- a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -51,7 +51,6 @@ import org.elasticsearch.xpack.core.security.authc.TokenMetaData; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.watcher.watch.ClockMock; import org.elasticsearch.xpack.security.SecurityLifecycleService; -import org.elasticsearch.xpack.security.authc.TokenService.BytesKey; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; diff --git a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthenticationTests.java b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthenticationTests.java index 30c3686efaf..e64a06d435f 100644 --- a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthenticationTests.java +++ b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthenticationTests.java @@ -19,9 +19,8 @@ import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.http.HttpServerTransport; -import org.elasticsearch.test.ESIntegTestCase.ClusterScope; -import org.elasticsearch.test.SecurityIntegTestCase; import org.elasticsearch.test.SecuritySettingsSource; +import org.elasticsearch.test.SecuritySingleNodeTestCase; import org.elasticsearch.transport.Transport; import org.elasticsearch.xpack.core.TestXPackTransportClient; import org.elasticsearch.xpack.core.common.socket.SocketAccess; @@ -50,15 +49,14 @@ import static org.hamcrest.Matchers.is; /** * Test authentication via PKI on both REST and Transport layers */ -@ClusterScope(numClientNodes = 0, supportsDedicatedMasters = false, numDataNodes = 1) -public class PkiAuthenticationTests extends SecurityIntegTestCase { +public class PkiAuthenticationTests extends SecuritySingleNodeTestCase { @Override - protected Settings nodeSettings(int nodeOrdinal) { + protected Settings nodeSettings() { SSLClientAuth sslClientAuth = randomBoolean() ? SSLClientAuth.REQUIRED : SSLClientAuth.OPTIONAL; Settings.Builder builder = Settings.builder() - .put(super.nodeSettings(nodeOrdinal)) + .put(super.nodeSettings()) .put(NetworkModule.HTTP_ENABLED.getKey(), true) .put("xpack.security.http.ssl.enabled", true) .put("xpack.security.http.ssl.client_authentication", sslClientAuth) @@ -80,11 +78,18 @@ public class PkiAuthenticationTests extends SecurityIntegTestCase { return true; } + @Override + protected boolean enableWarningsCheck() { + // the transport client uses deprecated SSL settings since we do not know what to do about + // secure settings for the transport client + return false; + } + public void testTransportClientCanAuthenticateViaPki() { Settings.Builder builder = Settings.builder(); addSSLSettingsForStore(builder, "/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks", "testnode"); try (TransportClient client = createTransportClient(builder.build())) { - client.addTransportAddress(randomFrom(internalCluster().getInstance(Transport.class).boundAddress().boundAddresses())); + client.addTransportAddress(randomFrom(node().injector().getInstance(Transport.class).boundAddress().boundAddresses())); IndexResponse response = client.prepareIndex("foo", "bar").setSource("pki", "auth").get(); assertEquals(DocWriteResponse.Result.CREATED, response.getResult()); } @@ -96,7 +101,7 @@ public class PkiAuthenticationTests extends SecurityIntegTestCase { */ public void testTransportClientAuthenticationFailure() { try (TransportClient client = createTransportClient(Settings.EMPTY)) { - client.addTransportAddress(randomFrom(internalCluster().getInstance(Transport.class).boundAddress().boundAddresses())); + client.addTransportAddress(randomFrom(node().injector().getInstance(Transport.class).boundAddress().boundAddresses())); client.prepareIndex("foo", "bar").setSource("pki", "auth").get(); fail("transport client should not have been able to authenticate"); } catch (NoNodeAvailableException e) { @@ -153,14 +158,14 @@ public class PkiAuthenticationTests extends SecurityIntegTestCase { Settings.Builder builder = Settings.builder().put(clientSettings, false) .put(additionalSettings) - .put("cluster.name", internalCluster().getClusterName()); + .put("cluster.name", node().settings().get("cluster.name")); builder.remove(SecurityField.USER_SETTING.getKey()); builder.remove("request.headers.Authorization"); return new TestXPackTransportClient(builder.build(), LocalStateSecurity.class); } private String getNodeUrl() { - TransportAddress transportAddress = randomFrom(internalCluster().getInstance(HttpServerTransport.class) + TransportAddress transportAddress = randomFrom(node().injector().getInstance(HttpServerTransport.class) .boundAddress().boundAddresses()); final InetSocketAddress inetSocketAddress = transportAddress.address(); return String.format(Locale.ROOT, "https://%s/", NetworkAddress.format(inetSocketAddress)); diff --git a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiOptionalClientAuthTests.java b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiOptionalClientAuthTests.java index 305fad85cf5..720ab17aedb 100644 --- a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiOptionalClientAuthTests.java +++ b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiOptionalClientAuthTests.java @@ -13,9 +13,9 @@ import org.elasticsearch.client.RestClient; import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.test.SecurityIntegTestCase; import org.elasticsearch.test.SecuritySettingsSource; import org.elasticsearch.test.SecuritySettingsSourceField; +import org.elasticsearch.test.SecuritySingleNodeTestCase; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.ssl.SSLClientAuth; import org.junit.BeforeClass; @@ -31,7 +31,7 @@ import java.security.SecureRandom; import static org.hamcrest.Matchers.is; -public class PkiOptionalClientAuthTests extends SecurityIntegTestCase { +public class PkiOptionalClientAuthTests extends SecuritySingleNodeTestCase { private static int randomClientPort; @@ -41,11 +41,11 @@ public class PkiOptionalClientAuthTests extends SecurityIntegTestCase { } @Override - protected Settings nodeSettings(int nodeOrdinal) { + protected Settings nodeSettings() { String randomClientPortRange = randomClientPort + "-" + (randomClientPort+100); Settings.Builder builder = Settings.builder() - .put(super.nodeSettings(nodeOrdinal)) + .put(super.nodeSettings()) .put(NetworkModule.HTTP_ENABLED.getKey(), true) .put("xpack.security.http.ssl.enabled", true) .put("xpack.security.http.ssl.client_authentication", SSLClientAuth.OPTIONAL) @@ -74,12 +74,8 @@ public class PkiOptionalClientAuthTests extends SecurityIntegTestCase { public void testRestClientWithoutClientCertificate() throws Exception { SSLIOSessionStrategy sessionStrategy = new SSLIOSessionStrategy(getSSLContext()); try (RestClient restClient = createRestClient(httpClientBuilder -> httpClientBuilder.setSSLStrategy(sessionStrategy), "https")) { - try { - restClient.performRequest("GET", "_nodes"); - fail("request should have failed"); - } catch(ResponseException e) { - assertThat(e.getResponse().getStatusLine().getStatusCode(), is(401)); - } + ResponseException e = expectThrows(ResponseException.class, () -> restClient.performRequest("GET", "_nodes")); + assertThat(e.getResponse().getStatusLine().getStatusCode(), is(401)); Response response = restClient.performRequest("GET", "_nodes", new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, diff --git a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java index 82b6faf5059..74f6598f8dd 100644 --- a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java +++ b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java @@ -48,6 +48,8 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class PkiRealmTests extends ESTestCase { @@ -84,18 +86,19 @@ public class PkiRealmTests extends ESTestCase { } public void testAuthenticateBasedOnCertToken() throws Exception { - assertSuccessfulAuthentiation(Collections.emptySet()); + assertSuccessfulAuthentication(Collections.emptySet()); } public void testAuthenticateWithRoleMapping() throws Exception { final Set roles = new HashSet<>(); roles.add("admin"); roles.add("kibana_user"); - assertSuccessfulAuthentiation(roles); + assertSuccessfulAuthentication(roles); } - private void assertSuccessfulAuthentiation(Set roles) throws Exception { + private void assertSuccessfulAuthentication(Set roles) throws Exception { String dn = "CN=Elasticsearch Test Node,"; + final String expectedUsername = "Elasticsearch Test Node"; X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); X509AuthenticationToken token = new X509AuthenticationToken(new X509Certificate[] { certificate }, "Elasticsearch Test Node", dn); UserRoleMapper roleMapper = mock(UserRoleMapper.class); @@ -118,10 +121,29 @@ public class PkiRealmTests extends ESTestCase { assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); User user = result.getUser(); assertThat(user, is(notNullValue())); - assertThat(user.principal(), is("Elasticsearch Test Node")); + assertThat(user.principal(), is(expectedUsername)); assertThat(user.roles(), is(notNullValue())); assertThat(user.roles().length, is(roles.size())); assertThat(user.roles(), arrayContainingInAnyOrder(roles.toArray())); + + final boolean testCaching = randomBoolean(); + final boolean invalidate = testCaching && randomBoolean(); + if (testCaching) { + if (invalidate) { + if (randomBoolean()) { + realm.expireAll(); + } else { + realm.expire(expectedUsername); + } + } + future = new PlainActionFuture<>(); + realm.authenticate(token, future); + assertEquals(AuthenticationResult.Status.SUCCESS, future.actionGet().getStatus()); + assertEquals(user, future.actionGet().getUser()); + } + + final int numTimes = invalidate ? 2 : 1; + verify(roleMapper, times(numTimes)).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); } public void testCustomUsernamePattern() throws Exception {