88
99import org .elasticsearch .action .ActionListener ;
1010import org .elasticsearch .action .ActionResponse ;
11+ import org .elasticsearch .action .ActionRunnable ;
1112import org .elasticsearch .action .ActionType ;
1213import org .elasticsearch .action .support .ActionFilters ;
14+ import org .elasticsearch .action .support .GroupedActionListener ;
1315import org .elasticsearch .action .support .HandledTransportAction ;
16+ import org .elasticsearch .common .ValidationException ;
1417import org .elasticsearch .common .settings .Settings ;
1518import org .elasticsearch .common .util .concurrent .EsExecutors ;
1619import org .elasticsearch .injection .guice .Inject ;
1720import org .elasticsearch .tasks .Task ;
1821import org .elasticsearch .transport .TransportService ;
1922import org .elasticsearch .xpack .core .XPackSettings ;
2023import 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 ;
2127import org .elasticsearch .xpack .core .security .authc .support .Hasher ;
2228import org .elasticsearch .xpack .core .security .user .AnonymousUser ;
29+ import org .elasticsearch .xpack .core .security .user .User ;
30+ import org .elasticsearch .xpack .security .authc .Realms ;
2331import org .elasticsearch .xpack .security .authc .esnative .NativeUsersStore ;
32+ import org .elasticsearch .xpack .security .authc .esnative .ReservedRealm ;
2433
34+ import java .util .List ;
2535import 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
2743public 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}
0 commit comments