From de99c8f2d069ffc9ac8f54d1bc21b5505e3fa404 Mon Sep 17 00:00:00 2001 From: Filipe Roque Date: Thu, 7 Nov 2024 11:33:59 +0000 Subject: [PATCH] Replace usage of expire time for cache expiration check Use leases to check credential validity. Due to Vault tokens hierarchy, a credential or token may be revoked even if the expire time has not been reached, because a parent token has reached the expire time or being manually revoked and thus revokes all children. The only way to work with this is to check the associated lease each time. > When a parent token is revoked, all of its child tokens -- and all of their leases -- are revoked as well. https://developer.hashicorp.com/vault/docs/concepts/tokens#token-hierarchies-and-orphan-tokens --- .../datagrip/vault/DynamicSecretValue.java | 26 ----- .../datagrip/vault/LeaseRequest.java | 14 +++ .../datagrip/vault/LeaseResponse.java | 107 ++++++++++++++++++ .../vault/VaultDatabaseAuthProvider.java | 59 ++++++++-- 4 files changed, 172 insertions(+), 34 deletions(-) delete mode 100644 src/main/java/com/premiumminds/datagrip/vault/DynamicSecretValue.java create mode 100644 src/main/java/com/premiumminds/datagrip/vault/LeaseRequest.java create mode 100644 src/main/java/com/premiumminds/datagrip/vault/LeaseResponse.java diff --git a/src/main/java/com/premiumminds/datagrip/vault/DynamicSecretValue.java b/src/main/java/com/premiumminds/datagrip/vault/DynamicSecretValue.java deleted file mode 100644 index ceea1a6..0000000 --- a/src/main/java/com/premiumminds/datagrip/vault/DynamicSecretValue.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.premiumminds.datagrip.vault; - -import java.time.Instant; - -public class DynamicSecretValue { - - private final Instant issueTime; - private final DynamicSecretResponse response; - - public DynamicSecretValue(Instant issueTime, DynamicSecretResponse response) { - this.issueTime = issueTime; - this.response = response; - } - - public Instant getIssueTime() { - return issueTime; - } - - public DynamicSecretResponse getResponse() { - return response; - } - - public Instant getExpireTime() { - return issueTime.plusSeconds(response.getLeaseDuration()); - } -} diff --git a/src/main/java/com/premiumminds/datagrip/vault/LeaseRequest.java b/src/main/java/com/premiumminds/datagrip/vault/LeaseRequest.java new file mode 100644 index 0000000..a617dcc --- /dev/null +++ b/src/main/java/com/premiumminds/datagrip/vault/LeaseRequest.java @@ -0,0 +1,14 @@ +package com.premiumminds.datagrip.vault; + +public class LeaseRequest { + + private String leaseId; + + public String getLeaseId() { + return leaseId; + } + + public void setLeaseId(String leaseId) { + this.leaseId = leaseId; + } +} diff --git a/src/main/java/com/premiumminds/datagrip/vault/LeaseResponse.java b/src/main/java/com/premiumminds/datagrip/vault/LeaseResponse.java new file mode 100644 index 0000000..871aca1 --- /dev/null +++ b/src/main/java/com/premiumminds/datagrip/vault/LeaseResponse.java @@ -0,0 +1,107 @@ +package com.premiumminds.datagrip.vault; + +public class LeaseResponse { + + public static class Data { + private String expireTime; + private String id; + private String issueTime; + private String lastRenewal; + private Boolean renewable; + private Long ttl; + + public String getExpireTime() { + return expireTime; + } + + public void setExpireTime(String expireTime) { + this.expireTime = expireTime; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getIssueTime() { + return issueTime; + } + + public void setIssueTime(String issueTime) { + this.issueTime = issueTime; + } + + public String getLastRenewal() { + return lastRenewal; + } + + public void setLastRenewal(String lastRenewal) { + this.lastRenewal = lastRenewal; + } + + public Boolean getRenewable() { + return renewable; + } + + public void setRenewable(Boolean renewable) { + this.renewable = renewable; + } + + public Long getTtl() { + return ttl; + } + + public void setTtl(Long ttl) { + this.ttl = ttl; + } + } + + private String requestId; + private String leaseId; + private Boolean renewable; + private Long leaseDuration; + private Data data; + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public String getLeaseId() { + return leaseId; + } + + public void setLeaseId(String leaseId) { + this.leaseId = leaseId; + } + + public Boolean getRenewable() { + return renewable; + } + + public void setRenewable(Boolean renewable) { + this.renewable = renewable; + } + + public Long getLeaseDuration() { + return leaseDuration; + } + + public void setLeaseDuration(Long leaseDuration) { + this.leaseDuration = leaseDuration; + } + + public Data getData() { + return data; + } + + public void setData(Data data) { + this.data = data; + } +} diff --git a/src/main/java/com/premiumminds/datagrip/vault/VaultDatabaseAuthProvider.java b/src/main/java/com/premiumminds/datagrip/vault/VaultDatabaseAuthProvider.java index d084a1d..0a65f48 100644 --- a/src/main/java/com/premiumminds/datagrip/vault/VaultDatabaseAuthProvider.java +++ b/src/main/java/com/premiumminds/datagrip/vault/VaultDatabaseAuthProvider.java @@ -14,8 +14,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; -import java.time.Instant; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ConcurrentHashMap; @@ -57,7 +57,7 @@ public class VaultDatabaseAuthProvider implements DatabaseAuthProvider { .connectTimeout(Duration.ofSeconds(10)) .build(); - private static final Map secretsCache = new ConcurrentHashMap<>(); + private static final Map secretsCache = new ConcurrentHashMap<>(); @Override public @NonNls @NotNull String getId() { @@ -85,18 +85,27 @@ public boolean isApplicable(@NotNull LocalDataSource dataSource, @NotNull Applic logger.info("Secret used: " + secret); DynamicSecretKey key = new DynamicSecretKey(address, secret); - DynamicSecretValue value = secretsCache.get(key); + DynamicSecretResponse value = secretsCache.get(key); - if (value == null || value.getExpireTime().isBefore(Instant.now())) { + if (value == null){ final var response = getCredentialsFromVault(protoConnection, address, secret); - value = new DynamicSecretValue(Instant.now(), response); + value = response; secretsCache.put(key, value); + } else { + final var lease = getLeaseFromVault(protoConnection, address, value.getLeaseId()); + if (!lease.isPresent()) { + + final var response = getCredentialsFromVault(protoConnection, address, secret); + + value = response; + secretsCache.put(key, value); + } } - logger.info("Username used " + value.getResponse().getData().getUsername()); + logger.info("Username used " + value.getData().getUsername()); - protoConnection.getConnectionProperties().put("user", value.getResponse().getData().getUsername()); - protoConnection.getConnectionProperties().put("password", value.getResponse().getData().getPassword()); + protoConnection.getConnectionProperties().put("user", value.getData().getUsername()); + protoConnection.getConnectionProperties().put("password", value.getData().getPassword()); } catch (IOException | InterruptedException e) { throw new RuntimeException("Problem connecting to Vault: " + e.getMessage(), e); @@ -110,6 +119,40 @@ public boolean isApplicable(@NotNull LocalDataSource dataSource, @NotNull Applic return new VaultWidget(); } + + private Optional getLeaseFromVault( + ProtoConnection protoConnection, + final String address, + final String leaseId) + throws IOException, InterruptedException + { + final var token = getToken(protoConnection, address); + + final var uri = URI.create(address).resolve("/v1/sys/leases/lookup"); + + final var gson = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create(); + + final var leaseRequest = new LeaseRequest(); + leaseRequest.setLeaseId(leaseId); + + final var request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(leaseRequest))) + .header("X-Vault-Token", token) + .uri(uri) + .build(); + + final var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != HttpURLConnection.HTTP_OK) { + logger.info("No lease found for " + leaseId); + return Optional.empty(); + } + + return Optional.of(gson.fromJson(response.body(), LeaseResponse.class)); + } + private DynamicSecretResponse getCredentialsFromVault( ProtoConnection protoConnection, final String address,