Load bootstrap elastic user password from keystore (elastic/x-pack-elasticsearch#1942)

This is related to elastic/x-pack-elasticsearch#1217. This commit adds a ClusterStateListener at
node startup. Once the cluster and security index are ready, this
listener will attempt to set the elastic user's password with the
bootstrap password pulled from the keystore. If the password is not in
the keystore or the elastic password has already been set, nothing will
be done.

Original commit: elastic/x-pack-elasticsearch@7fc4943c45
This commit is contained in:
Tim Brooks 2017-07-10 11:15:39 -05:00 committed by GitHub
parent 31b02c3941
commit f9eabcdf08
6 changed files with 575 additions and 9 deletions

View File

@ -128,8 +128,8 @@ import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore;
import org.elasticsearch.xpack.security.authz.store.FileRolesStore;
import org.elasticsearch.xpack.security.authz.store.NativeRolesStore;
import org.elasticsearch.xpack.security.authz.store.ReservedRolesStore;
import org.elasticsearch.xpack.security.bootstrap.BootstrapElasticPassword;
import org.elasticsearch.xpack.security.bootstrap.ContainerPasswordBootstrapCheck;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.xpack.security.rest.SecurityRestFilter;
import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction;
import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction;
@ -180,7 +180,6 @@ import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.elasticsearch.common.settings.Setting.groupSetting;
import static org.elasticsearch.xpack.XPackSettings.HTTP_SSL_ENABLED;
public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
@ -386,6 +385,11 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
DestructiveOperations destructiveOperations = new DestructiveOperations(settings, clusterService.getClusterSettings());
securityInterceptor.set(new SecurityServerTransportInterceptor(settings, threadPool, authcService.get(), authzService, licenseState,
sslService, securityContext.get(), destructiveOperations));
BootstrapElasticPassword bootstrapElasticPassword = new BootstrapElasticPassword(settings, logger, clusterService, reservedRealm,
securityLifecycleService);
bootstrapElasticPassword.initiatePasswordBootstrap();
return components;
}

View File

@ -39,18 +39,23 @@ public class ChangePasswordRequestBuilder
return this;
}
public static char[] validateAndHashPassword(SecureString password) {
Validation.Error error = Validation.Users.validatePassword(password.getChars());
if (error != null) {
ValidationException validationException = new ValidationException();
validationException.addValidationError(error.toString());
throw validationException;
}
return Hasher.BCRYPT.hash(password);
}
/**
* Sets the password. Note: the char[] passed to this method will be cleared.
*/
public ChangePasswordRequestBuilder password(char[] password) {
try (SecureString secureString = new SecureString(password)) {
Validation.Error error = Validation.Users.validatePassword(password);
if (error != null) {
ValidationException validationException = new ValidationException();
validationException.addValidationError(error.toString());
throw validationException;
}
request.passwordHash(Hasher.BCRYPT.hash(secureString));
char[] hash = validateAndHashPassword(secureString);
request.passwordHash(hash);
}
return this;
}

View File

@ -9,14 +9,23 @@ import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.ClusterChangedEvent;
import org.elasticsearch.cluster.ClusterStateListener;
import org.elasticsearch.common.settings.SecureSetting;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.Environment;
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.xpack.XPackSettings;
import org.elasticsearch.xpack.security.InternalClient;
import org.elasticsearch.xpack.security.Security;
import org.elasticsearch.xpack.security.SecurityLifecycleService;
import org.elasticsearch.xpack.security.action.user.ChangePasswordAction;
import org.elasticsearch.xpack.security.action.user.ChangePasswordRequest;
import org.elasticsearch.xpack.security.action.user.ChangePasswordResponse;
import org.elasticsearch.xpack.security.authc.IncomingRequest;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ReservedUserInfo;
@ -39,6 +48,8 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.locks.ReentrantLock;
/**
* A realm for predefined users. These users can only be modified in terms of changing their passwords; no other modifications are allowed.
@ -58,6 +69,7 @@ public class ReservedRealm extends CachingUsernamePasswordRealm {
public static final Setting<Boolean> ACCEPT_DEFAULT_PASSWORD_SETTING = Setting.boolSetting(
Security.setting("authc.accept_default_password"), true, Setting.Property.NodeScope, Setting.Property.Filtered,
Setting.Property.Deprecated);
public static final Setting<SecureString> BOOTSTRAP_ELASTIC_PASSWORD = SecureSetting.secureString("es.bootstrap.passwd.elastic", null);
private final NativeUsersStore nativeUsersStore;
private final AnonymousUser anonymousUser;
@ -186,6 +198,31 @@ public class ReservedRealm extends CachingUsernamePasswordRealm {
}
}
public synchronized void bootstrapElasticUserCredentials(SecureString passwordHash, ActionListener<Boolean> listener) {
getUserInfo(ElasticUser.NAME, new ActionListener<ReservedUserInfo>() {
@Override
public void onResponse(ReservedUserInfo reservedUserInfo) {
if (reservedUserInfo == null) {
listener.onFailure(new IllegalStateException("unexpected state: ReservedUserInfo was null"));
} else if (reservedUserInfo.hasEmptyPassword) {
ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest();
changePasswordRequest.username(ElasticUser.NAME);
changePasswordRequest.passwordHash(passwordHash.getChars());
nativeUsersStore.changePassword(changePasswordRequest,
ActionListener.wrap(v -> listener.onResponse(true), listener::onFailure));
} else {
listener.onResponse(false);
}
}
@Override
public void onFailure(Exception e) {
listener.onFailure(e);
}
});
}
private User getUser(String username, ReservedUserInfo userInfo) {
assert username != null;
switch (username) {
@ -277,5 +314,6 @@ public class ReservedRealm extends CachingUsernamePasswordRealm {
public static void addSettings(List<Setting<?>> settingsList) {
settingsList.add(ACCEPT_DEFAULT_PASSWORD_SETTING);
settingsList.add(BOOTSTRAP_ELASTIC_PASSWORD);
}
}

View File

@ -0,0 +1,122 @@
/*
* 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.bootstrap;
import org.apache.logging.log4j.Logger;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.SetOnce;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.ClusterChangedEvent;
import org.elasticsearch.cluster.ClusterStateListener;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.xpack.XPackSettings;
import org.elasticsearch.xpack.security.SecurityLifecycleService;
import org.elasticsearch.xpack.security.action.user.ChangePasswordRequestBuilder;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
import java.util.concurrent.Semaphore;
/**
* This process adds a ClusterStateListener to the ClusterService that will listen for cluster state updates.
* Once the cluster and the security index are ready, it will attempt to bootstrap the elastic user's
* password with a password from the keystore. If the password is not in the keystore or the elastic user
* already has a password, then the user's password will not be set. Once the process is complete, the
* listener will remove itself.
*/
public final class BootstrapElasticPassword {
private final Settings settings;
private final Logger logger;
private final ClusterService clusterService;
private final ReservedRealm reservedRealm;
private final SecurityLifecycleService lifecycleService;
private final boolean reservedRealmDisabled;
public BootstrapElasticPassword(Settings settings, Logger logger, ClusterService clusterService, ReservedRealm reservedRealm,
SecurityLifecycleService lifecycleService) {
this.reservedRealmDisabled = XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings) == false;
this.settings = settings;
this.logger = logger;
this.clusterService = clusterService;
this.reservedRealm = reservedRealm;
this.lifecycleService = lifecycleService;
}
public void initiatePasswordBootstrap() {
SecureString bootstrapPassword = ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.get(settings);
if (bootstrapPassword.length() == 0) {
return;
} else if (reservedRealmDisabled) {
logger.warn("elastic password will not be bootstrapped because the reserved realm is disabled");
bootstrapPassword.close();
return;
}
SecureString passwordHash = new SecureString(ChangePasswordRequestBuilder.validateAndHashPassword(bootstrapPassword));
bootstrapPassword.close();
clusterService.addListener(new BootstrapPasswordClusterStateListener(passwordHash));
}
private class BootstrapPasswordClusterStateListener implements ClusterStateListener {
private final Semaphore semaphore = new Semaphore(1);
private final SecureString passwordHash;
private final SetOnce<Boolean> isDone = new SetOnce<>();
private BootstrapPasswordClusterStateListener(SecureString passwordHash) {
this.passwordHash = passwordHash;
}
@Override
public void clusterChanged(ClusterChangedEvent event) {
if (event.state().blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)
|| lifecycleService.isSecurityIndexOutOfDate()
|| (lifecycleService.isSecurityIndexExisting() && lifecycleService.isSecurityIndexAvailable() == false)
|| lifecycleService.isSecurityIndexWriteable() == false) {
// We hold off bootstrapping until the node recovery is complete, the security index is up to date, and
// security index is writeable. If the security index currently exists, it must also be available.
return;
}
// Only allow one attempt to bootstrap the password at a time
if (semaphore.tryAcquire()) {
// Ensure that we do not attempt to bootstrap after the process is complete. This is important as we
// clear the password hash in the cleanup phase.
if (isDone.get() != null) {
semaphore.release();
return;
}
reservedRealm.bootstrapElasticUserCredentials(passwordHash, new ActionListener<Boolean>() {
@Override
public void onResponse(Boolean passwordSet) {
cleanup();
if (passwordSet == false) {
logger.warn("elastic password was not bootstrapped because its password was already set");
}
semaphore.release();
}
@Override
public void onFailure(Exception e) {
cleanup();
logger.error("unexpected exception when attempting to bootstrap password", e);
semaphore.release();
}
});
}
}
private void cleanup() {
isDone.set(true);
IOUtils.closeWhileHandlingException(() -> clusterService.removeListener(this), passwordHash);
}
}
}

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.authc.esnative;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionFuture;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.common.SuppressForbidden;
@ -17,6 +18,7 @@ import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.XPackSettings;
import org.elasticsearch.xpack.security.SecurityLifecycleService;
import org.elasticsearch.xpack.security.action.user.ChangePasswordRequest;
import org.elasticsearch.xpack.security.authc.IncomingRequest;
import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ReservedUserInfo;
import org.elasticsearch.xpack.security.authc.support.Hasher;
@ -373,6 +375,64 @@ public class ReservedRealmTests extends ESTestCase {
assertThat(e.getMessage(), containsString("failed to authenticate"));
}
@SuppressWarnings("unchecked")
public void testBootstrapElasticPassword() {
ReservedUserInfo user = new ReservedUserInfo(ReservedRealm.EMPTY_PASSWORD_HASH, true, true);
mockGetAllReservedUserInfo(usersStore, Collections.singletonMap(ElasticUser.NAME, user));
Settings settings = Settings.builder().build();
when(securityLifecycleService.isSecurityIndexExisting()).thenReturn(true);
final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore,
new AnonymousUser(Settings.EMPTY), securityLifecycleService, new ThreadContext(Settings.EMPTY));
PlainActionFuture<Boolean> listenerFuture = new PlainActionFuture<>();
SecureString passwordHash = new SecureString(randomAlphaOfLength(10).toCharArray());
reservedRealm.bootstrapElasticUserCredentials(passwordHash, listenerFuture);
ArgumentCaptor<ChangePasswordRequest> requestCaptor = ArgumentCaptor.forClass(ChangePasswordRequest.class);
ArgumentCaptor<ActionListener> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class);
verify(usersStore).changePassword(requestCaptor.capture(), listenerCaptor.capture());
assertEquals(passwordHash.getChars(), requestCaptor.getValue().passwordHash());
listenerCaptor.getValue().onResponse(null);
assertTrue(listenerFuture.actionGet());
}
public void testBootstrapElasticPasswordNotSetIfPasswordExists() {
mockGetAllReservedUserInfo(usersStore, Collections.singletonMap(ElasticUser.NAME, new ReservedUserInfo(new char[7], true, false)));
when(securityLifecycleService.isSecurityIndexExisting()).thenReturn(true);
Settings settings = Settings.builder().build();
final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore,
new AnonymousUser(Settings.EMPTY), securityLifecycleService, new ThreadContext(Settings.EMPTY));
SecureString passwordHash = new SecureString(randomAlphaOfLength(10).toCharArray());
reservedRealm.bootstrapElasticUserCredentials(passwordHash, new PlainActionFuture<>());
verify(usersStore, times(0)).changePassword(any(ChangePasswordRequest.class), any());
}
public void testBootstrapElasticPasswordSettingFails() {
ReservedUserInfo user = new ReservedUserInfo(ReservedRealm.EMPTY_PASSWORD_HASH, true, true);
mockGetAllReservedUserInfo(usersStore, Collections.singletonMap(ElasticUser.NAME, user));
Settings settings = Settings.builder().build();
when(securityLifecycleService.isSecurityIndexExisting()).thenReturn(true);
final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore,
new AnonymousUser(Settings.EMPTY), securityLifecycleService, new ThreadContext(Settings.EMPTY));
PlainActionFuture<Boolean> listenerFuture = new PlainActionFuture<>();
SecureString passwordHash = new SecureString(randomAlphaOfLength(10).toCharArray());
reservedRealm.bootstrapElasticUserCredentials(passwordHash, listenerFuture);
ArgumentCaptor<ChangePasswordRequest> requestCaptor = ArgumentCaptor.forClass(ChangePasswordRequest.class);
ArgumentCaptor<ActionListener> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class);
verify(usersStore).changePassword(requestCaptor.capture(), listenerCaptor.capture());
assertEquals(passwordHash.getChars(), requestCaptor.getValue().passwordHash());
listenerCaptor.getValue().onFailure(new RuntimeException());
expectThrows(RuntimeException.class, listenerFuture::actionGet);
}
/*
* NativeUserStore#getAllReservedUserInfo is pkg private we can't mock it otherwise
*/

View File

@ -0,0 +1,337 @@
/*
* 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.bootstrap;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.ClusterChangedEvent;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.ClusterStateListener;
import org.elasticsearch.cluster.block.ClusterBlocks;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.ValidationException;
import org.elasticsearch.common.settings.MockSecureSettings;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.XPackSettings;
import org.elasticsearch.xpack.security.SecurityLifecycleService;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
import org.elasticsearch.xpack.security.authc.support.Hasher;
import org.junit.Before;
import org.mockito.ArgumentCaptor;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
@SuppressWarnings("unchecked")
public class BootstrapElasticPasswordTests extends ESTestCase {
private ClusterService clusterService;
private ReservedRealm realm;
private SecurityLifecycleService lifecycle;
private ArgumentCaptor<ClusterStateListener> listenerCaptor;
private ArgumentCaptor<ActionListener> actionLister;
@Before
public void setupBootstrap() {
clusterService = mock(ClusterService.class);
realm = mock(ReservedRealm.class);
lifecycle = mock(SecurityLifecycleService.class);
listenerCaptor = ArgumentCaptor.forClass(ClusterStateListener.class);
actionLister = ArgumentCaptor.forClass(ActionListener.class);
}
public void testNoListenerAttachedWhenNoBootstrapPassword() {
BootstrapElasticPassword bootstrap = new BootstrapElasticPassword(Settings.EMPTY, logger, clusterService, realm, lifecycle);
bootstrap.initiatePasswordBootstrap();
verifyZeroInteractions(clusterService);
}
public void testNoListenerAttachedWhenReservedRealmDisabled() {
MockSecureSettings secureSettings = new MockSecureSettings();
secureSettings.setString(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(), randomAlphaOfLength(10));
Settings settings = Settings.builder()
.setSecureSettings(secureSettings)
.put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false)
.build();
BootstrapElasticPassword bootstrap = new BootstrapElasticPassword(settings, logger, clusterService, realm, lifecycle);
bootstrap.initiatePasswordBootstrap();
verifyZeroInteractions(clusterService);
}
public void testPasswordHasToBeValid() {
MockSecureSettings secureSettings = new MockSecureSettings();
secureSettings.setString(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(), randomAlphaOfLength(5));
Settings settings = Settings.builder()
.setSecureSettings(secureSettings)
.build();
BootstrapElasticPassword bootstrap = new BootstrapElasticPassword(settings, logger, clusterService, realm, lifecycle);
expectThrows(ValidationException.class, bootstrap::initiatePasswordBootstrap);
verifyZeroInteractions(clusterService);
}
public void testDoesNotBootstrapUntilStateRecovered() {
MockSecureSettings secureSettings = new MockSecureSettings();
secureSettings.setString(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(), randomAlphaOfLength(10));
Settings settings = Settings.builder()
.setSecureSettings(secureSettings)
.build();
BootstrapElasticPassword bootstrap = new BootstrapElasticPassword(settings, logger, clusterService, realm, lifecycle);
bootstrap.initiatePasswordBootstrap();
verify(clusterService).addListener(listenerCaptor.capture());
ClusterStateListener listener = listenerCaptor.getValue();
ClusterChangedEvent event = mock(ClusterChangedEvent.class);
ClusterState state = mock(ClusterState.class);
ClusterBlocks blocks = mock(ClusterBlocks.class);
when(event.state()).thenReturn(state);
when(state.blocks()).thenReturn(blocks);
when(blocks.hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)).thenReturn(true);
when(lifecycle.isSecurityIndexOutOfDate()).thenReturn(false);
when(lifecycle.isSecurityIndexWriteable()).thenReturn(true);
listener.clusterChanged(event);
verifyZeroInteractions(realm);
}
public void testDoesNotBootstrapUntilSecurityIndexUpdated() {
MockSecureSettings secureSettings = new MockSecureSettings();
secureSettings.setString(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(), randomAlphaOfLength(10));
Settings settings = Settings.builder()
.setSecureSettings(secureSettings)
.build();
BootstrapElasticPassword bootstrap = new BootstrapElasticPassword(settings, logger, clusterService, realm, lifecycle);
bootstrap.initiatePasswordBootstrap();
verify(clusterService).addListener(listenerCaptor.capture());
ClusterStateListener listener = listenerCaptor.getValue();
ClusterChangedEvent event = getStateRecoveredEvent();
when(lifecycle.isSecurityIndexOutOfDate()).thenReturn(true);
when(lifecycle.isSecurityIndexWriteable()).thenReturn(true);
when(lifecycle.isSecurityIndexExisting()).thenReturn(true);
when(lifecycle.isSecurityIndexAvailable()).thenReturn(true);
listener.clusterChanged(event);
verifyZeroInteractions(realm);
}
public void testDoesNotBootstrapUntilSecurityIndexIfExistingIsAvailable() {
MockSecureSettings secureSettings = new MockSecureSettings();
secureSettings.setString(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(), randomAlphaOfLength(10));
Settings settings = Settings.builder()
.setSecureSettings(secureSettings)
.build();
BootstrapElasticPassword bootstrap = new BootstrapElasticPassword(settings, logger, clusterService, realm, lifecycle);
bootstrap.initiatePasswordBootstrap();
verify(clusterService).addListener(listenerCaptor.capture());
ClusterStateListener listener = listenerCaptor.getValue();
ClusterChangedEvent event = getStateRecoveredEvent();
when(lifecycle.isSecurityIndexOutOfDate()).thenReturn(false);
when(lifecycle.isSecurityIndexWriteable()).thenReturn(true);
when(lifecycle.isSecurityIndexExisting()).thenReturn(true);
when(lifecycle.isSecurityIndexAvailable()).thenReturn(false);
listener.clusterChanged(event);
verifyZeroInteractions(realm);
}
public void testDoesNotBootstrapUntilSecurityIndexWriteable() {
MockSecureSettings secureSettings = new MockSecureSettings();
secureSettings.setString(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(), randomAlphaOfLength(10));
Settings settings = Settings.builder()
.setSecureSettings(secureSettings)
.build();
BootstrapElasticPassword bootstrap = new BootstrapElasticPassword(settings, logger, clusterService, realm, lifecycle);
bootstrap.initiatePasswordBootstrap();
verify(clusterService).addListener(listenerCaptor.capture());
ClusterStateListener listener = listenerCaptor.getValue();
ClusterChangedEvent event = getStateRecoveredEvent();
when(lifecycle.isSecurityIndexOutOfDate()).thenReturn(false);
when(lifecycle.isSecurityIndexWriteable()).thenReturn(false);
when(lifecycle.isSecurityIndexExisting()).thenReturn(true);
when(lifecycle.isSecurityIndexAvailable()).thenReturn(true);
listener.clusterChanged(event);
verifyZeroInteractions(realm);
}
public void testDoesAllowBootstrapForUnavailableIndexIfNotExisting() {
MockSecureSettings secureSettings = new MockSecureSettings();
secureSettings.setString(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(), randomAlphaOfLength(10));
Settings settings = Settings.builder()
.setSecureSettings(secureSettings)
.build();
BootstrapElasticPassword bootstrap = new BootstrapElasticPassword(settings, logger, clusterService, realm, lifecycle);
bootstrap.initiatePasswordBootstrap();
verify(clusterService).addListener(listenerCaptor.capture());
ClusterStateListener listener = listenerCaptor.getValue();
ClusterChangedEvent event = getStateRecoveredEvent();
when(lifecycle.isSecurityIndexOutOfDate()).thenReturn(false);
when(lifecycle.isSecurityIndexWriteable()).thenReturn(true);
when(lifecycle.isSecurityIndexExisting()).thenReturn(false);
when(lifecycle.isSecurityIndexAvailable()).thenReturn(false);
listener.clusterChanged(event);
verify(realm).bootstrapElasticUserCredentials(any(SecureString.class), any(ActionListener.class));
}
public void testDoesNotBootstrapBeginsWhenRecoveryDoneAndIndexReady() {
String password = randomAlphaOfLength(10);
ensureBootstrapStarted(password);
ArgumentCaptor<SecureString> hashedPasswordCaptor = ArgumentCaptor.forClass(SecureString.class);
verify(realm).bootstrapElasticUserCredentials(hashedPasswordCaptor.capture(), any(ActionListener.class));
assertTrue(Hasher.BCRYPT.verify(new SecureString(password.toCharArray()), hashedPasswordCaptor.getValue().getChars()));
}
public void testWillNotAllowTwoConcurrentBootstrapAttempts() {
String password = randomAlphaOfLength(10);
MockSecureSettings secureSettings = new MockSecureSettings();
secureSettings.setString(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(), password);
Settings settings = Settings.builder()
.setSecureSettings(secureSettings)
.build();
BootstrapElasticPassword bootstrap = new BootstrapElasticPassword(settings, logger, clusterService, realm, lifecycle);
bootstrap.initiatePasswordBootstrap();
verify(clusterService).addListener(listenerCaptor.capture());
ClusterStateListener listener = listenerCaptor.getValue();
ClusterChangedEvent event = getStateRecoveredEvent();
when(lifecycle.isSecurityIndexOutOfDate()).thenReturn(false);
when(lifecycle.isSecurityIndexWriteable()).thenReturn(true);
when(lifecycle.isSecurityIndexExisting()).thenReturn(true);
when(lifecycle.isSecurityIndexAvailable()).thenReturn(true);
listener.clusterChanged(event);
listener.clusterChanged(event);
verify(realm, times(1)).bootstrapElasticUserCredentials(any(SecureString.class), any(ActionListener.class));
}
public void testWillNotAllowSecondBootstrapAttempt() {
String password = randomAlphaOfLength(10);
MockSecureSettings secureSettings = new MockSecureSettings();
secureSettings.setString(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(), password);
Settings settings = Settings.builder()
.setSecureSettings(secureSettings)
.build();
BootstrapElasticPassword bootstrap = new BootstrapElasticPassword(settings, logger, clusterService, realm, lifecycle);
bootstrap.initiatePasswordBootstrap();
verify(clusterService).addListener(listenerCaptor.capture());
ClusterStateListener listener = listenerCaptor.getValue();
ClusterChangedEvent event = getStateRecoveredEvent();
when(lifecycle.isSecurityIndexOutOfDate()).thenReturn(false);
when(lifecycle.isSecurityIndexWriteable()).thenReturn(true);
when(lifecycle.isSecurityIndexExisting()).thenReturn(true);
when(lifecycle.isSecurityIndexAvailable()).thenReturn(true);
listener.clusterChanged(event);
verify(realm, times(1)).bootstrapElasticUserCredentials(any(SecureString.class), actionLister.capture());
actionLister.getValue().onResponse(true);
listener.clusterChanged(event);
verify(realm, times(1)).bootstrapElasticUserCredentials(any(SecureString.class), any());
}
public void testBootstrapCompleteRemovesListener() {
String password = randomAlphaOfLength(10);
ensureBootstrapStarted(password);
verify(realm).bootstrapElasticUserCredentials(any(SecureString.class), actionLister.capture());
actionLister.getValue().onResponse(randomBoolean());
verify(clusterService).removeListener(listenerCaptor.getValue());
}
public void testBootstrapFailedRemovesListener() {
String password = randomAlphaOfLength(10);
ensureBootstrapStarted(password);
verify(realm).bootstrapElasticUserCredentials(any(SecureString.class), actionLister.capture());
actionLister.getValue().onFailure(new RuntimeException("failed"));
verify(clusterService).removeListener(listenerCaptor.getValue());
}
private void ensureBootstrapStarted(String password) {
MockSecureSettings secureSettings = new MockSecureSettings();
secureSettings.setString(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(), password);
Settings settings = Settings.builder()
.setSecureSettings(secureSettings)
.build();
BootstrapElasticPassword bootstrap = new BootstrapElasticPassword(settings, logger, clusterService, realm, lifecycle);
bootstrap.initiatePasswordBootstrap();
verify(clusterService).addListener(listenerCaptor.capture());
ClusterStateListener listener = listenerCaptor.getValue();
ClusterChangedEvent event = getStateRecoveredEvent();
when(lifecycle.isSecurityIndexOutOfDate()).thenReturn(false);
when(lifecycle.isSecurityIndexWriteable()).thenReturn(true);
when(lifecycle.isSecurityIndexExisting()).thenReturn(true);
when(lifecycle.isSecurityIndexAvailable()).thenReturn(true);
listener.clusterChanged(event);
}
private ClusterChangedEvent getStateRecoveredEvent() {
ClusterChangedEvent event = mock(ClusterChangedEvent.class);
ClusterState state = mock(ClusterState.class);
ClusterBlocks blocks = mock(ClusterBlocks.class);
when(event.state()).thenReturn(state);
when(state.blocks()).thenReturn(blocks);
when(blocks.hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)).thenReturn(false);
return event;
}
}