package io.vertx.config.vault;
import io.vertx.config.spi.ConfigStore;
import io.vertx.config.vault.client.*;
import io.vertx.core.*;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import java.util.Objects;
public class VaultConfigStore implements ConfigStore {
private final SlimVaultClient client;
private final JsonObject config;
private final String path;
private final Vertx vertx;
private boolean renewable;
private long validity;
private Context context;
public VaultConfigStore(Vertx vertx, JsonObject config) {
client = new SlimVaultClient(vertx, config);
this.config = config;
this.vertx = vertx;
this.path = Objects.requireNonNull(config.getString("path"), "The path of the secret must be set");
}
@Override
public void close(Handler<Void> completionHandler) {
client.close();
completionHandler.handle(null);
}
@Override
public void get(Handler<AsyncResult<Buffer>> completionHandler) {
if (context == null) {
context = vertx.getOrCreateContext();
}
Handler<Void> actions = x -> {
authenticate(false)
.compose(v -> renew())
.compose(v -> retrieve())
.compose(this::extract)
.setHandler(completionHandler);
};
if (Vertx.currentContext() == context) {
actions.handle(null);
} else {
context.runOnContext(actions);
}
}
private Future<Buffer> (JsonObject json) {
Promise<Buffer> promise = Promise.promise();
if (json == null) {
promise.complete(new JsonObject().toBuffer());
} else if (config.getString("key") != null) {
Object value = json.getValue(config.getString("key"));
if (value == null) {
promise.complete(new JsonObject().toBuffer());
} else if (value instanceof String) {
promise.complete(Buffer.buffer((String) value));
} else if (value instanceof JsonObject) {
promise.complete(((JsonObject) value).toBuffer());
}
} else {
promise.complete(json.toBuffer());
}
return promise.future();
}
private Future<JsonObject> retrieve() {
Promise<JsonObject> promise = Promise.promise();
client.read(path, ar -> {
if (ar.failed() && !(ar.cause() instanceof VaultException)) {
promise.fail(ar.cause());
} else if (ar.failed() && ((VaultException) ar.cause()).getStatusCode() == 404) {
promise.complete(null);
} else if (ar.failed()) {
promise.fail(ar.cause());
} else {
Secret result = ar.result();
JsonObject copy = result.getData().copy();
copy.put("vault-lease-id", result.getLeaseId());
copy.put("vault-lease-duration", result.getLeaseDuration());
copy.put("vault-renewable", result.isRenewable());
promise.complete(copy);
}
});
return promise.future();
}
private Future<Void> renew() {
Promise<Void> promise = Promise.promise();
if (validity == 0) {
promise.complete();
} else {
if (shouldBeRenewed() && renewable) {
return renewToken();
} else if (shouldBeRenewed()) {
return authenticate(true);
} else {
promise.complete();
}
}
return promise.future();
}
private Future<Void> renewToken() {
Promise<Void> promise = Promise.promise();
client.renewSelf(config.getLong("lease-duration", 3600L), auth -> {
manageAuthenticationResult(promise, auth);
});
return promise.future();
}
private Future<Void> authenticate(boolean renew) {
Promise<Void> promise = Promise.promise();
if (!renew && client.getToken() != null) {
promise.complete();
return promise.future();
}
String policy = config.getString("auth-backend");
Objects.requireNonNull(policy, "If you don't provide a token, the auth-backend must be set");
switch (policy.toLowerCase()) {
case "token":
return loginWithToken();
case "approle":
return loginWithAppRole();
case "cert":
return loginWithCert();
case "userpass":
return loginWithUserName();
default:
throw new IllegalArgumentException("Non supported auth-backend: " + policy);
}
}
private Future<Void> loginWithUserName() {
Promise<Void> promise = Promise.promise();
JsonObject req = config.getJsonObject("user-credentials");
Objects.requireNonNull(req, "When using username, the `user-credentials` must be set in the " +
"configuration");
String username = req.getString("username");
String password = req.getString("password");
String token = req.getString("token");
Objects.requireNonNull(username, "When using userpass, the username must be set in the `user-credentials` " +
"configuration");
Objects.requireNonNull(password, "When using userpass, the password must be set in the `user-credentials` " +
"configuration");
client
.loginWithUserCredentials(username, password, auth -> manageAuthenticationResult(promise, auth));
return promise.future();
}
private Future<Void> loginWithCert() {
Promise<Void> promise = Promise.promise();
client.loginWithCert(auth -> manageAuthenticationResult(promise, auth));
return promise.future();
}
private Future<Void> loginWithAppRole() {
Promise<Void> promise = Promise.promise();
JsonObject req = config.getJsonObject("approle");
Objects.requireNonNull(req, "When using approle, the `app-role` must be set in the " +
"configuration");
String roleId = req.getString("role-id");
String secretId = req.getString("secret-id");
Objects.requireNonNull(roleId, "When using approle, the role-id must be set in the `approle` configuration");
Objects.requireNonNull(secretId, "When using approle, the secret-id must be set in the `approle` configuration");
client.loginWithAppRole(roleId, secretId, auth -> manageAuthenticationResult(promise, auth));
return promise.future();
}
private Future<Void> loginWithToken() {
Promise<Void> promise = Promise.promise();
JsonObject req = config.getJsonObject("token-request");
Objects.requireNonNull(req, "When using a token creation policy, the `token-request` must be set in the " +
"configuration");
String token = req.getString("token");
Objects.requireNonNull(req, "When using a token creation policy, the `token-request` must be set in the " +
"configuration and contains the `token` entry with the original token");
client
.setToken(token)
.createToken(new TokenRequest(req), auth -> manageAuthenticationResult(promise, auth));
return promise.future();
}
private void manageAuthenticationResult(Promise<Void> future, AsyncResult<Auth> auth) {
if (auth.failed()) {
future.fail(auth.cause());
} else {
Auth authentication = auth.result();
if (authentication.getToken() == null) {
future.fail("Authentication failed, the token is null");
} else {
client.setToken(authentication.getToken());
this.renewable = authentication.isRenewable();
this.validity = System.currentTimeMillis() + (authentication.getLeaseDuration() * 1000);
future.complete();
}
}
}
private boolean shouldBeRenewed() {
long now = System.currentTimeMillis();
long margin = config.getLong("renew-window", 60000L);
return now >= validity - margin;
}
public SlimVaultClient getVaultClient() {
return client;
}
}