diff --git a/src/main/java/com/bettercloud/vault/Vault.java b/src/main/java/com/bettercloud/vault/Vault.java index f0b3bd96..0663ae9a 100644 --- a/src/main/java/com/bettercloud/vault/Vault.java +++ b/src/main/java/com/bettercloud/vault/Vault.java @@ -5,6 +5,7 @@ import com.bettercloud.vault.api.Leases; import com.bettercloud.vault.api.Logical; import com.bettercloud.vault.api.Seal; +import com.bettercloud.vault.api.database.Database; import com.bettercloud.vault.api.mounts.Mounts; import com.bettercloud.vault.api.pki.Pki; import com.bettercloud.vault.json.Json; @@ -192,6 +193,10 @@ public Pki pki(final String mountPath) { return new Pki(vaultConfig, mountPath); } + public Database database() { return new Database(vaultConfig); } + + public Database database(final String mountPath) { return new Database(vaultConfig, mountPath); } + /** * Returns the implementing class for Vault's lease operations (e.g. revoke, revoke-prefix). * diff --git a/src/main/java/com/bettercloud/vault/api/database/Database.java b/src/main/java/com/bettercloud/vault/api/database/Database.java new file mode 100644 index 00000000..0fd12260 --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/database/Database.java @@ -0,0 +1,414 @@ +package com.bettercloud.vault.api.database; + +import com.bettercloud.vault.VaultConfig; +import com.bettercloud.vault.VaultException; +import com.bettercloud.vault.json.Json; +import com.bettercloud.vault.json.JsonObject; +import com.bettercloud.vault.response.DatabaseResponse; +import com.bettercloud.vault.rest.Rest; +import com.bettercloud.vault.rest.RestResponse; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + *
The implementing class for operations on Vault's database backend.
+ * + *This class is not intended to be constructed directly. Rather, it is meant to used by way of Vault
+ * in a DSL-style builder pattern. See the Javadoc comments of each public method for usage examples.
/v1/database).
+ *
+ * @param config A container for the configuration settings needed to initialize a Vault driver instance
+ */
+ public Database(final VaultConfig config) {
+ this(config, "database");
+ }
+
+ /**
+ * Constructor for use when the Database backend is mounted on some non-default custom path (e.g. /v1/db123).
+ *
+ * @param config A container for the configuration settings needed to initialize a Vault driver instance
+ * @param mountPath The path on which your Vault Database backend is mounted, without the /v1/ prefix (e.g. "root-ca")
+ */
+ public Database(final VaultConfig config, final String mountPath) {
+ this.config = config;
+ this.mountPath = mountPath;
+ if (this.config.getNameSpace() != null && !this.config.getNameSpace().isEmpty()) {
+ this.nameSpace = this.config.getNameSpace();
+ }
+ }
+
+ /**
+ * Operation to create or update an role using the Database Secret engine.
+ * Relies on an authentication token being present in the VaultConfig instance.
This version of the method accepts a DatabaseRoleOptions parameter, containing optional settings
+ * for the role creation operation. Example usage:
+ * {@code
+ * final VaultConfig config = new VaultConfig.address(...).token(...).build();
+ * final Vault vault = new Vault(config);
+ *
+ * final DatabaseRoleOptions options = new DatabaseRoleOptions()
+ * .dbName("test")
+ * .maxTtl("9h");
+ * final DatabaseResponse response = vault.database().createOrUpdateRole("testRole", options);
+ *
+ * assertEquals(204, response.getRestResponse().getStatus());
+ * }
+ *
+ *
+ * @param roleName A name for the role to be created or updated
+ * @param options Optional settings for the role to be created or updated (e.g. db_name, ttl, etc)
+ * @return A container for the information returned by Vault
+ * @throws VaultException If any error occurs or unexpected response is received from Vault
+ */
+ public DatabaseResponse createOrUpdateRole(final String roleName, final DatabaseRoleOptions options) throws VaultException {
+ int retryCount = 0;
+ while (true) {
+ try {
+ final String requestJson = roleOptionsToJson(options);
+
+ final RestResponse restResponse = new Rest()//NOPMD
+ .url(String.format("%s/v1/%s/roles/%s", config.getAddress(), this.mountPath, roleName))
+ .header("X-Vault-Token", config.getToken())
+ .optionalHeader("X-Vault-Namespace", this.nameSpace)
+ .body(requestJson.getBytes(StandardCharsets.UTF_8))
+ .connectTimeoutSeconds(config.getOpenTimeout())
+ .readTimeoutSeconds(config.getReadTimeout())
+ .sslVerification(config.getSslConfig().isVerify())
+ .sslContext(config.getSslConfig().getSslContext())
+ .post();
+
+ // Validate restResponse
+ if (restResponse.getStatus() != 204) {
+ throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus());
+ }
+ return new DatabaseResponse(restResponse, retryCount);
+ } catch (Exception e) {
+ // If there are retries to perform, then pause for the configured interval and then execute the loop again...
+ if (retryCount < config.getMaxRetries()) {
+ retryCount++;
+ try {
+ final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
+ Thread.sleep(retryIntervalMilliseconds);
+ } catch (InterruptedException e1) {
+ e1.printStackTrace();
+ }
+ } else if (e instanceof VaultException) {
+ // ... otherwise, give up.
+ throw (VaultException) e;
+ } else {
+ throw new VaultException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Operation to retrieve an role using the Database backend. Relies on an authentication token being present in
+ * the VaultConfig instance.
The role information will be populated in the roleOptions field of the DatabaseResponse
+ * return value. Example usage:
+ * {@code
+ * final VaultConfig config = new VaultConfig.address(...).token(...).build();
+ * final Vault vault = new Vault(config);
+ * final DatabaseResponse response = vault.database().getRole("testRole");
+ *
+ * final RoleOptions details = response.getRoleOptions();
+ * }
+ *
+ *
+ * @param roleName The name of the role to retrieve
+ * @return A container for the information returned by Vault
+ * @throws VaultException If any error occurs or unexpected response is received from Vault
+ */
+ public DatabaseResponse getRole(final String roleName) throws VaultException {
+ int retryCount = 0;
+ while (true) {
+ // Make an HTTP request to Vault
+ try {
+ final RestResponse restResponse = new Rest()//NOPMD
+ .url(String.format("%s/v1/%s/roles/%s", config.getAddress(), this.mountPath, roleName))
+ .header("X-Vault-Token", config.getToken())
+ .optionalHeader("X-Vault-Namespace", this.nameSpace)
+ .connectTimeoutSeconds(config.getOpenTimeout())
+ .readTimeoutSeconds(config.getReadTimeout())
+ .sslVerification(config.getSslConfig().isVerify())
+ .sslContext(config.getSslConfig().getSslContext())
+ .get();
+
+ // Validate response
+ if (restResponse.getStatus() != 200 && restResponse.getStatus() != 404) {
+ throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus());
+ }
+ return new DatabaseResponse(restResponse, retryCount);
+ } catch (Exception e) {
+ // If there are retries to perform, then pause for the configured interval and then execute the loop again...
+ if (retryCount < config.getMaxRetries()) {
+ retryCount++;
+ try {
+ final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
+ Thread.sleep(retryIntervalMilliseconds);
+ } catch (InterruptedException e1) {
+ e1.printStackTrace();
+ }
+ } else if (e instanceof VaultException) {
+ // ... otherwise, give up.
+ throw (VaultException) e;
+ } else {
+ throw new VaultException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Operation to revike a certificate in the vault using the Database backend.
+ * Relies on an authentication token being present in
+ * the VaultConfig instance.
A successful operation will return a 204 HTTP status. A VaultException will be thrown if
+ * the role does not exist, or if any other problem occurs. Example usage:
+ * {@code
+ * final VaultConfig config = new VaultConfig.address(...).token(...).build();
+ * final Vault vault = new Vault(config);
+ *
+ * final DatabaseResponse response = vault.database().revoke("serialnumber");
+ * assertEquals(204, response.getRestResponse().getStatus();
+ * }
+ *
+ *
+ * @param serialNumber The name of the role to delete
+ * @return A container for the information returned by Vault
+ * @throws VaultException If any error occurs or unexpected response is received from Vault
+ */
+ public DatabaseResponse revoke(final String serialNumber) throws VaultException {
+ int retryCount = 0;
+ while (true) {
+ // Make an HTTP request to Vault
+ JsonObject jsonObject = new JsonObject();
+ if (serialNumber != null) {
+ jsonObject.add("serial_number", serialNumber);
+ }
+ final String requestJson = jsonObject.toString();
+ try {
+ final RestResponse restResponse = new Rest()//NOPMD
+ .url(String.format("%s/v1/%s/revoke", config.getAddress(), this.mountPath))
+ .header("X-Vault-Token", config.getToken())
+ .optionalHeader("X-Vault-Namespace", this.nameSpace)
+ .connectTimeoutSeconds(config.getOpenTimeout())
+ .readTimeoutSeconds(config.getReadTimeout())
+ .body(requestJson.getBytes(StandardCharsets.UTF_8))
+ .sslVerification(config.getSslConfig().isVerify())
+ .sslContext(config.getSslConfig().getSslContext())
+ .post();
+
+ // Validate response
+ if (restResponse.getStatus() != 200) {
+ throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus());
+ }
+ return new DatabaseResponse(restResponse, retryCount);
+ } catch (Exception e) {
+ // If there are retries to perform, then pause for the configured interval and then execute the loop again...
+ if (retryCount < config.getMaxRetries()) {
+ retryCount++;
+ try {
+ final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
+ Thread.sleep(retryIntervalMilliseconds);
+ } catch (InterruptedException e1) {
+ e1.printStackTrace();
+ }
+ } else if (e instanceof VaultException) {
+ // ... otherwise, give up.
+ throw (VaultException) e;
+ } else {
+ throw new VaultException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Operation to delete an role using the Database backend. Relies on an authentication token being present in
+ * the VaultConfig instance.
A successful operation will return a 204 HTTP status. A VaultException will be thrown if
+ * the role does not exist, or if any other problem occurs. Example usage:
+ * {@code
+ * final VaultConfig config = new VaultConfig.address(...).token(...).build();
+ * final Vault vault = new Vault(config);
+ *
+ * final DatabaseResponse response = vault.database().deleteRole("testRole");
+ * assertEquals(204, response.getRestResponse().getStatus();
+ * }
+ *
+ *
+ * @param roleName The name of the role to delete
+ * @return A container for the information returned by Vault
+ * @throws VaultException If any error occurs or unexpected response is received from Vault
+ */
+ public DatabaseResponse deleteRole(final String roleName) throws VaultException {
+ int retryCount = 0;
+ while (true) {
+ // Make an HTTP request to Vault
+ try {
+ final RestResponse restResponse = new Rest()//NOPMD
+ .url(String.format("%s/v1/%s/roles/%s", config.getAddress(), this.mountPath, roleName))
+ .header("X-Vault-Token", config.getToken())
+ .optionalHeader("X-Vault-Namespace", this.nameSpace)
+ .connectTimeoutSeconds(config.getOpenTimeout())
+ .readTimeoutSeconds(config.getReadTimeout())
+ .sslVerification(config.getSslConfig().isVerify())
+ .sslContext(config.getSslConfig().getSslContext())
+ .delete();
+
+ // Validate response
+ if (restResponse.getStatus() != 204) {
+ throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus());
+ }
+ return new DatabaseResponse(restResponse, retryCount);
+ } catch (Exception e) {
+ // If there are retries to perform, then pause for the configured interval and then execute the loop again...
+ if (retryCount < config.getMaxRetries()) {
+ retryCount++;
+ try {
+ final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
+ Thread.sleep(retryIntervalMilliseconds);
+ } catch (InterruptedException e1) {
+ e1.printStackTrace();
+ }
+ } else if (e instanceof VaultException) {
+ // ... otherwise, give up.
+ throw (VaultException) e;
+ } else {
+ throw new VaultException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Operation to generate a new set of credentials using the Database backend. + * + *
A successful operation will return a 204 HTTP status. A VaultException will be thrown if
+ * the role does not exist, or if any other problem occurs. Credential information will be populated in the
+ * credential field of the DatabaseResponse return value. Example usage:
+ * {@code
+ * final VaultConfig config = new VaultConfig.address(...).token(...).build();
+ * final Vault vault = new Vault(config);
+ *
+ * final DatabaseResponse response = vault.database().creds("testRole");
+ * assertEquals(204, response.getRestResponse().getStatus();
+ * }
+ *
+ *
+ * @param roleName The role for which to retrieve credentials
+ * @return A container for the information returned by Vault
+ * @throws VaultException If any error occurs or unexpected response is received from Vault
+ */
+ public DatabaseResponse creds(final String roleName) throws VaultException {
+ int retryCount = 0;
+ while (true) {
+ try {
+ final RestResponse restResponse = new Rest()//NOPMD
+ .url(String.format("%s/v1/%s/creds/%s", config.getAddress(), this.mountPath, roleName))
+ .header("X-Vault-Token", config.getToken())
+ .optionalHeader("X-Vault-Namespace", this.nameSpace)
+ .connectTimeoutSeconds(config.getOpenTimeout())
+ .readTimeoutSeconds(config.getReadTimeout())
+ .sslVerification(config.getSslConfig().isVerify())
+ .sslContext(config.getSslConfig().getSslContext())
+ .get();
+
+ // Validate response
+ if (restResponse.getStatus() != 200 && restResponse.getStatus() != 404) {
+ String body = restResponse.getBody() != null ? new String(restResponse.getBody()) : "(no body)";
+ throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus() + " " + body, restResponse.getStatus());
+ }
+
+ return new DatabaseResponse(restResponse, retryCount);
+ } catch (Exception e) {
+ // If there are retries to perform, then pause for the configured interval and then execute the loop again...
+ if (retryCount < config.getMaxRetries()) {
+ retryCount++;
+ try {
+ final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
+ Thread.sleep(retryIntervalMilliseconds);
+ } catch (InterruptedException e1) {
+ e1.printStackTrace();
+ }
+ } else if (e instanceof VaultException) {
+ // ... otherwise, give up.
+ throw (VaultException) e;
+ } else {
+ throw new VaultException(e);
+ }
+ }
+ }
+ }
+
+ private String roleOptionsToJson(final DatabaseRoleOptions options) {
+ final JsonObject jsonObject = Json.object();
+
+ if (options != null) {
+ addJsonFieldIfNotNull(jsonObject, "db_name", options.getDbName());
+ addJsonFieldIfNotNull(jsonObject, "default_ttl", options.getDefaultTtl());
+ addJsonFieldIfNotNull(jsonObject, "max_ttl", options.getMaxTtl());
+ addJsonFieldIfNotNull(jsonObject, "creation_statements", joinList(options.getCreationStatements()));
+ addJsonFieldIfNotNull(jsonObject, "revocation_statements", joinList(options.getRevocationStatements()));
+ addJsonFieldIfNotNull(jsonObject, "rollback_statements", joinList(options.getRollbackStatements()));
+ addJsonFieldIfNotNull(jsonObject, "renew_statements", joinList(options.getRenewStatements()));
+ }
+
+ return jsonObject.toString();
+ }
+
+ private String joinList(List