Support lease lifecycle (renewal and revocation).
Spring Cloud Vault now handles lifecycle of obtained secrets by property sources. Secrets associated with a renewable lease are renewed before they expire until terminal expiration. Application shutdown revokes leases so generated credentials can be disabled by Vault. Fixes gh-40.
This commit is contained in:
@@ -690,3 +690,44 @@ trust-store.
|
||||
Please note that configuring `spring.cloud.vault.ssl.*` can be only
|
||||
applied when either Apache Http Components or the OkHttp client
|
||||
is on your class-path.
|
||||
|
||||
[[vault-lease-renewal]]
|
||||
== Lease lifecycle management (renewal and revocation)
|
||||
|
||||
With every secret, Vault creates a lease:
|
||||
metadata containing information such as a time duration,
|
||||
renewability, and more.
|
||||
|
||||
Vault promises that the data will be valid for the given duration,
|
||||
or Time To Live (TTL). Once the lease is expired, Vault can
|
||||
revoke the data, and the consumer of the secret can no longer
|
||||
be certain that it is valid.
|
||||
|
||||
Spring Cloud Vault maintains a lease lifecycle beyond
|
||||
the creation of login tokens and secrets. That said,
|
||||
login tokens and secrets associated with a lease
|
||||
are scheduled for renewal just before the lease expires
|
||||
until terminal expiry.
|
||||
Application shutdown revokes obtained login tokens and renewable
|
||||
leases.
|
||||
|
||||
Secret service and database backends (such as MongoDB or MySQL)
|
||||
usually generate a renewable lease so generated credentials will
|
||||
be disabled on application shutdown.
|
||||
|
||||
NOTE: Static tokens are not renewed or revoked.
|
||||
|
||||
Lease renewal and revocation is enabled by default and can
|
||||
be disabled by setting `spring.cloud.vault.config.lifecycle.enabled`
|
||||
to `false`. This is not recommended as leases can expire and
|
||||
Spring Cloud Vault cannot longer access Vault or services
|
||||
using generated credentials and valid credentials remain active
|
||||
after application shutdown.
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
spring.cloud.vault:
|
||||
config.lifecycle.enabled: true
|
||||
----
|
||||
|
||||
See also: https://www.vaultproject.io/docs/concepts/lease.html[Vault Documentation: Lease, Renew, and Revoke]
|
||||
|
||||
@@ -90,7 +90,8 @@ public class AwsSecretIntegrationTests extends IntegrationTestSupport {
|
||||
@Test
|
||||
public void shouldCreateCredentialsCorrectly() throws Exception {
|
||||
|
||||
Map<String, String> secretProperties = configOperations.read(forAws(aws));
|
||||
Map<String, String> secretProperties = configOperations.read(forAws(aws))
|
||||
.getData();
|
||||
|
||||
assertThat(secretProperties).containsKeys("cloud.aws.credentials.accessKey",
|
||||
"cloud.aws.credentials.secretKey");
|
||||
|
||||
@@ -112,7 +112,8 @@ public class ConsulSecretIntegrationTests extends IntegrationTestSupport {
|
||||
@Test
|
||||
public void shouldCreateCredentialsCorrectly() throws Exception {
|
||||
|
||||
Map<String, String> secretProperties = configOperations.read(forConsul(consul));
|
||||
Map<String, String> secretProperties = configOperations.read(forConsul(consul))
|
||||
.getData();
|
||||
|
||||
assertThat(secretProperties).containsKeys("spring.cloud.consul.token");
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public class CassandraSecretIntegrationTests extends IntegrationTestSupport {
|
||||
public void shouldCreateCredentialsCorrectly() throws Exception {
|
||||
|
||||
Map<String, String> secretProperties = configOperations
|
||||
.read(forDatabase(cassandra));
|
||||
.read(forDatabase(cassandra)).getData();
|
||||
|
||||
assertThat(secretProperties).containsKeys("spring.data.cassandra.username",
|
||||
"spring.data.cassandra.password");
|
||||
|
||||
@@ -89,7 +89,7 @@ public class MongoSecretIntegrationTests extends IntegrationTestSupport {
|
||||
public void shouldCreateCredentialsCorrectly() throws Exception {
|
||||
|
||||
Map<String, String> secretProperties = configOperations
|
||||
.read(forDatabase(mongodb));
|
||||
.read(forDatabase(mongodb)).getData();
|
||||
|
||||
assertThat(secretProperties).containsKeys("spring.data.mongodb.username",
|
||||
"spring.data.mongodb.password");
|
||||
|
||||
@@ -84,7 +84,8 @@ public class MySqlSecretIntegrationTests extends IntegrationTestSupport {
|
||||
@Test
|
||||
public void shouldCreateCredentialsCorrectly() throws Exception {
|
||||
|
||||
Map<String, String> secretProperties = configOperations.read(forDatabase(mySql));
|
||||
Map<String, String> secretProperties = configOperations.read(forDatabase(mySql))
|
||||
.getData();
|
||||
|
||||
assertThat(secretProperties).containsKeys("spring.datasource.username",
|
||||
"spring.datasource.password");
|
||||
|
||||
@@ -94,7 +94,7 @@ public class PostgreSqlSecretIntegrationTests extends IntegrationTestSupport {
|
||||
public void shouldCreateCredentialsCorrectly() throws Exception {
|
||||
|
||||
Map<String, String> secretProperties = configOperations
|
||||
.read(forDatabase(postgreSql));
|
||||
.read(forDatabase(postgreSql)).getData();
|
||||
|
||||
assertThat(secretProperties).containsKeys("spring.datasource.username",
|
||||
"spring.datasource.password");
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.springframework.cloud.vault.config.databases;
|
||||
import static org.junit.Assume.*;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -29,6 +28,7 @@ import org.bson.Document;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
@@ -43,8 +43,8 @@ import com.mongodb.MongoClient;
|
||||
import com.mongodb.client.MongoDatabase;
|
||||
|
||||
/**
|
||||
* Integration tests using the mongodb secret backend. In case this test should fail because
|
||||
* of SSL make sure you run the test within the
|
||||
* Integration tests using the mongodb secret backend. In case this test should fail
|
||||
* because of SSL make sure you run the test within the
|
||||
* spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore is
|
||||
* referenced with {@code ../work/keystore.jks}.
|
||||
*
|
||||
@@ -65,7 +65,7 @@ public class VaultConfigMongoTests {
|
||||
private final static String ROLES = "[ \"readWrite\", { \"role\": \"read\", \"db\": \"admin\" } ]";
|
||||
|
||||
/**
|
||||
* Initialize the mysql secret backend.
|
||||
* Initialize the mongo secret backend.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -103,7 +103,7 @@ public class VaultConfigMongoTests {
|
||||
MongoClient mongoClient;
|
||||
|
||||
@Test
|
||||
public void shouldConnectUsingDataSource() throws SQLException {
|
||||
public void shouldConnectUsingDataSource() {
|
||||
|
||||
MongoDatabase mongoDatabase = mongoClient.getDatabase("admin");
|
||||
|
||||
@@ -123,7 +123,7 @@ public class VaultConfigMongoTests {
|
||||
public static class TestApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TestApplication.class, args);
|
||||
SpringApplication.run(TestApplication.class, args).registerShutdownHook();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public class RabbitMqSecretIntegrationTests extends IntegrationTestSupport {
|
||||
public void shouldCreateCredentialsCorrectly() throws Exception {
|
||||
|
||||
Map<String, String> secretProperties = configOperations
|
||||
.read(forRabbitMq(rabbitmq));
|
||||
.read(forRabbitMq(rabbitmq)).getData();
|
||||
|
||||
assertThat(secretProperties).containsKeys("spring.rabbitmq.username",
|
||||
"spring.rabbitmq.password");
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2016 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.cloud.vault.config;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
class Lease {
|
||||
|
||||
private final String leaseId;
|
||||
|
||||
private final long leaseDuration;
|
||||
|
||||
private final boolean renewable;
|
||||
|
||||
private Lease(String leaseId, long leaseDuration, boolean renewable) {
|
||||
|
||||
Assert.hasText(leaseId, "LeaseId must not be empty");
|
||||
|
||||
this.leaseId = leaseId;
|
||||
this.leaseDuration = leaseDuration;
|
||||
this.renewable = renewable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates 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) {
|
||||
return new Lease(leaseId, leaseDuration, renewable);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the lease Id
|
||||
*/
|
||||
public String getLeaseId() {
|
||||
return leaseId;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public long getLeaseDuration() {
|
||||
return leaseDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return {@literal true} if the lease is renewable.
|
||||
*/
|
||||
public boolean isRenewable() {
|
||||
return renewable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
/*
|
||||
* Copyright 2016 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.cloud.vault.config;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.Trigger;
|
||||
import org.springframework.scheduling.TriggerContext;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.vault.client.VaultException;
|
||||
import org.springframework.vault.client.VaultResponseEntity;
|
||||
import org.springframework.vault.core.VaultOperations;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* A {@link VaultPropertySource} that renews a {@link Lease} associated with
|
||||
* {@link Secrets}.
|
||||
*
|
||||
* <p>
|
||||
* {@link Lease} is scheduled right before its expiry. Expiry threshold can be set by
|
||||
* calling {@link #setExpiryThresholdSeconds(int)}. Leases that reached their maximum
|
||||
* lifetime are not re-read from Vault.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@Slf4j
|
||||
class LeasingVaultPropertySource extends VaultPropertySource implements DisposableBean {
|
||||
|
||||
private final LeaseRenewalScheduler leaseRenewal;
|
||||
|
||||
private int minRenewalSeconds = 10;
|
||||
|
||||
private int expiryThresholdSeconds = 60;
|
||||
|
||||
private volatile Lease lease;
|
||||
|
||||
/**
|
||||
* Creates a new {@link VaultPropertySource}.
|
||||
*
|
||||
* @param operations must not be {@literal null}.
|
||||
* @param properties must not be {@literal null}.
|
||||
* @param secureBackendAccessor must not be {@literal null}.
|
||||
* @param taskScheduler must not be {@literal null}.
|
||||
*/
|
||||
public LeasingVaultPropertySource(VaultConfigTemplate operations,
|
||||
VaultProperties properties, SecureBackendAccessor secureBackendAccessor,
|
||||
TaskScheduler taskScheduler) {
|
||||
|
||||
super(operations, properties, secureBackendAccessor);
|
||||
|
||||
Assert.notNull(taskScheduler, "TaskScheduler must not be null");
|
||||
|
||||
leaseRenewal = new LeaseRenewalScheduler(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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
|
||||
super.init();
|
||||
|
||||
Secrets secrets = getSecrets();
|
||||
|
||||
this.lease = getLease(secrets);
|
||||
|
||||
potentiallyScheduleLeaseRenewal(this.lease);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown this {@link LeasingVaultPropertySource}
|
||||
*/
|
||||
public void destroy() {
|
||||
|
||||
if (this.lease != null) {
|
||||
try {
|
||||
leaseRenewal.disableScheduleRenewal();
|
||||
doRevokeLease(this.lease);
|
||||
}
|
||||
finally {
|
||||
this.lease = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Lease getLease(Secrets secrets) {
|
||||
|
||||
if (secrets == null || !StringUtils.hasText(secrets.getLeaseId())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Lease.of(secrets.getLeaseId(), secrets.getLeaseDuration(),
|
||||
secrets.isRenewable());
|
||||
}
|
||||
|
||||
private void potentiallyScheduleLeaseRenewal(Lease lease) {
|
||||
|
||||
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(lease);
|
||||
LeasingVaultPropertySource.this.lease = newLease;
|
||||
potentiallyScheduleLeaseRenewal(newLease);
|
||||
|
||||
return newLease;
|
||||
}
|
||||
}, lease, minRenewalSeconds, expiryThresholdSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renews a {@link Lease}.
|
||||
*
|
||||
* @param lease the lease
|
||||
* @return the new lease.
|
||||
*/
|
||||
private Lease doRenewLease(final Lease lease) {
|
||||
|
||||
VaultResponseEntity<Map<String, Object>> entity = getSource().getVaultOperations()
|
||||
.doWithVault(
|
||||
new VaultOperations.SessionCallback<VaultResponseEntity<Map<String, Object>>>() {
|
||||
@Override
|
||||
public VaultResponseEntity<Map<String, Object>> doWithVault(
|
||||
VaultOperations.VaultSession session) {
|
||||
|
||||
return session.putForEntity(
|
||||
String.format("sys/renew/%s", lease.getLeaseId()),
|
||||
null, Map.class);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if (entity.isSuccessful() && entity.hasBody()) {
|
||||
|
||||
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 (!StringUtils.hasText(leaseId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Lease.of(leaseId,
|
||||
leaseDuration != null ? leaseDuration.longValue() : 0, renewable);
|
||||
}
|
||||
|
||||
throw new VaultException(
|
||||
String.format("Cannot renew lease: %s", buildExceptionMessage(entity)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes the {@link Lease}.
|
||||
*
|
||||
* @param lease the lease.
|
||||
*/
|
||||
private void doRevokeLease(final Lease lease) {
|
||||
|
||||
VaultResponseEntity<Map<String, Object>> entity = getSource().getVaultOperations()
|
||||
.doWithVault(
|
||||
new VaultOperations.SessionCallback<VaultResponseEntity<Map<String, Object>>>() {
|
||||
@Override
|
||||
public VaultResponseEntity<Map<String, Object>> doWithVault(
|
||||
VaultOperations.VaultSession session) {
|
||||
|
||||
return session.putForEntity(String.format("sys/revoke/%s",
|
||||
lease.getLeaseId()), null, Map.class);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if (entity.isSuccessful()) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new VaultException(
|
||||
String.format("Cannot revoke lease: %s", buildExceptionMessage(entity)));
|
||||
}
|
||||
|
||||
private static String buildExceptionMessage(VaultResponseEntity<?> response) {
|
||||
|
||||
if (StringUtils.hasText(response.getMessage())) {
|
||||
return String.format("Status %s URI %s: %s", response.getStatusCode(),
|
||||
response.getUri(), response.getMessage());
|
||||
}
|
||||
|
||||
return String.format("Status %s URI %s", response.getStatusCode(),
|
||||
response.getUri());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private static class LeaseRenewalScheduler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private final TaskScheduler taskScheduler;
|
||||
|
||||
private final AtomicReference<Lease> currentLease = new AtomicReference<>();
|
||||
|
||||
private final Map<Lease, ScheduledFuture<?>> schedules = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
*
|
||||
* @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) {
|
||||
|
||||
logger.debug("Scheduling renewal for lease {}, lease duration {}",
|
||||
lease.getLeaseId(), lease.getLeaseDuration());
|
||||
|
||||
Lease currentLease = this.currentLease.get();
|
||||
this.currentLease.set(lease);
|
||||
|
||||
if (currentLease != null) {
|
||||
cancelSchedule(currentLease);
|
||||
}
|
||||
|
||||
ScheduledFuture<?> scheduledFuture = taskScheduler.schedule(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
schedules.remove(lease);
|
||||
|
||||
if (LeaseRenewalScheduler.this.currentLease.get() != lease) {
|
||||
logger.debug("Current lease has changed. Skipping renewal");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Renewing lease {}", lease.getLeaseId());
|
||||
LeaseRenewalScheduler.this.currentLease.compareAndSet(lease,
|
||||
renewLease.renewLease(lease));
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.error("Cannot renew lease {}", 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) {
|
||||
logger.debug("Canceling previously registered schedule for lease {}",
|
||||
lease.getLeaseId());
|
||||
scheduledFuture.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables schedule for already scheduled renewals.
|
||||
*/
|
||||
public void disableScheduleRenewal() {
|
||||
|
||||
currentLease.set(null);
|
||||
Set<Lease> leases = new HashSet<>(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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This one-shot trigger creates only one execution time to trigger an execution only
|
||||
* once.
|
||||
*/
|
||||
private static class OneShotTrigger implements Trigger {
|
||||
|
||||
private final AtomicBoolean fired = new AtomicBoolean();
|
||||
|
||||
private final long seconds;
|
||||
|
||||
OneShotTrigger(long seconds) {
|
||||
this.seconds = seconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date nextExecutionTime(TriggerContext triggerContext) {
|
||||
|
||||
if (fired.compareAndSet(false, true)) {
|
||||
return new Date(
|
||||
System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(seconds));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy interface to renew a {@link Lease}.
|
||||
*/
|
||||
private 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,105 @@
|
||||
/*
|
||||
* Copyright 2016 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.cloud.vault.config;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Extension to {@link LeasingVaultPropertySourceLocator} that creates
|
||||
* {@link LeasingVaultPropertySource}s.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see LeasingVaultPropertySource
|
||||
*/
|
||||
@Slf4j
|
||||
class LeasingVaultPropertySourceLocator extends VaultPropertySourceLocator
|
||||
implements DisposableBean {
|
||||
|
||||
private final VaultConfigTemplate operations;
|
||||
|
||||
private final VaultProperties properties;
|
||||
|
||||
private final TaskScheduler taskScheduler;
|
||||
|
||||
private final Set<PropertySource<?>> locatedPropertySources = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Creates a new {@link LeasingVaultPropertySourceLocator}.
|
||||
* @param operations must not be {@literal null}.
|
||||
* @param properties must not be {@literal null}.
|
||||
* @param genericBackendProperties must not be {@literal null}.
|
||||
* @param backendAccessors must not be {@literal null}.
|
||||
* @param taskScheduler must not be {@literal null}.
|
||||
*/
|
||||
public LeasingVaultPropertySourceLocator(VaultConfigTemplate operations,
|
||||
VaultProperties properties,
|
||||
VaultGenericBackendProperties genericBackendProperties,
|
||||
Collection<SecureBackendAccessor> backendAccessors,
|
||||
TaskScheduler taskScheduler) {
|
||||
|
||||
super(operations, properties, genericBackendProperties, backendAccessors);
|
||||
|
||||
Assert.notNull(taskScheduler, "TaskScheduler must not be null");
|
||||
Assert.notNull(operations, "VaultConfigTemplate must not be null");
|
||||
Assert.notNull(properties, "VaultProperties must not be null");
|
||||
|
||||
this.operations = operations;
|
||||
this.properties = properties;
|
||||
this.taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VaultPropertySource createVaultPropertySource(
|
||||
SecureBackendAccessor accessor) {
|
||||
LeasingVaultPropertySource propertySource = new LeasingVaultPropertySource(
|
||||
this.operations, this.properties, accessor, taskScheduler);
|
||||
|
||||
locatedPropertySources.add(propertySource);
|
||||
|
||||
return propertySource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
Set<PropertySource<?>> propertySources = new HashSet<>(locatedPropertySources);
|
||||
|
||||
for (PropertySource<?> propertySource : propertySources) {
|
||||
|
||||
locatedPropertySources.remove(propertySource);
|
||||
|
||||
if (propertySource instanceof LeasingVaultPropertySource) {
|
||||
|
||||
try {
|
||||
((LeasingVaultPropertySource) propertySource).destroy();
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("Cannot destroy property source {}",
|
||||
propertySource.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2016 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.cloud.vault.config;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.vault.support.VaultResponseSupport;
|
||||
|
||||
/**
|
||||
* Value object that represents Vault secrets. {@link Secrets} contains metadata, lease
|
||||
* details and a map of secret data.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class Secrets extends VaultResponseSupport<Map<String, String>> {
|
||||
}
|
||||
@@ -20,15 +20,20 @@ import java.net.URI;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.core.task.AsyncTaskExecutor;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -45,6 +50,7 @@ import org.springframework.vault.authentication.IpAddressUserId;
|
||||
import org.springframework.vault.authentication.LifecycleAwareSessionManager;
|
||||
import org.springframework.vault.authentication.MacAddressUserId;
|
||||
import org.springframework.vault.authentication.SessionManager;
|
||||
import org.springframework.vault.authentication.SimpleSessionManager;
|
||||
import org.springframework.vault.authentication.StaticUserId;
|
||||
import org.springframework.vault.authentication.TokenAuthentication;
|
||||
import org.springframework.vault.client.VaultClient;
|
||||
@@ -71,33 +77,48 @@ import org.springframework.vault.support.VaultToken;
|
||||
VaultGenericBackendProperties.class })
|
||||
public class VaultBootstrapConfiguration {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private final ConfigurableApplicationContext applicationContext;
|
||||
|
||||
private final VaultProperties vaultProperties;
|
||||
|
||||
private final Collection<VaultSecretBackend> vaultSecretBackends;
|
||||
|
||||
private final Collection<SecureBackendAccessorFactory<? super VaultSecretBackend>> factories;
|
||||
|
||||
public VaultBootstrapConfiguration(ApplicationContext applicationContext,
|
||||
public VaultBootstrapConfiguration(ConfigurableApplicationContext applicationContext,
|
||||
VaultProperties vaultProperties) {
|
||||
|
||||
this.applicationContext = applicationContext;
|
||||
this.vaultProperties = vaultProperties;
|
||||
|
||||
this.vaultSecretBackends = applicationContext.getBeansOfType(
|
||||
VaultSecretBackend.class).values();
|
||||
this.factories = (Collection) applicationContext.getBeansOfType(
|
||||
SecureBackendAccessorFactory.class).values();
|
||||
this.vaultSecretBackends = applicationContext
|
||||
.getBeansOfType(VaultSecretBackend.class).values();
|
||||
this.factories = (Collection) applicationContext
|
||||
.getBeansOfType(SecureBackendAccessorFactory.class).values();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public VaultPropertySourceLocator vaultPropertySourceLocator(
|
||||
VaultOperations operations, VaultProperties vaultProperties,
|
||||
VaultGenericBackendProperties vaultGenericBackendProperties) {
|
||||
VaultGenericBackendProperties vaultGenericBackendProperties,
|
||||
ObjectProvider<TaskSchedulerWrapper<? extends TaskScheduler>> taskSchedulerProvider) {
|
||||
|
||||
Collection<SecureBackendAccessor> backendAccessors = SecureBackendFactories
|
||||
.createBackendAcessors(vaultSecretBackends, factories);
|
||||
VaultConfigTemplate vaultConfigTemplate = new VaultConfigTemplate(operations,
|
||||
vaultProperties);
|
||||
|
||||
if (vaultProperties.getConfig().getLifecycle().isEnabled()) {
|
||||
|
||||
// This is to destroy bootstrap resources
|
||||
// otherwise, the bootstrap context is not shut down cleanly
|
||||
applicationContext.registerShutdownHook();
|
||||
|
||||
return new LeasingVaultPropertySourceLocator(vaultConfigTemplate,
|
||||
vaultProperties, vaultGenericBackendProperties, backendAccessors,
|
||||
taskSchedulerProvider.getObject().getTaskScheduler());
|
||||
}
|
||||
|
||||
return new VaultPropertySourceLocator(vaultConfigTemplate, vaultProperties,
|
||||
vaultGenericBackendProperties, backendAccessors);
|
||||
}
|
||||
@@ -129,9 +150,9 @@ public class VaultBootstrapConfiguration {
|
||||
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException(String.format(
|
||||
"Client authentication %s not supported",
|
||||
vaultProperties.getAuthentication()));
|
||||
throw new UnsupportedOperationException(
|
||||
String.format("Client authentication %s not supported",
|
||||
vaultProperties.getAuthentication()));
|
||||
}
|
||||
|
||||
private ClientAuthentication appIdAuthentication(VaultProperties vaultProperties,
|
||||
@@ -167,8 +188,8 @@ public class VaultBootstrapConfiguration {
|
||||
|
||||
if (StringUtils.hasText(appId.getNetworkInterface())) {
|
||||
try {
|
||||
return new MacAddressUserId(Integer.parseInt(appId
|
||||
.getNetworkInterface()));
|
||||
return new MacAddressUserId(
|
||||
Integer.parseInt(appId.getNetworkInterface()));
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
return new MacAddressUserId(appId.getNetworkInterface());
|
||||
@@ -238,8 +259,8 @@ public class VaultBootstrapConfiguration {
|
||||
sslConfiguration = SslConfiguration.NONE;
|
||||
}
|
||||
|
||||
return new ClientFactoryWrapper(ClientHttpRequestFactoryFactory.create(
|
||||
clientOptions, sslConfiguration));
|
||||
return new ClientFactoryWrapper(
|
||||
ClientHttpRequestFactoryFactory.create(clientOptions, sslConfiguration));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,8 +276,9 @@ public class VaultBootstrapConfiguration {
|
||||
vaultEndpoint.setPort(vaultProperties.getPort());
|
||||
vaultEndpoint.setScheme(vaultProperties.getScheme());
|
||||
|
||||
return new VaultClient(clientHttpRequestFactoryWrapper()
|
||||
.getClientHttpRequestFactory(), vaultEndpoint);
|
||||
return new VaultClient(
|
||||
clientHttpRequestFactoryWrapper().getClientHttpRequestFactory(),
|
||||
vaultEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,25 +299,80 @@ public class VaultBootstrapConfiguration {
|
||||
*
|
||||
* @return
|
||||
* @see #vaultClientFactory()
|
||||
* @see #sessionManager(ClientAuthentication)
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public VaultTemplate vaultTemplate(ClientAuthentication clientAuthentication) {
|
||||
return new VaultTemplate(vaultClientFactory(),
|
||||
sessionManager(clientAuthentication));
|
||||
public VaultTemplate vaultTemplate(ClientAuthentication clientAuthentication,
|
||||
SessionManager sessionManager) {
|
||||
return new VaultTemplate(vaultClientFactory(), sessionManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link TaskSchedulerWrapper} that encapsulates a bean implementing
|
||||
* {@link TaskScheduler} and {@link AsyncTaskExecutor}.
|
||||
*
|
||||
* @return
|
||||
* @see ThreadPoolTaskScheduler
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(TaskSchedulerWrapper.class)
|
||||
public TaskSchedulerWrapper<ThreadPoolTaskScheduler> vaultTaskScheduler() {
|
||||
|
||||
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
|
||||
threadPoolTaskScheduler.setPoolSize(2);
|
||||
threadPoolTaskScheduler.setThreadNamePrefix("Spring-Cloud-Vault-");
|
||||
|
||||
return new TaskSchedulerWrapper<>(threadPoolTaskScheduler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link SessionManager} for Vault session management.
|
||||
* @see SessionManager
|
||||
* @see LifecycleAwareSessionManager
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SessionManager sessionManager(ClientAuthentication clientAuthentication) {
|
||||
return new LifecycleAwareSessionManager(clientAuthentication,
|
||||
new SimpleAsyncTaskExecutor("Spring-Cloud-Vault-"), vaultClient());
|
||||
public SessionManager sessionManager(ClientAuthentication clientAuthentication,
|
||||
ObjectProvider<TaskSchedulerWrapper<? extends AsyncTaskExecutor>> asyncTaskExecutorProvider) {
|
||||
|
||||
if (vaultProperties.getConfig().getLifecycle().isEnabled()) {
|
||||
return new LifecycleAwareSessionManager(clientAuthentication,
|
||||
asyncTaskExecutorProvider.getObject().getTaskScheduler(),
|
||||
vaultClient());
|
||||
}
|
||||
|
||||
return new SimpleSessionManager(clientAuthentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to keep {@link TaskScheduler} local to Spring Cloud Vault.
|
||||
* @param <T>
|
||||
*/
|
||||
public static class TaskSchedulerWrapper<T extends AsyncTaskExecutor & TaskScheduler>
|
||||
implements InitializingBean, DisposableBean {
|
||||
|
||||
private final T taskScheduler;
|
||||
|
||||
public TaskSchedulerWrapper(T taskScheduler) {
|
||||
this.taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
T getTaskScheduler() {
|
||||
return taskScheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
if (taskScheduler instanceof DisposableBean) {
|
||||
((DisposableBean) taskScheduler).destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
if (taskScheduler instanceof InitializingBean) {
|
||||
((InitializingBean) taskScheduler).afterPropertiesSet();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.cloud.vault.config;
|
||||
|
||||
import java.util.Map;
|
||||
import org.springframework.vault.core.VaultOperations;
|
||||
|
||||
/**
|
||||
* Interface that specified a basic set of Vault operations, implemented by
|
||||
@@ -26,7 +26,7 @@ import java.util.Map;
|
||||
public interface VaultConfigOperations {
|
||||
|
||||
/**
|
||||
* Read configuration from a secure backend encapsulated within a
|
||||
* Read secrets from a secret backend encapsulated within a
|
||||
* {@link SecureBackendAccessor}. Reading data using this method is suitable for
|
||||
* secret backends that do not require a request body.
|
||||
*
|
||||
@@ -34,5 +34,11 @@ public interface VaultConfigOperations {
|
||||
* @return the configuration data. May be empty but never {@literal null}.
|
||||
* @throws IllegalStateException if {@link VaultProperties#isFailFast()} is enabled.
|
||||
*/
|
||||
Map<String, String> read(SecureBackendAccessor secureBackendAccessor);
|
||||
Secrets read(SecureBackendAccessor secureBackendAccessor);
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the underlying {@link VaultOperations}.
|
||||
*/
|
||||
VaultOperations getVaultOperations();
|
||||
}
|
||||
|
||||
@@ -15,16 +15,11 @@
|
||||
*/
|
||||
package org.springframework.cloud.vault.config;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.vault.client.VaultResponseEntity;
|
||||
import org.springframework.vault.core.VaultOperations;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -56,18 +51,18 @@ public class VaultConfigTemplate implements VaultConfigOperations {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public Map<String, String> read(final SecureBackendAccessor secureBackendAccessor) {
|
||||
public Secrets read(final SecureBackendAccessor secureBackendAccessor) {
|
||||
|
||||
Assert.notNull(secureBackendAccessor, "SecureBackendAccessor must not be null!");
|
||||
|
||||
VaultResponseEntity<VaultResponse> response = vaultOperations.doWithVault(
|
||||
new VaultOperations.SessionCallback<VaultResponseEntity<VaultResponse>>() {
|
||||
VaultResponseEntity<Secrets> response = vaultOperations.doWithVault(
|
||||
new VaultOperations.SessionCallback<VaultResponseEntity<Secrets>>() {
|
||||
@Override
|
||||
public VaultResponseEntity<VaultResponse> doWithVault(
|
||||
public VaultResponseEntity<Secrets> doWithVault(
|
||||
VaultOperations.VaultSession session) {
|
||||
|
||||
return session.exchange("{backend}/{key}", HttpMethod.GET, null,
|
||||
VaultResponse.class, secureBackendAccessor.variables());
|
||||
Secrets.class, secureBackendAccessor.variables());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,9 +70,10 @@ public class VaultConfigTemplate implements VaultConfigOperations {
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK) {
|
||||
|
||||
Map<String, String> stringMap = toStringMap(response.getBody().getData());
|
||||
Secrets secrets = response.getBody();
|
||||
secrets.setData(secureBackendAccessor.transformProperties(secrets.getData()));
|
||||
|
||||
return secureBackendAccessor.transformProperties(stringMap);
|
||||
return secrets;
|
||||
}
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||
@@ -94,20 +90,10 @@ public class VaultConfigTemplate implements VaultConfigOperations {
|
||||
response.getStatusCode().value(), response.getMessage()));
|
||||
}
|
||||
|
||||
return Collections.emptyMap();
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, String> toStringMap(Map<String, Object> data) {
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (String key : data.keySet()) {
|
||||
Object value = data.get(key);
|
||||
|
||||
if (value != null) {
|
||||
result.put(key, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
public VaultOperations getVaultOperations() {
|
||||
return vaultOperations;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.cloud.vault.config;
|
||||
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import org.hibernate.validator.constraints.Range;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
@@ -190,6 +191,21 @@ public class VaultProperties {
|
||||
* @see org.springframework.core.PriorityOrdered
|
||||
*/
|
||||
private int order = 0;
|
||||
|
||||
private Lifecycle lifecycle = new Lifecycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration to Vault lifecycle management (renewal, revocation of tokens and
|
||||
* secrets).
|
||||
*/
|
||||
@Data
|
||||
public static class Lifecycle {
|
||||
|
||||
/**
|
||||
* Enable lifecycle management.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
}
|
||||
|
||||
public enum AuthenticationMethod {
|
||||
|
||||
@@ -36,6 +36,7 @@ class VaultPropertySource extends EnumerablePropertySource<VaultConfigTemplate>
|
||||
private final VaultProperties vaultProperties;
|
||||
private final SecureBackendAccessor secureBackendAccessor;
|
||||
private final Map<String, String> properties = new LinkedHashMap<>();
|
||||
private Secrets secrets;
|
||||
|
||||
/**
|
||||
* Creates a new {@link VaultPropertySource}.
|
||||
@@ -63,9 +64,9 @@ class VaultPropertySource extends EnumerablePropertySource<VaultConfigTemplate>
|
||||
public void init() {
|
||||
|
||||
try {
|
||||
Map<String, String> values = this.source.read(this.secureBackendAccessor);
|
||||
if (values != null) {
|
||||
this.properties.putAll(values);
|
||||
this.secrets = this.source.read(this.secureBackendAccessor);
|
||||
if (this.secrets != null) {
|
||||
this.properties.putAll(secrets.getData());
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
@@ -85,6 +86,10 @@ class VaultPropertySource extends EnumerablePropertySource<VaultConfigTemplate>
|
||||
}
|
||||
}
|
||||
|
||||
Secrets getSecrets() {
|
||||
return secrets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(String name) {
|
||||
return this.properties.get(name);
|
||||
|
||||
@@ -116,10 +116,10 @@ class VaultPropertySourceLocator implements PropertySourceLocator, PriorityOrder
|
||||
return contexts;
|
||||
}
|
||||
|
||||
protected CompositePropertySource createCompositePropertySource(
|
||||
private CompositePropertySource createCompositePropertySource(
|
||||
ConfigurableEnvironment environment) {
|
||||
|
||||
CompositePropertySource propertySource = new CompositePropertySource("vault");
|
||||
List<PropertySource<?>> propertySources = new ArrayList<>();
|
||||
|
||||
if (genericBackendProperties.isEnabled()) {
|
||||
|
||||
@@ -132,7 +132,7 @@ class VaultPropertySourceLocator implements PropertySourceLocator, PriorityOrder
|
||||
generic(genericBackendProperties.getBackend(),
|
||||
propertySourceContext));
|
||||
|
||||
propertySource.addPropertySource(vaultPropertySource);
|
||||
propertySources.add(vaultPropertySource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,11 +141,41 @@ class VaultPropertySourceLocator implements PropertySourceLocator, PriorityOrder
|
||||
|
||||
VaultPropertySource vaultPropertySource = createVaultPropertySource(
|
||||
backendAccessor);
|
||||
propertySource.addPropertySource(vaultPropertySource);
|
||||
propertySources.add(vaultPropertySource);
|
||||
}
|
||||
return propertySource;
|
||||
|
||||
return doCreateCompositePropertySource(propertySources);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Implementation hooks and helper methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a {@link CompositePropertySource} given a {@link List} of
|
||||
* {@link PropertySource}s.
|
||||
*
|
||||
* @param propertySources the property sources.
|
||||
* @return the {@link CompositePropertySource} to use.
|
||||
*/
|
||||
protected CompositePropertySource doCreateCompositePropertySource(
|
||||
List<PropertySource<?>> propertySources) {
|
||||
|
||||
CompositePropertySource compositePropertySource = new CompositePropertySource(
|
||||
"vault");
|
||||
|
||||
for (PropertySource<?> propertySource : propertySources) {
|
||||
compositePropertySource.addPropertySource(propertySource);
|
||||
}
|
||||
|
||||
return compositePropertySource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize nested {@link PropertySource}s inside the
|
||||
* {@link CompositePropertySource}.
|
||||
* @param propertySource the {@link CompositePropertySource} to initialize.
|
||||
*/
|
||||
protected void initialize(CompositePropertySource propertySource) {
|
||||
|
||||
for (PropertySource<?> source : propertySource.getPropertySources()) {
|
||||
@@ -153,7 +183,14 @@ class VaultPropertySourceLocator implements PropertySourceLocator, PriorityOrder
|
||||
}
|
||||
}
|
||||
|
||||
private VaultPropertySource createVaultPropertySource(
|
||||
/**
|
||||
* Create {@link VaultPropertySource} initialized with a
|
||||
* {@link SecureBackendAccessor}.
|
||||
*
|
||||
* @param accessor the {@link SecureBackendAccessor}.
|
||||
* @return the {@link VaultPropertySource} to use.
|
||||
*/
|
||||
protected VaultPropertySource createVaultPropertySource(
|
||||
SecureBackendAccessor accessor) {
|
||||
return new VaultPropertySource(this.operations, this.properties, accessor);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.cloud.vault.util.IntegrationTestSupport;
|
||||
import org.springframework.cloud.vault.util.Settings;
|
||||
|
||||
@@ -50,7 +51,7 @@ public class GenericSecretIntegrationTests extends IntegrationTestSupport {
|
||||
public void shouldReturnSecretsCorrectly() throws Exception {
|
||||
|
||||
Map<String, String> secretProperties = configOperations
|
||||
.read(generic("secret", "app-name"));
|
||||
.read(generic("secret", "app-name")).getData();
|
||||
|
||||
assertThat(secretProperties).containsAllEntriesOf(createExpectedMap());
|
||||
}
|
||||
@@ -58,10 +59,9 @@ public class GenericSecretIntegrationTests extends IntegrationTestSupport {
|
||||
@Test
|
||||
public void shouldReturnNullIfNotFound() throws Exception {
|
||||
|
||||
Map<String, String> secretProperties = configOperations
|
||||
.read(generic("secret", "missing"));
|
||||
Secrets secrets = configOperations.read(generic("secret", "missing"));
|
||||
|
||||
assertThat(secretProperties).isEmpty();
|
||||
assertThat(secrets).isNull();
|
||||
}
|
||||
|
||||
private Map<String, Object> createData() {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2016 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.cloud.vault.config;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.core.env.CompositePropertySource;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link LeasingVaultPropertySourceLocator}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class LeasingVaultPropertySourceLocatorUnitTests {
|
||||
|
||||
private LeasingVaultPropertySourceLocator propertySourceLocator;
|
||||
|
||||
@Mock
|
||||
private VaultConfigTemplate operations;
|
||||
|
||||
@Mock
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
@Mock
|
||||
private ConfigurableEnvironment configurableEnvironment;
|
||||
|
||||
@Mock
|
||||
private LeasingVaultPropertySource leasingVaultPropertySource;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
|
||||
propertySourceLocator = new LeasingVaultPropertySourceLocator(operations,
|
||||
new VaultProperties(), new VaultGenericBackendProperties(),
|
||||
Collections.<SecureBackendAccessor> emptyList(), taskScheduler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOrderShouldReturnConfiguredOrder() {
|
||||
|
||||
VaultProperties vaultProperties = new VaultProperties();
|
||||
vaultProperties.getConfig().setOrder(10);
|
||||
|
||||
propertySourceLocator = new LeasingVaultPropertySourceLocator(operations,
|
||||
vaultProperties, new VaultGenericBackendProperties(),
|
||||
Collections.<SecureBackendAccessor> emptyList(), taskScheduler);
|
||||
|
||||
assertThat(propertySourceLocator.getOrder()).isEqualTo(10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldLocatePropertySources() {
|
||||
|
||||
when(configurableEnvironment.getActiveProfiles()).thenReturn(new String[0]);
|
||||
|
||||
PropertySource<?> propertySource = propertySourceLocator
|
||||
.locate(configurableEnvironment);
|
||||
|
||||
assertThat(propertySource).isInstanceOf(CompositePropertySource.class);
|
||||
|
||||
CompositePropertySource composite = (CompositePropertySource) propertySource;
|
||||
assertThat(composite.getPropertySources()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldDispose() {
|
||||
|
||||
Set set = (Set) ReflectionTestUtils.getField(propertySourceLocator,
|
||||
"locatedPropertySources");
|
||||
set.add(leasingVaultPropertySource);
|
||||
|
||||
propertySourceLocator.destroy();
|
||||
|
||||
verify(leasingVaultPropertySource).destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright 2016 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.cloud.vault.config;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
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.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.Trigger;
|
||||
import org.springframework.vault.client.VaultResponseEntity;
|
||||
import org.springframework.vault.core.VaultOperations;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link LeasingVaultPropertySource}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class LeasingVaultPropertySourceUnitTests {
|
||||
|
||||
@Mock
|
||||
private VaultConfigTemplate configOperations;
|
||||
|
||||
@Mock
|
||||
private VaultOperations vaultOperations;
|
||||
|
||||
@Mock
|
||||
private SecureBackendAccessor secureBackendAccessor;
|
||||
|
||||
@Mock
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
@Mock
|
||||
private ScheduledFuture scheduledFuture;
|
||||
|
||||
private LeasingVaultPropertySource propertySource;
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
|
||||
when(secureBackendAccessor.getName()).thenReturn("test");
|
||||
when(configOperations.getVaultOperations()).thenReturn(vaultOperations);
|
||||
|
||||
propertySource = new LeasingVaultPropertySource(configOperations,
|
||||
new VaultProperties(), secureBackendAccessor, taskScheduler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldWorkIfSecretsNotFound() {
|
||||
|
||||
propertySource.init();
|
||||
|
||||
assertThat(propertySource.getPropertyNames()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAcceptSecretsWithoutLease() {
|
||||
|
||||
Secrets secrets = new Secrets();
|
||||
secrets.setData(Collections.singletonMap("key", "value"));
|
||||
|
||||
when(configOperations.read(secureBackendAccessor)).thenReturn(secrets);
|
||||
|
||||
propertySource.init();
|
||||
|
||||
assertThat(propertySource.getPropertyNames()).contains("key");
|
||||
verifyZeroInteractions(taskScheduler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAcceptSecretsWithStaticLease() {
|
||||
|
||||
Secrets secrets = new Secrets();
|
||||
secrets.setLeaseId("lease");
|
||||
secrets.setRenewable(false);
|
||||
secrets.setData(Collections.singletonMap("key", "value"));
|
||||
|
||||
when(configOperations.read(secureBackendAccessor)).thenReturn(secrets);
|
||||
|
||||
propertySource.init();
|
||||
|
||||
verifyZeroInteractions(taskScheduler);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldAcceptSecretsWithRenewableLease() {
|
||||
|
||||
when(taskScheduler.schedule(any(Runnable.class), any(Trigger.class)))
|
||||
.thenReturn(scheduledFuture);
|
||||
when(configOperations.read(secureBackendAccessor)).thenReturn(createSecrets());
|
||||
|
||||
propertySource.init();
|
||||
|
||||
verify(taskScheduler).schedule(any(Runnable.class), any(Trigger.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldRenewLease() {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
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
|
||||
@SuppressWarnings("unchecked")
|
||||
public void scheduleRenewalShouldApplyExpiryThreshold() {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
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
|
||||
@SuppressWarnings("unchecked")
|
||||
public void subsequentScheduleRenewalShouldApplyExpiryThreshold() {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
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
|
||||
@SuppressWarnings("unchecked")
|
||||
public void scheduleRenewalShouldTriggerOnlyOnce() {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
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
|
||||
@SuppressWarnings("unchecked")
|
||||
public void subsequentInitShouldCancelExistingSchedule() {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(taskScheduler).schedule(captor.capture(), any(Trigger.class));
|
||||
|
||||
propertySource.init();
|
||||
|
||||
verify(scheduledFuture).cancel(false);
|
||||
verify(taskScheduler, times(2)).schedule(captor.capture(), any(Trigger.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void canceledRenewalShouldSkipRenewal() {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(taskScheduler).schedule(captor.capture(), any(Trigger.class));
|
||||
|
||||
propertySource.init();
|
||||
verify(scheduledFuture).cancel(false);
|
||||
|
||||
captor.getValue().run();
|
||||
|
||||
verifyZeroInteractions(vaultOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDisableRenewalOnDisposal() {
|
||||
|
||||
prepareRenewal();
|
||||
|
||||
propertySource.destroy();
|
||||
|
||||
verify(vaultOperations).doWithVault(any(VaultOperations.SessionCallback.class));
|
||||
verify(scheduledFuture).cancel(false);
|
||||
}
|
||||
|
||||
private void prepareRenewal() {
|
||||
|
||||
when(taskScheduler.schedule(any(Runnable.class), any(Trigger.class)))
|
||||
.thenReturn(scheduledFuture);
|
||||
when(configOperations.read(secureBackendAccessor)).thenReturn(createSecrets());
|
||||
propertySource.init();
|
||||
when(vaultOperations.doWithVault(any(VaultOperations.SessionCallback.class)))
|
||||
.thenReturn(getResponseEntity("new_lease", true, 70, HttpStatus.OK));
|
||||
}
|
||||
|
||||
private VaultResponseEntity<Map<String, Object>> getResponseEntity(String leaseId,
|
||||
Boolean renewable, Integer leaseDuration, HttpStatus httpStatus) {
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("lease_id", leaseId);
|
||||
body.put("renewable", renewable);
|
||||
body.put("lease_duration", leaseDuration);
|
||||
|
||||
return getEntity(body, httpStatus);
|
||||
}
|
||||
|
||||
private VaultResponseEntity<Map<String, Object>> getEntity(Map<String, Object> body,
|
||||
HttpStatus status) {
|
||||
|
||||
return new VaultResponseEntity<Map<String, Object>>(body, status, null, null) {
|
||||
};
|
||||
}
|
||||
|
||||
private Secrets createSecrets() {
|
||||
|
||||
Secrets secrets = new Secrets();
|
||||
|
||||
secrets.setLeaseId("lease");
|
||||
secrets.setRenewable(true);
|
||||
secrets.setLeaseDuration(100);
|
||||
secrets.setData(Collections.singletonMap("key", "value"));
|
||||
|
||||
return secrets;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user