Add SecretLeaseContainer to renew leases and rotate secrets.
We now provide an event-driven container to request secrets with renewal and rotation on terminal lease expiration. See gh-50.
This commit is contained in:
@@ -0,0 +1,727 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import lombok.extern.apachecommons.CommonsLog;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.Trigger;
|
||||
import org.springframework.scheduling.TriggerContext;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.vault.VaultException;
|
||||
import org.springframework.vault.client.VaultResponses;
|
||||
import org.springframework.vault.core.RestOperationsCallback;
|
||||
import org.springframework.vault.core.VaultOperations;
|
||||
import org.springframework.vault.core.lease.domain.Lease;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret.Mode;
|
||||
import org.springframework.vault.core.lease.event.LeaseErrorListener;
|
||||
import org.springframework.vault.core.lease.event.LeaseListener;
|
||||
import org.springframework.vault.support.VaultResponseSupport;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
|
||||
/**
|
||||
* Event-based container to request secrets from Vault and renew the associated
|
||||
* {@link Lease}. Secrets can be rotated, depending on the requested
|
||||
* {@link RequestedSecret.Mode}.
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
SecretLeaseContainer container = new SecretLeaseContainer(vaultOperations,
|
||||
taskScheduler);
|
||||
|
||||
final RequestedSecret requestedSecret = container
|
||||
.requestRotatingSecret("mysql/creds/my-role");
|
||||
container.addLeaseListener(new LeaseListenerAdapter() {
|
||||
@Override
|
||||
public void onLeaseEvent(LeaseEvent leaseEvent) {
|
||||
|
||||
if (requestedSecret == leaseEvent.getSource()) {
|
||||
|
||||
if (leaseEvent instanceof LeaseCreatedEvent) {
|
||||
|
||||
}
|
||||
|
||||
if (leaseEvent instanceof LeaseExpiredEvent) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
container.afterPropertiesSet();
|
||||
container.start(); // events are triggered after starting the container
|
||||
* </code>
|
||||
* </pre>
|
||||
* <p>
|
||||
* This container keeps track over {@link RequestedSecret}s and requests secrets upon
|
||||
* {@link #start()}. Leases qualified for {@link Lease#isRenewable() renewal} are renewed
|
||||
* by this container applying {@code minRenewalSeconds}/{@code expiryThresholdSeconds} on
|
||||
* a {@link TaskScheduler background thread}.
|
||||
* <p>
|
||||
* Requests for secrets can define either renewal or rotation. Renewable leases are
|
||||
* renewed until expiry. Rotating secrets renew their associated lease until expiry and
|
||||
* request new secrets after expiry. Vault requires active interaction from a caller side
|
||||
* to determine a secret is expired. Vault does not send any events. Expired secrets
|
||||
* events can dispatch later than the actual expiry.
|
||||
* <p>
|
||||
* The container dispatches lease events to {@link LeaseListener} and
|
||||
* {@link LeaseErrorListener}. Event notifications are dispatched either on the
|
||||
* {@link #start() stating} {@link Thread} or worker threads used for background renewal.
|
||||
*
|
||||
* Instances are thread-safe once {@link #afterPropertiesSet() initialized.}
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see RequestedSecret
|
||||
* @see SecretLeaseEventPublisher
|
||||
* @see Lease
|
||||
*/
|
||||
@CommonsLog
|
||||
public class SecretLeaseContainer extends SecretLeaseEventPublisher
|
||||
implements InitializingBean, DisposableBean {
|
||||
|
||||
private final static AtomicIntegerFieldUpdater<SecretLeaseContainer> UPDATER = AtomicIntegerFieldUpdater
|
||||
.newUpdater(SecretLeaseContainer.class, "status");
|
||||
|
||||
private static final AtomicInteger poolId = new AtomicInteger();
|
||||
|
||||
private final static int STATUS_INITIAL = 0;
|
||||
private final static int STATUS_STARTED = 1;
|
||||
private final static int STATUS_DESTROYED = 2;
|
||||
|
||||
private final List<RequestedSecret> requestedSecrets = new CopyOnWriteArrayList<RequestedSecret>();
|
||||
|
||||
private final Map<RequestedSecret, LeaseRenewalScheduler> renewals = new ConcurrentHashMap<RequestedSecret, LeaseRenewalScheduler>();
|
||||
|
||||
private final VaultOperations operations;
|
||||
|
||||
private int minRenewalSeconds = 10;
|
||||
|
||||
private int expiryThresholdSeconds = 60;
|
||||
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
private boolean manageTaskScheduler;
|
||||
|
||||
private volatile boolean initialized;
|
||||
|
||||
private volatile int status = STATUS_INITIAL;
|
||||
|
||||
/**
|
||||
* Creates a new {@link SecretLeaseContainer} given {@link VaultOperations}.
|
||||
*
|
||||
* @param operations must not be {@literal null}.
|
||||
*/
|
||||
public SecretLeaseContainer(VaultOperations operations) {
|
||||
|
||||
Assert.notNull(operations, "VaultOperations must not be null");
|
||||
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link SecretLeaseContainer} given {@link VaultOperations} and
|
||||
* {@link TaskScheduler}.
|
||||
*
|
||||
* @param operations must not be {@literal null}.
|
||||
* @param taskScheduler must not be {@literal null}.
|
||||
*/
|
||||
public SecretLeaseContainer(VaultOperations operations, TaskScheduler taskScheduler) {
|
||||
|
||||
Assert.notNull(operations, "VaultOperations must not be null");
|
||||
Assert.notNull(taskScheduler, "TaskScheduler must not be null");
|
||||
|
||||
this.operations = operations;
|
||||
this.taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the expiry threshold. {@link Lease} is renewed the given seconds before it
|
||||
* expires.
|
||||
*
|
||||
* @param expiryThresholdSeconds number of seconds before {@link Lease} expiry.
|
||||
*/
|
||||
public void setExpiryThresholdSeconds(int expiryThresholdSeconds) {
|
||||
this.expiryThresholdSeconds = expiryThresholdSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the amount of seconds that is at least required before renewing a lease.
|
||||
* {@code minRenewalSeconds} prevents renewals to happen too often.
|
||||
*
|
||||
* @param minRenewalSeconds number of seconds that is at least required before
|
||||
* renewing a {@link Lease}.
|
||||
*/
|
||||
public void setMinRenewalSeconds(int minRenewalSeconds) {
|
||||
this.minRenewalSeconds = minRenewalSeconds;
|
||||
}
|
||||
|
||||
public int getMinRenewalSeconds() {
|
||||
return minRenewalSeconds;
|
||||
}
|
||||
|
||||
public int getExpiryThresholdSeconds() {
|
||||
return expiryThresholdSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link TaskScheduler} to use for scheduling and execution of lease
|
||||
* renewals.
|
||||
*
|
||||
* @param taskScheduler must not be {@literal null}.
|
||||
*/
|
||||
public void setTaskScheduler(TaskScheduler taskScheduler) {
|
||||
|
||||
Assert.notNull(taskScheduler, "TaskScheduler must not be null");
|
||||
this.taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a renewable secret at {@code path}.
|
||||
*
|
||||
* @param path must not be {@literal null} or empty.
|
||||
* @return the {@link RequestedSecret}.
|
||||
*/
|
||||
public RequestedSecret requestRenewableSecret(String path) {
|
||||
|
||||
RequestedSecret requestedSecret = RequestedSecret.renewable(path);
|
||||
addRequestedSecret(requestedSecret);
|
||||
return requestedSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a rotating secret at {@code path}.
|
||||
*
|
||||
* @param path must not be {@literal null} or empty.
|
||||
* @return the {@link RequestedSecret}.
|
||||
*/
|
||||
public RequestedSecret requestRotatingSecret(String path) {
|
||||
|
||||
RequestedSecret requestedSecret = RequestedSecret.rotating(path);
|
||||
addRequestedSecret(requestedSecret);
|
||||
return requestedSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a {@link RequestedSecret}.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
*/
|
||||
public void addRequestedSecret(RequestedSecret requestedSecret) {
|
||||
|
||||
Assert.notNull(requestedSecret, "RequestedSecret must not be null");
|
||||
|
||||
this.requestedSecrets.add(requestedSecret);
|
||||
|
||||
if (initialized) {
|
||||
|
||||
LeaseRenewalScheduler leaseRenewalScheduler = new LeaseRenewalScheduler(
|
||||
this.taskScheduler);
|
||||
this.renewals.put(requestedSecret, leaseRenewalScheduler);
|
||||
|
||||
if (this.status == STATUS_STARTED) {
|
||||
start(requestedSecret, leaseRenewalScheduler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the {@link SecretLeaseContainer}. Starting the container will initially
|
||||
* obtain secrets and leases for the requested secrets. A started container publishes
|
||||
* events through {@link LeaseListener}. Additional secrets can be requested at any
|
||||
* time.
|
||||
* <p>
|
||||
* Multiple {@link #start()} calls are synchronized to start the container only once.
|
||||
* Container start requires {@link #afterPropertiesSet() initialization} and cannot be
|
||||
* started once the container was {@link #destroy() destroyed}.
|
||||
*
|
||||
* @see #afterPropertiesSet()
|
||||
* @see #stop()
|
||||
*/
|
||||
public void start() {
|
||||
|
||||
Assert.state(this.initialized, "Container is not initialized");
|
||||
Assert.state(this.status != STATUS_DESTROYED,
|
||||
"Container is destroyed and cannot be started");
|
||||
|
||||
Map<RequestedSecret, LeaseRenewalScheduler> renewals = new HashMap<RequestedSecret, LeaseRenewalScheduler>(
|
||||
this.renewals);
|
||||
|
||||
if (UPDATER.compareAndSet(this, STATUS_INITIAL, STATUS_STARTED)) {
|
||||
|
||||
for (Entry<RequestedSecret, LeaseRenewalScheduler> entry : renewals
|
||||
.entrySet()) {
|
||||
start(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void start(RequestedSecret requestedSecret,
|
||||
LeaseRenewalScheduler renewalScheduler) {
|
||||
|
||||
VaultResponseSupport<Map<String, Object>> secrets = doGetSecrets(requestedSecret);
|
||||
|
||||
if (secrets != null) {
|
||||
|
||||
Lease lease = !StringUtils.hasText(secrets.getLeaseId()) ? Lease.none()
|
||||
: Lease.of(secrets.getLeaseId(), secrets.getLeaseDuration(),
|
||||
secrets.isRenewable());
|
||||
|
||||
potentiallyScheduleLeaseRenewal(requestedSecret, lease, renewalScheduler);
|
||||
onSecretsObtained(requestedSecret, lease, secrets.getData());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the {@link SecretLeaseContainer}. Stopping the container will stop lease
|
||||
* renewal, secrets rotation and event publishing. Active leases are not expired.
|
||||
* <p>
|
||||
* Multiple {@link #stop()} calls are synchronized to stop the container only once.
|
||||
*
|
||||
* @see #start()
|
||||
*/
|
||||
public void stop() {
|
||||
|
||||
if (UPDATER.compareAndSet(this, STATUS_STARTED, STATUS_INITIAL)) {
|
||||
|
||||
for (LeaseRenewalScheduler leaseRenewal : this.renewals.values()) {
|
||||
leaseRenewal.disableScheduleRenewal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
|
||||
if (!this.initialized) {
|
||||
|
||||
super.afterPropertiesSet();
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
if (this.taskScheduler == null) {
|
||||
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setDaemon(true);
|
||||
scheduler.setThreadNamePrefix(String.format("%s-%d-",
|
||||
getClass().getSimpleName(), poolId.incrementAndGet()));
|
||||
scheduler.afterPropertiesSet();
|
||||
|
||||
this.taskScheduler = scheduler;
|
||||
this.manageTaskScheduler = true;
|
||||
}
|
||||
|
||||
for (RequestedSecret requestedSecret : requestedSecrets) {
|
||||
this.renewals.put(requestedSecret,
|
||||
new LeaseRenewalScheduler(this.taskScheduler));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown this {@link SecretLeaseContainer}, disable lease renewal and revoke
|
||||
* leases.
|
||||
*/
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
|
||||
int status = this.status;
|
||||
|
||||
if (status == STATUS_INITIAL || status == STATUS_STARTED) {
|
||||
|
||||
if (UPDATER.compareAndSet(this, status, STATUS_DESTROYED)) {
|
||||
|
||||
for (Entry<RequestedSecret, LeaseRenewalScheduler> entry : renewals
|
||||
.entrySet()) {
|
||||
|
||||
Lease lease = entry.getValue().getLease();
|
||||
entry.getValue().disableScheduleRenewal();
|
||||
doRevokeLease(entry.getKey(), lease);
|
||||
}
|
||||
|
||||
if (manageTaskScheduler) {
|
||||
|
||||
if (this.taskScheduler instanceof DisposableBean) {
|
||||
((DisposableBean) this.taskScheduler).destroy();
|
||||
this.taskScheduler = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void potentiallyScheduleLeaseRenewal(final RequestedSecret requestedSecret,
|
||||
final Lease lease, final LeaseRenewalScheduler leaseRenewal) {
|
||||
|
||||
if (leaseRenewal.isLeaseRenewable(lease)) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(String.format("Lease %s qualified for renewal",
|
||||
lease.getLeaseId()));
|
||||
}
|
||||
|
||||
leaseRenewal.scheduleRenewal(new RenewLease() {
|
||||
|
||||
@Override
|
||||
public Lease renewLease(Lease lease) {
|
||||
|
||||
Lease newLease = doRenewLease(requestedSecret, lease);
|
||||
|
||||
if (newLease == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
potentiallyScheduleLeaseRenewal(requestedSecret, newLease,
|
||||
leaseRenewal);
|
||||
|
||||
onAfterLeaseRenewed(requestedSecret, newLease);
|
||||
|
||||
return newLease;
|
||||
}
|
||||
}, lease, getMinRenewalSeconds(), getExpiryThresholdSeconds());
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Implementation hooks and helper methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Retrieve secrets from {@link VaultOperations}.
|
||||
*
|
||||
* @param requestedSecret the {@link RequestedSecret} providing the secret
|
||||
* {@code path}.
|
||||
* @return the response.
|
||||
*/
|
||||
protected VaultResponseSupport<Map<String, Object>> doGetSecrets(
|
||||
RequestedSecret requestedSecret) {
|
||||
|
||||
try {
|
||||
return this.operations.read(requestedSecret.getPath());
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
|
||||
onError(requestedSecret, Lease.none(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renew a {@link Lease} for a {@link RequestedSecret}.
|
||||
*
|
||||
* @param requestedSecret the requested secret.
|
||||
* @param lease the lease.
|
||||
* @return the new lease or {@literal null} if expired/secret cannot be rotated.
|
||||
*/
|
||||
protected Lease doRenewLease(RequestedSecret requestedSecret, final Lease lease) {
|
||||
|
||||
try {
|
||||
ResponseEntity<Map<String, Object>> entity = operations.doWithSession(
|
||||
new RestOperationsCallback<ResponseEntity<Map<String, Object>>>() {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public ResponseEntity<Map<String, Object>> doWithRestOperations(
|
||||
RestOperations restOperations) {
|
||||
return (ResponseEntity) restOperations.exchange(
|
||||
"/sys/renew/{leaseId}", HttpMethod.PUT, null,
|
||||
Map.class, lease.getLeaseId());
|
||||
}
|
||||
});
|
||||
|
||||
Map<String, Object> body = entity.getBody();
|
||||
String leaseId = (String) body.get("lease_id");
|
||||
Number leaseDuration = (Number) body.get("lease_duration");
|
||||
boolean renewable = (Boolean) body.get("renewable");
|
||||
|
||||
if (leaseDuration == null || leaseDuration.intValue() < minRenewalSeconds) {
|
||||
onLeaseExpired(requestedSecret, lease);
|
||||
return null;
|
||||
}
|
||||
|
||||
return Lease.of(leaseId, leaseDuration.longValue(), renewable);
|
||||
}
|
||||
catch (HttpStatusCodeException e) {
|
||||
|
||||
if (e.getStatusCode() == HttpStatus.BAD_REQUEST) {
|
||||
onLeaseExpired(requestedSecret, lease);
|
||||
}
|
||||
|
||||
onError(requestedSecret, lease,
|
||||
new VaultException(String.format("Cannot renew lease: %s",
|
||||
VaultResponses.getError(e.getResponseBodyAsString()))));
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
onError(requestedSecret, lease, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook method called when a {@link Lease} expired. The default implementation is to
|
||||
* notify {@link LeaseListener}. Implementations can override this method in
|
||||
* subclasses.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
*/
|
||||
protected void onLeaseExpired(RequestedSecret requestedSecret, Lease lease) {
|
||||
|
||||
super.onLeaseExpired(requestedSecret, lease);
|
||||
|
||||
if (requestedSecret.getMode() == Mode.ROTATE) {
|
||||
start(requestedSecret, renewals.get(requestedSecret));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke the {@link Lease}.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
*/
|
||||
protected void doRevokeLease(RequestedSecret requestedSecret, final Lease lease) {
|
||||
|
||||
try {
|
||||
|
||||
onBeforeLeaseRevocation(requestedSecret, lease);
|
||||
|
||||
operations.doWithSession(
|
||||
new RestOperationsCallback<ResponseEntity<Map<String, Object>>>() {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public ResponseEntity<Map<String, Object>> doWithRestOperations(
|
||||
RestOperations restOperations) {
|
||||
return (ResponseEntity) restOperations.exchange(
|
||||
"/sys/revoke/{leaseId}", HttpMethod.PUT, null,
|
||||
Map.class, lease.getLeaseId());
|
||||
}
|
||||
});
|
||||
|
||||
onAfterLeaseRevocation(requestedSecret, lease);
|
||||
}
|
||||
catch (HttpStatusCodeException e) {
|
||||
onError(requestedSecret, lease,
|
||||
new VaultException(String.format("Cannot revoke lease: %s",
|
||||
VaultResponses.getError(e.getResponseBodyAsString()))));
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
onError(requestedSecret, lease, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracts scheduled lease renewal. A {@link LeaseRenewalScheduler} can be accessed
|
||||
* concurrently to schedule lease renewal. Each renewal run checks if the previously
|
||||
* attached {@link Lease} is still relevant to update. If any other process scheduled
|
||||
* a newer {@link Lease} for renewal, the previously registered renewal task will skip
|
||||
* renewal.
|
||||
*/
|
||||
@CommonsLog
|
||||
static class LeaseRenewalScheduler {
|
||||
|
||||
private final TaskScheduler taskScheduler;
|
||||
|
||||
final AtomicReference<Lease> currentLeaseRef = new AtomicReference<Lease>();
|
||||
|
||||
final Map<Lease, ScheduledFuture<?>> schedules = new ConcurrentHashMap<Lease, ScheduledFuture<?>>();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param taskScheduler must not be {@literal null}.
|
||||
*/
|
||||
LeaseRenewalScheduler(TaskScheduler taskScheduler) {
|
||||
this.taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule {@link Lease} renewal. Previously registered renewal tasks are
|
||||
* canceled to prevent renewal of stale {@link Lease}s.
|
||||
* @param renewLease strategy to renew a {@link Lease}.
|
||||
* @param lease the current {@link Lease}.
|
||||
* @param minRenewalSeconds minimum number of seconds before renewing a
|
||||
* {@link Lease}. This is to prevent too many renewals in a very short timeframe.
|
||||
* @param expiryThresholdSeconds number of seconds to renew before {@link Lease}.
|
||||
* expires.
|
||||
*/
|
||||
void scheduleRenewal(final RenewLease renewLease, final Lease lease,
|
||||
final int minRenewalSeconds, final int expiryThresholdSeconds) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(String.format(
|
||||
"Scheduling renewal for lease %s, lease duration %d",
|
||||
lease.getLeaseId(), lease.getLeaseDuration()));
|
||||
}
|
||||
|
||||
Lease currentLease = this.currentLeaseRef.get();
|
||||
this.currentLeaseRef.set(lease);
|
||||
|
||||
if (currentLease != null) {
|
||||
cancelSchedule(currentLease);
|
||||
}
|
||||
|
||||
ScheduledFuture<?> scheduledFuture = taskScheduler.schedule(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
|
||||
schedules.remove(lease);
|
||||
|
||||
if (currentLeaseRef.get() != lease) {
|
||||
log.debug("Current lease has changed. Skipping renewal");
|
||||
return;
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(String.format("Renewing lease %s",
|
||||
lease.getLeaseId()));
|
||||
}
|
||||
|
||||
currentLeaseRef.compareAndSet(lease,
|
||||
renewLease.renewLease(lease));
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error(String.format("Cannot renew lease %s",
|
||||
lease.getLeaseId()), e);
|
||||
}
|
||||
}
|
||||
}, new OneShotTrigger(
|
||||
getRenewalSeconds(lease, minRenewalSeconds, expiryThresholdSeconds)));
|
||||
|
||||
schedules.put(lease, scheduledFuture);
|
||||
}
|
||||
|
||||
private void cancelSchedule(Lease lease) {
|
||||
|
||||
ScheduledFuture<?> scheduledFuture = schedules.get(lease);
|
||||
if (scheduledFuture != null) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(String.format(
|
||||
"Canceling previously registered schedule for lease %s",
|
||||
lease.getLeaseId()));
|
||||
}
|
||||
|
||||
scheduledFuture.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables schedule for already scheduled renewals.
|
||||
*/
|
||||
public void disableScheduleRenewal() {
|
||||
|
||||
currentLeaseRef.set(null);
|
||||
Set<Lease> leases = new HashSet<Lease>(schedules.keySet());
|
||||
|
||||
for (Lease lease : leases) {
|
||||
cancelSchedule(lease);
|
||||
schedules.remove(lease);
|
||||
}
|
||||
}
|
||||
|
||||
private long getRenewalSeconds(Lease lease, int minRenewalSeconds,
|
||||
int expiryThresholdSeconds) {
|
||||
return Math.max(minRenewalSeconds,
|
||||
lease.getLeaseDuration() - expiryThresholdSeconds);
|
||||
}
|
||||
|
||||
private boolean isLeaseRenewable(Lease lease) {
|
||||
return lease != null && lease.isRenewable();
|
||||
}
|
||||
|
||||
public Lease getLease() {
|
||||
return currentLeaseRef.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This one-shot trigger creates only one execution time to trigger an execution only
|
||||
* once.
|
||||
*/
|
||||
static class OneShotTrigger implements Trigger {
|
||||
|
||||
private final static AtomicIntegerFieldUpdater<OneShotTrigger> UPDATER = AtomicIntegerFieldUpdater
|
||||
.newUpdater(OneShotTrigger.class, "status");
|
||||
|
||||
private final static int STATUS_ARMED = 0;
|
||||
private final static int STATUS_FIRED = 1;
|
||||
|
||||
// see AtomicIntegerFieldUpdater UPDATER
|
||||
private volatile int status = 0;
|
||||
|
||||
private final long seconds;
|
||||
|
||||
OneShotTrigger(long seconds) {
|
||||
this.seconds = seconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date nextExecutionTime(TriggerContext triggerContext) {
|
||||
|
||||
if (UPDATER.compareAndSet(this, STATUS_ARMED, STATUS_FIRED)) {
|
||||
return new Date(
|
||||
System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(seconds));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy interface to renew a {@link Lease}.
|
||||
*/
|
||||
interface RenewLease {
|
||||
|
||||
/**
|
||||
* Renew a lease.
|
||||
*
|
||||
* @param lease must not be {@literal null}.
|
||||
* @return the new lease
|
||||
* @throws VaultException if lease renewal runs into problems
|
||||
*/
|
||||
Lease renewLease(Lease lease) throws VaultException;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import lombok.extern.apachecommons.CommonsLog;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.vault.core.lease.domain.Lease;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
import org.springframework.vault.core.lease.event.AfterSecretLeaseRenewedEvent;
|
||||
import org.springframework.vault.core.lease.event.AfterSecretLeaseRevocationEvent;
|
||||
import org.springframework.vault.core.lease.event.BeforeSecretLeaseRevocationEvent;
|
||||
import org.springframework.vault.core.lease.event.LeaseErrorListener;
|
||||
import org.springframework.vault.core.lease.event.LeaseListener;
|
||||
import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent;
|
||||
import org.springframework.vault.core.lease.event.SecretLeaseErrorEvent;
|
||||
import org.springframework.vault.core.lease.event.SecretLeaseEvent;
|
||||
import org.springframework.vault.core.lease.event.SecretLeaseExpiredEvent;
|
||||
|
||||
/**
|
||||
* Publisher for {@link SecretLeaseEvent}s.
|
||||
* <p>
|
||||
* This publisher dispatches events to {@link LeaseListener} and
|
||||
* {@link LeaseErrorListener}. Instances are thread-safe once {@link #afterPropertiesSet()
|
||||
* initialized}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see SecretLeaseEvent
|
||||
* @see LeaseListener
|
||||
* @see LeaseErrorListener
|
||||
*/
|
||||
public class SecretLeaseEventPublisher implements InitializingBean {
|
||||
|
||||
private final Set<LeaseListener> leaseListeners = new CopyOnWriteArraySet<LeaseListener>();
|
||||
|
||||
private final Set<LeaseErrorListener> leaseErrorListeners = new CopyOnWriteArraySet<LeaseErrorListener>();
|
||||
|
||||
/**
|
||||
* Add a {@link LeaseListener} to the container. The listener starts receiving events
|
||||
* as soon as possible.
|
||||
*
|
||||
* @param listener lease listener, must not be {@literal null}.
|
||||
*/
|
||||
public void addLeaseListener(LeaseListener listener) {
|
||||
|
||||
Assert.notNull(listener, "LeaseListener must not be null");
|
||||
|
||||
this.leaseListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a {@link LeaseListener}.
|
||||
*
|
||||
* @param listener must not be {@literal null}.
|
||||
*/
|
||||
public void removeLeaseListener(LeaseListener listener) {
|
||||
this.leaseListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a {@link LeaseErrorListener} to the container. The listener starts receiving
|
||||
* events as soon as possible.
|
||||
*
|
||||
* @param listener lease listener, must not be {@literal null}.
|
||||
*/
|
||||
public void addErrorListener(LeaseErrorListener listener) {
|
||||
|
||||
Assert.notNull(listener, "LeaseListener must not be null");
|
||||
|
||||
this.leaseErrorListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a {@link LeaseErrorListener}.
|
||||
*
|
||||
* @param listener must not be {@literal null}.
|
||||
*/
|
||||
public void removeLeaseErrorListener(LeaseErrorListener listener) {
|
||||
this.leaseErrorListeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
|
||||
if (this.leaseErrorListeners.isEmpty()) {
|
||||
addErrorListener(LoggingErrorListener.INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook method called when secrets were obtained. The default implementation is to
|
||||
* notify {@link LeaseListener}. Implementations can override this method in
|
||||
* subclasses.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
* @param body must not be {@literal null}.
|
||||
*/
|
||||
protected void onSecretsObtained(RequestedSecret requestedSecret, Lease lease,
|
||||
Map<String, Object> body) {
|
||||
|
||||
for (LeaseListener leaseListener : leaseListeners) {
|
||||
leaseListener.onLeaseEvent(
|
||||
new SecretLeaseCreatedEvent(requestedSecret, lease, body));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook method called when a {@link Lease} is renewed. The default implementation is
|
||||
* to notify {@link LeaseListener}. Implementations can override this method in
|
||||
* subclasses.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
*/
|
||||
protected void onAfterLeaseRenewed(RequestedSecret requestedSecret, Lease lease) {
|
||||
|
||||
for (LeaseListener leaseListener : leaseListeners) {
|
||||
leaseListener.onLeaseEvent(
|
||||
new AfterSecretLeaseRenewedEvent(requestedSecret, lease));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook method called before triggering revocation for a {@link Lease}. The default
|
||||
* implementation is to notify {@link LeaseListener}. Implementations can override
|
||||
* this method in subclasses.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
*/
|
||||
protected void onBeforeLeaseRevocation(RequestedSecret requestedSecret, Lease lease) {
|
||||
|
||||
for (LeaseListener leaseListener : leaseListeners) {
|
||||
leaseListener.onLeaseEvent(
|
||||
new BeforeSecretLeaseRevocationEvent(requestedSecret, lease));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook method called after triggering revocation for a {@link Lease}. The default
|
||||
* implementation is to notify {@link LeaseListener}. Implementations can override
|
||||
* this method in subclasses.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
*/
|
||||
protected void onAfterLeaseRevocation(RequestedSecret requestedSecret, Lease lease) {
|
||||
|
||||
for (LeaseListener leaseListener : leaseListeners) {
|
||||
leaseListener.onLeaseEvent(
|
||||
new AfterSecretLeaseRevocationEvent(requestedSecret, lease));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook method called when a {@link Lease} expires. The default implementation is to
|
||||
* notify {@link LeaseListener}. Implementations can override this method in
|
||||
* subclasses.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
*/
|
||||
protected void onLeaseExpired(RequestedSecret requestedSecret, Lease lease) {
|
||||
|
||||
for (LeaseListener leaseListener : leaseListeners) {
|
||||
leaseListener
|
||||
.onLeaseEvent(new SecretLeaseExpiredEvent(requestedSecret, lease));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook method called when an error occurred during secret retrieval, lease renewal,
|
||||
* and other Vault interactions. The default implementation is to notify
|
||||
* {@link LeaseErrorListener}. Implementations can override this method in subclasses.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease may be {@literal null}
|
||||
* @param e the causing exception.
|
||||
*/
|
||||
protected void onError(RequestedSecret requestedSecret, Lease lease, Exception e) {
|
||||
|
||||
for (LeaseErrorListener leaseErrorListener : leaseErrorListeners) {
|
||||
leaseErrorListener.onLeaseError(
|
||||
new SecretLeaseErrorEvent(requestedSecret, lease, e), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple {@link LeaseErrorListener} implementation to log errors.
|
||||
*/
|
||||
@CommonsLog
|
||||
public enum LoggingErrorListener implements LeaseErrorListener {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public void onLeaseError(SecretLeaseEvent leaseEvent, Exception exception) {
|
||||
log.warn(String.format("[%s] %s %s", leaseEvent.getSource(),
|
||||
leaseEvent.getLease(), exception.getMessage()), exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease.domain;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A lease abstracting the lease Id, duration and renewability.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class Lease {
|
||||
|
||||
private final static Lease NONE = new Lease(null, 0, false);
|
||||
|
||||
private final String leaseId;
|
||||
|
||||
private final long leaseDuration;
|
||||
|
||||
private final boolean renewable;
|
||||
|
||||
private Lease(String leaseId, long leaseDuration, boolean renewable) {
|
||||
this.leaseId = leaseId;
|
||||
this.leaseDuration = leaseDuration;
|
||||
this.renewable = renewable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Lease}.
|
||||
*
|
||||
* @param leaseId must not be empty or {@literal null}.
|
||||
* @param leaseDuration the lease duration in seconds
|
||||
* @param renewable {@literal true} if this lease is renewable.
|
||||
* @return the created {@link Lease}
|
||||
*/
|
||||
public static Lease of(String leaseId, long leaseDuration, boolean renewable) {
|
||||
|
||||
Assert.hasText(leaseId, "LeaseId must not be empty");
|
||||
return new Lease(leaseId, leaseDuration, renewable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to return a non-renewable, zero-duration {@link Lease}.
|
||||
*
|
||||
* @return a non-renewable, zero-duration {@link Lease}.
|
||||
*/
|
||||
public static Lease none() {
|
||||
return NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the lease Id
|
||||
*/
|
||||
public String getLeaseId() {
|
||||
return leaseId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the lease duration in seconds.
|
||||
*/
|
||||
public long getLeaseDuration() {
|
||||
return leaseDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return {@literal true} if the lease is renewable.
|
||||
*/
|
||||
public boolean isRenewable() {
|
||||
return renewable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (!(o instanceof Lease))
|
||||
return false;
|
||||
|
||||
Lease lease = (Lease) o;
|
||||
|
||||
if (leaseDuration != lease.leaseDuration)
|
||||
return false;
|
||||
if (renewable != lease.renewable)
|
||||
return false;
|
||||
return leaseId != null ? leaseId.equals(lease.leaseId) : lease.leaseId == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
int result = leaseId != null ? leaseId.hashCode() : 0;
|
||||
result = 31 * result + (int) (leaseDuration ^ (leaseDuration >>> 32));
|
||||
result = 31 * result + (renewable ? 1 : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append(getClass().getSimpleName());
|
||||
sb.append(" [leaseId='").append(leaseId).append('\'');
|
||||
sb.append(", leaseDuration=").append(leaseDuration);
|
||||
sb.append(", renewable=").append(renewable);
|
||||
sb.append(']');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease.domain;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Represents a requested secret from a specific Vault path associated with a lease
|
||||
* {@link Mode}.
|
||||
* <p>
|
||||
* A {@link RequestedSecret} can be renewing or rotating.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see Mode
|
||||
* @see Lease#isRenewable()
|
||||
*/
|
||||
public class RequestedSecret {
|
||||
|
||||
private final String path;
|
||||
private final Mode mode;
|
||||
|
||||
private RequestedSecret(String path, Mode mode) {
|
||||
|
||||
Assert.hasText(path, "Path must not be null or empty");
|
||||
this.path = path;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a renewable {@link RequestedSecret} at {@code path}. A lease associated with
|
||||
* this secret will be renewed if the lease is qualified for renewal. The lease is no
|
||||
* longer valid after expiry.
|
||||
*
|
||||
* @param path must not be {@literal null} or empty.
|
||||
* @return the renewable {@link RequestedSecret}.
|
||||
*/
|
||||
public static RequestedSecret renewable(String path) {
|
||||
return new RequestedSecret(path, Mode.RENEW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a rotating {@link RequestedSecret} at {@code path}. A lease associated with
|
||||
* this secret will be renewed if the lease is qualified for renewal. Once the lease
|
||||
* expires, a new secret with a new lease is obtained.
|
||||
*
|
||||
* @param path must not be {@literal null} or empty.
|
||||
* @return the rotating {@link RequestedSecret}.
|
||||
*/
|
||||
public static RequestedSecret rotating(String path) {
|
||||
return new RequestedSecret(path, Mode.ROTATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Vault path of the requested secret.
|
||||
*/
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return lease mode.
|
||||
* @see Mode
|
||||
*/
|
||||
public Mode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuffer sb = new StringBuffer();
|
||||
sb.append(getClass().getSimpleName());
|
||||
sb.append(" [path='").append(path).append('\'');
|
||||
sb.append(", mode=").append(mode);
|
||||
sb.append(']');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public enum Mode {
|
||||
|
||||
/**
|
||||
* Renew lease of the requested secret until secret expires its max lease time.
|
||||
*/
|
||||
RENEW,
|
||||
|
||||
/**
|
||||
* Renew lease of the requested secret. Obtains new secret along a new lease once
|
||||
* the previous lease expires its max lease time.
|
||||
*/
|
||||
ROTATE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease.event;
|
||||
|
||||
import org.springframework.vault.core.lease.domain.Lease;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
|
||||
/**
|
||||
* Event published after renewing a {@link Lease} for a {@link RequestedSecret}. The
|
||||
* secrets associated with {@link Lease} should be considered valid and the lease extended
|
||||
* when this event is received.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class AfterSecretLeaseRenewedEvent extends SecretLeaseEvent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Create a new {@link SecretLeaseExpiredEvent} given {@link RequestedSecret} and
|
||||
* {@link Lease}.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
*/
|
||||
public AfterSecretLeaseRenewedEvent(RequestedSecret requestedSecret, Lease lease) {
|
||||
super(requestedSecret, lease);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease.event;
|
||||
|
||||
import org.springframework.vault.core.lease.domain.Lease;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
|
||||
/**
|
||||
* Event published after revoking a {@link Lease} for a {@link RequestedSecret}. The
|
||||
* secrets associated with {@link Lease} should be considered invalid when this event is
|
||||
* received.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class AfterSecretLeaseRevocationEvent extends SecretLeaseEvent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Create a new {@link SecretLeaseExpiredEvent} given {@link RequestedSecret} and
|
||||
* {@link Lease}.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
*/
|
||||
public AfterSecretLeaseRevocationEvent(RequestedSecret requestedSecret, Lease lease) {
|
||||
super(requestedSecret, lease);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease.event;
|
||||
|
||||
import org.springframework.vault.core.lease.domain.Lease;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
|
||||
/**
|
||||
* Event published before revoking a {@link Lease} for a {@link RequestedSecret}. The
|
||||
* secrets associated with {@link Lease} should be considered still valid when this event
|
||||
* is received.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see AfterSecretLeaseRevocationEvent
|
||||
*/
|
||||
public class BeforeSecretLeaseRevocationEvent extends SecretLeaseEvent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Create a new {@link SecretLeaseExpiredEvent} given {@link RequestedSecret} and
|
||||
* {@link Lease}.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
*/
|
||||
public BeforeSecretLeaseRevocationEvent(RequestedSecret requestedSecret,
|
||||
Lease lease) {
|
||||
super(requestedSecret, lease);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease.event;
|
||||
|
||||
/**
|
||||
* Listener for Vault exceptional {@link SecretLeaseEvent}s.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public interface LeaseErrorListener {
|
||||
|
||||
/**
|
||||
* Callback for a {@link SecretLeaseEvent}
|
||||
*
|
||||
* @param leaseEvent the event object, must not be {@literal null}.
|
||||
* @param exception the thrown {@link Exception}.
|
||||
*/
|
||||
void onLeaseError(SecretLeaseEvent leaseEvent, Exception exception);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease.event;
|
||||
|
||||
/**
|
||||
* Listener for Vault {@link SecretLeaseEvent}s.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public interface LeaseListener {
|
||||
|
||||
/**
|
||||
* Callback for a {@link SecretLeaseEvent}
|
||||
*
|
||||
* @param leaseEvent the event object, must not be {@literal null}.
|
||||
*/
|
||||
void onLeaseEvent(SecretLeaseEvent leaseEvent);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease.event;
|
||||
|
||||
/**
|
||||
* Empty listener adapter implementing {@link LeaseListener} and
|
||||
* {@link LeaseErrorListener}. Typically used to facilitate interface evolution.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see SecretLeaseEvent
|
||||
*/
|
||||
public abstract class LeaseListenerAdapter implements LeaseListener, LeaseErrorListener {
|
||||
|
||||
@Override
|
||||
public void onLeaseEvent(SecretLeaseEvent leaseEvent) {
|
||||
// empty listener method
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLeaseError(SecretLeaseEvent leaseEvent, Exception exception) {
|
||||
// empty listener method
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease.event;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.vault.core.lease.domain.Lease;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
|
||||
/**
|
||||
* Event published after creating a {@link Lease} for a {@link RequestedSecret}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class SecretLeaseCreatedEvent extends SecretLeaseEvent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Map<String, Object> secrets;
|
||||
|
||||
/**
|
||||
* Create a new {@link SecretLeaseExpiredEvent} given {@link RequestedSecret},
|
||||
* {@link Lease} and {@code secrets}.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
*/
|
||||
public SecretLeaseCreatedEvent(RequestedSecret requestedSecret, Lease lease,
|
||||
Map<String, Object> secrets) {
|
||||
|
||||
super(requestedSecret, lease);
|
||||
this.secrets = Collections.unmodifiableMap(new HashMap<String, Object>(secrets));
|
||||
}
|
||||
|
||||
public Map<String, Object> getSecrets() {
|
||||
return secrets;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease.event;
|
||||
|
||||
import org.springframework.vault.core.lease.domain.Lease;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
|
||||
/**
|
||||
* Event published when caught an {@link Exception} during secret retrieval and lease
|
||||
* interaction.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class SecretLeaseErrorEvent extends SecretLeaseEvent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Throwable exception;
|
||||
|
||||
/**
|
||||
* Create a new {@link SecretLeaseExpiredEvent} given {@link RequestedSecret},
|
||||
* {@link Lease} and {@link Throwable}.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease can be {@literal null}.
|
||||
* @param exception must not be {@literal null}.
|
||||
*/
|
||||
public SecretLeaseErrorEvent(RequestedSecret requestedSecret, Lease lease,
|
||||
Throwable exception) {
|
||||
super(requestedSecret, lease);
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
public Throwable getException() {
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease.event;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.vault.core.lease.domain.Lease;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
|
||||
/**
|
||||
* Abstract base class for {@link Lease} based
|
||||
* events.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public abstract class SecretLeaseEvent extends ApplicationEvent {
|
||||
|
||||
private final Lease lease;
|
||||
|
||||
/**
|
||||
* Create a new {@link SecretLeaseExpiredEvent} given {@link RequestedSecret} and
|
||||
* {@link Lease}.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
*/
|
||||
protected SecretLeaseEvent(RequestedSecret requestedSecret, Lease lease) {
|
||||
super(requestedSecret);
|
||||
this.lease = lease;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestedSecret getSource() {
|
||||
return (RequestedSecret) super.getSource();
|
||||
}
|
||||
|
||||
public Lease getLease() {
|
||||
return lease;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease.event;
|
||||
|
||||
import org.springframework.vault.core.lease.domain.Lease;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
|
||||
/**
|
||||
* Event published after an expired {@link Lease} for a {@link RequestedSecret} was
|
||||
* observed.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class SecretLeaseExpiredEvent extends SecretLeaseEvent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Create a new {@link SecretLeaseExpiredEvent} given {@link RequestedSecret} and
|
||||
* {@link Lease}.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
*/
|
||||
public SecretLeaseExpiredEvent(RequestedSecret requestedSecret, Lease lease) {
|
||||
super(requestedSecret, lease);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.vault.core.lease;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.Trigger;
|
||||
import org.springframework.vault.VaultException;
|
||||
import org.springframework.vault.core.RestOperationsCallback;
|
||||
import org.springframework.vault.core.VaultOperations;
|
||||
import org.springframework.vault.core.lease.domain.Lease;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
import org.springframework.vault.core.lease.event.AfterSecretLeaseRevocationEvent;
|
||||
import org.springframework.vault.core.lease.event.BeforeSecretLeaseRevocationEvent;
|
||||
import org.springframework.vault.core.lease.event.LeaseListenerAdapter;
|
||||
import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent;
|
||||
import org.springframework.vault.core.lease.event.SecretLeaseEvent;
|
||||
import org.springframework.vault.core.lease.event.SecretLeaseExpiredEvent;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SecretLeaseContainer}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class SecretLeaseContainerUnitTests {
|
||||
|
||||
@Mock
|
||||
private VaultOperations vaultOperations;
|
||||
|
||||
@Mock
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
@Mock
|
||||
private ScheduledFuture scheduledFuture;
|
||||
|
||||
@Mock
|
||||
private LeaseListenerAdapter leaseListenerAdapter;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<SecretLeaseEvent> captor;
|
||||
|
||||
private RequestedSecret requestedSecret = RequestedSecret.renewable("my-secret");
|
||||
|
||||
private SecretLeaseContainer secretLeaseContainer;
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
|
||||
secretLeaseContainer = new SecretLeaseContainer(vaultOperations, taskScheduler);
|
||||
secretLeaseContainer.addLeaseListener(leaseListenerAdapter);
|
||||
secretLeaseContainer.addErrorListener(leaseListenerAdapter);
|
||||
secretLeaseContainer.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldWorkIfNoSecretsRequested() {
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
verifyZeroInteractions(leaseListenerAdapter);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldWorkIfNoSecretsFound() {
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
secretLeaseContainer.requestRenewableSecret(requestedSecret.getPath());
|
||||
|
||||
verifyZeroInteractions(leaseListenerAdapter);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAcceptSecretsWithoutLease() {
|
||||
|
||||
VaultResponse secrets = new VaultResponse();
|
||||
secrets.setData(Collections.singletonMap("key", (Object) "value"));
|
||||
|
||||
when(vaultOperations.read(requestedSecret.getPath())).thenReturn(secrets);
|
||||
|
||||
secretLeaseContainer.addRequestedSecret(requestedSecret);
|
||||
secretLeaseContainer.start();
|
||||
|
||||
verifyZeroInteractions(taskScheduler);
|
||||
verify(leaseListenerAdapter).onLeaseEvent(captor.capture());
|
||||
|
||||
SecretLeaseCreatedEvent leaseCreatedEvent = (SecretLeaseCreatedEvent) captor
|
||||
.getValue();
|
||||
|
||||
assertThat(leaseCreatedEvent.getSource()).isEqualTo(requestedSecret);
|
||||
assertThat(leaseCreatedEvent.getLease()).isNotNull();
|
||||
assertThat(leaseCreatedEvent.getSecrets()).containsKey("key");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAcceptSecretsWithStaticLease() {
|
||||
|
||||
VaultResponse secrets = new VaultResponse();
|
||||
secrets.setLeaseId("lease");
|
||||
secrets.setRenewable(false);
|
||||
secrets.setData(Collections.singletonMap("key", (Object) "value"));
|
||||
|
||||
when(vaultOperations.read(requestedSecret.getPath())).thenReturn(secrets);
|
||||
|
||||
secretLeaseContainer.addRequestedSecret(requestedSecret);
|
||||
secretLeaseContainer.start();
|
||||
|
||||
verifyZeroInteractions(taskScheduler);
|
||||
verify(leaseListenerAdapter).onLeaseEvent(captor.capture());
|
||||
|
||||
SecretLeaseCreatedEvent leaseCreatedEvent = (SecretLeaseCreatedEvent) captor
|
||||
.getValue();
|
||||
|
||||
assertThat(leaseCreatedEvent.getSource()).isEqualTo(requestedSecret);
|
||||
assertThat(leaseCreatedEvent.getLease()).isNotNull();
|
||||
assertThat(leaseCreatedEvent.getSecrets()).containsKey("key");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldPropagateErrorsToListenerOnInitialRetrieval() {
|
||||
|
||||
VaultException e = new VaultException("error");
|
||||
when(vaultOperations.read(requestedSecret.getPath())).thenThrow(e);
|
||||
|
||||
secretLeaseContainer.addRequestedSecret(requestedSecret);
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
verify(leaseListenerAdapter).onLeaseError(captor.capture(), eq(e));
|
||||
verifyNoMoreInteractions(leaseListenerAdapter);
|
||||
|
||||
SecretLeaseEvent leaseEvent = captor.getValue();
|
||||
|
||||
assertThat(leaseEvent.getSource()).isEqualTo(requestedSecret);
|
||||
assertThat(leaseEvent.getLease()).isEqualTo(Lease.none());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldAcceptSecretsWithRenewableLease() {
|
||||
|
||||
when(taskScheduler.schedule(any(Runnable.class), any(Trigger.class)))
|
||||
.thenReturn(scheduledFuture);
|
||||
|
||||
when(vaultOperations.read(requestedSecret.getPath())).thenReturn(createSecrets());
|
||||
|
||||
secretLeaseContainer.addRequestedSecret(requestedSecret);
|
||||
secretLeaseContainer.start();
|
||||
|
||||
verify(taskScheduler).schedule(any(Runnable.class), any(Trigger.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRenewLease() {
|
||||
|
||||
prepareRenewal();
|
||||
when(vaultOperations.doWithSession(any(RestOperationsCallback.class)))
|
||||
.thenReturn(getResponseEntity("new_lease", true, 70, HttpStatus.OK));
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(taskScheduler).schedule(captor.capture(), any(Trigger.class));
|
||||
|
||||
captor.getValue().run();
|
||||
verifyZeroInteractions(scheduledFuture);
|
||||
verify(taskScheduler, times(2)).schedule(captor.capture(), any(Trigger.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotRenewExpiringLease() {
|
||||
|
||||
prepareRenewal();
|
||||
when(vaultOperations.doWithSession(any(RestOperationsCallback.class)))
|
||||
.thenReturn(getResponseEntity("new_lease", true, 5, HttpStatus.OK));
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(taskScheduler).schedule(captor.capture(), any(Trigger.class));
|
||||
|
||||
captor.getValue().run();
|
||||
verifyZeroInteractions(scheduledFuture);
|
||||
verify(taskScheduler, times(1)).schedule(captor.capture(), any(Trigger.class));
|
||||
verify(leaseListenerAdapter).onLeaseEvent(any(SecretLeaseCreatedEvent.class));
|
||||
verify(leaseListenerAdapter).onLeaseEvent(any(SecretLeaseExpiredEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotRotateExpiringLease() {
|
||||
|
||||
when(taskScheduler.schedule(any(Runnable.class), any(Trigger.class)))
|
||||
.thenReturn(scheduledFuture);
|
||||
|
||||
VaultResponse first = createSecrets();
|
||||
VaultResponse second = createSecrets();
|
||||
second.setData(Collections.singletonMap("foo", (Object) "bar"));
|
||||
|
||||
when(vaultOperations.read(requestedSecret.getPath())).thenReturn(first, second);
|
||||
when(vaultOperations.doWithSession(any(RestOperationsCallback.class)))
|
||||
.thenReturn(getResponseEntity("new_lease", true, 5, HttpStatus.OK));
|
||||
|
||||
secretLeaseContainer.requestRotatingSecret("my-secret");
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(taskScheduler).schedule(captor.capture(), any(Trigger.class));
|
||||
|
||||
captor.getValue().run();
|
||||
verify(taskScheduler, times(2)).schedule(captor.capture(), any(Trigger.class));
|
||||
|
||||
ArgumentCaptor<SecretLeaseCreatedEvent> createdEvents = ArgumentCaptor
|
||||
.forClass(SecretLeaseCreatedEvent.class);
|
||||
verify(leaseListenerAdapter, times(3)).onLeaseEvent(createdEvents.capture());
|
||||
|
||||
List<SecretLeaseCreatedEvent> events = createdEvents.getAllValues();
|
||||
|
||||
assertThat(events).hasSize(3);
|
||||
assertThat(events.get(0)).isInstanceOf(SecretLeaseCreatedEvent.class);
|
||||
assertThat(events.get(0).getSecrets()).containsOnlyKeys("key");
|
||||
|
||||
assertThat(events.get(1)).isInstanceOf(SecretLeaseExpiredEvent.class);
|
||||
|
||||
assertThat(events.get(2)).isInstanceOf(SecretLeaseCreatedEvent.class);
|
||||
assertThat(events.get(2).getSecrets()).containsOnlyKeys("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scheduleRenewalShouldApplyExpiryThreshold() {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
ArgumentCaptor<Trigger> captor = ArgumentCaptor.forClass(Trigger.class);
|
||||
verify(taskScheduler).schedule(any(Runnable.class), captor.capture());
|
||||
|
||||
Date nextExecutionTime = captor.getValue().nextExecutionTime(null);
|
||||
assertThat(nextExecutionTime).isBetween(
|
||||
new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(35)),
|
||||
new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(41)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldPublishRenewalErrors() {
|
||||
|
||||
prepareRenewal();
|
||||
when(vaultOperations.doWithSession(any(RestOperationsCallback.class)))
|
||||
.thenThrow(new HttpClientErrorException(HttpStatus.I_AM_A_TEAPOT));
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(taskScheduler).schedule(runnableCaptor.capture(), any(Trigger.class));
|
||||
|
||||
runnableCaptor.getValue().run();
|
||||
|
||||
verify(leaseListenerAdapter).onLeaseEvent(any(SecretLeaseCreatedEvent.class));
|
||||
verify(leaseListenerAdapter).onLeaseError(captor.capture(),
|
||||
any(VaultException.class));
|
||||
verifyNoMoreInteractions(leaseListenerAdapter);
|
||||
|
||||
SecretLeaseEvent leaseEvent = captor.getValue();
|
||||
|
||||
assertThat(leaseEvent.getSource()).isEqualTo(requestedSecret);
|
||||
assertThat(leaseEvent.getLease()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void subsequentScheduleRenewalShouldApplyExpiryThreshold() {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
when(vaultOperations.doWithSession(any(RestOperationsCallback.class)))
|
||||
.thenReturn(getResponseEntity("new_lease", true, 70, HttpStatus.OK));
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(taskScheduler).schedule(runnableCaptor.capture(), any(Trigger.class));
|
||||
|
||||
runnableCaptor.getValue().run();
|
||||
|
||||
ArgumentCaptor<Trigger> captor = ArgumentCaptor.forClass(Trigger.class);
|
||||
verify(taskScheduler, times(2)).schedule(any(Runnable.class), captor.capture());
|
||||
|
||||
assertThat(captor.getAllValues().get(0).nextExecutionTime(null)).isBetween(
|
||||
new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(35)),
|
||||
new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(41)));
|
||||
|
||||
assertThat(captor.getAllValues().get(1).nextExecutionTime(null)).isBetween(
|
||||
new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(9)),
|
||||
new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(11)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scheduleRenewalShouldTriggerOnlyOnce() {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
ArgumentCaptor<Trigger> captor = ArgumentCaptor.forClass(Trigger.class);
|
||||
verify(taskScheduler).schedule(any(Runnable.class), captor.capture());
|
||||
|
||||
Trigger trigger = captor.getValue();
|
||||
|
||||
assertThat(trigger.nextExecutionTime(null)).isNotNull();
|
||||
assertThat(trigger.nextExecutionTime(null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subsequentStartShouldNoOp() {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(taskScheduler).schedule(captor.capture(), any(Trigger.class));
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
verifyNoMoreInteractions(scheduledFuture);
|
||||
verifyNoMoreInteractions(taskScheduler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canceledRenewalShouldSkipRenewal() {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
secretLeaseContainer.start();
|
||||
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(taskScheduler).schedule(captor.capture(), any(Trigger.class));
|
||||
verify(vaultOperations).read(anyString());
|
||||
|
||||
secretLeaseContainer.stop();
|
||||
|
||||
verify(scheduledFuture).cancel(false);
|
||||
|
||||
captor.getValue().run();
|
||||
|
||||
verifyNoMoreInteractions(vaultOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDisableRenewalOnDisposal() throws Exception {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
secretLeaseContainer.start();
|
||||
secretLeaseContainer.destroy();
|
||||
|
||||
verify(vaultOperations).doWithSession(any(RestOperationsCallback.class));
|
||||
verify(scheduledFuture).cancel(false);
|
||||
verify(leaseListenerAdapter).onLeaseEvent(any(SecretLeaseCreatedEvent.class));
|
||||
verify(leaseListenerAdapter)
|
||||
.onLeaseEvent(any(BeforeSecretLeaseRevocationEvent.class));
|
||||
verify(leaseListenerAdapter)
|
||||
.onLeaseEvent(any(AfterSecretLeaseRevocationEvent.class));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void prepareRenewal() {
|
||||
|
||||
when(taskScheduler.schedule(any(Runnable.class), any(Trigger.class)))
|
||||
.thenReturn(scheduledFuture);
|
||||
|
||||
when(vaultOperations.read(requestedSecret.getPath())).thenReturn(createSecrets());
|
||||
|
||||
secretLeaseContainer.addRequestedSecret(requestedSecret);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> getResponseEntity(String leaseId,
|
||||
Boolean renewable, Integer leaseDuration, HttpStatus httpStatus) {
|
||||
|
||||
Map<String, Object> body = new HashMap<String, Object>();
|
||||
body.put("lease_id", leaseId);
|
||||
body.put("renewable", renewable);
|
||||
body.put("lease_duration", leaseDuration);
|
||||
|
||||
return getEntity(body, httpStatus);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> getEntity(Map<String, Object> body,
|
||||
HttpStatus status) {
|
||||
|
||||
return new ResponseEntity<Map<String, Object>>(body, status);
|
||||
}
|
||||
|
||||
private VaultResponse createSecrets() {
|
||||
|
||||
VaultResponse secrets = new VaultResponse();
|
||||
|
||||
secrets.setLeaseId("lease");
|
||||
secrets.setRenewable(true);
|
||||
secrets.setLeaseDuration(100);
|
||||
secrets.setData(Collections.singletonMap("key", (Object) "value"));
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user