Skip to content

Commit 099f814

Browse files
committed
Security: don't call prepare index for reads (#34568)
The security native stores follow a pattern where `SecurityIndexManager#prepareIndexIfNeededThenExecute` wraps most calls made for the security index. The reasoning behind this was to check if the security index had been upgraded to the latest version in a consistent manner. However, this has the potential side effect that a read will trigger the creation of the security index or an updating of its mappings, which can lead to issues such as failures due to put mapping requests timing out even though we might have been able to read from the index and get the data necessary. This change introduces a new method, `checkIndexVersionThenExecute`, that provides the consistent checking of the security index to make sure it has been upgraded. That is the only check that this method performs prior to running the passed in operation, which removes the possible triggering of index creation and mapping updates for reads. Additionally, areas where we do reads now check the availability of the security index and can short circuit requests. Availability in this context means that the index exists and all primaries are active. This is the fixed version of #34246, which was reverted. Relates #33205
1 parent e259689 commit 099f814

16 files changed

+461
-249
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ Collection<Object> createComponents(Client client, ThreadPool threadPool, Cluste
444444
components.add(auditTrailService);
445445
this.auditTrailService.set(auditTrailService);
446446

447-
securityIndex.set(new SecurityIndexManager(settings, client, SecurityIndexManager.SECURITY_INDEX_NAME, clusterService));
447+
securityIndex.set(new SecurityIndexManager(client, SecurityIndexManager.SECURITY_INDEX_NAME, clusterService));
448448

449449
final TokenService tokenService = new TokenService(settings, Clock.systemUTC(), client, securityIndex.get(), clusterService);
450450
this.tokenService.set(tokenService);

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

Lines changed: 96 additions & 79 deletions
Large diffs are not rendered by default.

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

Lines changed: 95 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -115,31 +115,36 @@ public void getUser(String username, ActionListener<User> listener) {
115115
*/
116116
public void getUsers(String[] userNames, final ActionListener<Collection<User>> listener) {
117117
final Consumer<Exception> handleException = (t) -> {
118-
if (t instanceof IndexNotFoundException) {
119-
logger.trace("could not retrieve users because security index does not exist");
120-
// We don't invoke the onFailure listener here, instead just pass an empty list
121-
listener.onResponse(Collections.emptyList());
122-
} else {
123-
listener.onFailure(t);
118+
if (TransportActions.isShardNotAvailableException(t)) {
119+
logger.trace("could not retrieve users because of a shard not available exception", t);
120+
if (t instanceof IndexNotFoundException) {
121+
// We don't invoke the onFailure listener here, instead just pass an empty list
122+
// as the index doesn't exist. Could have been deleted between checks and execution
123+
listener.onResponse(Collections.emptyList());
124+
} else {
125+
listener.onFailure(t);
126+
}
124127
}
128+
listener.onFailure(t);
125129
};
126130

127-
if (securityIndex.indexExists() == false) {
128-
// TODO remove this short circuiting and fix tests that fail without this!
131+
final SecurityIndexManager frozenSecurityIndex = this.securityIndex.freeze();
132+
if (frozenSecurityIndex.indexExists() == false) {
129133
listener.onResponse(Collections.emptyList());
134+
} else if (frozenSecurityIndex.isAvailable() == false) {
135+
listener.onFailure(frozenSecurityIndex.getUnavailableReason());
130136
} else if (userNames.length == 1) { // optimization for single user lookup
131137
final String username = userNames[0];
132138
getUserAndPassword(username, ActionListener.wrap(
133139
(uap) -> listener.onResponse(uap == null ? Collections.emptyList() : Collections.singletonList(uap.user())),
134140
handleException));
135141
} else {
136-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> {
142+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () -> {
137143
final QueryBuilder query;
138144
if (userNames == null || userNames.length == 0) {
139145
query = QueryBuilders.termQuery(Fields.TYPE.getPreferredName(), USER_DOC_TYPE);
140146
} else {
141-
final String[] users = Arrays.asList(userNames).stream()
142-
.map(s -> getIdForUser(USER_DOC_TYPE, s)).toArray(String[]::new);
147+
final String[] users = Arrays.stream(userNames).map(s -> getIdForUser(USER_DOC_TYPE, s)).toArray(String[]::new);
143148
query = QueryBuilders.boolQuery().filter(QueryBuilders.idsQuery(NativeUserStoreField.INDEX_TYPE).addIds(users));
144149
}
145150
final Supplier<ThreadContext.StoredContext> supplier = client.threadPool().getThreadContext().newRestorableContext(false);
@@ -161,10 +166,13 @@ public void getUsers(String[] userNames, final ActionListener<Collection<User>>
161166
}
162167

163168
void getUserCount(final ActionListener<Long> listener) {
164-
if (securityIndex.indexExists() == false) {
169+
final SecurityIndexManager frozenSecurityIndex = this.securityIndex.freeze();
170+
if (frozenSecurityIndex.indexExists() == false) {
165171
listener.onResponse(0L);
172+
} else if (frozenSecurityIndex.isAvailable() == false) {
173+
listener.onFailure(frozenSecurityIndex.getUnavailableReason());
166174
} else {
167-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
175+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () ->
168176
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
169177
client.prepareSearch(SECURITY_INDEX_NAME)
170178
.setQuery(QueryBuilders.termQuery(Fields.TYPE.getPreferredName(), USER_DOC_TYPE))
@@ -188,11 +196,16 @@ public void onFailure(Exception e) {
188196
* Async method to retrieve a user and their password
189197
*/
190198
private void getUserAndPassword(final String user, final ActionListener<UserAndPassword> listener) {
191-
if (securityIndex.indexExists() == false) {
192-
// TODO remove this short circuiting and fix tests that fail without this!
199+
final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze();
200+
if (frozenSecurityIndex.isAvailable() == false) {
201+
if (frozenSecurityIndex.indexExists()) {
202+
logger.trace("could not retrieve user [{}] because security index does not exist", user);
203+
} else {
204+
logger.error("security index is unavailable. short circuiting retrieval of user [{}]", user);
205+
}
193206
listener.onResponse(null);
194207
} else {
195-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
208+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () ->
196209
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
197210
client.prepareGet(SECURITY_INDEX_NAME,
198211
NativeUserStoreField.INDEX_TYPE, getIdForUser(USER_DOC_TYPE, user)).request(),
@@ -473,26 +486,31 @@ public void onFailure(Exception e) {
473486
}
474487

475488
public void deleteUser(final DeleteUserRequest deleteUserRequest, final ActionListener<Boolean> listener) {
489+
final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze();
476490
if (isTribeNode) {
477491
listener.onFailure(new UnsupportedOperationException("users may not be deleted using a tribe node"));
492+
} else if (frozenSecurityIndex.indexExists() == false) {
493+
listener.onResponse(false);
494+
} else if (frozenSecurityIndex.isAvailable() == false) {
495+
listener.onFailure(frozenSecurityIndex.getUnavailableReason());
478496
} else {
479-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> {
497+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () -> {
480498
DeleteRequest request = client.prepareDelete(SECURITY_INDEX_NAME,
481-
NativeUserStoreField.INDEX_TYPE, getIdForUser(USER_DOC_TYPE, deleteUserRequest.username())).request();
499+
NativeUserStoreField.INDEX_TYPE, getIdForUser(USER_DOC_TYPE, deleteUserRequest.username())).request();
482500
request.setRefreshPolicy(deleteUserRequest.getRefreshPolicy());
483501
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, request,
484-
new ActionListener<DeleteResponse>() {
485-
@Override
486-
public void onResponse(DeleteResponse deleteResponse) {
487-
clearRealmCache(deleteUserRequest.username(), listener,
488-
deleteResponse.getResult() == DocWriteResponse.Result.DELETED);
489-
}
502+
new ActionListener<DeleteResponse>() {
503+
@Override
504+
public void onResponse(DeleteResponse deleteResponse) {
505+
clearRealmCache(deleteUserRequest.username(), listener,
506+
deleteResponse.getResult() == DocWriteResponse.Result.DELETED);
507+
}
490508

491-
@Override
492-
public void onFailure(Exception e) {
493-
listener.onFailure(e);
494-
}
495-
}, client::delete);
509+
@Override
510+
public void onFailure(Exception e) {
511+
listener.onFailure(e);
512+
}
513+
}, client::delete);
496514
});
497515
}
498516
}
@@ -516,11 +534,13 @@ void verifyPassword(String username, final SecureString password, ActionListener
516534
}
517535

518536
void getReservedUserInfo(String username, ActionListener<ReservedUserInfo> listener) {
519-
if (securityIndex.indexExists() == false) {
520-
// TODO remove this short circuiting and fix tests that fail without this!
537+
final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze();
538+
if (frozenSecurityIndex.indexExists() == false) {
521539
listener.onResponse(null);
540+
} else if (frozenSecurityIndex.isAvailable() == false) {
541+
listener.onFailure(frozenSecurityIndex.getUnavailableReason());
522542
} else {
523-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
543+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () ->
524544
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
525545
client.prepareGet(SECURITY_INDEX_NAME, NativeUserStoreField.INDEX_TYPE,
526546
getIdForUser(NativeUserStoreField.RESERVED_USER_TYPE, username)).request(),
@@ -559,49 +579,56 @@ public void onFailure(Exception e) {
559579
}
560580

561581
void getAllReservedUserInfo(ActionListener<Map<String, ReservedUserInfo>> listener) {
562-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
563-
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
564-
client.prepareSearch(SECURITY_INDEX_NAME)
565-
.setQuery(QueryBuilders.termQuery(Fields.TYPE.getPreferredName(), NativeUserStoreField.RESERVED_USER_TYPE))
582+
final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze();
583+
if (frozenSecurityIndex.indexExists() == false) {
584+
listener.onResponse(Collections.emptyMap());
585+
} else if (frozenSecurityIndex.isAvailable() == false) {
586+
listener.onFailure(frozenSecurityIndex.getUnavailableReason());
587+
} else {
588+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () ->
589+
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
590+
client.prepareSearch(SECURITY_INDEX_NAME)
591+
.setQuery(QueryBuilders.termQuery(Fields.TYPE.getPreferredName(), RESERVED_USER_TYPE))
566592
.setFetchSource(true).request(),
567-
new ActionListener<SearchResponse>() {
568-
@Override
569-
public void onResponse(SearchResponse searchResponse) {
570-
Map<String, ReservedUserInfo> userInfos = new HashMap<>();
571-
assert searchResponse.getHits().getTotalHits() <= 10 :
593+
new ActionListener<SearchResponse>() {
594+
@Override
595+
public void onResponse(SearchResponse searchResponse) {
596+
Map<String, ReservedUserInfo> userInfos = new HashMap<>();
597+
assert searchResponse.getHits().getTotalHits() <= 10 :
572598
"there are more than 10 reserved users we need to change this to retrieve them all!";
573-
for (SearchHit searchHit : searchResponse.getHits().getHits()) {
574-
Map<String, Object> sourceMap = searchHit.getSourceAsMap();
575-
String password = (String) sourceMap.get(Fields.PASSWORD.getPreferredName());
576-
Boolean enabled = (Boolean) sourceMap.get(Fields.ENABLED.getPreferredName());
577-
final String id = searchHit.getId();
578-
assert id != null && id.startsWith(NativeUserStoreField.RESERVED_USER_TYPE) :
599+
for (SearchHit searchHit : searchResponse.getHits().getHits()) {
600+
Map<String, Object> sourceMap = searchHit.getSourceAsMap();
601+
String password = (String) sourceMap.get(Fields.PASSWORD.getPreferredName());
602+
Boolean enabled = (Boolean) sourceMap.get(Fields.ENABLED.getPreferredName());
603+
final String id = searchHit.getId();
604+
assert id != null && id.startsWith(RESERVED_USER_TYPE) :
579605
"id [" + id + "] does not start with reserved-user prefix";
580-
final String username = id.substring(NativeUserStoreField.RESERVED_USER_TYPE.length() + 1);
581-
if (password == null) {
582-
listener.onFailure(new IllegalStateException("password hash must not be null!"));
583-
return;
584-
} else if (enabled == null) {
585-
listener.onFailure(new IllegalStateException("enabled must not be null!"));
586-
return;
587-
} else {
588-
userInfos.put(username, new ReservedUserInfo(password.toCharArray(), enabled, false));
606+
final String username = id.substring(RESERVED_USER_TYPE.length() + 1);
607+
if (password == null) {
608+
listener.onFailure(new IllegalStateException("password hash must not be null!"));
609+
return;
610+
} else if (enabled == null) {
611+
listener.onFailure(new IllegalStateException("enabled must not be null!"));
612+
return;
613+
} else {
614+
userInfos.put(username, new ReservedUserInfo(password.toCharArray(), enabled, false));
615+
}
589616
}
617+
listener.onResponse(userInfos);
590618
}
591-
listener.onResponse(userInfos);
592-
}
593619

594-
@Override
595-
public void onFailure(Exception e) {
596-
if (e instanceof IndexNotFoundException) {
597-
logger.trace("could not retrieve built in users since security index does not exist", e);
598-
listener.onResponse(Collections.emptyMap());
599-
} else {
600-
logger.error("failed to retrieve built in users", e);
601-
listener.onFailure(e);
620+
@Override
621+
public void onFailure(Exception e) {
622+
if (e instanceof IndexNotFoundException) {
623+
logger.trace("could not retrieve built in users since security index does not exist", e);
624+
listener.onResponse(Collections.emptyMap());
625+
} else {
626+
logger.error("failed to retrieve built in users", e);
627+
listener.onFailure(e);
628+
}
602629
}
603-
}
604-
}, client::search));
630+
}, client::search));
631+
}
605632
}
606633

607634
private <Response> void clearRealmCache(String username, ActionListener<Response> listener, Response response) {

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStore.java

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -225,32 +225,35 @@ public void onFailure(Exception e) {
225225
});
226226
}
227227

228-
private void innerDeleteMapping(DeleteRoleMappingRequest request, ActionListener<Boolean> listener) throws IOException {
229-
if (securityIndex.isIndexUpToDate() == false) {
230-
listener.onFailure(new IllegalStateException(
231-
"Security index is not on the current version - the native realm will not be operational until " +
232-
"the upgrade API is run on the security index"));
233-
return;
234-
}
235-
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
236-
client.prepareDelete(SECURITY_INDEX_NAME, SECURITY_GENERIC_TYPE, getIdForName(request.getName()))
228+
private void innerDeleteMapping(DeleteRoleMappingRequest request, ActionListener<Boolean> listener) {
229+
final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze();
230+
if (frozenSecurityIndex.indexExists() == false) {
231+
listener.onResponse(false);
232+
} else if (securityIndex.isAvailable() == false) {
233+
listener.onFailure(frozenSecurityIndex.getUnavailableReason());
234+
} else {
235+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () -> {
236+
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
237+
client.prepareDelete(SECURITY_INDEX_NAME, SECURITY_GENERIC_TYPE, getIdForName(request.getName()))
237238
.setRefreshPolicy(request.getRefreshPolicy())
238239
.request(),
239-
new ActionListener<DeleteResponse>() {
240+
new ActionListener<DeleteResponse>() {
240241

241-
@Override
242-
public void onResponse(DeleteResponse deleteResponse) {
243-
boolean deleted = deleteResponse.getResult() == DELETED;
244-
listener.onResponse(deleted);
245-
}
242+
@Override
243+
public void onResponse(DeleteResponse deleteResponse) {
244+
boolean deleted = deleteResponse.getResult() == DELETED;
245+
listener.onResponse(deleted);
246+
}
246247

247-
@Override
248-
public void onFailure(Exception e) {
249-
logger.error(new ParameterizedMessage("failed to delete role-mapping [{}]", request.getName()), e);
250-
listener.onFailure(e);
248+
@Override
249+
public void onFailure(Exception e) {
250+
logger.error(new ParameterizedMessage("failed to delete role-mapping [{}]", request.getName()), e);
251+
listener.onFailure(e);
251252

252-
}
253-
}, client::delete);
253+
}
254+
}, client::delete);
255+
});
256+
}
254257
}
255258

256259
/**
@@ -306,7 +309,7 @@ private void getMappings(ActionListener<List<ExpressionRoleMapping>> listener) {
306309
* </ul>
307310
*/
308311
public void usageStats(ActionListener<Map<String, Object>> listener) {
309-
if (securityIndex.indexExists() == false) {
312+
if (securityIndex.isAvailable() == false) {
310313
reportStats(listener, Collections.emptyList());
311314
} else {
312315
getMappings(ActionListener.wrap(mappings -> reportStats(listener, mappings), listener::onFailure));

0 commit comments

Comments
 (0)