From 8e906e11059217638f1f3089fafbc73509239b68 Mon Sep 17 00:00:00 2001 From: Yuriy Tkach Date: Wed, 28 Jun 2023 23:20:20 +0300 Subject: [PATCH] Added integration setup and test --- .../io/quarkus/vault/VaultAwsIamITCase.java | 39 +++++++ .../application-vault-aws-iam.properties | 20 ++++ .../VaultInternalAwsIamAuthMethod.java | 12 ++- .../config/VaultAuthenticationConfig.java | 5 + .../VaultAwsIamAuthenticationConfig.java | 21 ++++ .../runtime/config/VaultBootstrapConfig.java | 2 +- test-framework/pom.xml | 14 +++ .../vault/test/VaultTestExtension.java | 100 +++++++++++++++++- .../vault/test/VaultTestLifecycleManager.java | 3 + 9 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 integration-tests/vault/src/test/java/io/quarkus/vault/VaultAwsIamITCase.java create mode 100644 integration-tests/vault/src/test/resources/application-vault-aws-iam.properties diff --git a/integration-tests/vault/src/test/java/io/quarkus/vault/VaultAwsIamITCase.java b/integration-tests/vault/src/test/java/io/quarkus/vault/VaultAwsIamITCase.java new file mode 100644 index 00000000..f3eaf11f --- /dev/null +++ b/integration-tests/vault/src/test/java/io/quarkus/vault/VaultAwsIamITCase.java @@ -0,0 +1,39 @@ +package io.quarkus.vault; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.vault.test.VaultTestLifecycleManager; + +@QuarkusTestResource(VaultTestLifecycleManager.class) +public class VaultAwsIamITCase { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("application-vault-aws-iam.properties", "application.properties")); + + @ConfigProperty(name = "quarkus.vault.authentication.aws-iam.role") + String role; + + @ConfigProperty(name = "quarkus.vault.authentication.aws-iam.aws-access-key") + String key; + + @Test + public void testRole() { + assertEquals("myawsiamrole", role); + } + + @Test + public void testAuthMountPath() { + System.out.println("key: " + key); + assertNotNull(key); + } + +} diff --git a/integration-tests/vault/src/test/resources/application-vault-aws-iam.properties b/integration-tests/vault/src/test/resources/application-vault-aws-iam.properties new file mode 100644 index 00000000..28cce052 --- /dev/null +++ b/integration-tests/vault/src/test/resources/application-vault-aws-iam.properties @@ -0,0 +1,20 @@ +quarkus.vault.url=https://localhost:8200 + +# vault-test.client-token-wrapping-token provided by VaultTestLifecycleManager +quarkus.vault.authentication.aws-iam.role=myawsiamrole +quarkus.vault.authentication.aws-iam.region=us-east-1 +quarkus.vault.authentication.aws-iam.sts-url=http://mylocalstack:4566 +quarkus.vault.authentication.aws-iam.vault-server-id=vault.example.com +quarkus.vault.authentication.aws-iam.aws-access-key=${vault-test.aws-user.access-key} +quarkus.vault.authentication.aws-iam.aws-secret-key=${vault-test.aws-user.secret-key} + +#quarkus.vault.tls.skip-verify=true +quarkus.vault.tls.ca-cert=src/test/resources/vault-tls.crt + +#quarkus.vault.log-confidentiality-level=low +#quarkus.vault.renew-grace-period=10 + +quarkus.log.category."io.quarkus.vault".level=DEBUG + +#quarkus.log.level=DEBUG +#quarkus.log.console.level=DEBUG diff --git a/runtime/src/main/java/io/quarkus/vault/runtime/client/authmethod/VaultInternalAwsIamAuthMethod.java b/runtime/src/main/java/io/quarkus/vault/runtime/client/authmethod/VaultInternalAwsIamAuthMethod.java index b4f59754..11ae0348 100644 --- a/runtime/src/main/java/io/quarkus/vault/runtime/client/authmethod/VaultInternalAwsIamAuthMethod.java +++ b/runtime/src/main/java/io/quarkus/vault/runtime/client/authmethod/VaultInternalAwsIamAuthMethod.java @@ -12,9 +12,11 @@ import io.quarkus.vault.runtime.client.VaultInternalBase; import io.quarkus.vault.runtime.client.dto.auth.VaultAwsIamAuth; import io.quarkus.vault.runtime.client.dto.auth.VaultAwsIamAuthBody; +import io.quarkus.vault.runtime.config.VaultAwsIamAuthenticationConfig; import io.smallrye.mutiny.Uni; import jakarta.inject.Inject; import jakarta.inject.Singleton; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.auth.signer.Aws4Signer; @@ -97,8 +99,14 @@ private SdkHttpFullRequest signRequest( } private AwsCredentials getAwsCredentials() { - try (DefaultCredentialsProvider defaultCredentialsProvider = DefaultCredentialsProvider.create()) { - return defaultCredentialsProvider.resolveCredentials(); + final VaultAwsIamAuthenticationConfig awsIam = vaultConfigHolder.getVaultBootstrapConfig().authentication.awsIam; + + if (awsIam.awsAccessKey.isPresent() && awsIam.awsSecretKey.isPresent()) { + return AwsBasicCredentials.create(awsIam.awsAccessKey.get(), awsIam.awsSecretKey.get()); + } else { + try (DefaultCredentialsProvider defaultCredentialsProvider = DefaultCredentialsProvider.create()) { + return defaultCredentialsProvider.resolveCredentials(); + } } } diff --git a/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultAuthenticationConfig.java b/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultAuthenticationConfig.java index d5bf50f3..b85f7ba2 100644 --- a/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultAuthenticationConfig.java +++ b/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultAuthenticationConfig.java @@ -51,6 +51,11 @@ public class VaultAuthenticationConfig { @ConfigItem public VaultKubernetesAuthenticationConfig kubernetes; + /** + * AWS IAM authentication method + *

+ * See https://developer.hashicorp.com/vault/docs/auth/aws + */ @ConfigItem public VaultAwsIamAuthenticationConfig awsIam; diff --git a/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultAwsIamAuthenticationConfig.java b/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultAwsIamAuthenticationConfig.java index 62673f07..7ff5e713 100644 --- a/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultAwsIamAuthenticationConfig.java +++ b/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultAwsIamAuthenticationConfig.java @@ -16,12 +16,33 @@ public class VaultAwsIamAuthenticationConfig { @ConfigItem public String role; + /** + * The AWS region to use for AWS IAM authentication. + */ @ConfigItem public String region; + /** + * The URL of the AWS STS endpoint to use for AWS IAM authentication. + */ @ConfigItem(defaultValue = "https://sts.amazonaws.com") public String stsUrl; + /** + * The Vault server ID to use for AWS IAM authentication. + */ @ConfigItem public Optional vaultServerId; + + /** + * The AWS access key ID to use for AWS IAM authentication. + */ + @ConfigItem + public Optional awsAccessKey; + + /** + * The AWS secret access key to use for AWS IAM authentication. + */ + @ConfigItem + public Optional awsSecretKey; } diff --git a/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultBootstrapConfig.java b/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultBootstrapConfig.java index a3ff1341..3b22fd00 100644 --- a/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultBootstrapConfig.java +++ b/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultBootstrapConfig.java @@ -299,7 +299,7 @@ public String toString() { + ", awsIamRole=" + authentication.awsIam.role + ", awsIamSts=" + authentication.awsIam.stsUrl + ", awsIamRegion=" + authentication.awsIam.region - + ", awsIamVaultServerId" + logConfidentialityLevel.maskWithTolerance(authentication.awsIam.vaultServerId.orElse(""), LOW) + '\'' +// + ", awsIamVaultServerId" + logConfidentialityLevel.maskWithTolerance(authentication.awsIam.vaultServerId.orElse(""), LOW) + '\'' + ", clientToken=" + logConfidentialityLevel.maskWithTolerance(authentication.clientToken.orElse(""), LOW) + ", clientTokenWrappingToken=" diff --git a/test-framework/pom.xml b/test-framework/pom.xml index 89a3bfe6..ee7919c8 100644 --- a/test-framework/pom.xml +++ b/test-framework/pom.xml @@ -39,6 +39,20 @@ + + org.testcontainers + localstack + + + javax.xml.bind + jaxb-api + + + javax.annotation + javax.annotation-api + + + org.testcontainers rabbitmq diff --git a/test-framework/src/main/java/io/quarkus/vault/test/VaultTestExtension.java b/test-framework/src/main/java/io/quarkus/vault/test/VaultTestExtension.java index 01111280..095ef8c4 100644 --- a/test-framework/src/main/java/io/quarkus/vault/test/VaultTestExtension.java +++ b/test-framework/src/main/java/io/quarkus/vault/test/VaultTestExtension.java @@ -43,8 +43,15 @@ import org.testcontainers.containers.Network; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.containers.localstack.LocalStackContainer; import org.testcontainers.containers.output.OutputFrame; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; + import io.quarkus.vault.VaultException; import io.quarkus.vault.VaultKVSecretEngine; import io.quarkus.vault.runtime.VaultConfigHolder; @@ -77,6 +84,7 @@ public class VaultTestExtension { public static final String VAULT_AUTH_USERPASS_USER = "bob"; public static final String VAULT_AUTH_USERPASS_PASSWORD = "sinclair"; public static final String VAULT_AUTH_APPROLE = "myapprole"; + public static final String VAULT_AUTH_AWS_IAM_ROLE = "myawsiamrole"; public static final String SECRET_PATH_V1 = "secret-v1"; public static final String SECRET_PATH_V2 = "secret"; public static final String LIST_PATH = "hello"; @@ -89,6 +97,7 @@ public class VaultTestExtension { static final String VAULT_POLICY = "mypolicy"; static final String POSTGRESQL_HOST = "mypostgresdb"; static final String RABBITMQ_HOST = "myrabbitmq"; + static final String LOCALSTACK_HOST = "mylocalstack"; static final String VAULT_URL = (useTls() ? "https" : "http") + "://localhost:" + VAULT_PORT; public static final String SECRET_KEY = "secret"; public static final String ENCRYPTION_KEY_NAME = "my-encryption-key"; @@ -107,11 +116,13 @@ public class VaultTestExtension { public static final String HOST_POSTGRES_TMP_CMD = "target/postgres_cmd"; public static final String OUT_FILE = "/out"; public static final String WRAPPING_TEST_PATH = "wrapping-test"; + private static final String VAULT_AWS_SERVER_ID = "vault.example.com"; private static String CRUD_PATH = "crud"; public GenericContainer vaultContainer; public PostgreSQLContainer postgresContainer; + public LocalStackContainer localStackContainer; public RabbitMQContainer rabbitMQContainer; public String rootToken = null; public String appRoleSecretId = null; @@ -126,6 +137,9 @@ public class VaultTestExtension { private String db_default_ttl = "1m"; private String db_max_ttl = "10m"; + public CreateAccessKeyResponse userAwsAccessKey; + private CreateAccessKeyResponse vaultAwsAccessKey; + private CreateUserResponse vaultAwsUser; public static void testDataSource(DataSource ds) throws SQLException { try (Connection c = ds.getConnection()) { @@ -224,6 +238,15 @@ public void start() throws InterruptedException, IOException { .withNetwork(network) .withNetworkAliases(RABBITMQ_HOST); + Consumer localstackConsumer = outputFrame -> System.out. + print("AWS >> " + outputFrame.getUtf8String()); + + localStackContainer = new LocalStackContainer() + .withServices(LocalStackContainer.Service.STS, LocalStackContainer.Service.IAM) + .withLogConsumer(localstackConsumer) + .withNetwork(network) + .withNetworkAliases(LOCALSTACK_HOST); + String configFile = useTls() ? "vault-config-tls.json" : "vault-config.json"; String vaultImage = getVaultImage(); @@ -251,6 +274,9 @@ public void start() throws InterruptedException, IOException { rabbitMQContainer.start(); + localStackContainer.start(); + initLocalStack(); + Consumer consumer = outputFrame -> System.out.print("VAULT >> " + outputFrame.getUtf8String()); vaultContainer.setLogConsumers(Arrays.asList(consumer)); vaultContainer.start(); @@ -263,6 +289,56 @@ private String getVaultImage() { return "vault:" + VaultVersions.VAULT_TEST_VERSION; } + private void initLocalStack() throws IOException, InterruptedException { + String awsLocalstackUrl = localStackContainer.getEndpointOverride(LocalStackContainer.Service.STS).toString(); + System.out.println("AWS STS URL: " + awsLocalstackUrl); + + final ObjectMapper objectMapper = JsonMapper.builder() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .serializationInclusion(JsonInclude.Include.NON_NULL) + .build(); + + final String outCreateUser = execLocalStack("awslocal", "iam", "create-user", "--user-name", "admin-user"); + vaultAwsUser = objectMapper.readValue( + outCreateUser, + CreateUserResponse.class + ); + + final String outUserAccessKey = execLocalStack("awslocal", "iam", "create-access-key", "--user-name", "admin-user"); + userAwsAccessKey = objectMapper.readValue( + outUserAccessKey, + CreateAccessKeyResponse.class + ); + + execLocalStack("awslocal", "iam", "create-user", "--user-name", "vault-user"); + + final String outVaultAccessKey = execLocalStack("awslocal", "iam", "create-access-key", "--user-name", "vault-user"); + vaultAwsAccessKey = objectMapper.readValue( + outUserAccessKey, + CreateAccessKeyResponse.class + ); + } + + static class CreateAccessKeyResponse { + public AwsAccessKey accessKey; + } + + static class AwsAccessKey { + public String accessKeyId; + public String secretAccessKey; + } + + static class CreateUserResponse { + public AwsUser user; + } + + static class AwsUser { + public String userName; + public String userId; + public String arn; + } + private void initVault() throws InterruptedException, IOException { waitForContainerToStart(); @@ -306,6 +382,24 @@ private void initVault() throws InterruptedException, IOException { log.info( format("generated role_id=%s secret_id=%s for approle=%s", appRoleRoleId, appRoleSecretId, VAULT_AUTH_APPROLE)); + // aws iam auth + execVault("vault auth enable aws"); + execVault(format("vault write auth/aws/config/client secret_key=%s access_key=%s", + vaultAwsAccessKey.accessKey.secretAccessKey, vaultAwsAccessKey.accessKey.accessKeyId)); + + execVault(format("vault write auth/aws/config/client iam_server_id_header_value=%s " + + "iam_endpoint=%s sts_endpoint=%s", + VAULT_AWS_SERVER_ID, + "http://mylocalstack:4566", + "http://mylocalstack:4566" + )); + + execVault(format("vault write auth/aws/role/%s auth_type=iam " + + "bound_iam_principal_arn=%s policies=%s", + VAULT_AUTH_AWS_IAM_ROLE, + vaultAwsUser.user.arn.replaceAll("000000000000", "123456789012"), + VAULT_POLICY)); + // policy String policyContent = readResourceContent("vault.policy"); vaultInternalSystemBackend.createUpdatePolicy(vaultClient, rootToken, VAULT_POLICY, new VaultPolicyBody(policyContent)) @@ -494,6 +588,10 @@ private String execVault(String command) throws IOException, InterruptedExceptio return exec(vaultContainer, command, cmd, HOST_VAULT_TMP_CMD + OUT_FILE); } + private String execLocalStack(final String... command) throws IOException, InterruptedException { + return exec(localStackContainer, command).getStdout(); + } + private String exec(GenericContainer container, String command, String[] cmd, String outFile) throws IOException, InterruptedException { exec(container, cmd); @@ -502,7 +600,7 @@ private String exec(GenericContainer container, String command, String[] cmd, St return out; } - private static Container.ExecResult exec(GenericContainer container, String[] cmd) + private static Container.ExecResult exec(Container container, String[] cmd) throws IOException, InterruptedException { Container.ExecResult execResult = container.execInContainer(cmd); diff --git a/test-framework/src/main/java/io/quarkus/vault/test/VaultTestLifecycleManager.java b/test-framework/src/main/java/io/quarkus/vault/test/VaultTestLifecycleManager.java index 28b1d33f..5554eb7d 100644 --- a/test-framework/src/main/java/io/quarkus/vault/test/VaultTestLifecycleManager.java +++ b/test-framework/src/main/java/io/quarkus/vault/test/VaultTestLifecycleManager.java @@ -44,6 +44,9 @@ public Map start() { sysprops.put("vault-test.password-kv-v2-wrapping-token", vaultTestExtension.passwordKvv2WrappingToken); sysprops.put("vault-test.another-password-kv-v2-wrapping-token", vaultTestExtension.anotherPasswordKvv2WrappingToken); + sysprops.put("vault-test.aws-user.access-key", vaultTestExtension.userAwsAccessKey.accessKey.accessKeyId); + sysprops.put("vault-test.aws-user.secret-key", vaultTestExtension.userAwsAccessKey.accessKey.secretAccessKey); + log.info("using system properties " + sysprops); return sysprops;