Security: cache users in PKI realm (elastic/x-pack-elasticsearch#4428)
The PKI realm has never been a caching realm as the need had not presented itself until now. The PKI realm relies on role mappings to map the DN from a certificate to roles so that the users have the appropriate access permissions. Without caching, this role mapping will happen on every request. For file based role mappings, this is not an issue as the mappings are based on equality checks for the DN. However, the design of the API based role mappings allows for more complex matches. These matches are implemented using automata, which are built on every request that needs role mappings. Building automata is an expensive operation and in combination with the PKI realm's lack of caching leads to a significant performance impact. The change in this commit makes the PkiRealm a caching realm using the same pattern as other caching realms. The cache provided by elasticsearch core is used to map the fingerprint of a certificate to the user that was resolved from this certificate. The semantics of modifications to this cache during iteration requires that we use a read-write lock to protect access. There can be multiple concurrent modifications and retrievals but iteration must be protected from any attempts to modify the cache. Additionally, some PKI tests were converted to single node tests as part of this change. One test only used a single node and the other did not require multiple nodes. relates elastic/x-pack-elasticsearch#4406 Original commit: elastic/x-pack-elasticsearch@214772e1c1
This commit is contained in:
parent
cdf41cffd8
commit
91ab88e86e
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Pattern> 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<TimeValue> 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<Integer> 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<Setting<?>> getSettings() {
|
||||
Set<Setting<?>> 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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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)}
|
||||
|
|
|
@ -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<BytesKey, User> 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.<BytesKey, User>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<AuthenticationResult> listener) {
|
||||
X509AuthenticationToken token = (X509AuthenticationToken)authToken;
|
||||
if (isCertificateChainTrusted(trustManager, token, logger) == false) {
|
||||
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<String, Object> metadata = Collections.singletonMap("pki_dn", token.dn());
|
||||
final UserRoleMapper.UserData user = new UserRoleMapper.UserData(token.principal(),
|
||||
final UserRoleMapper.UserData userData = 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
|
||||
));
|
||||
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<User> 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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Class<? extends Plugin>> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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) {
|
||||
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,
|
||||
|
|
|
@ -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<String> roles = new HashSet<>();
|
||||
roles.add("admin");
|
||||
roles.add("kibana_user");
|
||||
assertSuccessfulAuthentiation(roles);
|
||||
assertSuccessfulAuthentication(roles);
|
||||
}
|
||||
|
||||
private void assertSuccessfulAuthentiation(Set<String> roles) throws Exception {
|
||||
private void assertSuccessfulAuthentication(Set<String> 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 {
|
||||
|
|
Loading…
Reference in New Issue