Skip to content

Commit d9dd6ae

Browse files
ankit--sethielasticsearchmachineslobodanadamovic
authored
Fix error message when changing the password for a user in the file realm (#127621) (#129434)
* This PR addresses #113535 - a confusing error message when the user attempts to update the password for the `elastic` superuser in a cloud deployment. At the heart of the issue is the difference in how the `elastic` superuser is implemented on self-hosted deployments vs. managed cloud deployments. Elasticsearch has two distinct security realms: `file` and `native`. On a self-hosted deployment, the `elastic` superuser is represented as a document in the `.security` index, whereas in a cloud deployment `elastic` is defined in the `ES_PATH_CONF/users` and `ES_PATH_CONF/user_roles` files placed on each node in the cluster. The TransportChangePasswordAction impl is designed to update the password for users in the `native` realm specifically, and a failure on cloud to change the password for `elastic` using the Change Password API fails with the error that the user does not exist. The solution here leverages `fileUserPasswdStore.userExists` to do a low cost check on whether the request username belongs to the `file` realm and will exit early with an informative error message if that is the case. * Update docs/changelog/127621.yaml * re-do the ticket in Continuation-passing style. Previous unanswered questions around CI/CD are resolved. * link issue * accidental commit, reverting * Update docs/changelog/127621.yaml * try to check for membership in all non-native realms * [CI] Auto commit changes from spotless * improve checks to fix failing tests * improve * improve * improve logic with GroupedActionListener * return early * extra spaces * revert * Update x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordAction.java * Update x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/esnative/ClientReservedRealm.java * PR feedback * fix imports * fix test * add test --------- Co-authored-by: elasticsearchmachine <[email protected]> Co-authored-by: Slobodan Adamović <[email protected]>
1 parent 8208fcb commit d9dd6ae

File tree

5 files changed

+331
-19
lines changed

5 files changed

+331
-19
lines changed

docs/changelog/127621.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 127621
2+
summary: Fix error message when changing the password for a user in the file realm
3+
area: Security
4+
type: bug
5+
issues: []

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/esnative/ClientReservedRealm.java

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,32 @@
1111
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
1212
import org.elasticsearch.xpack.core.security.user.UsernamesField;
1313

14+
import java.util.Set;
15+
1416
public class ClientReservedRealm {
1517

18+
private static final Set<String> RESERVED_USERNAMES = Set.of(
19+
UsernamesField.ELASTIC_NAME,
20+
UsernamesField.DEPRECATED_KIBANA_NAME,
21+
UsernamesField.KIBANA_NAME,
22+
UsernamesField.LOGSTASH_NAME,
23+
UsernamesField.BEATS_NAME,
24+
UsernamesField.APM_NAME,
25+
UsernamesField.REMOTE_MONITORING_NAME
26+
);
27+
1628
public static boolean isReserved(String username, Settings settings) {
1729
assert username != null;
18-
switch (username) {
19-
case UsernamesField.ELASTIC_NAME:
20-
case UsernamesField.DEPRECATED_KIBANA_NAME:
21-
case UsernamesField.KIBANA_NAME:
22-
case UsernamesField.LOGSTASH_NAME:
23-
case UsernamesField.BEATS_NAME:
24-
case UsernamesField.APM_NAME:
25-
case UsernamesField.REMOTE_MONITORING_NAME:
26-
return XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings);
27-
default:
28-
return AnonymousUser.isAnonymousUsername(username, settings);
30+
if (isReservedUsername(username)) {
31+
return XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings);
2932
}
33+
return AnonymousUser.isAnonymousUsername(username, settings);
34+
}
35+
36+
/**
37+
* checks membership in a set, doesn't care if the reserved realm is enabled
38+
*/
39+
public static boolean isReservedUsername(String username) {
40+
return RESERVED_USERNAMES.contains(username);
3041
}
3142
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordAction.java

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,57 @@
88

99
import org.elasticsearch.action.ActionListener;
1010
import org.elasticsearch.action.ActionResponse;
11+
import org.elasticsearch.action.ActionRunnable;
1112
import org.elasticsearch.action.ActionType;
1213
import org.elasticsearch.action.support.ActionFilters;
14+
import org.elasticsearch.action.support.GroupedActionListener;
1315
import org.elasticsearch.action.support.HandledTransportAction;
16+
import org.elasticsearch.common.ValidationException;
1417
import org.elasticsearch.common.settings.Settings;
1518
import org.elasticsearch.common.util.concurrent.EsExecutors;
1619
import org.elasticsearch.injection.guice.Inject;
1720
import org.elasticsearch.tasks.Task;
1821
import org.elasticsearch.transport.TransportService;
1922
import org.elasticsearch.xpack.core.XPackSettings;
2023
import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest;
24+
import org.elasticsearch.xpack.core.security.authc.Realm;
25+
import org.elasticsearch.xpack.core.security.authc.esnative.ClientReservedRealm;
26+
import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
2127
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
2228
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
29+
import org.elasticsearch.xpack.core.security.user.User;
30+
import org.elasticsearch.xpack.security.authc.Realms;
2331
import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore;
32+
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
2433

34+
import java.util.List;
2535
import java.util.Locale;
36+
import java.util.Objects;
37+
import java.util.Optional;
38+
import java.util.Set;
39+
40+
import static org.elasticsearch.xpack.core.security.user.UsernamesField.ELASTIC_NAME;
41+
import static org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.USER_NOT_FOUND_MESSAGE;
2642

2743
public class TransportChangePasswordAction extends HandledTransportAction<ChangePasswordRequest, ActionResponse.Empty> {
2844

2945
public static final ActionType<ActionResponse.Empty> TYPE = new ActionType<>("cluster:admin/xpack/security/user/change_password");
3046
private final Settings settings;
3147
private final NativeUsersStore nativeUsersStore;
48+
private final Realms realms;
3249

3350
@Inject
3451
public TransportChangePasswordAction(
3552
Settings settings,
3653
TransportService transportService,
3754
ActionFilters actionFilters,
38-
NativeUsersStore nativeUsersStore
55+
NativeUsersStore nativeUsersStore,
56+
Realms realms
3957
) {
4058
super(TYPE.name(), transportService, actionFilters, ChangePasswordRequest::new, EsExecutors.DIRECT_EXECUTOR_SERVICE);
4159
this.settings = settings;
4260
this.nativeUsersStore = nativeUsersStore;
61+
this.realms = realms;
4362
}
4463

4564
@Override
@@ -62,6 +81,76 @@ protected void doExecute(Task task, ChangePasswordRequest request, ActionListene
6281
);
6382
return;
6483
}
65-
nativeUsersStore.changePassword(request, listener.safeMap(v -> ActionResponse.Empty.INSTANCE));
84+
85+
if (ClientReservedRealm.isReservedUsername(username) && XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings) == false) {
86+
// when on cloud and resetting the elastic operator user by mistake
87+
ValidationException validationException = new ValidationException();
88+
validationException.addValidationError(
89+
"user ["
90+
+ username
91+
+ "] belongs to the "
92+
+ ReservedRealm.NAME
93+
+ " realm which is disabled."
94+
+ (ELASTIC_NAME.equalsIgnoreCase(username)
95+
? " In a cloud deployment, the password can be changed through the cloud console."
96+
: "")
97+
);
98+
listener.onFailure(validationException);
99+
return;
100+
}
101+
102+
// check if user exists in the native realm
103+
nativeUsersStore.getUser(username, new ActionListener<>() {
104+
@Override
105+
public void onResponse(User user) {
106+
// nativeUsersStore.changePassword can create a missing reserved user, so enter only if not reserved
107+
if (ClientReservedRealm.isReserved(username, settings) == false && user == null) {
108+
List<Realm> nonNativeRealms = realms.getActiveRealms()
109+
.stream()
110+
.filter(t -> Set.of(NativeRealmSettings.TYPE, ReservedRealm.TYPE).contains(t.type()) == false) // Reserved realm is
111+
// implemented in the
112+
// native store
113+
.toList();
114+
if (nonNativeRealms.isEmpty()) {
115+
listener.onFailure(createUserNotFoundException());
116+
return;
117+
}
118+
119+
GroupedActionListener<User> gal = new GroupedActionListener<>(nonNativeRealms.size(), ActionListener.wrap(users -> {
120+
final Optional<User> nonNativeUser = users.stream().filter(Objects::nonNull).findAny();
121+
if (nonNativeUser.isPresent()) {
122+
listener.onFailure(
123+
new ValidationException().addValidationError(
124+
"user [" + username + "] does not belong to the native realm and cannot be managed via this API."
125+
)
126+
);
127+
} else {
128+
// user wasn't found in any other realm, display standard not-found message
129+
listener.onFailure(createUserNotFoundException());
130+
}
131+
}, listener::onFailure));
132+
for (Realm realm : nonNativeRealms) {
133+
EsExecutors.DIRECT_EXECUTOR_SERVICE.execute(
134+
ActionRunnable.wrap(gal, userActionListener -> realm.lookupUser(username, userActionListener))
135+
);
136+
}
137+
} else {
138+
// safe to proceed
139+
nativeUsersStore.changePassword(request, listener.safeMap(v -> ActionResponse.Empty.INSTANCE));
140+
}
141+
}
142+
143+
@Override
144+
public void onFailure(Exception e) {
145+
listener.onFailure(e);
146+
}
147+
});
148+
149+
}
150+
151+
private static ValidationException createUserNotFoundException() {
152+
ValidationException validationException = new ValidationException();
153+
validationException.addValidationError(USER_NOT_FOUND_MESSAGE);
154+
return validationException;
66155
}
67156
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public class NativeUsersStore {
8181

8282
public static final String USER_DOC_TYPE = "user";
8383
public static final String RESERVED_USER_TYPE = "reserved-user";
84+
public static final String USER_NOT_FOUND_MESSAGE = "user must exist in order to change password";
8485
private static final Logger logger = LogManager.getLogger(NativeUsersStore.class);
8586

8687
private final Settings settings;
@@ -315,7 +316,7 @@ public void onFailure(Exception e) {
315316
} else {
316317
logger.debug(() -> format("failed to change password for user [%s]", request.username()), e);
317318
ValidationException validationException = new ValidationException();
318-
validationException.addValidationError("user must exist in order to change password");
319+
validationException.addValidationError(USER_NOT_FOUND_MESSAGE);
319320
listener.onFailure(validationException);
320321
}
321322
} else {

0 commit comments

Comments
 (0)