davsclaus commented on code in PR #24455: URL: https://github.com/apache/camel/pull/24455#discussion_r3527993401
########## components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyRotationScheduler.java: ########## @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.pqc.lifecycle; + +import java.security.KeyPair; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.apache.camel.support.service.ServiceSupport; +import org.apache.camel.util.ObjectHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An opt-in background scheduler that periodically evaluates the keys managed by a {@link KeyLifecycleManager} and + * rotates the ones that need rotation according to a configurable policy (maximum age and/or maximum usage). A rotated + * key is replaced by a freshly generated key while the previous key is deprecated - this is the behaviour of + * {@link KeyLifecycleManager#rotateKey(String, String, String)}. + * <p/> + * The scheduler is inactive until {@link #start()} is called. It implements the Camel {@code Service} contract, so it + * can be added to a {@code CamelContext} to participate in the context lifecycle: + * + * <pre>{@code + * KeyRotationScheduler scheduler = new KeyRotationScheduler(keyManager) + * .setCheckInterval(Duration.ofHours(6)) + * .setMaxKeyAge(Duration.ofDays(90)) + * .setMaxKeyUsage(1_000_000); + * camelContext.addService(scheduler); // started and stopped with the context + * }</pre> + * + * On every tick the scheduler lists the managed keys, keeps only those matching the configured {@link #setKeyFilter key + * filter} (by default only {@link KeyMetadata.KeyStatus#ACTIVE ACTIVE} keys) and, for each of them, delegates the + * decision to {@link KeyLifecycleManager#needsRotation(String, Duration, long)}. When a key needs rotation, the id of + * its replacement is produced by the configured {@link #setKeyIdStrategy key id strategy} (by default the previous key + * id with a rotation marker and timestamp appended). + * <p/> + * At least one rotation signal should be configured (a {@link #setMaxKeyAge maximum age}, a {@link #setMaxKeyUsage + * maximum usage}, or a per-key {@code nextRotationAt}/{@code PENDING_ROTATION} status recorded in the key metadata); + * otherwise no key will ever be selected for rotation. + * <p/> + * A single check can also be triggered on demand with {@link #checkAndRotate()}, which is what the scheduled task + * calls. The method is thread-safe: concurrent invocations (for example a manual call overlapping a scheduled tick) are + * serialized. + */ +public class KeyRotationScheduler extends ServiceSupport { + + private static final Logger LOG = LoggerFactory.getLogger(KeyRotationScheduler.class); + + private final KeyLifecycleManager keyManager; + + private Duration checkInterval = Duration.ofHours(1); + private Duration maxKeyAge; + private long maxKeyUsage; + private Predicate<KeyMetadata> keyFilter = metadata -> metadata.getStatus() == KeyMetadata.KeyStatus.ACTIVE; + private Function<KeyMetadata, String> keyIdStrategy = KeyRotationScheduler::defaultRotatedKeyId; + private KeyRotationListener listener; + + private final AtomicLong checksPerformed = new AtomicLong(); + private final AtomicLong rotationsPerformed = new AtomicLong(); + private final AtomicLong rotationFailures = new AtomicLong(); + private volatile Instant lastCheckAt; + + private ScheduledExecutorService executorService; + + public KeyRotationScheduler(KeyLifecycleManager keyManager) { + this.keyManager = ObjectHelper.notNull(keyManager, "keyManager"); + } + + /** + * Performs a single rotation pass over the managed keys and returns how many keys were rotated. This is invoked by + * the scheduled task and can also be called manually. Never throws for an individual key failure: such failures are + * counted, logged and reported to the {@link KeyRotationListener listener}, then the pass continues with the next + * key. + * + * @return the number of keys rotated during this pass + */ + public synchronized int checkAndRotate() { + checksPerformed.incrementAndGet(); + lastCheckAt = Instant.now(); + + List<KeyMetadata> keys; + try { + keys = keyManager.listKeys(); + } catch (Exception e) { + LOG.warn("Failed to list keys during rotation check", e); + notifyError(null, e); + return 0; + } + + int rotated = 0; + for (KeyMetadata metadata : keys) { + if (metadata == null || !keyFilter.test(metadata)) { + continue; + } + String keyId = metadata.getKeyId(); + try { + if (keyManager.needsRotation(keyId, maxKeyAge, maxKeyUsage)) { + String newKeyId = keyIdStrategy.apply(metadata); + LOG.info("Rotating PQC key '{}' -> '{}' (algorithm={})", keyId, newKeyId, metadata.getAlgorithm()); + KeyPair newKeyPair = keyManager.rotateKey(keyId, newKeyId, metadata.getAlgorithm()); + rotationsPerformed.incrementAndGet(); + rotated++; + notifyRotated(keyId, newKeyId, metadata, newKeyPair); + } + } catch (Exception e) { + rotationFailures.incrementAndGet(); + LOG.warn("Failed to rotate PQC key '{}'", keyId, e); + notifyError(keyId, e); + } + } + return rotated; + } + + @Override + protected void doStart() throws Exception { + ObjectHelper.notNull(checkInterval, "checkInterval"); + if (checkInterval.isNegative() || checkInterval.isZero()) { Review Comment: Please use Camel's executor service manager instead of creating the executor directly: ```suggestion executorService = getCamelContext().getExecutorServiceManager() .newSingleThreadScheduledExecutor(this, "PQCKeyRotationScheduler"); ``` This requires the scheduler to have access to a `CamelContext` — either via constructor injection or by implementing `CamelContextAware`. Since it extends `ServiceSupport` and is added via `context.addService()`, implementing `CamelContextAware` is the idiomatic approach. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
