Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/127621.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 127621
summary: Fix error message when changing the password for a user in the file realm
area: Security
type: bug
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,32 @@
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
import org.elasticsearch.xpack.core.security.user.UsernamesField;

import java.util.Set;

public class ClientReservedRealm {

private static final Set<String> RESERVED_USERNAMES = Set.of(
UsernamesField.ELASTIC_NAME,
UsernamesField.DEPRECATED_KIBANA_NAME,
UsernamesField.KIBANA_NAME,
UsernamesField.LOGSTASH_NAME,
UsernamesField.BEATS_NAME,
UsernamesField.APM_NAME,
UsernamesField.REMOTE_MONITORING_NAME
);

public static boolean isReserved(String username, Settings settings) {
assert username != null;
switch (username) {
case UsernamesField.ELASTIC_NAME:
case UsernamesField.DEPRECATED_KIBANA_NAME:
case UsernamesField.KIBANA_NAME:
case UsernamesField.LOGSTASH_NAME:
case UsernamesField.BEATS_NAME:
case UsernamesField.APM_NAME:
case UsernamesField.REMOTE_MONITORING_NAME:
return XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings);
default:
return AnonymousUser.isAnonymousUsername(username, settings);
if (isReservedUsername(username)) {
return XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings);
}
return AnonymousUser.isAnonymousUsername(username, settings);
}

/**
* checks membership in a set, doesn't care if the reserved realm is enabled
*/
public static boolean isReservedUsername(String username) {
return RESERVED_USERNAMES.contains(username);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,57 @@

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.ActionRunnable;
import org.elasticsearch.action.ActionType;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.GroupedActionListener;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.common.ValidationException;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.EsExecutors;
import org.elasticsearch.injection.guice.Inject;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest;
import org.elasticsearch.xpack.core.security.authc.Realm;
import org.elasticsearch.xpack.core.security.authc.esnative.ClientReservedRealm;
import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.authc.Realms;
import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;

import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import static org.elasticsearch.xpack.core.security.user.UsernamesField.ELASTIC_NAME;
import static org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.USER_NOT_FOUND_MESSAGE;

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

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

@Inject
public TransportChangePasswordAction(
Settings settings,
TransportService transportService,
ActionFilters actionFilters,
NativeUsersStore nativeUsersStore
NativeUsersStore nativeUsersStore,
Realms realms
) {
super(TYPE.name(), transportService, actionFilters, ChangePasswordRequest::new, EsExecutors.DIRECT_EXECUTOR_SERVICE);
this.settings = settings;
this.nativeUsersStore = nativeUsersStore;
this.realms = realms;
}

@Override
Expand All @@ -62,6 +81,76 @@ protected void doExecute(Task task, ChangePasswordRequest request, ActionListene
);
return;
}
nativeUsersStore.changePassword(request, listener.safeMap(v -> ActionResponse.Empty.INSTANCE));

if (ClientReservedRealm.isReservedUsername(username) && XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings) == false) {
// when on cloud and resetting the elastic operator user by mistake
ValidationException validationException = new ValidationException();
validationException.addValidationError(
"user ["
+ username
+ "] belongs to the "
+ ReservedRealm.NAME
+ " realm which is disabled."
+ (ELASTIC_NAME.equalsIgnoreCase(username)
? " In a cloud deployment, the password can be changed through the cloud console."
: "")
);
listener.onFailure(validationException);
return;
}

// check if user exists in the native realm
nativeUsersStore.getUser(username, new ActionListener<>() {
@Override
public void onResponse(User user) {
// nativeUsersStore.changePassword can create a missing reserved user, so enter only if not reserved
if (ClientReservedRealm.isReserved(username, settings) == false && user == null) {
List<Realm> nonNativeRealms = realms.getActiveRealms()
.stream()
.filter(t -> Set.of(NativeRealmSettings.TYPE, ReservedRealm.TYPE).contains(t.type()) == false) // Reserved realm is
// implemented in the
// native store
.toList();
if (nonNativeRealms.isEmpty()) {
listener.onFailure(createUserNotFoundException());
return;
}

GroupedActionListener<User> gal = new GroupedActionListener<>(nonNativeRealms.size(), ActionListener.wrap(users -> {
final Optional<User> nonNativeUser = users.stream().filter(Objects::nonNull).findAny();
if (nonNativeUser.isPresent()) {
listener.onFailure(
new ValidationException().addValidationError(
"user [" + username + "] does not belong to the native realm and cannot be managed via this API."
)
);
} else {
// user wasn't found in any other realm, display standard not-found message
listener.onFailure(createUserNotFoundException());
}
}, listener::onFailure));
for (Realm realm : nonNativeRealms) {
EsExecutors.DIRECT_EXECUTOR_SERVICE.execute(
ActionRunnable.wrap(gal, userActionListener -> realm.lookupUser(username, userActionListener))
);
}
} else {
// safe to proceed
nativeUsersStore.changePassword(request, listener.safeMap(v -> ActionResponse.Empty.INSTANCE));
}
}

@Override
public void onFailure(Exception e) {
listener.onFailure(e);
}
});

}

private static ValidationException createUserNotFoundException() {
ValidationException validationException = new ValidationException();
validationException.addValidationError(USER_NOT_FOUND_MESSAGE);
return validationException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public class NativeUsersStore {

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

private final Settings settings;
Expand Down Expand Up @@ -315,7 +316,7 @@ public void onFailure(Exception e) {
} else {
logger.debug(() -> format("failed to change password for user [%s]", request.username()), e);
ValidationException validationException = new ValidationException();
validationException.addValidationError("user must exist in order to change password");
validationException.addValidationError(USER_NOT_FOUND_MESSAGE);
listener.onFailure(validationException);
}
} else {
Expand Down
Loading