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.

+ */ +public class Database { + + private final VaultConfig config; + private final String mountPath; + private String nameSpace; + + public Database withNameSpace(final String nameSpace) { + this.nameSpace = nameSpace; + return this; + } + + /** + * Constructor for use when the Database backend is mounted on the default path (i.e. /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 data) { + String result = ""; + + if (data != null && !data.isEmpty()) { + result = String.join(",", data); + } + + return result; + } + + private JsonObject addJsonFieldIfNotNull(final JsonObject jsonObject, final String name, final Object value) { + if (value == null) { + return jsonObject; + } + if (value instanceof String) { + jsonObject.add(name, (String) value); + } else if (value instanceof Boolean) { + jsonObject.add(name, (Boolean) value); + } else if (value instanceof Long) { + jsonObject.add(name, (Long) value); + } + + return jsonObject; + } +} diff --git a/src/main/java/com/bettercloud/vault/api/database/DatabaseCredential.java b/src/main/java/com/bettercloud/vault/api/database/DatabaseCredential.java new file mode 100644 index 00000000..93fe667e --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/database/DatabaseCredential.java @@ -0,0 +1,31 @@ +package com.bettercloud.vault.api.database; + +public class DatabaseCredential { + + private String username; + private String password; + + /** + * @return Returns the Username associated with the retrieved Credential + */ + public String getUsername() { + return username; + } + + /** + * @return Returns the Password associated with the retrieved Credential + */ + public String getPassword() { + return password; + } + + public DatabaseCredential username(String username) { + this.username = username; + return this; + } + + public DatabaseCredential password(String password) { + this.password = password; + return this; + } +} diff --git a/src/main/java/com/bettercloud/vault/api/database/DatabaseRoleOptions.java b/src/main/java/com/bettercloud/vault/api/database/DatabaseRoleOptions.java new file mode 100644 index 00000000..9e2ce4ed --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/database/DatabaseRoleOptions.java @@ -0,0 +1,119 @@ +package com.bettercloud.vault.api.database; + +import java.util.ArrayList; +import java.util.List; + +public class DatabaseRoleOptions { + private String name; + private String dbName; + private String defaultTtl = "0"; + private String maxTtl = "0"; + private List creationStatements = new ArrayList<>(); + private List revocationStatements = new ArrayList<>(); + private List rollbackStatements = new ArrayList<>(); + private List renewStatements = new ArrayList<>(); + + public String getName() { + return name; + } + + public String getDbName() { + return dbName; + } + + public String getDefaultTtl() { + return defaultTtl; + } + + public String getMaxTtl() { + return maxTtl; + } + + public List getCreationStatements() { + return creationStatements; + } + + public List getRenewStatements() { + return renewStatements; + } + + public List getRevocationStatements() { + return revocationStatements; + } + + public List getRollbackStatements() { + return rollbackStatements; + } + + /** + * @param name (string: ) – Specifies the name of the role to create. This is specified as part of the URL. + * @return This object, with name populated, ready for other builder methods or immediate use. + */ + public DatabaseRoleOptions name(final String name) { + this.name = name; + return this; + } + + /** + * @param dbName (string: ) - The name of the database connection to use for this role. + * @return This object, with dbName populated, ready for other builder methods or immediate use. + */ + public DatabaseRoleOptions dbName(final String dbName) { + this.dbName = dbName; + return this; + } + + /** + * @param defaultTtl (string/int: 0) - Specifies the TTL for the leases associated with this role. Accepts time suffixed strings ("1h") or an integer number of seconds. Defaults to system/engine default TTL time. + * @return This object, with defaultTtl populated, ready for other builder methods or immediate use. + */ + public DatabaseRoleOptions defaultTtl(final String defaultTtl) { + this.defaultTtl = defaultTtl; + return this; + } + + /** + * @param maxTtl (string/int: 0) - Specifies the maximum TTL for the leases associated with this role. Accepts time suffixed strings ("1h") or an integer number of seconds. Defaults to system/mount default TTL time; this value is allowed to be less than the mount max TTL (or, if not set, the system max TTL), but it is not allowed to be longer. See also The TTL General Case. + * @return This object, with maxTtl populated, ready for other builder methods or immediate use. + */ + public DatabaseRoleOptions maxTtl(final String maxTtl) { + this.maxTtl = maxTtl; + return this; + } + + /** + * @param creationStatements (list: ) – Specifies the database statements executed to create and configure a user. See the plugin's API page for more information on support and formatting for this parameter. + * @return This object, with creationStatements populated, ready for other builder methods or immediate use. + */ + public DatabaseRoleOptions creationStatements(final List creationStatements) { + this.creationStatements = creationStatements; + return this; + } + + /** + * @param revocationStatements (list: []) – Specifies the database statements to be executed to revoke a user. See the plugin's API page for more information on support and formatting for this parameter. + * @return This object, with revocationStatements populated, ready for other builder methods or immediate use. + */ + public DatabaseRoleOptions revocationStatements(final List revocationStatements) { + this.revocationStatements = revocationStatements; + return this; + } + + /** + * @param rollbackStatements (list: []) – Specifies the database statements to be executed rollback a create operation in the event of an error. Not every plugin type will support this functionality. See the plugin's API page for more information on support and formatting for this parameter. + * @return This object, with rollbackStatements populated, ready for other builder methods or immediate use. + */ + public DatabaseRoleOptions rollbackStatements(final List rollbackStatements) { + this.rollbackStatements = rollbackStatements; + return this; + } + + /** + * @param renewStatements (list: []) – Specifies the database statements to be executed to renew a user. Not every plugin type will support this functionality. See the plugin's API page for more information on support and formatting for this parameter. + * @return This object, with renewStatements populated, ready for other builder methods or immediate use. + */ + public DatabaseRoleOptions renewStatements(final List renewStatements) { + this.renewStatements = renewStatements; + return this; + } +} diff --git a/src/main/java/com/bettercloud/vault/response/DatabaseResponse.java b/src/main/java/com/bettercloud/vault/response/DatabaseResponse.java new file mode 100644 index 00000000..b8e9a42d --- /dev/null +++ b/src/main/java/com/bettercloud/vault/response/DatabaseResponse.java @@ -0,0 +1,97 @@ +package com.bettercloud.vault.response; + +import com.bettercloud.vault.api.Logical; +import com.bettercloud.vault.api.database.DatabaseCredential; +import com.bettercloud.vault.api.database.DatabaseRoleOptions; +import com.bettercloud.vault.json.JsonArray; +import com.bettercloud.vault.json.JsonObject; +import com.bettercloud.vault.rest.RestResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class DatabaseResponse extends LogicalResponse { + + private DatabaseCredential credential; + + private DatabaseRoleOptions roleOptions; + + public DatabaseCredential getCredential() { + return credential; + } + + public DatabaseRoleOptions getRoleOptions() { + return roleOptions; + } + + /** + * @param restResponse The raw HTTP response from Vault. + * @param retries The number of retry attempts that occurred during the API call (can be zero). + */ + public DatabaseResponse(RestResponse restResponse, int retries) { + super(restResponse, retries, Logical.logicalOperations.authentication); + credential = buildCredentialFromData(this.getData()); + roleOptions = buildRoleOptionsFromData(this.getData(), this.getDataObject()); + } + + private DatabaseRoleOptions buildRoleOptionsFromData(final Map data, final JsonObject jsonObject) { + if (data == null || data.size() == 0) { + return null; + } + + final List creationStatements = extractFromJsonArray(safeGetJsonArray(jsonObject, "creation_statements")); + final List renewStatements = extractFromJsonArray(safeGetJsonArray(jsonObject, "renew_statements")); + final List revocationStatements = extractFromJsonArray(safeGetJsonArray(jsonObject, "revocation_statements")); + final List rollbackStatements = extractFromJsonArray(safeGetJsonArray(jsonObject, "rollback_statements")); + + final String dbName = data.get("db_name"); + final String defaultTtl = data.get("default_ttl"); + final String maxTtl = data.get("default_ttl"); + + if (dbName == null && defaultTtl == null && maxTtl == null) { + return null; + } + + return new DatabaseRoleOptions() + .creationStatements(creationStatements) + .renewStatements(renewStatements) + .revocationStatements(revocationStatements) + .rollbackStatements(rollbackStatements) + .dbName(dbName) + .defaultTtl(defaultTtl) + .maxTtl(maxTtl); + } + + private DatabaseCredential buildCredentialFromData(final Map data) { + if (data == null) { + return null; + } + final String username = data.get("username"); + final String password = data.get("password"); + + if (username == null && password == null) { + return null; + } + + return new DatabaseCredential() + .username(username) + .password(password); + } + + private JsonArray safeGetJsonArray(JsonObject source, String key) { + if (source == null || source.get(key) == null || source.get(key) == null || !source.get(key).isArray()) { + return new JsonArray(); + } + + return source.get(key).asArray(); + } + + private List extractFromJsonArray(JsonArray array) { + List result = new ArrayList<>(); + + array.forEach(entry -> result.add(entry.asString())); + + return result; + } +} diff --git a/src/test-integration/java/com/bettercloud/vault/api/AuthBackendDatabaseTests.java b/src/test-integration/java/com/bettercloud/vault/api/AuthBackendDatabaseTests.java new file mode 100644 index 00000000..5f773f62 --- /dev/null +++ b/src/test-integration/java/com/bettercloud/vault/api/AuthBackendDatabaseTests.java @@ -0,0 +1,107 @@ +package com.bettercloud.vault.api; + +import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultException; +import com.bettercloud.vault.api.database.DatabaseRoleOptions; +import com.bettercloud.vault.response.DatabaseResponse; +import com.bettercloud.vault.util.DbContainer; +import com.bettercloud.vault.util.VaultContainer; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; + +public class AuthBackendDatabaseTests { + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + @ClassRule + public static final DbContainer dbContainer = new DbContainer(); + + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException { + container.initAndUnsealVault(); + container.setupBackendDatabase(dbContainer.getDbContainerIp()); + } + + @Test + public void testRoleCreation() throws VaultException { + final Vault vault = container.getRootVault(); + + List creationStatements = new ArrayList<>(); + creationStatements.add("CREATE USER \"{{name}}\" WITH PASSWORD '{{password}}'; GRANT ALL PRIVILEGES ON DATABASE \"postgres\" to \"{{name}}\";"); + + DatabaseRoleOptions roleToCreate = new DatabaseRoleOptions().dbName("postgres").creationStatements(creationStatements); + + DatabaseResponse response = vault.database().createOrUpdateRole("test-role", roleToCreate); + assertEquals(204, response.getRestResponse().getStatus()); + + DatabaseResponse role = vault.database().getRole("test-role"); + assertEquals(200, role.getRestResponse().getStatus()); + + assertTrue(compareRoleOptions(role.getRoleOptions(), roleToCreate)); + } + + @Test + public void testDeleteRole() throws VaultException { + final Vault vault = container.getRootVault(); + + List creationStatements = new ArrayList<>(); + creationStatements.add("CREATE USER \"{{name}}\" WITH PASSWORD '{{password}}'; GRANT ALL PRIVILEGES ON DATABASE \"postgres\" to \"{{name}}\";"); + + DatabaseRoleOptions roleToCreate = new DatabaseRoleOptions().dbName("postgres").creationStatements(creationStatements); + + DatabaseResponse response = vault.database().createOrUpdateRole("delete-role", roleToCreate); + assertEquals(204, response.getRestResponse().getStatus()); + + DatabaseResponse deletedRole = vault.database().deleteRole("delete-role"); + assertEquals(204, deletedRole.getRestResponse().getStatus()); + + try { + DatabaseResponse role = vault.database().getRole("delete-role"); + } catch (VaultException e) { + assertEquals("This should have failed", 404, e.getHttpStatusCode()); + } + } + + @Test + public void testRoleNotFound() throws VaultException { + final Vault vault = container.getRootVault(); + + try { + DatabaseResponse role = vault.database().getRole("i-do-not-exist"); + } catch (VaultException e) { + assertEquals("This should have failed", 404, e.getHttpStatusCode()); + } + } + + @Test + public void testGetCredentials() throws VaultException { + final Vault vault = container.getRootVault(); + + List creationStatements = new ArrayList<>(); + creationStatements.add("CREATE USER \"{{name}}\" WITH PASSWORD '{{password}}'; GRANT ALL PRIVILEGES ON DATABASE \"postgres\" to \"{{name}}\";"); + + DatabaseResponse response = vault.database().createOrUpdateRole("new-role", new DatabaseRoleOptions().dbName("postgres").creationStatements(creationStatements)); + assertEquals(204, response.getRestResponse().getStatus()); + + DatabaseResponse credsResponse = vault.database().creds("new-role"); + assertEquals(200, credsResponse.getRestResponse().getStatus()); + + assertTrue(credsResponse.getCredential().getUsername().contains("new-role")); + } + + private boolean compareRoleOptions(DatabaseRoleOptions expected, DatabaseRoleOptions actual) { + return expected.getCreationStatements().size() == actual.getCreationStatements().size() && + expected.getRenewStatements().size() == actual.getRenewStatements().size() && + expected.getRevocationStatements().size() == actual.getRevocationStatements().size() && + expected.getRollbackStatements().size() == actual.getRollbackStatements().size(); + } +} diff --git a/src/test-integration/java/com/bettercloud/vault/util/DbContainer.java b/src/test-integration/java/com/bettercloud/vault/util/DbContainer.java new file mode 100644 index 00000000..570a20e1 --- /dev/null +++ b/src/test-integration/java/com/bettercloud/vault/util/DbContainer.java @@ -0,0 +1,35 @@ +package com.bettercloud.vault.util; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.HostPortWaitStrategy; + +import java.io.IOException; + +public class DbContainer implements TestRule, TestConstants { + + private final GenericContainer container; + + public DbContainer() { + container = new GenericContainer("postgres:11.3-alpine") + .withEnv("POSTGRES_PASSWORD", POSTGRES_PASSWORD) + .withEnv("POSTGRES_USER", POSTGRES_USER) + .withExposedPorts(5432) + .waitingFor(new HostPortWaitStrategy()); + } + + public String getDbContainerIp() throws IOException, InterruptedException { + Container.ExecResult ip = container.execInContainer("hostname", "-i"); + return ip.getStdout().replace("\n", ""); + + //return container.getContainerIpAddress(); + } + + @Override + public Statement apply(Statement base, Description description) { + return container.apply(base, description); + } +} diff --git a/src/test-integration/java/com/bettercloud/vault/util/TestConstants.java b/src/test-integration/java/com/bettercloud/vault/util/TestConstants.java index 440e6192..98df371b 100644 --- a/src/test-integration/java/com/bettercloud/vault/util/TestConstants.java +++ b/src/test-integration/java/com/bettercloud/vault/util/TestConstants.java @@ -9,6 +9,9 @@ */ interface TestConstants { + String POSTGRES_PASSWORD = "superpassword1"; + String POSTGRES_USER = "superuser1"; + String APP_ID = "fake_app"; String USER_ID = "fake_user"; String PASSWORD = "fake_password"; diff --git a/src/test-integration/java/com/bettercloud/vault/util/VaultContainer.java b/src/test-integration/java/com/bettercloud/vault/util/VaultContainer.java index 88244d88..de1749ef 100644 --- a/src/test-integration/java/com/bettercloud/vault/util/VaultContainer.java +++ b/src/test-integration/java/com/bettercloud/vault/util/VaultContainer.java @@ -192,6 +192,24 @@ public void setupBackendCert() throws IOException, InterruptedException { "policies=web,prod", "certificate=@" + CONTAINER_CLIENT_CERT_PEMFILE, "ttl=3600"); } + /** + * Prepares the Vault server for testing of the Database Backend using Postgres + * + * @throws IOException + * @throws InterruptedException + */ + public void setupBackendDatabase(String databaseIp) throws IOException, InterruptedException { + runCommand("vault", "login", "-ca-cert=" + CONTAINER_CERT_PEMFILE, rootToken); + + runCommand("vault", "secrets", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "database"); + runCommand("vault", "write", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "database/config/postgres", + "plugin_name=postgresql-database-plugin", + "allowed_roles=*", + "connection_url=postgresql://{{username}}:{{password}}@" + databaseIp + ":5432/postgres?sslmode=disable", + "password=" + POSTGRES_PASSWORD, + "username=" + POSTGRES_USER); + } + public void setEngineVersions() throws IOException, InterruptedException { //Upgrade default secrets/ Engine to V2, set a new V1 secrets path at "kv-v1/" runCommand("vault", "kv", "enable-versioning", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "secret/");