diff --git a/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/AwsCredentialType.java b/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/AwsCredentialType.java index cdd6a8cb..c07e6b4f 100644 --- a/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/AwsCredentialType.java +++ b/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/AwsCredentialType.java @@ -14,13 +14,14 @@ * limitations under the License. */ -/** - * @author Kris Iyer - * - * Supported Aws credential types. - */ package org.springframework.cloud.vault.config.aws; +/** + * Supported AWS credential types. + * + * @author Kris Iyer + * @since 3.0.2 + */ public enum AwsCredentialType { IAM_USER, ASSUMED_ROLE, FEDERATION_TOKEN diff --git a/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultAwsProperties.java b/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultAwsProperties.java index 6d669f05..4ba6b096 100644 --- a/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultAwsProperties.java +++ b/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultAwsProperties.java @@ -16,6 +16,8 @@ package org.springframework.cloud.vault.config.aws; +import java.time.Duration; + import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.vault.config.VaultSecretBackendDescriptor; import org.springframework.lang.Nullable; @@ -66,17 +68,19 @@ public class VaultAwsProperties implements VaultSecretBackendDescriptor { private String sessionTokenKeyProperty = "cloud.aws.credentials.sessionToken"; /** - * * Role arn for assumed_role in case we have multiple roles associated with the vault - * role + * role. + * @since 3.0.2 */ + @Nullable private String roleArn; /** * TTL for sts tokens. Defaults to whatever the vault Role may have for Max. Also * limited to what AWS supports to be the max for STS. + * @since 3.0.2 */ - private String ttl; + private Duration ttl = Duration.ZERO; @Override public boolean isEnabled() { @@ -122,7 +126,7 @@ public class VaultAwsProperties implements VaultSecretBackendDescriptor { } public AwsCredentialType getCredentialType() { - return credentialType; + return this.credentialType; } public void setCredentialType(AwsCredentialType credentialType) { @@ -130,26 +134,27 @@ public class VaultAwsProperties implements VaultSecretBackendDescriptor { } public String getSessionTokenKeyProperty() { - return sessionTokenKeyProperty; + return this.sessionTokenKeyProperty; } public void setSessionTokenKeyProperty(String sessionTokenKeyProperty) { this.sessionTokenKeyProperty = sessionTokenKeyProperty; } + @Nullable public String getRoleArn() { - return roleArn; + return this.roleArn; } public void setRoleArn(String roleArn) { this.roleArn = roleArn; } - public String getTtl() { - return ttl; + public Duration getTtl() { + return this.ttl; } - public void setTtl(String ttl) { + public void setTtl(Duration ttl) { this.ttl = ttl; } diff --git a/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsBootstrapConfiguration.java b/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsBootstrapConfiguration.java index ebfeb050..58b544f8 100644 --- a/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsBootstrapConfiguration.java +++ b/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsBootstrapConfiguration.java @@ -18,6 +18,7 @@ package org.springframework.cloud.vault.config.aws; import java.util.HashMap; import java.util.Map; +import java.util.StringJoiner; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -30,7 +31,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.springframework.vault.core.lease.domain.RequestedSecret; import org.springframework.vault.core.lease.domain.RequestedSecret.Mode; import org.springframework.vault.core.util.PropertyTransformer; @@ -39,7 +39,6 @@ import org.springframework.vault.core.util.PropertyTransformer; * * @author Mark Paluch * @author Kris Iyer - * */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(VaultAwsProperties.class) @@ -81,93 +80,10 @@ public class VaultConfigAwsBootstrapConfiguration { // security token transformer for STS transformer.addKeyTransformation("security_token", properties.getSessionTokenKeyProperty()); - return new LeasingSecretBackendMetadata() { - - @Override - public String getName() { - return String.format("%s with Role %s", properties.getBackend(), properties.getRole()); - } - - @Override - public String getPath() { - String defaultPath = "%s/sts/%s"; - - // do we have any ttl or role parameters configured? - if (StringUtils.hasText(properties.getTtl()) - || (properties.getCredentialType() == AwsCredentialType.ASSUMED_ROLE - && StringUtils.hasText(properties.getRoleArn()))) { - defaultPath += "?"; - } - // ttl for assumed_role or federation_token - // pass through to let aws take care of min and max validations - // per - // the vault role - if (StringUtils.hasText(properties.getTtl())) { - defaultPath += "ttl=" + properties.getTtl() + "&"; - } - - // role_arn for assumed_role for vault role that has multiple role - // associations. - if (properties.getCredentialType() == AwsCredentialType.ASSUMED_ROLE - && StringUtils.hasText(properties.getRoleArn())) { - defaultPath += "role_arn=" + properties.getRoleArn(); - } - return String.format(defaultPath, properties.getBackend(), properties.getRole()); - } - - @Override - public PropertyTransformer getPropertyTransformer() { - return transformer; - } - - @Override - public Map getVariables() { - - Map variables = new HashMap<>(); - - variables.put("backend", properties.getBackend()); - variables.put("key", String.format("sts/%s", properties.getRole())); - - return variables; - } - - @Override - public Mode getLeaseMode() { - return RequestedSecret.Mode.ROTATE; - } - }; - + return new AwsStsLeasingSecretBackendMetadata(properties, transformer); } else { - - return new SecretBackendMetadata() { - - @Override - public String getName() { - return String.format("%s with Role %s", properties.getBackend(), properties.getRole()); - } - - @Override - public String getPath() { - return String.format("%s/creds/%s", properties.getBackend(), properties.getRole()); - } - - @Override - public PropertyTransformer getPropertyTransformer() { - return transformer; - } - - @Override - public Map getVariables() { - - Map variables = new HashMap<>(); - - variables.put("backend", properties.getBackend()); - variables.put("key", String.format("creds/%s", properties.getRole())); - - return variables; - } - }; + return new AwsLeasingSecretBackendMetadata(properties, transformer); } } @@ -181,6 +97,109 @@ public class VaultConfigAwsBootstrapConfiguration { return backendDescriptor instanceof VaultAwsProperties; } + private static class AwsStsLeasingSecretBackendMetadata implements LeasingSecretBackendMetadata { + + private final VaultAwsProperties properties; + + private final PropertyNameTransformer transformer; + + AwsStsLeasingSecretBackendMetadata(VaultAwsProperties properties, PropertyNameTransformer transformer) { + this.properties = properties; + this.transformer = transformer; + } + + @Override + public String getName() { + return String.format("%s with Role %s", this.properties.getBackend(), this.properties.getRole()); + } + + @Override + public String getPath() { + + String defaultPath = "%s/sts/%s"; + StringJoiner joiner = new StringJoiner("&"); + + // ttl for assumed_role or federation_token + // pass through to let aws take care of min and max validations + // per the vault role + if (!this.properties.getTtl().isZero()) { + joiner.add("ttl=" + this.properties.getTtl().toMillis() + "ms"); + } + + // role_arn for assumed_role for vault role that has multiple role + // associations. + if (this.properties.getCredentialType() == AwsCredentialType.ASSUMED_ROLE + && StringUtils.hasText(this.properties.getRoleArn())) { + joiner.add("role_arn=" + this.properties.getRoleArn()); + } + + String pathToUse = joiner.length() == 0 ? defaultPath : defaultPath + "?" + joiner; + + return String.format(pathToUse, this.properties.getBackend(), this.properties.getRole()); + } + + @Override + public PropertyTransformer getPropertyTransformer() { + return this.transformer; + } + + @Override + public Map getVariables() { + + Map variables = new HashMap<>(); + + variables.put("backend", this.properties.getBackend()); + variables.put("key", String.format("sts/%s", this.properties.getRole())); + + return variables; + } + + @Override + public Mode getLeaseMode() { + return Mode.ROTATE; + } + + } + + private static class AwsLeasingSecretBackendMetadata implements SecretBackendMetadata { + + private final VaultAwsProperties properties; + + private final PropertyNameTransformer transformer; + + AwsLeasingSecretBackendMetadata(VaultAwsProperties properties, PropertyNameTransformer transformer) { + this.properties = properties; + this.transformer = transformer; + } + + @Override + public String getName() { + return String.format("%s with Role %s", this.properties.getBackend(), this.properties.getRole()); + } + + @Override + public String getPath() { + return String.format("%s/creds/%s", this.properties.getBackend(), this.properties.getRole()); + } + + @Override + public PropertyTransformer getPropertyTransformer() { + return this.transformer; + } + + @Override + public Map getVariables() { + + Map variables = new HashMap<>(); + + variables.put("backend", this.properties.getBackend()); + variables.put("key", String.format("creds/%s", this.properties.getRole())); + + return variables; + } + + } + } } diff --git a/spring-cloud-vault-config-aws/src/test/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsBootstrapConfigurationUnitTests.java b/spring-cloud-vault-config-aws/src/test/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsBootstrapConfigurationUnitTests.java new file mode 100644 index 00000000..e52c3803 --- /dev/null +++ b/spring-cloud-vault-config-aws/src/test/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsBootstrapConfigurationUnitTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.vault.config.aws; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.vault.config.SecretBackendMetadata; +import org.springframework.cloud.vault.config.SecretBackendMetadataFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link VaultConfigAwsBootstrapConfiguration}. + * + * @author Mark Paluch + */ +public class VaultConfigAwsBootstrapConfigurationUnitTests { + + @Test + void shouldCreateIamTokenSecretBackendMetadataFactory() { + + VaultAwsProperties properties = new VaultAwsProperties(); + properties.setRole("readonly"); + + SecretBackendMetadataFactory factory = new VaultConfigAwsBootstrapConfiguration() + .awsSecretBackendMetadataFactory(); + + SecretBackendMetadata metadata = factory.createMetadata(properties); + + assertThat(metadata.getPath()).isEqualTo("aws/creds/readonly"); + assertThat(metadata.getVariables()).containsEntry("backend", "aws").containsEntry("key", "creds/readonly"); + } + + @Test + void shouldCreateStsTokenSecretBackendMetadataFactory() { + + VaultAwsProperties properties = new VaultAwsProperties(); + properties.setCredentialType(AwsCredentialType.FEDERATION_TOKEN); + properties.setRole("readonly"); + + SecretBackendMetadataFactory factory = new VaultConfigAwsBootstrapConfiguration() + .awsSecretBackendMetadataFactory(); + + SecretBackendMetadata metadata = factory.createMetadata(properties); + + assertThat(metadata.getPath()).isEqualTo("aws/sts/readonly"); + assertThat(metadata.getVariables()).containsEntry("backend", "aws").containsEntry("key", "sts/readonly"); + } + + @Test + void shouldCreateStsTokenSecretBackendMetadataFactoryWithTtlAndRoleArn() { + + VaultAwsProperties properties = new VaultAwsProperties(); + properties.setCredentialType(AwsCredentialType.ASSUMED_ROLE); + properties.setRoleArn("1:2:3"); + properties.setTtl(Duration.ofMinutes(1)); + properties.setRole("readonly"); + + SecretBackendMetadataFactory factory = new VaultConfigAwsBootstrapConfiguration() + .awsSecretBackendMetadataFactory(); + + SecretBackendMetadata metadata = factory.createMetadata(properties); + + assertThat(metadata.getPath()).isEqualTo("aws/sts/readonly?ttl=60000ms&role_arn=1:2:3"); + assertThat(metadata.getVariables()).containsEntry("backend", "aws").containsEntry("key", "sts/readonly"); + } + +}