diff --git a/elasticsearch/src/main/java/org/elasticsearch/xpack/security/Security.java b/elasticsearch/src/main/java/org/elasticsearch/xpack/security/Security.java index 8f7e68d24c4..43b8270d352 100644 --- a/elasticsearch/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/elasticsearch/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -14,6 +14,7 @@ import org.elasticsearch.action.support.DestructiveOperations; import org.elasticsearch.bootstrap.BootstrapCheck; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.bootstrap.BootstrapCheck; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Booleans; import org.elasticsearch.common.Nullable; @@ -108,6 +109,7 @@ 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.DefaultPasswordBootstrapCheck; import org.elasticsearch.xpack.security.crypto.CryptoService; import org.elasticsearch.xpack.security.rest.SecurityRestFilter; import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction; @@ -416,6 +418,7 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin { AnonymousUser.addSettings(settingsList); RealmSettings.addSettings(settingsList, extensionsService == null ? null : extensionsService.getExtensions()); NativeRolesStore.addSettings(settingsList); + ReservedRealm.addSettings(settingsList); AuthenticationService.addSettings(settingsList); AuthorizationService.addSettings(settingsList); settingsList.add(CompositeRolesStore.CACHE_SIZE_SETTING); @@ -452,7 +455,10 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin { public List getBootstrapChecks() { if (enabled) { - return Collections.singletonList(new SSLBootstrapCheck(sslService, settings, env)); + return Arrays.asList( + new DefaultPasswordBootstrapCheck(settings), + new SSLBootstrapCheck(sslService, settings, env) + ); } else { return Collections.emptyList(); } diff --git a/elasticsearch/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmMigrator.java b/elasticsearch/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmMigrator.java index 0c7ee76e68e..059a3573a12 100644 --- a/elasticsearch/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmMigrator.java +++ b/elasticsearch/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmMigrator.java @@ -5,6 +5,12 @@ */ package org.elasticsearch.xpack.security.authc.esnative; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + import org.apache.logging.log4j.Logger; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; @@ -12,14 +18,18 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.common.inject.internal.Nullable; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.common.GroupedActionListener; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.xpack.security.SecurityTemplateService; +import org.elasticsearch.xpack.security.action.user.ChangePasswordRequest; +import org.elasticsearch.xpack.security.authc.support.Hasher; import org.elasticsearch.xpack.security.user.LogstashSystemUser; /** * Performs migration steps for the {@link NativeRealm} and {@link ReservedRealm}. * When upgrading an Elasticsearch/X-Pack installation from a previous version, this class is responsible for ensuring that user/role * data stored in the security index is converted to a format that is appropriate for the newly installed version. + * * @see SecurityTemplateService */ public class NativeRealmMigrator { @@ -48,31 +58,34 @@ public class NativeRealmMigrator { */ public void performUpgrade(@Nullable Version previousVersion, ActionListener listener) { try { - if (shouldDisableLogstashUser(previousVersion)) { - logger.info("Upgrading security from version [{}] - new reserved user [{}] will default to disabled", - previousVersion, LogstashSystemUser.NAME); - // Only clear the cache is authentication is allowed by the current license - // otherwise the license management checks will prevent it from completing successfully. - final boolean clearCache = licenseState.isAuthAllowed(); - nativeUsersStore.ensureReservedUserIsDisabled(LogstashSystemUser.NAME, clearCache, new ActionListener() { - @Override - public void onResponse(Void aVoid) { - listener.onResponse(true); - } - - @Override - public void onFailure(Exception e) { - listener.onFailure(e); - } - }); - } else { + List>> tasks = collectUpgradeTasks(previousVersion); + if (tasks.isEmpty()) { listener.onResponse(false); + } else { + final GroupedActionListener countDownListener = new GroupedActionListener<>( + ActionListener.wrap(r -> listener.onResponse(true), listener::onFailure), + tasks.size(), + Collections.emptyList() + ); + logger.info("Performing {} security migration task(s)", tasks.size()); + tasks.forEach(t -> t.accept(previousVersion, countDownListener)); } } catch (Exception e) { listener.onFailure(e); } } + private List>> collectUpgradeTasks(@Nullable Version previousVersion) { + List>> tasks = new ArrayList<>(); + if (shouldDisableLogstashUser(previousVersion)) { + tasks.add(this::doDisableLogstashUser); + } + if (shouldConvertDefaultPasswords(previousVersion)) { + tasks.add(this::doConvertDefaultPasswords); + } + return tasks; + } + /** * If we're upgrading from a security version where the {@link LogstashSystemUser} did not exist, then we mark the user as disabled. * Otherwise the user will exist with a default password, which is desirable for an out-of-the-box experience in fresh installs @@ -82,4 +95,67 @@ public class NativeRealmMigrator { return previousVersion != null && previousVersion.before(LogstashSystemUser.DEFINED_SINCE); } + private void doDisableLogstashUser(@Nullable Version previousVersion, ActionListener listener) { + logger.info("Upgrading security from version [{}] - new reserved user [{}] will default to disabled", + previousVersion, LogstashSystemUser.NAME); + // Only clear the cache is authentication is allowed by the current license + // otherwise the license management checks will prevent it from completing successfully. + final boolean clearCache = licenseState.isAuthAllowed(); + nativeUsersStore.ensureReservedUserIsDisabled(LogstashSystemUser.NAME, clearCache, listener); + } + + /** + * Old versions of X-Pack security would assign the default password content to a user if it was enabled/disabled before the password + * was explicitly set to another value. If upgrading from one of those versions, then we want to change those users to be flagged as + * having a "default password" (which is stored as blank) so that {@link ReservedRealm#ACCEPT_DEFAULT_PASSWORD_SETTING} does the + * right thing. + */ + private boolean shouldConvertDefaultPasswords(@Nullable Version previousVersion) { + return previousVersion != null && previousVersion.before(Version.V_6_0_0_alpha1_UNRELEASED); + } + + @SuppressWarnings("unused") + private void doConvertDefaultPasswords(@Nullable Version previousVersion, ActionListener listener) { + nativeUsersStore.getAllReservedUserInfo(ActionListener.wrap( + users -> { + final List toConvert = users.entrySet().stream() + .filter(entry -> hasOldStyleDefaultPassword(entry.getValue())) + .map(entry -> entry.getKey()) + .collect(Collectors.toList()); + if (toConvert.isEmpty()) { + listener.onResponse(null); + } else { + GroupedActionListener countDownListener = new GroupedActionListener( + ActionListener.wrap((r) -> listener.onResponse(null), listener::onFailure), + toConvert.size(), Collections.emptyList() + ); + toConvert.forEach(username -> { + logger.debug( + "Upgrading security from version [{}] - marking reserved user [{}] as having default password", + previousVersion, username); + resetReservedUserPassword(username, countDownListener); + }); + } + }, listener::onFailure) + ); + } + + /** + * Determines whether the supplied {@link NativeUsersStore.ReservedUserInfo} has its password set to be the default password, without + * having the {@link NativeUsersStore.ReservedUserInfo#hasDefaultPassword} flag set. + */ + private boolean hasOldStyleDefaultPassword(NativeUsersStore.ReservedUserInfo userInfo) { + return userInfo.hasDefaultPassword == false && Hasher.BCRYPT.verify(ReservedRealm.DEFAULT_PASSWORD_TEXT, userInfo.passwordHash); + } + + /** + * Sets a reserved user's password back to blank, so that the default password functionality applies. + */ + void resetReservedUserPassword(String username, ActionListener listener) { + final ChangePasswordRequest request = new ChangePasswordRequest(); + request.username(username); + request.passwordHash(new char[0]); + nativeUsersStore.changePassword(request, true, listener); + } + } diff --git a/elasticsearch/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java b/elasticsearch/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java index 5f6c00d853d..2d02b28e30a 100644 --- a/elasticsearch/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java +++ b/elasticsearch/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java @@ -41,11 +41,13 @@ import org.elasticsearch.index.engine.DocumentMissingException; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; +import org.elasticsearch.xpack.common.GroupedActionListener; import org.elasticsearch.xpack.security.InternalClient; import org.elasticsearch.xpack.security.SecurityTemplateService; import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheRequest; import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheResponse; import org.elasticsearch.xpack.security.action.user.ChangePasswordRequest; +import org.elasticsearch.xpack.security.action.user.ChangePasswordRequestBuilder; import org.elasticsearch.xpack.security.action.user.DeleteUserRequest; import org.elasticsearch.xpack.security.action.user.PutUserRequest; import org.elasticsearch.xpack.security.authc.support.Hasher; @@ -89,7 +91,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL } private static final String USER_DOC_TYPE = "user"; - private static final String RESERVED_USER_DOC_TYPE = "reserved-user"; + static final String RESERVED_USER_DOC_TYPE = "reserved-user"; private final Hasher hasher = Hasher.BCRYPT; private final AtomicReference state = new AtomicReference<>(State.INITIALIZED); @@ -210,6 +212,15 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL * with a hash of the provided password. */ public void changePassword(final ChangePasswordRequest request, final ActionListener listener) { + changePassword(request, false, listener); + } + + /** + * This version of {@link #changePassword(ChangePasswordRequest, ActionListener)} exists to that the {@link NativeRealmMigrator} + * can force change passwords before the security mapping is {@link #canWrite ready for writing} + * @param forceWrite If true, allow the change to take place even if the store is currently read-only. + */ + void changePassword(final ChangePasswordRequest request, boolean forceWrite, final ActionListener listener) { final String username = request.username(); assert SystemUser.NAME.equals(username) == false && XPackUser.NAME.equals(username) == false : username + "is internal!"; if (state() != State.STARTED) { @@ -218,7 +229,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL } else if (isTribeNode) { listener.onFailure(new UnsupportedOperationException("users may not be created or modified using a tribe node")); return; - } else if (canWrite == false) { + } else if (canWrite == false && forceWrite == false) { listener.onFailure(new IllegalStateException("password cannot be changed as user service cannot write until template and " + "mappings are up to date")); return; @@ -449,7 +460,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL client.prepareUpdate(SecurityTemplateService.SECURITY_INDEX_NAME, RESERVED_USER_DOC_TYPE, username) .setDoc(Requests.INDEX_CONTENT_TYPE, User.Fields.ENABLED.getPreferredName(), enabled) .setUpsert(XContentType.JSON, - User.Fields.PASSWORD.getPreferredName(), String.valueOf(ReservedRealm.DEFAULT_PASSWORD_HASH), + User.Fields.PASSWORD.getPreferredName(), "", User.Fields.ENABLED.getPreferredName(), enabled) .setRefreshPolicy(refreshPolicy) .execute(new ActionListener() { @@ -620,12 +631,14 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL Map sourceMap = getResponse.getSourceAsMap(); String password = (String) sourceMap.get(User.Fields.PASSWORD.getPreferredName()); Boolean enabled = (Boolean) sourceMap.get(Fields.ENABLED.getPreferredName()); - if (password == null || password.isEmpty()) { - listener.onFailure(new IllegalStateException("password hash must not be empty!")); + if (password == null) { + listener.onFailure(new IllegalStateException("password hash must not be null!")); } else if (enabled == null) { listener.onFailure(new IllegalStateException("enabled must not be null!")); + } else if (password.isEmpty()) { + listener.onResponse(new ReservedUserInfo(ReservedRealm.DEFAULT_PASSWORD_HASH, enabled, true)); } else { - listener.onResponse(new ReservedUserInfo(password.toCharArray(), enabled)); + listener.onResponse(new ReservedUserInfo(password.toCharArray(), enabled, false)); } } else { listener.onResponse(null); @@ -671,7 +684,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL listener.onFailure(new IllegalStateException("enabled must not be null!")); break; } else { - userInfos.put(searchHit.getId(), new ReservedUserInfo(password.toCharArray(), enabled)); + userInfos.put(searchHit.getId(), new ReservedUserInfo(password.toCharArray(), enabled, false)); } } listener.onResponse(userInfos); @@ -786,10 +799,12 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL final char[] passwordHash; final boolean enabled; + final boolean hasDefaultPassword; - ReservedUserInfo(char[] passwordHash, boolean enabled) { + ReservedUserInfo(char[] passwordHash, boolean enabled, boolean hasDefaultPassword) { this.passwordHash = passwordHash; this.enabled = enabled; + this.hasDefaultPassword = hasDefaultPassword; } } } diff --git a/elasticsearch/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/elasticsearch/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index b736bf55944..8baca3a2d98 100644 --- a/elasticsearch/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/elasticsearch/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -9,9 +9,11 @@ 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.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.XPackSettings; +import org.elasticsearch.xpack.security.Security; import org.elasticsearch.xpack.security.authc.RealmConfig; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ReservedUserInfo; import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm; @@ -30,7 +32,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.function.Predicate; /** * A realm for predefined users. These users can only be modified in terms of changing their passwords; no other modifications are allowed. @@ -39,26 +40,34 @@ import java.util.function.Predicate; public class ReservedRealm extends CachingUsernamePasswordRealm { public static final String TYPE = "reserved"; - static final char[] DEFAULT_PASSWORD_HASH = Hasher.BCRYPT.hash(new SecuredString("changeme".toCharArray())); - private static final ReservedUserInfo DEFAULT_USER_INFO = new ReservedUserInfo(DEFAULT_PASSWORD_HASH, true); - private static final ReservedUserInfo DISABLED_USER_INFO = new ReservedUserInfo(DEFAULT_PASSWORD_HASH, false); + + static final SecuredString DEFAULT_PASSWORD_TEXT = new SecuredString("changeme".toCharArray()); + static final char[] DEFAULT_PASSWORD_HASH = Hasher.BCRYPT.hash(DEFAULT_PASSWORD_TEXT); + + private static final ReservedUserInfo DEFAULT_USER_INFO = new ReservedUserInfo(DEFAULT_PASSWORD_HASH, true, true); + private static final ReservedUserInfo DISABLED_USER_INFO = new ReservedUserInfo(DEFAULT_PASSWORD_HASH, false, true); + + public static final Setting ACCEPT_DEFAULT_PASSWORD_SETTING = Setting.boolSetting( + Security.setting("authc.accept_default_password"), true, Setting.Property.NodeScope, Setting.Property.Filtered); private final NativeUsersStore nativeUsersStore; private final AnonymousUser anonymousUser; + private final boolean realmEnabled; private final boolean anonymousEnabled; - private final boolean enabled; + private final boolean defaultPasswordEnabled; public ReservedRealm(Environment env, Settings settings, NativeUsersStore nativeUsersStore, AnonymousUser anonymousUser) { super(TYPE, new RealmConfig(TYPE, Settings.EMPTY, settings, env)); this.nativeUsersStore = nativeUsersStore; - this.enabled = XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings); + this.realmEnabled = XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings); this.anonymousUser = anonymousUser; this.anonymousEnabled = AnonymousUser.isAnonymousEnabled(settings); + this.defaultPasswordEnabled = ACCEPT_DEFAULT_PASSWORD_SETTING.get(settings); } @Override protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener) { - if (enabled == false) { + if (realmEnabled == false) { listener.onResponse(null); } else if (isReserved(token.principal(), config.globalSettings()) == false) { listener.onResponse(null); @@ -67,7 +76,7 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { Runnable action; if (userInfo != null) { try { - if (Hasher.BCRYPT.verify(token.credentials(), userInfo.passwordHash)) { + if (verifyPassword(userInfo, token)) { final User user = getUser(token.principal(), userInfo); action = () -> listener.onResponse(user); } else { @@ -89,9 +98,19 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { } } + private boolean verifyPassword(ReservedUserInfo userInfo, UsernamePasswordToken token) { + if (Hasher.BCRYPT.verify(token.credentials(), userInfo.passwordHash)) { + if (userInfo.hasDefaultPassword && this.defaultPasswordEnabled == false) { + return false; + } + return true; + } + return false; + } + @Override protected void doLookupUser(String username, ActionListener listener) { - if (enabled == false) { + if (realmEnabled == false) { if (anonymousEnabled && AnonymousUser.isAnonymousUsername(username, config.globalSettings())) { listener.onResponse(anonymousUser); } @@ -143,7 +162,7 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { public void users(ActionListener> listener) { - if (nativeUsersStore.started() == false || enabled == false) { + if (nativeUsersStore.started() == false || realmEnabled == false) { listener.onResponse(anonymousEnabled ? Collections.singletonList(anonymousUser) : Collections.emptyList()); } else { nativeUsersStore.getAllReservedUserInfo(ActionListener.wrap((reservedUserInfos) -> { @@ -207,4 +226,8 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { return Version.V_5_0_0; } } + + public static void addSettings(List> settingsList) { + settingsList.add(ACCEPT_DEFAULT_PASSWORD_SETTING); + } } diff --git a/elasticsearch/src/main/java/org/elasticsearch/xpack/security/bootstrap/DefaultPasswordBootstrapCheck.java b/elasticsearch/src/main/java/org/elasticsearch/xpack/security/bootstrap/DefaultPasswordBootstrapCheck.java new file mode 100644 index 00000000000..9b1a2770d36 --- /dev/null +++ b/elasticsearch/src/main/java/org/elasticsearch/xpack/security/bootstrap/DefaultPasswordBootstrapCheck.java @@ -0,0 +1,37 @@ +/* + * 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 java.util.Locale; + +import org.elasticsearch.bootstrap.BootstrapCheck; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; + +public class DefaultPasswordBootstrapCheck implements BootstrapCheck { + + private Settings settings; + + public DefaultPasswordBootstrapCheck(Settings settings) { + this.settings = settings; + } + + /** + * This check fails if the "accept default password" is set to true. + */ + @Override + public boolean check() { + return ReservedRealm.ACCEPT_DEFAULT_PASSWORD_SETTING.get(settings) == true; + } + + @Override + public String errorMessage() { + return String.format(Locale.ROOT, "The configuration setting '%s' is %s - it should be set to false", + ReservedRealm.ACCEPT_DEFAULT_PASSWORD_SETTING.getKey(), + ReservedRealm.ACCEPT_DEFAULT_PASSWORD_SETTING.exists(settings) ? "set to true" : "not set" + ); + } +} diff --git a/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmMigratorTests.java b/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmMigratorTests.java index 9255d1615c6..af4e3495f62 100644 --- a/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmMigratorTests.java +++ b/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmMigratorTests.java @@ -5,23 +5,42 @@ */ package org.elasticsearch.xpack.security.authc.esnative; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.security.action.user.ChangePasswordRequest; +import org.elasticsearch.xpack.security.authc.support.Hasher; +import org.elasticsearch.xpack.security.user.ElasticUser; +import org.elasticsearch.xpack.security.user.KibanaUser; import org.elasticsearch.xpack.security.user.LogstashSystemUser; import org.junit.Before; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -30,6 +49,7 @@ import static org.mockito.Mockito.when; public class NativeRealmMigratorTests extends ESTestCase { private Consumer> ensureDisabledHandler; + private Map reservedUsers; private NativeUsersStore nativeUsersStore; private NativeRealmMigrator migrator; private XPackLicenseState licenseState; @@ -44,7 +64,21 @@ public class NativeRealmMigratorTests extends ESTestCase { ActionListener listener = (ActionListener) invocation.getArguments()[2]; ensureDisabledHandler.accept(listener); return null; - }).when(nativeUsersStore).ensureReservedUserIsDisabled(any(), eq(allowClearCache), any()); + }).when(nativeUsersStore).ensureReservedUserIsDisabled(anyString(), eq(allowClearCache), any(ActionListener.class)); + + Mockito.doAnswer(invocation -> { + ActionListener listener = (ActionListener) invocation.getArguments()[2]; + listener.onResponse(null); + return null; + }).when(nativeUsersStore).changePassword(any(ChangePasswordRequest.class), anyBoolean(), any(ActionListener.class)); + + reservedUsers = Collections.emptyMap(); + Mockito.doAnswer(invocation -> { + ActionListener> listener = + (ActionListener>) invocation.getArguments()[0]; + listener.onResponse(reservedUsers); + return null; + }).when(nativeUsersStore).getAllReservedUserInfo(any(ActionListener.class)); final Settings settings = Settings.EMPTY; @@ -55,15 +89,28 @@ public class NativeRealmMigratorTests extends ESTestCase { } public void testNoChangeOnFreshInstall() throws Exception { - verifyNoOpUpgrade(null); + verifyUpgrade(null, false, false); } - public void testNoChangeOnUpgradeOnOrAfterV5_2() throws Exception { - verifyNoOpUpgrade(randomFrom(Version.V_5_2_0_UNRELEASED, Version.V_6_0_0_alpha1_UNRELEASED)); + public void testNoChangeOnUpgradeAfterV5_3() throws Exception { + verifyUpgrade(randomFrom(Version.V_6_0_0_alpha1_UNRELEASED), false, false); } - public void testDisableLogstashOnUpgradeFromVersionPriorToV5_2() throws Exception { - verifyUpgradeDisablesLogstashSystemUser(randomFrom(Version.V_5_1_1_UNRELEASED, Version.V_5_0_2, Version.V_5_0_0)); + public void testDisableLogstashAndConvertPasswordsOnUpgradeFromVersionPriorToV5_2() throws Exception { + this.reservedUsers = Collections.singletonMap( + KibanaUser.NAME, + new NativeUsersStore.ReservedUserInfo(Hasher.BCRYPT.hash(ReservedRealm.DEFAULT_PASSWORD_TEXT), false, false) + ); + verifyUpgrade(randomFrom(Version.V_5_1_1_UNRELEASED, Version.V_5_0_2, Version.V_5_0_0), true, true); + } + + public void testConvertPasswordsOnUpgradeFromVersion5_2() throws Exception { + this.reservedUsers = randomSubsetOf(randomIntBetween(0, 3), LogstashSystemUser.NAME, KibanaUser.NAME, ElasticUser.NAME) + .stream().collect(Collectors.toMap(Function.identity(), + name -> new NativeUsersStore.ReservedUserInfo(Hasher.BCRYPT.hash(ReservedRealm.DEFAULT_PASSWORD_TEXT), + randomBoolean(), false) + )); + verifyUpgrade(Version.V_5_2_0_UNRELEASED, false, true); } public void testExceptionInUsersStoreIsPropagatedToListener() throws Exception { @@ -74,18 +121,31 @@ public class NativeRealmMigratorTests extends ESTestCase { assertThat(caught.getCause(), is(thrown)); } - private void verifyNoOpUpgrade(Version fromVersion) throws ExecutionException, InterruptedException { + private void verifyUpgrade(Version fromVersion, boolean disableLogstashUser, boolean convertDefaultPasswords) throws Exception { final PlainActionFuture future = doUpgrade(fromVersion); + boolean expectedResult = false; + if (disableLogstashUser) { + final boolean clearCache = licenseState.isAuthAllowed(); + verify(nativeUsersStore).ensureReservedUserIsDisabled(eq(LogstashSystemUser.NAME), eq(clearCache), any()); + expectedResult = true; + } + if (convertDefaultPasswords) { + verify(nativeUsersStore).getAllReservedUserInfo(any()); + ArgumentCaptor captor = ArgumentCaptor.forClass(ChangePasswordRequest.class); + verify(nativeUsersStore, times(this.reservedUsers.size())) + .changePassword(captor.capture(), eq(true), any(ActionListener.class)); + final List requests = captor.getAllValues(); + this.reservedUsers.keySet().forEach(u -> { + ChangePasswordRequest request = requests.stream().filter(r -> r.username().equals(u)).findFirst().get(); + assertThat(request.validate(), nullValue(ActionRequestValidationException.class)); + assertThat(request.username(), equalTo(u)); + assertThat(request.passwordHash().length, equalTo(0)); + assertThat(request.getRefreshPolicy(), equalTo(WriteRequest.RefreshPolicy.IMMEDIATE)); + }); + expectedResult = true; + } verifyNoMoreInteractions(nativeUsersStore); - assertThat(future.get(), is(Boolean.FALSE)); - } - - private void verifyUpgradeDisablesLogstashSystemUser(Version fromVersion) throws ExecutionException, InterruptedException { - final PlainActionFuture future = doUpgrade(fromVersion); - final boolean clearCache = licenseState.isAuthAllowed(); - verify(nativeUsersStore).ensureReservedUserIsDisabled(eq(LogstashSystemUser.NAME), eq(clearCache), any()); - verifyNoMoreInteractions(nativeUsersStore); - assertThat(future.get(), is(Boolean.TRUE)); + assertThat(future.get(), is(expectedResult)); } private PlainActionFuture doUpgrade(Version fromVersion) { @@ -93,4 +153,4 @@ public class NativeRealmMigratorTests extends ESTestCase { migrator.performUpgrade(fromVersion, future); return future; } -} \ No newline at end of file +} diff --git a/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java b/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java new file mode 100644 index 00000000000..e9994b13bf6 --- /dev/null +++ b/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java @@ -0,0 +1,137 @@ +/* + * 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.esnative; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.get.GetResult; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.security.InternalClient; +import org.elasticsearch.xpack.security.SecurityTemplateService; +import org.elasticsearch.xpack.security.test.SecurityTestUtils; +import org.elasticsearch.xpack.security.user.ElasticUser; +import org.elasticsearch.xpack.security.user.KibanaUser; +import org.elasticsearch.xpack.security.user.LogstashSystemUser; +import org.elasticsearch.xpack.security.user.User; +import org.junit.Before; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; + +public class NativeUsersStoreTests extends ESTestCase { + + private static final String ENABLED_FIELD = User.Fields.ENABLED.getPreferredName(); + private static final String PASSWORD_FIELD = User.Fields.PASSWORD.getPreferredName(); + private static final String BLANK_PASSWORD = ""; + + private InternalClient internalClient; + private final List>> requests = new CopyOnWriteArrayList<>(); + + @Before + public void setupMocks() { + internalClient = new InternalClient(Settings.EMPTY, null, null, null) { + + @Override + protected < + Request extends ActionRequest, + Response extends ActionResponse, + RequestBuilder extends ActionRequestBuilder + > void doExecute( + Action action, + Request request, + ActionListener listener) { + requests.add(new Tuple<>(request, listener)); + } + }; + } + + public void testPasswordUpsertWhenSetEnabledOnReservedUser() throws Exception { + final NativeUsersStore nativeUsersStore = startNativeUsersStore(); + + final String user = randomFrom(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME); + + final PlainActionFuture future = new PlainActionFuture<>(); + nativeUsersStore.setEnabled(user, true, WriteRequest.RefreshPolicy.IMMEDIATE, future); + final UpdateRequest update = actionRespond(UpdateRequest.class, null); + + final Map docMap = update.doc().sourceAsMap(); + assertThat(docMap.get(ENABLED_FIELD), equalTo(Boolean.TRUE)); + assertThat(docMap.get(PASSWORD_FIELD), nullValue()); + + final Map upsertMap = update.upsertRequest().sourceAsMap(); + assertThat(upsertMap.get(User.Fields.ENABLED.getPreferredName()), equalTo(Boolean.TRUE)); + assertThat(upsertMap.get(User.Fields.PASSWORD.getPreferredName()), equalTo(BLANK_PASSWORD)); + } + + public void testBlankPasswordInIndexImpliesDefaultPassword() throws Exception { + final NativeUsersStore nativeUsersStore = startNativeUsersStore(); + + final String user = randomFrom(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME); + final Map values = new HashMap<>(); + values.put(ENABLED_FIELD, Boolean.TRUE); + values.put(PASSWORD_FIELD, BLANK_PASSWORD); + + final GetResult result = new GetResult( + SecurityTemplateService.SECURITY_INDEX_NAME, + NativeUsersStore.RESERVED_USER_DOC_TYPE, + randomAsciiOfLength(12), + 1L, + true, + jsonBuilder().map(values).bytes(), + Collections.emptyMap()); + + final PlainActionFuture future = new PlainActionFuture<>(); + nativeUsersStore.getReservedUserInfo(user, future); + + actionRespond(GetRequest.class, new GetResponse(result)); + + final NativeUsersStore.ReservedUserInfo userInfo = future.get(); + assertThat(userInfo.hasDefaultPassword, equalTo(true)); + assertThat(userInfo.enabled, equalTo(true)); + assertThat(userInfo.passwordHash, equalTo(ReservedRealm.DEFAULT_PASSWORD_HASH)); + } + + private ARequest actionRespond(Class requestClass, + AResponse response) { + Tuple> tuple = findRequest(requestClass); + ((ActionListener) tuple.v2()).onResponse(response); + return tuple.v1(); + } + + private Tuple> findRequest( + Class requestClass) { + return this.requests.stream() + .filter(t -> requestClass.isInstance(t.v1())) + .map(t -> new Tuple>(requestClass.cast(t.v1()), t.v2())) + .findFirst().orElseThrow(() -> new RuntimeException("Cannot find request of type " + requestClass)); + } + + private NativeUsersStore startNativeUsersStore() { + final NativeUsersStore nativeUsersStore = new NativeUsersStore(Settings.EMPTY, internalClient); + assertTrue(nativeUsersStore + " should be ready to start", + nativeUsersStore.canStart(SecurityTestUtils.getClusterStateWithSecurityIndex(), true)); + nativeUsersStore.start(); + return nativeUsersStore; + } + +} \ No newline at end of file diff --git a/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java b/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java index e2b70ab7df5..c3fd7fde812 100644 --- a/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java +++ b/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.authc.esnative; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.xpack.security.client.SecurityClient; import org.elasticsearch.xpack.security.user.ElasticUser; import org.elasticsearch.xpack.security.user.KibanaUser; import org.elasticsearch.xpack.security.action.user.ChangePasswordResponse; @@ -41,6 +42,25 @@ public class ReservedRealmIntegTests extends NativeRealmIntegTestCase { } } + /** + * Enabling a user forces a doc to be written to the security index, and "user doc with default password" has a special case code in + * the reserved realm. + */ + public void testAuthenticateAfterEnablingUser() { + final SecurityClient c = securityClient(); + for (String username : Arrays.asList(ElasticUser.NAME, KibanaUser.NAME)) { + c.prepareSetEnabled(username, true).get(); + ClusterHealthResponse response = client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(username, DEFAULT_PASSWORD))) + .admin() + .cluster() + .prepareHealth() + .get(); + + assertThat(response.getClusterName(), is(cluster().getClusterName())); + } + } + public void testChangingPassword() { String username = randomFrom(ElasticUser.NAME, KibanaUser.NAME); final char[] newPassword = "supersecretvalue".toCharArray(); diff --git a/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmNoDefaultPasswordIntegTests.java b/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmNoDefaultPasswordIntegTests.java new file mode 100644 index 00000000000..6d91181805a --- /dev/null +++ b/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmNoDefaultPasswordIntegTests.java @@ -0,0 +1,64 @@ +/* + * 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.esnative; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.NativeRealmIntegTestCase; +import org.elasticsearch.xpack.security.authc.support.SecuredString; +import org.elasticsearch.xpack.security.client.SecurityClient; +import org.elasticsearch.xpack.security.user.KibanaUser; + +import static java.util.Collections.singletonMap; +import static org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +/** + * Integration tests for the built in realm with default passwords disabled + */ +public class ReservedRealmNoDefaultPasswordIntegTests extends NativeRealmIntegTestCase { + + private static final SecuredString DEFAULT_PASSWORD = new SecuredString("changeme".toCharArray()); + + @Override + protected Settings nodeSettings(int nodeOrdinal) { + Settings.Builder builder = Settings.builder() + .put(super.nodeSettings(nodeOrdinal)) + .put(ReservedRealm.ACCEPT_DEFAULT_PASSWORD_SETTING.getKey(), false); + return builder.build(); + } + + /** + * This ensures that if a user is explicitly enabled, thus creating an entry in the security index, but no password is ever set, + * then the user is treated as having a default password, and cannot login. + */ + public void testEnablingUserWithoutPasswordCannotLogin() throws Exception { + final SecurityClient c = securityClient(); + c.prepareSetEnabled(KibanaUser.NAME, true).get(); + + ElasticsearchSecurityException elasticsearchSecurityException = expectThrows(ElasticsearchSecurityException.class, () -> client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(KibanaUser.NAME, DEFAULT_PASSWORD))) + .admin() + .cluster() + .prepareHealth() + .get()); + assertThat(elasticsearchSecurityException.getMessage(), containsString("authenticate")); + + final SecuredString newPassword = new SecuredString("not-the-default-password".toCharArray()); + c.prepareChangePassword(KibanaUser.NAME, newPassword.copyChars()).get(); + + ClusterHealthResponse response = client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(KibanaUser.NAME, newPassword))) + .admin() + .cluster() + .prepareHealth() + .get(); + + assertThat(response.getClusterName(), is(cluster().getClusterName())); + } +} diff --git a/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index ef89f95535e..6b2d27a1cf1 100644 --- a/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -32,6 +32,7 @@ import java.util.Map.Entry; import java.util.concurrent.ExecutionException; import java.util.function.Predicate; +import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -55,6 +56,7 @@ import static org.mockito.Mockito.when; public class ReservedRealmTests extends ESTestCase { private static final SecuredString DEFAULT_PASSWORD = new SecuredString("changeme".toCharArray()); + public static final String ACCEPT_DEFAULT_PASSWORDS = ReservedRealm.ACCEPT_DEFAULT_PASSWORD_SETTING.getKey(); private NativeUsersStore usersStore; @Before @@ -90,7 +92,7 @@ public class ReservedRealmTests extends ESTestCase { assertThat(future.get().enabled(), equalTo(false)); } - public void testDefaultPasswordAuthentication() throws Throwable { + public void testSuccessfulDefaultPasswordAuthentication() throws Throwable { final User expected = randomFrom(new ElasticUser(true), new KibanaUser(true), new LogstashSystemUser(true)); final String principal = expected.principal(); final boolean securityIndexExists = randomBoolean(); @@ -105,7 +107,6 @@ public class ReservedRealmTests extends ESTestCase { final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY)); - PlainActionFuture listener = new PlainActionFuture<>(); reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, DEFAULT_PASSWORD), listener); final User authenticated = listener.actionGet(); @@ -121,6 +122,29 @@ public class ReservedRealmTests extends ESTestCase { verifyNoMoreInteractions(usersStore); } + public void testDisableDefaultPasswordAuthentication() throws Throwable { + final User expected = randomFrom(new ElasticUser(true), new KibanaUser(true), new LogstashSystemUser(true)); + + final Environment environment = mock(Environment.class); + final AnonymousUser anonymousUser = new AnonymousUser(Settings.EMPTY); + final Settings settings = Settings.builder().put(ACCEPT_DEFAULT_PASSWORDS, false).build(); + final ReservedRealm reservedRealm = new ReservedRealm(environment, settings, usersStore, anonymousUser); + + final ActionListener listener = new ActionListener() { + @Override + public void onResponse(User user) { + fail("Authentication should have failed because default-password is not allowed"); + } + + @Override + public void onFailure(Exception e) { + assertThat(e, instanceOf(ElasticsearchSecurityException.class)); + assertThat(e.getMessage(), containsString("failed to authenticate")); + } + }; + reservedRealm.doAuthenticate(new UsernamePasswordToken(expected.principal(), DEFAULT_PASSWORD), listener); + } + public void testAuthenticationDisabled() throws Throwable { Settings settings = Settings.builder().put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false).build(); final boolean securityIndexExists = randomBoolean(); @@ -147,15 +171,15 @@ public class ReservedRealmTests extends ESTestCase { } private void verifySuccessfulAuthentication(boolean enabled) { - final ReservedRealm reservedRealm = - new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY)); + final Settings settings = Settings.builder().put(ACCEPT_DEFAULT_PASSWORDS, randomBoolean()).build(); + final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings)); final User expectedUser = randomFrom(new ElasticUser(enabled), new KibanaUser(enabled), new LogstashSystemUser(enabled)); final String principal = expectedUser.principal(); final SecuredString newPassword = new SecuredString("foobar".toCharArray()); when(usersStore.securityIndexExists()).thenReturn(true); doAnswer((i) -> { ActionListener callback = (ActionListener) i.getArguments()[1]; - callback.onResponse(new ReservedUserInfo(Hasher.BCRYPT.hash(newPassword), enabled)); + callback.onResponse(new ReservedUserInfo(Hasher.BCRYPT.hash(newPassword), enabled, false)); return null; }).when(usersStore).getReservedUserInfo(eq(principal), any(ActionListener.class)); @@ -168,7 +192,7 @@ public class ReservedRealmTests extends ESTestCase { // the realm assumes it owns the hashed password so it fills it with 0's doAnswer((i) -> { ActionListener callback = (ActionListener) i.getArguments()[1]; - callback.onResponse(new ReservedUserInfo(Hasher.BCRYPT.hash(newPassword), true)); + callback.onResponse(new ReservedUserInfo(Hasher.BCRYPT.hash(newPassword), true, false)); return null; }).when(usersStore).getReservedUserInfo(eq(principal), any(ActionListener.class)); diff --git a/elasticsearch/src/test/java/org/elasticsearch/xpack/security/test/SecurityTestUtils.java b/elasticsearch/src/test/java/org/elasticsearch/xpack/security/test/SecurityTestUtils.java index c13ea7ed832..4b3d4b9399e 100644 --- a/elasticsearch/src/test/java/org/elasticsearch/xpack/security/test/SecurityTestUtils.java +++ b/elasticsearch/src/test/java/org/elasticsearch/xpack/security/test/SecurityTestUtils.java @@ -10,12 +10,14 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.IndexTemplateMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.routing.IndexRoutingTable; import org.elasticsearch.cluster.routing.IndexShardRoutingTable; import org.elasticsearch.cluster.routing.RoutingTable; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.routing.UnassignedInfo; +import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.settings.Settings; @@ -30,6 +32,7 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.UUID; import static org.elasticsearch.cluster.routing.RecoverySource.StoreRecoverySource.EXISTING_STORE_INSTANCE; @@ -76,7 +79,11 @@ public class SecurityTestUtils { .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) .build(); MetaData metaData = MetaData.builder() - .put(IndexMetaData.builder(SecurityTemplateService.SECURITY_INDEX_NAME).settings(settings)).build(); + .put(IndexMetaData.builder(SecurityTemplateService.SECURITY_INDEX_NAME).settings(settings)) + .put(new IndexTemplateMetaData(SecurityTemplateService.SECURITY_TEMPLATE_NAME, 0, 0, + Collections.singletonList(SecurityTemplateService.SECURITY_INDEX_NAME), Settings.EMPTY, ImmutableOpenMap.of(), + ImmutableOpenMap.of(), ImmutableOpenMap.of())) + .build(); RoutingTable routingTable = buildSecurityIndexRoutingTable(); return ClusterState.builder(new ClusterName(NativeRolesStoreTests.class.getName())) diff --git a/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/20_security.yaml b/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/20_security.yaml index e849403417a..bf6111d3923 100644 --- a/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/20_security.yaml +++ b/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/20_security.yaml @@ -29,3 +29,14 @@ - do: xpack.security.clear_cached_realms: realms: "_all" + +--- +"verify users for default password migration in mixed cluster": + - do: + xpack.security.get_user: + username: "kibana,logstash_system" + - match: { kibana.enabled: false } + - match: { logstash_system.enabled: true } + + + diff --git a/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_security.yaml b/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_security.yaml index a2c98e54793..cf8c10b1b06 100644 --- a/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_security.yaml +++ b/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_security.yaml @@ -57,3 +57,34 @@ index: ".security" wait_for_active_shards: 2 # 1 primary and 1 replica since we have two nodes timeout: 25s + +--- +"default password migration": + # Check that enabling a user in old cluster will not prevent the user from having a "default password" in the new cluster. + # See: org.elasticsearch.xpack.security.authc.esnative.NativeRealmMigrator.doConvertDefaultPasswords + - do: + xpack.security.disable_user: + username: "kibana" + + - do: + xpack.security.get_user: + username: "kibana" + - match: { kibana.enabled: false } + + - do: + xpack.security.change_password: + username: "logstash_system" + body: > + { + "password" : "changed-it" + } + + - do: + xpack.security.enable_user: + username: "logstash_system" + + - do: + xpack.security.get_user: + username: "logstash_system" + - match: { logstash_system.enabled: true } + diff --git a/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/20_security.yaml b/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/20_security.yaml index 889efb3d1f5..828a06a9657 100644 --- a/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/20_security.yaml +++ b/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/20_security.yaml @@ -21,3 +21,30 @@ - match: { native_role.cluster.0: "all" } - match: { native_role.indices.0.names.0: "test_index" } - match: { native_role.indices.0.privileges.0: "all" } + +--- +"Verify default password migration results in upgraded cluster": + - do: + headers: + Authorization: "Basic bmF0aXZlX3VzZXI6Y2hhbmdlbWU=" + cluster.health: + wait_for_status: green + wait_for_nodes: 2 + timeout: 25s + - match: { timed_out: false } + + - do: + get: + index: ".security" + type: "reserved-user" + id: "kibana" + - match: { _source.password: "" } + - match: { _source.enabled: false } + + - do: + get: + index: ".security" + type: "reserved-user" + id: "logstash_system" + - match: { _source.password: "/^\\$2a\\$10\\$[a-zA-Z0-9/.]{53}$/" } + - match: { _source.enabled: true }