From 734a4ee66db51c6e8ae793673a753376bc9cd4b1 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 8 Feb 2017 16:19:55 +1100 Subject: [PATCH] Prevent default passwords in production mode (elastic/elasticsearch#4724) Adds a new `xpack.security.authc.accept_default_password` setting that defaults to `true`. If it is set to false, then the default password is not accepted in the reserved realm. Adds a bootstrap check that the above setting must be set to `false` if security is enabled. Adds docs for the new setting and bootstrap. Changed `/_enable` and `/_disable`, to store a blank password if the user record did not previously exist, which is interpreted to mean "treat this user as having the default password". The previous functionality would explicitly set the user's password to `changeme`, which would then prevent the new configuration setting from doing its job. For any existing reserved users that had their password set to `changeme`, migrates them to the blank password (per above paragraph) Closes: elastic/elasticsearch#4333 Original commit: elastic/x-pack-elasticsearch@db645640936672353d840526ce94858573f82ff5 --- .../xpack/security/Security.java | 8 +- .../authc/esnative/NativeRealmMigrator.java | 112 +++++++++++--- .../authc/esnative/NativeUsersStore.java | 31 +++- .../authc/esnative/ReservedRealm.java | 43 ++++-- .../DefaultPasswordBootstrapCheck.java | 37 +++++ .../esnative/NativeRealmMigratorTests.java | 94 +++++++++--- .../authc/esnative/NativeUsersStoreTests.java | 137 ++++++++++++++++++ .../esnative/ReservedRealmIntegTests.java | 20 +++ ...ervedRealmNoDefaultPasswordIntegTests.java | 64 ++++++++ .../authc/esnative/ReservedRealmTests.java | 36 ++++- .../security/test/SecurityTestUtils.java | 9 +- .../test/mixed_cluster/20_security.yaml | 11 ++ .../test/old_cluster/20_security.yaml | 31 ++++ .../test/upgraded_cluster/20_security.yaml | 27 ++++ 14 files changed, 599 insertions(+), 61 deletions(-) create mode 100644 elasticsearch/src/main/java/org/elasticsearch/xpack/security/bootstrap/DefaultPasswordBootstrapCheck.java create mode 100644 elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java create mode 100644 elasticsearch/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmNoDefaultPasswordIntegTests.java 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 }