From 10f347488dfa013d1f3cc3a23658f296b3bdb0cb Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Mon, 27 Sep 2021 11:36:24 +0300 Subject: [PATCH 01/16] Main --- ...csearchAuthenticationProcessException.java | 18 +++ .../security/authc/AuthenticationResult.java | 14 ++ .../DefaultAuthenticationFailureHandler.java | 13 +- .../core/security/support/Exceptions.java | 10 ++ .../xpack/security/Security.java | 3 +- .../authc/esnative/NativeUsersStore.java | 9 +- .../authc/esnative/ReservedRealm.java | 121 +++++++++++------- ...AutoConfigGenerateElasticPasswordHash.java | 4 +- .../user/TransportGetUsersActionTests.java | 6 +- .../authc/esnative/ReservedRealmTests.java | 32 ++--- ...onfigGenerateElasticPasswordHashTests.java | 4 +- 11 files changed, 158 insertions(+), 76 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessException.java diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessException.java b/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessException.java new file mode 100644 index 0000000000000..f2841ba55a191 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessException.java @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch; + +import org.elasticsearch.rest.RestStatus; + +public class ElasticsearchAuthenticationProcessException extends ElasticsearchSecurityException { + + public ElasticsearchAuthenticationProcessException(String msg, RestStatus status, Throwable cause, Object... args) { + super(msg, status, cause, args); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationResult.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationResult.java index 459e91dec7598..bcc2bfde049e3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationResult.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationResult.java @@ -134,6 +134,20 @@ public static AuthenticationResult terminate(String message, @Nullable Exception return new AuthenticationResult(Status.TERMINATE, null, message, cause, null); } + /** + * Creates an {@code AuthenticationResult} that indicates that the realm attempted to handle the authentication request, was + * unsuccessful and wants to terminate this authentication request. + * The reason for the failure is given in the supplied message. + *

+ * The {@link #getStatus() status} is set to {@link Status#TERMINATE}. + *

+ * The {@link #getUser() user} is not populated. + *

+ */ + public static AuthenticationResult terminate(String message) { + return terminate(message, null); + } + public boolean isAuthenticated() { return status == Status.SUCCESS; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java index e0f8d4adb076f..63589f3845caf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java @@ -6,6 +6,7 @@ */ package org.elasticsearch.xpack.core.security.authc; +import org.elasticsearch.ElasticsearchAuthenticationProcessException; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.rest.RestRequest; @@ -100,12 +101,20 @@ public ElasticsearchSecurityException failedAuthentication(TransportMessage mess @Override public ElasticsearchSecurityException exceptionProcessingRequest(RestRequest request, Exception e, ThreadContext context) { + // a couple of authn processing errors can also return 500 & 503, besides the obvious 401 + if (e instanceof ElasticsearchAuthenticationProcessException) { + return (ElasticsearchAuthenticationProcessException) e; + } return createAuthenticationError("error attempting to authenticate request", e, (Object[]) null); } @Override public ElasticsearchSecurityException exceptionProcessingRequest(TransportMessage message, String action, Exception e, ThreadContext context) { + // a couple of authn processing errors can also return 500 & 503, besides the obvious 401 + if (e instanceof ElasticsearchAuthenticationProcessException) { + return (ElasticsearchAuthenticationProcessException) e; + } return createAuthenticationError("error attempting to authenticate request", e, (Object[]) null); } @@ -155,8 +164,8 @@ private ElasticsearchSecurityException createAuthenticationError(final String me * 'WWW-Authenticate' header value to communicate outToken to peer. */ containsNegotiateWithToken = - ese.getHeader("WWW-Authenticate").stream() - .anyMatch(s -> s != null && s.regionMatches(true, 0, "Negotiate ", 0, "Negotiate ".length())); + ese.getHeader("WWW-Authenticate").stream() + .anyMatch(s -> s != null && s.regionMatches(true, 0, "Negotiate ", 0, "Negotiate ".length())); } else { containsNegotiateWithToken = false; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java index 9b2652883455f..cfd9a51388097 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java @@ -6,7 +6,9 @@ */ package org.elasticsearch.xpack.core.security.support; +import org.elasticsearch.ElasticsearchAuthenticationProcessException; import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.core.XPackField; @@ -34,4 +36,12 @@ public static ElasticsearchSecurityException authorizationError(String msg, Obje public static ElasticsearchSecurityException authorizationError(String msg, Exception cause, Object... args) { return new ElasticsearchSecurityException(msg, RestStatus.FORBIDDEN, cause, args); } + + public static ElasticsearchAuthenticationProcessException authenticationProcessError(String msg, Exception cause, Object... args) { + RestStatus restStatus = RestStatus.SERVICE_UNAVAILABLE; + if (RestStatus.INTERNAL_SERVER_ERROR == ExceptionsHelper.status(ExceptionsHelper.unwrapCause(cause))) { + restStatus = RestStatus.INTERNAL_SERVER_ERROR; + } + return new ElasticsearchAuthenticationProcessException(msg, restStatus, cause, args); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index a9fca4aff7ba8..7de75dfa9e8d9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -503,8 +503,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste scriptService); final AnonymousUser anonymousUser = new AnonymousUser(settings); components.add(anonymousUser); - final ReservedRealm reservedRealm = new ReservedRealm(environment, settings, nativeUsersStore, - anonymousUser, securityIndex.get(), threadPool); + final ReservedRealm reservedRealm = new ReservedRealm(environment, settings, nativeUsersStore, anonymousUser, threadPool); final SecurityExtension.SecurityComponents extensionComponents = new ExtensionComponents(environment, client, clusterService, resourceWatcherService, nativeRoleMappingStore); Map realmFactories = new HashMap<>(InternalRealms.getFactories(threadPool, resourceWatcherService, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java index 47bdae1d846d2..49058d0c06a83 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java @@ -48,6 +48,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.esnative.ClientReservedRealm; import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.User.Fields; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -309,6 +310,10 @@ public void updateReservedUser( }); } + void createElasticUser(char[] passwordHash, ActionListener listener) { + updateReservedUser(ElasticUser.NAME, passwordHash, DocWriteRequest.OpType.CREATE, RefreshPolicy.IMMEDIATE, listener); + } + /** * Asynchronous method to put a user. A put user request without a password hash is treated as an update and will fail with a * {@link ValidationException} if the user does not exist. If a password hash is provided, then we issue a update request with an @@ -685,10 +690,6 @@ static final class ReservedUserInfo { this.hasher = Hasher.resolveFromHash(this.passwordHash); } - ReservedUserInfo deepClone() { - return new ReservedUserInfo(Arrays.copyOf(passwordHash, passwordHash.length), enabled); - } - boolean hasEmptyPassword() { return passwordHash.length == 0; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index bc1c848359092..dfb360e95ab1b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -38,7 +38,6 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ReservedUserInfo; import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm; -import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.util.ArrayList; import java.util.Arrays; @@ -46,6 +45,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; /** * A realm for predefined users. These users can only be modified in terms of changing their passwords; no other modifications are allowed. @@ -57,21 +58,26 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { public static final String NAME = "reserved"; private final ReservedUserInfo bootstrapUserInfo; + private final ReservedUserInfo autoconfigUserInfo; public static final Setting BOOTSTRAP_ELASTIC_PASSWORD = SecureSetting.secureString("bootstrap.password", KeyStoreWrapper.SEED_SETTING); - public static final Setting AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH = - SecureSetting.secureString("autoconfig.password_hash", null); + + // we do not document this setting on the website because it mustn't be set by the users + // it is only set by various installation scripts + // TODO validate that it is not empty + // TODO validate that hash is recognized + public static final Setting AUTOCONFIG_ELASTIC_PASSWORD_HASH = + SecureSetting.secureString("autoconfiguration.password_hash", null); private final NativeUsersStore nativeUsersStore; private final AnonymousUser anonymousUser; private final boolean realmEnabled; private final boolean anonymousEnabled; - private final SecurityIndexManager securityIndex; private final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(logger.getName()); public ReservedRealm(Environment env, Settings settings, NativeUsersStore nativeUsersStore, AnonymousUser anonymousUser, - SecurityIndexManager securityIndex, ThreadPool threadPool) { + ThreadPool threadPool) { super(new RealmConfig(new RealmConfig.RealmIdentifier(TYPE, NAME), Settings.builder() .put(settings) @@ -81,11 +87,21 @@ public ReservedRealm(Environment env, Settings settings, NativeUsersStore native this.realmEnabled = XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings); this.anonymousUser = anonymousUser; this.anonymousEnabled = AnonymousUser.isAnonymousEnabled(settings); - this.securityIndex = securityIndex; - final Hasher reservedRealmHasher = Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(settings)); - final char[] hash = BOOTSTRAP_ELASTIC_PASSWORD.get(settings).length() == 0 ? new char[0] : - reservedRealmHasher.hash(BOOTSTRAP_ELASTIC_PASSWORD.get(settings)); - bootstrapUserInfo = new ReservedUserInfo(hash, true); + if (AUTOCONFIG_ELASTIC_PASSWORD_HASH.exists(settings) && false == BOOTSTRAP_ELASTIC_PASSWORD.exists(settings)) { + char[] autoconfigPasswordHash = AUTOCONFIG_ELASTIC_PASSWORD_HASH.get(settings).getChars(); + if (autoconfigPasswordHash.length == 0 || Set.of(Hasher.SHA1, Hasher.MD5, Hasher.SSHA256, Hasher.NOOP) + .contains(Hasher.resolveFromHash(autoconfigPasswordHash))) { + throw new IllegalArgumentException("Invalid password hash for elastic user auto configuration"); + } + autoconfigUserInfo = new ReservedUserInfo(autoconfigPasswordHash, true); + bootstrapUserInfo = null; + } else { + autoconfigUserInfo = null; + final Hasher reservedRealmHasher = Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(settings)); + final char[] hash = BOOTSTRAP_ELASTIC_PASSWORD.get(settings).length() == 0 ? new char[0] : + reservedRealmHasher.hash(BOOTSTRAP_ELASTIC_PASSWORD.get(settings)); + bootstrapUserInfo = new ReservedUserInfo(hash, true); + } } @Override @@ -95,29 +111,42 @@ protected void doAuthenticate(UsernamePasswordToken token, ActionListener { - AuthenticationResult result; + getUserInfo(token.principal(), (userInfo) -> { if (userInfo != null) { - try { - if (userInfo.hasEmptyPassword()) { - result = AuthenticationResult.terminate("failed to authenticate user [" + token.principal() + "]", null); - } else if (userInfo.verifyPassword(token.credentials())) { + if (userInfo.hasEmptyPassword()) { + listener.onResponse(AuthenticationResult.terminate("failed to authenticate user [" + token.principal() + "]")); + } else { + ActionListener hashCleanupListener = ActionListener.runBefore(listener, () -> { + if (userInfo != null && userInfo != bootstrapUserInfo && userInfo != autoconfigUserInfo) { + Arrays.fill(userInfo.passwordHash, (char) 0); + } + }); + if (userInfo.verifyPassword(token.credentials())) { final User user = getUser(token.principal(), userInfo); logDeprecatedUser(user); - result = AuthenticationResult.success(user); + // promote the auto-configured password as the elastic user password + if (userInfo == autoconfigUserInfo) { + nativeUsersStore.createElasticUser(userInfo.passwordHash, ActionListener.wrap(aVoid -> { + hashCleanupListener.onResponse(AuthenticationResult.success(user)); + }, e -> { + // exceptionally, we must propagate a 500 or a 503 error if the auto config password hash + // can't be promoted as the elastic user password, otherwise, such errors will + // implicitly translate to 401s, which is wrong because the presented password was successfully + // verified by the auto-config hash; the client must retry the request. + listener.onFailure(Exceptions.authenticationProcessError("failed to promote the auto-configured " + + "elastic password hash", e)); + })); + } else { + hashCleanupListener.onResponse(AuthenticationResult.success(user)); + } } else { - result = AuthenticationResult.terminate("failed to authenticate user [" + token.principal() + "]", null); + hashCleanupListener.onResponse(AuthenticationResult.terminate("failed to authenticate user [" + token.principal() + "]")); } - } finally { - assert userInfo.passwordHash != bootstrapUserInfo.passwordHash : "bootstrap user info must be cloned"; - Arrays.fill(userInfo.passwordHash, (char) 0); } } else { - result = AuthenticationResult.terminate("failed to authenticate user [" + token.principal() + "]", null); + listener.onResponse(AuthenticationResult.terminate("failed to authenticate user [" + token.principal() + "]")); } - // we want the finally block to clear out the chars before we proceed further so we handle the result here - listener.onResponse(result); - }, listener::onFailure)); + }); } } @@ -134,14 +163,14 @@ protected void doLookupUser(String username, ActionListener listener) { } else if (AnonymousUser.isAnonymousUsername(username, config.settings())) { listener.onResponse(anonymousEnabled ? anonymousUser : null); } else { - getUserInfo(username, ActionListener.wrap((userInfo) -> { + getUserInfo(username, (userInfo) -> { if (userInfo != null) { listener.onResponse(getUser(username, userInfo)); } else { // this was a reserved username - don't allow this to go to another realm... listener.onFailure(Exceptions.authenticationError("failed to lookup user [{}]", username)); } - }, listener::onFailure)); + }); } } @@ -211,23 +240,18 @@ public void users(ActionListener> listener) { } } - - private void getUserInfo(final String username, ActionListener listener) { - if (securityIndex.indexExists() == false) { - listener.onResponse(getDefaultUserInfo(username)); - } else { - nativeUsersStore.getReservedUserInfo(username, ActionListener.wrap((userInfo) -> { - if (userInfo == null) { - listener.onResponse(getDefaultUserInfo(username)); - } else { - listener.onResponse(userInfo); - } - }, (e) -> { - logger.error((Supplier) () -> - new ParameterizedMessage("failed to retrieve password hash for reserved user [{}]", username), e); - listener.onResponse(null); - })); - } + private void getUserInfo(final String username, Consumer consumer) { + nativeUsersStore.getReservedUserInfo(username, ActionListener.wrap((userInfo) -> { + if (userInfo == null) { + consumer.accept(getDefaultUserInfo(username)); + } else { + consumer.accept(userInfo); + } + }, (e) -> { + logger.error((Supplier) () -> + new ParameterizedMessage("failed to retrieve password hash for reserved user [{}]", username), e); + consumer.accept(null); + })); } private void logDeprecatedUser(final User user){ @@ -242,7 +266,13 @@ private void logDeprecatedUser(final User user){ private ReservedUserInfo getDefaultUserInfo(String username) { if (ElasticUser.NAME.equals(username)) { - return bootstrapUserInfo.deepClone(); + if (autoconfigUserInfo != null) { + assert bootstrapUserInfo == null; + return autoconfigUserInfo; + } else { + assert bootstrapUserInfo != null; + return bootstrapUserInfo; + } } else { return ReservedUserInfo.defaultEnabledUserInfo(); } @@ -250,5 +280,6 @@ private ReservedUserInfo getDefaultUserInfo(String username) { public static void addSettings(List> settingsList) { settingsList.add(BOOTSTRAP_ELASTIC_PASSWORD); + settingsList.add(AUTOCONFIG_ELASTIC_PASSWORD_HASH); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHash.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHash.java index c96aeead444a0..b762f7648775b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHash.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHash.java @@ -19,7 +19,7 @@ import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.authc.support.Hasher; -import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH; +import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_ELASTIC_PASSWORD_HASH; import static org.elasticsearch.xpack.security.tool.CommandUtils.generatePassword; /** @@ -53,7 +53,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th SecureString elasticPassword = new SecureString(generatePassword(20)); KeyStoreWrapper nodeKeystore = KeyStoreWrapper.bootstrap(env.configFile(), () -> new SecureString(new char[0])) ) { - nodeKeystore.setString(AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH.getKey(), hasher.hash(elasticPassword)); + nodeKeystore.setString(AUTOCONFIG_ELASTIC_PASSWORD_HASH.getKey(), hasher.hash(elasticPassword)); nodeKeystore.save(env.configFile(), new char[0]); terminal.print(Terminal.Verbosity.NORMAL, elasticPassword.toString()); } catch (Exception e) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersActionTests.java index ea97f4bd26755..c0e6c61855380 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersActionTests.java @@ -91,7 +91,7 @@ public void testAnonymousUser() { when(securityIndex.isAvailable()).thenReturn(true); AnonymousUser anonymousUser = new AnonymousUser(settings); ReservedRealm reservedRealm = - new ReservedRealm(mock(Environment.class), settings, usersStore, anonymousUser, securityIndex, threadPool); + new ReservedRealm(mock(Environment.class), settings, usersStore, anonymousUser, threadPool); TransportService transportService = new TransportService(Settings.EMPTY, mock(Transport.class), null, TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null, Collections.emptySet()); TransportGetUsersAction action = new TransportGetUsersAction(Settings.EMPTY, mock(ActionFilters.class), @@ -163,7 +163,7 @@ public void testReservedUsersOnly() { ReservedRealmTests.mockGetAllReservedUserInfo(usersStore, Collections.emptyMap()); ReservedRealm reservedRealm = - new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings), securityIndex, threadPool); + new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings), threadPool); PlainActionFuture> userFuture = new PlainActionFuture<>(); reservedRealm.users(userFuture); final Collection allReservedUsers = userFuture.actionGet(); @@ -209,7 +209,7 @@ public void testGetAllUsers() { when(securityIndex.isAvailable()).thenReturn(true); ReservedRealmTests.mockGetAllReservedUserInfo(usersStore, Collections.emptyMap()); ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings), - securityIndex, threadPool); + threadPool); TransportService transportService = new TransportService(Settings.EMPTY, mock(Transport.class), null, TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null, Collections.emptySet()); TransportGetUsersAction action = new TransportGetUsersAction(Settings.EMPTY, mock(ActionFilters.class), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index 5d3f2f518bb19..a4382099adccf 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -84,7 +84,7 @@ public void testInvalidHashingAlgorithmFails() { final String invalidAlgoId = randomFrom("sha1", "md5", "noop"); final Settings invalidSettings = Settings.builder().put("xpack.security.authc.password_hashing.algorithm", invalidAlgoId).build(); IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> new ReservedRealm(mock(Environment.class), - invalidSettings, usersStore, new AnonymousUser(Settings.EMPTY), securityIndex, threadPool)); + invalidSettings, usersStore, new AnonymousUser(Settings.EMPTY), threadPool)); assertThat(exception.getMessage(), containsString(invalidAlgoId)); assertThat(exception.getMessage(), containsString("Invalid algorithm")); } @@ -94,7 +94,7 @@ public void testReservedUserEmptyPasswordAuthenticationFails() throws Throwable UsernamesField.BEATS_NAME); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, - new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + new AnonymousUser(Settings.EMPTY), threadPool); PlainActionFuture listener = new PlainActionFuture<>(); @@ -110,7 +110,7 @@ public void testAuthenticationDisabled() throws Throwable { } final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, - new AnonymousUser(settings), securityIndex, threadPool); + new AnonymousUser(settings), threadPool); final User expected = randomReservedUser(true); final String principal = expected.principal(); @@ -132,7 +132,7 @@ public void testAuthenticationDisabledUserWithStoredPassword() throws Throwable private void verifySuccessfulAuthentication(boolean enabled) throws Exception { final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, - new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + new AnonymousUser(Settings.EMPTY), threadPool); final User expectedUser = randomReservedUser(enabled); final String principal = expectedUser.principal(); final SecureString newPassword = new SecureString("foobar".toCharArray()); @@ -178,7 +178,7 @@ private Answer> getAnswer(boolean enabled, SecureString newPassword, public void testLookup() throws Exception { final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, - new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + new AnonymousUser(Settings.EMPTY), threadPool); final User expectedUser = randomReservedUser(true); final String principal = expectedUser.principal(); @@ -199,7 +199,7 @@ public void testLookupDisabled() throws Exception { Settings settings = Settings.builder().put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false).build(); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings), - securityIndex, threadPool); + threadPool); final User expectedUser = randomReservedUser(true); final String principal = expectedUser.principal(); @@ -218,7 +218,7 @@ public void testLookupDisabledAnonymous() throws Exception { .build(); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings), - securityIndex, threadPool); + threadPool); final User expectedUser = new AnonymousUser(settings); final String principal = expectedUser.principal(); @@ -230,7 +230,7 @@ public void testLookupDisabledAnonymous() throws Exception { public void testLookupThrows() throws Exception { final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, - new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + new AnonymousUser(Settings.EMPTY), threadPool); final User expectedUser = randomReservedUser(true); final String principal = expectedUser.principal(); when(securityIndex.indexExists()).thenReturn(true); @@ -273,7 +273,7 @@ public void testIsReservedDisabled() { public void testGetUsers() { final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, - new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + new AnonymousUser(Settings.EMPTY), threadPool); PlainActionFuture> userFuture = new PlainActionFuture<>(); reservedRealm.users(userFuture); assertThat(userFuture.actionGet(), @@ -289,7 +289,7 @@ public void testGetUsersDisabled() { .build(); final AnonymousUser anonymousUser = new AnonymousUser(settings); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, anonymousUser, - securityIndex, threadPool); + threadPool); PlainActionFuture> userFuture = new PlainActionFuture<>(); reservedRealm.users(userFuture); if (anonymousEnabled) { @@ -308,7 +308,7 @@ public void testFailedAuthentication() throws Exception { ReservedUserInfo userInfo = new ReservedUserInfo(hash, true); mockGetAllReservedUserInfo(usersStore, Collections.singletonMap("elastic", userInfo)); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, - new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + new AnonymousUser(Settings.EMPTY), threadPool); if (randomBoolean()) { PlainActionFuture future = new PlainActionFuture<>(); @@ -340,7 +340,7 @@ public void testBootstrapElasticPasswordWorksOnceSecurityIndexExists() throws Ex when(securityIndex.indexExists()).thenReturn(true); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, - new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + new AnonymousUser(Settings.EMPTY), threadPool); PlainActionFuture listener = new PlainActionFuture<>(); doAnswer((i) -> { @@ -363,7 +363,7 @@ public void testBootstrapElasticPasswordFailsOnceElasticUserExists() throws Exce when(securityIndex.indexExists()).thenReturn(true); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, - new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + new AnonymousUser(Settings.EMPTY), threadPool); PlainActionFuture listener = new PlainActionFuture<>(); SecureString password = new SecureString("password".toCharArray()); // Mocked users store is initiated with default hashing algorithm @@ -393,7 +393,7 @@ public void testBootstrapElasticPasswordWorksBeforeSecurityIndexExists() throws when(securityIndex.indexExists()).thenReturn(false); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, - new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + new AnonymousUser(Settings.EMPTY), threadPool); PlainActionFuture listener = new PlainActionFuture<>(); reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), @@ -411,7 +411,7 @@ public void testNonElasticUsersCannotUseBootstrapPasswordWhenSecurityIndexExists when(securityIndex.indexExists()).thenReturn(true); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, - new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + new AnonymousUser(Settings.EMPTY), threadPool); PlainActionFuture listener = new PlainActionFuture<>(); final String principal = randomFrom(KibanaUser.NAME, KibanaSystemUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME, @@ -434,7 +434,7 @@ public void testNonElasticUsersCannotUseBootstrapPasswordWhenSecurityIndexDoesNo when(securityIndex.indexExists()).thenReturn(false); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, - new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + new AnonymousUser(Settings.EMPTY), threadPool); PlainActionFuture listener = new PlainActionFuture<>(); final String principal = randomFrom(KibanaUser.NAME, KibanaSystemUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME, diff --git a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHashTests.java b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHashTests.java index 2b279eb9b3c11..b9b56f0ec8ea5 100644 --- a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHashTests.java +++ b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHashTests.java @@ -31,7 +31,7 @@ import java.util.Map; import static org.elasticsearch.test.SecurityIntegTestCase.getFastStoredHashAlgoForTests; -import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH; +import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_ELASTIC_PASSWORD_HASH; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.equalTo; @@ -98,7 +98,7 @@ public void testSuccessfullyGenerateAndStoreHash() throws Exception { assertNotNull(keyStoreWrapper); keyStoreWrapper.decrypt(new char[0]); assertThat(keyStoreWrapper.getSettingNames(), - containsInAnyOrder(AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH.getKey(), "keystore.seed")); + containsInAnyOrder(AUTOCONFIG_ELASTIC_PASSWORD_HASH.getKey(), "keystore.seed")); } public void testExistingKeystoreWithWrongPassword() throws Exception { From 6076aab171ce30b33b050cdc13c15c4310799fa0 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Mon, 27 Sep 2021 12:07:05 +0300 Subject: [PATCH 02/16] Nit rename & javadoc --- ...csearchAuthenticationProcessException.java | 18 ------------ ...icsearchAuthenticationProcessingError.java | 29 +++++++++++++++++++ .../DefaultAuthenticationFailureHandler.java | 14 ++++----- .../core/security/support/Exceptions.java | 6 ++-- 4 files changed, 39 insertions(+), 28 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessException.java create mode 100644 server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessingError.java diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessException.java b/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessException.java deleted file mode 100644 index f2841ba55a191..0000000000000 --- a/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessException.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch; - -import org.elasticsearch.rest.RestStatus; - -public class ElasticsearchAuthenticationProcessException extends ElasticsearchSecurityException { - - public ElasticsearchAuthenticationProcessException(String msg, RestStatus status, Throwable cause, Object... args) { - super(msg, status, cause, args); - } -} diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessingError.java b/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessingError.java new file mode 100644 index 0000000000000..cf542d79e3afd --- /dev/null +++ b/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessingError.java @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch; + +import org.elasticsearch.rest.RestStatus; + +/** + * Used to indicate that the authentication process encountered a server-side error (5xx) that prevented the credentials verification. + * The presented client credentials might or might not be valid. + * This differs from an authentication failure error in subtle ways. This should be preferred when the issue hindering credentials + * verification is transient, such as network congestion or overloaded instances, but not in cases of misconfiguration. + * However this distinction is further blurred because in certain configurations and for certain credential types, the same + * credential can be validated in multiple ways, only some of which might experience transient problems. + * When in doubt, rely on the implicit behavior of 401 authentication failure. + */ +public class ElasticsearchAuthenticationProcessingError extends ElasticsearchSecurityException { + + public ElasticsearchAuthenticationProcessingError(String msg, RestStatus status, Throwable cause, Object... args) { + super(msg, status, cause, args); + assert status == RestStatus.INTERNAL_SERVER_ERROR; + assert status == RestStatus.SERVICE_UNAVAILABLE; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java index 63589f3845caf..ca3c35e54d265 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java @@ -6,7 +6,7 @@ */ package org.elasticsearch.xpack.core.security.authc; -import org.elasticsearch.ElasticsearchAuthenticationProcessException; +import org.elasticsearch.ElasticsearchAuthenticationProcessingError; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.rest.RestRequest; @@ -102,8 +102,8 @@ public ElasticsearchSecurityException failedAuthentication(TransportMessage mess @Override public ElasticsearchSecurityException exceptionProcessingRequest(RestRequest request, Exception e, ThreadContext context) { // a couple of authn processing errors can also return 500 & 503, besides the obvious 401 - if (e instanceof ElasticsearchAuthenticationProcessException) { - return (ElasticsearchAuthenticationProcessException) e; + if (e instanceof ElasticsearchAuthenticationProcessingError) { + return (ElasticsearchAuthenticationProcessingError) e; } return createAuthenticationError("error attempting to authenticate request", e, (Object[]) null); } @@ -112,8 +112,8 @@ public ElasticsearchSecurityException exceptionProcessingRequest(RestRequest req public ElasticsearchSecurityException exceptionProcessingRequest(TransportMessage message, String action, Exception e, ThreadContext context) { // a couple of authn processing errors can also return 500 & 503, besides the obvious 401 - if (e instanceof ElasticsearchAuthenticationProcessException) { - return (ElasticsearchAuthenticationProcessException) e; + if (e instanceof ElasticsearchAuthenticationProcessingError) { + return (ElasticsearchAuthenticationProcessingError) e; } return createAuthenticationError("error attempting to authenticate request", e, (Object[]) null); } @@ -164,8 +164,8 @@ private ElasticsearchSecurityException createAuthenticationError(final String me * 'WWW-Authenticate' header value to communicate outToken to peer. */ containsNegotiateWithToken = - ese.getHeader("WWW-Authenticate").stream() - .anyMatch(s -> s != null && s.regionMatches(true, 0, "Negotiate ", 0, "Negotiate ".length())); + ese.getHeader("WWW-Authenticate").stream() + .anyMatch(s -> s != null && s.regionMatches(true, 0, "Negotiate ", 0, "Negotiate ".length())); } else { containsNegotiateWithToken = false; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java index cfd9a51388097..ed3e0204c6dd5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java @@ -6,7 +6,7 @@ */ package org.elasticsearch.xpack.core.security.support; -import org.elasticsearch.ElasticsearchAuthenticationProcessException; +import org.elasticsearch.ElasticsearchAuthenticationProcessingError; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.rest.RestStatus; @@ -37,11 +37,11 @@ public static ElasticsearchSecurityException authorizationError(String msg, Exce return new ElasticsearchSecurityException(msg, RestStatus.FORBIDDEN, cause, args); } - public static ElasticsearchAuthenticationProcessException authenticationProcessError(String msg, Exception cause, Object... args) { + public static ElasticsearchAuthenticationProcessingError authenticationProcessError(String msg, Exception cause, Object... args) { RestStatus restStatus = RestStatus.SERVICE_UNAVAILABLE; if (RestStatus.INTERNAL_SERVER_ERROR == ExceptionsHelper.status(ExceptionsHelper.unwrapCause(cause))) { restStatus = RestStatus.INTERNAL_SERVER_ERROR; } - return new ElasticsearchAuthenticationProcessException(msg, restStatus, cause, args); + return new ElasticsearchAuthenticationProcessingError(msg, restStatus, cause, args); } } From 674202dd1538134553876684fb808d70a0adb80e Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Mon, 27 Sep 2021 12:08:59 +0300 Subject: [PATCH 03/16] ReservedRealm security index param remove fallout --- .../security/action/user/TransportPutUserActionTests.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportPutUserActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportPutUserActionTests.java index e489d36c7c544..864a11878e210 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportPutUserActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportPutUserActionTests.java @@ -121,14 +121,12 @@ public void onFailure(Exception e) { public void testReservedUser() { NativeUsersStore usersStore = mock(NativeUsersStore.class); - SecurityIndexManager securityIndex = mock(SecurityIndexManager.class); - when(securityIndex.isAvailable()).thenReturn(true); ReservedRealmTests.mockGetAllReservedUserInfo(usersStore, Collections.emptyMap()); Settings settings = Settings.builder().put("path.home", createTempDir()).build(); final ThreadPool threadPool = mock(ThreadPool.class); when(threadPool.getThreadContext()).thenReturn(new ThreadContext(settings)); ReservedRealm reservedRealm = new ReservedRealm(TestEnvironment.newEnvironment(settings), settings, usersStore, - new AnonymousUser(settings), securityIndex, threadPool); + new AnonymousUser(settings), threadPool); PlainActionFuture> userFuture = new PlainActionFuture<>(); reservedRealm.users(userFuture); final User reserved = randomFrom(userFuture.actionGet().toArray(new User[0])); From ccd1dbe0094c5dd9f6089c57f44f6863cf43b692 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Mon, 27 Sep 2021 21:39:12 +0300 Subject: [PATCH 04/16] WIP --- .../xpack/security/authc/esnative/ReservedRealm.java | 5 ++--- .../security/action/user/TransportPutUserActionTests.java | 1 - .../xpack/security/authc/esnative/ReservedRealmTests.java | 7 ++++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index dfb360e95ab1b..e7fae896cf9e1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -64,8 +64,6 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { // we do not document this setting on the website because it mustn't be set by the users // it is only set by various installation scripts - // TODO validate that it is not empty - // TODO validate that hash is recognized public static final Setting AUTOCONFIG_ELASTIC_PASSWORD_HASH = SecureSetting.secureString("autoconfiguration.password_hash", null); @@ -140,7 +138,8 @@ protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener = new PlainActionFuture<>(); From c2092d8d5c6a26b5a0bd848f7f2b893643d362bd Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 28 Sep 2021 07:36:25 +0300 Subject: [PATCH 05/16] ReservedRealm tests --- .../authc/esnative/ReservedRealmTests.java | 79 ++++--------------- 1 file changed, 17 insertions(+), 62 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index 66d120b9c66f0..9c827fe176752 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -9,7 +9,6 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; -import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; @@ -33,7 +32,6 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.UsernamesField; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ReservedUserInfo; -import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.Before; import org.mockito.stubbing.Answer; @@ -41,7 +39,6 @@ import java.util.Collections; import java.util.Map; import java.util.Map.Entry; -import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; import static org.elasticsearch.test.ActionListenerUtils.anyActionListener; @@ -52,6 +49,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -68,15 +66,11 @@ public class ReservedRealmTests extends ESTestCase { private static final SecureString EMPTY_PASSWORD = new SecureString("".toCharArray()); private NativeUsersStore usersStore; - private SecurityIndexManager securityIndex; private ThreadPool threadPool; @Before public void setupMocks() throws Exception { usersStore = mock(NativeUsersStore.class); - securityIndex = mock(SecurityIndexManager.class); - when(securityIndex.isAvailable()).thenReturn(true); - when(securityIndex.freeze()).thenReturn(securityIndex); mockGetAllReservedUserInfo(usersStore, Collections.emptyMap()); threadPool = mock(ThreadPool.class); when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); @@ -96,6 +90,12 @@ public void testInvalidHashingAlgorithmFails() { public void testReservedUserEmptyPasswordAuthenticationFails() throws Throwable { final String principal = randomFrom(UsernamesField.ELASTIC_NAME, UsernamesField.KIBANA_NAME, UsernamesField.LOGSTASH_NAME, UsernamesField.BEATS_NAME); + SecureString password = new SecureString("password".toCharArray()); + // Mocked users store is initiated with default hashing algorithm + final Hasher hasher = Hasher.resolve("bcrypt"); + char[] hash = hasher.hash(password); + ReservedUserInfo userInfo = new ReservedUserInfo(hash, true); + mockGetAllReservedUserInfo(usersStore, Collections.singletonMap(principal, userInfo)); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY), threadPool); @@ -108,10 +108,6 @@ public void testReservedUserEmptyPasswordAuthenticationFails() throws Throwable public void testAuthenticationDisabled() throws Throwable { Settings settings = Settings.builder().put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false).build(); - final boolean securityIndexExists = randomBoolean(); - if (securityIndexExists) { - when(securityIndex.indexExists()).thenReturn(true); - } final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings), threadPool); @@ -142,7 +138,6 @@ private void verifySuccessfulAuthentication(boolean enabled) throws Exception { final SecureString newPassword = new SecureString("foobar".toCharArray()); // Mocked users store is initiated with default hashing algorithm final Hasher hasher = Hasher.resolve("bcrypt"); - when(securityIndex.indexExists()).thenReturn(true); doAnswer(getAnswer(enabled, newPassword, hasher)).when(usersStore).getReservedUserInfo(eq(principal), anyActionListener()); // test empty password @@ -160,7 +155,6 @@ private void verifySuccessfulAuthentication(boolean enabled) throws Exception { assertEquals(expectedUser, authenticated); assertThat(expectedUser.enabled(), is(enabled)); - verify(securityIndex, times(2)).indexExists(); verify(usersStore, times(2)).getReservedUserInfo(eq(principal), anyActionListener()); verifyNoMoreInteractions(usersStore); @@ -190,7 +184,7 @@ public void testLookup() throws Exception { reservedRealm.doLookupUser(principal, listener); final User user = listener.actionGet(); assertEquals(expectedUser, user); - verify(securityIndex).indexExists(); + verify(usersStore).getReservedUserInfo(eq(principal), anyActionListener()); PlainActionFuture future = new PlainActionFuture<>(); reservedRealm.doLookupUser("foobar", assertListenerIsOnlyCalledOnce(future)); @@ -214,7 +208,6 @@ public void testLookupDisabled() throws Exception { verifyZeroInteractions(usersStore); } - public void testLookupDisabledAnonymous() throws Exception { Settings settings = Settings.builder() .put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false) @@ -237,7 +230,6 @@ public void testLookupThrows() throws Exception { new AnonymousUser(Settings.EMPTY), threadPool); final User expectedUser = randomReservedUser(true); final String principal = expectedUser.principal(); - when(securityIndex.indexExists()).thenReturn(true); final RuntimeException e = new RuntimeException("store threw"); doAnswer((i) -> { ActionListener callback = (ActionListener) i.getArguments()[1]; @@ -250,7 +242,6 @@ public void testLookupThrows() throws Exception { ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, future::actionGet); assertThat(securityException.getMessage(), containsString("failed to lookup")); - verify(securityIndex).indexExists(); verify(usersStore).getReservedUserInfo(eq(principal), anyActionListener()); verifyNoMoreInteractions(usersStore); @@ -304,7 +295,6 @@ public void testGetUsersDisabled() { } public void testFailedAuthentication() throws Exception { - when(securityIndex.indexExists()).thenReturn(true); SecureString password = new SecureString("password".toCharArray()); // Mocked users store is initiated with default hashing algorithm final Hasher hasher = Hasher.resolve("bcrypt"); @@ -336,20 +326,18 @@ private void assertFailedAuthentication(PlainActionFuture assertThat(result.getException(), is(nullValue())); } - @SuppressWarnings("unchecked") - public void testBootstrapElasticPasswordWorksOnceSecurityIndexExists() throws Exception { + public void testBootstrapElasticPasswordWorksWhenElasticUserIsMissing() throws Exception { MockSecureSettings mockSecureSettings = new MockSecureSettings(); mockSecureSettings.setString("bootstrap.password", "foobar"); Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); - when(securityIndex.indexExists()).thenReturn(true); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(Settings.EMPTY), threadPool); PlainActionFuture listener = new PlainActionFuture<>(); doAnswer((i) -> { - @SuppressWarnings("rawtypes") - ActionListener callback = (ActionListener) i.getArguments()[1]; + @SuppressWarnings("unchecked") + ActionListener callback = (ActionListener) i.getArguments()[1]; callback.onResponse(null); return null; }).when(usersStore).getReservedUserInfo(eq("elastic"), anyActionListener()); @@ -364,7 +352,6 @@ public void testBootstrapElasticPasswordFailsOnceElasticUserExists() throws Exce MockSecureSettings mockSecureSettings = new MockSecureSettings(); mockSecureSettings.setString("bootstrap.password", "foobar"); Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); - when(securityIndex.indexExists()).thenReturn(true); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(Settings.EMPTY), threadPool); @@ -390,29 +377,11 @@ public void testBootstrapElasticPasswordFailsOnceElasticUserExists() throws Exce assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); } - public void testBootstrapElasticPasswordWorksBeforeSecurityIndexExists() throws ExecutionException, InterruptedException { - MockSecureSettings mockSecureSettings = new MockSecureSettings(); - mockSecureSettings.setString("bootstrap.password", "foobar"); - Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); - when(securityIndex.indexExists()).thenReturn(false); - - final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, - new AnonymousUser(Settings.EMPTY), threadPool); - PlainActionFuture listener = new PlainActionFuture<>(); - - reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), - mockSecureSettings.getString("bootstrap.password")), - listener); - final AuthenticationResult result = listener.get(); - assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); - } - - public void testNonElasticUsersCannotUseBootstrapPasswordWhenSecurityIndexExists() throws Exception { + public void testNonElasticUsersCannotUseBootstrapPassword() throws Exception { final MockSecureSettings mockSecureSettings = new MockSecureSettings(); final String password = randomAlphaOfLengthBetween(8, 24); mockSecureSettings.setString("bootstrap.password", password); Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); - when(securityIndex.indexExists()).thenReturn(true); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(Settings.EMPTY), threadPool); @@ -430,25 +399,6 @@ public void testNonElasticUsersCannotUseBootstrapPasswordWhenSecurityIndexExists assertThat(result.getStatus(), is(AuthenticationResult.Status.TERMINATE)); } - public void testNonElasticUsersCannotUseBootstrapPasswordWhenSecurityIndexDoesNotExists() throws Exception { - final MockSecureSettings mockSecureSettings = new MockSecureSettings(); - final String password = randomAlphaOfLengthBetween(8, 24); - mockSecureSettings.setString("bootstrap.password", password); - Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); - when(securityIndex.indexExists()).thenReturn(false); - NativeUsersStore nativeUsersStore = new NativeUsersStore(settings, mock(NodeClient.class), securityIndex); - - final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, nativeUsersStore, - new AnonymousUser(Settings.EMPTY), threadPool); - PlainActionFuture listener = new PlainActionFuture<>(); - - final String principal = randomFrom(KibanaUser.NAME, KibanaSystemUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME, - APMSystemUser.NAME, RemoteMonitoringUser.NAME); - reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, mockSecureSettings.getString("bootstrap.password")), listener); - final AuthenticationResult result = listener.get(); - assertThat(result.getStatus(), is(AuthenticationResult.Status.TERMINATE)); - } - private User randomReservedUser(boolean enabled) { return randomFrom(new ElasticUser(enabled), new KibanaUser(enabled), new KibanaSystemUser(enabled), new LogstashSystemUser(enabled), new BeatsSystemUser(enabled), new APMSystemUser(enabled), new RemoteMonitoringUser(enabled)); @@ -464,6 +414,11 @@ public static void mockGetAllReservedUserInfo(NativeUsersStore usersStore, Map { + ((ActionListener) i.getArguments()[1]).onResponse(null); + return null; + }).when(usersStore).getReservedUserInfo(anyString(), anyActionListener()); + for (Entry entry : collection.entrySet()) { doAnswer((i) -> { ((ActionListener) i.getArguments()[1]).onResponse(entry.getValue()); From e8b390c1fd1e99e4ae81ce7d8bed012c6038917a Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 28 Sep 2021 09:30:31 +0300 Subject: [PATCH 06/16] Exception serialization test --- .../ElasticsearchAuthenticationProcessingError.java | 7 +++++++ .../java/org/elasticsearch/ElasticsearchException.java | 7 ++++++- .../org/elasticsearch/ExceptionSerializationTests.java | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessingError.java b/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessingError.java index cf542d79e3afd..af50de707f0f6 100644 --- a/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessingError.java +++ b/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessingError.java @@ -8,8 +8,11 @@ package org.elasticsearch; +import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.rest.RestStatus; +import java.io.IOException; + /** * Used to indicate that the authentication process encountered a server-side error (5xx) that prevented the credentials verification. * The presented client credentials might or might not be valid. @@ -26,4 +29,8 @@ public ElasticsearchAuthenticationProcessingError(String msg, RestStatus status, assert status == RestStatus.INTERNAL_SERVER_ERROR; assert status == RestStatus.SERVICE_UNAVAILABLE; } + + public ElasticsearchAuthenticationProcessingError(StreamInput in) throws IOException { + super(in); + } } diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchException.java b/server/src/main/java/org/elasticsearch/ElasticsearchException.java index e592ba06240c2..5fd18008826d5 100644 --- a/server/src/main/java/org/elasticsearch/ElasticsearchException.java +++ b/server/src/main/java/org/elasticsearch/ElasticsearchException.java @@ -1038,7 +1038,12 @@ private enum ElasticsearchExceptionHandle { org.elasticsearch.action.search.VersionMismatchException.class, org.elasticsearch.action.search.VersionMismatchException::new, 161, - Version.V_7_12_0); + Version.V_7_12_0), + AUTHENTICATION_PROCESSING_ERROR( + org.elasticsearch.ElasticsearchAuthenticationProcessingError.class, + org.elasticsearch.ElasticsearchAuthenticationProcessingError::new, + 162, + Version.V_7_16_0); final Class exceptionClass; final CheckedFunction constructor; diff --git a/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java b/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java index 6e6f3a7aed5d1..64f0869b17502 100644 --- a/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java @@ -822,6 +822,7 @@ public void testIds() { ids.put(159, NodeHealthCheckFailureException.class); ids.put(160, NoSeedNodeLeftException.class); ids.put(161, VersionMismatchException.class); + ids.put(162, ElasticsearchAuthenticationProcessingError.class); Map, Integer> reverse = new HashMap<>(); for (Map.Entry> entry : ids.entrySet()) { From 93abfb2d3d6946be6360facf4cff17dd63cf5037 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 28 Sep 2021 13:31:15 +0300 Subject: [PATCH 07/16] Almost --- ...icsearchAuthenticationProcessingError.java | 3 +- .../authc/esnative/ReservedRealm.java | 8 +- .../authc/esnative/ReservedRealmTests.java | 133 +++++++++++++++++- 3 files changed, 137 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessingError.java b/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessingError.java index af50de707f0f6..c12ef1c213bf2 100644 --- a/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessingError.java +++ b/server/src/main/java/org/elasticsearch/ElasticsearchAuthenticationProcessingError.java @@ -26,8 +26,7 @@ public class ElasticsearchAuthenticationProcessingError extends ElasticsearchSec public ElasticsearchAuthenticationProcessingError(String msg, RestStatus status, Throwable cause, Object... args) { super(msg, status, cause, args); - assert status == RestStatus.INTERNAL_SERVER_ERROR; - assert status == RestStatus.SERVICE_UNAVAILABLE; + assert status == RestStatus.INTERNAL_SERVER_ERROR || status == RestStatus.SERVICE_UNAVAILABLE; } public ElasticsearchAuthenticationProcessingError(StreamInput in) throws IOException { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index e7fae896cf9e1..2625bd869d6c4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -85,12 +85,16 @@ public ReservedRealm(Environment env, Settings settings, NativeUsersStore native this.realmEnabled = XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings); this.anonymousUser = anonymousUser; this.anonymousEnabled = AnonymousUser.isAnonymousEnabled(settings); - if (AUTOCONFIG_ELASTIC_PASSWORD_HASH.exists(settings) && false == BOOTSTRAP_ELASTIC_PASSWORD.exists(settings)) { - char[] autoconfigPasswordHash = AUTOCONFIG_ELASTIC_PASSWORD_HASH.get(settings).getChars(); + char[] autoconfigPasswordHash = null; + // validate the password hash setting value, even if it is not going to be used + if (AUTOCONFIG_ELASTIC_PASSWORD_HASH.exists(settings)) { + autoconfigPasswordHash = AUTOCONFIG_ELASTIC_PASSWORD_HASH.get(settings).getChars(); if (autoconfigPasswordHash.length == 0 || Set.of(Hasher.SHA1, Hasher.MD5, Hasher.SSHA256, Hasher.NOOP) .contains(Hasher.resolveFromHash(autoconfigPasswordHash))) { throw new IllegalArgumentException("Invalid password hash for elastic user auto configuration"); } + } + if (AUTOCONFIG_ELASTIC_PASSWORD_HASH.exists(settings) && false == BOOTSTRAP_ELASTIC_PASSWORD.exists(settings)) { autoconfigUserInfo = new ReservedUserInfo(autoconfigPasswordHash, true); bootstrapUserInfo = null; } else { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index 9c827fe176752..c0192d6bebe2a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -6,6 +6,7 @@ */ package org.elasticsearch.xpack.security.authc.esnative; +import org.elasticsearch.ElasticsearchAuthenticationProcessingError; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; @@ -14,6 +15,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; +import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.XPackSettings; @@ -33,12 +35,14 @@ import org.elasticsearch.xpack.core.security.user.UsernamesField; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ReservedUserInfo; import org.junit.Before; +import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; import static org.elasticsearch.test.ActionListenerUtils.anyActionListener; @@ -47,8 +51,10 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; @@ -85,7 +91,24 @@ public void testInvalidHashingAlgorithmFails() { assertThat(exception.getMessage(), containsString("Invalid algorithm")); } - // TODO check Reserved Realm auto config hash error + public void testInvalidAutoConfigPasswordHashFails() { + char[] invalidAutoConfHash = + randomFrom(Hasher.MD5.hash(new SecureString(randomAlphaOfLengthBetween(0, 8).toCharArray())), + Hasher.SHA1.hash(new SecureString(randomAlphaOfLengthBetween(0, 8).toCharArray())), + Hasher.SSHA256.hash(new SecureString(randomAlphaOfLengthBetween(0, 8).toCharArray())), + randomAlphaOfLengthBetween(1, 16).toCharArray(), + new char[0] + ); + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + mockSecureSettings.setString("autoconfiguration.password_hash", new String(invalidAutoConfHash)); + if (randomBoolean()) { + mockSecureSettings.setString("bootstrap.password", "foobar"); + } + Settings invalidSettings = Settings.builder().setSecureSettings(mockSecureSettings).build(); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> new ReservedRealm(mock(Environment.class), + invalidSettings, usersStore, new AnonymousUser(Settings.EMPTY), threadPool)); + assertThat(exception.getMessage(), containsString("Invalid password hash for elastic user auto configuration")); + } public void testReservedUserEmptyPasswordAuthenticationFails() throws Throwable { final String principal = randomFrom(UsernamesField.ELASTIC_NAME, UsernamesField.KIBANA_NAME, UsernamesField.LOGSTASH_NAME, @@ -327,10 +350,57 @@ private void assertFailedAuthentication(PlainActionFuture } public void testBootstrapElasticPasswordWorksWhenElasticUserIsMissing() throws Exception { + doAnswer((i) -> { + @SuppressWarnings("unchecked") + ActionListener callback = (ActionListener) i.getArguments()[1]; + callback.onResponse(null); + return null; + }).when(usersStore).getReservedUserInfo(eq("elastic"), anyActionListener()); + MockSecureSettings mockSecureSettings = new MockSecureSettings(); mockSecureSettings.setString("bootstrap.password", "foobar"); Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); + ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, + new AnonymousUser(Settings.EMPTY), threadPool); + PlainActionFuture listener = new PlainActionFuture<>(); + + reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), + mockSecureSettings.getString("bootstrap.password")), + listener); + AuthenticationResult result = listener.get(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + + // add auto configured password which should be ignored because the bootstrap password has priority + mockSecureSettings.setString("autoconfiguration.password_hash", new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2) + .hash(new SecureString("bazbar".toCharArray())))); + settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); + + reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(Settings.EMPTY), threadPool); + + // authn still works for the bootstrap password + listener = new PlainActionFuture<>(); + reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), + new SecureString("foobar".toCharArray())), + listener); + result = listener.get(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + + // authn fails for the auto configured password hash + listener = new PlainActionFuture<>(); + reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), + new SecureString("bazbar".toCharArray())), + listener); + result = listener.get(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.TERMINATE)); + } + + public void testAutoconfigElasticPasswordWorksWhenElasticUserIsMissing() throws Exception { + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + char[] autoconfHash = randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("foobar".toCharArray())); + mockSecureSettings.setString("autoconfiguration.password_hash", new String(autoconfHash)); + Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); + final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(Settings.EMPTY), threadPool); PlainActionFuture listener = new PlainActionFuture<>(); @@ -341,11 +411,68 @@ public void testBootstrapElasticPasswordWorksWhenElasticUserIsMissing() throws E callback.onResponse(null); return null; }).when(usersStore).getReservedUserInfo(eq("elastic"), anyActionListener()); + // mock auto config password is promoted successfully + doAnswer((i) -> { + @SuppressWarnings("unchecked") + ActionListener callback = (ActionListener) i.getArguments()[1]; + callback.onResponse(null); + return null; + }).when(usersStore).createElasticUser(any(char[].class), anyActionListener()); reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), - mockSecureSettings.getString("bootstrap.password")), + new SecureString("foobar".toCharArray())), listener); - final AuthenticationResult result = listener.get(); + AuthenticationResult result = listener.get(); assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + verify(usersStore).getReservedUserInfo(eq("elastic"), anyActionListener()); + ArgumentCaptor userHashCaptor = ArgumentCaptor.forClass(char[].class); + verify(usersStore).createElasticUser(userHashCaptor.capture(), anyActionListener()); + assertThat(userHashCaptor.getValue(), is(autoconfHash)); + + // wrong password doesn't attempt to promote + listener = new PlainActionFuture<>(); + reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), + new SecureString("wrong password".toCharArray())), + listener); + result = listener.get(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.TERMINATE)); + verify(usersStore, times(2)).getReservedUserInfo(eq("elastic"), anyActionListener()); + verify(usersStore).createElasticUser(any(char[].class), anyActionListener()); + verifyNoMoreInteractions(usersStore); + } + + public void testAutoconfigElasticPasswordAuthnErrorWhenHashPromotionFails() throws Exception { + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + char[] autoconfHash = randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("foobar".toCharArray())); + mockSecureSettings.setString("autoconfiguration.password_hash", new String(autoconfHash)); + Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); + + final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, + new AnonymousUser(Settings.EMPTY), threadPool); + PlainActionFuture listener = new PlainActionFuture<>(); + + doAnswer((i) -> { + @SuppressWarnings("unchecked") + ActionListener callback = (ActionListener) i.getArguments()[1]; + callback.onResponse(null); + return null; + }).when(usersStore).getReservedUserInfo(eq("elastic"), anyActionListener()); + // mock auto config password is NOT promoted successfully + doAnswer((i) -> { + @SuppressWarnings("unchecked") + ActionListener callback = (ActionListener) i.getArguments()[1]; + callback.onFailure(new Exception("any failure to promote the auto configured password")); + return null; + }).when(usersStore).createElasticUser(any(char[].class), anyActionListener()); + reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), + new SecureString("foobar".toCharArray())), + listener); + ExecutionException exception = expectThrows(ExecutionException.class, () -> listener.get()); + assertThat(exception.getCause(), instanceOf(ElasticsearchAuthenticationProcessingError.class)); + assertThat(((ElasticsearchAuthenticationProcessingError)exception.getCause()).status(), is(RestStatus.INTERNAL_SERVER_ERROR)); + verify(usersStore).getReservedUserInfo(eq("elastic"), anyActionListener()); + ArgumentCaptor userHashCaptor = ArgumentCaptor.forClass(char[].class); + verify(usersStore).createElasticUser(userHashCaptor.capture(), anyActionListener()); + assertThat(userHashCaptor.getValue(), is(autoconfHash)); } public void testBootstrapElasticPasswordFailsOnceElasticUserExists() throws Exception { From ed8646fe2e075b7207553c626f19e811bbb580fe Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 28 Sep 2021 19:58:50 +0300 Subject: [PATCH 08/16] ReservedRealm tests --- .../authc/esnative/ReservedRealm.java | 1 - .../authc/esnative/ReservedRealmTests.java | 154 ++++++++++++++++-- 2 files changed, 138 insertions(+), 17 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index 2625bd869d6c4..af489eb57f0db 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -202,7 +202,6 @@ private User getUser(String username, ReservedUserInfo userInfo) { } } - public void users(ActionListener> listener) { if (realmEnabled == false) { listener.onResponse(anonymousEnabled ? Collections.singletonList(anonymousUser) : Collections.emptyList()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index c0192d6bebe2a..cb38d699ee626 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -130,7 +130,18 @@ public void testReservedUserEmptyPasswordAuthenticationFails() throws Throwable } public void testAuthenticationDisabled() throws Throwable { - Settings settings = Settings.builder().put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false).build(); + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + if (randomBoolean()) { + mockSecureSettings.setString("bootstrap.password", "foobar"); + } + if (randomBoolean()) { + mockSecureSettings.setString("autoconfiguration.password_hash", + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + } + Settings settings = Settings.builder() + .put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false) + .setSecureSettings(mockSecureSettings) + .build(); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings), threadPool); @@ -197,11 +208,23 @@ private Answer> getAnswer(boolean enabled, SecureString newPassword, } public void testLookup() throws Exception { - final ReservedRealm reservedRealm = - new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, - new AnonymousUser(Settings.EMPTY), threadPool); final User expectedUser = randomReservedUser(true); final String principal = expectedUser.principal(); + // auto conf and bootstrap passwords only influence the elastic user + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + if (randomBoolean()) { + mockSecureSettings.setString("bootstrap.password", "foobar"); + } + if (randomBoolean()) { + mockSecureSettings.setString("autoconfiguration.password_hash", + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + } + final ReservedRealm reservedRealm = + new ReservedRealm(mock(Environment.class), + Settings.builder() + .setSecureSettings(mockSecureSettings) + .build(), usersStore, + new AnonymousUser(Settings.EMPTY), threadPool); PlainActionFuture listener = new PlainActionFuture<>(); reservedRealm.doLookupUser(principal, listener); @@ -217,7 +240,18 @@ public void testLookup() throws Exception { } public void testLookupDisabled() throws Exception { - Settings settings = Settings.builder().put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false).build(); + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + if (randomBoolean()) { + mockSecureSettings.setString("bootstrap.password", "foobar"); + } + if (randomBoolean()) { + mockSecureSettings.setString("autoconfiguration.password_hash", + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + } + Settings settings = Settings.builder() + .put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false) + .setSecureSettings(mockSecureSettings) + .build(); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings), threadPool); @@ -232,9 +266,18 @@ public void testLookupDisabled() throws Exception { } public void testLookupDisabledAnonymous() throws Exception { + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + if (randomBoolean()) { + mockSecureSettings.setString("bootstrap.password", "foobar"); + } + if (randomBoolean()) { + mockSecureSettings.setString("autoconfiguration.password_hash", + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + } Settings settings = Settings.builder() .put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false) .put(AnonymousUser.ROLES_SETTING.getKey(), "anonymous") + .setSecureSettings(mockSecureSettings) .build(); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings), @@ -245,6 +288,7 @@ public void testLookupDisabledAnonymous() throws Exception { PlainActionFuture listener = new PlainActionFuture<>(); reservedRealm.doLookupUser(principal, assertListenerIsOnlyCalledOnce(listener)); assertThat(listener.actionGet(), equalTo(expectedUser)); + verifyZeroInteractions(usersStore); } public void testLookupThrows() throws Exception { @@ -301,9 +345,18 @@ public void testGetUsers() { public void testGetUsersDisabled() { final boolean anonymousEnabled = randomBoolean(); + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + if (randomBoolean()) { + mockSecureSettings.setString("bootstrap.password", "foobar"); + } + if (randomBoolean()) { + mockSecureSettings.setString("autoconfiguration.password_hash", + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + } Settings settings = Settings.builder() .put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false) .put(AnonymousUser.ROLES_SETTING.getKey(), anonymousEnabled ? "user" : "") + .setSecureSettings(mockSecureSettings) .build(); final AnonymousUser anonymousUser = new AnonymousUser(settings); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, anonymousUser, @@ -318,26 +371,38 @@ public void testGetUsersDisabled() { } public void testFailedAuthentication() throws Exception { + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + if (randomBoolean()) { + mockSecureSettings.setString("bootstrap.password", "foobar"); + } + if (randomBoolean()) { + mockSecureSettings.setString("autoconfiguration.password_hash", + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + } SecureString password = new SecureString("password".toCharArray()); // Mocked users store is initiated with default hashing algorithm final Hasher hasher = Hasher.resolve("bcrypt"); char[] hash = hasher.hash(password); + User reservedUser = randomReservedUser(randomBoolean()); + String principal = reservedUser.principal(); ReservedUserInfo userInfo = new ReservedUserInfo(hash, true); - mockGetAllReservedUserInfo(usersStore, Collections.singletonMap("elastic", userInfo)); - final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, + mockGetAllReservedUserInfo(usersStore, Collections.singletonMap(principal, userInfo)); + final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), + Settings.builder().setSecureSettings(mockSecureSettings).build(), + usersStore, new AnonymousUser(Settings.EMPTY), threadPool); if (randomBoolean()) { PlainActionFuture future = new PlainActionFuture<>(); - reservedRealm.authenticate(new UsernamePasswordToken(ElasticUser.NAME, password), future); + reservedRealm.authenticate(new UsernamePasswordToken(principal, password), future); User user = future.actionGet().getUser(); - assertEquals(new ElasticUser(true), user); + assertEquals(reservedUser, user); } PlainActionFuture future = new PlainActionFuture<>(); - reservedRealm.authenticate(new UsernamePasswordToken(ElasticUser.NAME, new SecureString("foobar".toCharArray())), future); - assertFailedAuthentication(future, ElasticUser.NAME); + reservedRealm.authenticate(new UsernamePasswordToken(principal, new SecureString("foobar".toCharArray())), future); + assertFailedAuthentication(future, principal); } private void assertFailedAuthentication(PlainActionFuture future, String principal) throws Exception { @@ -391,8 +456,7 @@ public void testBootstrapElasticPasswordWorksWhenElasticUserIsMissing() throws E reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), new SecureString("bazbar".toCharArray())), listener); - result = listener.get(); - assertThat(result.getStatus(), is(AuthenticationResult.Status.TERMINATE)); + assertFailedAuthentication(listener, ElasticUser.NAME); } public void testAutoconfigElasticPasswordWorksWhenElasticUserIsMissing() throws Exception { @@ -433,11 +497,9 @@ public void testAutoconfigElasticPasswordWorksWhenElasticUserIsMissing() throws reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), new SecureString("wrong password".toCharArray())), listener); - result = listener.get(); - assertThat(result.getStatus(), is(AuthenticationResult.Status.TERMINATE)); + assertFailedAuthentication(listener, ElasticUser.NAME); verify(usersStore, times(2)).getReservedUserInfo(eq("elastic"), anyActionListener()); verify(usersStore).createElasticUser(any(char[].class), anyActionListener()); - verifyNoMoreInteractions(usersStore); } public void testAutoconfigElasticPasswordAuthnErrorWhenHashPromotionFails() throws Exception { @@ -478,6 +540,10 @@ public void testAutoconfigElasticPasswordAuthnErrorWhenHashPromotionFails() thro public void testBootstrapElasticPasswordFailsOnceElasticUserExists() throws Exception { MockSecureSettings mockSecureSettings = new MockSecureSettings(); mockSecureSettings.setString("bootstrap.password", "foobar"); + if (randomBoolean()) { + mockSecureSettings.setString("autoconfiguration.password_hash", + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + } Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, @@ -497,6 +563,10 @@ public void testBootstrapElasticPasswordFailsOnceElasticUserExists() throws Exce reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), mockSecureSettings.getString("bootstrap.password")), listener); assertFailedAuthentication(listener, "elastic"); + listener = new PlainActionFuture<>(); + reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), + new SecureString("barbaz".toCharArray())), listener); + assertFailedAuthentication(listener, "elastic"); // now try with the real password listener = new PlainActionFuture<>(); reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), password), listener); @@ -504,10 +574,40 @@ public void testBootstrapElasticPasswordFailsOnceElasticUserExists() throws Exce assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); } + public void testAutoconfigPasswordHashFailsOnceElasticUserExists() throws Exception { + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + mockSecureSettings.setString("autoconfiguration.password_hash", + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("auto_password".toCharArray())))); + Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); + + final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, + new AnonymousUser(Settings.EMPTY), threadPool); + PlainActionFuture listener = new PlainActionFuture<>(); + // Mocked users store is initiated with default hashing algorithm + final Hasher hasher = Hasher.resolve("bcrypt"); + doAnswer(getAnswer(true, new SecureString("password".toCharArray()), hasher)).when(usersStore) + .getReservedUserInfo(eq("elastic"), anyActionListener()); + reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), + new SecureString("password".toCharArray())), listener); + final AuthenticationResult result = listener.get(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + // but auto config password does not work + listener = new PlainActionFuture<>(); + reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), + new SecureString("auto_password".toCharArray())), listener); + assertFailedAuthentication(listener, "elastic"); + verify(usersStore, times(2)).getReservedUserInfo(eq("elastic"), anyActionListener()); + verify(usersStore, times(0)).createElasticUser(any(char[].class), anyActionListener()); + } + public void testNonElasticUsersCannotUseBootstrapPassword() throws Exception { final MockSecureSettings mockSecureSettings = new MockSecureSettings(); final String password = randomAlphaOfLengthBetween(8, 24); mockSecureSettings.setString("bootstrap.password", password); + if (randomBoolean()) { + mockSecureSettings.setString("autoconfiguration.password_hash", + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + } Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, @@ -526,6 +626,28 @@ public void testNonElasticUsersCannotUseBootstrapPassword() throws Exception { assertThat(result.getStatus(), is(AuthenticationResult.Status.TERMINATE)); } + public void testNonElasticUsersCannotUseAutoconfigPasswordHash() throws Exception { + final MockSecureSettings mockSecureSettings = new MockSecureSettings(); + final String password = randomAlphaOfLengthBetween(8, 24); + mockSecureSettings.setString("autoconfiguration.password_hash", + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString(password.toCharArray())))); + Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); + + final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, + new AnonymousUser(Settings.EMPTY), threadPool); + PlainActionFuture listener = new PlainActionFuture<>(); + + final String principal = randomFrom(KibanaUser.NAME, KibanaSystemUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME, + APMSystemUser.NAME, RemoteMonitoringUser.NAME); + doAnswer((i) -> { + ActionListener callback = (ActionListener) i.getArguments()[1]; + callback.onResponse(null); + return null; + }).when(usersStore).getReservedUserInfo(eq(principal), anyActionListener()); + reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, new SecureString(password.toCharArray())), listener); + assertFailedAuthentication(listener, principal); + } + private User randomReservedUser(boolean enabled) { return randomFrom(new ElasticUser(enabled), new KibanaUser(enabled), new KibanaSystemUser(enabled), new LogstashSystemUser(enabled), new BeatsSystemUser(enabled), new APMSystemUser(enabled), new RemoteMonitoringUser(enabled)); From 53443bd7c0cbf836c4488b7ac7c2b28114fe953f Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Wed, 29 Sep 2021 08:19:58 +0300 Subject: [PATCH 09/16] NativeUsersStoreTests --- .../authc/esnative/ReservedRealm.java | 1 + .../authc/esnative/NativeUsersStoreTests.java | 40 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index af489eb57f0db..9da810c9a7294 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -128,6 +128,7 @@ protected void doAuthenticate(UsernamePasswordToken token, ActionListener { hashCleanupListener.onResponse(AuthenticationResult.success(user)); }, e -> { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java index f328ede14107f..d1a90f7859521 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java @@ -10,21 +10,26 @@ import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.DocWriteRequest; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.client.Client; import org.elasticsearch.client.FilterClient; import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.core.Tuple; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.Tuple; import org.elasticsearch.index.get.GetResult; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheRequest; +import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheResponse; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames; @@ -38,6 +43,7 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.Before; +import org.mockito.Mockito; import java.io.IOException; import java.util.Collections; @@ -45,6 +51,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; import java.util.function.Consumer; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; @@ -53,6 +60,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.is; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -210,6 +218,36 @@ public void testDefaultReservedUserInfoPasswordEmpty() { assertThat(constructedUserInfo.hasEmptyPassword(), equalTo(false)); } + @SuppressWarnings("unchecked") + public void testCreateElasticUser() throws Exception { + final NativeUsersStore nativeUsersStore = startNativeUsersStore(); + + PlainActionFuture future = new PlainActionFuture<>(); + char[] passwordHash = randomAlphaOfLength(4).toCharArray(); + nativeUsersStore.createElasticUser(passwordHash, future); + + Tuple> createElasticUserRequest = findRequest(IndexRequest.class); + assertThat(createElasticUserRequest.v1().opType(), is(DocWriteRequest.OpType.CREATE)); + assertThat(createElasticUserRequest.v1().index(), is(RestrictedIndicesNames.SECURITY_MAIN_ALIAS)); + assertThat(createElasticUserRequest.v1().id(), is(NativeUsersStore.getIdForUser(NativeUsersStore.RESERVED_USER_TYPE, "elastic"))); + assertThat(createElasticUserRequest.v1().getRefreshPolicy(), is(WriteRequest.RefreshPolicy.IMMEDIATE)); + + if (randomBoolean()) { + ((ActionListener) createElasticUserRequest.v2()).onResponse(Mockito.mock(IndexResponse.class)); + Tuple> clearRealmCacheRequest = findRequest(ClearRealmCacheRequest.class); + assertThat(clearRealmCacheRequest.v1().allRealms(), is(true)); + assertThat(clearRealmCacheRequest.v1().usernames().length, is(1)); + assertThat(clearRealmCacheRequest.v1().usernames()[0], is("elastic")); + ((ActionListener) clearRealmCacheRequest.v2()).onResponse(Mockito.mock(ClearRealmCacheResponse.class)); + future.get(); + } else { + Exception exception = mock(Exception.class); + createElasticUserRequest.v2().onFailure(exception); + ExecutionException executionException = expectThrows(ExecutionException.class, () -> future.get()); + assertThat(executionException.getCause(), is(exception)); + } + } + @SuppressWarnings("unchecked") private ARequest actionRespond(Class requestClass, AResponse response) { From 5645b7dc2a065c6a7fd72bb0efb3cb75e1d96227 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Wed, 29 Sep 2021 08:47:20 +0300 Subject: [PATCH 10/16] isElasticUserAutoConfigured --- .../xpack/security/Security.java | 23 +++++++++---------- .../authc/esnative/ReservedRealm.java | 13 +++++++++-- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 7de75dfa9e8d9..182eeb3cb55c2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -76,11 +76,11 @@ import org.elasticsearch.threadpool.ExecutorBuilder; import org.elasticsearch.threadpool.FixedExecutorBuilder; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.netty4.SharedGroupFactory; import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportInterceptor; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestHandler; +import org.elasticsearch.transport.netty4.SharedGroupFactory; import org.elasticsearch.transport.nio.NioGroupFactory; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.XPackField; @@ -99,8 +99,8 @@ import org.elasticsearch.xpack.core.security.action.GrantApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyAction; -import org.elasticsearch.xpack.core.security.action.enrollment.NodeEnrollmentAction; import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentAction; +import org.elasticsearch.xpack.core.security.action.enrollment.NodeEnrollmentAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction; @@ -172,8 +172,8 @@ import org.elasticsearch.xpack.security.action.TransportGrantApiKeyAction; import org.elasticsearch.xpack.security.action.TransportInvalidateApiKeyAction; import org.elasticsearch.xpack.security.action.apikey.TransportQueryApiKeyAction; -import org.elasticsearch.xpack.security.action.enrollment.TransportNodeEnrollmentAction; import org.elasticsearch.xpack.security.action.enrollment.TransportKibanaEnrollmentAction; +import org.elasticsearch.xpack.security.action.enrollment.TransportNodeEnrollmentAction; import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter; import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectAuthenticateAction; import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectLogoutAction; @@ -259,8 +259,8 @@ import org.elasticsearch.xpack.security.rest.action.apikey.RestGrantApiKeyAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestInvalidateApiKeyAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestQueryApiKeyAction; -import org.elasticsearch.xpack.security.rest.action.enrollment.RestNodeEnrollmentAction; import org.elasticsearch.xpack.security.rest.action.enrollment.RestKibanaEnrollAction; +import org.elasticsearch.xpack.security.rest.action.enrollment.RestNodeEnrollmentAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestInvalidateTokenAction; import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectAuthenticateAction; @@ -336,10 +336,8 @@ import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; import static org.elasticsearch.xpack.core.XPackSettings.API_KEY_SERVICE_ENABLED_SETTING; import static org.elasticsearch.xpack.core.XPackSettings.HTTP_SSL_ENABLED; -import static org.elasticsearch.xpack.core.XPackSettings.SECURITY_AUTOCONFIGURATION_ENABLED; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_TOKENS_ALIAS; -import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD; import static org.elasticsearch.xpack.security.operator.OperatorPrivileges.OPERATOR_PRIVILEGES_ENABLED; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.INTERNAL_MAIN_INDEX_FORMAT; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.INTERNAL_TOKENS_INDEX_FORMAT; @@ -493,17 +491,18 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste components.add(tokenService); // realms construction + final AnonymousUser anonymousUser = new AnonymousUser(settings); + components.add(anonymousUser); final NativeUsersStore nativeUsersStore = new NativeUsersStore(settings, client, securityIndex.get()); - GenerateInitialBuiltinUsersPasswordListener generateInitialBuiltinUsersPasswordListener = - new GenerateInitialBuiltinUsersPasswordListener(nativeUsersStore, securityIndex.get()); - if (BOOTSTRAP_ELASTIC_PASSWORD.exists(settings) == false && SECURITY_AUTOCONFIGURATION_ENABLED.get(settings)) { + final ReservedRealm reservedRealm = new ReservedRealm(environment, settings, nativeUsersStore, anonymousUser, threadPool); + if (reservedRealm.isElasticUserAutoconfigured()) { + //TODO kibana enrollment token should be printed even if the elastic user is auto configured + GenerateInitialBuiltinUsersPasswordListener generateInitialBuiltinUsersPasswordListener = + new GenerateInitialBuiltinUsersPasswordListener(nativeUsersStore, securityIndex.get()); securityIndex.get().addStateListener(generateInitialBuiltinUsersPasswordListener); } final NativeRoleMappingStore nativeRoleMappingStore = new NativeRoleMappingStore(settings, client, securityIndex.get(), scriptService); - final AnonymousUser anonymousUser = new AnonymousUser(settings); - components.add(anonymousUser); - final ReservedRealm reservedRealm = new ReservedRealm(environment, settings, nativeUsersStore, anonymousUser, threadPool); final SecurityExtension.SecurityComponents extensionComponents = new ExtensionComponents(environment, client, clusterService, resourceWatcherService, nativeRoleMappingStore); Map realmFactories = new HashMap<>(InternalRealms.getFactories(threadPool, resourceWatcherService, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index 9da810c9a7294..76ded4fa63b8e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -71,6 +71,7 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { private final AnonymousUser anonymousUser; private final boolean realmEnabled; private final boolean anonymousEnabled; + private final boolean elasticUserAutoconfigured; private final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(logger.getName()); @@ -94,7 +95,9 @@ public ReservedRealm(Environment env, Settings settings, NativeUsersStore native throw new IllegalArgumentException("Invalid password hash for elastic user auto configuration"); } } - if (AUTOCONFIG_ELASTIC_PASSWORD_HASH.exists(settings) && false == BOOTSTRAP_ELASTIC_PASSWORD.exists(settings)) { + elasticUserAutoconfigured = + AUTOCONFIG_ELASTIC_PASSWORD_HASH.exists(settings) && false == BOOTSTRAP_ELASTIC_PASSWORD.exists(settings); + if (elasticUserAutoconfigured) { autoconfigUserInfo = new ReservedUserInfo(autoconfigPasswordHash, true); bootstrapUserInfo = null; } else { @@ -178,6 +181,10 @@ protected void doLookupUser(String username, ActionListener listener) { } } + public boolean isElasticUserAutoconfigured() { + return elasticUserAutoconfigured; + } + private User getUser(String username, ReservedUserInfo userInfo) { assert username != null; switch (username) { @@ -269,11 +276,13 @@ private void logDeprecatedUser(final User user){ private ReservedUserInfo getDefaultUserInfo(String username) { if (ElasticUser.NAME.equals(username)) { - if (autoconfigUserInfo != null) { + if (elasticUserAutoconfigured) { assert bootstrapUserInfo == null; + assert autoconfigUserInfo != null; return autoconfigUserInfo; } else { assert bootstrapUserInfo != null; + assert autoconfigUserInfo == null; return bootstrapUserInfo; } } else { From 14e1f36c4163f1f0d9be1d9bb052cddf462a4a36 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 30 Sep 2021 18:52:36 +0300 Subject: [PATCH 11/16] Negative integ tests --- .../test/SecuritySingleNodeTestCase.java | 20 ++-- ...ervedRealmElasticAutoconfigIntegTests.java | 107 ++++++++++++++++++ ...teInitialBuiltinUsersPasswordListener.java | 3 + 3 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java index c6753a3dff1a5..183589a2bfbac 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java @@ -8,6 +8,7 @@ import io.netty.util.ThreadDeathWatcher; import io.netty.util.concurrent.GlobalEventExecutor; + import org.apache.http.HttpHost; import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; @@ -58,12 +59,6 @@ public abstract class SecuritySingleNodeTestCase extends ESSingleNodeTestCase { private static SecuritySettingsSource SECURITY_DEFAULT_SETTINGS = null; private static CustomSecuritySettingsSource customSecuritySettingsSource = null; private static RestClient restClient = null; - private static SecureString BOOTSTRAP_PASSWORD = null; - - @BeforeClass - public static void generateBootstrapPassword() { - BOOTSTRAP_PASSWORD = TEST_PASSWORD_SECURE_STRING.clone(); - } @BeforeClass public static void initDefaultSettings() { @@ -82,10 +77,6 @@ public static void initDefaultSettings() { public static void destroyDefaultSettings() { SECURITY_DEFAULT_SETTINGS = null; customSecuritySettingsSource = null; - if (BOOTSTRAP_PASSWORD != null) { - BOOTSTRAP_PASSWORD.close(); - BOOTSTRAP_PASSWORD = null; - } tearDownRestClient(); } @@ -180,10 +171,17 @@ protected Settings nodeSettings() { if (builder.getSecureSettings() == null) { builder.setSecureSettings(new MockSecureSettings()); } - ((MockSecureSettings) builder.getSecureSettings()).setString("bootstrap.password", BOOTSTRAP_PASSWORD.toString()); + SecureString boostrapPassword = getBootstrapPassword(); + if (boostrapPassword != null) { + ((MockSecureSettings) builder.getSecureSettings()).setString("bootstrap.password", boostrapPassword.toString()); + } return builder.build(); } + protected SecureString getBootstrapPassword() { + return TEST_PASSWORD_SECURE_STRING; + } + @Override protected Collection> getPlugins() { return customSecuritySettingsSource.nodePlugins(); diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java new file mode 100644 index 0000000000000..f05a627cee707 --- /dev/null +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.esnative; + +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; +import org.elasticsearch.action.admin.indices.get.GetIndexRequest; +import org.elasticsearch.action.admin.indices.get.GetIndexResponse; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.SecuritySingleNodeTestCase; +import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames; +import org.junit.BeforeClass; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS; +import static org.hamcrest.Matchers.is; + +public class ReservedRealmElasticAutoconfigIntegTests extends SecuritySingleNodeTestCase { + + private static Hasher hasher; + + @BeforeClass + public static void setHasher() { + hasher = getFastStoredHashAlgoForTests(); + } + + @Override + public Settings nodeSettings() { + Settings.Builder settingsBuilder = Settings.builder() + .put(super.nodeSettings()) + .put("xpack.security.authc.password_hashing.algorithm", hasher.name()); + ((MockSecureSettings) settingsBuilder.getSecureSettings()).setString("autoconfiguration.password_hash", + new String(hasher.hash(new SecureString("auto_password".toCharArray())))); + return settingsBuilder.build(); + } + + @Override + protected boolean addMockHttpTransport() { + return false; // enable HTTP + } + + @Override + protected SecureString getBootstrapPassword() { + return null; // no bootstrap password for this test + } + + public void testAutoconfigFailedPasswordPromotion() { + try { + // prevents the .security index from being created automatically (after elastic user authentication) + ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); + updateSettingsRequest.transientSettings(Settings.builder().put(Metadata.SETTING_READ_ONLY_ALLOW_DELETE_SETTING.getKey(), + true)); + assertAcked(client().admin().cluster().updateSettings(updateSettingsRequest).actionGet()); + + // delete the security index, if it exist + GetIndexRequest getIndexRequest = new GetIndexRequest(); + getIndexRequest.indices(SECURITY_MAIN_ALIAS); + getIndexRequest.indicesOptions(IndicesOptions.lenientExpandOpen()); + GetIndexResponse getIndexResponse = client().admin().indices().getIndex(getIndexRequest).actionGet(); + if (getIndexResponse.getIndices().length > 0) { + assertThat(getIndexResponse.getIndices().length, is(1)); + assertThat(getIndexResponse.getIndices()[0], is(RestrictedIndicesNames.INTERNAL_SECURITY_MAIN_INDEX_7)); + DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(getIndexResponse.getIndices()); + assertAcked(client().admin().indices().delete(deleteIndexRequest).actionGet()); + } + + // elastic user gets 503 for the good password + Request restRequest = randomFrom(new Request("GET", "/_security/_authenticate"), new Request("GET", "_cluster/health"), + new Request("GET", "_nodes")); + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, UsernamePasswordToken.basicAuthHeaderValue("elastic", + new SecureString("auto_password".toCharArray()))); + restRequest.setOptions(options); + ResponseException exception = expectThrows(ResponseException.class, () -> getRestClient().performRequest(restRequest)); + assertThat(exception.getResponse().getStatusLine().getStatusCode(), is(RestStatus.SERVICE_UNAVAILABLE.getStatus())); + + // but gets a 401 for the wrong password + Request restRequest2 = randomFrom(new Request("GET", "/_security/_authenticate"), new Request("GET", "_cluster/health"), + new Request("GET", "_nodes")); + options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, UsernamePasswordToken.basicAuthHeaderValue("elastic", + new SecureString("wrong password".toCharArray()))); + restRequest2.setOptions(options); + exception = expectThrows(ResponseException.class, () -> getRestClient().performRequest(restRequest2)); + assertThat(exception.getResponse().getStatusLine().getStatusCode(), is(RestStatus.UNAUTHORIZED.getStatus())); + } finally { + ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); + updateSettingsRequest.transientSettings(Settings.builder().put(Metadata.SETTING_READ_ONLY_ALLOW_DELETE_SETTING.getKey(), (String) null)); + assertAcked(client().admin().cluster().updateSettings(updateSettingsRequest).actionGet()); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/GenerateInitialBuiltinUsersPasswordListener.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/GenerateInitialBuiltinUsersPasswordListener.java index bc11a678bdb16..712fc1fc6398e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/GenerateInitialBuiltinUsersPasswordListener.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/GenerateInitialBuiltinUsersPasswordListener.java @@ -41,6 +41,9 @@ public GenerateInitialBuiltinUsersPasswordListener(NativeUsersStore nativeUsersS public void accept(SecurityIndexManager.State previousState, SecurityIndexManager.State currentState) { final PrintStream out = BootstrapInfo.getOriginalStandardOut(); // Check if it has been closed, try to write something so that we trigger PrintStream#ensureOpen + if (out == null) { + return; + } out.println(); if (out.checkError()) { outputOnError(null); From 4128d61d2bbf85815b5410cbeb01a01ee841e2c3 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 30 Sep 2021 19:08:36 +0300 Subject: [PATCH 12/16] Merge fallout --- .../InitialSecurityConfigurationListener.java | 4 ++-- .../org/elasticsearch/xpack/security/Security.java | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialSecurityConfigurationListener.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialSecurityConfigurationListener.java index 9b1935228aa4b..b887e3a28db08 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialSecurityConfigurationListener.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialSecurityConfigurationListener.java @@ -31,7 +31,7 @@ import java.util.Map; import java.util.function.BiConsumer; -import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH; +import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_ELASTIC_PASSWORD_HASH; import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD; import static org.elasticsearch.xpack.security.tool.CommandUtils.generatePassword; @@ -87,7 +87,7 @@ public void accept(SecurityIndexManager.State previousState, SecurityIndexManage }, this::outputOnError), 2); if (false == BOOTSTRAP_ELASTIC_PASSWORD.exists(environment.settings()) - && false == AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH.exists(environment.settings())) { + && false == AUTOCONFIG_ELASTIC_PASSWORD_HASH.exists(environment.settings())) { final SecureString elasticPassword = new SecureString(generatePassword(20)); nativeUsersStore.updateReservedUser( ElasticUser.NAME, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 66177d795af53..d04bd24ed1c51 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -76,11 +76,11 @@ import org.elasticsearch.threadpool.ExecutorBuilder; import org.elasticsearch.threadpool.FixedExecutorBuilder; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.netty4.SharedGroupFactory; import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportInterceptor; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestHandler; -import org.elasticsearch.transport.netty4.SharedGroupFactory; import org.elasticsearch.transport.nio.NioGroupFactory; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.XPackField; @@ -99,8 +99,8 @@ import org.elasticsearch.xpack.core.security.action.GrantApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyAction; -import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentAction; import org.elasticsearch.xpack.core.security.action.enrollment.NodeEnrollmentAction; +import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction; @@ -172,8 +172,8 @@ import org.elasticsearch.xpack.security.action.TransportGrantApiKeyAction; import org.elasticsearch.xpack.security.action.TransportInvalidateApiKeyAction; import org.elasticsearch.xpack.security.action.apikey.TransportQueryApiKeyAction; -import org.elasticsearch.xpack.security.action.enrollment.TransportKibanaEnrollmentAction; import org.elasticsearch.xpack.security.action.enrollment.TransportNodeEnrollmentAction; +import org.elasticsearch.xpack.security.action.enrollment.TransportKibanaEnrollmentAction; import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter; import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectAuthenticateAction; import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectLogoutAction; @@ -259,8 +259,8 @@ import org.elasticsearch.xpack.security.rest.action.apikey.RestGrantApiKeyAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestInvalidateApiKeyAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestQueryApiKeyAction; -import org.elasticsearch.xpack.security.rest.action.enrollment.RestKibanaEnrollAction; import org.elasticsearch.xpack.security.rest.action.enrollment.RestNodeEnrollmentAction; +import org.elasticsearch.xpack.security.rest.action.enrollment.RestKibanaEnrollAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestInvalidateTokenAction; import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectAuthenticateAction; @@ -336,6 +336,7 @@ import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; import static org.elasticsearch.xpack.core.XPackSettings.API_KEY_SERVICE_ENABLED_SETTING; import static org.elasticsearch.xpack.core.XPackSettings.HTTP_SSL_ENABLED; +import static org.elasticsearch.xpack.core.XPackSettings.SECURITY_AUTOCONFIGURATION_ENABLED; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_TOKENS_ALIAS; import static org.elasticsearch.xpack.security.operator.OperatorPrivileges.OPERATOR_PRIVILEGES_ENABLED; @@ -491,12 +492,13 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste components.add(tokenService); // realms construction - final AnonymousUser anonymousUser = new AnonymousUser(settings); - components.add(anonymousUser); final NativeUsersStore nativeUsersStore = new NativeUsersStore(settings, client, securityIndex.get()); final NativeRoleMappingStore nativeRoleMappingStore = new NativeRoleMappingStore(settings, client, securityIndex.get(), scriptService); + final AnonymousUser anonymousUser = new AnonymousUser(settings); + components.add(anonymousUser); + final ReservedRealm reservedRealm = new ReservedRealm(environment, settings, nativeUsersStore, anonymousUser, threadPool); final SecurityExtension.SecurityComponents extensionComponents = new ExtensionComponents(environment, client, clusterService, resourceWatcherService, nativeRoleMappingStore); Map realmFactories = new HashMap<>(InternalRealms.getFactories(threadPool, resourceWatcherService, From 59c254276836dcecfd994501d93bf7a042f6024d Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 30 Sep 2021 19:13:06 +0300 Subject: [PATCH 13/16] Checkstyle --- .../esnative/ReservedRealmElasticAutoconfigIntegTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java index f05a627cee707..91203f93ff4c7 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java @@ -100,7 +100,8 @@ public void testAutoconfigFailedPasswordPromotion() { assertThat(exception.getResponse().getStatusLine().getStatusCode(), is(RestStatus.UNAUTHORIZED.getStatus())); } finally { ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); - updateSettingsRequest.transientSettings(Settings.builder().put(Metadata.SETTING_READ_ONLY_ALLOW_DELETE_SETTING.getKey(), (String) null)); + updateSettingsRequest.transientSettings(Settings.builder().put(Metadata.SETTING_READ_ONLY_ALLOW_DELETE_SETTING.getKey(), + (String) null)); assertAcked(client().admin().cluster().updateSettings(updateSettingsRequest).actionGet()); } } From 299c710ed63a519dd7694a8df551a5f88d74b3b1 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 30 Sep 2021 19:18:40 +0300 Subject: [PATCH 14/16] Nit --- .../java/org/elasticsearch/test/SecuritySingleNodeTestCase.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java index 183589a2bfbac..ee42a07b41c1e 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java @@ -8,7 +8,6 @@ import io.netty.util.ThreadDeathWatcher; import io.netty.util.concurrent.GlobalEventExecutor; - import org.apache.http.HttpHost; import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; From cf0f4b3a055750016d13c3eb244c3cdc7d2d7d1e Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 5 Oct 2021 08:56:00 +0300 Subject: [PATCH 15/16] Review Ioannis --- .../DefaultAuthenticationFailureHandler.java | 6 +- ...ervedRealmElasticAutoconfigIntegTests.java | 6 +- .../authc/esnative/ReservedRealm.java | 6 +- .../authc/esnative/ReservedRealmTests.java | 88 +++++++++++-------- 4 files changed, 60 insertions(+), 46 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java index ca3c35e54d265..7299f1ffd6c5a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java @@ -101,7 +101,8 @@ public ElasticsearchSecurityException failedAuthentication(TransportMessage mess @Override public ElasticsearchSecurityException exceptionProcessingRequest(RestRequest request, Exception e, ThreadContext context) { - // a couple of authn processing errors can also return 500 & 503, besides the obvious 401 + // a couple of authn processing errors can also return {@link RestStatus#INTERNAL_SERVER_ERROR} or + // {@link RestStatus#SERVICE_UNAVAILABLE}, besides the obvious {@link RestStatus#UNAUTHORIZED} if (e instanceof ElasticsearchAuthenticationProcessingError) { return (ElasticsearchAuthenticationProcessingError) e; } @@ -111,7 +112,8 @@ public ElasticsearchSecurityException exceptionProcessingRequest(RestRequest req @Override public ElasticsearchSecurityException exceptionProcessingRequest(TransportMessage message, String action, Exception e, ThreadContext context) { - // a couple of authn processing errors can also return 500 & 503, besides the obvious 401 + // a couple of authn processing errors can also return {@link RestStatus#INTERNAL_SERVER_ERROR} or + // {@link RestStatus#SERVICE_UNAVAILABLE}, besides the obvious {@link RestStatus#UNAUTHORIZED} if (e instanceof ElasticsearchAuthenticationProcessingError) { return (ElasticsearchAuthenticationProcessingError) e; } diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java index 91203f93ff4c7..f0c49e2d68bd7 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java @@ -45,7 +45,7 @@ public Settings nodeSettings() { .put(super.nodeSettings()) .put("xpack.security.authc.password_hashing.algorithm", hasher.name()); ((MockSecureSettings) settingsBuilder.getSecureSettings()).setString("autoconfiguration.password_hash", - new String(hasher.hash(new SecureString("auto_password".toCharArray())))); + new String(hasher.hash(new SecureString("auto_password_that_is_longer_than_14_chars_because_of_FIPS".toCharArray())))); return settingsBuilder.build(); } @@ -84,7 +84,7 @@ public void testAutoconfigFailedPasswordPromotion() { new Request("GET", "_nodes")); RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, UsernamePasswordToken.basicAuthHeaderValue("elastic", - new SecureString("auto_password".toCharArray()))); + new SecureString("auto_password_that_is_longer_than_14_chars_because_of_FIPS".toCharArray()))); restRequest.setOptions(options); ResponseException exception = expectThrows(ResponseException.class, () -> getRestClient().performRequest(restRequest)); assertThat(exception.getResponse().getStatusLine().getStatusCode(), is(RestStatus.SERVICE_UNAVAILABLE.getStatus())); @@ -94,7 +94,7 @@ public void testAutoconfigFailedPasswordPromotion() { new Request("GET", "_nodes")); options = RequestOptions.DEFAULT.toBuilder(); options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, UsernamePasswordToken.basicAuthHeaderValue("elastic", - new SecureString("wrong password".toCharArray()))); + new SecureString("wrong password_that_is_longer_than_14_chars_because_of_FIPS".toCharArray()))); restRequest2.setOptions(options); exception = expectThrows(ResponseException.class, () -> getRestClient().performRequest(restRequest2)); assertThat(exception.getResponse().getStatusLine().getStatusCode(), is(RestStatus.UNAUTHORIZED.getStatus())); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index 76ded4fa63b8e..ebfaf45c3bfbb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -122,7 +122,7 @@ protected void doAuthenticate(UsernamePasswordToken token, ActionListener hashCleanupListener = ActionListener.runBefore(listener, () -> { - if (userInfo != null && userInfo != bootstrapUserInfo && userInfo != autoconfigUserInfo) { + if (userInfo != bootstrapUserInfo && userInfo != autoconfigUserInfo) { Arrays.fill(userInfo.passwordHash, (char) 0); } }); @@ -181,10 +181,6 @@ protected void doLookupUser(String username, ActionListener listener) { } } - public boolean isElasticUserAutoconfigured() { - return elasticUserAutoconfigured; - } - private User getUser(String username, ReservedUserInfo userInfo) { assert username != null; switch (username) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index cb38d699ee626..3ad210962c991 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -102,7 +102,7 @@ public void testInvalidAutoConfigPasswordHashFails() { MockSecureSettings mockSecureSettings = new MockSecureSettings(); mockSecureSettings.setString("autoconfiguration.password_hash", new String(invalidAutoConfHash)); if (randomBoolean()) { - mockSecureSettings.setString("bootstrap.password", "foobar"); + mockSecureSettings.setString("bootstrap.password", "foobar longer than 14 chars because of FIPS"); } Settings invalidSettings = Settings.builder().setSecureSettings(mockSecureSettings).build(); IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> new ReservedRealm(mock(Environment.class), @@ -113,7 +113,7 @@ public void testInvalidAutoConfigPasswordHashFails() { public void testReservedUserEmptyPasswordAuthenticationFails() throws Throwable { final String principal = randomFrom(UsernamesField.ELASTIC_NAME, UsernamesField.KIBANA_NAME, UsernamesField.LOGSTASH_NAME, UsernamesField.BEATS_NAME); - SecureString password = new SecureString("password".toCharArray()); + SecureString password = new SecureString("password longer than 14 chars because of FIPS".toCharArray()); // Mocked users store is initiated with default hashing algorithm final Hasher hasher = Hasher.resolve("bcrypt"); char[] hash = hasher.hash(password); @@ -132,11 +132,12 @@ public void testReservedUserEmptyPasswordAuthenticationFails() throws Throwable public void testAuthenticationDisabled() throws Throwable { MockSecureSettings mockSecureSettings = new MockSecureSettings(); if (randomBoolean()) { - mockSecureSettings.setString("bootstrap.password", "foobar"); + mockSecureSettings.setString("bootstrap.password", "foobar longer than 14 chars because of FIPS"); } if (randomBoolean()) { mockSecureSettings.setString("autoconfiguration.password_hash", - new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash( + new SecureString("barbaz longer than 14 chars because of FIPS".toCharArray())))); } Settings settings = Settings.builder() .put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false) @@ -169,7 +170,7 @@ private void verifySuccessfulAuthentication(boolean enabled) throws Exception { new AnonymousUser(Settings.EMPTY), threadPool); final User expectedUser = randomReservedUser(enabled); final String principal = expectedUser.principal(); - final SecureString newPassword = new SecureString("foobar".toCharArray()); + final SecureString newPassword = new SecureString("foobar longer than 14 chars because of FIPS".toCharArray()); // Mocked users store is initiated with default hashing algorithm final Hasher hasher = Hasher.resolve("bcrypt"); doAnswer(getAnswer(enabled, newPassword, hasher)).when(usersStore).getReservedUserInfo(eq(principal), anyActionListener()); @@ -213,11 +214,12 @@ public void testLookup() throws Exception { // auto conf and bootstrap passwords only influence the elastic user MockSecureSettings mockSecureSettings = new MockSecureSettings(); if (randomBoolean()) { - mockSecureSettings.setString("bootstrap.password", "foobar"); + mockSecureSettings.setString("bootstrap.password", "foobar longer than 14 chars because of FIPS"); } if (randomBoolean()) { mockSecureSettings.setString("autoconfiguration.password_hash", - new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash( + new SecureString("barbaz longer than 14 chars because of FIPS".toCharArray())))); } final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), @@ -242,11 +244,12 @@ public void testLookup() throws Exception { public void testLookupDisabled() throws Exception { MockSecureSettings mockSecureSettings = new MockSecureSettings(); if (randomBoolean()) { - mockSecureSettings.setString("bootstrap.password", "foobar"); + mockSecureSettings.setString("bootstrap.password", "foobar longer than 14 chars because of FIPS"); } if (randomBoolean()) { mockSecureSettings.setString("autoconfiguration.password_hash", - new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash( + new SecureString("barbaz longer than 14 chars because of FIPS".toCharArray())))); } Settings settings = Settings.builder() .put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false) @@ -268,11 +271,12 @@ public void testLookupDisabled() throws Exception { public void testLookupDisabledAnonymous() throws Exception { MockSecureSettings mockSecureSettings = new MockSecureSettings(); if (randomBoolean()) { - mockSecureSettings.setString("bootstrap.password", "foobar"); + mockSecureSettings.setString("bootstrap.password", "foobar longer than 14 chars because of FIPS"); } if (randomBoolean()) { mockSecureSettings.setString("autoconfiguration.password_hash", - new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash( + new SecureString("barbaz longer than 14 chars because of FIPS".toCharArray())))); } Settings settings = Settings.builder() .put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false) @@ -347,11 +351,12 @@ public void testGetUsersDisabled() { final boolean anonymousEnabled = randomBoolean(); MockSecureSettings mockSecureSettings = new MockSecureSettings(); if (randomBoolean()) { - mockSecureSettings.setString("bootstrap.password", "foobar"); + mockSecureSettings.setString("bootstrap.password", "foobar longer than 14 chars because of FIPS"); } if (randomBoolean()) { mockSecureSettings.setString("autoconfiguration.password_hash", - new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash( + new SecureString("barbaz longer than 14 chars because of FIPS".toCharArray())))); } Settings settings = Settings.builder() .put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false) @@ -373,17 +378,19 @@ public void testGetUsersDisabled() { public void testFailedAuthentication() throws Exception { MockSecureSettings mockSecureSettings = new MockSecureSettings(); if (randomBoolean()) { - mockSecureSettings.setString("bootstrap.password", "foobar"); + mockSecureSettings.setString("bootstrap.password", "foobar longer than 14 chars because of FIPS"); } if (randomBoolean()) { mockSecureSettings.setString("autoconfiguration.password_hash", - new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash( + new SecureString("barbaz longer than 14 chars because of FIPS".toCharArray())))); } SecureString password = new SecureString("password".toCharArray()); // Mocked users store is initiated with default hashing algorithm final Hasher hasher = Hasher.resolve("bcrypt"); char[] hash = hasher.hash(password); - User reservedUser = randomReservedUser(randomBoolean()); + boolean enabled = randomBoolean(); + User reservedUser = randomReservedUser(enabled); String principal = reservedUser.principal(); ReservedUserInfo userInfo = new ReservedUserInfo(hash, true); mockGetAllReservedUserInfo(usersStore, Collections.singletonMap(principal, userInfo)); @@ -394,14 +401,18 @@ public void testFailedAuthentication() throws Exception { if (randomBoolean()) { PlainActionFuture future = new PlainActionFuture<>(); - reservedRealm.authenticate(new UsernamePasswordToken(principal, password), future); User user = future.actionGet().getUser(); assertEquals(reservedUser, user); + if (new KibanaUser(enabled).equals(reservedUser)) { + assertWarnings("The user [kibana] is deprecated and will be removed in a future version of Elasticsearch. " + + "Please use the [kibana_system] user instead."); + } } PlainActionFuture future = new PlainActionFuture<>(); - reservedRealm.authenticate(new UsernamePasswordToken(principal, new SecureString("foobar".toCharArray())), future); + reservedRealm.authenticate(new UsernamePasswordToken(principal, + new SecureString("foobar longer than 14 chars because of FIPS".toCharArray())), future); assertFailedAuthentication(future, principal); } @@ -423,7 +434,7 @@ public void testBootstrapElasticPasswordWorksWhenElasticUserIsMissing() throws E }).when(usersStore).getReservedUserInfo(eq("elastic"), anyActionListener()); MockSecureSettings mockSecureSettings = new MockSecureSettings(); - mockSecureSettings.setString("bootstrap.password", "foobar"); + mockSecureSettings.setString("bootstrap.password", "foobar longer than 14 chars because of FIPS"); Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, @@ -438,7 +449,7 @@ public void testBootstrapElasticPasswordWorksWhenElasticUserIsMissing() throws E // add auto configured password which should be ignored because the bootstrap password has priority mockSecureSettings.setString("autoconfiguration.password_hash", new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2) - .hash(new SecureString("bazbar".toCharArray())))); + .hash(new SecureString("bazbar longer than 14 chars because of FIPS".toCharArray())))); settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(Settings.EMPTY), threadPool); @@ -446,7 +457,7 @@ public void testBootstrapElasticPasswordWorksWhenElasticUserIsMissing() throws E // authn still works for the bootstrap password listener = new PlainActionFuture<>(); reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), - new SecureString("foobar".toCharArray())), + new SecureString("foobar longer than 14 chars because of FIPS".toCharArray())), listener); result = listener.get(); assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); @@ -454,14 +465,15 @@ public void testBootstrapElasticPasswordWorksWhenElasticUserIsMissing() throws E // authn fails for the auto configured password hash listener = new PlainActionFuture<>(); reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), - new SecureString("bazbar".toCharArray())), + new SecureString("bazbar longer than 14 chars because of FIPS".toCharArray())), listener); assertFailedAuthentication(listener, ElasticUser.NAME); } public void testAutoconfigElasticPasswordWorksWhenElasticUserIsMissing() throws Exception { MockSecureSettings mockSecureSettings = new MockSecureSettings(); - char[] autoconfHash = randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("foobar".toCharArray())); + char[] autoconfHash = randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash( + new SecureString("foobar longer than 14 chars because of FIPS".toCharArray())); mockSecureSettings.setString("autoconfiguration.password_hash", new String(autoconfHash)); Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); @@ -483,7 +495,7 @@ public void testAutoconfigElasticPasswordWorksWhenElasticUserIsMissing() throws return null; }).when(usersStore).createElasticUser(any(char[].class), anyActionListener()); reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), - new SecureString("foobar".toCharArray())), + new SecureString("foobar longer than 14 chars because of FIPS".toCharArray())), listener); AuthenticationResult result = listener.get(); assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); @@ -504,7 +516,8 @@ public void testAutoconfigElasticPasswordWorksWhenElasticUserIsMissing() throws public void testAutoconfigElasticPasswordAuthnErrorWhenHashPromotionFails() throws Exception { MockSecureSettings mockSecureSettings = new MockSecureSettings(); - char[] autoconfHash = randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("foobar".toCharArray())); + char[] autoconfHash = randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash( + new SecureString("foobar longer than 14 chars because of FIPS".toCharArray())); mockSecureSettings.setString("autoconfiguration.password_hash", new String(autoconfHash)); Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); @@ -526,7 +539,7 @@ public void testAutoconfigElasticPasswordAuthnErrorWhenHashPromotionFails() thro return null; }).when(usersStore).createElasticUser(any(char[].class), anyActionListener()); reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), - new SecureString("foobar".toCharArray())), + new SecureString("foobar longer than 14 chars because of FIPS".toCharArray())), listener); ExecutionException exception = expectThrows(ExecutionException.class, () -> listener.get()); assertThat(exception.getCause(), instanceOf(ElasticsearchAuthenticationProcessingError.class)); @@ -539,10 +552,11 @@ public void testAutoconfigElasticPasswordAuthnErrorWhenHashPromotionFails() thro public void testBootstrapElasticPasswordFailsOnceElasticUserExists() throws Exception { MockSecureSettings mockSecureSettings = new MockSecureSettings(); - mockSecureSettings.setString("bootstrap.password", "foobar"); + mockSecureSettings.setString("bootstrap.password", "foobar longer than 14 chars because of FIPS"); if (randomBoolean()) { mockSecureSettings.setString("autoconfiguration.password_hash", - new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash( + new SecureString("barbaz longer than 14 chars because of FIPS".toCharArray())))); } Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); @@ -565,7 +579,7 @@ public void testBootstrapElasticPasswordFailsOnceElasticUserExists() throws Exce assertFailedAuthentication(listener, "elastic"); listener = new PlainActionFuture<>(); reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), - new SecureString("barbaz".toCharArray())), listener); + new SecureString("barbaz longer than 14 chars because of FIPS".toCharArray())), listener); assertFailedAuthentication(listener, "elastic"); // now try with the real password listener = new PlainActionFuture<>(); @@ -577,7 +591,8 @@ public void testBootstrapElasticPasswordFailsOnceElasticUserExists() throws Exce public void testAutoconfigPasswordHashFailsOnceElasticUserExists() throws Exception { MockSecureSettings mockSecureSettings = new MockSecureSettings(); mockSecureSettings.setString("autoconfiguration.password_hash", - new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("auto_password".toCharArray())))); + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash( + new SecureString("auto_password longer than 14 chars because of FIPS".toCharArray())))); Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, @@ -585,16 +600,16 @@ public void testAutoconfigPasswordHashFailsOnceElasticUserExists() throws Except PlainActionFuture listener = new PlainActionFuture<>(); // Mocked users store is initiated with default hashing algorithm final Hasher hasher = Hasher.resolve("bcrypt"); - doAnswer(getAnswer(true, new SecureString("password".toCharArray()), hasher)).when(usersStore) + doAnswer(getAnswer(true, new SecureString("password longer than 14 chars because of FIPS".toCharArray()), hasher)).when(usersStore) .getReservedUserInfo(eq("elastic"), anyActionListener()); reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), - new SecureString("password".toCharArray())), listener); + new SecureString("password longer than 14 chars because of FIPS".toCharArray())), listener); final AuthenticationResult result = listener.get(); assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); // but auto config password does not work listener = new PlainActionFuture<>(); reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), - new SecureString("auto_password".toCharArray())), listener); + new SecureString("auto_password longer than 14 chars because of FIPS".toCharArray())), listener); assertFailedAuthentication(listener, "elastic"); verify(usersStore, times(2)).getReservedUserInfo(eq("elastic"), anyActionListener()); verify(usersStore, times(0)).createElasticUser(any(char[].class), anyActionListener()); @@ -602,11 +617,12 @@ public void testAutoconfigPasswordHashFailsOnceElasticUserExists() throws Except public void testNonElasticUsersCannotUseBootstrapPassword() throws Exception { final MockSecureSettings mockSecureSettings = new MockSecureSettings(); - final String password = randomAlphaOfLengthBetween(8, 24); + final String password = randomAlphaOfLengthBetween(15, 24); mockSecureSettings.setString("bootstrap.password", password); if (randomBoolean()) { mockSecureSettings.setString("autoconfiguration.password_hash", - new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString("barbaz".toCharArray())))); + new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash( + new SecureString("barbaz longer than 14 chars because of FIPS".toCharArray())))); } Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); @@ -628,7 +644,7 @@ public void testNonElasticUsersCannotUseBootstrapPassword() throws Exception { public void testNonElasticUsersCannotUseAutoconfigPasswordHash() throws Exception { final MockSecureSettings mockSecureSettings = new MockSecureSettings(); - final String password = randomAlphaOfLengthBetween(8, 24); + final String password = randomAlphaOfLengthBetween(15, 24); mockSecureSettings.setString("autoconfiguration.password_hash", new String(randomFrom(Hasher.BCRYPT, Hasher.PBKDF2).hash(new SecureString(password.toCharArray())))); Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); From b249f9c8ceb854c4a18c9ca656c50d03b919027e Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 5 Oct 2021 09:57:19 +0300 Subject: [PATCH 16/16] Integ test for authn success after promotion failure --- ...ervedRealmElasticAutoconfigIntegTests.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java index f0c49e2d68bd7..9d4e3d9f2a288 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmElasticAutoconfigIntegTests.java @@ -16,11 +16,14 @@ import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.ResponseException; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.SecuritySingleNodeTestCase; +import org.elasticsearch.xpack.core.security.action.user.PutUserAction; +import org.elasticsearch.xpack.core.security.action.user.PutUserRequest; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames; @@ -105,4 +108,64 @@ public void testAutoconfigFailedPasswordPromotion() { assertAcked(client().admin().cluster().updateSettings(updateSettingsRequest).actionGet()); } } + + public void testAutoconfigSucceedsAfterPromotionFailure() throws Exception { + try { + // create any non-elastic user, which triggers .security index creation + final PutUserRequest putUserRequest = new PutUserRequest(); + final String username = randomAlphaOfLength(8); + putUserRequest.username(username); + final SecureString password = new SecureString("super-strong-password!".toCharArray()); + putUserRequest.passwordHash(Hasher.PBKDF2.hash(password)); + putUserRequest.roles(Strings.EMPTY_ARRAY); + client().execute(PutUserAction.INSTANCE, putUserRequest).get(); + + // but then make the cluster read-only + ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); + updateSettingsRequest.transientSettings(Settings.builder().put(Metadata.SETTING_READ_ONLY_ALLOW_DELETE_SETTING.getKey(), + true)); + assertAcked(client().admin().cluster().updateSettings(updateSettingsRequest).actionGet()); + + // elastic user now gets 503 for the good password + Request restRequest = randomFrom(new Request("GET", "/_security/_authenticate"), new Request("GET", "_cluster/health"), + new Request("GET", "_nodes")); + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, UsernamePasswordToken.basicAuthHeaderValue("elastic", + new SecureString("auto_password_that_is_longer_than_14_chars_because_of_FIPS".toCharArray()))); + restRequest.setOptions(options); + ResponseException exception = expectThrows(ResponseException.class, () -> getRestClient().performRequest(restRequest)); + assertThat(exception.getResponse().getStatusLine().getStatusCode(), is(RestStatus.SERVICE_UNAVAILABLE.getStatus())); + + // clear cluster-wide write block + updateSettingsRequest = new ClusterUpdateSettingsRequest(); + updateSettingsRequest.transientSettings(Settings.builder().put(Metadata.SETTING_READ_ONLY_ALLOW_DELETE_SETTING.getKey(), + (String) null)); + assertAcked(client().admin().cluster().updateSettings(updateSettingsRequest).actionGet()); + + if (randomBoolean()) { + Request restRequest2 = randomFrom(new Request("GET", "/_security/_authenticate"), new Request("GET", "_cluster/health"), + new Request("GET", "_nodes")); + options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, UsernamePasswordToken.basicAuthHeaderValue("elastic", + new SecureString("wrong password_that_is_longer_than_14_chars_because_of_FIPS".toCharArray()))); + restRequest2.setOptions(options); + exception = expectThrows(ResponseException.class, () -> getRestClient().performRequest(restRequest2)); + assertThat(exception.getResponse().getStatusLine().getStatusCode(), is(RestStatus.UNAUTHORIZED.getStatus())); + } + + // now the auto config password can be promoted, and authn succeeds + Request restRequest3 = randomFrom(new Request("GET", "/_security/_authenticate"), new Request("GET", "_cluster/health"), + new Request("GET", "_nodes")); + options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, UsernamePasswordToken.basicAuthHeaderValue("elastic", + new SecureString("auto_password_that_is_longer_than_14_chars_because_of_FIPS".toCharArray()))); + restRequest3.setOptions(options); + assertThat(getRestClient().performRequest(restRequest3).getStatusLine().getStatusCode(), is(RestStatus.OK.getStatus())); + } finally { + ClusterUpdateSettingsRequest updateSettingsRequest = new ClusterUpdateSettingsRequest(); + updateSettingsRequest.transientSettings(Settings.builder().put(Metadata.SETTING_READ_ONLY_ALLOW_DELETE_SETTING.getKey(), + (String) null)); + assertAcked(client().admin().cluster().updateSettings(updateSettingsRequest).actionGet()); + } + } }