Skip to content

Commit 0f2f5e2

Browse files
authored
Fix iterate-from-1 bug in smart realm order (#49617)
The AuthenticationService has a feature to "smart order" the realm chain so that whicherver realm was the last one to successfully authenticate a given user will be tried first when that user tries to authenticate again. There was a bug where the building of this realm order would incorrectly drop the first realm from the default chain unless that realm was the "last successful" realm. In most cases this didn't cause problems because the first realm is the reserved realm and so it is unusual for a user that authenticated against a different realm to later need to authenticate against the resevered realm. This commit fixes that bug and adds relevant asserts and tests. Backport of: #49473
1 parent 6e643fe commit 0f2f5e2

File tree

2 files changed

+44
-5
lines changed

2 files changed

+44
-5
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,11 +420,13 @@ private List<Realm> getRealmList(String principal) {
420420
if (index > 0) {
421421
final List<Realm> smartOrder = new ArrayList<>(orderedRealmList.size());
422422
smartOrder.add(lastSuccess);
423-
for (int i = 1; i < orderedRealmList.size(); i++) {
423+
for (int i = 0; i < orderedRealmList.size(); i++) {
424424
if (i != index) {
425425
smartOrder.add(orderedRealmList.get(i));
426426
}
427427
}
428+
assert smartOrder.size() == orderedRealmList.size() && smartOrder.containsAll(orderedRealmList)
429+
: "Element mismatch between SmartOrder=" + smartOrder + " and DefaultOrder=" + orderedRealmList;
428430
return Collections.unmodifiableList(smartOrder);
429431
}
430432
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@
136136
*/
137137
public class AuthenticationServiceTests extends ESTestCase {
138138

139+
private static final String SECOND_REALM_NAME = "second_realm";
140+
private static final String SECOND_REALM_TYPE = "second";
141+
private static final String FIRST_REALM_NAME = "file_realm";
142+
private static final String FIRST_REALM_TYPE = "file";
139143
private AuthenticationService service;
140144
private TransportMessage message;
141145
private RestRequest restRequest;
@@ -170,11 +174,11 @@ public void init() throws Exception {
170174
threadContext = new ThreadContext(Settings.EMPTY);
171175

172176
firstRealm = mock(Realm.class);
173-
when(firstRealm.type()).thenReturn("file");
174-
when(firstRealm.name()).thenReturn("file_realm");
177+
when(firstRealm.type()).thenReturn(FIRST_REALM_TYPE);
178+
when(firstRealm.name()).thenReturn(FIRST_REALM_NAME);
175179
secondRealm = mock(Realm.class);
176-
when(secondRealm.type()).thenReturn("second");
177-
when(secondRealm.name()).thenReturn("second_realm");
180+
when(secondRealm.type()).thenReturn(SECOND_REALM_TYPE);
181+
when(secondRealm.name()).thenReturn(SECOND_REALM_NAME);
178182
settings = Settings.builder()
179183
.put("path.home", createTempDir())
180184
.put("node.name", "authc_test")
@@ -311,26 +315,35 @@ public void testAuthenticateSmartRealmOrdering() {
311315
when(secondRealm.token(threadContext)).thenReturn(token);
312316
final String reqId = AuditUtil.getOrGenerateRequestId(threadContext);
313317

318+
// Authenticate against the normal chain. 1st Realm will be checked (and not pass) then 2nd realm will successfully authc
314319
final AtomicBoolean completed = new AtomicBoolean(false);
315320
service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
316321
assertThat(result, notNullValue());
317322
assertThat(result.getUser(), is(user));
318323
assertThat(result.getLookedUpBy(), is(nullValue()));
319324
assertThat(result.getAuthenticatedBy(), is(notNullValue())); // TODO implement equals
325+
assertThat(result.getAuthenticatedBy().getName(), is(SECOND_REALM_NAME));
326+
assertThat(result.getAuthenticatedBy().getType(), is(SECOND_REALM_TYPE));
320327
assertThreadContextContainsAuthentication(result);
321328
setCompletedToTrue(completed);
322329
}, this::logAndFail));
323330
assertTrue(completed.get());
324331

325332
completed.set(false);
333+
// Authenticate against the smart chain.
334+
// "SecondRealm" will be at the top of the list and will successfully authc.
335+
// "FirstRealm" will not be used
326336
service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
327337
assertThat(result, notNullValue());
328338
assertThat(result.getUser(), is(user));
329339
assertThat(result.getLookedUpBy(), is(nullValue()));
330340
assertThat(result.getAuthenticatedBy(), is(notNullValue())); // TODO implement equals
341+
assertThat(result.getAuthenticatedBy().getName(), is(SECOND_REALM_NAME));
342+
assertThat(result.getAuthenticatedBy().getType(), is(SECOND_REALM_TYPE));
331343
assertThreadContextContainsAuthentication(result);
332344
setCompletedToTrue(completed);
333345
}, this::logAndFail));
346+
334347
verify(auditTrail).authenticationFailed(reqId, firstRealm.name(), token, "_action", message);
335348
verify(auditTrail, times(2)).authenticationSuccess(reqId, secondRealm.name(), user, "_action", message);
336349
verify(firstRealm, times(2)).name(); // used above one time
@@ -343,6 +356,30 @@ public void testAuthenticateSmartRealmOrdering() {
343356
verify(firstRealm).authenticate(eq(token), any(ActionListener.class));
344357
verify(secondRealm, times(2)).authenticate(eq(token), any(ActionListener.class));
345358
verifyNoMoreInteractions(auditTrail, firstRealm, secondRealm);
359+
360+
// Now assume some change in the backend system so that 2nd realm no longer has the user, but the 1st realm does.
361+
mockAuthenticate(secondRealm, token, null);
362+
mockAuthenticate(firstRealm, token, user);
363+
364+
completed.set(false);
365+
// This will authenticate against the smart chain.
366+
// "SecondRealm" will be at the top of the list but will no longer authenticate the user.
367+
// Then "FirstRealm" will be checked.
368+
service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
369+
assertThat(result, notNullValue());
370+
assertThat(result.getUser(), is(user));
371+
assertThat(result.getLookedUpBy(), is(nullValue()));
372+
assertThat(result.getAuthenticatedBy(), is(notNullValue()));
373+
assertThat(result.getAuthenticatedBy().getName(), is(FIRST_REALM_NAME));
374+
assertThat(result.getAuthenticatedBy().getType(), is(FIRST_REALM_TYPE));
375+
assertThreadContextContainsAuthentication(result);
376+
setCompletedToTrue(completed);
377+
}, this::logAndFail));
378+
379+
verify(auditTrail, times(1)).authenticationFailed(reqId, SECOND_REALM_NAME, token, "_action", message);
380+
verify(auditTrail, times(1)).authenticationSuccess(reqId, FIRST_REALM_NAME, user, "_action", message);
381+
verify(secondRealm, times(3)).authenticate(eq(token), any(ActionListener.class)); // 2 from above + 1 more
382+
verify(firstRealm, times(2)).authenticate(eq(token), any(ActionListener.class)); // 1 from above + 1 more
346383
}
347384

348385
public void testCacheClearOnSecurityIndexChange() {

0 commit comments

Comments
 (0)