All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.xpack.security.authc.InactiveApiKeysRemover Maven / Gradle / Ivy

There is a newer version: 8.16.1
Show newest version
/*
 * 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.cluster.service.ClusterService;
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.SecuritySystemIndices;

import java.time.Instant;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

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;

/**
 * Responsible for cleaning the invalidated and expired API keys from the security index.
 */
public final class InactiveApiKeysRemover extends AbstractRunnable {
    private static final Logger logger = LogManager.getLogger(InactiveApiKeysRemover.class);

    private final Client client;
    private final AtomicBoolean inProgress = new AtomicBoolean(false);
    private final TimeValue timeout;
    private final AtomicLong retentionPeriodInMs;
    private final AtomicLong deleteIntervalInMs;
    private volatile long lastRunMs = -1;

    InactiveApiKeysRemover(Settings settings, Client client, ClusterService clusterService) {
        this.client = client;
        this.timeout = ApiKeyService.DELETE_TIMEOUT.get(settings);
        this.retentionPeriodInMs = new AtomicLong(ApiKeyService.DELETE_RETENTION_PERIOD.get(settings).getMillis());
        clusterService.getClusterSettings()
            .addSettingsUpdateConsumer(
                ApiKeyService.DELETE_RETENTION_PERIOD,
                newRetentionPeriod -> this.retentionPeriodInMs.set(newRetentionPeriod.getMillis())
            );
        this.deleteIntervalInMs = new AtomicLong(ApiKeyService.DELETE_INTERVAL.get(settings).getMillis());
        clusterService.getClusterSettings()
            .addSettingsUpdateConsumer(
                ApiKeyService.DELETE_INTERVAL,
                newDeleteInterval -> this.deleteIntervalInMs.set(newDeleteInterval.getMillis())
            );
    }

    @Override
    public void doRun() {
        DeleteByQueryRequest expiredDbq = new DeleteByQueryRequest(SecuritySystemIndices.SECURITY_MAIN_ALIAS);
        if (timeout != TimeValue.MINUS_ONE) {
            expiredDbq.setTimeout(timeout);
            expiredDbq.getSearchRequest().source().timeout(timeout);
        }
        final Instant now = Instant.now();
        final long cutoffTimestamp = now.minusMillis(retentionPeriodInMs.get()).toEpochMilli();
        expiredDbq.setQuery(
            QueryBuilders.boolQuery()
                .filter(QueryBuilders.termsQuery("doc_type", "api_key"))
                .should(QueryBuilders.rangeQuery("expiration_time").lte(cutoffTimestamp))
                .should(
                    QueryBuilders.boolQuery()
                        .filter(QueryBuilders.termsQuery("api_key_invalidated", true))
                        .should(QueryBuilders.rangeQuery("invalidation_time").lte(cutoffTimestamp))
                        .should(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery("invalidation_time")))
                        .minimumShouldMatch(1)
                )
                .minimumShouldMatch(1)
        );

        executeAsyncWithOrigin(client, SECURITY_ORIGIN, DeleteByQueryAction.INSTANCE, expiredDbq, ActionListener.wrap(r -> {
            debugDbqResponse(r);
            markComplete();
        }, this::onFailure));
    }

    void maybeSubmit(ThreadPool threadPool) {
        if (lastRunMs == -1 || threadPool.relativeTimeInMillis() - lastRunMs > deleteIntervalInMs.get()) {
            if (inProgress.compareAndSet(false, true)) {
                threadPool.executor(Names.GENERIC).submit(this);
            }
            lastRunMs = client.threadPool().relativeTimeInMillis();
        }
    }

    long getLastRunTimestamp() {
        return lastRunMs;
    }

    private static void debugDbqResponse(BulkByScrollResponse response) {
        if (logger.isDebugEnabled()) {
            logger.debug(
                "delete by query of api keys 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 or invalidated api keys", e);
        } else {
            logger.error("failed to delete expired or invalidated api keys", 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!");
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy