Skip to content

Commit c344293

Browse files
authored
Security: don't call prepare index for reads (elastic#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 elastic#34246, which was reverted. Relates elastic#33205
1 parent 5dd79bf commit c344293

16 files changed

+446
-234
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
@@ -419,7 +419,7 @@ Collection<Object> createComponents(Client client, ThreadPool threadPool, Cluste
419419
components.add(auditTrailService);
420420
this.auditTrailService.set(auditTrailService);
421421

422-
securityIndex.set(new SecurityIndexManager(settings, client, SecurityIndexManager.SECURITY_INDEX_NAME, clusterService));
422+
securityIndex.set(new SecurityIndexManager(client, SecurityIndexManager.SECURITY_INDEX_NAME, clusterService));
423423

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

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

Lines changed: 98 additions & 80 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: 89 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -109,31 +109,36 @@ public void getUser(String username, ActionListener<User> listener) {
109109
*/
110110
public void getUsers(String[] userNames, final ActionListener<Collection<User>> listener) {
111111
final Consumer<Exception> handleException = (t) -> {
112-
if (t instanceof IndexNotFoundException) {
113-
logger.trace("could not retrieve users because security index does not exist");
114-
// We don't invoke the onFailure listener here, instead just pass an empty list
115-
listener.onResponse(Collections.emptyList());
116-
} else {
117-
listener.onFailure(t);
112+
if (TransportActions.isShardNotAvailableException(t)) {
113+
logger.trace("could not retrieve users because of a shard not available exception", t);
114+
if (t instanceof IndexNotFoundException) {
115+
// We don't invoke the onFailure listener here, instead just pass an empty list
116+
// as the index doesn't exist. Could have been deleted between checks and execution
117+
listener.onResponse(Collections.emptyList());
118+
} else {
119+
listener.onFailure(t);
120+
}
118121
}
122+
listener.onFailure(t);
119123
};
120124

121-
if (securityIndex.indexExists() == false) {
122-
// TODO remove this short circuiting and fix tests that fail without this!
125+
final SecurityIndexManager frozenSecurityIndex = this.securityIndex.freeze();
126+
if (frozenSecurityIndex.indexExists() == false) {
123127
listener.onResponse(Collections.emptyList());
128+
} else if (frozenSecurityIndex.isAvailable() == false) {
129+
listener.onFailure(frozenSecurityIndex.getUnavailableReason());
124130
} else if (userNames.length == 1) { // optimization for single user lookup
125131
final String username = userNames[0];
126132
getUserAndPassword(username, ActionListener.wrap(
127133
(uap) -> listener.onResponse(uap == null ? Collections.emptyList() : Collections.singletonList(uap.user())),
128134
handleException));
129135
} else {
130-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> {
136+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () -> {
131137
final QueryBuilder query;
132138
if (userNames == null || userNames.length == 0) {
133139
query = QueryBuilders.termQuery(Fields.TYPE.getPreferredName(), USER_DOC_TYPE);
134140
} else {
135-
final String[] users = Arrays.asList(userNames).stream()
136-
.map(s -> getIdForUser(USER_DOC_TYPE, s)).toArray(String[]::new);
141+
final String[] users = Arrays.stream(userNames).map(s -> getIdForUser(USER_DOC_TYPE, s)).toArray(String[]::new);
137142
query = QueryBuilders.boolQuery().filter(QueryBuilders.idsQuery(INDEX_TYPE).addIds(users));
138143
}
139144
final Supplier<ThreadContext.StoredContext> supplier = client.threadPool().getThreadContext().newRestorableContext(false);
@@ -155,10 +160,13 @@ public void getUsers(String[] userNames, final ActionListener<Collection<User>>
155160
}
156161

157162
void getUserCount(final ActionListener<Long> listener) {
158-
if (securityIndex.indexExists() == false) {
163+
final SecurityIndexManager frozenSecurityIndex = this.securityIndex.freeze();
164+
if (frozenSecurityIndex.indexExists() == false) {
159165
listener.onResponse(0L);
166+
} else if (frozenSecurityIndex.isAvailable() == false) {
167+
listener.onFailure(frozenSecurityIndex.getUnavailableReason());
160168
} else {
161-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
169+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () ->
162170
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
163171
client.prepareSearch(SECURITY_INDEX_NAME)
164172
.setQuery(QueryBuilders.termQuery(Fields.TYPE.getPreferredName(), USER_DOC_TYPE))
@@ -182,11 +190,16 @@ public void onFailure(Exception e) {
182190
* Async method to retrieve a user and their password
183191
*/
184192
private void getUserAndPassword(final String user, final ActionListener<UserAndPassword> listener) {
185-
if (securityIndex.indexExists() == false) {
186-
// TODO remove this short circuiting and fix tests that fail without this!
193+
final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze();
194+
if (frozenSecurityIndex.isAvailable() == false) {
195+
if (frozenSecurityIndex.indexExists()) {
196+
logger.trace("could not retrieve user [{}] because security index does not exist", user);
197+
} else {
198+
logger.error("security index is unavailable. short circuiting retrieval of user [{}]", user);
199+
}
187200
listener.onResponse(null);
188201
} else {
189-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
202+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () ->
190203
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
191204
client.prepareGet(SECURITY_INDEX_NAME,
192205
INDEX_TYPE, getIdForUser(USER_DOC_TYPE, user)).request(),
@@ -459,24 +472,31 @@ public void onFailure(Exception e) {
459472
}
460473

461474
public void deleteUser(final DeleteUserRequest deleteUserRequest, final ActionListener<Boolean> listener) {
462-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> {
463-
DeleteRequest request = client.prepareDelete(SECURITY_INDEX_NAME,
475+
final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze();
476+
if (frozenSecurityIndex.indexExists() == false) {
477+
listener.onResponse(false);
478+
} else if (frozenSecurityIndex.isAvailable() == false) {
479+
listener.onFailure(frozenSecurityIndex.getUnavailableReason());
480+
} else {
481+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () -> {
482+
DeleteRequest request = client.prepareDelete(SECURITY_INDEX_NAME,
464483
INDEX_TYPE, getIdForUser(USER_DOC_TYPE, deleteUserRequest.username())).request();
465-
request.setRefreshPolicy(deleteUserRequest.getRefreshPolicy());
466-
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, request,
484+
request.setRefreshPolicy(deleteUserRequest.getRefreshPolicy());
485+
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, request,
467486
new ActionListener<DeleteResponse>() {
468487
@Override
469488
public void onResponse(DeleteResponse deleteResponse) {
470489
clearRealmCache(deleteUserRequest.username(), listener,
471-
deleteResponse.getResult() == DocWriteResponse.Result.DELETED);
490+
deleteResponse.getResult() == DocWriteResponse.Result.DELETED);
472491
}
473492

474493
@Override
475494
public void onFailure(Exception e) {
476495
listener.onFailure(e);
477496
}
478497
}, client::delete);
479-
});
498+
});
499+
}
480500
}
481501

482502
/**
@@ -498,11 +518,13 @@ void verifyPassword(String username, final SecureString password, ActionListener
498518
}
499519

500520
void getReservedUserInfo(String username, ActionListener<ReservedUserInfo> listener) {
501-
if (securityIndex.indexExists() == false) {
502-
// TODO remove this short circuiting and fix tests that fail without this!
521+
final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze();
522+
if (frozenSecurityIndex.indexExists() == false) {
503523
listener.onResponse(null);
524+
} else if (frozenSecurityIndex.isAvailable() == false) {
525+
listener.onFailure(frozenSecurityIndex.getUnavailableReason());
504526
} else {
505-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
527+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () ->
506528
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
507529
client.prepareGet(SECURITY_INDEX_NAME, INDEX_TYPE,
508530
getIdForUser(RESERVED_USER_TYPE, username)).request(),
@@ -541,49 +563,56 @@ public void onFailure(Exception e) {
541563
}
542564

543565
void getAllReservedUserInfo(ActionListener<Map<String, ReservedUserInfo>> listener) {
544-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
545-
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
546-
client.prepareSearch(SECURITY_INDEX_NAME)
566+
final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze();
567+
if (frozenSecurityIndex.indexExists() == false) {
568+
listener.onResponse(Collections.emptyMap());
569+
} else if (frozenSecurityIndex.isAvailable() == false) {
570+
listener.onFailure(frozenSecurityIndex.getUnavailableReason());
571+
} else {
572+
securityIndex.checkIndexVersionThenExecute(listener::onFailure, () ->
573+
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
574+
client.prepareSearch(SECURITY_INDEX_NAME)
547575
.setQuery(QueryBuilders.termQuery(Fields.TYPE.getPreferredName(), RESERVED_USER_TYPE))
548576
.setFetchSource(true).request(),
549-
new ActionListener<SearchResponse>() {
550-
@Override
551-
public void onResponse(SearchResponse searchResponse) {
552-
Map<String, ReservedUserInfo> userInfos = new HashMap<>();
553-
assert searchResponse.getHits().getTotalHits() <= 10 :
577+
new ActionListener<SearchResponse>() {
578+
@Override
579+
public void onResponse(SearchResponse searchResponse) {
580+
Map<String, ReservedUserInfo> userInfos = new HashMap<>();
581+
assert searchResponse.getHits().getTotalHits() <= 10 :
554582
"there are more than 10 reserved users we need to change this to retrieve them all!";
555-
for (SearchHit searchHit : searchResponse.getHits().getHits()) {
556-
Map<String, Object> sourceMap = searchHit.getSourceAsMap();
557-
String password = (String) sourceMap.get(Fields.PASSWORD.getPreferredName());
558-
Boolean enabled = (Boolean) sourceMap.get(Fields.ENABLED.getPreferredName());
559-
final String id = searchHit.getId();
560-
assert id != null && id.startsWith(RESERVED_USER_TYPE) :
583+
for (SearchHit searchHit : searchResponse.getHits().getHits()) {
584+
Map<String, Object> sourceMap = searchHit.getSourceAsMap();
585+
String password = (String) sourceMap.get(Fields.PASSWORD.getPreferredName());
586+
Boolean enabled = (Boolean) sourceMap.get(Fields.ENABLED.getPreferredName());
587+
final String id = searchHit.getId();
588+
assert id != null && id.startsWith(RESERVED_USER_TYPE) :
561589
"id [" + id + "] does not start with reserved-user prefix";
562-
final String username = id.substring(RESERVED_USER_TYPE.length() + 1);
563-
if (password == null) {
564-
listener.onFailure(new IllegalStateException("password hash must not be null!"));
565-
return;
566-
} else if (enabled == null) {
567-
listener.onFailure(new IllegalStateException("enabled must not be null!"));
568-
return;
569-
} else {
570-
userInfos.put(username, new ReservedUserInfo(password.toCharArray(), enabled, false));
590+
final String username = id.substring(RESERVED_USER_TYPE.length() + 1);
591+
if (password == null) {
592+
listener.onFailure(new IllegalStateException("password hash must not be null!"));
593+
return;
594+
} else if (enabled == null) {
595+
listener.onFailure(new IllegalStateException("enabled must not be null!"));
596+
return;
597+
} else {
598+
userInfos.put(username, new ReservedUserInfo(password.toCharArray(), enabled, false));
599+
}
571600
}
601+
listener.onResponse(userInfos);
572602
}
573-
listener.onResponse(userInfos);
574-
}
575603

576-
@Override
577-
public void onFailure(Exception e) {
578-
if (e instanceof IndexNotFoundException) {
579-
logger.trace("could not retrieve built in users since security index does not exist", e);
580-
listener.onResponse(Collections.emptyMap());
581-
} else {
582-
logger.error("failed to retrieve built in users", e);
583-
listener.onFailure(e);
604+
@Override
605+
public void onFailure(Exception e) {
606+
if (e instanceof IndexNotFoundException) {
607+
logger.trace("could not retrieve built in users since security index does not exist", e);
608+
listener.onResponse(Collections.emptyMap());
609+
} else {
610+
logger.error("failed to retrieve built in users", e);
611+
listener.onFailure(e);
612+
}
584613
}
585-
}
586-
}, client::search));
614+
}, client::search));
615+
}
587616
}
588617

589618
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
@@ -220,32 +220,35 @@ public void onFailure(Exception e) {
220220
});
221221
}
222222

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

236-
@Override
237-
public void onResponse(DeleteResponse deleteResponse) {
238-
boolean deleted = deleteResponse.getResult() == DELETED;
239-
listener.onResponse(deleted);
240-
}
237+
@Override
238+
public void onResponse(DeleteResponse deleteResponse) {
239+
boolean deleted = deleteResponse.getResult() == DELETED;
240+
listener.onResponse(deleted);
241+
}
241242

242-
@Override
243-
public void onFailure(Exception e) {
244-
logger.error(new ParameterizedMessage("failed to delete role-mapping [{}]", request.getName()), e);
245-
listener.onFailure(e);
243+
@Override
244+
public void onFailure(Exception e) {
245+
logger.error(new ParameterizedMessage("failed to delete role-mapping [{}]", request.getName()), e);
246+
listener.onFailure(e);
246247

247-
}
248-
}, client::delete);
248+
}
249+
}, client::delete);
250+
});
251+
}
249252
}
250253

251254
/**
@@ -301,7 +304,7 @@ private void getMappings(ActionListener<List<ExpressionRoleMapping>> listener) {
301304
* </ul>
302305
*/
303306
public void usageStats(ActionListener<Map<String, Object>> listener) {
304-
if (securityIndex.indexExists() == false) {
307+
if (securityIndex.isAvailable() == false) {
305308
reportStats(listener, Collections.emptyList());
306309
} else {
307310
getMappings(ActionListener.wrap(mappings -> reportStats(listener, mappings), listener::onFailure));

0 commit comments

Comments
 (0)