org.elasticsearch.xpack.security.authc.ExpiredTokenRemover Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of x-pack-security Show documentation
Show all versions of x-pack-security Show documentation
Elasticsearch Expanded Pack Plugin - Security
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
package org.elasticsearch.xpack.security.authc;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.bulk.BulkItemResponse;
import org.elasticsearch.client.internal.Client;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.AbstractRunnable;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.reindex.BulkByScrollResponse;
import org.elasticsearch.index.reindex.DeleteByQueryAction;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.elasticsearch.index.reindex.ScrollableHitSource;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.threadpool.ThreadPool.Names;
import org.elasticsearch.xpack.security.support.SecurityIndexManager;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.elasticsearch.action.support.TransportActions.isShardNotAvailableException;
import static org.elasticsearch.core.Strings.format;
import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN;
import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin;
import static org.elasticsearch.xpack.security.support.SecurityIndexManager.Availability.PRIMARY_SHARDS;
/**
* Responsible for cleaning the invalidated and expired tokens from the security indices (`main` and `tokens`).
* The document is deleted if it was created more than {@code #MAXIMUM_TOKEN_LIFETIME_HOURS} hours in the past.
*/
final class ExpiredTokenRemover extends AbstractRunnable {
private static final Logger logger = LogManager.getLogger(ExpiredTokenRemover.class);
public static final long MAXIMUM_TOKEN_LIFETIME_HOURS = 24L;
private final Client client;
private final SecurityIndexManager securityMainIndex;
private final SecurityIndexManager securityTokensIndex;
private final AtomicBoolean inProgress;
private final TimeValue timeout;
private boolean checkMainIndexForExpiredTokens;
ExpiredTokenRemover(
Settings settings,
Client client,
SecurityIndexManager securityMainIndex,
SecurityIndexManager securityTokensIndex
) {
this.client = client;
this.securityMainIndex = securityMainIndex;
this.securityTokensIndex = securityTokensIndex;
this.inProgress = new AtomicBoolean(false);
this.timeout = TokenService.DELETE_TIMEOUT.get(settings);
this.checkMainIndexForExpiredTokens = true;
}
@Override
public void doRun() {
final List indicesWithTokens = new ArrayList<>();
if (securityTokensIndex.isAvailable(PRIMARY_SHARDS)) {
indicesWithTokens.add(securityTokensIndex.aliasName());
}
if (securityMainIndex.isAvailable(PRIMARY_SHARDS) && checkMainIndexForExpiredTokens) {
indicesWithTokens.add(securityMainIndex.aliasName());
}
if (indicesWithTokens.isEmpty()) {
markComplete();
return;
}
DeleteByQueryRequest expiredDbq = new DeleteByQueryRequest(indicesWithTokens.toArray(new String[0]));
if (timeout != TimeValue.MINUS_ONE) {
expiredDbq.setTimeout(timeout);
expiredDbq.getSearchRequest().source().timeout(timeout);
}
final Instant now = Instant.now();
expiredDbq.setQuery(
QueryBuilders.boolQuery()
.filter(QueryBuilders.termsQuery("doc_type", TokenService.TOKEN_DOC_TYPE))
.filter(
QueryBuilders.rangeQuery("creation_time").lte(now.minus(MAXIMUM_TOKEN_LIFETIME_HOURS, ChronoUnit.HOURS).toEpochMilli())
)
);
logger.trace(() -> "Removing old tokens: [" + Strings.toString(expiredDbq) + "]");
executeAsyncWithOrigin(client, SECURITY_ORIGIN, DeleteByQueryAction.INSTANCE, expiredDbq, ActionListener.wrap(bulkResponse -> {
debugDbqResponse(bulkResponse);
// tokens can still linger on the main index for their maximum lifetime after the tokens index has been created, because
// only after the tokens index has been created all nodes will store tokens there and not on the main security index
if (checkMainIndexForExpiredTokens
&& securityTokensIndex.indexExists()
&& securityTokensIndex.getCreationTime().isBefore(now.minus(MAXIMUM_TOKEN_LIFETIME_HOURS, ChronoUnit.HOURS))
&& bulkResponse.getBulkFailures().isEmpty()
&& bulkResponse.getSearchFailures().isEmpty()) {
checkMainIndexForExpiredTokens = false;
}
markComplete();
}, this::onFailure));
}
void submit(ThreadPool threadPool) {
if (inProgress.compareAndSet(false, true)) {
threadPool.executor(Names.GENERIC).submit(this);
}
}
private static void debugDbqResponse(BulkByScrollResponse response) {
if (logger.isDebugEnabled()) {
logger.debug(
"delete by query of tokens finished with [{}] deletions, [{}] bulk failures, [{}] search failures",
response.getDeleted(),
response.getBulkFailures().size(),
response.getSearchFailures().size()
);
for (BulkItemResponse.Failure failure : response.getBulkFailures()) {
logger.debug(
() -> format("deletion failed for index [%s], id [%s]", failure.getIndex(), failure.getId()),
failure.getCause()
);
}
for (ScrollableHitSource.SearchFailure failure : response.getSearchFailures()) {
logger.debug(
() -> format(
"search failed for index [%s], shard [%s] on node [%s]",
failure.getIndex(),
failure.getShardId(),
failure.getNodeId()
),
failure.getReason()
);
}
}
}
boolean isExpirationInProgress() {
return inProgress.get();
}
@Override
public void onFailure(Exception e) {
if (isShardNotAvailableException(e)) {
logger.debug("failed to delete expired tokens", e);
} else {
logger.error("failed to delete expired tokens", e);
}
markComplete();
}
private void markComplete() {
if (inProgress.compareAndSet(true, false) == false) {
throw new IllegalStateException("in progress was set to false but should have been true!");
}
}
}