Skip to content

Commit e9ad1a7

Browse files
authored
Fix iterate-from-1 bug in smart realm order (#49614)
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 e965a6f commit e9ad1a7

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -427,11 +427,13 @@ private List<Realm> getRealmList(String principal) {
427427
if (index > 0) {
428428
final List<Realm> smartOrder = new ArrayList<>(orderedRealmList.size());
429429
smartOrder.add(lastSuccess);
430-
for (int i = 1; i < orderedRealmList.size(); i++) {
430+
for (int i = 0; i < orderedRealmList.size(); i++) {
431431
if (i != index) {
432432
smartOrder.add(orderedRealmList.get(i));
433433
}
434434
}
435+
assert smartOrder.size() == orderedRealmList.size() && smartOrder.containsAll(orderedRealmList)
436+
: "Element mismatch between SmartOrder=" + smartOrder + " and DefaultOrder=" + orderedRealmList;
435437
return Collections.unmodifiableList(smartOrder);
436438
}
437439
}

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

+41-4
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@
134134
*/
135135
public class AuthenticationServiceTests extends ESTestCase {
136136

137+
private static final String SECOND_REALM_NAME = "second_realm";
138+
private static final String SECOND_REALM_TYPE = "second";
139+
private static final String FIRST_REALM_NAME = "file_realm";
140+
private static final String FIRST_REALM_TYPE = "file";
137141
private AuthenticationService service;
138142
private TransportMessage message;
139143
private RestRequest restRequest;
@@ -167,11 +171,11 @@ public void init() throws Exception {
167171
threadContext = new ThreadContext(Settings.EMPTY);
168172

169173
firstRealm = mock(Realm.class);
170-
when(firstRealm.type()).thenReturn("file");
171-
when(firstRealm.name()).thenReturn("file_realm");
174+
when(firstRealm.type()).thenReturn(FIRST_REALM_TYPE);
175+
when(firstRealm.name()).thenReturn(FIRST_REALM_NAME);
172176
secondRealm = mock(Realm.class);
173-
when(secondRealm.type()).thenReturn("second");
174-
when(secondRealm.name()).thenReturn("second_realm");
177+
when(secondRealm.type()).thenReturn(SECOND_REALM_TYPE);
178+
when(secondRealm.name()).thenReturn(SECOND_REALM_NAME);
175179
Settings settings = Settings.builder()
176180
.put("path.home", createTempDir())
177181
.put("node.name", "authc_test")
@@ -305,26 +309,35 @@ public void testAuthenticateSmartRealmOrdering() {
305309
when(secondRealm.token(threadContext)).thenReturn(token);
306310
final String reqId = AuditUtil.getOrGenerateRequestId(threadContext);
307311

312+
// Authenticate against the normal chain. 1st Realm will be checked (and not pass) then 2nd realm will successfully authc
308313
final AtomicBoolean completed = new AtomicBoolean(false);
309314
service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
310315
assertThat(result, notNullValue());
311316
assertThat(result.getUser(), is(user));
312317
assertThat(result.getLookedUpBy(), is(nullValue()));
313318
assertThat(result.getAuthenticatedBy(), is(notNullValue())); // TODO implement equals
319+
assertThat(result.getAuthenticatedBy().getName(), is(SECOND_REALM_NAME));
320+
assertThat(result.getAuthenticatedBy().getType(), is(SECOND_REALM_TYPE));
314321
assertThreadContextContainsAuthentication(result);
315322
setCompletedToTrue(completed);
316323
}, this::logAndFail));
317324
assertTrue(completed.get());
318325

319326
completed.set(false);
327+
// Authenticate against the smart chain.
328+
// "SecondRealm" will be at the top of the list and will successfully authc.
329+
// "FirstRealm" will not be used
320330
service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
321331
assertThat(result, notNullValue());
322332
assertThat(result.getUser(), is(user));
323333
assertThat(result.getLookedUpBy(), is(nullValue()));
324334
assertThat(result.getAuthenticatedBy(), is(notNullValue())); // TODO implement equals
335+
assertThat(result.getAuthenticatedBy().getName(), is(SECOND_REALM_NAME));
336+
assertThat(result.getAuthenticatedBy().getType(), is(SECOND_REALM_TYPE));
325337
assertThreadContextContainsAuthentication(result);
326338
setCompletedToTrue(completed);
327339
}, this::logAndFail));
340+
328341
verify(auditTrail).authenticationFailed(reqId, firstRealm.name(), token, "_action", message);
329342
verify(auditTrail, times(2)).authenticationSuccess(reqId, secondRealm.name(), user, "_action", message);
330343
verify(firstRealm, times(2)).name(); // used above one time
@@ -337,6 +350,30 @@ public void testAuthenticateSmartRealmOrdering() {
337350
verify(firstRealm).authenticate(eq(token), any(ActionListener.class));
338351
verify(secondRealm, times(2)).authenticate(eq(token), any(ActionListener.class));
339352
verifyNoMoreInteractions(auditTrail, firstRealm, secondRealm);
353+
354+
// Now assume some change in the backend system so that 2nd realm no longer has the user, but the 1st realm does.
355+
mockAuthenticate(secondRealm, token, null);
356+
mockAuthenticate(firstRealm, token, user);
357+
358+
completed.set(false);
359+
// This will authenticate against the smart chain.
360+
// "SecondRealm" will be at the top of the list but will no longer authenticate the user.
361+
// Then "FirstRealm" will be checked.
362+
service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
363+
assertThat(result, notNullValue());
364+
assertThat(result.getUser(), is(user));
365+
assertThat(result.getLookedUpBy(), is(nullValue()));
366+
assertThat(result.getAuthenticatedBy(), is(notNullValue()));
367+
assertThat(result.getAuthenticatedBy().getName(), is(FIRST_REALM_NAME));
368+
assertThat(result.getAuthenticatedBy().getType(), is(FIRST_REALM_TYPE));
369+
assertThreadContextContainsAuthentication(result);
370+
setCompletedToTrue(completed);
371+
}, this::logAndFail));
372+
373+
verify(auditTrail, times(1)).authenticationFailed(reqId, SECOND_REALM_NAME, token, "_action", message);
374+
verify(auditTrail, times(1)).authenticationSuccess(reqId, FIRST_REALM_NAME, user, "_action", message);
375+
verify(secondRealm, times(3)).authenticate(eq(token), any(ActionListener.class)); // 2 from above + 1 more
376+
verify(firstRealm, times(2)).authenticate(eq(token), any(ActionListener.class)); // 1 from above + 1 more
340377
}
341378

342379
public void testCacheClearOnSecurityIndexChange() {

0 commit comments

Comments
 (0)